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

Commits:

9 changed files:

Changes:

  • gajim/application.py
    ... ... @@ -593,7 +593,7 @@ class GajimApplication(Gtk.Application):
    593 593
             if event.feature == nbxmpp.NS_VCARD:
    
    594 594
                 action = '%s-profile' % event.account
    
    595 595
                 self.lookup_action(action).set_enabled(True)
    
    596
    -        elif event.feature in (nbxmpp.NS_MAM_1, nbxmpp.NS_MAM_2):
    
    596
    +        elif event.feature == nbxmpp.NS_MAM_2:
    
    597 597
                 action = '%s-archive' % event.account
    
    598 598
                 self.lookup_action(action).set_enabled(True)
    
    599 599
             elif event.feature == nbxmpp.NS_PRIVACY:
    

  • gajim/common/logger.py
    ... ... @@ -1333,42 +1333,6 @@ class Logger:
    1333 1333
                 return True
    
    1334 1334
             return False
    
    1335 1335
     
    
    1336
    -    @timeit
    
    1337
    -    def search_for_duplicate(self, account, jid, timestamp, msg):
    
    1338
    -        """
    
    1339
    -        Check if a message is already in the `logs` table
    
    1340
    -
    
    1341
    -        :param account:     The account
    
    1342
    -
    
    1343
    -        :param jid:         The jid as string
    
    1344
    -
    
    1345
    -        :param timestamp:   The timestamp in UTC epoch
    
    1346
    -
    
    1347
    -        :param msg:         The message text
    
    1348
    -        """
    
    1349
    -
    
    1350
    -        # Add 10 seconds around the timestamp
    
    1351
    -        start_time = timestamp - 30
    
    1352
    -        end_time = timestamp + 30
    
    1353
    -
    
    1354
    -        account_id = self.get_account_id(account)
    
    1355
    -        log.debug('start: %s, end: %s, jid: %s, message: %s',
    
    1356
    -                  start_time, end_time, jid, msg)
    
    1357
    -
    
    1358
    -        sql = '''
    
    1359
    -            SELECT * FROM logs
    
    1360
    -            NATURAL JOIN jids WHERE jid = ? AND message = ? AND account_id = ?
    
    1361
    -            AND time BETWEEN ? AND ?
    
    1362
    -            '''
    
    1363
    -
    
    1364
    -        result = self._con.execute(
    
    1365
    -            sql, (jid, msg, account_id, start_time, end_time)).fetchone()
    
    1366
    -
    
    1367
    -        if result is not None:
    
    1368
    -            log.debug('Message already in DB')
    
    1369
    -            return True
    
    1370
    -        return False
    
    1371
    -
    
    1372 1336
         @timeit
    
    1373 1337
         def find_stanza_id(self, account, archive_jid, stanza_id, origin_id=None,
    
    1374 1338
                            groupchat=False):
    
    ... ... @@ -1679,6 +1643,23 @@ class Logger:
    1679 1643
             log.info('Set message archive info: %s %s', jid, kwargs)
    
    1680 1644
             self._timeout_commit()
    
    1681 1645
     
    
    1646
    +    @timeit
    
    1647
    +    def reset_archive_infos(self, jid):
    
    1648
    +        """
    
    1649
    +        Set archive infos
    
    1650
    +
    
    1651
    +        :param jid:                     The jid of the archive
    
    1652
    +
    
    1653
    +        """
    
    1654
    +        jid_id = self.get_jid_id(jid)
    
    1655
    +        sql = '''UPDATE last_archive_message
    
    1656
    +                 SET last_mam_id = NULL, oldest_mam_timestamp = NULL,
    
    1657
    +                 last_muc_timestamp = NULL
    
    1658
    +                 WHERE jid_id = ?'''
    
    1659
    +        self._con.execute(sql, (jid_id,))
    
    1660
    +        log.info('Reset message archive info: %s', jid)
    
    1661
    +        self._timeout_commit()
    
    1662
    +
    
    1682 1663
         @timeit
    
    1683 1664
         def _fill_disco_info_cache(self):
    
    1684 1665
             sql = '''SELECT disco_info as "disco_info [disco_info]",
    

  • gajim/common/modules/mam.py
    ... ... @@ -15,10 +15,12 @@
    15 15
     # XEP-0313: Message Archive Management
    
    16 16
     
    
    17 17
     import time
    
    18
    -from datetime import datetime, timedelta
    
    18
    +from datetime import datetime
    
    19
    +from datetime import timedelta
    
    19 20
     
    
    20 21
     import nbxmpp
    
    21 22
     from nbxmpp.util import generate_id
    
    23
    +from nbxmpp.util import is_error_result
    
    22 24
     from nbxmpp.structs import StanzaHandler
    
    23 25
     
    
    24 26
     from gajim.common import app
    
    ... ... @@ -36,6 +38,14 @@ from gajim.common.modules.base import BaseModule
    36 38
     
    
    37 39
     
    
    38 40
     class MAM(BaseModule):
    
    41
    +
    
    42
    +    _nbxmpp_extends = 'MAM'
    
    43
    +    _nbxmpp_methods = [
    
    44
    +        'request_preferences',
    
    45
    +        'set_preferences',
    
    46
    +        'make_query',
    
    47
    +    ]
    
    48
    +
    
    39 49
         def __init__(self, con):
    
    40 50
             BaseModule.__init__(self, con)
    
    41 51
     
    
    ... ... @@ -49,33 +59,30 @@ class MAM(BaseModule):
    49 59
             ]
    
    50 60
     
    
    51 61
             self.available = False
    
    52
    -        self.archiving_namespace = None
    
    53 62
             self._mam_query_ids = {}
    
    54 63
     
    
    55 64
             # Holds archive jids where catch up was successful
    
    56 65
             self._catch_up_finished = []
    
    57 66
     
    
    58 67
         def pass_disco(self, info):
    
    59
    -        if nbxmpp.NS_MAM_2 in info.features:
    
    60
    -            self.archiving_namespace = nbxmpp.NS_MAM_2
    
    61
    -        elif nbxmpp.NS_MAM_1 in info.features:
    
    62
    -            self.archiving_namespace = nbxmpp.NS_MAM_1
    
    63
    -        else:
    
    68
    +        if nbxmpp.NS_MAM_2 not in info.features:
    
    64 69
                 return
    
    65 70
     
    
    66 71
             self.available = True
    
    67
    -        self._log.info('Discovered MAM %s: %s',
    
    68
    -                       self.archiving_namespace, info.jid)
    
    72
    +        self._log.info('Discovered MAM: %s', info.jid)
    
    69 73
     
    
    70 74
             app.nec.push_incoming_event(
    
    71 75
                 NetworkEvent('feature-discovered',
    
    72 76
                              account=self._account,
    
    73
    -                         feature=self.archiving_namespace))
    
    77
    +                         feature=nbxmpp.NS_MAM_2))
    
    74 78
     
    
    75 79
         def reset_state(self):
    
    76 80
             self._mam_query_ids.clear()
    
    77 81
             self._catch_up_finished.clear()
    
    78 82
     
    
    83
    +    def _remove_query_id(self, jid):
    
    84
    +        self._mam_query_ids.pop(jid, None)
    
    85
    +
    
    79 86
         def is_catch_up_finished(self, jid):
    
    80 87
             return jid in self._catch_up_finished
    
    81 88
     
    
    ... ... @@ -112,27 +119,26 @@ class MAM(BaseModule):
    112 119
     
    
    113 120
             if properties.type.is_groupchat:
    
    114 121
                 archive_jid = properties.jid.getBare()
    
    122
    +            timestamp = properties.timestamp
    
    123
    +
    
    115 124
                 disco_info = app.logger.get_last_disco_info(archive_jid)
    
    116 125
                 if disco_info is None:
    
    117 126
                     # This is the case on MUC creation
    
    118 127
                     # After MUC configuration we receive a configuration change
    
    119 128
                     # message before we had the chance to disco the new MUC
    
    120 129
                     return
    
    121
    -            namespace = disco_info.mam_namespace
    
    122
    -            timestamp = properties.timestamp
    
    123
    -            if namespace is None:
    
    124
    -                # MUC History
    
    125
    -                app.logger.set_archive_infos(
    
    126
    -                    archive_jid,
    
    127
    -                    last_muc_timestamp=timestamp)
    
    130
    +
    
    131
    +            if disco_info.mam_namespace != nbxmpp.NS_MAM_2:
    
    128 132
                     return
    
    129 133
     
    
    130 134
             else:
    
    135
    +            if not self.available:
    
    136
    +                return
    
    137
    +
    
    131 138
                 archive_jid = self._con.get_own_jid().getBare()
    
    132
    -            namespace = self.archiving_namespace
    
    133 139
                 timestamp = None
    
    134 140
     
    
    135
    -        if properties.stanza_id is None or namespace != nbxmpp.NS_MAM_2:
    
    141
    +        if properties.stanza_id is None:
    
    136 142
                 return
    
    137 143
     
    
    138 144
             if not archive_jid == properties.stanza_id.by:
    
    ... ... @@ -179,16 +185,15 @@ class MAM(BaseModule):
    179 185
     
    
    180 186
             stanza_id, message_id = self._get_unique_id(properties)
    
    181 187
     
    
    182
    -        if properties.mam.is_ver_2:
    
    183
    -            # Search only with stanza-id for duplicates on mam:2
    
    184
    -            if app.logger.find_stanza_id(self._account,
    
    185
    -                                         str(properties.mam.archive),
    
    186
    -                                         stanza_id,
    
    187
    -                                         message_id,
    
    188
    -                                         groupchat=is_groupchat):
    
    189
    -                self._log.info('Found duplicate with stanza-id: %s, '
    
    190
    -                               'message-id: %s', stanza_id, message_id)
    
    191
    -                raise nbxmpp.NodeProcessed
    
    188
    +        # Search for duplicates
    
    189
    +        if app.logger.find_stanza_id(self._account,
    
    190
    +                                     str(properties.mam.archive),
    
    191
    +                                     stanza_id,
    
    192
    +                                     message_id,
    
    193
    +                                     groupchat=is_groupchat):
    
    194
    +            self._log.info('Found duplicate with stanza-id: %s, '
    
    195
    +                           'message-id: %s', stanza_id, message_id)
    
    196
    +            raise nbxmpp.NodeProcessed
    
    192 197
     
    
    193 198
             additional_data = AdditionalDataDict()
    
    194 199
             if properties.has_user_delay:
    
    ... ... @@ -223,12 +228,6 @@ class MAM(BaseModule):
    223 228
                     return
    
    224 229
                 stanza_id = message_id
    
    225 230
     
    
    226
    -        if properties.mam.namespace == nbxmpp.NS_MAM_1:
    
    227
    -            if app.logger.search_for_duplicate(
    
    228
    -                    self._account, with_, properties.mam.timestamp, msgtxt):
    
    229
    -                self._log.info('Found duplicate with fallback for mam:1')
    
    230
    -                return
    
    231
    -
    
    232 231
             app.logger.insert_into_logs(self._account,
    
    233 232
                                         with_,
    
    234 233
                                         properties.mam.timestamp,
    
    ... ... @@ -253,7 +252,7 @@ class MAM(BaseModule):
    253 252
             )
    
    254 253
     
    
    255 254
         def _is_valid_request(self, properties):
    
    256
    -        valid_id = self._mam_query_ids.get(str(properties.mam.archive), None)
    
    255
    +        valid_id = self._mam_query_ids.get(properties.mam.archive, None)
    
    257 256
             return valid_id == properties.mam.query_id
    
    258 257
     
    
    259 258
         def _get_query_id(self, jid):
    
    ... ... @@ -261,59 +260,8 @@ class MAM(BaseModule):
    261 260
             self._mam_query_ids[jid] = query_id
    
    262 261
             return query_id
    
    263 262
     
    
    264
    -    def _parse_iq(self, stanza):
    
    265
    -        if not nbxmpp.isResultNode(stanza):
    
    266
    -            self._log.error('Error on MAM query: %s', stanza.getError())
    
    267
    -            raise InvalidMamIQ
    
    268
    -
    
    269
    -        fin = stanza.getTag('fin')
    
    270
    -        if fin is None:
    
    271
    -            self._log.error('Malformed MAM query result received: %s', stanza)
    
    272
    -            raise InvalidMamIQ
    
    273
    -
    
    274
    -        set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM)
    
    275
    -        if set_ is None:
    
    276
    -            self._log.error(
    
    277
    -                'Malformed MAM query result received (no "set" Node): %s',
    
    278
    -                stanza)
    
    279
    -            raise InvalidMamIQ
    
    280
    -        return fin, set_
    
    281
    -
    
    282
    -    def _get_from_jid(self, stanza):
    
    283
    -        jid = stanza.getFrom()
    
    284
    -        if jid is None:
    
    285
    -            # No from means, iq from our own archive
    
    286
    -            jid = self._con.get_own_jid().getStripped()
    
    287
    -        else:
    
    288
    -            jid = jid.getStripped()
    
    289
    -        return jid
    
    290
    -
    
    291
    -    def request_archive_count(self, start_date, end_date):
    
    292
    -        jid = self._con.get_own_jid().getStripped()
    
    293
    -        self._log.info('Request archive count from: %s', jid)
    
    294
    -        query_id = self._get_query_id(jid)
    
    295
    -        query = self._get_archive_query(
    
    296
    -            query_id, start=start_date, end=end_date, max_=0)
    
    297
    -        self._con.connection.SendAndCallForResponse(
    
    298
    -            query, self._received_count, {'query_id': query_id})
    
    299
    -        return query_id
    
    300
    -
    
    301
    -    def _received_count(self, _nbxmpp_client, stanza, query_id):
    
    302
    -        try:
    
    303
    -            _, set_ = self._parse_iq(stanza)
    
    304
    -        except InvalidMamIQ:
    
    305
    -            return
    
    306
    -
    
    307
    -        jid = self._get_from_jid(stanza)
    
    308
    -        self._mam_query_ids.pop(jid)
    
    309
    -
    
    310
    -        count = set_.getTagData('count')
    
    311
    -        self._log.info('Received archive count: %s', count)
    
    312
    -        app.nec.push_incoming_event(ArchivingCountReceived(
    
    313
    -            None, query_id=query_id, count=count))
    
    314
    -
    
    315 263
         def request_archive_on_signin(self):
    
    316
    -        own_jid = self._con.get_own_jid().getStripped()
    
    264
    +        own_jid = self._con.get_own_jid().getBare()
    
    317 265
     
    
    318 266
             if own_jid in self._mam_query_ids:
    
    319 267
                 self._log.warning('MAM request for %s already running', own_jid)
    
    ... ... @@ -331,49 +279,53 @@ class MAM(BaseModule):
    331 279
                     app.config.del_per('accounts', self._account, 'last_mam_id')
    
    332 280
     
    
    333 281
             start_date = None
    
    334
    -        query_id = self._get_query_id(own_jid)
    
    282
    +        queryid = self._get_query_id(own_jid)
    
    335 283
             if mam_id:
    
    336 284
                 self._log.info('MAM query after: %s', mam_id)
    
    337
    -            query = self._get_archive_query(query_id, after=mam_id)
    
    285
    +
    
    338 286
             else:
    
    339 287
                 # First Start, we request the last week
    
    340 288
                 start_date = datetime.utcnow() - timedelta(days=7)
    
    341 289
                 self._log.info('First start: query archive start: %s', start_date)
    
    342
    -            query = self._get_archive_query(query_id, start=start_date)
    
    290
    +
    
    291
    +        self._nbxmpp('MAM').make_query(own_jid,
    
    292
    +                                       queryid,
    
    293
    +                                       after=mam_id,
    
    294
    +                                       start=start_date,
    
    295
    +                                       callback=self._result_finished,
    
    296
    +                                       user_data={'queryid': queryid,
    
    297
    +                                                  'start': start_date,
    
    298
    +                                                  'groupchat': False})
    
    343 299
     
    
    344 300
             if own_jid in self._catch_up_finished:
    
    345 301
                 self._catch_up_finished.remove(own_jid)
    
    346
    -        self._send_archive_query(query, query_id, start_date)
    
    347 302
     
    
    348 303
         def request_archive_on_muc_join(self, jid):
    
    349 304
             archive = app.logger.get_archive_infos(jid)
    
    350 305
             threshold = get_sync_threshold(jid, archive)
    
    351 306
             self._log.info('Threshold for %s: %s', jid, threshold)
    
    352
    -        query_id = self._get_query_id(jid)
    
    307
    +
    
    308
    +        mam_id = None
    
    353 309
             start_date = None
    
    354 310
             if archive is None or archive.last_mam_id is None:
    
    355
    -            # First Start, we dont request history
    
    356
    -            # Depending on what a MUC saves, there could be thousands
    
    357
    -            # of Messages even in just one day.
    
    311
    +            # First join
    
    358 312
                 start_date = datetime.utcnow() - timedelta(days=1)
    
    359 313
                 self._log.info('First join: query archive %s from: %s',
    
    360 314
                                jid, start_date)
    
    361
    -            query = self._get_archive_query(
    
    362
    -                query_id, jid=jid, start=start_date)
    
    363 315
     
    
    364 316
             elif threshold == SyncThreshold.NO_THRESHOLD:
    
    365 317
                 # Not our first join and no threshold set
    
    318
    +
    
    319
    +            mam_id = archive.last_mam_id
    
    366 320
                 self._log.info('Request from archive: %s, after mam-id %s',
    
    367 321
                                jid, archive.last_mam_id)
    
    368
    -            query = self._get_archive_query(
    
    369
    -                query_id, jid=jid, after=archive.last_mam_id)
    
    370 322
     
    
    371 323
             else:
    
    372 324
                 # Not our first join, check how much time elapsed since our
    
    373 325
                 # last join and check against threshold
    
    374 326
                 last_timestamp = archive.last_muc_timestamp
    
    375 327
                 if last_timestamp is None:
    
    376
    -                self._log.info('No last muc timestamp found ( mam:1? )')
    
    328
    +                self._log.info('No last muc timestamp found')
    
    377 329
                     last_timestamp = 0
    
    378 330
     
    
    379 331
                 last = datetime.utcfromtimestamp(float(last_timestamp))
    
    ... ... @@ -383,242 +335,138 @@ class MAM(BaseModule):
    383 335
                     self._log.info('Too much time elapsed since last join, '
    
    384 336
                                    'request from: %s, threshold: %s',
    
    385 337
                                    start_date, threshold)
    
    386
    -                query = self._get_archive_query(
    
    387
    -                    query_id, jid=jid, start=start_date)
    
    338
    +
    
    388 339
                 else:
    
    389 340
                     # Request from last mam-id
    
    341
    +                mam_id = archive.last_mam_id
    
    390 342
                     self._log.info('Request from archive %s after %s:',
    
    391 343
                                    jid, archive.last_mam_id)
    
    392
    -                query = self._get_archive_query(
    
    393
    -                    query_id, jid=jid, after=archive.last_mam_id)
    
    394 344
     
    
    395 345
             if jid in self._catch_up_finished:
    
    396 346
                 self._catch_up_finished.remove(jid)
    
    397
    -        self._send_archive_query(query, query_id, start_date, groupchat=True)
    
    398
    -
    
    399
    -    def _send_archive_query(self, query, query_id, start_date=None,
    
    400
    -                            groupchat=False):
    
    401
    -        self._con.connection.SendAndCallForResponse(
    
    402
    -            query, self._result_finished, {'query_id': query_id,
    
    403
    -                                           'start_date': start_date,
    
    404
    -                                           'groupchat': groupchat})
    
    405
    -
    
    406
    -    def _result_finished(self, _nbxmpp_client, stanza, query_id,
    
    407
    -                         start_date, groupchat):
    
    408
    -        try:
    
    409
    -            fin, set_ = self._parse_iq(stanza)
    
    410
    -        except InvalidMamIQ:
    
    411
    -            return
    
    412 347
     
    
    413
    -        jid = self._get_from_jid(stanza)
    
    348
    +        queryid = self._get_query_id(jid)
    
    349
    +        self._nbxmpp('MAM').make_query(jid,
    
    350
    +                                       queryid,
    
    351
    +                                       after=mam_id,
    
    352
    +                                       start=start_date,
    
    353
    +                                       callback=self._result_finished,
    
    354
    +                                       user_data={'queryid': queryid,
    
    355
    +                                                  'start': start_date,
    
    356
    +                                                  'groupchat': True})
    
    357
    +
    
    358
    +    def _result_finished(self, result, user_data):
    
    359
    +        queryid, start_date, groupchat = user_data.values()
    
    360
    +        self._remove_query_id(result.jid)
    
    361
    +
    
    362
    +        if is_error_result(result):
    
    363
    +            if result.condition == 'item-not-found':
    
    364
    +                app.logger.reset_archive_infos(result.jid)
    
    365
    +                if groupchat:
    
    366
    +                    self.request_archive_on_muc_join(result.jid)
    
    367
    +                else:
    
    368
    +                    self.request_archive_on_signin()
    
    369
    +            return
    
    414 370
     
    
    415
    -        last = set_.getTagData('last')
    
    416
    -        if last is None:
    
    417
    -            self._log.info('End of MAM query, no items retrieved')
    
    418
    -            self._catch_up_finished.append(jid)
    
    419
    -            self._mam_query_ids.pop(jid)
    
    371
    +        if result.rsm.last is None:
    
    372
    +            self._log.info('MAM catchup finished: %s', result.jid)
    
    373
    +            self._catch_up_finished.append(result.jid)
    
    420 374
                 return
    
    421 375
     
    
    422
    -        complete = fin.getAttr('complete')
    
    423
    -        if complete != 'true':
    
    424
    -            app.logger.set_archive_infos(jid, last_mam_id=last)
    
    425
    -            self._mam_query_ids.pop(jid)
    
    426
    -            query_id = self._get_query_id(jid)
    
    427
    -            query = self._get_archive_query(query_id, jid=jid, after=last)
    
    428
    -            self._send_archive_query(query, query_id, groupchat=groupchat)
    
    376
    +        if not result.complete:
    
    377
    +            app.logger.set_archive_infos(result.jid,
    
    378
    +                                         last_mam_id=result.rsm.last)
    
    379
    +            queryid = self._get_query_id(result.jid)
    
    380
    +
    
    381
    +            self._nbxmpp('MAM').make_query(result.jid,
    
    382
    +                                           queryid,
    
    383
    +                                           after=result.rsm.last,
    
    384
    +                                           callback=self._result_finished,
    
    385
    +                                           user_data={'queryid': queryid,
    
    386
    +                                                      'start': None,
    
    387
    +                                                      'groupchat': groupchat})
    
    388
    +
    
    429 389
             else:
    
    430
    -            self._mam_query_ids.pop(jid)
    
    431
    -            app.logger.set_archive_infos(
    
    432
    -                jid, last_mam_id=last, last_muc_timestamp=time.time())
    
    390
    +            app.logger.set_archive_infos(result.jid,
    
    391
    +                                         last_mam_id=result.rsm.last,
    
    392
    +                                         last_muc_timestamp=time.time())
    
    393
    +
    
    433 394
                 if start_date is not None and not groupchat:
    
    434 395
                     # Record the earliest timestamp we request from
    
    435 396
                     # the account archive. For the account archive we only
    
    436 397
                     # set start_date at the very first request.
    
    437 398
                     app.logger.set_archive_infos(
    
    438
    -                    jid, oldest_mam_timestamp=start_date.timestamp())
    
    399
    +                    result.jid,
    
    400
    +                    oldest_mam_timestamp=start_date.timestamp())
    
    401
    +
    
    402
    +            self._catch_up_finished.append(result.jid)
    
    403
    +            self._log.info('End of MAM query, last mam id: %s',
    
    404
    +                           result.rsm.last)
    
    439 405
     
    
    440
    -            self._catch_up_finished.append(jid)
    
    441
    -            self._log.info('End of MAM query, last mam id: %s', last)
    
    406
    +    def request_archive_interval(self,
    
    407
    +                                 start_date,
    
    408
    +                                 end_date,
    
    409
    +                                 after=None,
    
    410
    +                                 queryid=None):
    
    442 411
     
    
    443
    -    def request_archive_interval(self, start_date, end_date, after=None,
    
    444
    -                                 query_id=None):
    
    445
    -        jid = self._con.get_own_jid().getStripped()
    
    446 412
             if after is None:
    
    447
    -            self._log.info('Request intervall from %s to %s from %s',
    
    448
    -                           start_date, end_date, jid)
    
    413
    +            self._log.info('Request interval from %s to %s',
    
    414
    +                           start_date, end_date)
    
    449 415
             else:
    
    450
    -            self._log.info('Query page after %s from %s', after, jid)
    
    451
    -        if query_id is None:
    
    452
    -            query_id = self._get_query_id(jid)
    
    453
    -        self._mam_query_ids[jid] = query_id
    
    454
    -        query = self._get_archive_query(query_id, start=start_date,
    
    455
    -                                        end=end_date, after=after, max_=30)
    
    456
    -
    
    457
    -        self._con.connection.SendAndCallForResponse(
    
    458
    -            query, self._intervall_result, {'query_id': query_id,
    
    459
    -                                            'start_date': start_date,
    
    460
    -                                            'end_date': end_date})
    
    461
    -        return query_id
    
    462
    -
    
    463
    -    def _intervall_result(self, _nbxmpp_client, stanza, query_id,
    
    464
    -                          start_date, end_date):
    
    465
    -        try:
    
    466
    -            fin, set_ = self._parse_iq(stanza)
    
    467
    -        except InvalidMamIQ:
    
    416
    +            self._log.info('Query page after %s', after)
    
    417
    +
    
    418
    +        jid = self._con.get_own_jid().getBare()
    
    419
    +        if queryid is None:
    
    420
    +            queryid = self._get_query_id(jid)
    
    421
    +        self._mam_query_ids[jid] = queryid
    
    422
    +
    
    423
    +        self._nbxmpp('MAM').make_query(jid,
    
    424
    +                                       queryid,
    
    425
    +                                       after=after,
    
    426
    +                                       start=start_date,
    
    427
    +                                       end=end_date,
    
    428
    +                                       callback=self._on_interval_result,
    
    429
    +                                       user_data={'queryid': queryid,
    
    430
    +                                                  'start_date': start_date,
    
    431
    +                                                  'end_date': end_date})
    
    432
    +        return queryid
    
    433
    +
    
    434
    +    def _on_interval_result(self, result, user_data):
    
    435
    +        queryid, start_date, end_date = user_data.values()
    
    436
    +        self._remove_query_id(result.jid)
    
    437
    +
    
    438
    +        if is_error_result(result):
    
    468 439
                 return
    
    469 440
     
    
    470
    -        jid = self._get_from_jid(stanza)
    
    471
    -        self._mam_query_ids.pop(jid)
    
    472 441
             if start_date:
    
    473 442
                 timestamp = start_date.timestamp()
    
    474 443
             else:
    
    475 444
                 timestamp = ArchiveState.ALL
    
    476 445
     
    
    477
    -        last = set_.getTagData('last')
    
    478
    -        if last is None:
    
    479
    -            app.nec.push_incoming_event(ArchivingIntervalFinished(
    
    480
    -                None, query_id=query_id))
    
    481
    -            app.logger.set_archive_infos(
    
    482
    -                jid, oldest_mam_timestamp=timestamp)
    
    446
    +        if result.rsm.last is None:
    
    447
    +            app.nec.push_incoming_event(NetworkEvent(
    
    448
    +                'archiving-interval-finished',
    
    449
    +                account=self._account,
    
    450
    +                query_id=queryid))
    
    451
    +
    
    452
    +            app.logger.set_archive_infos(result.jid,
    
    453
    +                                         oldest_mam_timestamp=timestamp)
    
    483 454
                 self._log.info('End of MAM request, no items retrieved')
    
    484 455
                 return
    
    485 456
     
    
    486
    -        complete = fin.getAttr('complete')
    
    487
    -        if complete != 'true':
    
    488
    -            self.request_archive_interval(start_date, end_date, last, query_id)
    
    457
    +        if not result.complete:
    
    458
    +            self.request_archive_interval(start_date,
    
    459
    +                                          end_date,
    
    460
    +                                          result.rsm.last,
    
    461
    +                                          queryid)
    
    489 462
             else:
    
    490 463
                 self._log.info('Request finished')
    
    491
    -            app.logger.set_archive_infos(
    
    492
    -                jid, oldest_mam_timestamp=timestamp)
    
    493
    -            app.nec.push_incoming_event(ArchivingIntervalFinished(
    
    494
    -                None, query_id=query_id))
    
    495
    -
    
    496
    -    def _get_archive_query(self, query_id, jid=None, start=None, end=None,
    
    497
    -                           with_=None, after=None, max_=70):
    
    498
    -        # Muc archive query?
    
    499
    -        disco_info = app.logger.get_last_disco_info(jid)
    
    500
    -        if disco_info is None:
    
    501
    -            # Query to our own archive
    
    502
    -            namespace = self.archiving_namespace
    
    503
    -        else:
    
    504
    -            namespace = disco_info.mam_namespace
    
    505
    -
    
    506
    -        iq = nbxmpp.Iq('set', to=jid)
    
    507
    -        query = iq.addChild('query', namespace=namespace)
    
    508
    -        form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
    
    509
    -        field = nbxmpp.DataField(typ='hidden',
    
    510
    -                                 name='FORM_TYPE',
    
    511
    -                                 value=namespace)
    
    512
    -        form.addChild(node=field)
    
    513
    -        if start:
    
    514
    -            field = nbxmpp.DataField(
    
    515
    -                typ='text-single',
    
    516
    -                name='start',
    
    517
    -                value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
    
    518
    -            form.addChild(node=field)
    
    519
    -        if end:
    
    520
    -            field = nbxmpp.DataField(typ='text-single',
    
    521
    -                                     name='end',
    
    522
    -                                     value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
    
    523
    -            form.addChild(node=field)
    
    524
    -        if with_:
    
    525
    -            field = nbxmpp.DataField(typ='jid-single',
    
    526
    -                                     name='with',
    
    527
    -                                     value=with_)
    
    528
    -            form.addChild(node=field)
    
    529
    -
    
    530
    -        set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
    
    531
    -        set_.setTagData('max', max_)
    
    532
    -        if after:
    
    533
    -            set_.setTagData('after', after)
    
    534
    -        query.setAttr('queryid', query_id)
    
    535
    -        return iq
    
    536
    -
    
    537
    -    def request_mam_preferences(self):
    
    538
    -        self._log.info('Request MAM preferences')
    
    539
    -        iq = nbxmpp.Iq('get', self.archiving_namespace)
    
    540
    -        iq.setQuery('prefs')
    
    541
    -        self._con.connection.SendAndCallForResponse(
    
    542
    -            iq, self._preferences_received)
    
    543
    -
    
    544
    -    def _preferences_received(self, _nbxmpp_client, stanza):
    
    545
    -        if not nbxmpp.isResultNode(stanza):
    
    546
    -            self._log.info('Error: %s', stanza.getError())
    
    547
    -            app.nec.push_incoming_event(MAMPreferenceError(
    
    548
    -                None, conn=self._con, error=stanza.getError()))
    
    549
    -            return
    
    550
    -
    
    551
    -        self._log.info('Received MAM preferences')
    
    552
    -        prefs = stanza.getTag('prefs', namespace=self.archiving_namespace)
    
    553
    -        if prefs is None:
    
    554
    -            self._log.error('Malformed stanza (no prefs node): %s', stanza)
    
    555
    -            return
    
    556
    -
    
    557
    -        rules = []
    
    558
    -        default = prefs.getAttr('default')
    
    559
    -        for item in prefs.getTag('always').getTags('jid'):
    
    560
    -            rules.append((item.getData(), 'Always'))
    
    561
    -
    
    562
    -        for item in prefs.getTag('never').getTags('jid'):
    
    563
    -            rules.append((item.getData(), 'Never'))
    
    564
    -
    
    565
    -        app.nec.push_incoming_event(MAMPreferenceReceived(
    
    566
    -            None, conn=self._con, rules=rules, default=default))
    
    567
    -
    
    568
    -    def set_mam_preferences(self, rules, default):
    
    569
    -        iq = nbxmpp.Iq(typ='set')
    
    570
    -        prefs = iq.addChild(name='prefs',
    
    571
    -                            namespace=self.archiving_namespace,
    
    572
    -                            attrs={'default': default})
    
    573
    -        always = prefs.addChild(name='always')
    
    574
    -        never = prefs.addChild(name='never')
    
    575
    -        for item in rules:
    
    576
    -            jid, archive = item
    
    577
    -            if archive:
    
    578
    -                always.addChild(name='jid').setData(jid)
    
    579
    -            else:
    
    580
    -                never.addChild(name='jid').setData(jid)
    
    581
    -
    
    582
    -        self._con.connection.SendAndCallForResponse(
    
    583
    -            iq, self._preferences_saved)
    
    584
    -
    
    585
    -    def _preferences_saved(self, _nbxmpp_client, stanza):
    
    586
    -        if not nbxmpp.isResultNode(stanza):
    
    587
    -            self._log.info('Error: %s', stanza.getError())
    
    588
    -            app.nec.push_incoming_event(MAMPreferenceError(
    
    589
    -                None, conn=self._con, error=stanza.getError()))
    
    590
    -        else:
    
    591
    -            self._log.info('Preferences saved')
    
    592
    -            app.nec.push_incoming_event(
    
    593
    -                MAMPreferenceSaved(None, conn=self._con))
    
    594
    -
    
    595
    -
    
    596
    -class MAMPreferenceError(NetworkIncomingEvent):
    
    597
    -    name = 'mam-prefs-error'
    
    598
    -
    
    599
    -
    
    600
    -class MAMPreferenceReceived(NetworkIncomingEvent):
    
    601
    -    name = 'mam-prefs-received'
    
    602
    -
    
    603
    -
    
    604
    -class MAMPreferenceSaved(NetworkIncomingEvent):
    
    605
    -    name = 'mam-prefs-saved'
    
    606
    -
    
    607
    -
    
    608
    -class ArchivingCountReceived(NetworkIncomingEvent):
    
    609
    -    name = 'archiving-count-received'
    
    610
    -
    
    611
    -
    
    612
    -class ArchivingIntervalFinished(NetworkIncomingEvent):
    
    613
    -    name = 'archiving-interval-finished'
    
    614
    -
    
    615
    -
    
    616
    -class ArchivingErrorReceived(NetworkIncomingEvent):
    
    617
    -    name = 'archiving-error-received'
    
    618
    -
    
    619
    -
    
    620
    -class InvalidMamIQ(Exception):
    
    621
    -    pass
    
    464
    +            app.logger.set_archive_infos(result.jid,
    
    465
    +                                         oldest_mam_timestamp=timestamp)
    
    466
    +            app.nec.push_incoming_event(NetworkEvent(
    
    467
    +                'archiving-interval-finished',
    
    468
    +                account=self._account,
    
    469
    +                query_id=queryid))
    
    622 470
     
    
    623 471
     
    
    624 472
     def get_instance(*args, **kwargs):
    

  • gajim/common/modules/message.py
    ... ... @@ -266,15 +266,16 @@ class Message(BaseModule):
    266 266
             if properties.type.is_groupchat:
    
    267 267
                 disco_info = app.logger.get_last_disco_info(
    
    268 268
                     properties.jid.getBare())
    
    269
    -            namespace = disco_info.mam_namespace
    
    269
    +
    
    270
    +            if disco_info.mam_namespace != nbxmpp.NS_MAM_2:
    
    271
    +                return None, None
    
    272
    +
    
    270 273
                 archive = properties.jid
    
    271 274
             else:
    
    272
    -            namespace = self._con.get_module('MAM').archiving_namespace
    
    273
    -            archive = self._con.get_own_jid()
    
    275
    +            if not self._con.get_module('MAM').available:
    
    276
    +                return None, None
    
    274 277
     
    
    275
    -        if namespace != nbxmpp.NS_MAM_2:
    
    276
    -            # Only mam:2 ensures valid stanza-id
    
    277
    -            return None, None
    
    278
    +            archive = self._con.get_own_jid()
    
    278 279
     
    
    279 280
             if archive.bareMatch(properties.stanza_id.by):
    
    280 281
                 return properties.stanza_id.id, None
    

  • gajim/common/modules/muc.py
    ... ... @@ -17,9 +17,6 @@
    17 17
     
    
    18 18
     import time
    
    19 19
     import logging
    
    20
    -from datetime import datetime
    
    21
    -from datetime import timedelta
    
    22
    -from datetime import timezone
    
    23 20
     
    
    24 21
     import nbxmpp
    
    25 22
     from nbxmpp.const import InviteType
    
    ... ... @@ -35,10 +32,8 @@ from gajim.common import helpers
    35 32
     from gajim.common import ged
    
    36 33
     from gajim.common.const import KindConstant
    
    37 34
     from gajim.common.const import MUCJoinedState
    
    38
    -from gajim.common.const import SyncThreshold
    
    39 35
     from gajim.common.helpers import AdditionalDataDict
    
    40 36
     from gajim.common.helpers import get_default_muc_config
    
    41
    -from gajim.common.helpers import get_sync_threshold
    
    42 37
     from gajim.common.helpers import to_user_string
    
    43 38
     from gajim.common.helpers import event_filter
    
    44 39
     from gajim.common import idle
    
    ... ... @@ -185,7 +180,7 @@ class MUC(BaseModule):
    185 180
                 status=self._con.status_message)
    
    186 181
     
    
    187 182
             muc_x = presence.setTag(nbxmpp.NS_MUC + ' x')
    
    188
    -        self._add_history_query(muc_x, str(muc_data.jid))
    
    183
    +        muc_x.setTag('history', {'maxchars': '0'})
    
    189 184
     
    
    190 185
             if muc_data.password is not None:
    
    191 186
                 muc_x.setTagData('password', muc_data.password)
    
    ... ... @@ -340,32 +335,6 @@ class MUC(BaseModule):
    340 335
                 show=self._con.status,
    
    341 336
                 status=self._con.status_message)
    
    342 337
     
    
    343
    -    def _add_history_query(self, muc_x, room_jid):
    
    344
    -        disco_info = app.logger.get_last_disco_info(room_jid)
    
    345
    -        if disco_info.has_mam:
    
    346
    -            # The room is MAM capable dont get MUC History
    
    347
    -            muc_x.setTag('history', {'maxchars': '0'})
    
    348
    -        else:
    
    349
    -            # Request MUC History (not MAM)
    
    350
    -            archive = app.logger.get_archive_infos(room_jid)
    
    351
    -            threshold = get_sync_threshold(room_jid, archive)
    
    352
    -
    
    353
    -            since_epoch = 0
    
    354
    -            if archive is not None and archive.last_muc_timestamp is not None:
    
    355
    -                since_epoch = float(archive.last_muc_timestamp)
    
    356
    -
    
    357
    -            since_date = datetime.fromtimestamp(since_epoch, timezone.utc)
    
    358
    -            if threshold != SyncThreshold.NO_THRESHOLD:
    
    359
    -                now = datetime.now(timezone.utc)
    
    360
    -                threshold_date = now - timedelta(days=threshold)
    
    361
    -                since_date = max(threshold_date, since_date)
    
    362
    -
    
    363
    -            date_string = since_date.strftime('%Y-%m-%dT%H:%M:%SZ')
    
    364
    -            muc_x.setTag('history', {'since': date_string})
    
    365
    -            self._log.info('Request MUC history since: %s (%s)',
    
    366
    -                           date_string, since_date.timestamp())
    
    367
    -            self._log.info('Threshold for %s: %s', room_jid, threshold)
    
    368
    -
    
    369 338
         def _on_error_presence(self, _con, _stanza, properties):
    
    370 339
             room_jid = properties.jid.getBare()
    
    371 340
             muc_data = self._manager.get(room_jid)
    

  • gajim/gtk/groupchat_info.py
    ... ... @@ -18,7 +18,6 @@ from gi.repository import Gdk
    18 18
     from gi.repository import GLib
    
    19 19
     from gi.repository import Gtk
    
    20 20
     
    
    21
    -from nbxmpp.protocol import NS_MAM_1
    
    22 21
     from nbxmpp.protocol import NS_MAM_2
    
    23 22
     
    
    24 23
     from gajim.common import app
    
    ... ... @@ -206,8 +205,7 @@ class GroupChatInfoScrolled(Gtk.ScrolledWindow):
    206 205
                 grid.remove_row(row)
    
    207 206
             features = list(features)
    
    208 207
     
    
    209
    -        has_mam = NS_MAM_2 in features or NS_MAM_1 in features
    
    210
    -        if has_mam:
    
    208
    +        if NS_MAM_2 in features:
    
    211 209
                 features.append('mam')
    
    212 210
     
    
    213 211
             row = 10
    

  • gajim/gtk/history_sync.py
    ... ... @@ -19,6 +19,8 @@ from datetime import datetime, timedelta
    19 19
     from gi.repository import Gtk
    
    20 20
     from gi.repository import GLib
    
    21 21
     
    
    22
    +from nbxmpp.util import is_error_result
    
    23
    +
    
    22 24
     from gajim.common import app
    
    23 25
     from gajim.common import ged
    
    24 26
     from gajim.common.i18n import _
    
    ... ... @@ -139,18 +141,25 @@ class HistorySyncAssistant(Gtk.Assistant, EventHelper):
    139 141
             log.info('Start: %s', self.start)
    
    140 142
             log.info('End: %s', self.end)
    
    141 143
     
    
    142
    -        self.query_id = self.con.get_module('MAM').request_archive_count(
    
    143
    -            self.start, self.end)
    
    144
    +        jid = self.con.get_own_jid().getBare()
    
    144 145
     
    
    145
    -    def _received_count(self, event):
    
    146
    -        if event.query_id != self.query_id:
    
    146
    +        self.con.get_module('MAM').make_query(jid,
    
    147
    +                                              start=self.start,
    
    148
    +                                              end=self.end,
    
    149
    +                                              max_=0,
    
    150
    +                                              callback=self._received_count)
    
    151
    +
    
    152
    +
    
    153
    +    def _received_count(self, result):
    
    154
    +        if is_error_result(result):
    
    147 155
                 return
    
    148 156
     
    
    149
    -        if event.count is not None:
    
    150
    -            self.download_history.count = int(event.count)
    
    157
    +        if result.rsm.count is not None:
    
    158
    +            self.download_history.count = int(result.rsm.count)
    
    151 159
             self.query_id = self.con.get_module('MAM').request_archive_interval(
    
    152 160
                 self.start, self.end)
    
    153 161
     
    
    162
    +    @event_filter(['account'])
    
    154 163
         def _received_finished(self, event):
    
    155 164
             if event.query_id != self.query_id:
    
    156 165
                 return
    

  • gajim/gtk/mam_preferences.py
    ... ... @@ -17,12 +17,14 @@ import logging
    17 17
     from gi.repository import Gtk
    
    18 18
     from gi.repository import Gdk
    
    19 19
     
    
    20
    +from nbxmpp.util import is_error_result
    
    21
    +
    
    20 22
     from gajim.common import app
    
    21
    -from gajim.common import ged
    
    22 23
     from gajim.common.i18n import _
    
    23 24
     
    
    24 25
     from gajim.gtk.util import get_builder
    
    25 26
     from gajim.gtk.util import EventHelper
    
    27
    +from gajim.gtk.util import ensure_not_destroyed
    
    26 28
     from gajim.gtk.dialogs import DialogButton
    
    27 29
     from gajim.gtk.dialogs import NewConfirmationDialog
    
    28 30
     from gajim.gtk.dialogs import InformationDialog
    
    ... ... @@ -43,6 +45,7 @@ class MamPreferences(Gtk.ApplicationWindow, EventHelper):
    43 45
     
    
    44 46
             self.account = account
    
    45 47
             self._con = app.connections[account]
    
    48
    +        self._destroyed = False
    
    46 49
     
    
    47 50
             self._ui = get_builder('mam_preferences.ui')
    
    48 51
             self.add(self._ui.get_object('mam_box'))
    
    ... ... @@ -50,33 +53,40 @@ class MamPreferences(Gtk.ApplicationWindow, EventHelper):
    50 53
             self._spinner = Gtk.Spinner()
    
    51 54
             self._ui.overlay.add_overlay(self._spinner)
    
    52 55
     
    
    53
    -        self.register_events([
    
    54
    -            ('mam-prefs-received', ged.GUI1, self._mam_prefs_received),
    
    55
    -            ('mam-prefs-saved', ged.GUI1, self._mam_prefs_saved),
    
    56
    -            ('mam-prefs-error', ged.GUI1, self._mam_prefs_error),
    
    57
    -        ])
    
    58
    -
    
    59 56
             self._set_mam_box_state(False)
    
    57
    +        self.connect('destroy', self._on_destroy)
    
    60 58
             self._ui.connect_signals(self)
    
    61 59
             self.show_all()
    
    62 60
     
    
    63 61
             self._activate_spinner()
    
    64 62
     
    
    65
    -        self._con.get_module('MAM').request_mam_preferences()
    
    63
    +        self._con.get_module('MAM').request_preferences(
    
    64
    +            callback=self._mam_prefs_received)
    
    66 65
     
    
    67
    -    def _mam_prefs_received(self, obj):
    
    68
    -        if obj.conn.name != self.account:
    
    66
    +    def _on_destroy(self, *args):
    
    67
    +        self._destroyed = True
    
    68
    +
    
    69
    +    @ensure_not_destroyed
    
    70
    +    def _mam_prefs_received(self, result):
    
    71
    +        if is_error_result(result):
    
    72
    +            self._on_error(result.get_text())
    
    69 73
                 return
    
    74
    +
    
    70 75
             self._disable_spinner()
    
    71 76
             self._set_mam_box_state(True)
    
    72 77
     
    
    73
    -        self._ui.default_combo.set_active_id(obj.default)
    
    78
    +        self._ui.default_combo.set_active_id(result.default)
    
    74 79
             self._ui.preferences_store.clear()
    
    75
    -        for item in obj.rules:
    
    76
    -            self._ui.preferences_store.append(item)
    
    80
    +        for jid in result.always:
    
    81
    +            self._ui.preferences_store.append((str(jid), 'Always'))
    
    77 82
     
    
    78
    -    def _mam_prefs_saved(self, obj):
    
    79
    -        if obj.conn.name != self.account:
    
    83
    +        for jid in result.never:
    
    84
    +            self._ui.preferences_store.append((str(jid), 'Never'))
    
    85
    +
    
    86
    +    @ensure_not_destroyed
    
    87
    +    def _mam_prefs_saved(self, result):
    
    88
    +        if is_error_result(result):
    
    89
    +            self._on_error(result.get_text())
    
    80 90
                 return
    
    81 91
     
    
    82 92
             self._disable_spinner()
    
    ... ... @@ -91,18 +101,11 @@ class MamPreferences(Gtk.ApplicationWindow, EventHelper):
    91 101
                 [DialogButton.make('OK',
    
    92 102
                                    callback=_on_ok)]).show()
    
    93 103
     
    
    94
    -    def _mam_prefs_error(self, obj=None):
    
    95
    -        if obj and obj.conn.name != self.account:
    
    96
    -            return
    
    97
    -
    
    104
    +    def _on_error(self, error):
    
    98 105
             self._disable_spinner()
    
    99 106
     
    
    100
    -        if not obj:
    
    101
    -            msg = _('No response from the Server')
    
    102
    -        else:
    
    103
    -            msg = _('Error received: {}').format(obj.error_msg)
    
    104
    -
    
    105
    -        InformationDialog(_('Archiving Preferences Error'), msg)
    
    107
    +        InformationDialog(_('Archiving Preferences Error'),
    
    108
    +                          _('Error received: {}').format(error))
    
    106 109
     
    
    107 110
             self._set_mam_box_state(True)
    
    108 111
     
    
    ... ... @@ -130,11 +133,17 @@ class MamPreferences(Gtk.ApplicationWindow, EventHelper):
    130 133
         def _on_save(self, _button):
    
    131 134
             self._activate_spinner()
    
    132 135
             self._set_mam_box_state(False)
    
    133
    -        items = []
    
    136
    +        always = []
    
    137
    +        never = []
    
    134 138
             default = self._ui.default_combo.get_active_id()
    
    135 139
             for item in self._ui.preferences_store:
    
    136
    -            items.append((item[0].lower(), item[1]))
    
    137
    -        self._con.get_module('MAM').set_mam_preferences(items, default)
    
    140
    +            jid, type_ = item
    
    141
    +            if type_ == 'Always':
    
    142
    +                always.append(jid)
    
    143
    +            else:
    
    144
    +                never.append(jid)
    
    145
    +        self._con.get_module('MAM').set_preferences(
    
    146
    +            default, always, never, callback=self._mam_prefs_saved)
    
    138 147
     
    
    139 148
         def _activate_spinner(self):
    
    140 149
             self._spinner.show()
    

  • gajim/gtk/server_info.py
    ... ... @@ -288,8 +288,7 @@ class ServerInfo(Gtk.ApplicationWindow, EventHelper):
    288 288
                         con.get_module('Carbons').supported,
    
    289 289
                         nbxmpp.NS_CARBONS),
    
    290 290
                 Feature('XEP-0313: Message Archive Management',
    
    291
    -                    con.get_module('MAM').available,
    
    292
    -                    con.get_module('MAM').archiving_namespace),
    
    291
    +                    con.get_module('MAM').available),
    
    293 292
                 Feature('XEP-0363: HTTP File Upload',
    
    294 293
                         con.get_module('HTTPUpload').available,
    
    295 294
                         http_upload_info),