changeset 1284:41ffe2c2dddc

plugin XEP-0313: update (still draft)
author souliane <souliane@mailoo.org>
date Fri, 09 Jan 2015 10:51:12 +0100
parents 7d9ff14a2d9d
children ed2c718bfe03
files src/plugins/plugin_xep_0313.py src/test/helpers.py src/test/test_plugin_xep_0313.py
diffstat 3 files changed, 137 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0313.py	Fri Jan 09 10:50:11 2015 +0100
+++ b/src/plugins/plugin_xep_0313.py	Fri Jan 09 10:51:12 2015 +0100
@@ -23,17 +23,18 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from wokkel import disco, iwokkel, compat, data_form
-from wokkel.rsm import RSMRequest
-from wokkel.generic import parseXml
 try:
     from twisted.words.protocols.xmlstream import XMPPHandler
 except ImportError:
     from wokkel.subprotocols import XMPPHandler
 from twisted.words.xish import domish
+from twisted.words.protocols.jabber import jid
+
 from zope.interface import implements
 
-from dateutil.tz import tzutc
+from wokkel import disco, data_form, mam
+from wokkel.rsm import RSMRequest
+from wokkel.generic import parseXml
 
 
 NS_MAM = 'urn:xmpp:mam:0'
@@ -59,92 +60,120 @@
     def __init__(self, host):
         log.info(_("Message Archive Management plugin initialization"))
         self.host = host
-        host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='s', out_sign='s',
-                              method=self.queryFields,
+        self.clients = {}  # bind profile name to SatMAMClient
+        host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='ss', out_sign='s',
+                              method=self._queryFields,
                               async=True,
                               doc={})
-        host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssss', out_sign='s',
+        host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssa{ss}ss', out_sign='s',
                               method=self._queryArchive,
                               async=True,
                               doc={})
+        host.bridge.addMethod("MAMgetPrefs", ".plugin", in_sign='ss', out_sign='s',
+                              method=self._getPrefs,
+                              async=True,
+                              doc={})
+        host.bridge.addMethod("MAMsetPrefs", ".plugin", in_sign='ssasass', out_sign='s',
+                              method=self._setPrefs,
+                              async=True,
+                              doc={})
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
 
     def getHandler(self, profile):
-        return XEP_0313_handler(self, profile)
+        self.clients[profile] = SatMAMClient(self, profile)
+        return self.clients[profile]
 
-    def _queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE):
-        form_elt = parseXml(form) if form else None
-        rsm_inst = RSMRequest(**rsm) if rsm else None
-        return self.queryArchive(form_elt, rsm_inst, node, profile_key)
+    def profileDisconnected(self, profile):
+        try:
+            del self.clients[profile]
+        except KeyError:
+            pass
 
-    def queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE):
-        """Query a user, MUC or pubsub archive.
+    def _queryFields(self, service_s=None, profile_key=C.PROF_KEY_NONE):
+        service = jid.JID(service_s) if service_s else None
+        return self.queryFields(service, profile_key)
 
-        @param form (domish.Element): data form to filter the request
-        @param rsm (RSMRequest): RSM request instance
-        @param node (unicode): pubsub node to query, or None if inappropriate
+    def queryFields(self, service=None, profile_key=C.PROF_KEY_NONE):
+        """Ask the server about additional supported fields.
+
+        @param service: entity offering the MAM service (None for user archives)
         @param profile_key (unicode): %(doc_profile_key)s
-        @return: a Deferred when the message has been sent
+        @return: the server response as a Deferred domish.Element
         """
-        client = self.host.getClient(profile_key)
-        iq = compat.IQ(client.xmlstream, 'set')
-        query_elt = iq.addElement((NS_MAM, 'query'))
-        if form:
-            query_elt.addChild(form)
-        if rsm:
-            rsm.render(query_elt)
-        if node:
-            query_elt['node'] = node
-        d = iq.send()
-
+        # http://xmpp.org/extensions/xep-0313.html#query-form
         def eb(failure):
             # typically StanzaError with condition u'service-unavailable'
             log.error(failure.getErrorMessage())
             return ''
 
+        profile = self.host.memory.getProfileName(profile_key)
+        d = self.clients[profile].queryFields(service)
         return d.addCallbacks(lambda elt: elt.toXml(), eb)
 
-    def queryFields(self, profile_key=C.PROF_KEY_NONE):
-        """Ask the server about additional supported fields.
+    def _queryArchive(self, service_s=None, form_xml=None, rsm_dict=None, node=None, profile_key=C.PROF_KEY_NONE):
+        service = jid.JID(service_s) if service_s else None
+        if form_xml:
+            form = data_form.Form.fromElement(parseXml(form_xml))
+            if form.formNamespace != NS_MAM:
+                log.error(_("Expected a MAM Data Form, got instead: %s") % form.formNamespace)
+                form = None
+        else:
+            form = None
+        rsm = RSMRequest(**rsm_dict) if rsm_dict else None
+        return self.queryArchive(service, form, rsm, node, profile_key)
 
