Philipp Hörist pushed to branch soup3 at gajim / gajim
Commits:
-
747c5696
by Philipp Hörist at 2022-12-28T16:23:51+01:00
15 changed files:
- .gitlab-ci.yml
- README.md
- gajim/common/application.py
- gajim/common/client.py
- gajim/common/filetransfer.py
- gajim/common/helpers.py
- gajim/common/modules/httpupload.py
- gajim/common/preview.py
- + gajim/common/util/http.py
- gajim/gajim.py
- gajim/gtk/conversation/rows/file_transfer.py
- gajim/gtk/exception.py
- gajim/plugins/repository.py
- win/_base.sh
- win/dev_env.sh
Changes:
| ... | ... | @@ -26,7 +26,7 @@ test-pyright: |
| 26 | 26 | - "**/*.py"
|
| 27 | 27 | script:
|
| 28 | 28 | - pip3 install git+https://dev.gajim.org/gajim/python-nbxmpp.git
|
| 29 | - - PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3,Soup2 pip3 install git+https://github.com/pygobject/pygobject-stubs.git
|
|
| 29 | + - PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3 pip3 install git+https://github.com/pygobject/pygobject-stubs.git
|
|
| 30 | 30 | - npm install pyright
|
| 31 | 31 | - python3 .ci/link-gtk.py
|
| 32 | 32 | - $(npm bin)/pyright --version
|
| ... | ... | @@ -18,7 +18,6 @@ ### Runtime Requirements |
| 18 | 18 | - [GLib](https://gitlab.com/gnome/glib) (>=2.60.0)
|
| 19 | 19 | - [GtkSourceView](https://gitlab.gnome.org/GNOME/gtksourceview)
|
| 20 | 20 | - [Pango](https://gitlab.gnome.org/GNOME/pango) (>=1.50.0)
|
| 21 | -- [libsoup](https://gitlab.gnome.org/GNOME/libsoup/)
|
|
| 22 | 21 | - [sqlite](https://www.sqlite.org/) (>=3.33.0)
|
| 23 | 22 | |
| 24 | 23 | ### Optional Runtime Requirements
|
| ... | ... | @@ -29,9 +29,9 @@ |
| 29 | 29 | |
| 30 | 30 | from gi.repository import Gio
|
| 31 | 31 | from gi.repository import GLib
|
| 32 | -from gi.repository import Soup
|
|
| 33 | 32 | from nbxmpp.const import ConnectionType
|
| 34 | 33 | from nbxmpp.const import ConnectionProtocol
|
| 34 | +from nbxmpp.http import HTTPRequest
|
|
| 35 | 35 | |
| 36 | 36 | from gajim.common import app
|
| 37 | 37 | from gajim.common import ged
|
| ... | ... | @@ -48,7 +48,6 @@ |
| 48 | 48 | from gajim.common.events import GajimUpdateAvailable
|
| 49 | 49 | from gajim.common.events import SignedIn
|
| 50 | 50 | from gajim.common.client import Client
|
| 51 | -from gajim.common.helpers import make_http_request
|
|
| 52 | 51 | from gajim.common.helpers import get_random_string
|
| 53 | 52 | from gajim.common.helpers import get_global_show
|
| 54 | 53 | from gajim.common.helpers import from_one_line
|
| ... | ... | @@ -60,6 +59,7 @@ |
| 60 | 59 | from gajim.common.storage.cache import CacheStorage
|
| 61 | 60 | from gajim.common.storage.draft import DraftStorage
|
| 62 | 61 | from gajim.common.storage.archive import MessageArchiveStorage
|
| 62 | +from gajim.common.util.http import create_http_request
|
|
| 63 | 63 | |
| 64 | 64 | from gajim.plugins.pluginmanager import PluginManager
|
| 65 | 65 | from gajim.plugins.repository import PluginRepository
|
| ... | ... | @@ -310,22 +310,21 @@ def _check_for_updates(self) -> None: |
| 310 | 310 | |
| 311 | 311 | def check_for_gajim_updates(self) -> None:
|
| 312 | 312 | self._log.info('Checking for Gajim updates')
|
| 313 | - make_http_request('https://gajim.org/current-version.json',
|
|
| 314 | - self._on_update_response)
|
|
| 313 | + request = create_http_request()
|
|
| 314 | + request.send('GET', 'https://gajim.org/current-version.json',
|
|
| 315 | + callback=self._on_update_response)
|
|
| 315 | 316 | |
| 316 | - def _on_update_response(self,
|
|
| 317 | - _session: Soup.Session,
|
|
| 318 | - message: Soup.Message) -> None:
|
|
| 317 | + def _on_update_response(self, request: HTTPRequest) -> None:
|
|
| 318 | + |
|
| 319 | + if not request.is_complete():
|
|
| 320 | + self._log.warning('Error while performing gajim update check: %s',
|
|
| 321 | + request.get_error(), request.get_status())
|
|
| 322 | + return
|
|
| 319 | 323 | |
| 320 | 324 | now = datetime.now()
|
| 321 | 325 | app.settings.set('last_update_check', now.strftime('%Y-%m-%d %H:%M'))
|
| 322 | 326 | |
| 323 | - response_body = message.props.response_body
|
|
| 324 | - if response_body is None or not response_body.data:
|
|
| 325 | - self._log.warning('Could not reach gajim.org for update check')
|
|
| 326 | - return
|
|
| 327 | - |
|
| 328 | - data = json.loads(response_body.data)
|
|
| 327 | + data = json.loads(request.get_data())
|
|
| 329 | 328 | latest_version = data['current_version']
|
| 330 | 329 | |
| 331 | 330 | if V(latest_version) > V(app.version):
|
| ... | ... | @@ -55,6 +55,7 @@ |
| 55 | 55 | from gajim.common.idle import IdleMonitorManager
|
| 56 | 56 | from gajim.common.i18n import _
|
| 57 | 57 | from gajim.common.structs import OutgoingMessage
|
| 58 | +from gajim.common.util.http import create_http_session
|
|
| 58 | 59 | |
| 59 | 60 | from gajim.gui.util import open_window
|
| 60 | 61 | |
| ... | ... | @@ -175,6 +176,8 @@ def _create_client(self) -> None: |
| 175 | 176 | |
| 176 | 177 | self._client.set_password(self.password)
|
| 177 | 178 | |
| 179 | + self._client.set_http_session(create_http_session())
|
|
| 180 | + |
|
| 178 | 181 | self._client.subscribe('resume-failed', self._on_resume_failed)
|
| 179 | 182 | self._client.subscribe('resume-successful', self._on_resume_successful)
|
| 180 | 183 | self._client.subscribe('disconnected', self._on_disconnected)
|
| ... | ... | @@ -27,8 +27,7 @@ def __init__(self, account: str) -> None: |
| 27 | 27 | |
| 28 | 28 | self._account = account
|
| 29 | 29 | |
| 30 | - self._seen: int = 0
|
|
| 31 | - self.size: int = 0
|
|
| 30 | + self._progress = 0
|
|
| 32 | 31 | |
| 33 | 32 | self._state = FTState.INIT
|
| 34 | 33 | self._error_text: str = ''
|
| ... | ... | @@ -42,16 +41,6 @@ def account(self) -> str: |
| 42 | 41 | def state(self) -> FTState:
|
| 43 | 42 | return self._state
|
| 44 | 43 | |
| 45 | - @property
|
|
| 46 | - def seen(self) -> int:
|
|
| 47 | - return self._seen
|
|
| 48 | - |
|
| 49 | - @property
|
|
| 50 | - def is_complete(self) -> bool:
|
|
| 51 | - if self.size == 0:
|
|
| 52 | - return False
|
|
| 53 | - return self._seen >= self.size
|
|
| 54 | - |
|
| 55 | 44 | @property
|
| 56 | 45 | def filename(self) -> str:
|
| 57 | 46 | raise NotImplementedError
|
| ... | ... | @@ -64,6 +53,13 @@ def error_text(self) -> str: |
| 64 | 53 | def error_domain(self) -> Optional[str]:
|
| 65 | 54 | return self._error_domain
|
| 66 | 55 | |
| 56 | + def get_progress(self) -> float:
|
|
| 57 | + return self._progress
|
|
| 58 | + |
|
| 59 | + def set_progress(self, progress: float) -> None:
|
|
| 60 | + self._progress = progress
|
|
| 61 | + self.update_progress()
|
|
| 62 | + |
|
| 67 | 63 | def get_state_description(self) -> str:
|
| 68 | 64 | if self._state is None:
|
| 69 | 65 | return ''
|
| ... | ... | @@ -81,7 +81,6 @@ |
| 81 | 81 | from gi.repository import Gio
|
| 82 | 82 | from gi.repository import GLib
|
| 83 | 83 | from gi.repository import GObject
|
| 84 | -from gi.repository import Soup
|
|
| 85 | 84 | |
| 86 | 85 | import precis_i18n.codec # noqa: F401
|
| 87 | 86 | |
| ... | ... | @@ -1546,24 +1545,3 @@ def make_path_from_jid(base_path: Path, jid: JID) -> Path: |
| 1546 | 1545 | if jid.resource is not None:
|
| 1547 | 1546 | return path / jid.resource[:30]
|
| 1548 | 1547 | return path |
| 1549 | - |
|
| 1550 | - |
|
| 1551 | -def make_http_request(uri: str,
|
|
| 1552 | - callback: Any,
|
|
| 1553 | - user_data: Optional[Any] = None) -> None:
|
|
| 1554 | - |
|
| 1555 | - proxy = determine_proxy()
|
|
| 1556 | - if proxy is None:
|
|
| 1557 | - resolver = None
|
|
| 1558 | - |
|
| 1559 | - else:
|
|
| 1560 | - resolver = proxy.get_resolver()
|
|
| 1561 | - |
|
| 1562 | - session = Soup.Session()
|
|
| 1563 | - session.props.proxy_resolver = resolver
|
|
| 1564 | - session.props.user_agent = f'Gajim {app.version}'
|
|
| 1565 | - message = Soup.Message.new('GET', uri)
|
|
| 1566 | - if user_data is None:
|
|
| 1567 | - session.queue_message(message, callback)
|
|
| 1568 | - else:
|
|
| 1569 | - session.queue_message(message, callback, user_data) |
| ... | ... | @@ -20,12 +20,13 @@ |
| 20 | 20 | from typing import Callable
|
| 21 | 21 | from typing import Optional
|
| 22 | 22 | |
| 23 | -import os
|
|
| 24 | -import io
|
|
| 25 | -from urllib.parse import urlparse
|
|
| 26 | -import mimetypes
|
|
| 27 | 23 | from collections import defaultdict
|
| 24 | +import functools
|
|
| 28 | 25 | from pathlib import Path
|
| 26 | +from urllib.parse import urlparse
|
|
| 27 | +import mimetypes
|
|
| 28 | +import os
|
|
| 29 | +import tempfile
|
|
| 29 | 30 | |
| 30 | 31 | from nbxmpp.errors import StanzaError
|
| 31 | 32 | from nbxmpp.errors import MalformedStanzaError
|
| ... | ... | @@ -36,23 +37,25 @@ |
| 36 | 37 | from nbxmpp.structs import HTTPUploadData
|
| 37 | 38 | from nbxmpp.task import Task
|
| 38 | 39 | from nbxmpp.util import convert_tls_error_flags
|
| 40 | +from nbxmpp.http import HTTPRequest
|
|
| 41 | +from nbxmpp.const import HTTPRequestError
|
|
| 39 | 42 | |
| 40 | 43 | from gi.repository import Gio
|
| 41 | 44 | from gi.repository import GLib
|
| 42 | -from gi.repository import Soup
|
|
| 43 | 45 | |
| 44 | 46 | from gajim.common import app
|
| 45 | 47 | from gajim.common import types
|
| 46 | 48 | from gajim.common.events import HTTPUploadError
|
| 47 | 49 | from gajim.common.events import HTTPUploadStarted
|
| 48 | 50 | from gajim.common.i18n import _
|
| 51 | +from gajim.common.helpers import get_random_string
|
|
| 49 | 52 | from gajim.common.helpers import get_tls_error_phrases
|
| 50 | -from gajim.common.helpers import get_account_proxy
|
|
| 51 | 53 | from gajim.common.const import FTState
|
| 52 | 54 | from gajim.common.filetransfer import FileTransfer
|
| 53 | 55 | from gajim.common.modules.base import BaseModule
|
| 54 | 56 | from gajim.common.exceptions import FileError
|
| 55 | 57 | from gajim.common.structs import OutgoingMessage
|
| 58 | +from gajim.common.util.http import create_http_request
|
|
| 56 | 59 | |
| 57 | 60 | |
| 58 | 61 | class HTTPUpload(BaseModule):
|
| ... | ... | @@ -67,24 +70,11 @@ def __init__(self, con: types.Client) -> None: |
| 67 | 70 | self.httpupload_namespace: Optional[str] = None
|
| 68 | 71 | self.max_file_size: Optional[float] = None # max file size in bytes
|
| 69 | 72 | |
| 70 | - self._proxy_resolver: Optional[Gio.SimpleProxyResolver] = None
|
|
| 71 | - self._queued_messages: dict[int, Soup.Message] = {}
|
|
| 72 | - self._session = Soup.Session()
|
|
| 73 | - self._session.props.ssl_strict = False
|
|
| 74 | - self._session.props.user_agent = f'Gajim {app.version}'
|
|
| 73 | + self._requests_in_progress: dict[int, HTTPRequest] = {}
|
|
| 75 | 74 | |
| 76 | 75 | self._running_transfers: dict[
|
| 77 | 76 | tuple[str, JID], set[HTTPFileTransfer]] = defaultdict(set)
|
| 78 | 77 | |
| 79 | - def _set_proxy_if_available(self) -> None:
|
|
| 80 | - proxy = get_account_proxy(self._account)
|
|
| 81 | - if proxy is None:
|
|
| 82 | - self._proxy_resolver = None
|
|
| 83 | - self._session.props.proxy_resolver = None
|
|
| 84 | - else:
|
|
| 85 | - self._proxy_resolver = proxy.get_resolver()
|
|
| 86 | - self._session.props.proxy_resolver = self._proxy_resolver
|
|
| 87 | - |
|
| 88 | 78 | def pass_disco(self, info: DiscoInfo) -> None:
|
| 89 | 79 | if not info.has_httpupload:
|
| 90 | 80 | return
|
| ... | ... | @@ -215,11 +205,11 @@ def _on_cancel_upload(self, |
| 215 | 205 | key = (transfer.account, transfer.contact.jid)
|
| 216 | 206 | self._running_transfers[key].discard(transfer)
|
| 217 | 207 | |
| 218 | - message = self._queued_messages.get(id(transfer))
|
|
| 219 | - if message is None:
|
|
| 208 | + request = self._requests_in_progress.get(id(transfer))
|
|
| 209 | + if request is None:
|
|
| 220 | 210 | return
|
| 221 | 211 | |
| 222 | - self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 212 | + request.cancel()
|
|
| 223 | 213 | |
| 224 | 214 | def _start_transfer(self, transfer: HTTPFileTransfer) -> None:
|
| 225 | 215 | if transfer.encryption is not None and not transfer.is_encrypted:
|
| ... | ... | @@ -283,106 +273,61 @@ def _upload_file(self, transfer: HTTPFileTransfer) -> None: |
| 283 | 273 | transfer.set_started()
|
| 284 | 274 | |
| 285 | 275 | assert transfer.put_uri is not None
|
| 286 | - message = Soup.Message.new('PUT', transfer.put_uri)
|
|
| 287 | - message.connect('starting', self._check_certificate, transfer)
|
|
| 288 | - |
|
| 289 | - # Set CAN_REBUILD so chunks get discarded after they have been
|
|
| 290 | - # written to the network
|
|
| 291 | - message.set_flags(Soup.MessageFlags.CAN_REBUILD |
|
|
| 292 | - Soup.MessageFlags.NO_REDIRECT)
|
|
| 276 | + request = create_http_request(self._account)
|
|
| 277 | + request.set_user_data(transfer)
|
|
| 278 | + request.connect('accept-certificate', self._accept_certificate)
|
|
| 279 | + request.connect('request-progress', self._on_request_progress)
|
|
| 293 | 280 | |
| 294 | - assert message.props.request_body is not None
|
|
| 295 | - message.props.request_body.set_accumulate(False)
|
|
| 281 | + request.set_request_body_from_path(transfer.mime, transfer.payload_path)
|
|
| 296 | 282 | |
| 297 | - assert message.props.request_headers is not None
|
|
| 298 | - message.props.request_headers.set_content_type(transfer.mime, None)
|
|
| 299 | - message.props.request_headers.set_content_length(transfer.size)
|
|
| 300 | 283 | for name, value in transfer.headers.items():
|
| 301 | - message.props.request_headers.append(name, value)
|
|
| 302 | - |
|
| 303 | - message.connect('wrote-headers', self._on_wrote_headers, transfer)
|
|
| 304 | - message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
|
|
| 305 | - |
|
| 306 | - self._queued_messages[id(transfer)] = message
|
|
| 307 | - self._set_proxy_if_available()
|
|
| 308 | - self._session.queue_message(message, self._on_finish, transfer)
|
|
| 309 | - |
|
| 310 | - def _check_certificate(self,
|
|
| 311 | - message: Soup.Message,
|
|
| 312 | - transfer: HTTPFileTransfer
|
|
| 313 | - ) -> None:
|
|
| 314 | - https_used, tls_certificate, tls_errors = message.get_https_status()
|
|
| 315 | - if not https_used:
|
|
| 316 | - self._log.warning('HTTPS was not used for upload')
|
|
| 317 | - transfer.set_error('unsecure')
|
|
| 318 | - self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 319 | - return
|
|
| 284 | + request.get_request_headers().append(name, value)
|
|
| 320 | 285 | |
| 321 | - tls_error_set = convert_tls_error_flags(tls_errors)
|
|
| 322 | - if app.cert_store.verify(tls_certificate, tls_error_set):
|
|
| 323 | - return
|
|
| 286 | + request.send('PUT', transfer.put_uri, callback=self._on_finish)
|
|
| 287 | + |
|
| 288 | + self._requests_in_progress[id(transfer)] = request
|
|
| 289 | + |
|
| 290 | + def _accept_certificate(self,
|
|
| 291 | + request: HTTPRequest,
|
|
| 292 | + _certificate: Gio.TlsCertificate,
|
|
| 293 | + certificate_errors: Gio.TlsCertificateFlags,
|
|
| 294 | + ) -> bool:
|
|
| 324 | 295 | |
| 325 | - phrases = get_tls_error_phrases(tls_error_set)
|
|
| 296 | + transfer = request.get_user_data()
|
|
| 297 | + phrases = get_tls_error_phrases(
|
|
| 298 | + convert_tls_error_flags(certificate_errors))
|
|
| 326 | 299 | self._log.warning(
|
| 327 | - 'TLS verification failed: %s (0x%02x)', phrases, tls_errors)
|
|
| 300 | + 'TLS verification failed: %s (0x%02x)', phrases, certificate_errors)
|
|
| 301 | + |
|
| 328 | 302 | transfer.set_error('tls-verification-failed', phrases[0])
|
| 329 | - self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 303 | + return False
|
|
| 330 | 304 | |
| 331 | - def _on_finish(self,
|
|
| 332 | - _session: Soup.Session,
|
|
| 333 | - message: Soup.Message,
|
|
| 334 | - transfer: HTTPFileTransfer
|
|
| 335 | - ) -> None:
|
|
| 305 | + def _on_finish(self, request: HTTPRequest) -> None:
|
|
| 306 | + transfer = request.get_user_data()
|
|
| 336 | 307 | |
| 337 | - self._queued_messages.pop(id(transfer), None)
|
|
| 308 | + self._requests_in_progress.pop(id(transfer), None)
|
|
| 338 | 309 | |
| 339 | 310 | key = (transfer.account, transfer.contact.jid)
|
| 340 | 311 | self._running_transfers[key].discard(transfer)
|
| 341 | 312 | |
| 342 | - if message.props.status_code == Soup.Status.CANCELLED:
|
|
| 343 | - self._log.info('Upload cancelled')
|
|
| 344 | - return
|
|
| 345 | - |
|
| 346 | - if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
|
|
| 347 | - self._log.info('Upload completed successfully')
|
|
| 348 | - transfer.set_finished()
|
|
| 349 | - |
|
| 350 | - else:
|
|
| 351 | - phrase = Soup.Status.get_phrase(message.props.status_code)
|
|
| 352 | - self._log.error('Got unexpected http upload response code: %s',
|
|
| 353 | - phrase)
|
|
| 354 | - transfer.set_error('http-response', phrase)
|
|
| 355 | - |
|
| 356 | - def _on_wrote_chunk(self,
|
|
| 357 | - message: Soup.Message,
|
|
| 358 | - transfer: HTTPFileTransfer
|
|
| 359 | - ) -> None:
|
|
| 360 | - transfer.update_progress()
|
|
| 361 | - if transfer.is_complete:
|
|
| 362 | - assert message.props.request_body is not None
|
|
| 363 | - message.props.request_body.complete()
|
|
| 313 | + if not request.is_complete():
|
|
| 314 | + error = request.get_error_string()
|
|
| 315 | + if request.get_error() == HTTPRequestError.CANCELLED:
|
|
| 316 | + self._log.info('Upload cancelled')
|
|
| 317 | + else:
|
|
| 318 | + transfer.set_error('http-response', error)
|
|
| 364 | 319 | return
|
| 365 | 320 | |
| 366 | - bytes_ = transfer.get_chunk()
|
|
| 367 | - assert bytes_ is not None
|
|
| 368 | - self._session.pause_message(message)
|
|
| 369 | - GLib.idle_add(self._append, message, bytes_)
|
|
| 321 | + transfer.set_finished()
|
|
| 322 | + self._log.info('Upload completed successfully')
|
|
| 370 | 323 | |
| 371 | - def _append(self, message: Soup.Message, bytes_: bytes) -> None:
|
|
| 372 | - if message.props.status_code == Soup.Status.CANCELLED:
|
|
| 373 | - return
|
|
| 374 | - self._session.unpause_message(message)
|
|
| 375 | - assert message.props.request_body is not None
|
|
| 376 | - message.props.request_body.append(bytes_)
|
|
| 324 | + def _on_request_progress(self,
|
|
| 325 | + request: HTTPRequest,
|
|
| 326 | + progress: float
|
|
| 327 | + ) -> None:
|
|
| 377 | 328 | |
| 378 | - @staticmethod
|
|
| 379 | - def _on_wrote_headers(message: Soup.Message,
|
|
| 380 | - transfer: HTTPFileTransfer
|
|
| 381 | - ) -> None:
|
|
| 382 | - bytes_ = transfer.get_chunk()
|
|
| 383 | - assert bytes_ is not None
|
|
| 384 | - assert message.props.request_body is not None
|
|
| 385 | - message.props.request_body.append(bytes_)
|
|
| 329 | + transfer = request.get_user_data()
|
|
| 330 | + transfer.set_progress(progress)
|
|
| 386 | 331 | |
| 387 | 332 | |
| 388 | 333 | class HTTPFileTransfer(FileTransfer):
|
| ... | ... | @@ -410,23 +355,32 @@ def __init__(self, |
| 410 | 355 | |
| 411 | 356 | FileTransfer.__init__(self, account)
|
| 412 | 357 | |
| 413 | - self._path = path
|
|
| 358 | + self._path = Path(path)
|
|
| 414 | 359 | self._encryption = encryption
|
| 415 | 360 | self._groupchat = groupchat
|
| 416 | 361 | self._contact = contact
|
| 417 | 362 | self._mime = mime
|
| 418 | 363 | |
| 419 | - self.size = os.stat(path).st_size
|
|
| 420 | 364 | self.put_uri: Optional[str] = None
|
| 421 | 365 | self.get_uri: Optional[str] = None
|
| 422 | 366 | self._uri_transform_func: Optional[Callable[[str], str]] = None
|
| 423 | 367 | |
| 424 | - self._stream = None
|
|
| 425 | 368 | self._data: Optional[bytes] = None
|
| 426 | 369 | self._headers: dict[str, str] = {}
|
| 427 | 370 | |
| 428 | 371 | self._is_encrypted = False
|
| 429 | 372 | |
| 373 | + @property
|
|
| 374 | + def size(self) -> int:
|
|
| 375 | + if self._encryption is not None and not self._is_encrypted:
|
|
| 376 | + raise ValueError('File size unknown at this point')
|
|
| 377 | + return self.payload_path.stat().st_size
|
|
| 378 | + |
|
| 379 | + @size.setter
|
|
| 380 | + def size(self, size: int) -> None:
|
|
| 381 | + # Backwards compatibility with plugins
|
|
| 382 | + pass
|
|
| 383 | + |
|
| 430 | 384 | @property
|
| 431 | 385 | def mime(self) -> str:
|
| 432 | 386 | return self._mime
|
| ... | ... | @@ -448,7 +402,13 @@ def headers(self) -> dict[str, str]: |
| 448 | 402 | return self._headers
|
| 449 | 403 | |
| 450 | 404 | @property
|
| 451 | - def path(self) -> str:
|
|
| 405 | + def path(self) -> Path:
|
|
| 406 | + return self._path
|
|
| 407 | + |
|
| 408 | + @property
|
|
| 409 | + def payload_path(self) -> Path:
|
|
| 410 | + if self._encryption is not None:
|
|
| 411 | + return self.get_temp_path()
|
|
| 452 | 412 | return self._path
|
| 453 | 413 | |
| 454 | 414 | @property
|
| ... | ... | @@ -465,47 +425,30 @@ def set_uri_transform_func(self, func: Callable[[str], str]) -> None: |
| 465 | 425 | |
| 466 | 426 | @property
|
| 467 | 427 | def filename(self) -> str:
|
| 468 | - return os.path.basename(self._path)
|
|
| 428 | + return self._path.name
|
|
| 429 | + |
|
| 430 | + @staticmethod
|
|
| 431 | + @functools.cache
|
|
| 432 | + def get_temp_path() -> Path:
|
|
| 433 | + tempdir = tempfile.gettempdir()
|
|
| 434 | + return Path(tempdir) / get_random_string(16)
|
|
| 469 | 435 | |
| 470 | 436 | def set_error(self, domain: str, text: str = '') -> None:
|
| 471 | 437 | if not text:
|
| 472 | 438 | text = self._errors[domain]
|
| 473 | 439 | |
| 474 | - self._close()
|
|
| 475 | 440 | super().set_error(domain, text)
|
| 476 | 441 | |
| 477 | 442 | def set_finished(self) -> None:
|
| 478 | - self._close()
|
|
| 479 | 443 | super().set_finished()
|
| 480 | 444 | |
| 481 | 445 | def set_encrypted_data(self, data: bytes) -> None:
|
| 482 | - self._data = data
|
|
| 446 | + temp_path = self.get_temp_path()
|
|
| 447 | + temp_path.write_bytes(data)
|
|
| 483 | 448 | self._is_encrypted = True
|
| 484 | 449 | |
| 485 | - def _close(self) -> None:
|
|
| 486 | - if self._stream is not None:
|
|
| 487 | - self._stream.close()
|
|
| 488 | - |
|
| 489 | - def get_chunk(self) -> Optional[bytes]:
|
|
| 490 | - if self._stream is None:
|
|
| 491 | - if self._encryption is None:
|
|
| 492 | - self._stream = open(self._path, 'rb') # pylint: disable=consider-using-with # noqa: E501
|
|
| 493 | - else:
|
|
| 494 | - self._stream = io.BytesIO(self._data)
|
|
| 495 | - |
|
| 496 | - data = self._stream.read(16384)
|
|
| 497 | - if not data:
|
|
| 498 | - self._close()
|
|
| 499 | - return None
|
|
| 500 | - self._seen += len(data)
|
|
| 501 | - if self.is_complete:
|
|
| 502 | - self._close()
|
|
| 503 | - return data
|
|
| 504 | - |
|
| 505 | 450 | def get_data(self) -> bytes:
|
| 506 | - with open(self._path, 'rb') as file:
|
|
| 507 | - data = file.read()
|
|
| 508 | - return data
|
|
| 451 | + return self._path.read_bytes()
|
|
| 509 | 452 | |
| 510 | 453 | def process_result(self, result: HTTPUploadData) -> None:
|
| 511 | 454 | self.put_uri = result.put_uri
|
| ... | ... | @@ -14,12 +14,14 @@ |
| 14 | 14 | |
| 15 | 15 | from __future__ import annotations
|
| 16 | 16 | |
| 17 | -from dataclasses import dataclass, field
|
|
| 18 | 17 | from typing import Any
|
| 18 | +from typing import cast
|
|
| 19 | 19 | from typing import Callable
|
| 20 | 20 | from typing import Optional
|
| 21 | 21 | |
| 22 | 22 | import logging
|
| 23 | +from dataclasses import dataclass
|
|
| 24 | +from dataclasses import field
|
|
| 23 | 25 | import os
|
| 24 | 26 | import re
|
| 25 | 27 | from pathlib import Path
|
| ... | ... | @@ -29,7 +31,8 @@ |
| 29 | 31 | |
| 30 | 32 | from gi.repository import Gio
|
| 31 | 33 | from gi.repository import GLib
|
| 32 | -from gi.repository import Soup
|
|
| 34 | +from nbxmpp.const import HTTPRequestError
|
|
| 35 | +from nbxmpp.http import HTTPRequest
|
|
| 33 | 36 | from nbxmpp.util import convert_tls_error_flags
|
| 34 | 37 | |
| 35 | 38 | from gajim.common import app
|
| ... | ... | @@ -40,7 +43,6 @@ |
| 40 | 43 | from gajim.common.helpers import load_file_async
|
| 41 | 44 | from gajim.common.helpers import write_file_async
|
| 42 | 45 | from gajim.common.helpers import get_tls_error_phrases
|
| 43 | -from gajim.common.helpers import get_account_proxy
|
|
| 44 | 46 | from gajim.common.i18n import _
|
| 45 | 47 | from gajim.common.preview_helpers import aes_decrypt
|
| 46 | 48 | from gajim.common.preview_helpers import filename_from_uri
|
| ... | ... | @@ -52,6 +54,7 @@ |
| 52 | 54 | from gajim.common.preview_helpers import guess_mime_type
|
| 53 | 55 | from gajim.common.preview_helpers import pixbuf_from_data
|
| 54 | 56 | from gajim.common.types import GdkPixbufType
|
| 57 | +from gajim.common.util.http import create_http_request
|
|
| 55 | 58 | |
| 56 | 59 | log = logging.getLogger('gajim.c.preview')
|
| 57 | 60 | |
| ... | ... | @@ -104,12 +107,11 @@ def __init__(self, |
| 104 | 107 | self.thumbnail: Optional[bytes] = None
|
| 105 | 108 | self.mime_type: str = ''
|
| 106 | 109 | self.file_size: int = 0
|
| 107 | - self._received_size: int = 0
|
|
| 108 | 110 | self.download_in_progress = False
|
| 109 | 111 | |
| 110 | 112 | self.info_message: Optional[str] = None
|
| 111 | 113 | |
| 112 | - self._soup_message: Optional[Soup.Message] = None
|
|
| 114 | + self._request = None
|
|
| 113 | 115 | |
| 114 | 116 | self.key: Optional[bytes] = None
|
| 115 | 117 | self.iv: Optional[bytes] = None
|
| ... | ... | @@ -154,8 +156,8 @@ def filename(self) -> str: |
| 154 | 156 | return self._filename
|
| 155 | 157 | |
| 156 | 158 | @property
|
| 157 | - def soup_message(self) -> Soup.Message:
|
|
| 158 | - return self._soup_message
|
|
| 159 | + def request(self) -> HTTPRequest:
|
|
| 160 | + return self._request
|
|
| 159 | 161 | |
| 160 | 162 | @property
|
| 161 | 163 | def request_uri(self) -> Optional[str]:
|
| ... | ... | @@ -193,29 +195,17 @@ def create_thumbnail(self, data: bytes) -> bool: |
| 193 | 195 | return False
|
| 194 | 196 | return True
|
| 195 | 197 | |
| 196 | - def reset_received_size(self) -> None:
|
|
| 197 | - self._received_size = 0
|
|
| 198 | - |
|
| 199 | 198 | def update_widget(self, data: Optional[GdkPixbufType] = None) -> None:
|
| 200 | 199 | self._widget.update(self, data)
|
| 201 | 200 | |
| 202 | - def update_progress(self, size: int, message: Soup.Message) -> None:
|
|
| 201 | + def update_progress(self, progress: float, request: HTTPRequest) -> None:
|
|
| 203 | 202 | self.download_in_progress = True
|
| 204 | - self._soup_message = message
|
|
| 205 | - self._received_size += size
|
|
| 206 | - if self.file_size == 0 or self._received_size == 0:
|
|
| 207 | - return
|
|
| 208 | - |
|
| 209 | - progress = self._received_size / self.file_size
|
|
| 203 | + self._request = request
|
|
| 210 | 204 | self._widget.update_progress(self, progress)
|
| 211 | 205 | |
| 212 | 206 | |
| 213 | 207 | class PreviewManager:
|
| 214 | 208 | def __init__(self) -> None:
|
| 215 | - self._sessions: dict[
|
|
| 216 | - str,
|
|
| 217 | - tuple[Soup.Session, Optional[Gio.SimpleProxyResolver]]] = {}
|
|
| 218 | - |
|
| 219 | 209 | self._orig_dir = Path(configpaths.get('MY_DATA')) / 'downloads'
|
| 220 | 210 | self._thumb_dir = Path(configpaths.get('MY_CACHE')) / 'downloads.thumb'
|
| 221 | 211 | |
| ... | ... | @@ -268,29 +258,6 @@ def stop_audio_except(self, preview_id: int) -> None: |
| 268 | 258 | if id_ != preview_id:
|
| 269 | 259 | stop_func()
|
| 270 | 260 | |
| 271 | - def _get_session(self, account: str) -> Soup.Session:
|
|
| 272 | - if account not in self._sessions:
|
|
| 273 | - self._sessions[account] = self._create_session(account)
|
|
| 274 | - return self._sessions[account][0]
|
|
| 275 | - |
|
| 276 | - @staticmethod
|
|
| 277 | - def _create_session(account: str) -> tuple[
|
|
| 278 | - Soup.Session, Optional[Gio.SimpleProxyResolver]]:
|
|
| 279 | - session = Soup.Session()
|
|
| 280 | - session.add_feature_by_type(Soup.ContentSniffer)
|
|
| 281 | - session.props.https_aliases = ['aesgcm']
|
|
| 282 | - session.props.ssl_strict = False
|
|
| 283 | - session.props.user_agent = f'Gajim {app.version}'
|
|
| 284 | - |
|
| 285 | - proxy = get_account_proxy(account)
|
|
| 286 | - if proxy is None:
|
|
| 287 | - resolver = None
|
|
| 288 | - else:
|
|
| 289 | - resolver = proxy.get_resolver()
|
|
| 290 | - |
|
| 291 | - session.props.proxy_resolver = resolver
|
|
| 292 | - return session, resolver
|
|
| 293 | - |
|
| 294 | 261 | @staticmethod
|
| 295 | 262 | def _accept_uri(urlparts: ParseResult,
|
| 296 | 263 | uri: str,
|
| ... | ... | @@ -476,58 +443,62 @@ def download_content(self, |
| 476 | 443 | log.info('Download already in progress')
|
| 477 | 444 | return
|
| 478 | 445 | |
| 479 | - preview.reset_received_size()
|
|
| 480 | 446 | if preview.account is None:
|
| 481 | 447 | # History Window can be opened without account context
|
| 482 | 448 | # This means we can not apply proxy settings
|
| 483 | 449 | return
|
| 484 | 450 | log.info('Start downloading: %s', preview.request_uri)
|
| 485 | - message = Soup.Message.new('GET', preview.request_uri)
|
|
| 486 | - message.connect('starting', self._check_certificate, preview)
|
|
| 487 | - message.connect(
|
|
| 488 | - 'content-sniffed', self._on_content_sniffed, preview, force)
|
|
| 489 | - message.connect('got-chunk', self._on_got_chunk, preview)
|
|
| 490 | 451 | |
| 491 | - session = self._get_session(preview.account)
|
|
| 492 | - session.queue_message(message, self._on_finished, preview)
|
|
| 452 | + request = create_http_request(preview.account, sniffer=True)
|
|
| 453 | + request.set_user_data(preview)
|
|
| 454 | + request.connect('accept-certificate', self._accept_certificate)
|
|
| 455 | + request.connect('content-sniffed', self._on_content_sniffed, force)
|
|
| 456 | + request.connect('response-progress', self._on_response_progress)
|
|
| 493 | 457 | |
| 494 | - def _check_certificate(self,
|
|
| 495 | - message: Soup.Message,
|
|
| 496 | - preview: Preview) -> None:
|
|
| 497 | - _https_used, _tls_certificate, tls_errors = message.get_https_status()
|
|
| 458 | + request.send('GET', preview.request_uri, callback=self._on_finished)
|
|
| 459 | + |
|
| 460 | + def _accept_certificate(self,
|
|
| 461 | + request: HTTPRequest,
|
|
| 462 | + certificate: Gio.TlsCertificate,
|
|
| 463 | + certificate_errors: Gio.TlsCertificateFlags,
|
|
| 464 | + ) -> bool:
|
|
| 498 | 465 | |
| 499 | 466 | if not app.settings.get('preview_verify_https'):
|
| 500 | - return
|
|
| 467 | + return True
|
|
| 501 | 468 | |
| 502 | - if tls_errors:
|
|
| 503 | - phrases = get_tls_error_phrases(convert_tls_error_flags(tls_errors))
|
|
| 504 | - log.warning(
|
|
| 505 | - 'TLS verification failed: %s (0x%02x)', phrases, tls_errors)
|
|
| 506 | - session = self._get_session(preview.account)
|
|
| 507 | - session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 508 | - preview.info_message = _('TLS verification failed: %s') % phrases[0]
|
|
| 509 | - preview.update_widget()
|
|
| 469 | + phrases = get_tls_error_phrases(
|
|
| 470 | + convert_tls_error_flags(certificate_errors))
|
|
| 471 | + log.warning(
|
|
| 472 | + 'TLS verification failed: %s (0x%02x)', phrases, certificate_errors)
|
|
| 473 | + |
|
| 474 | + preview = cast(Preview, request.get_user_data())
|
|
| 475 | + preview.info_message = _('TLS verification failed: %s') % phrases[0]
|
|
| 476 | + preview.update_widget()
|
|
| 477 | + return False
|
|
| 510 | 478 | |
| 511 | 479 | def _on_content_sniffed(self,
|
| 512 | - message: Soup.Message,
|
|
| 513 | - type_: str,
|
|
| 480 | + request: HTTPRequest,
|
|
| 481 | + content_type: str,
|
|
| 514 | 482 | _params: GLib.HashTable,
|
| 515 | - preview: Preview,
|
|
| 516 | - force: bool) -> None:
|
|
| 517 | - file_size = message.props.response_headers.get_content_length()
|
|
| 518 | - uri = message.props.uri.to_string(False)
|
|
| 519 | - session = self._get_session(preview.account)
|
|
| 520 | - preview.mime_type = type_
|
|
| 483 | + force: bool
|
|
| 484 | + ) -> None:
|
|
| 485 | + |
|
| 486 | + file_size = request.get_response_headers().get_content_length()
|
|
| 487 | + uri = request.get_uri().to_string()
|
|
| 488 | + |
|
| 489 | + |
|
| 490 | + preview = cast(Preview, request.get_user_data())
|
|
| 491 | + preview.mime_type = content_type
|
|
| 521 | 492 | preview.file_size = file_size
|
| 522 | 493 | |
| 523 | - if type_ not in ALLOWED_MIME_TYPES and not force:
|
|
| 524 | - log.info('Not an allowed content type: %s, %s', type_, uri)
|
|
| 525 | - session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 494 | + if content_type not in ALLOWED_MIME_TYPES and not force:
|
|
| 495 | + log.info('Not an allowed content type: %s, %s', content_type, uri)
|
|
| 496 | + request.cancel()
|
|
| 526 | 497 | return
|
| 527 | 498 | |
| 528 | 499 | if file_size == 0:
|
| 529 | 500 | log.info('File size is unknown (zero) for URL: "%s"', uri)
|
| 530 | - session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 501 | + request.cancel()
|
|
| 531 | 502 | return
|
| 532 | 503 | |
| 533 | 504 | if file_size > int(app.settings.get('preview_max_file_size')):
|
| ... | ... | @@ -537,42 +508,37 @@ def _on_content_sniffed(self, |
| 537 | 508 | if force:
|
| 538 | 509 | preview.info_message = None
|
| 539 | 510 | else:
|
| 540 | - session.cancel_message(message, Soup.Status.CANCELLED)
|
|
| 511 | + request.cancel()
|
|
| 541 | 512 | preview.info_message = _('Automatic preview disabled '
|
| 542 | 513 | '(file too big)')
|
| 543 | 514 | |
| 544 | 515 | preview.update_widget()
|
| 545 | 516 | |
| 546 | - def _on_got_chunk(self,
|
|
| 547 | - message: Soup.Message,
|
|
| 548 | - chunk: Soup.Buffer,
|
|
| 549 | - preview: Preview
|
|
| 550 | - ) -> None:
|
|
| 551 | - preview.update_progress(len(chunk.get_data()), message)
|
|
| 517 | + def _on_response_progress(self,
|
|
| 518 | + request: HTTPRequest,
|
|
| 519 | + progress: float,
|
|
| 520 | + ) -> None:
|
|
| 552 | 521 | |
| 553 | - def _on_finished(self,
|
|
| 554 | - _session: Soup.Session,
|
|
| 555 | - message: Soup.Message,
|
|
| 556 | - preview: Preview
|
|
| 557 | - ) -> None:
|
|
| 522 | + preview = cast(Preview, request.get_user_data())
|
|
| 523 | + preview.update_progress(progress, request)
|
|
| 558 | 524 | |
| 525 | + def _on_finished(self, request: HTTPRequest) -> None:
|
|
| 526 | + |
|
| 527 | + preview = cast(Preview, request.get_user_data())
|
|
| 559 | 528 | preview.download_in_progress = False
|
| 560 | 529 | |
| 561 | - if message.status_code != Soup.Status.OK:
|
|
| 562 | - log.warning('Download failed: %s', preview.request_uri)
|
|
| 563 | - status_code = Soup.Status.get_phrase(message.status_code)
|
|
| 564 | - log.warning(status_code)
|
|
| 565 | - preview.reset_received_size()
|
|
| 566 | - if message.status_code != 1:
|
|
| 567 | - # status_code 1: 'Cancelled'
|
|
| 568 | - preview.info_message = _('Download failed (%s)') % status_code
|
|
| 530 | + if not request.is_complete():
|
|
| 531 | + error = request.get_error_string()
|
|
| 532 | + log.warning('Download failed: %s - %s', preview.request_uri, error)
|
|
| 533 | + if request.get_error() != HTTPRequestError.CANCELLED:
|
|
| 534 | + preview.info_message = _('Download failed (%s)') % error
|
|
| 569 | 535 | preview.update_widget()
|
| 570 | 536 | return
|
| 571 | 537 | |
| 572 | 538 | preview.info_message = None
|
| 573 | 539 | |
| 574 | - data = message.props.response_body_data.get_data()
|
|
| 575 | - if data is None:
|
|
| 540 | + data = request.get_data()
|
|
| 541 | + if not data:
|
|
| 576 | 542 | return
|
| 577 | 543 | |
| 578 | 544 | if preview.is_aes_encrypted:
|
| ... | ... | @@ -649,6 +615,4 @@ def _on_thumb_write_finished(_result: bool, |
| 649 | 615 | preview.update_widget(data=pixbuf)
|
| 650 | 616 | |
| 651 | 617 | def cancel_download(self, preview: Preview) -> None:
|
| 652 | - preview.reset_received_size()
|
|
| 653 | - session = self._get_session(preview.account)
|
|
| 654 | - session.cancel_message(preview.soup_message, Soup.Status.CANCELLED) |
|
| 618 | + preview.request.cancel() |
| 1 | +# This file is part of Gajim.
|
|
| 2 | +#
|
|
| 3 | +# Gajim is free software; you can redistribute it and/or modify
|
|
| 4 | +# it under the terms of the GNU General Public License as published
|
|
| 5 | +# by the Free Software Foundation; version 3 only.
|
|
| 6 | +#
|
|
| 7 | +# Gajim is distributed in the hope that it will be useful,
|
|
| 8 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 9 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 10 | +# GNU General Public License for more details.
|
|
| 11 | +#
|
|
| 12 | +# You should have received a copy of the GNU General Public License
|
|
| 13 | +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
| 14 | + |
|
| 15 | +from typing import Optional
|
|
| 16 | + |
|
| 17 | +from nbxmpp.http import HTTPRequest
|
|
| 18 | +from nbxmpp.http import HTTPSession
|
|
| 19 | + |
|
| 20 | +from gajim.common import app
|
|
| 21 | +from gajim.common.helpers import determine_proxy
|
|
| 22 | +from gajim.common.helpers import get_account_proxy
|
|
| 23 | + |
|
| 24 | + |
|
| 25 | +def create_http_session(account: Optional[str] = None,
|
|
| 26 | + sniffer: bool = False
|
|
| 27 | + ) -> HTTPSession:
|
|
| 28 | + |
|
| 29 | + session = HTTPSession(user_agent=f'Gajim {app.version}',
|
|
| 30 | + sniffer=sniffer)
|
|
| 31 | + |
|
| 32 | + if account is None:
|
|
| 33 | + proxy = determine_proxy()
|
|
| 34 | + else:
|
|
| 35 | + proxy = get_account_proxy(account)
|
|
| 36 | + |
|
| 37 | + if proxy is not None:
|
|
| 38 | + session.set_proxy_resolver(proxy.get_resolver())
|
|
| 39 | + |
|
| 40 | + return session
|
|
| 41 | + |
|
| 42 | + |
|
| 43 | +def create_http_request(account: Optional[str] = None,
|
|
| 44 | + sniffer: bool = False
|
|
| 45 | + ) -> HTTPRequest:
|
|
| 46 | + |
|
| 47 | + session = create_http_session(account, sniffer)
|
|
| 48 | + return session.create_request() |
| ... | ... | @@ -65,8 +65,7 @@ def _check_required_deps() -> None: |
| 65 | 65 | 'GtkSource': '4',
|
| 66 | 66 | 'GObject': '2.0',
|
| 67 | 67 | 'Pango': '1.0',
|
| 68 | - 'PangoCairo': '1.0',
|
|
| 69 | - 'Soup': '2.4'})
|
|
| 68 | + 'PangoCairo': '1.0'})
|
|
| 70 | 69 | except ValueError as error:
|
| 71 | 70 | sys.exit('Missing dependency: %s' % error)
|
| 72 | 71 |
| ... | ... | @@ -131,7 +131,10 @@ def _on_transfer_progress(self, |
| 131 | 131 | size_total = GLib.format_size_full(transfer.size, self._units)
|
| 132 | 132 | self._ui.file_size.set_text(size_total)
|
| 133 | 133 | |
| 134 | - bytes_sec = int(round(transfer.seen / (time_now - self._start_time), 1))
|
|
| 134 | + progress = transfer.get_progress()
|
|
| 135 | + seen = transfer.size * progress
|
|
| 136 | + |
|
| 137 | + bytes_sec = int(round(seen / (time_now - self._start_time), 1))
|
|
| 135 | 138 | speed = f'{GLib.format_size_full(bytes_sec, self._units)}/s'
|
| 136 | 139 | self._ui.transfer_progress.set_tooltip_text(_('Speed: %s') % speed)
|
| 137 | 140 | |
| ... | ... | @@ -139,9 +142,8 @@ def _on_transfer_progress(self, |
| 139 | 142 | eta = '∞'
|
| 140 | 143 | else:
|
| 141 | 144 | eta = format_eta(round(
|
| 142 | - (transfer.size - transfer.seen) / bytes_sec))
|
|
| 145 | + (transfer.size - seen) / bytes_sec))
|
|
| 143 | 146 | |
| 144 | - progress = float(transfer.seen) / transfer.size
|
|
| 145 | 147 | self._ui.transfer_progress.set_text(
|
| 146 | 148 | _('%(progress)s %% (%(time)s remaining)') % {
|
| 147 | 149 | 'progress': round(progress * 100),
|
| ... | ... | @@ -20,11 +20,11 @@ |
| 20 | 20 | from __future__ import annotations
|
| 21 | 21 | |
| 22 | 22 | from typing import Any
|
| 23 | -from types import TracebackType
|
|
| 24 | 23 | |
| 25 | 24 | import sys
|
| 26 | 25 | import json
|
| 27 | 26 | import traceback
|
| 27 | +from types import TracebackType
|
|
| 28 | 28 | import threading
|
| 29 | 29 | import webbrowser
|
| 30 | 30 | import pprint
|
| ... | ... | @@ -33,9 +33,9 @@ |
| 33 | 33 | |
| 34 | 34 | from gi.repository import Gdk
|
| 35 | 35 | from gi.repository import Gtk
|
| 36 | -from gi.repository import Soup
|
|
| 37 | 36 | |
| 38 | 37 | import nbxmpp
|
| 38 | +from nbxmpp.http import HTTPRequest
|
|
| 39 | 39 | |
| 40 | 40 | import gajim
|
| 41 | 41 | from gajim.common import app
|
| ... | ... | @@ -44,8 +44,8 @@ |
| 44 | 44 | from gajim.common.helpers import get_os_name
|
| 45 | 45 | from gajim.common.helpers import get_os_version
|
| 46 | 46 | from gajim.common.helpers import determine_proxy
|
| 47 | -from gajim.common.helpers import make_http_request
|
|
| 48 | 47 | from gajim.common.i18n import _
|
| 48 | +from gajim.common.util.http import create_http_request
|
|
| 49 | 49 | |
| 50 | 50 | from .builder import get_builder
|
| 51 | 51 | from .util import get_gtk_version
|
| ... | ... | @@ -182,24 +182,17 @@ def _request_sentry_endpoint(self) -> None: |
| 182 | 182 | self._ui.report_spinner.show()
|
| 183 | 183 | self._ui.report_spinner.start()
|
| 184 | 184 | |
| 185 | - make_http_request('https://gajim.org/updates.json',
|
|
| 186 | - self._on_endpoint_received)
|
|
| 185 | + request = create_http_request()
|
|
| 186 | + request.send('GET', 'https://gajim.org/updates.json',
|
|
| 187 | + callback=self._on_endpoint_received)
|
|
| 187 | 188 | |
| 188 | - def _parse_endpoint(self, message: Soup.Message) -> str:
|
|
| 189 | - status = message.props.status_code
|
|
| 190 | - response_body = message.props.response_body
|
|
| 191 | - |
|
| 192 | - if status != Soup.Status.OK:
|
|
| 193 | - error = message.props.reason_phrase
|
|
| 189 | + def _parse_endpoint(self, request: HTTPRequest) -> str:
|
|
| 190 | + if not request.is_complete():
|
|
| 194 | 191 | raise ValueError('Failed to retrieve sentry endpoint: %s %s' % (
|
| 195 | - status, error))
|
|
| 196 | - |
|
| 197 | - if response_body is None or not response_body.data:
|
|
| 198 | - raise ValueError('Failed to retrieve sentry endpoint, '
|
|
| 199 | - 'no response body')
|
|
| 192 | + request.get_status(), request.get_error()))
|
|
| 200 | 193 | |
| 201 | 194 | try:
|
| 202 | - data = json.loads(response_body.data)
|
|
| 195 | + data = json.loads(request.get_data())
|
|
| 203 | 196 | except Exception as error:
|
| 204 | 197 | raise ValueError('Json parsing error: %s' % error)
|
| 205 | 198 | |
| ... | ... | @@ -209,12 +202,9 @@ def _parse_endpoint(self, message: Soup.Message) -> str: |
| 209 | 202 | |
| 210 | 203 | return endpoint
|
| 211 | 204 | |
| 212 | - def _on_endpoint_received(self,
|
|
| 213 | - _session: Soup.Session,
|
|
| 214 | - message: Soup.Message
|
|
| 215 | - ) -> None:
|
|
| 205 | + def _on_endpoint_received(self, request: HTTPRequest) -> None:
|
|
| 216 | 206 | try:
|
| 217 | - endpoint = self._parse_endpoint(message)
|
|
| 207 | + endpoint = self._parse_endpoint(request)
|
|
| 218 | 208 | except ValueError as error:
|
| 219 | 209 | print(error)
|
| 220 | 210 | self._report_with_browser()
|
| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | # You should have received a copy of the GNU General Public License
|
| 13 | 13 | # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
| 14 | 14 | |
| 15 | -from typing import Any
|
|
| 15 | +from typing import Any, cast
|
|
| 16 | 16 | from typing import Iterable
|
| 17 | 17 | from typing import Optional
|
| 18 | 18 | |
| ... | ... | @@ -21,14 +21,14 @@ |
| 21 | 21 | from io import BytesIO
|
| 22 | 22 | from zipfile import ZipFile
|
| 23 | 23 | |
| 24 | -from gi.repository import Soup
|
|
| 25 | 24 | from gi.repository import GLib
|
| 25 | +from nbxmpp.http import HTTPRequest
|
|
| 26 | 26 | |
| 27 | 27 | from gajim.common import app
|
| 28 | 28 | from gajim.common import configpaths
|
| 29 | 29 | from gajim.common.helpers import Observable
|
| 30 | -from gajim.common.helpers import make_http_request
|
|
| 31 | 30 | from gajim.plugins.manifest import PluginManifest
|
| 31 | +from gajim.common.util.http import create_http_request
|
|
| 32 | 32 | |
| 33 | 33 | |
| 34 | 34 | log = logging.getLogger('gajim.p.repository')
|
| ... | ... | @@ -60,28 +60,27 @@ def __init__(self) -> None: |
| 60 | 60 | self._download_queue: set[PluginManifest] = set()
|
| 61 | 61 | |
| 62 | 62 | if app.settings.get('plugins_repository_enabled'):
|
| 63 | - make_http_request('https://gajim.org/updates.json',
|
|
| 64 | - self._on_repository_received)
|
|
| 63 | + request = create_http_request()
|
|
| 64 | + request.send('GET', 'https://gajim.org/updates.json',
|
|
| 65 | + callback=self._on_repository_received)
|
|
| 65 | 66 | |
| 66 | 67 | @property
|
| 67 | 68 | def available(self):
|
| 68 | 69 | return bool(self._repository_url)
|
| 69 | 70 | |
| 70 | - def _on_repository_received(self,
|
|
| 71 | - _session: Soup.Session,
|
|
| 72 | - message: Soup.Message) -> None:
|
|
| 71 | + def _on_repository_received(self, request: HTTPRequest) -> None:
|
|
| 73 | 72 | |
| 74 | - if message.status_code != Soup.Status.OK:
|
|
| 75 | - log.warning('Repository retrieval failed')
|
|
| 76 | - log.warning(Soup.Status.get_phrase(message.status_code))
|
|
| 73 | + if not request.is_complete():
|
|
| 74 | + log.warning('Repository retrieval failed: %s',
|
|
| 75 | + request.get_error_string())
|
|
| 77 | 76 | return
|
| 78 | 77 | |
| 79 | - assert message.props.response_body_data is not None
|
|
| 80 | - data = message.props.response_body_data.get_data()
|
|
| 81 | - if data is None:
|
|
| 78 | + try:
|
|
| 79 | + updates = json.loads(request.get_data())
|
|
| 80 | + except Exception as error:
|
|
| 81 | + log.warning('Unable to parse repository information: %s', error)
|
|
| 82 | 82 | return
|
| 83 | 83 | |
| 84 | - updates = json.loads(data.decode())
|
|
| 85 | 84 | repository_url = updates.get('plugin_repository')
|
| 86 | 85 | if repository_url is None:
|
| 87 | 86 | log.warning('No plugin repository url found')
|
| ... | ... | @@ -148,56 +147,49 @@ def _parse_package_json(self, |
| 148 | 147 | |
| 149 | 148 | def _refresh_plugin_index(self, callback: Optional[Any] = None) -> None:
|
| 150 | 149 | log.info('Refresh index')
|
| 151 | - make_http_request(self._repository_index_url,
|
|
| 152 | - self._on_index_received,
|
|
| 153 | - callback)
|
|
| 154 | - |
|
| 155 | - def _on_index_received(self,
|
|
| 156 | - _session: Soup.Session,
|
|
| 157 | - message: Soup.Message,
|
|
| 158 | - callback: Optional[Any] = None) -> None:
|
|
| 159 | - |
|
| 160 | - if message.status_code != Soup.Status.OK:
|
|
| 161 | - log.warning('Refresh failed: %s', self._repository_index_url)
|
|
| 162 | - log.warning(Soup.Status.get_phrase(message.status_code))
|
|
| 163 | - return
|
|
| 150 | + request = create_http_request()
|
|
| 151 | + request.set_user_data(callback)
|
|
| 152 | + request.send('GET', self._repository_index_url,
|
|
| 153 | + callback=self._on_index_received)
|
|
| 164 | 154 | |
| 165 | - assert message.props.response_body_data is not None
|
|
| 166 | - data = message.props.response_body_data.get_data()
|
|
| 167 | - if data is None:
|
|
| 155 | + def _on_index_received(self, request: HTTPRequest) -> None:
|
|
| 156 | + |
|
| 157 | + if not request.is_complete():
|
|
| 158 | + log.warning('Refresh failed: %s %s',
|
|
| 159 | + self._repository_index_url,
|
|
| 160 | + request.get_error_string())
|
|
| 168 | 161 | return
|
| 169 | 162 | |
| 170 | - package_index = json.loads(data.decode())
|
|
| 163 | + try:
|
|
| 164 | + package_index = json.loads(request.get_data())
|
|
| 165 | + except Exception as error:
|
|
| 166 | + log.warning('Unable to parse repository index: %s', error)
|
|
| 167 | + return
|
|
| 171 | 168 | |
| 172 | 169 | self._manifests = self._parse_package_json(package_index)
|
| 173 | 170 | |
| 174 | 171 | image_path = package_index['metadata']['image_path']
|
| 175 | - make_http_request(f'{self._repository_url}/{image_path}',
|
|
| 176 | - self._on_images_received)
|
|
| 172 | + callback = request.get_user_data()
|
|
| 173 | + |
|
| 174 | + request = create_http_request()
|
|
| 175 | + request.send('GET', f'{self._repository_url}/{image_path}',
|
|
| 176 | + callback=self._on_images_received)
|
|
| 177 | 177 | |
| 178 | 178 | log.info('Refresh successful')
|
| 179 | 179 | |
| 180 | 180 | if callback is not None:
|
| 181 | 181 | callback()
|
| 182 | 182 | |
| 183 | - def _on_images_received(self,
|
|
| 184 | - _session: Soup.Session,
|
|
| 185 | - message: Soup.Message) -> None:
|
|
| 183 | + def _on_images_received(self, request: HTTPRequest) -> None:
|
|
| 186 | 184 | |
| 187 | - if message.status_code != Soup.Status.OK:
|
|
| 188 | - log.warning('Image download failed')
|
|
| 189 | - log.warning(Soup.Status.get_phrase(message.status_code))
|
|
| 190 | - return
|
|
| 191 | - |
|
| 192 | - assert message.props.response_body_data is not None
|
|
| 193 | - data = message.props.response_body_data.get_data()
|
|
| 194 | - if data is None:
|
|
| 185 | + if not request.is_complete():
|
|
| 186 | + log.warning('Image download failed: %s', request.get_error_string())
|
|
| 195 | 187 | return
|
| 196 | 188 | |
| 197 | 189 | path = configpaths.get('PLUGINS_IMAGES')
|
| 198 | 190 | |
| 199 | 191 | try:
|
| 200 | - zipfile = ZipFile(BytesIO(data))
|
|
| 192 | + zipfile = ZipFile(BytesIO(request.get_data()))
|
|
| 201 | 193 | zipfile.extractall(path)
|
| 202 | 194 | except Exception as error:
|
| 203 | 195 | log.warning('Failed to extract images: %s', error)
|
| ... | ... | @@ -245,27 +237,25 @@ def download_plugins(self, manifests: Iterable[PluginManifest]) -> None: |
| 245 | 237 | def _download_plugin(self, manifest: PluginManifest) -> None:
|
| 246 | 238 | log.info('Download plugin %s', manifest.short_name)
|
| 247 | 239 | url = manifest.get_remote_url(self._repository_url)
|
| 248 | - make_http_request(url, self._on_download_plugin_finished, manifest)
|
|
| 240 | + request = create_http_request()
|
|
| 241 | + request.set_user_data(manifest)
|
|
| 242 | + request.send('GET', url,
|
|
| 243 | + callback=self._on_download_plugin_finished)
|
|
| 249 | 244 | |
| 250 | - def _on_download_plugin_finished(self,
|
|
| 251 | - _session: Soup.Session,
|
|
| 252 | - message: Soup.Message,
|
|
| 253 | - manifest: PluginManifest) -> None:
|
|
| 245 | + def _on_download_plugin_finished(self, request: HTTPRequest) -> None:
|
|
| 254 | 246 | |
| 247 | + manifest = cast(PluginManifest, request.get_user_data())
|
|
| 255 | 248 | self._download_queue.remove(manifest)
|
| 256 | 249 | |
| 257 | - if message.status_code != Soup.Status.OK:
|
|
| 258 | - error = Soup.Status.get_phrase(message.status_code)
|
|
| 259 | - log.error('Download failed: %s',
|
|
| 260 | - manifest.get_remote_url(self._repository_url))
|
|
| 261 | - log.error(error)
|
|
| 250 | + if not request.is_complete():
|
|
| 251 | + error = request.get_error_string()
|
|
| 252 | + log.error('Download failed: %s %s',
|
|
| 253 | + manifest.get_remote_url(self._repository_url),
|
|
| 254 | + error)
|
|
| 262 | 255 | self.notify('download-failed', manifest, error)
|
| 263 | 256 | return
|
| 264 | 257 | |
| 265 | - assert message.props.response_body_data is not None
|
|
| 266 | - data = message.props.response_body_data.get_data()
|
|
| 267 | - |
|
| 268 | - with ZipFile(BytesIO(data)) as zip_file:
|
|
| 258 | + with ZipFile(BytesIO(request.get_data())) as zip_file:
|
|
| 269 | 259 | zip_file.extractall(self._download_path / manifest.short_name)
|
| 270 | 260 | |
| 271 | 261 | log.info('Finished downloading %s', manifest.short_name)
|
| ... | ... | @@ -94,7 +94,7 @@ function install_deps { |
| 94 | 94 | mingw-w64-"${ARCH}"-goocanvas \
|
| 95 | 95 | mingw-w64-"${ARCH}"-gspell \
|
| 96 | 96 | mingw-w64-"${ARCH}"-hunspell \
|
| 97 | - mingw-w64-"${ARCH}"-libsoup \
|
|
| 97 | + mingw-w64-"${ARCH}"-libsoup3 \
|
|
| 98 | 98 | mingw-w64-"${ARCH}"-python-setuptools \
|
| 99 | 99 | mingw-w64-"${ARCH}"-python-pillow \
|
| 100 | 100 | mingw-w64-"${ARCH}"-python-setuptools-scm \
|
| ... | ... | @@ -22,7 +22,7 @@ function main { |
| 22 | 22 | mingw-w64-x86_64-goocanvas \
|
| 23 | 23 | mingw-w64-x86_64-gspell \
|
| 24 | 24 | mingw-w64-x86_64-hunspell \
|
| 25 | - mingw-w64-x86_64-libsoup \
|
|
| 25 | + mingw-w64-x86_64-libsoup3 \
|
|
| 26 | 26 | |
| 27 | 27 | PIP_REQUIREMENTS="\
|
| 28 | 28 | git+https://dev.gajim.org/gajim/python-nbxmpp.git
|