Philipp Hörist pushed to branch soup3 at gajim / gajim

Commits:

15 changed files:

Changes:

  • .gitlab-ci.yml
    ... ... @@ -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
    

  • README.md
    ... ... @@ -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
    

  • gajim/common/application.py
    ... ... @@ -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):
    

  • gajim/common/client.py
    ... ... @@ -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)
    

  • gajim/common/filetransfer.py
    ... ... @@ -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 ''
    

  • gajim/common/helpers.py
    ... ... @@ -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)

  • gajim/common/modules/httpupload.py
    ... ... @@ -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
    

  • gajim/common/preview.py
    ... ... @@ -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()

  • gajim/common/util/http.py
    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()

  • gajim/gajim.py
    ... ... @@ -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
     
    

  • gajim/gtk/conversation/rows/file_transfer.py
    ... ... @@ -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),
    

  • gajim/gtk/exception.py
    ... ... @@ -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()
    

  • gajim/plugins/repository.py
    ... ... @@ -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)
    

  • win/_base.sh
    ... ... @@ -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 \
    

  • win/dev_env.sh
    ... ... @@ -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