mam.py 23.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.

# XEP-0313: Message Archive Management

17
import time
18 19 20
from datetime import datetime, timedelta

import nbxmpp
Philipp Hörist's avatar
Philipp Hörist committed
21
from nbxmpp.structs import StanzaHandler
22 23

from gajim.common import app
24
from gajim.common.nec import NetworkEvent
25
from gajim.common.nec import NetworkIncomingEvent
26 27 28 29
from gajim.common.const import ArchiveState
from gajim.common.const import KindConstant
from gajim.common.const import SyncThreshold
from gajim.common.helpers import get_sync_threshold
30
from gajim.common.helpers import AdditionalDataDict
31 32
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction
33
from gajim.common.modules.util import get_eme_message
Philipp Hörist's avatar
Philipp Hörist committed
34
from gajim.common.modules.base import BaseModule
35 36


Philipp Hörist's avatar
Philipp Hörist committed
37
class MAM(BaseModule):
38
    def __init__(self, con):
Philipp Hörist's avatar
Philipp Hörist committed
39
        BaseModule.__init__(self, con)
40 41

        self.handlers = [
42 43 44
            StanzaHandler(name='message',
                          callback=self._set_message_archive_info,
                          priority=41),
Philipp Hörist's avatar
Philipp Hörist committed
45 46 47
            StanzaHandler(name='message',
                          callback=self._mam_message_received,
                          priority=51),
48 49 50 51 52 53
        ]

        self.available = False
        self.archiving_namespace = None
        self._mam_query_ids = {}

54 55 56
        # Holds archive jids where catch up was successful
        self._catch_up_finished = []

57 58
    def pass_disco(self, info):
        if nbxmpp.NS_MAM_2 in info.features:
59
            self.archiving_namespace = nbxmpp.NS_MAM_2
60
        elif nbxmpp.NS_MAM_1 in info.features:
61 62 63 64 65
            self.archiving_namespace = nbxmpp.NS_MAM_1
        else:
            return

        self.available = True
66 67
        self._log.info('Discovered MAM %s: %s',
                       self.archiving_namespace, info.jid)
68 69 70 71 72

        app.nec.push_incoming_event(
            NetworkEvent('feature-discovered',
                         account=self._account,
                         feature=self.archiving_namespace))
73

74 75 76 77
    def reset_state(self):
        self._mam_query_ids.clear()
        self._catch_up_finished.clear()

Philipp Hörist's avatar
Philipp Hörist committed
78 79 80
    def is_catch_up_finished(self, jid):
        return jid in self._catch_up_finished

Philipp Hörist's avatar
Philipp Hörist committed
81
    def _from_valid_archive(self, _stanza, properties):
Philipp Hörist's avatar
Philipp Hörist committed
82 83
        if properties.type.is_groupchat:
            expected_archive = properties.jid
84 85 86
        else:
            expected_archive = self._con.get_own_jid()

Philipp Hörist's avatar
Philipp Hörist committed
87
        return properties.mam.archive.bareMatch(expected_archive)
88

Philipp Hörist's avatar
Philipp Hörist committed
89 90 91
    def _get_unique_id(self, properties):
        if properties.type.is_groupchat:
            return properties.mam.id, None
92

Philipp Hörist's avatar
Philipp Hörist committed
93 94
        if properties.is_self_message:
            return None, properties.id
95

Philipp Hörist's avatar
Philipp Hörist committed
96 97
        if properties.is_muc_pm:
            return properties.mam.id, properties.id
98

99
        if self._con.get_own_jid().bareMatch(properties.from_):
100
            # message we sent
Philipp Hörist's avatar
Philipp Hörist committed
101
            return properties.mam.id, properties.id
102 103

        # A message we received
Philipp Hörist's avatar
Philipp Hörist committed
104 105
        return properties.mam.id, None

106 107 108 109 110 111 112 113
    def _set_message_archive_info(self, _con, _stanza, properties):
        if (properties.is_mam_message or
                properties.is_pubsub or
                properties.is_muc_subject):
            return

        if properties.type.is_groupchat:
            archive_jid = properties.jid.getBare()
114
            disco_info = app.logger.get_last_disco_info(archive_jid)
Philipp Hörist's avatar
Philipp Hörist committed
115 116 117 118 119
            if disco_info is None:
                # This is the case on MUC creation
                # After MUC configuration we receive a configuration change
                # message before we had the chance to disco the new MUC
                return
120
            namespace = disco_info.mam_namespace
121 122 123 124 125 126 127 128 129 130
            timestamp = properties.timestamp
            if namespace is None:
                # MUC History
                app.logger.set_archive_infos(
                    archive_jid,
                    last_muc_timestamp=timestamp)
                return

        else:
            archive_jid = self._con.get_own_jid().getBare()
131
            namespace = self.archiving_namespace
132 133 134 135 136 137 138 139
            timestamp = None

        if properties.stanza_id is None or namespace != nbxmpp.NS_MAM_2:
            return

        if not archive_jid == properties.stanza_id.by:
            return

140
        if not self.is_catch_up_finished(archive_jid):
141 142 143 144 145 146
            return

        app.logger.set_archive_infos(archive_jid,
                                     last_mam_id=properties.stanza_id.id,
                                     last_muc_timestamp=timestamp)

Philipp Hörist's avatar
Philipp Hörist committed
147 148 149
    def _mam_message_received(self, _con, stanza, properties):
        if not properties.is_mam_message:
            return
150 151

        app.nec.push_incoming_event(
152 153 154 155
            NetworkIncomingEvent('mam-message-received',
                                 account=self._account,
                                 stanza=stanza,
                                 properties=properties))
156

Philipp Hörist's avatar
Philipp Hörist committed
157
        if not self._from_valid_archive(stanza, properties):
Philipp Hörist's avatar
Philipp Hörist committed
158 159
            self._log.warning('Message from invalid archive %s',
                              properties.mam.archive)
160 161
            raise nbxmpp.NodeProcessed

Philipp Hörist's avatar
Philipp Hörist committed
162 163
        self._log.info('Received message from archive: %s',
                       properties.mam.archive)
Philipp Hörist's avatar
Philipp Hörist committed
164
        if not self._is_valid_request(properties):
Philipp Hörist's avatar
Philipp Hörist committed
165 166 167
            self._log.warning('Invalid MAM Message: unknown query id %s',
                              properties.mam.query_id)
            self._log.debug(stanza)
168 169
            raise nbxmpp.NodeProcessed

170 171 172
        is_groupchat = properties.type.is_groupchat
        if is_groupchat:
            kind = KindConstant.GC_MSG
173
        else:
174 175 176 177
            if properties.from_.bareMatch(self._con.get_own_jid()):
                kind = KindConstant.CHAT_MSG_SENT
            else:
                kind = KindConstant.CHAT_MSG_RECV
178

Philipp Hörist's avatar
Philipp Hörist committed
179
        stanza_id, message_id = self._get_unique_id(properties)
180

Philipp Hörist's avatar
Philipp Hörist committed
181
        if properties.mam.is_ver_2:
182 183
            # Search only with stanza-id for duplicates on mam:2
            if app.logger.find_stanza_id(self._account,
Philipp Hörist's avatar
Philipp Hörist committed
184
                                         str(properties.mam.archive),
185
                                         stanza_id,
Philipp Hörist's avatar
Philipp Hörist committed
186
                                         message_id,
187
                                         groupchat=is_groupchat):
Philipp Hörist's avatar
Philipp Hörist committed
188 189
                self._log.info('Found duplicate with stanza-id: %s, '
                               'message-id: %s', stanza_id, message_id)
190 191
                raise nbxmpp.NodeProcessed

192 193 194 195 196 197
        additional_data = AdditionalDataDict()
        if properties.has_user_delay:
            # Record it as a user timestamp
            additional_data.set_value(
                'gajim', 'user_timestamp', properties.user_timestamp)

Philipp Hörist's avatar
Philipp Hörist committed
198 199
        parse_oob(properties, additional_data)

