Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0313.py @ 1277:3a3e3014f9f8
plugin XEP-0313: first draft:
- you can already test it with d-feet but it will bug unless you apply the changeset 60dfa2f5d61f which is waiting in the branch "frontends_multi_profiles" (actually just one "assert" to comment in plugin_xep_0085.py)
author | souliane <souliane@mailoo.org> |
---|---|
date | Fri, 19 Dec 2014 14:43:42 +0100 |
parents | |
children | 41ffe2c2dddc |
comparison
equal
deleted
inserted
replaced
1276:56adf73bedeb | 1277:3a3e3014f9f8 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Message Archive Management (XEP-0313) | |
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) | |
6 # Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.org) | |
7 | |
8 # This program is free software: you can redistribute it and/or modify | |
9 # it under the terms of the GNU Affero General Public License as published by | |
10 # the Free Software Foundation, either version 3 of the License, or | |
11 # (at your option) any later version. | |
12 | |
13 # This program is distributed in the hope that it will be useful, | |
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 # GNU Affero General Public License for more details. | |
17 | |
18 # You should have received a copy of the GNU Affero General Public License | |
19 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 | |
21 from sat.core.constants import Const as C | |
22 from sat.core.i18n import _ | |
23 from sat.core.log import getLogger | |
24 log = getLogger(__name__) | |
25 | |
26 from wokkel import disco, iwokkel, compat, data_form | |
27 from wokkel.rsm import RSMRequest | |
28 from wokkel.generic import parseXml | |
29 try: | |
30 from twisted.words.protocols.xmlstream import XMPPHandler | |
31 except ImportError: | |
32 from wokkel.subprotocols import XMPPHandler | |
33 from twisted.words.xish import domish | |
34 from zope.interface import implements | |
35 | |
36 from dateutil.tz import tzutc | |
37 | |
38 | |
39 NS_MAM = 'urn:xmpp:mam:0' | |
40 NS_SF = 'urn:xmpp:forward:0' | |
41 NS_DD = 'urn:xmpp:delay' | |
42 NS_CLIENT = 'jabber:client' | |
43 | |
44 PLUGIN_INFO = { | |
45 "name": "Message Archive Management", | |
46 "import_name": "XEP-0313", | |
47 "type": "XEP", | |
48 "protocols": ["XEP-0313"], | |
49 "dependencies": ["XEP-0059", "XEP-0297", "XEP-0203"], | |
50 "recommendations": ["XEP-0334"], | |
51 "main": "XEP_0313", | |
52 "handler": "yes", | |
53 "description": _("""Implementation of Message Archive Management""") | |
54 } | |
55 | |
56 | |
57 class XEP_0313(object): | |
58 | |
59 def __init__(self, host): | |
60 log.info(_("Message Archive Management plugin initialization")) | |
61 self.host = host | |
62 host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='s', out_sign='s', | |
63 method=self.queryFields, | |
64 async=True, | |
65 doc={}) | |
66 host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssss', out_sign='s', | |
67 method=self._queryArchive, | |
68 async=True, | |
69 doc={}) | |
70 host.trigger.add("MessageReceived", self.messageReceivedTrigger) | |
71 | |
72 def getHandler(self, profile): | |
73 return XEP_0313_handler(self, profile) | |
74 | |
75 def _queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE): | |
76 form_elt = parseXml(form) if form else None | |
77 rsm_inst = RSMRequest(**rsm) if rsm else None | |
78 return self.queryArchive(form_elt, rsm_inst, node, profile_key) | |
79 | |
80 def queryArchive(self, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE): | |
81 """Query a user, MUC or pubsub archive. | |
82 | |
83 @param form (domish.Element): data form to filter the request | |
84 @param rsm (RSMRequest): RSM request instance | |
85 @param node (unicode): pubsub node to query, or None if inappropriate | |
86 @param profile_key (unicode): %(doc_profile_key)s | |
87 @return: a Deferred when the message has been sent | |
88 """ | |
89 client = self.host.getClient(profile_key) | |
90 iq = compat.IQ(client.xmlstream, 'set') | |
91 query_elt = iq.addElement((NS_MAM, 'query')) | |
92 if form: | |
93 query_elt.addChild(form) | |
94 if rsm: | |
95 rsm.render(query_elt) | |
96 if node: | |
97 query_elt['node'] = node | |
98 d = iq.send() | |
99 | |
100 def eb(failure): | |
101 # typically StanzaError with condition u'service-unavailable' | |
102 log.error(failure.getErrorMessage()) | |
103 return '' | |
104 | |
105 return d.addCallbacks(lambda elt: elt.toXml(), eb) | |
106 | |
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 | |
111 @return: the server response as a Deferred domish.Element | |
112 """ | |
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 | |
133 client = self.host.getClient(profile_key) | |
134 iq = compat.IQ(client.xmlstream, 'get') | |
135 iq.addElement((NS_MAM, 'prefs')) | |
136 d = iq.send() | |
137 | |
138 def eb(failure): | |
139 # typically StanzaError with condition u'service-unavailable' | |
140 log.error(failure.getErrorMessage()) | |
141 return '' | |
142 | |
143 return d.addCallbacks(lambda elt: elt.toXml(), eb) | |
144 | |
145 def setPrefs(self, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE): | |
146 """Set news user preferences. | |
147 | |
148 @param default (unicode): a value in ('always', 'never', 'roster') | |
149 @param always (list): a list of JID instances | |
150 @param never (list): a list of JID instances | |
151 @param profile_key (unicode): %(doc_profile_key)s | |
152 @return: the server response as a Deferred domish.Element | |
153 """ | |
154 # http://xmpp.org/extensions/xep-0313.html#prefs | |
155 assert(default in ('always', 'never', 'roster')) | |
156 client = self.host.getClient(profile_key) | |
157 iq = compat.IQ(client.xmlstream, 'set') | |
158 prefs = iq.addElement((NS_MAM, 'prefs')) | |
159 prefs['default'] = default | |
160 | |
161 for var, attr in ((always, 'always'), (never, 'never')): | |
162 if var is not None: | |
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 | |
210 def messageReceivedTrigger(self, message, post_treat, profile): | |
211 """Check if the message is a MAM result. If so, extract the original | |
212 message, stop processing the current message and process the original | |
213 message instead. | |
214 """ | |
215 try: | |
216 result = domish.generateElementsQNamed(message.elements(), "result", NS_MAM).next() | |
217 except StopIteration: | |
218 return True | |
219 try: | |
220 forwarded = domish.generateElementsQNamed(result.elements(), "forwarded", NS_SF).next() | |
221 except StopIteration: | |
222 log.error(_("MAM result misses its <forwarded/> mandatory element!")) | |
223 return False | |
224 try: | |
225 # TODO: delay is not here for nothing, get benefice of it! | |
226 delay = domish.generateElementsQNamed(forwarded.elements(), "delay", NS_DD).next() | |
227 msg = domish.generateElementsQNamed(forwarded.elements(), "message", NS_CLIENT).next() | |
228 except StopIteration: | |
229 log.error(_("<forwarded/> element misses a mandatory child!")) | |
230 return False | |
231 log.debug(_("MAM found a forwarded message")) | |
232 client = self.host.getClient(profile) | |
233 client.messageProt.onMessage(msg) | |
234 return False | |
235 | |
236 | |
237 class XEP_0313_handler(XMPPHandler): | |
238 implements(iwokkel.IDisco) | |
239 | |
240 def __init__(self, plugin_parent, profile): | |
241 self.plugin_parent = plugin_parent | |
242 self.host = plugin_parent.host | |
243 self.profile = profile | |
244 | |
245 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
246 return [disco.DiscoFeature(NS_MAM)] | |
247 | |
248 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
249 return [] |