+    def queryArchive(self, service=None, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE):
+        """Query a user, MUC or pubsub archive.
+
+        @param service: entity offering the MAM service (None for user archives)
+        @param form (Form): data form to filter the request
+        @param rsm (RSMRequest): RSM request instance
+        @param node (unicode): pubsub node to query, or None if inappropriate
         @param profile_key (unicode): %(doc_profile_key)s
-        @return: the server response as a Deferred domish.Element
+        @return: a Deferred when the message has been sent
         """
-        # http://xmpp.org/extensions/xep-0313.html#query-form
-        client = self.host.getClient(profile_key)
-        iq = compat.IQ(client.xmlstream, 'get')
-        iq.addElement((NS_MAM, 'query'))
-        d = iq.send()
-
         def eb(failure):
             # typically StanzaError with condition u'service-unavailable'
             log.error(failure.getErrorMessage())
             return ''
 
+        profile = self.host.memory.getProfileName(profile_key)
+        d = self.clients[profile].queryArchive(service, form, rsm, node)
         return d.addCallbacks(lambda elt: elt.toXml(), eb)
+        # TODO: add the handler for receiving the final message
 
-    def queryPrefs(self, profile_key=C.PROF_KEY_NONE):
+    def _getPrefs(self, service_s=None, profile_key=C.PROF_KEY_NONE):
+        service = jid.JID(service_s) if service_s else None
+        return self.getPrefs(service, profile_key)
+
+    def getPrefs(self, service=None, profile_key=C.PROF_KEY_NONE):
         """Retrieve the current user preferences.
 
+        @param service: entity offering the MAM service (None for user archives)
         @param profile_key (unicode): %(doc_profile_key)s
         @return: the server response as a Deferred domish.Element
         """
         # http://xmpp.org/extensions/xep-0313.html#prefs
-        client = self.host.getClient(profile_key)
-        iq = compat.IQ(client.xmlstream, 'get')
-        iq.addElement((NS_MAM, 'prefs'))
-        d = iq.send()
-
         def eb(failure):
             # typically StanzaError with condition u'service-unavailable'
             log.error(failure.getErrorMessage())
             return ''
 
+        profile = self.host.memory.getProfileName(profile_key)
+        d = self.clients[profile].queryPrefs(service)
         return d.addCallbacks(lambda elt: elt.toXml(), eb)
 