Philipp Hörist's avatar
Philipp Hörist committed
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        msgtxt = properties.body

        if properties.is_encrypted:
            additional_data['encrypted'] = properties.encrypted.additional_data
        else:
            if properties.eme is not None:
                msgtxt = get_eme_message(properties.eme)

        if not msgtxt:
            # For example Chatstates, Receipts, Chatmarkers
            self._log.debug(stanza.getProperties())
            return

        with_ = properties.jid.getStripped()
        if properties.is_muc_pm:
            # we store the message with the full JID
            with_ = str(with_)

        if properties.is_self_message:
            # Self messages can only be deduped with origin-id
            if message_id is None:
                self._log.warning('Self message without origin-id found')
                return
            stanza_id = message_id

        if properties.mam.namespace == nbxmpp.NS_MAM_1:
            if app.logger.search_for_duplicate(
                    self._account, with_, properties.mam.timestamp, msgtxt):
                self._log.info('Found duplicate with fallback for mam:1')
                return

        app.logger.insert_into_logs(self._account,
                                    with_,
                                    properties.mam.timestamp,
234
                                    kind,
Philipp Hörist's avatar
Philipp Hörist committed
235 236
                                    unread=False,
                                    message=msgtxt,
237
                                    contact_name=properties.muc_nickname,
Philipp Hörist's avatar
Philipp Hörist committed
238 239 240
                                    additional_data=additional_data,
                                    stanza_id=stanza_id,
                                    message_id=properties.id)
241

Philipp Hörist's avatar
Philipp Hörist committed
242
        app.nec.push_incoming_event(
243 244 245 246 247 248 249 250 251 252
            NetworkEvent('mam-decrypted-message-received',
                         account=self._account,
                         additional_data=additional_data,
                         correct_id=parse_correction(properties),
                         archive_jid=properties.mam.archive,
                         msgtxt=properties.body,
                         properties=properties,
                         kind=kind,
                         )
        )
253

Philipp Hörist's avatar
Philipp Hörist committed
254 255 256
    def _is_valid_request(self, properties):
        valid_id = self._mam_query_ids.get(str(properties.mam.archive), None)
        return valid_id == properties.mam.query_id
257 258 259 260 261 262

    def _get_query_id(self, jid):
        query_id = self._con.connection.getAnID()
        self._mam_query_ids[jid] = query_id
        return query_id

Philipp Hörist's avatar
Philipp Hörist committed
263
    def _parse_iq(self, stanza):
264
        if not nbxmpp.isResultNode(stanza):
Philipp Hörist's avatar
Philipp Hörist committed
265
            self._log.error('Error on MAM query: %s', stanza.getError())
266 267 268 269
            raise InvalidMamIQ

        fin = stanza.getTag('fin')
        if fin is None:
Philipp Hörist's avatar
Philipp Hörist committed
270
            self._log.error('Malformed MAM query result received: %s', stanza)
271 272 273 274
            raise InvalidMamIQ

        set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM)
        if set_ is None:
Philipp Hörist's avatar
Philipp Hörist committed
275
            self._log.error(
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
                'Malformed MAM query result received (no "set" Node): %s',
                stanza)
            raise InvalidMamIQ
        return fin, set_

    def _get_from_jid(self, stanza):
        jid = stanza.getFrom()
        if jid is None:
            # No from means, iq from our own archive
            jid = self._con.get_own_jid().getStripped()
        else:
            jid = jid.getStripped()
        return jid

    def request_archive_count(self, start_date, end_date):
        jid = self._con.get_own_jid().getStripped()
Philipp Hörist's avatar
Philipp Hörist committed
292
        self._log.info('Request archive count from: %s', jid)
293 294 295 296 297 298 299
        query_id = self._get_query_id(jid)
        query = self._get_archive_query(
            query_id, start=start_date, end=end_date, max_=0)
        self._con.connection.SendAndCallForResponse(
            query, self._received_count, {'query_id': query_id})
        return query_id

300
    def _received_count(self, _con, stanza, query_id):
301 302 303 304 305 306 307 308 309
        try:
            _, set_ = self._parse_iq(stanza)
        except InvalidMamIQ:
            return

        jid = self._get_from_jid(stanza)
        self._mam_query_ids.pop(jid)

        count = set_.getTagData('count')
Philipp Hörist's avatar
Philipp Hörist committed
310
        self._log.info('Received archive count: %s', count)
311 312 313 314 315 316 317
        app.nec.push_incoming_event(ArchivingCountReceived(
            None, query_id=query_id, count=count))

    def request_archive_on_signin(self):
        own_jid = self._con.get_own_jid().getStripped()

        if own_jid in self._mam_query_ids:
