comparison src/plugins/plugin_xep_0313.py @ 1284:41ffe2c2dddc

plugin XEP-0313: update (still draft)
author souliane <souliane@mailoo.org>
date Fri, 09 Jan 2015 10:51:12 +0100
parents 3a3e3014f9f8
children ed2c718bfe03
comparison
equal deleted inserted replaced
1283:7d9ff14a2d9d 1284:41ffe2c2dddc
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core.i18n import _ 22 from sat.core.i18n import _
23 from sat.core.log import getLogger 23 from sat.core.log import getLogger
24 log = getLogger(__name__) 24 log = getLogger(__name__)
25 25
26 from wokkel import disco, iwokkel, compat, data_form
27 from wokkel.rsm import RSMRequest
28 from wokkel.generic import parseXml
29 try: 26 try:
30 from twisted.words.protocols.xmlstream import XMPPHandler 27 from twisted.words.protocols.xmlstream import XMPPHandler
31 except ImportError: 28 except ImportError:
32 from wokkel.subprotocols import XMPPHandler 29 from wokkel.subprotocols import XMPPHandler
33 from twisted.words.xish import domish 30 from twisted.words.xish import domish
31 from twisted.words.protocols.jabber import jid
32
34 from zope.interface import implements 33 from zope.interface import implements
35 34
36 from dateutil.tz import tzutc 35 from wokkel import disco, data_form, mam
36 from wokkel.rsm import RSMRequest
37 from wokkel.generic import parseXml
37 38
38 39
39 NS_MAM = 'urn:xmpp:mam:0' 40 NS_MAM = 'urn:xmpp:mam:0'
40 NS_SF = 'urn:xmpp:forward:0' 41 NS_SF = 'urn:xmpp:forward:0'
41 NS_DD = 'urn:xmpp:delay' 42 NS_DD = 'urn:xmpp:delay'
57 class XEP_0313(object): 58 class XEP_0313(object):
58 59
59 def __init__(self, host): 60 def __init__(self, host):
60 log.info(_("Message Archive Management plugin initialization")) 61 log.info(_("Message Archive Management plugin initialization"))
61 self.host = host 62 self.host = host
62 host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='s', out_sign='s', 63 self.clients = {} # bind profile name to SatMAMClient
63 method=self.queryFields, 64 host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='ss', out_sign='s',
64 async=True, 65 method=self._queryFields,
65 doc={}) 66 async=True,
66 host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssss', out_sign='s', 67 doc={})
68 host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssa{ss}ss', out_sign='s',
67 method=self._queryArchive, 69 method=self._queryArchive,
68 async=True, 70 async=True,
69 doc={}) 71 doc={})
72 host.bridge.addMethod("MAMgetPrefs", ".plugin", in_sign='ss', out_sign='s',
73 method=self._getPrefs,
74 async=True,
75 doc={})
76 host.bridge.addMethod("MAMsetPrefs", ".plugin", in_sign='ssasass', out_sign='s',
77 method=self._setPrefs,
78 async=True,
79 doc={})
70 host.trigger.add("MessageReceived", self.messageReceivedTrigger) 80 host.trigger.add("MessageReceived", self.messageReceivedTrigger)
71 81
72 def getHandler(self, profile): 82 def getHandler(self, profile):
73 return XEP_0313_handler(self, profile) 83 self.clients[profile] = SatMAMClient(self, profile)
74 84 return self.clients[profile]
75 def _queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE): 85
76 form_elt = parseXml(form) if form else None 86 def profileDisconnected(self, profile):
77 rsm_inst = RSMRequest(**rsm) if rsm else None 87 try:
78 return self.queryArchive(form_elt, rsm_inst, node, profile_key) 88 del self.clients[profile]
79 89 except KeyError:
80 def queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE): 90 pass
91
92 def _queryFields(self, service_s=None, profile_key=C.PROF_KEY_NONE):
93 service = jid.JID(service_s) if service_s else None
94 return self.queryFields(service, profile_key)
95
96 def queryFields(self, service=None, profile_key=C.PROF_KEY_NONE):
97 """Ask the server about additional supported fields.
98
99 @param service: entity offering the MAM service (None for user archives)
100 @param profile_key (unicode): %(doc_profile_key)s
101 @return: the server response as a Deferred domish.Element
102 """
103 # http://xmpp.org/extensions/xep-0313.html#query-form
104 def eb(failure):
105 # typically StanzaError with condition u'service-unavailable'
106 log.error(failure.getErrorMessage())
107 return ''
108
109 profile = self.host.memory.getProfileName(profile_key)
110 d = self.clients[profile].queryFields(service)
111 return d.addCallbacks(lambda elt: elt.toXml(), eb)
112
113 def _queryArchive(self, service_s=None, form_xml=None, rsm_dict=None, node=None, profile_key=C.PROF_KEY_NONE):
114 service = jid.JID(service_s) if service_s else None
115 if form_xml:
116 form = data_form.Form.fromElement(parseXml(form_xml))
117 if form.formNamespace != NS_MAM:
118 log.error(_("Expected a MAM Data Form, got instead: %s") % form.formNamespace)
119 form = None
120 else:
121 form = None
122 rsm = RSMRequest(**rsm_dict) if rsm_dict else None
123 return self.queryArchive(service, form, rsm, node, profile_key)
124
125 def queryArchive(self, service=None, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE):
81 """Query a user, MUC or pubsub archive. 126 """Query a user, MUC or pubsub archive.
82 127
83 @param form (domish.Element): data form to filter the request 128 @param service: entity offering the MAM service (None for user archives)
129 @param form (Form): data form to filter the request
84 @param rsm (RSMRequest): RSM request instance 130 @param rsm (RSMRequest): RSM request instance
85 @param node (unicode): pubsub node to query, or None if inappropriate 131 @param node (unicode): pubsub node to query, or None if inappropriate
86 @param profile_key (unicode): %(doc_profile_key)s 132 @param profile_key (unicode): %(doc_profile_key)s
87 @return: a Deferred when the message has been sent 133 @return: a Deferred when the message has been sent
88 """ 134 """
89 client = self.host.getClient(profile_key) 135 def eb(failure):
90 iq = compat.IQ(client.xmlstream, 'set') 136 # typically StanzaError with condition u'service-unavailable'
91 query_elt = iq.addElement((NS_MAM, 'query')) 137 log.error(failure.getErrorMessage())
92 if form: 138 return ''
93 query_elt.addChild(form) 139
94 if rsm: 140 profile = self.host.memory.getProfileName(profile_key)
95 rsm.render(query_elt) 141 d = self.clients[profile].queryArchive(service, form, rsm, node)
96 if node: 142 return d.addCallbacks(lambda elt: elt.toXml(), eb)
97 query_elt['node'] = node 143 # TODO: add the handler for receiving the final message
98 d = iq.send() 144
99 145 def _getPrefs(self, service_s=None, profile_key=C.PROF_KEY_NONE):
100 def eb(failure): 146 service = jid.JID(service_s) if service_s else None
101 # typically StanzaError with condition u'service-unavailable' 147 return self.getPrefs(service, profile_key)
102 log.error(failure.getErrorMessage()) 148
103 return '' 149 def getPrefs(self, service=None, profile_key=C.PROF_KEY_NONE):
104 150 """Retrieve the current user preferences.
105 return d.addCallbacks(lambda elt: elt.toXml(), eb) 151
106 152 @param service: entity offering the MAM service (None for user archives)
107 def queryFields(self, profile_key=C.PROF_KEY_NONE):
108 """Ask the server about additional supported fields.
109
110 @param profile_key (unicode): %(doc_profile_key)s 153 @param profile_key (unicode): %(doc_profile_key)s
111 @return: the server response as a Deferred domish.Element 154 @return: the server response as a Deferred domish.Element
112 """ 155 """
113 # http://xmpp.org/extensions/xep-0313.html#query-form
114 client = self.host.getClient(profile_key)
115 iq = compat.IQ(client.xmlstream, 'get')
116 iq.addElement((NS_MAM, 'query'))
117 d = iq.send()
118
119 def eb(failure):
120 # typically StanzaError with condition u'service-unavailable'
121 log.error(failure.getErrorMessage())
122 return ''
123
124 return d.addCallbacks(lambda elt: elt.toXml(), eb)
125
126 def queryPrefs(self, profile_key=C.PROF_KEY_NONE):
127 """Retrieve the current user preferences.
128
129 @param profile_key (unicode): %(doc_profile_key)s
130 @return: the server response as a Deferred domish.Element
131 """
132 # http://xmpp.org/extensions/xep-0313.html#prefs 156 # http://xmpp.org/extensions/xep-0313.html#prefs
133 client = self.host.getClient(profile_key) 157 def eb(failure):
134 iq = compat.IQ(client.xmlstream, 'get') 158 # typically StanzaError with condition u'service-unavailable'
135 iq.addElement((NS_MAM, 'prefs')) 159 log.error(failure.getErrorMessage())
136 d = iq.send() 160 return ''
137 161
138 def eb(failure): 162 profile = self.host.memory.getProfileName(profile_key)
139 # typically StanzaError with condition u'service-unavailable' 163 d = self.clients[profile].queryPrefs(service)
140 log.error(failure.getErrorMessage()) 164 return d.addCallbacks(lambda elt: elt.toXml(), eb)
141 return '' 165
142 166 def _setPrefs(self, service_s=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
143 return d.addCallbacks(lambda elt: elt.toXml(), eb) 167 service = jid.JID(service_s) if service_s else None
144 168 always_jid = [jid.JID(entity) for entity in always]
145 def setPrefs(self, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE): 169 never_jid = [jid.JID(entity) for entity in never]
170 #TODO: why not build here a MAMPrefs object instead of passing the args separately?
171 return self.setPrefs(service, default, always_jid, never_jid, profile_key)
172
173 def setPrefs(self, service=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
146 """Set news user preferences. 174 """Set news user preferences.
147 175
176 @param service: entity offering the MAM service (None for user archives)
148 @param default (unicode): a value in ('always', 'never', 'roster') 177 @param default (unicode): a value in ('always', 'never', 'roster')
149 @param always (list): a list of JID instances 178 @param always (list): a list of JID instances
150 @param never (list): a list of JID instances 179 @param never (list): a list of JID instances
151 @param profile_key (unicode): %(doc_profile_key)s 180 @param profile_key (unicode): %(doc_profile_key)s
152 @return: the server response as a Deferred domish.Element 181 @return: the server response as a Deferred domish.Element
153 """ 182 """
154 # http://xmpp.org/extensions/xep-0313.html#prefs 183 # http://xmpp.org/extensions/xep-0313.html#prefs
155 assert(default in ('always', 'never', 'roster')) 184 def eb(failure):
156 client = self.host.getClient(profile_key) 185 # typically StanzaError with condition u'service-unavailable'
157 iq = compat.IQ(client.xmlstream, 'set') 186 log.error(failure.getErrorMessage())
158 prefs = iq.addElement((NS_MAM, 'prefs')) 187 return ''
159 prefs['default'] = default 188
160 189 profile = self.host.memory.getProfileName(profile_key)
161 for var, attr in ((always, 'always'), (never, 'never')): 190 d = self.clients[profile].setPrefs(service, default, always, never)
162 if var is not None: 191 return d.addCallbacks(lambda elt: elt.toXml(), eb)
163 elt = prefs.addElement((None, attr))
164 for entity in var:
165 elt.addElement((None, 'jid')).addContent(entity.full())
166 d = iq.send()
167
168 def eb(failure):
169 # typically StanzaError with condition u'service-unavailable'
170 log.error(failure.getErrorMessage())
171 return ''
172
173 return d.addCallbacks(lambda elt: elt.toXml(), eb)
174
175 @classmethod
176 def datetime2utc(cls, datetime):
177 """Convert a datetime to a XEP-0082 compliant UTC datetime.
178
179 @param datetime (datetime): offset-aware timestamp to convert.
180 @return: unicode
181 """
182 # receipt from wokkel.delay.Delay.toElement
183 stampFormat = '%Y-%m-%dT%H:%M:%SZ'
184 return datetime.astimezone(tzutc()).strftime(stampFormat)
185
186 @classmethod
187 def buildForm(cls, start=None, end=None, with_jid=None, extra=None):
188 """Prepare a data form for MAM query.
189
190 @param start (datetime): offset-aware timestamp to filter out older messages.
191 @param end (datetime): offset-aware timestamp to filter out later messages.
192 @param with_jid (JID): JID against which to match messages.
193 @param extra (list): list of extra fields that are not defined by the
194 specification. Each element must be a 3-tuple containing the field
195 type, name and value.
196 @return: a XEP-0004 data form as domish.Element
197 """
198 form = data_form.Form('submit', formNamespace=NS_MAM)
199 if start:
200 form.addField(data_form.Field('text-single', 'start', XEP_0313.datetime2utc(start)))
201 if end:
202 form.addField(data_form.Field('text-single', 'end', XEP_0313.datetime2utc(end)))
203 if with_jid:
204 form.addField(data_form.Field('jid-single', 'with', with_jid.full()))
205 if extra is not None:
206 for field in extra:
207 form.addField(data_form.Field(*field))
208 return form.toElement()
209 192
210 def messageReceivedTrigger(self, message, post_treat, profile): 193 def messageReceivedTrigger(self, message, post_treat, profile):
211 """Check if the message is a MAM result. If so, extract the original 194 """Check if the message is a MAM result. If so, extract the original
212 message, stop processing the current message and process the original 195 message, stop processing the current message and process the original
213 message instead. 196 message instead.
232 client = self.host.getClient(profile) 215 client = self.host.getClient(profile)
233 client.messageProt.onMessage(msg) 216 client.messageProt.onMessage(msg)
234 return False 217 return False
235 218
236 219
237 class XEP_0313_handler(XMPPHandler): 220 class SatMAMClient(mam.MAMClient):
238 implements(iwokkel.IDisco) 221 implements(disco.IDisco)
239 222
240 def __init__(self, plugin_parent, profile): 223 def __init__(self, plugin_parent, profile):
241 self.plugin_parent = plugin_parent 224 self.plugin_parent = plugin_parent
242 self.host = plugin_parent.host 225 self.host = plugin_parent.host
243 self.profile = profile 226 self.profile = profile
227 mam.MAMClient.__init__(self)
244 228
245 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 229 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
246 return [disco.DiscoFeature(NS_MAM)] 230 return [disco.DiscoFeature(NS_MAM)]
247 231
248 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 232 def getDiscoItems(self, requestor, target, nodeIdentifier=''):