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 []