Philipp Hörist's avatar
Philipp Hörist committed
318
            self._log.warning('MAM request for %s already running', own_jid)
319 320
            return

321
        archive = app.logger.get_archive_infos(own_jid)
322 323 324 325 326 327 328 329 330 331 332 333 334

        # Migration of last_mam_id from config to DB
        if archive is not None:
            mam_id = archive.last_mam_id
        else:
            mam_id = app.config.get_per(
                'accounts', self._account, 'last_mam_id')
            if mam_id:
                app.config.del_per('accounts', self._account, 'last_mam_id')

        start_date = None
        query_id = self._get_query_id(own_jid)
        if mam_id:
Philipp Hörist's avatar
Philipp Hörist committed
335
            self._log.info('MAM query after: %s', mam_id)
336 337 338 339
            query = self._get_archive_query(query_id, after=mam_id)
        else:
            # First Start, we request the last week
            start_date = datetime.utcnow() - timedelta(days=7)
Philipp Hörist's avatar
Philipp Hörist committed
340
            self._log.info('First start: query archive start: %s', start_date)
341
            query = self._get_archive_query(query_id, start=start_date)
342 343 344

        if own_jid in self._catch_up_finished:
            self._catch_up_finished.remove(own_jid)
345 346 347
        self._send_archive_query(query, query_id, start_date)

    def request_archive_on_muc_join(self, jid):
348 349
        archive = app.logger.get_archive_infos(jid)
        threshold = get_sync_threshold(jid, archive)
Philipp Hörist's avatar
Philipp Hörist committed
350
        self._log.info('Threshold for %s: %s', jid, threshold)
351 352
        query_id = self._get_query_id(jid)
        start_date = None
353
        if archive is None or archive.last_mam_id is None:
354 355 356 357
            # First Start, we dont request history
            # Depending on what a MUC saves, there could be thousands
            # of Messages even in just one day.
            start_date = datetime.utcnow() - timedelta(days=1)
Philipp Hörist's avatar
Philipp Hörist committed
358 359
            self._log.info('First join: query archive %s from: %s',
                           jid, start_date)
360 361
            query = self._get_archive_query(
                query_id, jid=jid, start=start_date)
362

363 364
        elif threshold == SyncThreshold.NO_THRESHOLD:
            # Not our first join and no threshold set
Philipp Hörist's avatar
Philipp Hörist committed
365 366
            self._log.info('Request from archive: %s, after mam-id %s',
                           jid, archive.last_mam_id)
367 368 369 370 371 372 373 374
            query = self._get_archive_query(
                query_id, jid=jid, after=archive.last_mam_id)

        else:
            # Not our first join, check how much time elapsed since our
            # last join and check against threshold
            last_timestamp = archive.last_muc_timestamp
            if last_timestamp is None:
Philipp Hörist's avatar
Philipp Hörist committed
375
                self._log.info('No last muc timestamp found ( mam:1? )')
376 377 378 379 380 381
                last_timestamp = 0

            last = datetime.utcfromtimestamp(float(last_timestamp))
            if datetime.utcnow() - last > timedelta(days=threshold):
                # To much time has elapsed since last join, apply threshold
                start_date = datetime.utcnow() - timedelta(days=threshold)
Philipp Hörist's avatar
Philipp Hörist committed
382 383 384
                self._log.info('Too much time elapsed since last join, '
                               'request from: %s, threshold: %s',
                               start_date, threshold)
385 386 387 388
                query = self._get_archive_query(
                    query_id, jid=jid, start=start_date)
            else:
                # Request from last mam-id
Philipp Hörist's avatar
Philipp Hörist committed
389 390
                self._log.info('Request from archive %s after %s:',
                               jid, archive.last_mam_id)
391 392 393
                query = self._get_archive_query(
                    query_id, jid=jid, after=archive.last_mam_id)

394 395
        if jid in self._catch_up_finished:
            self._catch_up_finished.remove(jid)
396 397 398 399 400 401 402 403 404
        self._send_archive_query(query, query_id, start_date, groupchat=True)

    def _send_archive_query(self, query, query_id, start_date=None,
                            groupchat=False):
        self._con.connection.SendAndCallForResponse(
            query, self._result_finished, {'query_id': query_id,
                                           'start_date': start_date,
                                           'groupchat': groupchat})

