Mercurial > libervia-backend
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=''): |