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

Commits:

2 changed files:

Changes:

  • gajim/common/modules/omemo.py
    ... ... @@ -76,6 +76,7 @@
    76 76
     from gajim.common.modules.util import prepare_stanza
    
    77 77
     from gajim.common.storage.omemo import OMEMOStorage
    
    78 78
     from gajim.common.structs import OutgoingMessage
    
    79
    +from gajim.common.util.decorators import lru_cache_with_ttl
    
    79 80
     
    
    80 81
     ALLOWED_TAGS = [
    
    81 82
         ('request', Namespace.RECEIPTS),
    
    ... ... @@ -149,13 +150,9 @@ def __init__(self, client: types.Client) -> None:
    149 150
     
    
    150 151
             self._omemo_groupchats: set[str] = set()
    
    151 152
             self._muc_temp_store: dict[bytes, str] = {}
    
    152
    -        self._query_for_bundles: list[str] = []
    
    153
    -        self._device_bundle_querys: list[int] = []
    
    154
    -        self._query_for_devicelists: list[str] = []
    
    155 153
     
    
    156 154
         def _on_signed_in(self, _event: SignedIn) -> None:
    
    157
    -        self._log.info('Announce Support after Sign In')
    
    158
    -        self._query_for_bundles = []
    
    155
    +        self._log.info('Publish our bundle after sign in')
    
    159 156
             self.set_bundle()
    
    160 157
             self.request_devicelist()
    
    161 158
     
    
    ... ... @@ -180,8 +177,8 @@ def _on_room_joined(self,
    180 177
     
    
    181 178
             jid = str(contact.jid)
    
    182 179
             self._check_if_omemo_capable(jid)
    
    183
    -        if self.is_omemo_groupchat(jid):
    
    184
    -            self.get_affiliation_list(jid)
    
    180
    +        if self._is_omemo_groupchat(jid):
    
    181
    +            self._get_affiliation_list(jid)
    
    185 182
     
    
    186 183
         def _on_republish_bundle(self,
    
    187 184
                                  _session: OMEMOSessionManager,
    
    ... ... @@ -198,52 +195,79 @@ def backend(self) -> OMEMOSessionManager:
    198 195
         def check_send_preconditions(self, contact: types.ChatContactT) -> bool:
    
    199 196
             jid = str(contact.jid)
    
    200 197
             if contact.is_groupchat:
    
    201
    -            if not self.is_omemo_groupchat(jid):
    
    202
    -                app.ged.raise_event(EncryptionInfo(
    
    203
    -                    account=contact.account,
    
    204
    -                    jid=contact.jid,
    
    205
    -                    message=EncryptionInfoMsg.BAD_OMEMO_CONFIG))
    
    198
    +            if not self._groupchat_pre_conditions_satisfied(contact):
    
    206 199
                     return False
    
    207 200
     
    
    208
    -            missing = True
    
    209
    -            for member_jid in self.backend.get_group_members(jid):
    
    210
    -                if not self.are_keys_missing(member_jid):
    
    211
    -                    missing = False
    
    212
    -            if missing:
    
    213
    -                self._log.info('%s => No Trusted Fingerprints for %s',
    
    214
    -                               contact.account, jid)
    
    215
    -                app.ged.raise_event(EncryptionInfo(
    
    216
    -                    account=contact.account,
    
    217
    -                    jid=contact.jid,
    
    218
    -                    message=EncryptionInfoMsg.NO_FINGERPRINTS))
    
    219
    -                return False
    
    220 201
             else:
    
    221
    -            # check if we have devices for the contact
    
    222
    -            if not self.backend.get_devices(jid, without_self=True):
    
    223
    -                self.request_devicelist(jid)
    
    224
    -                app.ged.raise_event(EncryptionInfo(
    
    225
    -                    account=contact.account,
    
    226
    -                    jid=contact.jid,
    
    227
    -                    message=EncryptionInfoMsg.QUERY_DEVICES))
    
    202
    +            if not self._chat_pre_conditions_satisfied(contact):
    
    228 203
                     return False
    
    229 204
     
    
    230 205
             if self.backend.get_identity_infos(jid,
    
    231 206
                                                only_active=True,
    
    232 207
                                                trust=OMEMOTrust.UNDECIDED):
    
    233
    -            self._log.info('%s => Undecided Fingerprints for %s',
    
    234
    -                           contact.account, jid)
    
    208
    +            self._log.info('Undecided keys for %s', jid)
    
    235 209
                 app.ged.raise_event(EncryptionInfo(
    
    236 210
                     account=contact.account,
    
    237 211
                     jid=contact.jid,
    
    238 212
                     message=EncryptionInfoMsg.UNDECIDED_FINGERPRINTS))
    
    239 213
                 return False
    
    240 214
     
    
    241
    -        self._log.debug('%s => Sending Message to %s',
    
    242
    -                        contact.account, jid)
    
    215
    +        self._log.debug('Sending message to %s', jid)
    
    243 216
     
    
    244 217
             return True
    
    245 218
     
    
    246
    -    def is_omemo_groupchat(self, room_jid: str) -> bool:
    
    219
    +    def _groupchat_pre_conditions_satisfied(
    
    220
    +            self,
    
    221
    +            contact: types.GroupchatContactT
    
    222
    +        ) -> bool:
    
    223
    +
    
    224
    +        jid = str(contact.jid)
    
    225
    +        if not self._is_omemo_groupchat(jid):
    
    226
    +            app.ged.raise_event(EncryptionInfo(
    
    227
    +                account=contact.account,
    
    228
    +                jid=contact.jid,
    
    229
    +                message=EncryptionInfoMsg.BAD_OMEMO_CONFIG))
    
    230
    +            return False
    
    231
    +
    
    232
    +        has_trusted_keys = False
    
    233
    +        for member_jid in self.backend.get_group_members(jid):
    
    234
    +            self._request_bundles_for_new_devices(member_jid)
    
    235
    +            if self._has_trusted_keys(member_jid):
    
    236
    +                has_trusted_keys = True
    
    237
    +
    
    238
    +        if not has_trusted_keys:
    
    239
    +            self._log.info('No trusted keys for %s', jid)
    
    240
    +            app.ged.raise_event(EncryptionInfo(
    
    241
    +                account=contact.account,
    
    242
    +                jid=contact.jid,
    
    243
    +                message=EncryptionInfoMsg.NO_FINGERPRINTS))
    
    244
    +            return False
    
    245
    +        return True
    
    246
    +
    
    247
    +    def _chat_pre_conditions_satisfied(
    
    248
    +            self,
    
    249
    +            contact: types.ChatContactT
    
    250
    +        ) -> bool:
    
    251
    +
    
    252
    +        jid = str(contact.jid)
    
    253
    +        if not self.backend.get_devices(jid, without_self=True):
    
    254
    +            self.request_devicelist(jid)
    
    255
    +            app.ged.raise_event(EncryptionInfo(
    
    256
    +                account=contact.account,
    
    257
    +                jid=contact.jid,
    
    258
    +                message=EncryptionInfoMsg.QUERY_DEVICES))
    
    259
    +            return False
    
    260
    +
    
    261
    +        if not self._has_trusted_keys(jid):
    
    262
    +            self._log.info('No trusted keys for %s', jid)
    
    263
    +            app.ged.raise_event(EncryptionInfo(
    
    264
    +                account=contact.account,
    
    265
    +                jid=contact.jid,
    
    266
    +                message=EncryptionInfoMsg.NO_FINGERPRINTS))
    
    267
    +            return False
    
    268
    +        return True
    
    269
    +
    
    270
    +    def _is_omemo_groupchat(self, room_jid: str) -> bool:
    
    247 271
             return room_jid in self._omemo_groupchats
    
    248 272
     
    
    249 273
         def encrypt_message(self, event: OutgoingMessage) -> bool:
    
    ... ... @@ -311,7 +335,7 @@ def _send_key_transport_message(self,
    311 335
                 return
    
    312 336
     
    
    313 337
             transport_message = get_key_transport_message(typ, jid, omemo_message)
    
    314
    -        self._log.info('Send key transport message %s (%s)', jid, devices)
    
    338
    +        self._log.info('Send key transport message to %s (%s)', jid, devices)
    
    315 339
             self._client.send_stanza(transport_message)
    
    316 340
     
    
    317 341
         def _message_received(self,
    
    ... ... @@ -352,7 +376,7 @@ def _message_received(self,
    352 376
                     raise NodeProcessed
    
    353 377
     
    
    354 378
                 if properties.omemo.payload not in self._muc_temp_store:
    
    355
    -                self._log.warning("Can't decrypt own GroupChat Message")
    
    379
    +                self._log.warning("Can't decrypt own group chat message")
    
    356 380
                     return
    
    357 381
     
    
    358 382
                 plaintext = self._muc_temp_store[properties.omemo.payload]
    
    ... ... @@ -397,7 +421,7 @@ def _process_muc_message(self,
    397 421
                 return contact.real_jid.bare
    
    398 422
     
    
    399 423
             self._log.error(
    
    400
    -            'Unable to find JID for group chat message from %s', resource)
    
    424
    +            'Unable to find jid for group chat message from %s', resource)
    
    401 425
             return None
    
    402 426
     
    
    403 427
         def _process_mam_message(self,
    
    ... ... @@ -408,7 +432,7 @@ def _process_mam_message(self,
    408 432
             if properties.from_muc:
    
    409 433
                 self._log.info('MUC MAM Message received')
    
    410 434
                 if properties.muc_user is None or properties.muc_user.jid is None:
    
    411
    -                self._log.warning('Received MAM Message which can '
    
    435
    +                self._log.warning('Received MAM message which can '
    
    412 436
                                       'not be mapped to a real jid')
    
    413 437
                     return None
    
    414 438
                 return properties.muc_user.jid.bare
    
    ... ... @@ -439,13 +463,13 @@ def _on_muc_user_presence(self,
    439 463
             else:
    
    440 464
                 self.backend.add_group_member(room, jid)
    
    441 465
     
    
    442
    -        if self.is_omemo_groupchat(room):
    
    443
    -            if not self.is_contact_in_roster(jid):
    
    466
    +        if self._is_omemo_groupchat(room):
    
    467
    +            if not self._is_contact_in_roster(jid):
    
    444 468
                     # Query Devicelists from JIDs not in our Roster
    
    445
    -                self._log.info('%s not in Roster, query devicelist...', jid)
    
    446
    -                self.request_devicelist(jid)
    
    469
    +                self._log.info('%s not in roster, query devicelist...', jid)
    
    470
    +                self._request_device_list_ttl(jid)
    
    447 471
     
    
    448
    -    def get_affiliation_list(self, room_jid: str) -> None:
    
    472
    +    def _get_affiliation_list(self, room_jid: str) -> None:
    
    449 473
             for affiliation in ('owner', 'admin', 'member'):
    
    450 474
                 self._nbxmpp('MUC').get_affiliation(
    
    451 475
                     room_jid,
    
    ... ... @@ -465,12 +489,11 @@ def _on_affiliations_received(self, task: Task) -> None:
    465 489
                 jid = str(user_jid)
    
    466 490
                 self.backend.add_group_member(room_jid, jid)
    
    467 491
     
    
    468
    -            if not self.is_contact_in_roster(jid):
    
    469
    -                # Query Devicelists from JIDs not in our Roster
    
    470
    -                self._log.info('%s not in Roster, query devicelist...', jid)
    
    471
    -                self.request_devicelist(jid)
    
    492
    +            if not self._is_contact_in_roster(jid):
    
    493
    +                self._log.info('%s not in roster, query devicelist...', jid)
    
    494
    +                self._request_device_list_ttl(jid)
    
    472 495
     
    
    473
    -    def is_contact_in_roster(self, jid: str) -> bool:
    
    496
    +    def _is_contact_in_roster(self, jid: str) -> bool:
    
    474 497
             if jid == self._own_jid:
    
    475 498
                 return True
    
    476 499
     
    
    ... ... @@ -490,60 +513,19 @@ def _check_if_omemo_capable(self, jid: str) -> None:
    490 513
                 self._log.info('OMEMO room removed due to config change: %s', jid)
    
    491 514
                 self._omemo_groupchats.discard(jid)
    
    492 515
     
    
    493
    -    def _check_for_missing_sessions(self, jid: str) -> None:
    
    494
    -        devices_without_session = self.backend.get_devices_without_sessions(jid)
    
    495
    -        for device_id in devices_without_session:
    
    496
    -            if device_id in self._device_bundle_querys:
    
    497
    -                continue
    
    498
    -            self._device_bundle_querys.append(device_id)
    
    499
    -            self.request_bundle(jid, device_id)
    
    500
    -
    
    501
    -    def are_keys_missing(self, contact_jid: str) -> bool:
    
    502
    -        ''' Checks if devicekeys are missing and queries the
    
    503
    -            bundles
    
    504
    -
    
    505
    -            Parameters
    
    506
    -            ----------
    
    507
    -            contact_jid : str
    
    508
    -                bare jid of the contact
    
    509
    -
    
    510
    -            Returns
    
    511
    -            -------
    
    512
    -            bool
    
    513
    -                Returns True if there are no trusted Fingerprints
    
    514
    -        '''
    
    515
    -
    
    516
    -        # Fetch Bundles of own other Devices
    
    517
    -        if self._own_jid not in self._query_for_bundles:
    
    518
    -
    
    519
    -            devices_without_session = self.backend.get_devices_without_sessions(
    
    520
    -                self._own_jid)
    
    521
    -
    
    522
    -            self._query_for_bundles.append(self._own_jid)
    
    523
    -
    
    524
    -            if devices_without_session:
    
    525
    -                for device_id in devices_without_session:
    
    526
    -                    self.request_bundle(self._own_jid, device_id)
    
    527
    -
    
    528
    -        # Fetch Bundles of contacts devices
    
    529
    -        if contact_jid not in self._query_for_bundles:
    
    530
    -
    
    531
    -            devices_without_session = self.backend.get_devices_without_sessions(
    
    532
    -                contact_jid)
    
    533
    -
    
    534
    -            self._query_for_bundles.append(contact_jid)
    
    535
    -
    
    536
    -            if devices_without_session:
    
    537
    -                for device_id in devices_without_session:
    
    538
    -                    self.request_bundle(contact_jid, device_id)
    
    516
    +    def _request_bundles_for_new_devices(self, jid_: str) -> None:
    
    517
    +        for jid in [jid_, self._own_jid]:
    
    518
    +            device_ids = self.backend.get_devices_without_sessions(jid)
    
    519
    +            for device_id in device_ids:
    
    520
    +                self._request_bundle_ttl(jid, device_id)
    
    539 521
     
    
    522
    +    def _has_trusted_keys(self, jid: str) -> bool:
    
    540 523
             if self.backend.get_identity_infos(
    
    541
    -                contact_jid,
    
    524
    +                jid,
    
    542 525
                     only_active=True,
    
    543 526
                     trust=[OMEMOTrust.VERIFIED, OMEMOTrust.BLIND]):
    
    544
    -            return False
    
    545
    -
    
    546
    -        return True
    
    527
    +            return True
    
    528
    +        return False
    
    547 529
     
    
    548 530
         def set_bundle(self, bundle: OMEMOBundle | None = None) -> None:
    
    549 531
             if bundle is None:
    
    ... ... @@ -551,11 +533,15 @@ def set_bundle(self, bundle: OMEMOBundle | None = None) -> None:
    551 533
             self._nbxmpp('OMEMO').set_bundle(bundle,
    
    552 534
                                              self.backend.get_our_device())
    
    553 535
     
    
    536
    +    @lru_cache_with_ttl(maxsize=512, ttl=7200)
    
    537
    +    def _request_bundle_ttl(self, jid: str, device_id: int) -> None:
    
    538
    +        self.request_bundle(jid, device_id)
    
    539
    +
    
    554 540
         @as_task
    
    555 541
         def request_bundle(self, jid: str, device_id: int):
    
    556 542
             _task = yield  # noqa: F841
    
    557 543
     
    
    558
    -        self._log.info('Fetch device bundle %s %s', device_id, jid)
    
    544
    +        self._log.info('Request device bundle %s %s', device_id, jid)
    
    559 545
     
    
    560 546
             bundle = yield self._nbxmpp('OMEMO').request_bundle(jid, device_id)
    
    561 547
     
    
    ... ... @@ -588,6 +574,10 @@ def clear_devicelist(self) -> None:
    588 574
                 self._own_jid, [self.backend.get_our_device()])
    
    589 575
             self.set_devicelist()
    
    590 576
     
    
    577
    +    @lru_cache_with_ttl(maxsize=512, ttl=7200)
    
    578
    +    def _request_device_list_ttl(self, jid: str) -> None:
    
    579
    +        self.request_devicelist(jid)
    
    580
    +
    
    591 581
         @as_task
    
    592 582
         def request_devicelist(self, jid: Optional[str] = None):
    
    593 583
             _task = yield  # noqa: F841
    
    ... ... @@ -595,10 +585,7 @@ def request_devicelist(self, jid: Optional[str] = None):
    595 585
             if jid is None:
    
    596 586
                 jid = self._own_jid
    
    597 587
     
    
    598
    -        if jid in self._query_for_devicelists:
    
    599
    -            return
    
    600
    -
    
    601
    -        self._query_for_devicelists.append(jid)
    
    588
    +        self._log.info('Request devicelist for %s', jid)
    
    602 589
     
    
    603 590
             devicelist = yield self._nbxmpp('OMEMO').request_devicelist(jid=jid)
    
    604 591
             if is_error(devicelist) or devicelist is None:
    
    ... ... @@ -634,16 +621,13 @@ def _process_devicelist_update(self,
    634 621
             # Pass a copy, we need the full list for potential set_devicelist()
    
    635 622
             self.backend.update_devicelist(jid, list(devicelist))
    
    636 623
     
    
    637
    -        if jid in self._query_for_devicelists:
    
    638
    -            self._query_for_devicelists.remove(jid)
    
    639
    -
    
    640 624
             if own_devices:
    
    641 625
                 if not self.backend.is_our_device_published():
    
    642 626
                     # Our own device_id is not in the list, it could be
    
    643 627
                     # overwritten by some other client
    
    644 628
                     self.set_devicelist(devicelist)
    
    645 629
     
    
    646
    -        self._check_for_missing_sessions(jid)
    
    630
    +        self._request_bundles_for_new_devices(jid)
    
    647 631
     
    
    648 632
         def _debug_print_stanza(self, stanza: Any) -> None:
    
    649 633
             stanzastr = '\n' + stanza.__str__(fancy=True)
    

  • gajim/common/util/decorators.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 Any
    
    16
    +
    
    17
    +from functools import lru_cache
    
    18
    +from functools import wraps
    
    19
    +from time import monotonic
    
    20
    +
    
    21
    +
    
    22
    +def lru_cache_with_ttl(maxsize: int,
    
    23
    +                       typed: bool = False,
    
    24
    +                       ttl: int = 2
    
    25
    +                       ) -> Any:
    
    26
    +    '''LRU-Cache with TTL'''
    
    27
    +
    
    28
    +    class Result:
    
    29
    +        __slots__ = ('value', 'timeout')
    
    30
    +
    
    31
    +        def __init__(self, value: Any, timeout: float) -> None:
    
    32
    +            self.value = value
    
    33
    +            self.timeout = timeout
    
    34
    +
    
    35
    +    def decorator(func: Any) -> Any:
    
    36
    +        @lru_cache(maxsize=maxsize, typed=typed)
    
    37
    +        def cached_func(*args: Any, **kwargs: Any) -> Result:
    
    38
    +            value = func(*args, **kwargs)
    
    39
    +            timeout = monotonic() + ttl
    
    40
    +            return Result(value, timeout)
    
    41
    +
    
    42
    +        @wraps(func)
    
    43
    +        def wrapper(*args: Any, **kwargs: Any) -> Any:
    
    44
    +            result = cached_func(*args, **kwargs)
    
    45
    +            if result.timeout < monotonic():
    
    46
    +                result.value = func(*args, **kwargs)
    
    47
    +                result.timeout = monotonic() + ttl
    
    48
    +            return result.value
    
    49
    +
    
    50
    +        return wrapper
    
    51
    +
    
    52
    +    return decorator