comparison sat/plugins/plugin_exp_events.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_exp_events.py@0046283a285d
children 3e4e78de9cca
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin to detect language (experimental)
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core import exceptions
22 from sat.core.constants import Const as C
23 from sat.core.log import getLogger
24 log = getLogger(__name__)
25 from sat.tools import utils
26 from sat.tools.common import uri as uri_parse
27 from twisted.internet import defer
28 from twisted.words.protocols.jabber import jid, error
29 from twisted.words.xish import domish
30 from wokkel import pubsub
31
32
33 PLUGIN_INFO = {
34 C.PI_NAME: "Event plugin",
35 C.PI_IMPORT_NAME: "EVENTS",
36 C.PI_TYPE: "EXP",
37 C.PI_PROTOCOLS: [],
38 C.PI_DEPENDENCIES: ["XEP-0060"],
39 C.PI_RECOMMENDATIONS: ["INVITATIONS", "XEP-0277"],
40 C.PI_MAIN: "Events",
41 C.PI_HANDLER: "no",
42 C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management""")
43 }
44
45 NS_EVENT = 'org.salut-a-toi.event:0'
46
47
48 class Events(object):
49 """Q&D module to handle event attendance answer, experimentation only"""
50
51 def __init__(self, host):
52 log.info(_(u"Event plugin initialization"))
53 self.host = host
54 self._p = self.host.plugins["XEP-0060"]
55 self._i = self.host.plugins.get("INVITATIONS")
56 self._b = self.host.plugins.get("XEP-0277")
57 host.bridge.addMethod("eventGet", ".plugin",
58 in_sign='ssss', out_sign='(ia{ss})',
59 method=self._eventGet,
60 async=True)
61 host.bridge.addMethod("eventCreate", ".plugin",
62 in_sign='ia{ss}ssss', out_sign='s',
63 method=self._eventCreate,
64 async=True)
65 host.bridge.addMethod("eventModify", ".plugin",
66 in_sign='sssia{ss}s', out_sign='',
67 method=self._eventModify,
68 async=True)
69 host.bridge.addMethod("eventInviteeGet", ".plugin",
70 in_sign='sss', out_sign='a{ss}',
71 method=self._eventInviteeGet,
72 async=True)
73 host.bridge.addMethod("eventInviteeSet", ".plugin",
74 in_sign='ssa{ss}s', out_sign='',
75 method=self._eventInviteeSet,
76 async=True)
77 host.bridge.addMethod("eventInviteesList", ".plugin",
78 in_sign='sss', out_sign='a{sa{ss}}',
79 method=self._eventInviteesList,
80 async=True),
81 host.bridge.addMethod("eventInvite", ".plugin", in_sign='ssssassssssss', out_sign='',
82 method=self._invite,
83 async=True)
84
85 def _eventGet(self, service, node, id_=u'', profile_key=C.PROF_KEY_NONE):
86 service = jid.JID(service) if service else None
87 node = node if node else NS_EVENT
88 client = self.host.getClient(profile_key)
89 return self.eventGet(client, service, node, id_)
90
91 @defer.inlineCallbacks
92 def eventGet(self, client, service, node, id_=NS_EVENT):
93 """Retrieve event data
94
95 @param service(unicode, None): PubSub service
96 @param node(unicode): PubSub node of the event
97 @param id_(unicode): id_ with even data
98 @return (tuple[int, dict[unicode, unicode]): event data:
99 - timestamp of the event
100 - event metadata where key can be:
101 location: location of the event
102 image: URL of a picture to use to represent event
103 background-image: URL of a picture to use in background
104 """
105 if not id_:
106 id_ = NS_EVENT
107 items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_])
108 try:
109 event_elt = next(items[0].elements(NS_EVENT, u'event'))
110 except IndexError:
111 raise exceptions.NotFound(_(u"No event with this id has been found"))
112
113 try:
114 timestamp = utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
115 except StopIteration:
116 timestamp = -1
117
118 data = {}
119
120 for key in (u'name',):
121 try:
122 data[key] = event_elt[key]
123 except KeyError:
124 continue
125
126 for elt_name in (u'description',):
127 try:
128 elt = next(event_elt.elements(NS_EVENT, elt_name))
129 except StopIteration:
130 continue
131 else:
132 data[elt_name] = unicode(elt)
133
134 for elt_name in (u'image', 'background-image'):
135 try:
136 image_elt = next(event_elt.elements(NS_EVENT, elt_name))
137 data[elt_name] = image_elt['src']
138 except StopIteration:
139 continue
140 except KeyError:
141 log.warning(_(u'no src found for image'))
142
143 for uri_type in (u'invitees', u'blog'):
144 try:
145 elt = next(event_elt.elements(NS_EVENT, uri_type))
146 uri = data[uri_type + u'_uri'] = elt['uri']
147 uri_data = uri_parse.parseXMPPUri(uri)
148 if uri_data[u'type'] != u'pubsub':
149 raise ValueError
150 except StopIteration:
151 log.warning(_(u"no {uri_type} element found!").format(uri_type=uri_type))
152 except KeyError:
153 log.warning(_(u"incomplete {uri_type} element").format(uri_type=uri_type))
154 except ValueError:
155 log.warning(_(u"bad {uri_type} element").format(uri_type=uri_type))
156 else:
157 data[uri_type + u'_service'] = uri_data[u'path']
158 data[uri_type + u'_node'] = uri_data[u'node']
159
160 for meta_elt in event_elt.elements(NS_EVENT, 'meta'):
161 key = meta_elt[u'name']
162 if key in data:
163 log.warning(u'Ignoring conflicting meta element: {xml}'.format(xml=meta_elt.toXml()))
164 continue
165 data[key] = unicode(meta_elt)
166
167 defer.returnValue((timestamp, data))
168
169 def _eventCreate(self, timestamp, data, service, node, id_=u'', profile_key=C.PROF_KEY_NONE):
170 service = jid.JID(service) if service else None
171 node = node if node else NS_EVENT
172 client = self.host.getClient(profile_key)
173 return self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT)
174
175 @defer.inlineCallbacks
176 def eventCreate(self, client, timestamp, data, service, node=None, item_id=NS_EVENT):
177 """Create or replace an event
178
179 @param service(jid.JID, None): PubSub service
180 @param node(unicode, None): PubSub node of the event
181 None will create instant node.
182 @param item_id(unicode): ID of the item to create.
183 @param timestamp(timestamp, None)
184 @param data(dict[unicode, unicode]): data to update
185 dict will be cleared, do a copy if data are still needed
186 key can be:
187 - name: name of the event
188 - description: details
189 - image: main picture of the event
190 - background-image: image to use as background
191 @return (unicode): created node
192 """
193 if not item_id:
194 raise ValueError(_(u"item_id must be set"))
195 if not service:
196 service = client.jid.userhostJID()
197 event_elt = domish.Element((NS_EVENT, 'event'))
198 if timestamp is not None and timestamp != -1:
199 formatted_date = utils.xmpp_date(timestamp)
200 event_elt.addElement((NS_EVENT, 'date'), content=formatted_date)
201 for key in (u'name',):
202 if key in data:
203 event_elt[key] = data.pop(key)
204 for key in (u'description',):
205 if key in data:
206 event_elt.addElement((NS_EVENT, key), content=data.pop(key))
207 for key in (u'image', u'background-image'):
208 if key in data:
209 elt = event_elt.addElement((NS_EVENT, key))
210 elt['src'] = data.pop(key)
211
212 # we first create the invitees and blog nodes (if not specified in data)
213 for uri_type in (u'invitees', u'blog'):
214 key = uri_type + u'_uri'
215 for to_delete in (u'service', u'node'):
216 k = uri_type + u'_' + to_delete
217 if k in data:
218 del data[k]
219 if key not in data:
220 # FIXME: affiliate invitees
221 uri_node = yield self._p.createNode(client, service)
222 yield self._p.setConfiguration(client, service, uri_node, {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST})
223 uri_service = service
224 else:
225 uri = data.pop(key)
226 uri_data = uri_parse.parseXMPPUri(uri)
227 if uri_data[u'type'] != u'pubsub':
228 raise ValueError(_(u'The given URI is not valid: {uri}').format(uri=uri))
229 uri_service = jid.JID(uri_data[u'path'])
230 uri_node = uri_data[u'node']
231
232 elt = event_elt.addElement((NS_EVENT, uri_type))
233 elt['uri'] = uri_parse.buildXMPPUri('pubsub', path=uri_service.full(), node=uri_node)
234
235 # remaining data are put in <meta> elements
236 for key in data.keys():
237 elt = event_elt.addElement((NS_EVENT, 'meta'), content = data.pop(key))
238 elt['name'] = key
239
240 item_elt = pubsub.Item(id=item_id, payload=event_elt)
241 try:
242 # TODO: check auto-create, no need to create node first if available
243 node = yield self._p.createNode(client, service, nodeIdentifier=node)
244 except error.StanzaError as e:
245 if e.condition == u'conflict':
246 log.debug(_(u"requested node already exists"))
247
248 yield self._p.publish(client, service, node, items=[item_elt])
249
250 defer.returnValue(node)
251
252 def _eventModify(self, service, node, id_, timestamp_update, data_update, profile_key=C.PROF_KEY_NONE):
253 service = jid.JID(service) if service else None
254 node = node if node else NS_EVENT
255 client = self.host.getClient(profile_key)
256 return self.eventModify(client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update)
257
258 @defer.inlineCallbacks
259 def eventModify(self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None):
260 """Update an event
261
262 Similar as create instead that it update existing item instead of
263 creating or replacing it. Params are the same as for [eventCreate].
264 """
265 event_timestamp, event_metadata = yield self.eventGet(client, service, node, id_)
266 new_timestamp = event_timestamp if timestamp_update is None else timestamp_update
267 new_data = event_metadata
268 if data_update:
269 for k, v in data_update.iteritems():
270 new_data[k] = v
271 yield self.eventCreate(client, new_timestamp, new_data, service, node, id_)
272
273 def _eventInviteeGet(self, service, node, profile_key):
274 service = jid.JID(service) if service else None
275 node = node if node else NS_EVENT
276 client = self.host.getClient(profile_key)
277 return self.eventInviteeGet(client, service, node)
278
279 @defer.inlineCallbacks
280 def eventInviteeGet(self, client, service, node):
281 """Retrieve attendance from event node
282
283 @param service(unicode, None): PubSub service
284 @param node(unicode): PubSub node of the event
285 @return (dict): a dict with current attendance status,
286 an empty dict is returned if nothing has been answered yed
287 """
288 items, metadata = yield self._p.getItems(client, service, node, item_ids=[client.jid.userhost()])
289 try:
290 event_elt = next(items[0].elements(NS_EVENT, u'invitee'))
291 except IndexError:
292 # no item found, event data are not set yet
293 defer.returnValue({})
294 data = {}
295 for key in (u'attend', u'guests'):
296 try:
297 data[key] = event_elt[key]
298 except KeyError:
299 continue
300 defer.returnValue(data)
301
302 def _eventInviteeSet(self, service, node, event_data, profile_key):
303 service = jid.JID(service) if service else None
304 node = node if node else NS_EVENT
305 client = self.host.getClient(profile_key)
306 return self.eventInviteeSet(client, service, node, event_data)
307
308 def eventInviteeSet(self, client, service, node, data):
309 """Set or update attendance data in event node
310
311 @param service(unicode, None): PubSub service
312 @param node(unicode): PubSub node of the event
313 @param data(dict[unicode, unicode]): data to update
314 key can be:
315 attend: one of "yes", "no", "maybe"
316 guests: an int
317 """
318 event_elt = domish.Element((NS_EVENT, 'invitee'))
319 for key in (u'attend', u'guests'):
320 try:
321 event_elt[key] = data.pop(key)
322 except KeyError:
323 pass
324 item_elt = pubsub.Item(id=client.jid.userhost(), payload=event_elt)
325 return self._p.publish(client, service, node, items=[item_elt])
326
327 def _eventInviteesList(self, service, node, profile_key):
328 service = jid.JID(service) if service else None
329 node = node if node else NS_EVENT
330 client = self.host.getClient(profile_key)
331 return self.eventInviteesList(client, service, node)
332
333 @defer.inlineCallbacks
334 def eventInviteesList(self, client, service, node):
335 """Retrieve attendance from event node
336
337 @param service(unicode, None): PubSub service
338 @param node(unicode): PubSub node of the event
339 @return (dict): a dict with current attendance status,
340 an empty dict is returned if nothing has been answered yed
341 """
342 items, metadata = yield self._p.getItems(client, service, node)
343 invitees = {}
344 for item in items:
345 try:
346 event_elt = next(item.elements(NS_EVENT, u'invitee'))
347 except IndexError:
348 # no item found, event data are not set yet
349 log.warning(_(u"no data found for {item_id} (service: {service}, node: {node})".format(
350 item_id=item['id'],
351 service=service,
352 node=node
353 )))
354 data = {}
355 for key in (u'attend', u'guests'):
356 try:
357 data[key] = event_elt[key]
358 except KeyError:
359 continue
360 invitees[item['id']] = data
361 defer.returnValue(invitees)
362
363 def _invite(self, service, node, id_=NS_EVENT, email=u'', emails_extra=None, name=u'', host_name=u'', language=u'', url_template=u'',
364 message_subject=u'', message_body=u'', profile_key=C.PROF_KEY_NONE):
365 client = self.host.getClient(profile_key)
366 kwargs = {u'profile': client.profile,
367 u'emails_extra': [unicode(e) for e in emails_extra]
368 }
369 for key in ("email", "name", "host_name", "language", "url_template", "message_subject", "message_body"):
370 value = locals()[key]
371 kwargs[key] = unicode(value)
372 return self.invite(client,
373 jid.JID(service) if service else None,
374 node,
375 id_ or NS_EVENT,
376 **kwargs)
377
378 @defer.inlineCallbacks
379 def invite(self, client, service, node, id_=NS_EVENT, **kwargs):
380 """High level method to create an email invitation to an event
381
382 @param service(unicode, None): PubSub service
383 @param node(unicode): PubSub node of the event
384 @param id_(unicode): id_ with even data
385 """
386 if self._i is None:
387 raise exceptions.FeatureNotFound(_(u'"Invitations" plugin is needed for this feature'))
388 if self._b is None:
389 raise exceptions.FeatureNotFound(_(u'"XEP-0277" (blog) plugin is needed for this feature'))
390 event_service = (service or client.jid.userhostJID())
391 event_uri = uri_parse.buildXMPPUri('pubsub',
392 path=event_service.full(),
393 node=node,
394 item=id_)
395 kwargs['extra'] = {u'event_uri': event_uri}
396 invitation_data = yield self._i.create(**kwargs)
397 invitee_jid = invitation_data[u'jid']
398 log.debug(_(u'invitation created'))
399 yield self._p.setNodeAffiliations(client, event_service, node, {invitee_jid: u'member'})
400 log.debug(_(u'affiliation set on event node'))
401 dummy, event_data = yield self.eventGet(client, service, node, id_)
402 log.debug(_(u'got event data'))
403 invitees_service = jid.JID(event_data['invitees_service'])
404 invitees_node = event_data['invitees_node']
405 blog_service = jid.JID(event_data['blog_service'])
406 blog_node = event_data['blog_node']
407 yield self._p.setNodeAffiliations(client, invitees_service, invitees_node, {invitee_jid: u'publisher'})
408 log.debug(_(u'affiliation set on invitee node'))
409 yield self._p.setNodeAffiliations(client, blog_service, blog_node, {invitee_jid: u'member'})
410 # FIXME: what follow is crazy, we have no good way to handle comments affiliations for blog
411 blog_items, dummy = yield self._b.mbGet(client, blog_service, blog_node, None)
412
413 for item in blog_items:
414 comments_service = jid.JID(item['comments_service'])
415 comments_node = item['comments_node']
416 yield self._p.setNodeAffiliations(client, comments_service, comments_node, {invitee_jid: u'publisher'})
417 log.debug(_(u'affiliation set on blog and comments nodes'))
418
419