summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/plugins/index.rst3
-rw-r--r--docs/api/plugins/xep_0313.rst1
-rw-r--r--docs/api/plugins/xep_0441.rst18
-rw-r--r--itests/test_mam.py45
-rw-r--r--slixmpp/plugins/__init__.py1
-rw-r--r--slixmpp/plugins/xep_0059/rsm.py6
-rw-r--r--slixmpp/plugins/xep_0313/__init__.py4
-rw-r--r--slixmpp/plugins/xep_0313/mam.py215
-rw-r--r--slixmpp/plugins/xep_0313/stanza.py371
-rw-r--r--slixmpp/plugins/xep_0441/__init__.py13
-rw-r--r--slixmpp/plugins/xep_0441/mam_prefs.py75
-rw-r--r--slixmpp/plugins/xep_0441/stanza.py91
-rw-r--r--slixmpp/types.py2
-rw-r--r--tests/test_stanza_xep_0313.py105
-rw-r--r--tests/test_stream_xep_0313.py340
15 files changed, 1142 insertions, 148 deletions
diff --git a/docs/api/plugins/index.rst b/docs/api/plugins/index.rst
index e6aa6913..6737d3c9 100644
--- a/docs/api/plugins/index.rst
+++ b/docs/api/plugins/index.rst
@@ -92,6 +92,5 @@ Plugin index
xep_0428
xep_0437
xep_0439
+ xep_0441
xep_0444
-
-
diff --git a/docs/api/plugins/xep_0313.rst b/docs/api/plugins/xep_0313.rst
index f6271d72..fcd2af90 100644
--- a/docs/api/plugins/xep_0313.rst
+++ b/docs/api/plugins/xep_0313.rst
@@ -14,5 +14,6 @@ Stanza elements
.. automodule:: slixmpp.plugins.xep_0313.stanza
:members:
+ :member-order: bysource
:undoc-members:
diff --git a/docs/api/plugins/xep_0441.rst b/docs/api/plugins/xep_0441.rst
new file mode 100644
index 00000000..34e51ea1
--- /dev/null
+++ b/docs/api/plugins/xep_0441.rst
@@ -0,0 +1,18 @@
+
+XEP-0441: Message Archive Management Preferences
+================================================
+
+.. module:: slixmpp.plugins.xep_0441
+
+.. autoclass:: XEP_0441
+ :members:
+ :exclude-members: session_bind, plugin_init, plugin_end
+
+
+Stanza elements
+---------------
+
+.. automodule:: slixmpp.plugins.xep_0441.stanza
+ :members:
+ :undoc-members:
+
diff --git a/itests/test_mam.py b/itests/test_mam.py
index f61bc1e6..d10241bd 100644
--- a/itests/test_mam.py
+++ b/itests/test_mam.py
@@ -22,11 +22,14 @@ class TestMAM(SlixIntegration):
"""Make sure we can get messages from our archive"""
# send messages first
tok = randint(1, 999999)
- self.clients[0].make_message(mto=self.clients[1].boundjid, mbody='coucou').send()
+ self.clients[0].make_message(
+ mto=self.clients[1].boundjid,
+ mbody=f'coucou {tok}'
+ ).send()
await self.clients[1].wait_until('message')
self.clients[1].make_message(
mto=self.clients[0].boundjid,
- mbody='coucou coucou %s' % tok,
+ mbody=f'coucou coucou {tok}',
).send()
await self.clients[0].wait_until('message')
@@ -48,8 +51,42 @@ class TestMAM(SlixIntegration):
if count >= 2:
break
- self.assertEqual(msgs[0]['body'], 'coucou')
- self.assertEqual(msgs[1]['body'], 'coucou coucou %s' % tok)
+ self.assertEqual(msgs[0]['body'], f'coucou {tok}')
+ self.assertEqual(msgs[1]['body'], f'coucou coucou {tok}')
+
+ async def test_mam_iterate(self):
+ """Make sure we can iterate over messages from our archive"""
+ # send messages first
+ tok = randint(1, 999999)
+ self.clients[0].make_message(
+ mto=self.clients[1].boundjid,
+ mbody=f'coucou {tok}'
+ ).send()
+ await self.clients[1].wait_until('message')
+ self.clients[1].make_message(
+ mto=self.clients[0].boundjid,
+ mbody='coucou coucou %s' % tok,
+ ).send()
+ await self.clients[0].wait_until('message')
+
+ # Get archive
+ retrieve = self.clients[0]['xep_0313'].iterate(
+ with_jid=JID(self.envjid('CI_ACCOUNT2')),
+ reverse=True,
+ rsm={'max': 1}
+ )
+ msgs = []
+ count = 0
+ async for msg in retrieve:
+ msgs.append(
+ msg['mam_result']['forwarded']['stanza']
+ )
+ count += 1
+ if count >= 2:
+ break
+
+ self.assertEqual(msgs[0]['body'], f'coucou coucou {tok}')
+ self.assertEqual(msgs[1]['body'], f'coucou {tok}')
suite = unittest.TestLoader().loadTestsFromTestCase(TestMAM)
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index d087f92b..55627113 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -110,5 +110,6 @@ __all__ = [
'xep_0428', # Message Fallback
'xep_0437', # Room Activity Indicators
'xep_0439', # Quick Response
+ 'xep_0441', # Message Archive Management Preferences
'xep_0444', # Message Reactions
]
diff --git a/slixmpp/plugins/xep_0059/rsm.py b/slixmpp/plugins/xep_0059/rsm.py
index 61752af4..085ee474 100644
--- a/slixmpp/plugins/xep_0059/rsm.py
+++ b/slixmpp/plugins/xep_0059/rsm.py
@@ -135,6 +135,9 @@ class ResultIterator(AsyncIterator):
not r[self.recv_interface]['rsm']['last']:
raise StopAsyncIteration
+ if self.post_cb:
+ self.post_cb(r)
+
if r[self.recv_interface]['rsm']['count'] and \
r[self.recv_interface]['rsm']['first_index']:
count = int(r[self.recv_interface]['rsm']['count'])
@@ -147,9 +150,6 @@ class ResultIterator(AsyncIterator):
self.start = r[self.recv_interface]['rsm']['first']
else:
self.start = r[self.recv_interface]['rsm']['last']
-
- if self.post_cb:
- self.post_cb(r)
return r
except XMPPError:
raise StopAsyncIteration
diff --git a/slixmpp/plugins/xep_0313/__init__.py b/slixmpp/plugins/xep_0313/__init__.py
index eace3dc7..2b7f3273 100644
--- a/slixmpp/plugins/xep_0313/__init__.py
+++ b/slixmpp/plugins/xep_0313/__init__.py
@@ -5,8 +5,10 @@
# See the file LICENSE for copying permissio
from slixmpp.plugins.base import register_plugin
-from slixmpp.plugins.xep_0313.stanza import Result, MAM, Preferences
+from slixmpp.plugins.xep_0313.stanza import Result, MAM, Metadata
from slixmpp.plugins.xep_0313.mam import XEP_0313
register_plugin(XEP_0313)
+
+__all__ = ['XEP_0313', 'Result', 'MAM', 'Metadata']
diff --git a/slixmpp/plugins/xep_0313/mam.py b/slixmpp/plugins/xep_0313/mam.py
index eaa598a6..02efd3ce 100644
--- a/slixmpp/plugins/xep_0313/mam.py
+++ b/slixmpp/plugins/xep_0313/mam.py
@@ -5,8 +5,17 @@
# See the file LICENSE for copying permission
import logging
+from asyncio import Future
+from collections.abc import AsyncGenerator
from datetime import datetime
-from typing import Any, Dict, Callable, Optional, Awaitable
+from typing import (
+ Any,
+ Awaitable,
+ Callable,
+ Dict,
+ Optional,
+ Tuple,
+)
from slixmpp import JID
from slixmpp.stanza import Message, Iq
@@ -15,6 +24,7 @@ from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0313 import stanza
+from slixmpp.plugins.xep_0004.stanza import Form
log = logging.getLogger(__name__)
@@ -28,17 +38,25 @@ class XEP_0313(BasePlugin):
name = 'xep_0313'
description = 'XEP-0313: Message Archive Management'
- dependencies = {'xep_0030', 'xep_0050', 'xep_0059', 'xep_0297'}
+ dependencies = {
+ 'xep_0004', 'xep_0030', 'xep_0050', 'xep_0059', 'xep_0297'
+ }
stanza = stanza
def plugin_init(self):
+ register_stanza_plugin(stanza.MAM, Form)
register_stanza_plugin(Iq, stanza.MAM)
- register_stanza_plugin(Iq, stanza.Preferences)
register_stanza_plugin(Message, stanza.Result)
register_stanza_plugin(Iq, stanza.Fin)
- register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded)
+ register_stanza_plugin(
+ stanza.Result,
+ self.xmpp['xep_0297'].stanza.Forwarded
+ )
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set)
+ register_stanza_plugin(Iq, stanza.Metadata)
+ register_stanza_plugin(stanza.Metadata, stanza.Start)
+ register_stanza_plugin(stanza.Metadata, stanza.End)
def retrieve(
self,
@@ -66,16 +84,10 @@ class XEP_0313(BasePlugin):
:param bool iterator: Use RSM and iterate over a paginated query
:param dict rsm: RSM custom options
"""
- iq = self.xmpp.Iq()
+ iq, stanza_mask = self._pre_mam_retrieve(
+ jid, start, end, with_jid, ifrom
+ )
query_id = iq['id']
-
- iq['to'] = jid
- iq['from'] = ifrom
- iq['type'] = 'set'
- iq['mam']['queryid'] = query_id
- iq['mam']['start'] = start
- iq['mam']['end'] = end
- iq['mam']['with'] = with_jid
amount = 10
if rsm:
for key, value in rsm.items():
@@ -84,12 +96,6 @@ class XEP_0313(BasePlugin):
amount = value
cb_data = {}
- stanza_mask = self.xmpp.Message()
- stanza_mask.xml.remove(stanza_mask.xml.find('{urn:xmpp:sid:0}origin-id'))
- del stanza_mask['id']
- del stanza_mask['lang']
- stanza_mask['from'] = jid
- stanza_mask['mam_result']['queryid'] = query_id
xml_mask = str(stanza_mask)
def pre_cb(query: Iq) -> None:
@@ -106,11 +112,14 @@ class XEP_0313(BasePlugin):
results = cb_data['collector'].stop()
if result['type'] == 'result':
result['mam']['results'] = results
+ result['mam_fin']['results'] = results
if iterator:
- return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', amount=amount,
- reverse=reverse, recv_interface='mam_fin',
- pre_cb=pre_cb, post_cb=post_cb)
+ return self.xmpp['xep_0059'].iterate(
+ iq, 'mam', 'results', amount=amount,
+ reverse=reverse, recv_interface='mam_fin',
+ pre_cb=pre_cb, post_cb=post_cb
+ )
collector = Collector(
'MAM_Results_%s' % query_id,
@@ -126,26 +135,144 @@ class XEP_0313(BasePlugin):
return iq.send(timeout=timeout, callback=wrapped_cb)
- def get_preferences(self, timeout=None, callback=None):
- iq = self.xmpp.Iq()
- iq['type'] = 'get'
+ async def iterate(
+ self,
+ jid: Optional[JID] = None,
+ start: Optional[datetime] = None,
+ end: Optional[datetime] = None,
+ with_jid: Optional[JID] = None,
+ ifrom: Optional[JID] = None,
+ reverse: bool = False,
+ rsm: Optional[Dict[str, Any]] = None,
+ total: Optional[int] = None,
+ ) -> AsyncGenerator:
+ """
+ Iterate over each message of MAM query.
+
+ :param jid: Entity holding the MAM records
+ :param start: MAM query start time
+ :param end: MAM query end time
+ :param with_jid: Filter results on this JID
+ :param ifrom: To change the from address of the query
+ :param reverse: Get the results in reverse order
+ :param rsm: RSM custom options
+ :param total: A number of messages received after which the query
+ should stop.
+ """
+ iq, stanza_mask = self._pre_mam_retrieve(
+ jid, start, end, with_jid, ifrom
+ )
query_id = iq['id']
- iq['mam_prefs']['query_id'] = query_id
- return iq.send(timeout=timeout, callback=callback)
-
- def set_preferences(self, jid=None, default=None, always=None, never=None,
- ifrom=None, timeout=None, callback=None):
- iq = self.xmpp.Iq()
- iq['type'] = 'set'
- iq['to'] = jid
- iq['from'] = ifrom
- iq['mam_prefs']['default'] = default
- iq['mam_prefs']['always'] = always
- iq['mam_prefs']['never'] = never
- return iq.send(timeout=timeout, callback=callback)
-
- def get_configuration_commands(self, jid, **kwargs):
- return self.xmpp['xep_0030'].get_items(
- jid=jid,
- node='urn:xmpp:mam#configure',
- **kwargs)
+ amount = 10
+
+ if rsm:
+ for key, value in rsm.items():
+ iq['mam']['rsm'][key] = str(value)
+ if key == 'max':
+ amount = value
+ cb_data = {}
+
+ def pre_cb(query: Iq) -> None:
+ stanza_mask['mam_result']['queryid'] = query['id']
+ xml_mask = str(stanza_mask)
+ query['mam']['queryid'] = query['id']
+ collector = Collector(
+ 'MAM_Results_%s' % query_id,
+ MatchXMLMask(xml_mask))
+ self.xmpp.register_handler(collector)
+ cb_data['collector'] = collector
+
+ def post_cb(result: Iq) -> None:
+ results = cb_data['collector'].stop()
+ if result['type'] == 'result':
+ result['mam']['results'] = results
+ result['mam_fin']['results'] = results
+
+ iterator = self.xmpp['xep_0059'].iterate(
+ iq, 'mam', 'results', amount=amount,
+ reverse=reverse, recv_interface='mam_fin',
+ pre_cb=pre_cb, post_cb=post_cb
+ )
+ recv_count = 0
+
+ async for page in iterator:
+ messages = [message for message in page['mam']['results']]
+ if reverse:
+ messages.reverse()
+ for message in messages:
+ yield message
+ recv_count += 1
+ if total is not None and recv_count >= total:
+ break
+ if total is not None and recv_count >= total:
+ break
+
+ def _pre_mam_retrieve(
+ self,
+ jid: Optional[JID] = None,
+ start: Optional[datetime] = None,
+ end: Optional[datetime] = None,
+ with_jid: Optional[JID] = None,
+ ifrom: Optional[JID] = None,
+ ) -> Tuple[Iq, Message]:
+ """Build the IQ and stanza mask for MAM results
+ """
+ iq = self.xmpp.make_iq_set(ito=jid, ifrom=ifrom)
+ query_id = iq['id']
+ iq['mam']['queryid'] = query_id
+ iq['mam']['start'] = start
+ iq['mam']['end'] = end
+ iq['mam']['with'] = with_jid
+
+ stanza_mask = self.xmpp.Message()
+
+ auto_origin = stanza_mask.xml.find('{urn:xmpp:sid:0}origin-id')
+ if auto_origin is not None:
+ stanza_mask.xml.remove(auto_origin)
+ del stanza_mask['id']
+ del stanza_mask['lang']
+ stanza_mask['from'] = jid
+ stanza_mask['mam_result']['queryid'] = query_id
+
+ return (iq, stanza_mask)
+
+ async def get_fields(self, jid: Optional[JID] = None, **iqkwargs) -> Form:
+ """Get MAM query fields.
+
+ .. versionaddedd:: 1.8.0
+
+ :param jid: JID to retrieve the policy from.
+ :return: The Form of allowed options
+ """
+ ifrom = iqkwargs.pop('ifrom', None)
+ iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
+ iq.enable('mam')
+ result = await iq.send(**iqkwargs)
+ return result['mam']['form']
+
+ async def get_configuration_commands(self, jid: Optional[JID],
+ **discokwargs) -> Future:
+ """Get the list of MAM advanced configuration commands.
+
+ .. versionchanged:: 1.8.0
+
+ :param jid: JID to get the commands from.
+ """
+ if jid is None:
+ jid = self.xmpp.boundjid.bare
+ return await self.xmpp['xep_0030'].get_items(
+ jid=jid,
+ node='urn:xmpp:mam#configure',
+ **discokwargs
+ )
+
+ def get_archive_metadata(self, jid: Optional[JID] = None,
+ **iqkwargs) -> Future:
+ """Get the archive metadata from a JID.
+
+ :param jid: JID to get the metadata from.
+ """
+ ifrom = iqkwargs.pop('ifrom', None)
+ iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
+ iq.enable('mam_metadata')
+ return iq.send(**iqkwargs)
diff --git a/slixmpp/plugins/xep_0313/stanza.py b/slixmpp/plugins/xep_0313/stanza.py
index 4e43eeba..6633717f 100644
--- a/slixmpp/plugins/xep_0313/stanza.py
+++ b/slixmpp/plugins/xep_0313/stanza.py
@@ -1,159 +1,342 @@
-
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
-import datetime as dt
-
+from datetime import datetime
+from typing import (
+ Any,
+ Iterable,
+ List,
+ Optional,
+ Set,
+ Union,
+)
+
+from slixmpp.stanza import Message
from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, ET
-from slixmpp.plugins import xep_0082, xep_0004
+from slixmpp.plugins import xep_0082
class MAM(ElementBase):
+ """A MAM Query element.
+
+ .. code-block:: xml
+
+ <iq type='set' id='juliet1'>
+ <query xmlns='urn:xmpp:mam:2'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>juliet@capulet.lit</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+ """
name = 'query'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam'
- interfaces = {'queryid', 'start', 'end', 'with', 'results'}
- sub_interfaces = {'start', 'end', 'with'}
+ #: Available interfaces:
+ #:
+ #: - ``queryid``: The MAM query id
+ #: - ``start`` and ``end``: Temporal boundaries of the query
+ #: - ``with``: JID of the other entity the conversation is with
+ #: - ``after_id``: Fetch stanzas after this specific ID
+ #: - ``before_id``: Fetch stanzas before this specific ID
+ #: - ``ids``: Fetch the stanzas matching those IDs
+ #: - ``results``: pseudo-interface used to accumulate MAM results during
+ #: fetch, not relevant for the stanza itself.
+ interfaces = {
+ 'queryid', 'start', 'end', 'with', 'results',
+ 'before_id', 'after_id', 'ids',
+ }
+ sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids'}
def setup(self, xml=None):
ElementBase.setup(self, xml)
- self._form = xep_0004.stanza.Form()
- self._form['type'] = 'submit'
- field = self._form.add_field(var='FORM_TYPE', ftype='hidden',
- value='urn:xmpp:mam:2')
- self.append(self._form)
- self._results = []
-
- def __get_fields(self):
- return self._form.get_fields()
-
- def get_start(self):
- fields = self.__get_fields()
+ self._results: List[Message] = []
+
+ def _setup_form(self):
+ found = self.xml.find(
+ '{jabber:x:data}x/'
+ '{jabber:x:data}field[@var="FORM_TYPE"]/'
+ "{jabber:x:data}value[.='urn:xmpp:mam:2']"
+ )
+ if found is None:
+ self['form']['type'] = 'submit'
+ self['form'].add_field(
+ var='FORM_TYPE', ftype='hidden', value='urn:xmpp:mam:2'
+ )
+
+ def get_fields(self):
+ form = self.get_plugin('form', check=True)
+ if not form:
+ return {}
+ return form.get_fields()
+
+ def get_start(self) -> Optional[datetime]:
+ fields = self.get_fields()
field = fields.get('start')
if field:
return xep_0082.parse(field['value'])
+ return None
- def set_start(self, value):
- if isinstance(value, dt.datetime):
+ def set_start(self, value: Union[str, datetime]):
+ self._setup_form()
+ if isinstance(value, datetime):
value = xep_0082.format_datetime(value)
- fields = self.__get_fields()
- field = fields.get('start')
- if field:
- field['value'] = value
- else:
- field = self._form.add_field(var='start')
- field['value'] = value
+ self.set_custom_field('start', value)
- def get_end(self):
- fields = self.__get_fields()
+ def get_end(self) -> Optional[datetime]:
+ fields = self.get_fields()
field = fields.get('end')
if field:
return xep_0082.parse(field['value'])
+ return None
- def set_end(self, value):
- if isinstance(value, dt.datetime):
+ def set_end(self, value: Union[str, datetime]):
+ if isinstance(value, datetime):
value = xep_0082.format_datetime(value)
- fields = self.__get_fields()
- field = fields.get('end')
- if field:
- field['value'] = value
- else:
- field = self._form.add_field(var='end')
- field['value'] = value
+ self.set_custom_field('end', value)
- def get_with(self):
- fields = self.__get_fields()
+ def get_with(self) -> Optional[JID]:
+ fields = self.get_fields()
field = fields.get('with')
if field:
return JID(field['value'])
+ return None
- def set_with(self, value):
- fields = self.__get_fields()
- field = fields.get('with')
+ def set_with(self, value: JID):
+ self.set_custom_field('with', value)
+
+ def set_custom_field(self, fieldname: str, value: Any):
+ self._setup_form()
+ fields = self.get_fields()
+ field = fields.get(fieldname)
if field:
- field['with'] = str(value)
+ field['value'] = str(value)
else:
- field = self._form.add_field(var='with')
+ field = self['form'].add_field(var=fieldname)
field['value'] = str(value)
+
+ def get_custom_field(self, fieldname: str) -> Optional[str]:
+ fields = self.get_fields()
+ field = fields.get(fieldname)
+ if field:
+ return field['value']
+ return None
+
+ def set_before_id(self, value: str):
+ self.set_custom_field('before-id', value)
+
+ def get_before_id(self):
+ self.get_custom_field('before-id')
+
+ def set_after_id(self, value: str):
+ self.set_custom_field('after-id', value)
+
+ def get_after_id(self):
+ self.get_custom_field('after-id')
+
+ def set_ids(self, value: List[str]):
+ self._setup_form()
+ fields = self.get_fields()
+ field = fields.get('ids')
+ if field:
+ field['ids'] = value
+ else:
+ field = self['form'].add_field(var='ids')
+ field['value'] = value
+
+ def get_ids(self):
+ self.get_custom_field('id')
+
# The results interface is meant only as an easy
# way to access the set of collected message responses
# from the query.
- def get_results(self):
+ def get_results(self) -> List[Message]:
return self._results
- def set_results(self, values):
+ def set_results(self, values: List[Message]):
self._results = values
def del_results(self):
self._results = []
-class Preferences(ElementBase):
- name = 'prefs'
+class Fin(ElementBase):
+ """A MAM fin element (end of query).
+
+ .. code-block:: xml
+
+ <iq type='result' id='juliet1'>
+ <fin xmlns='urn:xmpp:mam:2'>
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='0'>28482-98726-73623</first>
+ <last>09af3-cc343-b409f</last>
+ </set>
+ </fin>
+ </iq>
+
+ """
+ name = 'fin'
namespace = 'urn:xmpp:mam:2'
- plugin_attrib = 'mam_prefs'
- interfaces = {'default', 'always', 'never'}
- sub_interfaces = {'always', 'never'}
+ plugin_attrib = 'mam_fin'
+ interfaces = {'results'}
- def get_always(self):
- results = set()
+ def setup(self, xml=None):
+ ElementBase.setup(self, xml)
+ self._results: List[Message] = []
- jids = self.xml.findall('{%s}always/{%s}jid' % (
- self.namespace, self.namespace))
+ # The results interface is meant only as an easy
+ # way to access the set of collected message responses
+ # from the query.
- for jid in jids:
- results.add(JID(jid.text))
+ def get_results(self) -> List[Message]:
+ return self._results
- return results
+ def set_results(self, values: List[Message]):
+ self._results = values
+
+ def del_results(self):
+ self._results = []
- def set_always(self, value):
- self._set_sub_text('always', '', keep=True)
- always = self.xml.find('{%s}always' % self.namespace)
- always.clear()
- if not isinstance(value, (list, set)):
- value = [value]
+class Result(ElementBase):
+ """A MAM result payload.
+
+ .. code-block:: xml
+
+ <message id='aeb213' to='juliet@capulet.lit/chamber'>
+ <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="macbeth@shakespeare.lit">
+ <body>Hail to thee</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """
+ name = 'result'
+ namespace = 'urn:xmpp:mam:2'
+ plugin_attrib = 'mam_result'
+ #: Available interfaces:
+ #:
+ #: - ``queryid``: MAM queryid
+ #: - ``id``: ID of the result
+ interfaces = {'queryid', 'id'}
- for jid in value:
- jid_xml = ET.Element('{%s}jid' % self.namespace)
- jid_xml.text = str(jid)
- always.append(jid_xml)
- def get_never(self):
- results = set()
+class Metadata(ElementBase):
+ """Element containing archive metadata
- jids = self.xml.findall('{%s}never/{%s}jid' % (
- self.namespace, self.namespace))
+ .. code-block:: xml
- for jid in jids:
- results.add(JID(jid.text))
+ <iq type='result' id='jui8921rr9'>
+ <metadata xmlns='urn:xmpp:mam:2'>
+ <start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z' />
+ <end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z' />
+ </metadata>
+ </iq>
- return results
+ """
+ name = 'metadata'
+ namespace = 'urn:xmpp:mam:2'
+ plugin_attrib = 'mam_metadata'
- def set_never(self, value):
- self._set_sub_text('never', '', keep=True)
- never = self.xml.find('{%s}never' % self.namespace)
- never.clear()
- if not isinstance(value, (list, set)):
- value = [value]
+class Start(ElementBase):
+ """Metadata about the start of an archive.
- for jid in value:
- jid_xml = ET.Element('{%s}jid' % self.namespace)
- jid_xml.text = str(jid)
- never.append(jid_xml)
+ .. code-block:: xml
+ <iq type='result' id='jui8921rr9'>
+ <metadata xmlns='urn:xmpp:mam:2'>
+ <start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z' />
+ <end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z' />
+ </metadata>
+ </iq>
-class Fin(ElementBase):
- name = 'fin'
+ """
+ name = 'start'
namespace = 'urn:xmpp:mam:2'
- plugin_attrib = 'mam_fin'
-
-class Result(ElementBase):
- name = 'result'
+ plugin_attrib = name
+ #: Available interfaces:
+ #:
+ #: - ``id``: ID of the first message of the archive
+ #: - ``timestamp`` (``datetime``): timestamp of the first message of the
+ #: archive
+ interfaces = {'id', 'timestamp'}
+
+ def get_timestamp(self) -> Optional[datetime]:
+ """Get the timestamp.
+
+ :returns: The timestamp.
+ """
+ stamp = self.xml.attrib.get('timestamp', None)
+ if stamp is not None:
+ return xep_0082.parse(stamp)
+ return stamp
+
+ def set_timestamp(self, value: Union[datetime, str]):
+ """Set the timestamp.
+
+ :param value: Value of the timestamp (either a datetime or a
+ XEP-0082 timestamp string.
+ """
+ if isinstance(value, str):
+ value = xep_0082.parse(value)
+ value = xep_0082.format_datetime(value)
+ self.xml.attrib['timestamp'] = value
+
+
+class End(ElementBase):
+ """Metadata about the end of an archive.
+
+ .. code-block:: xml
+
+ <iq type='result' id='jui8921rr9'>
+ <metadata xmlns='urn:xmpp:mam:2'>
+ <start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z' />
+ <end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z' />
+ </metadata>
+ </iq>
+
+ """
+ name = 'end'
namespace = 'urn:xmpp:mam:2'
- plugin_attrib = 'mam_result'
- interfaces = {'queryid', 'id'}
+ plugin_attrib = name
+ #: Available interfaces:
+ #:
+ #: - ``id``: ID of the first message of the archive
+ #: - ``timestamp`` (``datetime``): timestamp of the first message of the
+ #: archive
+ interfaces = {'id', 'timestamp'}
+
+ def get_timestamp(self) -> Optional[datetime]:
+ """Get the timestamp.
+
+ :returns: The timestamp.
+ """
+ stamp = self.xml.attrib.get('timestamp', None)
+ if stamp is not None:
+ return xep_0082.parse(stamp)
+ return stamp
+
+ def set_timestamp(self, value: Union[datetime, str]):
+ """Set the timestamp.
+
+ :param value: Value of the timestamp (either a datetime or a
+ XEP-0082 timestamp string.
+ """
+ if isinstance(value, str):
+ value = xep_0082.parse(value)
+ value = xep_0082.format_datetime(value)
+ self.xml.attrib['timestamp'] = value
diff --git a/slixmpp/plugins/xep_0441/__init__.py b/slixmpp/plugins/xep_0441/__init__.py
new file mode 100644
index 00000000..0e169082
--- /dev/null
+++ b/slixmpp/plugins/xep_0441/__init__.py
@@ -0,0 +1,13 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2021 Mathieu Pasquet
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permissio
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0441.stanza import Preferences
+from slixmpp.plugins.xep_0441.mam_prefs import XEP_0441
+
+
+register_plugin(XEP_0441)
+
+__all__ = ['XEP_0441', 'Preferences']
diff --git a/slixmpp/plugins/xep_0441/mam_prefs.py b/slixmpp/plugins/xep_0441/mam_prefs.py
new file mode 100644
index 00000000..06a830bc
--- /dev/null
+++ b/slixmpp/plugins/xep_0441/mam_prefs.py
@@ -0,0 +1,75 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2021 Mathieu Pasquet
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permission
+import logging
+
+from asyncio import Future
+from typing import (
+ List,
+ Optional,
+ Tuple,
+)
+
+from slixmpp import JID
+from slixmpp.types import MAMDefault
+from slixmpp.stanza import Iq
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0441 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0441(BasePlugin):
+
+ """
+ XEP-0441: Message Archive Management Preferences
+ """
+
+ name = 'xep_0441'
+ description = 'XEP-0441: Message Archive Management Preferences'
+ dependencies = {'xep_0313'}
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.Preferences)
+
+ async def get_preferences(self, **iqkwargs
+ ) -> Tuple[MAMDefault, List[JID], List[JID]]:
+ """Get the current MAM preferences.
+
+ :returns: A tuple of MAM preferences with (default, always, never)
+ """
+ ifrom = iqkwargs.pop('ifrom', None)
+ ito = iqkwargs.pop('ito', None)
+ iq = self.xmpp.make_iq_get(ito=ito, ifrom=ifrom)
+ iq['type'] = 'get'
+ query_id = iq['id']
+ iq['mam_prefs']['query_id'] = query_id
+ result = await iq.send(**iqkwargs)
+ return (
+ result['mam_prefs']['default'],
+ result['mam_prefs']['always'],
+ result['mam_prefs']['never']
+ )
+
+ def set_preferences(self, default: Optional[MAMDefault] = 'roster',
+ always: Optional[List[JID]] = None,
+ never: Optional[List[JID]] = None, *,
+ ito: Optional[JID] = None, ifrom: Optional[JID] = None,
+ **iqkwargs) -> Future:
+ """Set MAM Preferences.
+
+ The server answer MAY contain different items.
+
+ :param default: Default behavior (one of 'always', 'never', 'roster').
+ :param always: List of JIDs whose messages will always be stored.
+ :param never: List of JIDs whose messages will never be stored.
+ """
+ iq = self.xmpp.make_iq_set(ito=ito, ifrom=ifrom)
+ iq['mam_prefs']['default'] = default
+ iq['mam_prefs']['always'] = always
+ iq['mam_prefs']['never'] = never
+ return iq.send(**iqkwargs)
diff --git a/slixmpp/plugins/xep_0441/stanza.py b/slixmpp/plugins/xep_0441/stanza.py
new file mode 100644
index 00000000..99dbe802
--- /dev/null
+++ b/slixmpp/plugins/xep_0441/stanza.py
@@ -0,0 +1,91 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2021 Mathieu Pasquet
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permissio
+from typing import (
+ Iterable,
+ Set,
+)
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Preferences(ElementBase):
+ """MAM Preferences payload.
+
+ .. code-block:: xml
+
+ <iq type='set' id='juliet3'>
+ <prefs xmlns='urn:xmpp:mam:2' default='roster'>
+ <always>
+ <jid>romeo@montague.lit</jid>
+ </always>
+ <never>
+ <jid>montague@montague.lit</jid>
+ </never>
+ </prefs>
+ </iq>
+
+ """
+ name = 'prefs'
+ namespace = 'urn:xmpp:mam:2'
+ plugin_attrib = 'mam_prefs'
+ #: Available interfaces:
+ #:
+ #: - ``default``: Default MAM policy (must be one of 'roster', 'always',
+ #: 'never'
+ #: - ``always`` (``List[JID]``): list of JIDs to always store
+ #: conversations with.
+ #: - ``never`` (``List[JID]``): list of JIDs to never store
+ #: conversations with.
+ interfaces = {'default', 'always', 'never'}
+ sub_interfaces = {'always', 'never'}
+
+ def get_always(self) -> Set[JID]:
+ results = set()
+
+ jids = self.xml.findall('{%s}always/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_always(self, value: Iterable[JID]):
+ self._set_sub_text('always', '', keep=True)
+ always = self.xml.find('{%s}always' % self.namespace)
+ always.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ always.append(jid_xml)
+
+ def get_never(self) -> Set[JID]:
+ results = set()
+
+ jids = self.xml.findall('{%s}never/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_never(self, value: Iterable[JID]):
+ self._set_sub_text('never', '', keep=True)
+ never = self.xml.find('{%s}never' % self.namespace)
+ never.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ never.append(jid_xml)
diff --git a/slixmpp/types.py b/slixmpp/types.py
index b9a1d319..453d25e3 100644
--- a/slixmpp/types.py
+++ b/slixmpp/types.py
@@ -76,3 +76,5 @@ MucRoomItemKeys = Literal[
OptJid = Optional[JID]
JidStr = Union[str, JID]
OptJidStr = Optional[Union[str, JID]]
+
+MAMDefault = Literal['always', 'never', 'roster']
diff --git a/tests/test_stanza_xep_0313.py b/tests/test_stanza_xep_0313.py
new file mode 100644
index 00000000..ac5fcb14
--- /dev/null
+++ b/tests/test_stanza_xep_0313.py
@@ -0,0 +1,105 @@
+import unittest
+from slixmpp import JID, Iq, Message
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0313 import stanza
+from slixmpp.plugins.xep_0004.stanza import Form
+from slixmpp.plugins.xep_0297 import stanza as fstanza
+from slixmpp.plugins.xep_0059 import stanza as rstanza
+from slixmpp.xmlstream import register_stanza_plugin
+
+
+class TestMAM(SlixTest):
+
+ def setUp(self):
+ register_stanza_plugin(stanza.MAM, Form)
+ register_stanza_plugin(Iq, stanza.MAM)
+ register_stanza_plugin(Message, stanza.Result)
+ register_stanza_plugin(Iq, stanza.Fin)
+ register_stanza_plugin(
+ stanza.Result,
+ fstanza.Forwarded
+ )
+ register_stanza_plugin(stanza.MAM, rstanza.Set)
+ register_stanza_plugin(stanza.Fin, rstanza.Set)
+
+ register_stanza_plugin(Iq, stanza.Metadata)
+ register_stanza_plugin(stanza.Metadata, stanza.Start)
+ register_stanza_plugin(stanza.Metadata, stanza.End)
+
+ def testMAMQuery(self):
+ """Test that we can build a simple MAM query."""
+ iq = Iq()
+ iq['type'] = 'set'
+ iq['mam']['queryid'] = 'f27'
+
+ self.check(iq, """
+ <iq type='set'>
+ <query xmlns='urn:xmpp:mam:2' queryid='f27'/>
+ </iq>
+ """)
+
+ def testMAMQueryOptions(self):
+ """Test that we can build a mam query with all options."""
+ iq = Iq()
+ iq['type'] = 'set'
+ iq['mam']['with'] = JID('juliet@capulet.lit')
+ iq['mam']['start'] = '2010-06-07T00:00:00Z'
+ iq['mam']['end'] = '2010-07-07T13:23:54Z'
+ iq['mam']['after_id'] = 'id1'
+ iq['mam']['before_id'] = 'id2'
+ iq['mam']['ids'] = ['a', 'b', 'c']
+
+ self.check(iq, """
+ <iq type='set'>
+ <query xmlns='urn:xmpp:mam:2'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>juliet@capulet.lit</value>
+ </field>
+ <field var='start'>
+ <value>2010-06-07T00:00:00Z</value>
+ </field>
+ <field var='end'>
+ <value>2010-07-07T13:23:54Z</value>
+ </field>
+ <field var='after-id'>
+ <value>id1</value>
+ </field>
+ <field var='before-id'>
+ <value>id2</value>
+ </field>
+ <field var='ids'>
+ <value>a</value>
+ <value>b</value>
+ <value>c</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """, use_values=False)
+
+ def testMAMMetadata(self):
+ """Test that we can build a MAM metadata payload"""
+
+ iq = Iq()
+ iq['type'] = 'result'
+ iq['mam_metadata']['start']['id'] = 'YWxwaGEg'
+ iq['mam_metadata']['start']['timestamp'] = '2008-08-22T21:09:04Z'
+ iq['mam_metadata']['end']['id'] = 'b21lZ2Eg'
+ iq['mam_metadata']['end']['timestamp'] = '2020-04-20T14:34:21Z'
+
+ self.check(iq, """
+ <iq type='result'>
+ <metadata xmlns='urn:xmpp:mam:2'>
+ <start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z' />
+ <end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z' />
+ </metadata>
+ </iq>
+ """)
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMAM)
diff --git a/tests/test_stream_xep_0313.py b/tests/test_stream_xep_0313.py
new file mode 100644
index 00000000..25a3a926
--- /dev/null
+++ b/tests/test_stream_xep_0313.py
@@ -0,0 +1,340 @@
+import unittest
+from datetime import datetime
+from slixmpp.test import SlixTest
+from slixmpp import JID
+
+
+class TestMAM(SlixTest):
+
+ def setUp(self):
+ self.stream_start(plugins=['xep_0313'])
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testRetrieveSimple(self):
+ """Test requesting MAM messages without RSM"""
+
+ msgs = []
+
+ async def test():
+ iq = await self.xmpp['xep_0313'].retrieve()
+ for message in iq['mam']['results']:
+ msgs.append(message)
+
+ fut = self.xmpp.wrap(test())
+ self.wait_()
+ self.send("""
+ <iq type='set' id='1'>
+ <query xmlns='urn:xmpp:mam:2' queryid='1' />
+ </iq>
+ """)
+
+ self.recv("""
+ <message id='abc' to='tester@localhost/resource'>
+ <result xmlns='urn:xmpp:mam:2' queryid='1'
+ id='28482-98726-73623'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="tester@localhost">
+ <body>Hail to thee</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1" to="tester@localhost">
+ <fin xmlns="urn:xmpp:mam:2">
+ <first index='0'>28482-98726-73623</first>
+ <last>28482-98726-73623</last>
+ </fin>
+ </iq>
+ """)
+
+ self.run_coro(fut)
+ self.assertEqual(
+ msgs[0]['mam_result']['forwarded']['message']['body'],
+ "Hail to thee",
+ )
+ self.assertEqual(len(msgs),1)
+
+ def testRetrieveRSM(self):
+ """Test requesting MAM messages with RSM"""
+
+ msgs = []
+
+ async def test():
+ iterator = self.xmpp['xep_0313'].retrieve(
+ with_jid=JID('toto@titi'),
+ start='2010-06-07T00:00:00Z',
+ iterator=True,
+ )
+ async for page in iterator:
+ for message in page['mam']['results']:
+ msgs.append(message)
+
+ fut = self.xmpp.wrap(test())
+ self.wait_()
+ self.send("""
+ <iq type='set' id='2'>
+ <query xmlns='urn:xmpp:mam:2' queryid='2'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>toto@titi</value>
+ </field>
+ <field var='start'>
+ <value>2010-06-07T00:00:00Z</value>
+ </field>
+ </x>
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>10</max>
+ </set>
+ </query>
+ </iq>
+ """)
+
+ self.recv("""
+ <message id='abc' to='tester@localhost/resource'>
+ <result xmlns='urn:xmpp:mam:2' queryid='2'
+ id='28482-98726-73623'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="tester@localhost">
+ <body>Hail to thee</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """)
+
+ self.recv("""
+ <iq type="result" id="2" to="tester@localhost">
+ <fin xmlns="urn:xmpp:mam:2">
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='0'>28482-98726-73623</first>
+ <last>28482-98726-73623</last>
+ <count>2</count>
+ </set>
+ </fin>
+ </iq>
+ """)
+
+ self.send("""
+ <iq type='set' id='3'>
+ <query xmlns='urn:xmpp:mam:2' queryid='3'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>toto@titi</value>
+ </field>
+ <field var='start'>
+ <value>2010-06-07T00:00:00Z</value>
+ </field>
+ </x>
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>10</max>
+ <after>28482-98726-73623</after>
+ </set>
+ </query>
+ </iq>
+ """)
+
+ self.recv("""
+ <message id='abc' to='tester@localhost/resource'>
+ <result xmlns='urn:xmpp:mam:2' queryid='3'
+ id='28482-98726-73624'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:26Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="tester@localhost">
+ <body>Hi Y'all</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """)
+
+ self.recv("""
+ <iq type="result" id="3" to="tester@localhost">
+ <fin xmlns="urn:xmpp:mam:2">
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='1'>28482-98726-73624</first>
+ <last>28482-98726-73624</last>
+ <count>2</count>
+ </set>
+ </fin>
+ </iq>
+ """)
+
+ self.run_coro(fut)
+ self.assertEqual(
+ msgs[0]['mam_result']['forwarded']['message']['body'],
+ "Hail to thee",
+ )
+ self.assertEqual(
+ msgs[1]['mam_result']['forwarded']['message']['body'],
+ "Hi Y'all",
+ )
+ self.assertEqual(len(msgs), 2)
+
+ def testIterate(self):
+ """Test iterating over MAM messages with RSM"""
+
+ msgs = []
+
+ async def test():
+ iterator = self.xmpp['xep_0313'].iterate(
+ with_jid=JID('toto@titi'),
+ start='2010-06-07T00:00:00Z',
+ )
+ async for message in iterator:
+ msgs.append(message)
+
+ fut = self.xmpp.wrap(test())
+ self.wait_()
+ self.send("""
+ <iq type='set' id='2'>
+ <query xmlns='urn:xmpp:mam:2' queryid='2'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>toto@titi</value>
+ </field>
+ <field var='start'>
+ <value>2010-06-07T00:00:00Z</value>
+ </field>
+ </x>
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>10</max>
+ </set>
+ </query>
+ </iq>
+ """)
+
+ self.recv("""
+ <message id='abc' to='tester@localhost/resource'>
+ <result xmlns='urn:xmpp:mam:2' queryid='2'
+ id='28482-98726-73623'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="tester@localhost">
+ <body>Hail to thee</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """)
+
+ self.recv("""
+ <iq type="result" id="2" to="tester@localhost">
+ <fin xmlns="urn:xmpp:mam:2">
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='0'>28482-98726-73623</first>
+ <last>28482-98726-73623</last>
+ <count>2</count>
+ </set>
+ </fin>
+ </iq>
+ """)
+
+ self.send("""
+ <iq type='set' id='3'>
+ <query xmlns='urn:xmpp:mam:2' queryid='3'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:mam:2</value>
+ </field>
+ <field var='with'>
+ <value>toto@titi</value>
+ </field>
+ <field var='start'>
+ <value>2010-06-07T00:00:00Z</value>
+ </field>
+ </x>
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>10</max>
+ <after>28482-98726-73623</after>
+ </set>
+ </query>
+ </iq>
+ """)
+
+ self.recv("""
+ <message id='abc' to='tester@localhost/resource'>
+ <result xmlns='urn:xmpp:mam:2' queryid='3'
+ id='28482-98726-73624'>
+ <forwarded xmlns='urn:xmpp:forward:0'>
+ <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:26Z'/>
+ <message xmlns='jabber:client' from="witch@shakespeare.lit"
+ to="tester@localhost">
+ <body>Hi Y'all</body>
+ </message>
+ </forwarded>
+ </result>
+ </message>
+ """)
+
+ self.recv("""
+ <iq type="result" id="3" to="tester@localhost">
+ <fin xmlns="urn:xmpp:mam:2">
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='1'>28482-98726-73624</first>
+ <last>28482-98726-73624</last>
+ <count>2</count>
+ </set>
+ </fin>
+ </iq>
+ """)
+
+ self.run_coro(fut)
+ self.assertEqual(
+ msgs[0]['mam_result']['forwarded']['message']['body'],
+ "Hail to thee",
+ )
+ self.assertEqual(
+ msgs[1]['mam_result']['forwarded']['message']['body'],
+ "Hi Y'all",
+ )
+ self.assertEqual(len(msgs), 2)
+
+ def test_get_metadata(self):
+ """Test a MAM metadata retrieval"""
+ fut = self.xmpp.wrap(
+ self.xmpp.plugin['xep_0313'].get_archive_metadata()
+ )
+ self.wait_()
+ self.send("""
+ <iq type='get' id='1'>
+ <metadata xmlns='urn:xmpp:mam:2'/>
+ </iq>
+ """)
+ self.recv("""
+ <iq type='result' id='1'>
+ <metadata xmlns='urn:xmpp:mam:2'>
+ <start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z' />
+ <end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z' />
+ </metadata>
+ </iq>
+ """)
+ self.run_coro(fut)
+ result = fut.result()
+ self.assertEqual(result['mam_metadata']['start']['id'], "YWxwaGEg")
+ self.assertEqual(
+ result['mam_metadata']['start']['timestamp'],
+ datetime.fromisoformat('2008-08-22T21:09:04+00:00')
+ )
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMAM)