Philipp Hörist pushed to branch soup3 at gajim / gajim
Commits: 747c5696 by Philipp Hörist at 2022-12-28T16:23:51+01:00 change: Port to libsoup3
- Remove all direct usage of libsoup and use the nbxmpp.http module instead
- - - - -
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:
===================================== .gitlab-ci.yml ===================================== @@ -26,7 +26,7 @@ test-pyright: - "**/*.py" script: - pip3 install git+https://dev.gajim.org/gajim/python-nbxmpp.git - - PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3,Soup2 pip3 install git+https://github.com/pygobject/pygobject-stubs.git + - PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3 pip3 install git+https://github.com/pygobject/pygobject-stubs.git - npm install pyright - python3 .ci/link-gtk.py - $(npm bin)/pyright --version
===================================== README.md ===================================== @@ -18,7 +18,6 @@ ### Runtime Requirements - [GLib](https://gitlab.com/gnome/glib) (>=2.60.0) - [GtkSourceView](https://gitlab.gnome.org/GNOME/gtksourceview) - [Pango](https://gitlab.gnome.org/GNOME/pango) (>=1.50.0) -- [libsoup](https://gitlab.gnome.org/GNOME/libsoup/) - [sqlite](https://www.sqlite.org/) (>=3.33.0)
### Optional Runtime Requirements
===================================== gajim/common/application.py ===================================== @@ -29,9 +29,9 @@
from gi.repository import Gio from gi.repository import GLib -from gi.repository import Soup from nbxmpp.const import ConnectionType from nbxmpp.const import ConnectionProtocol +from nbxmpp.http import HTTPRequest
from gajim.common import app from gajim.common import ged @@ -48,7 +48,6 @@ from gajim.common.events import GajimUpdateAvailable from gajim.common.events import SignedIn from gajim.common.client import Client -from gajim.common.helpers import make_http_request from gajim.common.helpers import get_random_string from gajim.common.helpers import get_global_show from gajim.common.helpers import from_one_line @@ -60,6 +59,7 @@ from gajim.common.storage.cache import CacheStorage from gajim.common.storage.draft import DraftStorage from gajim.common.storage.archive import MessageArchiveStorage +from gajim.common.util.http import create_http_request
from gajim.plugins.pluginmanager import PluginManager from gajim.plugins.repository import PluginRepository @@ -310,22 +310,21 @@ def _check_for_updates(self) -> None:
def check_for_gajim_updates(self) -> None: self._log.info('Checking for Gajim updates') - make_http_request('https://gajim.org/current-version.json', - self._on_update_response) + request = create_http_request() + request.send('GET', 'https://gajim.org/current-version.json', + callback=self._on_update_response)
- def _on_update_response(self, - _session: Soup.Session, - message: Soup.Message) -> None: + def _on_update_response(self, request: HTTPRequest) -> None: + + if not request.is_complete(): + self._log.warning('Error while performing gajim update check: %s', + request.get_error(), request.get_status()) + return
now = datetime.now() app.settings.set('last_update_check', now.strftime('%Y-%m-%d %H:%M'))
- response_body = message.props.response_body - if response_body is None or not response_body.data: - self._log.warning('Could not reach gajim.org for update check') - return - - data = json.loads(response_body.data) + data = json.loads(request.get_data()) latest_version = data['current_version']
if V(latest_version) > V(app.version):
===================================== gajim/common/client.py ===================================== @@ -55,6 +55,7 @@ from gajim.common.idle import IdleMonitorManager from gajim.common.i18n import _ from gajim.common.structs import OutgoingMessage +from gajim.common.util.http import create_http_session
from gajim.gui.util import open_window
@@ -175,6 +176,8 @@ def _create_client(self) -> None:
self._client.set_password(self.password)
+ self._client.set_http_session(create_http_session()) + self._client.subscribe('resume-failed', self._on_resume_failed) self._client.subscribe('resume-successful', self._on_resume_successful) self._client.subscribe('disconnected', self._on_disconnected)
===================================== gajim/common/filetransfer.py ===================================== @@ -27,8 +27,7 @@ def __init__(self, account: str) -> None:
self._account = account
- self._seen: int = 0 - self.size: int = 0 + self._progress = 0
self._state = FTState.INIT self._error_text: str = '' @@ -42,16 +41,6 @@ def account(self) -> str: def state(self) -> FTState: return self._state
- @property - def seen(self) -> int: - return self._seen - - @property - def is_complete(self) -> bool: - if self.size == 0: - return False - return self._seen >= self.size - @property def filename(self) -> str: raise NotImplementedError @@ -64,6 +53,13 @@ def error_text(self) -> str: def error_domain(self) -> Optional[str]: return self._error_domain
+ def get_progress(self) -> float: + return self._progress + + def set_progress(self, progress: float) -> None: + self._progress = progress + self.update_progress() + def get_state_description(self) -> str: if self._state is None: return ''
===================================== gajim/common/helpers.py ===================================== @@ -81,7 +81,6 @@ from gi.repository import Gio from gi.repository import GLib from gi.repository import GObject -from gi.repository import Soup
import precis_i18n.codec # noqa: F401
@@ -1546,24 +1545,3 @@ def make_path_from_jid(base_path: Path, jid: JID) -> Path: if jid.resource is not None: return path / jid.resource[:30] return path - - -def make_http_request(uri: str, - callback: Any, - user_data: Optional[Any] = None) -> None: - - proxy = determine_proxy() - if proxy is None: - resolver = None - - else: - resolver = proxy.get_resolver() - - session = Soup.Session() - session.props.proxy_resolver = resolver - session.props.user_agent = f'Gajim {app.version}' - message = Soup.Message.new('GET', uri) - if user_data is None: - session.queue_message(message, callback) - else: - session.queue_message(message, callback, user_data)
===================================== gajim/common/modules/httpupload.py ===================================== @@ -20,12 +20,13 @@ from typing import Callable from typing import Optional
-import os -import io -from urllib.parse import urlparse -import mimetypes from collections import defaultdict +import functools from pathlib import Path +from urllib.parse import urlparse +import mimetypes +import os +import tempfile
from nbxmpp.errors import StanzaError from nbxmpp.errors import MalformedStanzaError @@ -36,23 +37,25 @@ from nbxmpp.structs import HTTPUploadData from nbxmpp.task import Task from nbxmpp.util import convert_tls_error_flags +from nbxmpp.http import HTTPRequest +from nbxmpp.const import HTTPRequestError
from gi.repository import Gio from gi.repository import GLib -from gi.repository import Soup
from gajim.common import app from gajim.common import types from gajim.common.events import HTTPUploadError from gajim.common.events import HTTPUploadStarted from gajim.common.i18n import _ +from gajim.common.helpers import get_random_string from gajim.common.helpers import get_tls_error_phrases -from gajim.common.helpers import get_account_proxy from gajim.common.const import FTState from gajim.common.filetransfer import FileTransfer from gajim.common.modules.base import BaseModule from gajim.common.exceptions import FileError from gajim.common.structs import OutgoingMessage +from gajim.common.util.http import create_http_request
class HTTPUpload(BaseModule): @@ -67,24 +70,11 @@ def __init__(self, con: types.Client) -> None: self.httpupload_namespace: Optional[str] = None self.max_file_size: Optional[float] = None # max file size in bytes
- self._proxy_resolver: Optional[Gio.SimpleProxyResolver] = None - self._queued_messages: dict[int, Soup.Message] = {} - self._session = Soup.Session() - self._session.props.ssl_strict = False - self._session.props.user_agent = f'Gajim {app.version}' + self._requests_in_progress: dict[int, HTTPRequest] = {}
self._running_transfers: dict[ tuple[str, JID], set[HTTPFileTransfer]] = defaultdict(set)
- def _set_proxy_if_available(self) -> None: - proxy = get_account_proxy(self._account) - if proxy is None: - self._proxy_resolver = None - self._session.props.proxy_resolver = None - else: - self._proxy_resolver = proxy.get_resolver() - self._session.props.proxy_resolver = self._proxy_resolver - def pass_disco(self, info: DiscoInfo) -> None: if not info.has_httpupload: return @@ -215,11 +205,11 @@ def _on_cancel_upload(self, key = (transfer.account, transfer.contact.jid) self._running_transfers[key].discard(transfer)
- message = self._queued_messages.get(id(transfer)) - if message is None: + request = self._requests_in_progress.get(id(transfer)) + if request is None: return
- self._session.cancel_message(message, Soup.Status.CANCELLED) + request.cancel()
def _start_transfer(self, transfer: HTTPFileTransfer) -> None: if transfer.encryption is not None and not transfer.is_encrypted: @@ -283,106 +273,61 @@ def _upload_file(self, transfer: HTTPFileTransfer) -> None: transfer.set_started()
assert transfer.put_uri is not None - message = Soup.Message.new('PUT', transfer.put_uri) - message.connect('starting', self._check_certificate, transfer) - - # Set CAN_REBUILD so chunks get discarded after they have been - # written to the network - message.set_flags(Soup.MessageFlags.CAN_REBUILD | - Soup.MessageFlags.NO_REDIRECT) + request = create_http_request(self._account) + request.set_user_data(transfer) + request.connect('accept-certificate', self._accept_certificate) + request.connect('request-progress', self._on_request_progress)
- assert message.props.request_body is not None - message.props.request_body.set_accumulate(False) + request.set_request_body_from_path(transfer.mime, transfer.payload_path)
- assert message.props.request_headers is not None - message.props.request_headers.set_content_type(transfer.mime, None) - message.props.request_headers.set_content_length(transfer.size) for name, value in transfer.headers.items(): - message.props.request_headers.append(name, value) - - message.connect('wrote-headers', self._on_wrote_headers, transfer) - message.connect('wrote-chunk', self._on_wrote_chunk, transfer) - - self._queued_messages[id(transfer)] = message - self._set_proxy_if_available() - self._session.queue_message(message, self._on_finish, transfer) - - def _check_certificate(self, - message: Soup.Message, - transfer: HTTPFileTransfer - ) -> None: - https_used, tls_certificate, tls_errors = message.get_https_status() - if not https_used: - self._log.warning('HTTPS was not used for upload') - transfer.set_error('unsecure') - self._session.cancel_message(message, Soup.Status.CANCELLED) - return + request.get_request_headers().append(name, value)
- tls_error_set = convert_tls_error_flags(tls_errors) - if app.cert_store.verify(tls_certificate, tls_error_set): - return + request.send('PUT', transfer.put_uri, callback=self._on_finish) + + self._requests_in_progress[id(transfer)] = request + + def _accept_certificate(self, + request: HTTPRequest, + _certificate: Gio.TlsCertificate, + certificate_errors: Gio.TlsCertificateFlags, + ) -> bool:
- phrases = get_tls_error_phrases(tls_error_set) + transfer = request.get_user_data() + phrases = get_tls_error_phrases( + convert_tls_error_flags(certificate_errors)) self._log.warning( - 'TLS verification failed: %s (0x%02x)', phrases, tls_errors) + 'TLS verification failed: %s (0x%02x)', phrases, certificate_errors) + transfer.set_error('tls-verification-failed', phrases[0]) - self._session.cancel_message(message, Soup.Status.CANCELLED) + return False
- def _on_finish(self, - _session: Soup.Session, - message: Soup.Message, - transfer: HTTPFileTransfer - ) -> None: + def _on_finish(self, request: HTTPRequest) -> None: + transfer = request.get_user_data()
- self._queued_messages.pop(id(transfer), None) + self._requests_in_progress.pop(id(transfer), None)
key = (transfer.account, transfer.contact.jid) self._running_transfers[key].discard(transfer)
- if message.props.status_code == Soup.Status.CANCELLED: - self._log.info('Upload cancelled') - return - - if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED): - self._log.info('Upload completed successfully') - transfer.set_finished() - - else: - phrase = Soup.Status.get_phrase(message.props.status_code) - self._log.error('Got unexpected http upload response code: %s', - phrase) - transfer.set_error('http-response', phrase) - - def _on_wrote_chunk(self, - message: Soup.Message, - transfer: HTTPFileTransfer - ) -> None: - transfer.update_progress() - if transfer.is_complete: - assert message.props.request_body is not None - message.props.request_body.complete() + if not request.is_complete(): + error = request.get_error_string() + if request.get_error() == HTTPRequestError.CANCELLED: + self._log.info('Upload cancelled') + else: + transfer.set_error('http-response', error) return
- bytes_ = transfer.get_chunk() - assert bytes_ is not None - self._session.pause_message(message) - GLib.idle_add(self._append, message, bytes_) + transfer.set_finished() + self._log.info('Upload completed successfully')
- def _append(self, message: Soup.Message, bytes_: bytes) -> None: - if message.props.status_code == Soup.Status.CANCELLED: - return - self._session.unpause_message(message) - assert message.props.request_body is not None - message.props.request_body.append(bytes_) + def _on_request_progress(self, + request: HTTPRequest, + progress: float + ) -> None:
- @staticmethod - def _on_wrote_headers(message: Soup.Message, - transfer: HTTPFileTransfer - ) -> None: - bytes_ = transfer.get_chunk() - assert bytes_ is not None - assert message.props.request_body is not None - message.props.request_body.append(bytes_) + transfer = request.get_user_data() + transfer.set_progress(progress)
class HTTPFileTransfer(FileTransfer): @@ -410,23 +355,32 @@ def __init__(self,
FileTransfer.__init__(self, account)
- self._path = path + self._path = Path(path) self._encryption = encryption self._groupchat = groupchat self._contact = contact self._mime = mime
- self.size = os.stat(path).st_size self.put_uri: Optional[str] = None self.get_uri: Optional[str] = None self._uri_transform_func: Optional[Callable[[str], str]] = None
- self._stream = None self._data: Optional[bytes] = None self._headers: dict[str, str] = {}
self._is_encrypted = False
+ @property + def size(self) -> int: + if self._encryption is not None and not self._is_encrypted: + raise ValueError('File size unknown at this point') + return self.payload_path.stat().st_size + + @size.setter + def size(self, size: int) -> None: + # Backwards compatibility with plugins + pass + @property def mime(self) -> str: return self._mime @@ -448,7 +402,13 @@ def headers(self) -> dict[str, str]: return self._headers
@property - def path(self) -> str: + def path(self) -> Path: + return self._path + + @property + def payload_path(self) -> Path: + if self._encryption is not None: + return self.get_temp_path() return self._path
@property @@ -465,47 +425,30 @@ def set_uri_transform_func(self, func: Callable[[str], str]) -> None:
@property def filename(self) -> str: - return os.path.basename(self._path) + return self._path.name + + @staticmethod + @functools.cache + def get_temp_path() -> Path: + tempdir = tempfile.gettempdir() + return Path(tempdir) / get_random_string(16)
def set_error(self, domain: str, text: str = '') -> None: if not text: text = self._errors[domain]
- self._close() super().set_error(domain, text)
def set_finished(self) -> None: - self._close() super().set_finished()
def set_encrypted_data(self, data: bytes) -> None: - self._data = data + temp_path = self.get_temp_path() + temp_path.write_bytes(data) self._is_encrypted = True
- def _close(self) -> None: - if self._stream is not None: - self._stream.close() - - def get_chunk(self) -> Optional[bytes]: - if self._stream is None: - if self._encryption is None: - self._stream = open(self._path, 'rb') # pylint: disable=consider-using-with # noqa: E501 - else: - self._stream = io.BytesIO(self._data) - - data = self._stream.read(16384) - if not data: - self._close() - return None - self._seen += len(data) - if self.is_complete: - self._close() - return data - def get_data(self) -> bytes: - with open(self._path, 'rb') as file: - data = file.read() - return data + return self._path.read_bytes()
def process_result(self, result: HTTPUploadData) -> None: self.put_uri = result.put_uri
===================================== gajim/common/preview.py ===================================== @@ -14,12 +14,14 @@
from __future__ import annotations
-from dataclasses import dataclass, field from typing import Any +from typing import cast from typing import Callable from typing import Optional
import logging +from dataclasses import dataclass +from dataclasses import field import os import re from pathlib import Path @@ -29,7 +31,8 @@
from gi.repository import Gio from gi.repository import GLib -from gi.repository import Soup +from nbxmpp.const import HTTPRequestError +from nbxmpp.http import HTTPRequest from nbxmpp.util import convert_tls_error_flags
from gajim.common import app @@ -40,7 +43,6 @@ from gajim.common.helpers import load_file_async from gajim.common.helpers import write_file_async from gajim.common.helpers import get_tls_error_phrases -from gajim.common.helpers import get_account_proxy from gajim.common.i18n import _ from gajim.common.preview_helpers import aes_decrypt from gajim.common.preview_helpers import filename_from_uri @@ -52,6 +54,7 @@ from gajim.common.preview_helpers import guess_mime_type from gajim.common.preview_helpers import pixbuf_from_data from gajim.common.types import GdkPixbufType +from gajim.common.util.http import create_http_request
log = logging.getLogger('gajim.c.preview')
@@ -104,12 +107,11 @@ def __init__(self, self.thumbnail: Optional[bytes] = None self.mime_type: str = '' self.file_size: int = 0 - self._received_size: int = 0 self.download_in_progress = False
self.info_message: Optional[str] = None
- self._soup_message: Optional[Soup.Message] = None + self._request = None
self.key: Optional[bytes] = None self.iv: Optional[bytes] = None @@ -154,8 +156,8 @@ def filename(self) -> str: return self._filename
@property - def soup_message(self) -> Soup.Message: - return self._soup_message + def request(self) -> HTTPRequest: + return self._request
@property def request_uri(self) -> Optional[str]: @@ -193,29 +195,17 @@ def create_thumbnail(self, data: bytes) -> bool: return False return True
- def reset_received_size(self) -> None: - self._received_size = 0 - def update_widget(self, data: Optional[GdkPixbufType] = None) -> None: self._widget.update(self, data)
- def update_progress(self, size: int, message: Soup.Message) -> None: + def update_progress(self, progress: float, request: HTTPRequest) -> None: self.download_in_progress = True - self._soup_message = message - self._received_size += size - if self.file_size == 0 or self._received_size == 0: - return - - progress = self._received_size / self.file_size + self._request = request self._widget.update_progress(self, progress)
class PreviewManager: def __init__(self) -> None: - self._sessions: dict[ - str, - tuple[Soup.Session, Optional[Gio.SimpleProxyResolver]]] = {} - self._orig_dir = Path(configpaths.get('MY_DATA')) / 'downloads' self._thumb_dir = Path(configpaths.get('MY_CACHE')) / 'downloads.thumb'
@@ -268,29 +258,6 @@ def stop_audio_except(self, preview_id: int) -> None: if id_ != preview_id: stop_func()
- def _get_session(self, account: str) -> Soup.Session: - if account not in self._sessions: - self._sessions[account] = self._create_session(account) - return self._sessions[account][0] - - @staticmethod - def _create_session(account: str) -> tuple[ - Soup.Session, Optional[Gio.SimpleProxyResolver]]: - session = Soup.Session() - session.add_feature_by_type(Soup.ContentSniffer) - session.props.https_aliases = ['aesgcm'] - session.props.ssl_strict = False - session.props.user_agent = f'Gajim {app.version}' - - proxy = get_account_proxy(account) - if proxy is None: - resolver = None - else: - resolver = proxy.get_resolver() - - session.props.proxy_resolver = resolver - return session, resolver - @staticmethod def _accept_uri(urlparts: ParseResult, uri: str, @@ -476,58 +443,62 @@ def download_content(self, log.info('Download already in progress') return
- preview.reset_received_size() if preview.account is None: # History Window can be opened without account context # This means we can not apply proxy settings return log.info('Start downloading: %s', preview.request_uri) - message = Soup.Message.new('GET', preview.request_uri) - message.connect('starting', self._check_certificate, preview) - message.connect( - 'content-sniffed', self._on_content_sniffed, preview, force) - message.connect('got-chunk', self._on_got_chunk, preview)
- session = self._get_session(preview.account) - session.queue_message(message, self._on_finished, preview) + request = create_http_request(preview.account, sniffer=True) + request.set_user_data(preview) + request.connect('accept-certificate', self._accept_certificate) + request.connect('content-sniffed', self._on_content_sniffed, force) + request.connect('response-progress', self._on_response_progress)
- def _check_certificate(self, - message: Soup.Message, - preview: Preview) -> None: - _https_used, _tls_certificate, tls_errors = message.get_https_status() + request.send('GET', preview.request_uri, callback=self._on_finished) + + def _accept_certificate(self, + request: HTTPRequest, + certificate: Gio.TlsCertificate, + certificate_errors: Gio.TlsCertificateFlags, + ) -> bool:
if not app.settings.get('preview_verify_https'): - return + return True
- if tls_errors: - phrases = get_tls_error_phrases(convert_tls_error_flags(tls_errors)) - log.warning( - 'TLS verification failed: %s (0x%02x)', phrases, tls_errors) - session = self._get_session(preview.account) - session.cancel_message(message, Soup.Status.CANCELLED) - preview.info_message = _('TLS verification failed: %s') % phrases[0] - preview.update_widget() + phrases = get_tls_error_phrases( + convert_tls_error_flags(certificate_errors)) + log.warning( + 'TLS verification failed: %s (0x%02x)', phrases, certificate_errors) + + preview = cast(Preview, request.get_user_data()) + preview.info_message = _('TLS verification failed: %s') % phrases[0] + preview.update_widget() + return False
def _on_content_sniffed(self, - message: Soup.Message, - type_: str, + request: HTTPRequest, + content_type: str, _params: GLib.HashTable, - preview: Preview, - force: bool) -> None: - file_size = message.props.response_headers.get_content_length() - uri = message.props.uri.to_string(False) - session = self._get_session(preview.account) - preview.mime_type = type_ + force: bool + ) -> None: + + file_size = request.get_response_headers().get_content_length() + uri = request.get_uri().to_string() + + + preview = cast(Preview, request.get_user_data()) + preview.mime_type = content_type preview.file_size = file_size
- if type_ not in ALLOWED_MIME_TYPES and not force: - log.info('Not an allowed content type: %s, %s', type_, uri) - session.cancel_message(message, Soup.Status.CANCELLED) + if content_type not in ALLOWED_MIME_TYPES and not force: + log.info('Not an allowed content type: %s, %s', content_type, uri) + request.cancel() return
if file_size == 0: log.info('File size is unknown (zero) for URL: "%s"', uri) - session.cancel_message(message, Soup.Status.CANCELLED) + request.cancel() return
if file_size > int(app.settings.get('preview_max_file_size')): @@ -537,42 +508,37 @@ def _on_content_sniffed(self, if force: preview.info_message = None else: - session.cancel_message(message, Soup.Status.CANCELLED) + request.cancel() preview.info_message = _('Automatic preview disabled ' '(file too big)')
preview.update_widget()
- def _on_got_chunk(self, - message: Soup.Message, - chunk: Soup.Buffer, - preview: Preview - ) -> None: - preview.update_progress(len(chunk.get_data()), message) + def _on_response_progress(self, + request: HTTPRequest, + progress: float, + ) -> None:
- def _on_finished(self, - _session: Soup.Session, - message: Soup.Message, - preview: Preview - ) -> None: + preview = cast(Preview, request.get_user_data()) + preview.update_progress(progress, request)
+ def _on_finished(self, request: HTTPRequest) -> None: + + preview = cast(Preview, request.get_user_data()) preview.download_in_progress = False
- if message.status_code != Soup.Status.OK: - log.warning('Download failed: %s', preview.request_uri) - status_code = Soup.Status.get_phrase(message.status_code) - log.warning(status_code) - preview.reset_received_size() - if message.status_code != 1: - # status_code 1: 'Cancelled' - preview.info_message = _('Download failed (%s)') % status_code + if not request.is_complete(): + error = request.get_error_string() + log.warning('Download failed: %s - %s', preview.request_uri, error) + if request.get_error() != HTTPRequestError.CANCELLED: + preview.info_message = _('Download failed (%s)') % error preview.update_widget() return
preview.info_message = None
- data = message.props.response_body_data.get_data() - if data is None: + data = request.get_data() + if not data: return
if preview.is_aes_encrypted: @@ -649,6 +615,4 @@ def _on_thumb_write_finished(_result: bool, preview.update_widget(data=pixbuf)
def cancel_download(self, preview: Preview) -> None: - preview.reset_received_size() - session = self._get_session(preview.account) - session.cancel_message(preview.soup_message, Soup.Status.CANCELLED) + preview.request.cancel()
===================================== gajim/common/util/http.py ===================================== @@ -0,0 +1,48 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +from typing import Optional + +from nbxmpp.http import HTTPRequest +from nbxmpp.http import HTTPSession + +from gajim.common import app +from gajim.common.helpers import determine_proxy +from gajim.common.helpers import get_account_proxy + + +def create_http_session(account: Optional[str] = None, + sniffer: bool = False + ) -> HTTPSession: + + session = HTTPSession(user_agent=f'Gajim {app.version}', + sniffer=sniffer) + + if account is None: + proxy = determine_proxy() + else: + proxy = get_account_proxy(account) + + if proxy is not None: + session.set_proxy_resolver(proxy.get_resolver()) + + return session + + +def create_http_request(account: Optional[str] = None, + sniffer: bool = False + ) -> HTTPRequest: + + session = create_http_session(account, sniffer) + return session.create_request()
===================================== gajim/gajim.py ===================================== @@ -65,8 +65,7 @@ def _check_required_deps() -> None: 'GtkSource': '4', 'GObject': '2.0', 'Pango': '1.0', - 'PangoCairo': '1.0', - 'Soup': '2.4'}) + 'PangoCairo': '1.0'}) except ValueError as error: sys.exit('Missing dependency: %s' % error)
===================================== gajim/gtk/conversation/rows/file_transfer.py ===================================== @@ -131,7 +131,10 @@ def _on_transfer_progress(self, size_total = GLib.format_size_full(transfer.size, self._units) self._ui.file_size.set_text(size_total)
- bytes_sec = int(round(transfer.seen / (time_now - self._start_time), 1)) + progress = transfer.get_progress() + seen = transfer.size * progress + + bytes_sec = int(round(seen / (time_now - self._start_time), 1)) speed = f'{GLib.format_size_full(bytes_sec, self._units)}/s' self._ui.transfer_progress.set_tooltip_text(_('Speed: %s') % speed)
@@ -139,9 +142,8 @@ def _on_transfer_progress(self, eta = '∞' else: eta = format_eta(round( - (transfer.size - transfer.seen) / bytes_sec)) + (transfer.size - seen) / bytes_sec))
- progress = float(transfer.seen) / transfer.size self._ui.transfer_progress.set_text( _('%(progress)s %% (%(time)s remaining)') % { 'progress': round(progress * 100),
===================================== gajim/gtk/exception.py ===================================== @@ -20,11 +20,11 @@ from __future__ import annotations
from typing import Any -from types import TracebackType
import sys import json import traceback +from types import TracebackType import threading import webbrowser import pprint @@ -33,9 +33,9 @@
from gi.repository import Gdk from gi.repository import Gtk -from gi.repository import Soup
import nbxmpp +from nbxmpp.http import HTTPRequest
import gajim from gajim.common import app @@ -44,8 +44,8 @@ from gajim.common.helpers import get_os_name from gajim.common.helpers import get_os_version from gajim.common.helpers import determine_proxy -from gajim.common.helpers import make_http_request from gajim.common.i18n import _ +from gajim.common.util.http import create_http_request
from .builder import get_builder from .util import get_gtk_version @@ -182,24 +182,17 @@ def _request_sentry_endpoint(self) -> None: self._ui.report_spinner.show() self._ui.report_spinner.start()
- make_http_request('https://gajim.org/updates.json', - self._on_endpoint_received) + request = create_http_request() + request.send('GET', 'https://gajim.org/updates.json', + callback=self._on_endpoint_received)
- def _parse_endpoint(self, message: Soup.Message) -> str: - status = message.props.status_code - response_body = message.props.response_body - - if status != Soup.Status.OK: - error = message.props.reason_phrase + def _parse_endpoint(self, request: HTTPRequest) -> str: + if not request.is_complete(): raise ValueError('Failed to retrieve sentry endpoint: %s %s' % ( - status, error)) - - if response_body is None or not response_body.data: - raise ValueError('Failed to retrieve sentry endpoint, ' - 'no response body') + request.get_status(), request.get_error()))
try: - data = json.loads(response_body.data) + data = json.loads(request.get_data()) except Exception as error: raise ValueError('Json parsing error: %s' % error)
@@ -209,12 +202,9 @@ def _parse_endpoint(self, message: Soup.Message) -> str:
return endpoint
- def _on_endpoint_received(self, - _session: Soup.Session, - message: Soup.Message - ) -> None: + def _on_endpoint_received(self, request: HTTPRequest) -> None: try: - endpoint = self._parse_endpoint(message) + endpoint = self._parse_endpoint(request) except ValueError as error: print(error) self._report_with_browser()
===================================== gajim/plugins/repository.py ===================================== @@ -12,7 +12,7 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see http://www.gnu.org/licenses/.
-from typing import Any +from typing import Any, cast from typing import Iterable from typing import Optional
@@ -21,14 +21,14 @@ from io import BytesIO from zipfile import ZipFile
-from gi.repository import Soup from gi.repository import GLib +from nbxmpp.http import HTTPRequest
from gajim.common import app from gajim.common import configpaths from gajim.common.helpers import Observable -from gajim.common.helpers import make_http_request from gajim.plugins.manifest import PluginManifest +from gajim.common.util.http import create_http_request
log = logging.getLogger('gajim.p.repository') @@ -60,28 +60,27 @@ def __init__(self) -> None: self._download_queue: set[PluginManifest] = set()
if app.settings.get('plugins_repository_enabled'): - make_http_request('https://gajim.org/updates.json', - self._on_repository_received) + request = create_http_request() + request.send('GET', 'https://gajim.org/updates.json', + callback=self._on_repository_received)
@property def available(self): return bool(self._repository_url)
- def _on_repository_received(self, - _session: Soup.Session, - message: Soup.Message) -> None: + def _on_repository_received(self, request: HTTPRequest) -> None:
- if message.status_code != Soup.Status.OK: - log.warning('Repository retrieval failed') - log.warning(Soup.Status.get_phrase(message.status_code)) + if not request.is_complete(): + log.warning('Repository retrieval failed: %s', + request.get_error_string()) return
- assert message.props.response_body_data is not None - data = message.props.response_body_data.get_data() - if data is None: + try: + updates = json.loads(request.get_data()) + except Exception as error: + log.warning('Unable to parse repository information: %s', error) return
- updates = json.loads(data.decode()) repository_url = updates.get('plugin_repository') if repository_url is None: log.warning('No plugin repository url found') @@ -148,56 +147,49 @@ def _parse_package_json(self,
def _refresh_plugin_index(self, callback: Optional[Any] = None) -> None: log.info('Refresh index') - make_http_request(self._repository_index_url, - self._on_index_received, - callback) - - def _on_index_received(self, - _session: Soup.Session, - message: Soup.Message, - callback: Optional[Any] = None) -> None: - - if message.status_code != Soup.Status.OK: - log.warning('Refresh failed: %s', self._repository_index_url) - log.warning(Soup.Status.get_phrase(message.status_code)) - return + request = create_http_request() + request.set_user_data(callback) + request.send('GET', self._repository_index_url, + callback=self._on_index_received)
- assert message.props.response_body_data is not None - data = message.props.response_body_data.get_data() - if data is None: + def _on_index_received(self, request: HTTPRequest) -> None: + + if not request.is_complete(): + log.warning('Refresh failed: %s %s', + self._repository_index_url, + request.get_error_string()) return
- package_index = json.loads(data.decode()) + try: + package_index = json.loads(request.get_data()) + except Exception as error: + log.warning('Unable to parse repository index: %s', error) + return
self._manifests = self._parse_package_json(package_index)
image_path = package_index['metadata']['image_path'] - make_http_request(f'{self._repository_url}/{image_path}', - self._on_images_received) + callback = request.get_user_data() + + request = create_http_request() + request.send('GET', f'{self._repository_url}/{image_path}', + callback=self._on_images_received)
log.info('Refresh successful')
if callback is not None: callback()
- def _on_images_received(self, - _session: Soup.Session, - message: Soup.Message) -> None: + def _on_images_received(self, request: HTTPRequest) -> None:
- if message.status_code != Soup.Status.OK: - log.warning('Image download failed') - log.warning(Soup.Status.get_phrase(message.status_code)) - return - - assert message.props.response_body_data is not None - data = message.props.response_body_data.get_data() - if data is None: + if not request.is_complete(): + log.warning('Image download failed: %s', request.get_error_string()) return
path = configpaths.get('PLUGINS_IMAGES')
try: - zipfile = ZipFile(BytesIO(data)) + zipfile = ZipFile(BytesIO(request.get_data())) zipfile.extractall(path) except Exception as error: log.warning('Failed to extract images: %s', error) @@ -245,27 +237,25 @@ def download_plugins(self, manifests: Iterable[PluginManifest]) -> None: def _download_plugin(self, manifest: PluginManifest) -> None: log.info('Download plugin %s', manifest.short_name) url = manifest.get_remote_url(self._repository_url) - make_http_request(url, self._on_download_plugin_finished, manifest) + request = create_http_request() + request.set_user_data(manifest) + request.send('GET', url, + callback=self._on_download_plugin_finished)
- def _on_download_plugin_finished(self, - _session: Soup.Session, - message: Soup.Message, - manifest: PluginManifest) -> None: + def _on_download_plugin_finished(self, request: HTTPRequest) -> None:
+ manifest = cast(PluginManifest, request.get_user_data()) self._download_queue.remove(manifest)
- if message.status_code != Soup.Status.OK: - error = Soup.Status.get_phrase(message.status_code) - log.error('Download failed: %s', - manifest.get_remote_url(self._repository_url)) - log.error(error) + if not request.is_complete(): + error = request.get_error_string() + log.error('Download failed: %s %s', + manifest.get_remote_url(self._repository_url), + error) self.notify('download-failed', manifest, error) return
- assert message.props.response_body_data is not None - data = message.props.response_body_data.get_data() - - with ZipFile(BytesIO(data)) as zip_file: + with ZipFile(BytesIO(request.get_data())) as zip_file: zip_file.extractall(self._download_path / manifest.short_name)
log.info('Finished downloading %s', manifest.short_name)
===================================== win/_base.sh ===================================== @@ -94,7 +94,7 @@ function install_deps { mingw-w64-"${ARCH}"-goocanvas \ mingw-w64-"${ARCH}"-gspell \ mingw-w64-"${ARCH}"-hunspell \ - mingw-w64-"${ARCH}"-libsoup \ + mingw-w64-"${ARCH}"-libsoup3 \ mingw-w64-"${ARCH}"-python-setuptools \ mingw-w64-"${ARCH}"-python-pillow \ mingw-w64-"${ARCH}"-python-setuptools-scm \
===================================== win/dev_env.sh ===================================== @@ -22,7 +22,7 @@ function main { mingw-w64-x86_64-goocanvas \ mingw-w64-x86_64-gspell \ mingw-w64-x86_64-hunspell \ - mingw-w64-x86_64-libsoup \ + mingw-w64-x86_64-libsoup3 \
PIP_REQUIREMENTS="\ git+https://dev.gajim.org/gajim/python-nbxmpp.git
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/commit/747c569638e0abb877517cee162854c76...