-    def setPrefs(self, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
+    def _setPrefs(self, service_s=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
+        service = jid.JID(service_s) if service_s else None
+        always_jid = [jid.JID(entity) for entity in always]
+        never_jid = [jid.JID(entity) for entity in never]
+        #TODO: why not build here a MAMPrefs object instead of passing the args separately?
+        return self.setPrefs(service, default, always_jid, never_jid, profile_key)
+
+    def setPrefs(self, service=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
         """Set news user preferences.
 
+        @param service: entity offering the MAM service (None for user archives)
         @param default (unicode): a value in ('always', 'never', 'roster')
         @param always (list): a list of JID instances
         @param never (list): a list of JID instances
@@ -152,61 +181,15 @@
         @return: the server response as a Deferred domish.Element
         """
         # http://xmpp.org/extensions/xep-0313.html#prefs
-        assert(default in ('always', 'never', 'roster'))
-        client = self.host.getClient(profile_key)
-        iq = compat.IQ(client.xmlstream, 'set')
-        prefs = iq.addElement((NS_MAM, 'prefs'))
-        prefs['default'] = default
-
-        for var, attr in ((always, 'always'), (never, 'never')):
-            if var is not None:
-                elt = prefs.addElement((None, attr))
-                for entity in var:
-                    elt.addElement((None, 'jid')).addContent(entity.full())
-        d = iq.send()
-
         def eb(failure):
             # typically StanzaError with condition u'service-unavailable'
             log.error(failure.getErrorMessage())
             return ''
 
+        profile = self.host.memory.getProfileName(profile_key)
+        d = self.clients[profile].setPrefs(service, default, always, never)
         return d.addCallbacks(lambda elt: elt.toXml(), eb)
 
-    @classmethod
-    def datetime2utc(cls, datetime):
-        """Convert a datetime to a XEP-0082 compliant UTC datetime.
-
-        @param datetime (datetime): offset-aware timestamp to convert.
-        @return: unicode
-        """
-        # receipt from wokkel.delay.Delay.toElement
-        stampFormat = '%Y-%m-%dT%H:%M:%SZ'
-        return datetime.astimezone(tzutc()).strftime(stampFormat)
-
-    @classmethod
-    def buildForm(cls, start=None, end=None, with_jid=None, extra=None):
-        """Prepare a data form for MAM query.
-
-        @param start (datetime): offset-aware timestamp to filter out older messages.
-        @param end (datetime): offset-aware timestamp to filter out later messages.
-        @param with_jid (JID): JID against which to match messages.
-        @param extra (list): list of extra fields that are not defined by the
-            specification. Each element must be a 3-tuple containing the field
-            type, name and value.
-        @return: a XEP-0004 data form as domish.Element
-        """
-        form = data_form.Form('submit', formNamespace=NS_MAM)
-        if start:
-            form.addField(data_form.Field('text-single', 'start', XEP_0313.datetime2utc(start)))
-        if end:
-            form.addField(data_form.Field('text-single', 'end', XEP_0313.datetime2utc(end)))
-        if with_jid:
-            form.addField(data_form.Field('jid-single', 'with', with_jid.full()))
-        if extra is not None:
-            for field in extra:
-                form.addField(data_form.Field(*field))
-        return form.toElement()
-
     def messageReceivedTrigger(self, message, post_treat, profile):
         """Check if the message is a MAM result. If so, extract the original
         message, stop processing the current message and process the original
@@ -234,13 +217,14 @@
         return False
 
 
-class XEP_0313_handler(XMPPHandler):
-    implements(iwokkel.IDisco)
+class SatMAMClient(mam.MAMClient):
+    implements(disco.IDisco)
 
     def __init__(self, plugin_parent, profile):
         self.plugin_parent = plugin_parent
         self.host = plugin_parent.host
         self.profile = profile
+        mam.MAMClient.__init__(self)
 
     def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
         return [disco.DiscoFeature(NS_MAM)]
--- a/src/test/helpers.py	Fri Jan 09 10:50:11 2015 +0100
+++ b/src/test/helpers.py	Fri Jan 09 10:51:12 2015 +0100
@@ -115,6 +115,10 @@
         """
         return mess_data  # TODO
 
+    def getProfileName(self, profile_key):
+        """Get the profile name from the profile_key"""
+        return profile_key
+
     def getClient(self, profile_key):
         """Convenient method to get client from profile key
         @return: client or None if it doesn't exist"""
--- a/src/test/test_plugin_xep_0313.py	Fri Jan 09 10:50:11 2015 +0100
+++ b/src/test/test_plugin_xep_0313.py	Fri Jan 09 10:51:12 2015 +0100
@@ -28,8 +28,11 @@
 from dateutil.tz import tzutc
 import datetime
 from wokkel.rsm import RSMRequest
+from wokkel.mam import buildForm
 
 NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
+SERVICE = 'sat-pubsub.tazar.int'
+SERVICE_JID = JID(SERVICE)
 
 
 class XEP_0313Test(helpers.SatTestCase):
@@ -37,30 +40,32 @@
     def setUp(self):
         self.host = helpers.FakeSAT()
         self.plugin = XEP_0313(self.host)
+        client = self.plugin.getHandler(C.PROFILE[0])
+        client.makeConnection(self.host.getClient(C.PROFILE[0]).xmlstream)
 
     def test_queryArchive(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'/>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        d = self.plugin.queryArchive(profile_key=C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        d = self.plugin.queryArchive(SERVICE_JID, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchivePubsub(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0' node='fdp/submitted/capulet.lit/sonnets' />
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        d = self.plugin.queryArchive(node="fdp/submitted/capulet.lit/sonnets", profile_key=C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        d = self.plugin.queryArchive(SERVICE_JID, node="fdp/submitted/capulet.lit/sonnets", profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveWith(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
             <x xmlns='jabber:x:data' type='submit'>
               <field var='FORM_TYPE' type='hidden'>
@@ -72,15 +77,15 @@
             </x>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        form = self.plugin.buildForm(with_jid=JID('juliet@capulet.lit'))
-        d = self.plugin.queryArchive(form, profile_key=C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        form = buildForm(with_jid=JID('juliet@capulet.lit'))
+        d = self.plugin.queryArchive(SERVICE_JID, form, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveStartEnd(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
             <x xmlns='jabber:x:data' type='submit'>
               <field var='FORM_TYPE' type='hidden'>
@@ -95,17 +100,17 @@
             </x>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
         start = datetime.datetime(2010, 6, 7, 0, 0, 0, tzinfo=tzutc())
         end = datetime.datetime(2010, 7, 7, 13, 23, 54, tzinfo=tzutc())
-        form = self.plugin.buildForm(start=start, end=end)
-        d = self.plugin.queryArchive(form, profile_key=C.PROFILE[0])
+        form = buildForm(start=start, end=end)
+        d = self.plugin.queryArchive(SERVICE_JID, form, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveStart(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
             <x xmlns='jabber:x:data' type='submit'>
               <field var='FORM_TYPE' type='hidden'>
@@ -117,16 +122,16 @@
             </x>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
         start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc())
-        form = self.plugin.buildForm(start=start)
-        d = self.plugin.queryArchive(form, profile_key=C.PROFILE[0])
+        form = buildForm(start=start)
+        d = self.plugin.queryArchive(SERVICE_JID, form, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveRSM(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
             <x xmlns='jabber:x:data' type='submit'>
               <field var='FORM_TYPE' type='hidden'>
@@ -141,17 +146,17 @@
             </set>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
         start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc())
-        form = self.plugin.buildForm(start=start)
+        form = buildForm(start=start)
         rsm = RSMRequest(max=10)
-        d = self.plugin.queryArchive(form=form, rsm=rsm, profile_key=C.PROFILE[0])
+        d = self.plugin.queryArchive(SERVICE_JID, form=form, rsm=rsm, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveRSMPaging(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
               <x xmlns='jabber:x:data' type='submit'>
                 <field var='FORM_TYPE' type='hidden'><value>urn:xmpp:mam:0</value></field>
@@ -163,27 +168,27 @@
               </set>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
         start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc())
-        form = self.plugin.buildForm(start=start)
+        form = buildForm(start=start)
         rsm = RSMRequest(max=10, after=u'09af3-cc343-b409f')
-        d = self.plugin.queryArchive(form=form, rsm=rsm, profile_key=C.PROFILE[0])
+        d = self.plugin.queryArchive(SERVICE_JID, form=form, rsm=rsm, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryFields(self):
         xml = """
-        <iq type='get' id="%s">
+        <iq type='get' id="%s" to='%s'>
           <query xmlns='urn:xmpp:mam:0'/>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        d = self.plugin.queryFields(C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        d = self.plugin.queryFields(SERVICE_JID, C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryArchiveFields(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <query xmlns='urn:xmpp:mam:0'>
             <x xmlns='jabber:x:data' type='submit'>
               <field type='hidden' var='FORM_TYPE'>
@@ -198,29 +203,34 @@
             </x>
           </query>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        extra = (('text-single', 'urn:example:xmpp:free-text-search',
-                  'Where arth thou, my Juliet?'),
-                 ('text-single', 'urn:example:xmpp:stanza-content',
-                  '{http://jabber.org/protocol/mood}mood/lonely'))
-        form = self.plugin.buildForm(extra=extra)
-        d = self.plugin.queryArchive(form=form, profile_key=C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        extra = [{'fieldType': 'text-single',
+                  'var': 'urn:example:xmpp:free-text-search',
+                  'value': 'Where arth thou, my Juliet?'},
+                 {'fieldType': 'text-single',
+                  'var': 'urn:example:xmpp:stanza-content',
+                  'value': '{http://jabber.org/protocol/mood}mood/lonely'}]
+        form = buildForm(extra=extra)
+        d = self.plugin.queryArchive(SERVICE_JID, form=form, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_queryPrefs(self):
         xml = """
-        <iq type='get' id='%s'>
-          <prefs xmlns='urn:xmpp:mam:0'/>
+        <iq type='get' id='%s' to='%s'>
+          <prefs xmlns='urn:xmpp:mam:0'>
+            <always/>
+            <never/>
+          </prefs>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
-        d = self.plugin.queryPrefs(profile_key=C.PROFILE[0])
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
+        d = self.plugin.getPrefs(SERVICE_JID, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d
 
     def test_setPrefs(self):
         xml = """
-        <iq type='set' id='%s'>
+        <iq type='set' id='%s' to='%s'>
           <prefs xmlns='urn:xmpp:mam:0' default='roster'>
             <always>
               <jid>romeo@montague.lit</jid>
@@ -230,9 +240,9 @@
             </never>
           </prefs>
         </iq>
-        """ % ("H_%d" % domish.Element._idCounter)
+        """ % (("H_%d" % domish.Element._idCounter), SERVICE)
         always = [JID('romeo@montague.lit')]
         never = [JID('montague@montague.lit')]
-        d = self.plugin.setPrefs(always=always, never=never, profile_key=C.PROFILE[0])
+        d = self.plugin.setPrefs(SERVICE_JID, always=always, never=never, profile_key=C.PROFILE[0])
         d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True))
         return d