405
    def _result_finished(self, _con, stanza, query_id, start_date, groupchat):
406 407 408 409 410 411 412 413 414
        try:
            fin, set_ = self._parse_iq(stanza)
        except InvalidMamIQ:
            return

        jid = self._get_from_jid(stanza)

        last = set_.getTagData('last')
        if last is None:
Philipp Hörist's avatar
Philipp Hörist committed
415
            self._log.info('End of MAM query, no items retrieved')
416
            self._catch_up_finished.append(jid)
417 418 419 420 421
            self._mam_query_ids.pop(jid)
            return

        complete = fin.getAttr('complete')
        if complete != 'true':
422
            app.logger.set_archive_infos(jid, last_mam_id=last)
423 424 425 426 427 428
            self._mam_query_ids.pop(jid)
            query_id = self._get_query_id(jid)
            query = self._get_archive_query(query_id, jid=jid, after=last)
            self._send_archive_query(query, query_id, groupchat=groupchat)
        else:
            self._mam_query_ids.pop(jid)
429 430 431 432 433 434 435 436
            app.logger.set_archive_infos(
                jid, last_mam_id=last, last_muc_timestamp=time.time())
            if start_date is not None and not groupchat:
                # Record the earliest timestamp we request from
                # the account archive. For the account archive we only
                # set start_date at the very first request.
                app.logger.set_archive_infos(
                    jid, oldest_mam_timestamp=start_date.timestamp())
437 438

            self._catch_up_finished.append(jid)
Philipp Hörist's avatar
Philipp Hörist committed
439
            self._log.info('End of MAM query, last mam id: %s', last)
440 441 442 443 444

    def request_archive_interval(self, start_date, end_date, after=None,
                                 query_id=None):
        jid = self._con.get_own_jid().getStripped()
        if after is None:
Philipp Hörist's avatar
Philipp Hörist committed
445 446
            self._log.info('Request intervall from %s to %s from %s',
                           start_date, end_date, jid)
447
        else:
Philipp Hörist's avatar
Philipp Hörist committed
448
            self._log.info('Query page after %s from %s', after, jid)
449 450 451 452 453 454 455 456 457 458 459 460
        if query_id is None:
            query_id = self._get_query_id(jid)
        self._mam_query_ids[jid] = query_id
        query = self._get_archive_query(query_id, start=start_date,
                                        end=end_date, after=after, max_=30)

        self._con.connection.SendAndCallForResponse(
            query, self._intervall_result, {'query_id': query_id,
                                            'start_date': start_date,
                                            'end_date': end_date})
        return query_id

461
    def _intervall_result(self, _con, stanza, query_id,
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
                          start_date, end_date):
        try:
            fin, set_ = self._parse_iq(stanza)
        except InvalidMamIQ:
            return

        jid = self._get_from_jid(stanza)
        self._mam_query_ids.pop(jid)
        if start_date:
            timestamp = start_date.timestamp()
        else:
            timestamp = ArchiveState.ALL

        last = set_.getTagData('last')
        if last is None:
            app.nec.push_incoming_event(ArchivingIntervalFinished(
                None, query_id=query_id))
479
            app.logger.set_archive_infos(
480
                jid, oldest_mam_timestamp=timestamp)
Philipp Hörist's avatar
Philipp Hörist committed
481
            self._log.info('End of MAM request, no items retrieved')
482 483 484 485 486 487
            return

        complete = fin.getAttr('complete')
        if complete != 'true':
            self.request_archive_interval(start_date, end_date, last, query_id)
        else:
Philipp Hörist's avatar
Philipp Hörist committed
488
            self._log.info('Request finished')
489
            app.logger.set_archive_infos(
490 491 492 493 494
                jid, oldest_mam_timestamp=timestamp)
            app.nec.push_incoming_event(ArchivingIntervalFinished(
                None, query_id=query_id))

    def _get_archive_query(self, query_id, jid=None, start=None, end=None,
495
                           with_=None, after=None, max_=70):
496
        # Muc archive query?
497 498
        disco_info = app.logger.get_last_disco_info(jid)
        if disco_info is None:
499 500
            # Query to our own archive
            namespace = self.archiving_namespace
501 502
        else:
            namespace = disco_info.mam_namespace
503 504 505 506 507 508 509 510 511

        iq = nbxmpp.Iq('set', to=jid)
        query = iq.addChild('query', namespace=namespace)
        form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
        field = nbxmpp.DataField(typ='hidden',
                                 name='FORM_TYPE',
                                 value=namespace)
        form.addChild(node=field)
        if start:
512 513 514 515
            field = nbxmpp.DataField(
                typ='text-single',
                name='start',
                value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
516 517 518 519 520 521 522
            form.addChild(node=field)
        if end:
            field = nbxmpp.DataField(typ='text-single',
                                     name='end',
                                     value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
            form.addChild(node=field)
        if with_:
523 524 525
            field = nbxmpp.DataField(typ='jid-single',
                                     name='with',
                                     value=with_)
526 527 528 529 530 531 532 533 534 535
            form.addChild(node=field)

        set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
        set_.setTagData('max', max_)
        if after:
            set_.setTagData('after', after)
        query.setAttr('queryid', query_id)
        return iq

    def request_mam_preferences(self):
Philipp Hörist's avatar
Philipp Hörist committed
536
        self._log.info('Request MAM preferences')
537 538 539 540 541 542 543
        iq = nbxmpp.Iq('get', self.archiving_namespace)
        iq.setQuery('prefs')
        self._con.connection.SendAndCallForResponse(
            iq, self._preferences_received)

    def _preferences_received(self, stanza):
        if not nbxmpp.isResultNode(stanza):
Philipp Hörist's avatar
Philipp Hörist committed
544
            self._log.info('Error: %s', stanza.getError())
545 546 547 548
            app.nec.push_incoming_event(MAMPreferenceError(
                None, conn=self._con, error=stanza.getError()))
            return

Philipp Hörist's avatar
Philipp Hörist committed
549
        self._log.info('Received MAM preferences')
550 551
        prefs = stanza.getTag('prefs', namespace=self.archiving_namespace)
        if prefs is None:
Philipp Hörist's avatar
Philipp Hörist committed
552
            self._log.error('Malformed stanza (no prefs node): %s', stanza)
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
            return

        rules = []
        default = prefs.getAttr('default')
        for item in prefs.getTag('always').getTags('jid'):
            rules.append((item.getData(), 'Always'))

        for item in prefs.getTag('never').getTags('jid'):
            rules.append((item.getData(), 'Never'))

        app.nec.push_incoming_event(MAMPreferenceReceived(
            None, conn=self._con, rules=rules, default=default))

    def set_mam_preferences(self, rules, default):
        iq = nbxmpp.Iq(typ='set')
        prefs = iq.addChild(name='prefs',
                            namespace=self.archiving_namespace,
                            attrs={'default': default})
        always = prefs.addChild(name='always')
        never = prefs.addChild(name='never')
        for item in rules:
            jid, archive = item
            if archive:
                always.addChild(name='jid').setData(jid)
            else:
                never.addChild(name='jid').setData(jid)

        self._con.connection.SendAndCallForResponse(
            iq, self._preferences_saved)

    def _preferences_saved(self, stanza):
        if not nbxmpp.isResultNode(stanza):
Philipp Hörist's avatar
Philipp Hörist committed
585
            self._log.info('Error: %s', stanza.getError())
586 587 588
            app.nec.push_incoming_event(MAMPreferenceError(
                None, conn=self._con, error=stanza.getError()))
        else:
Philipp Hörist's avatar
Philipp Hörist committed
589
            self._log.info('Preferences saved')
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
            app.nec.push_incoming_event(
                MAMPreferenceSaved(None, conn=self._con))


class MAMPreferenceError(NetworkIncomingEvent):
    name = 'mam-prefs-error'


class MAMPreferenceReceived(NetworkIncomingEvent):
    name = 'mam-prefs-received'


class MAMPreferenceSaved(NetworkIncomingEvent):
    name = 'mam-prefs-saved'


class ArchivingCountReceived(NetworkIncomingEvent):
    name = 'archiving-count-received'


class ArchivingIntervalFinished(NetworkIncomingEvent):
    name = 'archiving-interval-finished'


class ArchivingErrorReceived(NetworkIncomingEvent):
    name = 'archiving-error-received'


class InvalidMamIQ(Exception):
    pass


def get_instance(*args, **kwargs):
    return MAM(*args, **kwargs), 'MAM'