comparison src/plugins/plugin_misc_groupblog.py @ 1672:dbd7c79aab2b

plugin group blog: big cleaning
author Goffi <goffi@goffi.org>
date Wed, 25 Nov 2015 11:12:51 +0100
parents 1895846fc9cb
children 95522b37bf5a
comparison
equal deleted inserted replaced
1671:1895846fc9cb 1672:dbd7c79aab2b
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from twisted.internet import defer 24 from twisted.internet import defer
25 from twisted.words.protocols.jabber import jid
26 from twisted.words.xish.domish import generateElementsNamed
27 from sat.core import exceptions 25 from sat.core import exceptions
28 from wokkel import disco, data_form, iwokkel 26 from wokkel import disco, data_form, iwokkel
29 from wokkel import rsm
30 from zope.interface import implements 27 from zope.interface import implements
31 from sat.tools import common 28 from sat.tools import common
32 # import uuid
33 29
34 try: 30 try:
35 from twisted.words.protocols.xmlstream import XMPPHandler 31 from twisted.words.protocols.xmlstream import XMPPHandler
36 except ImportError: 32 except ImportError:
37 from wokkel.subprotocols import XMPPHandler 33 from wokkel.subprotocols import XMPPHandler
38 34
39 NS_PUBSUB = 'http://jabber.org/protocol/pubsub' 35 NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
40 NS_GROUPBLOG = 'http://goffi.org/protocol/groupblog' 36 NS_GROUPBLOG = 'http://salut-a-toi.org/protocol/groupblog'
41 NS_NODE_PREFIX = 'urn:xmpp:groupblog:'
42 #NS_PUBSUB_EXP = 'http://goffi.org/protocol/pubsub' #for non official features 37 #NS_PUBSUB_EXP = 'http://goffi.org/protocol/pubsub' #for non official features
43 NS_PUBSUB_EXP = NS_PUBSUB # XXX: we can't use custom namespace as Wokkel's PubSubService use official NS 38 NS_PUBSUB_EXP = NS_PUBSUB # XXX: we can't use custom namespace as Wokkel's PubSubService use official NS
44 NS_PUBSUB_ITEM_ACCESS = NS_PUBSUB_EXP + "#item-access"
45 NS_PUBSUB_GROUPBLOG = NS_PUBSUB_EXP + "#groupblog" 39 NS_PUBSUB_GROUPBLOG = NS_PUBSUB_EXP + "#groupblog"
46 NS_PUBSUB_CREATOR_JID_CHECK = NS_PUBSUB_EXP + "#creator-jid-check"
47 NS_PUBSUB_ITEM_CONFIG = NS_PUBSUB_EXP + "#item-config" 40 NS_PUBSUB_ITEM_CONFIG = NS_PUBSUB_EXP + "#item-config"
48 NS_PUBSUB_AUTO_CREATE = NS_PUBSUB + "#auto-create"
49 ACCESS_TYPE_MAP = { 'PUBLIC': 'open',
50 'GROUP': 'roster',
51 'JID': None, #JID is not yet managed
52 }
53 41
54 MAX_ITEMS = 5
55 MAX_COMMENTS = 5
56 DO_NOT_COUNT_COMMENTS = -1 # must be lower than 0
57 42
58 PLUGIN_INFO = { 43 PLUGIN_INFO = {
59 "name": "Group blogging throught collections", 44 "name": "Group blogging through collections",
60 "import_name": "GROUPBLOG", 45 "import_name": "GROUPBLOG",
61 "type": "MISC", 46 "type": "MISC",
62 "protocols": [], 47 "protocols": [],
63 "dependencies": ["XEP-0277"], 48 "dependencies": ["XEP-0277"],
64 "main": "GroupBlog", 49 "main": "GroupBlog",
65 "handler": "yes", 50 "handler": "yes",
66 "description": _("""Implementation of microblogging fine permissions""") 51 "description": _("""Implementation of microblogging fine permissions""")
67 } 52 }
68 53
69 54
70 class NoCompatiblePubSubServerFound(Exception):
71 pass
72
73
74 class BadAccessTypeError(Exception):
75 pass
76
77
78 class BadAccessListError(Exception):
79 pass
80
81
82 class UnknownType(Exception):
83 pass
84
85 class GroupBlog(object): 55 class GroupBlog(object):
86 """This class use a SàT PubSub Service to manage access on microblog""" 56 """This class use a SàT PubSub Service to manage access on microblog"""
87 57
88 def __init__(self, host): 58 def __init__(self, host):
89 log.info(_("Group blog plugin initialization")) 59 log.info(_("Group blog plugin initialization"))
90 self.host = host 60 self.host = host
91 self._p = self.host.plugins["XEP-0060"] 61 self._p = self.host.plugins["XEP-0060"]
92
93 # host.bridge.addMethod("sendGroupBlog", ".plugin", in_sign='sassa{ss}s', out_sign='',
94 # method=self.sendGroupBlog,
95 # async=True)
96
97 # host.bridge.addMethod("deleteGroupBlog", ".plugin", in_sign='(sss)ss', out_sign='',
98 # method=self.deleteGroupBlog,
99 # async=True)
100
101 # host.bridge.addMethod("updateGroupBlog", ".plugin", in_sign='(sss)ssa{ss}s', out_sign='',
102 # method=self.updateGroupBlog,
103 # async=True)
104
105 # host.bridge.addMethod("sendGroupBlogComment", ".plugin", in_sign='ssa{ss}s', out_sign='',
106 # method=self.sendGroupBlogComment,
107 # async=True)
108
109 # host.bridge.addMethod("getGroupBlogs", ".plugin",
110 # in_sign='sasa{ss}bs', out_sign='(aa{ss}a{ss})',
111 # method=self.getGroupBlogs,
112 # async=True)
113
114 # host.bridge.addMethod("getGroupBlogsWithComments", ".plugin",
115 # in_sign='sasa{ss}is', out_sign='(a(a{ss}(aa{ss}a{ss}))a{ss})',
116 # method=self.getGroupBlogsWithComments,
117 # async=True)
118
119 # host.bridge.addMethod("getMassiveGroupBlogs", ".plugin",
120 # in_sign='sasa{ss}s', out_sign='a{s(aa{ss}a{ss})}',
121 # method=self._getMassiveGroupBlogs,
122 # async=True)
123
124 # host.bridge.addMethod("getGroupBlogComments", ".plugin",
125 # in_sign='ssa{ss}s', out_sign='(aa{ss}a{ss})',
126 # method=self.getGroupBlogComments,
127 # async=True)
128
129 # host.bridge.addMethod("subscribeGroupBlog", ".plugin", in_sign='ss', out_sign='',
130 # method=self.subscribeGroupBlog,
131 # async=True)
132
133 # host.trigger.add("PubSubItemsReceived", self.pubSubItemsReceivedTrigger)
134 host.trigger.add("XEP-0277_item2data", self._item2dataTrigger) 62 host.trigger.add("XEP-0277_item2data", self._item2dataTrigger)
135 host.trigger.add("XEP-0277_data2entry", self._data2entryTrigger) 63 host.trigger.add("XEP-0277_data2entry", self._data2entryTrigger)
136 host.trigger.add("XEP-0277_comments", self._commentsTrigger) 64 host.trigger.add("XEP-0277_comments", self._commentsTrigger)
137 65
138 ## plugin management methods ## 66 ## plugin management methods ##
170 """Parse item to find group permission elements""" 98 """Parse item to find group permission elements"""
171 config_form = data_form.findForm(item_elt, NS_PUBSUB_ITEM_CONFIG) 99 config_form = data_form.findForm(item_elt, NS_PUBSUB_ITEM_CONFIG)
172 if config_form is None: 100 if config_form is None:
173 return 101 return
174 access_model = config_form.get(self._p.OPT_ACCESS_MODEL, self._p.ACCESS_OPEN) 102 access_model = config_form.get(self._p.OPT_ACCESS_MODEL, self._p.ACCESS_OPEN)
103 # FIXME: ACCESS_ROSTER need to be changed to a new ACCESS_PUBLISHER_ROSTER when available
175 if access_model == self._p.ACCESS_ROSTER: 104 if access_model == self._p.ACCESS_ROSTER:
176 common.iter2dict('group', config_form.fields[self._p.OPT_ROSTER_GROUPS_ALLOWED].values, microblog_data) 105 common.iter2dict('group', config_form.fields[self._p.OPT_ROSTER_GROUPS_ALLOWED].values, microblog_data)
177 106
178 def _data2entryTrigger(self, client, mb_data, entry_elt, item_elt): 107 def _data2entryTrigger(self, client, mb_data, entry_elt, item_elt):
179 """Build fine access permission if needed 108 """Build fine access permission if needed
203 if "group" in mb_data: 132 if "group" in mb_data:
204 # FIXME: ACCESS_ROSTER need to be changed to a new ACCESS_PUBLISHER_ROSTER when available 133 # FIXME: ACCESS_ROSTER need to be changed to a new ACCESS_PUBLISHER_ROSTER when available
205 options[self._p.OPT_ACCESS_MODEL] = self._p.ACCESS_ROSTER 134 options[self._p.OPT_ACCESS_MODEL] = self._p.ACCESS_ROSTER
206 options[self._p.OPT_ROSTER_GROUPS_ALLOWED] = list(common.dict2iter('group', mb_data)) 135 options[self._p.OPT_ROSTER_GROUPS_ALLOWED] = list(common.dict2iter('group', mb_data))
207 136
208 @defer.inlineCallbacks
209 def _initialise(self, profile_key):
210 """Check that the data for this profile are initialised, and do it else
211 @param profile_key: %(doc_profile)s"""
212 profile = self.host.memory.getProfileName(profile_key)
213 if not profile:
214 raise exceptions.ProfileUnknownError
215
216 client = self.host.getClient(profile)
217
218 #we first check that we have a item-access pubsub server
219 if not hasattr(client, "item_access_pubsub"):
220 log.debug(_('Looking for item-access powered pubsub server'))
221 #we don't have any pubsub server featuring item access yet
222 item_access_pubsubs = yield self.host.findFeaturesSet((NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK), "pubsub", "service", profile=profile)
223 # item_access_pubsubs = yield self.host.findFeaturesSet((NS_PUBSUB_ITEM_ACCESS, NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK), "pubsub", "service", profile_key=profile)
224 try:
225 client.item_access_pubsub = item_access_pubsubs.pop()
226 log.info(_(u"item-access powered pubsub service found: [%s]") % client.item_access_pubsub.full())
227 except KeyError:
228 client.item_access_pubsub = None
229
230 if not client.item_access_pubsub:
231 log.error(_(u"No item-access powered pubsub server found, can't use group blog"))
232 raise NoCompatiblePubSubServerFound
233
234 defer.returnValue((profile, client))
235
236 # def pubSubItemsReceivedTrigger(self, event, profile):
237 # """"Trigger which catch groupblogs events"""
238
239 # if event.nodeIdentifier.startswith(NS_NODE_PREFIX):
240 # # Microblog
241 # publisher = jid.JID(event.nodeIdentifier[len(NS_NODE_PREFIX):])
242 # origin_host = publisher.host.split('.')
243 # event_host = event.sender.host.split('.')
244 # #FIXME: basic origin check, must be improved
245 # #TODO: automatic security test
246 # if (not (origin_host)
247 # or len(event_host) < len(origin_host)
248 # or event_host[-len(origin_host):] != origin_host):
249 # log.warning(u"Host incoherence between %s and %s (hack attempt ?)" % (unicode(event.sender),
250 # unicode(publisher)))
251 # return False
252
253 # client = self.host.getClient(profile)
254
255 # def gbdataManagementMicroblog(gbdata):
256 # for gbdatum in gbdata:
257 # self.host.bridge.personalEvent(publisher.full(), "MICROBLOG", gbdatum, profile)
258
259 # d = self._itemsConstruction(event.items, publisher, client)
260 # d.addCallback(gbdataManagementMicroblog)
261 # return False
262
263 # elif event.nodeIdentifier.startswith(NS_COMMENT_PREFIX):
264 # # Comment
265 # def gbdataManagementComments(gbdata):
266 # for gbdatum in gbdata:
267 # publisher = None # FIXME: see below (_handleCommentsItems)
268 # self.host.bridge.personalEvent(publisher.full() if publisher else gbdatum["author"], "MICROBLOG", gbdatum, profile)
269 # d = self._handleCommentsItems(event.items, event.sender, event.nodeIdentifier)
270 # d.addCallback(gbdataManagementComments)
271 # return False
272 # return True
273
274 ## internal helping methodes ##
275
276 def _handleCommentsItems(self, items, service, node_identifier):
277 """ Convert comments items to groupblog data, and send them as signals
278
279 @param items: comments items
280 @param service: jid of the PubSub service used
281 @param node_identifier: comments node
282 @return: deferred list of group blog data
283 """
284 d_list = []
285
286 def cb(microblog_data):
287 publisher = "" # FIXME: publisher attribute for item in SàT pubsub is not managed yet, so
288 # publisher is not checked and can be easily spoofed. This need to be fixed
289 # quickly.
290 microblog_data["service"] = service.userhost()
291 microblog_data["node"] = node_identifier
292 microblog_data["verified_publisher"] = "true" if publisher else "false"
293 return microblog_data
294
295 for item in items:
296 d_list.append(self.item2gbdata(item, "comment").addCallback(cb))
297 return defer.DeferredList(d_list, consumeErrors=True).addCallback(lambda result: [value for (success, value) in result if success])
298
299 def _parseAccessData(self, microblog_data, item):
300 P = self.host.plugins["XEP-0060"]
301 form_elts = [child for child in item.elements() if child.name == "x"]
302 for form_elt in form_elts:
303 form = data_form.Form.fromElement(form_elt)
304
305 if (form.formNamespace == NS_PUBSUB_ITEM_CONFIG):
306 access_model = form.get(P.OPT_ACCESS_MODEL, 'open')
307 if access_model == "roster":
308 try:
309 # FIXME: groups are xs:string, so they can contain "\n" ! This code is bugged
310 microblog_data["groups"] = '\n'.join(form.fields[P.OPT_ROSTER_GROUPS_ALLOWED].values)
311 except KeyError:
312 log.warning("No group found for roster access-model")
313 microblog_data["groups"] = ''
314
315 break
316
317 @defer.inlineCallbacks
318 def item2gbdata(self, item, _type="main_item"):
319 """ Convert item to microblog data dictionary + add access data """
320 microblog_data = yield self.host.plugins["XEP-0277"].item2mbdata(item)
321 microblog_data["type"] = _type
322 self._parseAccessData(microblog_data, item)
323 defer.returnValue(microblog_data)
324
325 def getNodeName(self, publisher):
326 """Retrieve the name of publisher's node
327
328 @param publisher: publisher's jid
329 @return: node's name (string)
330 """
331 return NS_NODE_PREFIX + publisher.userhost()
332
333 ## publish ##
334
335 def _publishMblog(self, service, client, access_type, access_list, message, extra):
336 """Actually publish the message on the group blog
337
338 @param service: jid of the item-access pubsub service
339 @param client: SatXMPPClient of the publisher
340 @param access_type: one of "PUBLIC", "GROUP", "JID"
341 @param access_list: set of entities (empty list for all, groups or jids) allowed to see the item
342 @param message: message to publish
343 @param extra: dict which option name as key, which can be:
344 - allow_comments: True to accept comments, False else (default: False)
345 - rich: if present, contain rich text in currently selected syntax
346 """
347 node_name = self.getNodeName(client.jid)
348 mblog_data = {'content': message}
349
350 for attr in ['content_rich', 'title', 'title_rich']:
351 if attr in extra and extra[attr]:
352 mblog_data[attr] = extra[attr]
353 P = self.host.plugins["XEP-0060"]
354 access_model_value = ACCESS_TYPE_MAP[access_type]
355
356 if extra.get('allow_comments', 'False').lower() == 'true':
357 # XXX: use the item identifier? http://bugs.goffi.org/show_bug.cgi?id=63
358 comments_node = self._fillCommentsElement(mblog_data, None, node_name, service)
359 _options = {P.OPT_ACCESS_MODEL: access_model_value,
360 P.OPT_PERSIST_ITEMS: 1,
361 P.OPT_MAX_ITEMS: -1,
362 P.OPT_DELIVER_PAYLOADS: 1,
363 P.OPT_SEND_ITEM_SUBSCRIBE: 1,
364 P.OPT_PUBLISH_MODEL: "subscribers", # TODO: should be open if *both* node and item access_model are open (public node and item)
365 }
366 if access_model_value == 'roster':
367 _options[P.OPT_ROSTER_GROUPS_ALLOWED] = list(access_list)
368
369 # FIXME: check comments node creation success, at the moment this is a potential security risk (if the node
370 # already exists, the creation will silently fail, but the comments link will stay the same, linking to a
371 # node owned by somebody else)
372 self.host.plugins["XEP-0060"].createNode(service, comments_node, _options, profile_key=client.profile)
373
374 def itemCreated(mblog_item):
375 form = data_form.Form('submit', formNamespace=NS_PUBSUB_ITEM_CONFIG)
376
377 if access_type == "PUBLIC":
378 if access_list:
379 raise BadAccessListError("access_list must be empty for PUBLIC access")
380 access = data_form.Field(None, P.OPT_ACCESS_MODEL, value=access_model_value)
381 form.addField(access)
382 elif access_type == "GROUP":
383 access = data_form.Field(None, P.OPT_ACCESS_MODEL, value=access_model_value)
384 allowed = data_form.Field(None, P.OPT_ROSTER_GROUPS_ALLOWED, values=access_list)
385 form.addField(access)
386 form.addField(allowed)
387 mblog_item.addChild(form.toElement())
388 elif access_type == "JID":
389 raise NotImplementedError
390 else:
391 log.error(_("Unknown access_type"))
392 raise BadAccessTypeError
393
394 defer_blog = self.host.plugins["XEP-0060"].publish(service, node_name, items=[mblog_item], profile_key=client.profile)
395 defer_blog.addErrback(self._mblogPublicationFailed)
396 return defer_blog
397
398 entry_d = self.host.plugins["XEP-0277"].data2entry(mblog_data, client.profile)
399 entry_d.addCallback(itemCreated)
400 return entry_d
401
402 # def _fillCommentsElement(self, mblog_data, entry_id, node_name, service_jid):
403 # """
404 # @param mblog_data: dict containing the microblog data
405 # @param entry_id: unique identifier of the entry
406 # @param node_name: the pubsub node name
407 # @param service_jid: the JID of the pubsub service
408 # @return: the comments node string
409 # """
410 # if entry_id is None:
411 # entry_id = unicode(uuid.uuid4())
412 # comments_node = "%s_%s__%s" % (NS_COMMENT_PREFIX, entry_id, node_name)
413 # mblog_data['comments'] = "xmpp:%(service)s?%(query)s" % {'service': service_jid.userhost(),
414 # 'query': urllib.urlencode([('node', comments_node.encode('utf-8'))])}
415 # return comments_node
416
417 def _mblogPublicationFailed(self, failure):
418 #TODO
419 return failure
420
421 def sendGroupBlog(self, access_type, access_list, message, extra, profile_key=C.PROF_KEY_NONE):
422 """Publish a microblog with given item access
423
424 @param access_type: one of "PUBLIC", "GROUP", "JID"
425 @param access_list: list of authorized entity (empty list for PUBLIC ACCESS,
426 list of groups or list of jids) for this item
427 @param message: microblog
428 @param extra: dict which option name as key, which can be:
429 - allow_comments: True to accept comments, False else (default: False)
430 - rich: if present, contain rich text in currently selected syntax
431 @profile_key: %(doc_profile_key)s
432 """
433
434 def initialised(result):
435 profile, client = result
436 if access_type == "PUBLIC":
437 if access_list:
438 raise Exception("Publishers list must be empty when getting microblogs for all contacts")
439 return self._publishMblog(client.item_access_pubsub, client, "PUBLIC", [], message, extra)
440 elif access_type == "GROUP":
441 _groups = set(access_list).intersection(client.roster.getGroups()) # We only keep group which actually exist
442 if not _groups:
443 raise BadAccessListError("No valid group")
444 return self._publishMblog(client.item_access_pubsub, client, "GROUP", _groups, message, extra)
445 elif access_type == "JID":
446 raise NotImplementedError
447 else:
448 log.error(_("Unknown access type"))
449 raise BadAccessTypeError
450
451 return self._initialise(profile_key).addCallback(initialised)
452
453 def sendGroupBlogComment(self, node_url, message, extra, profile_key=C.PROF_KEY_NONE):
454 """Publish a comment in the given node
455 @param node url: link to the comments node as specified in XEP-0277 and given in microblog data's comments key
456 @param message: comment
457 @param extra: dict which option name as key, which can be:
458 - allow_comments: True to accept an other level of comments, False else (default: False)
459 - rich: if present, contain rich text in currently selected syntax
460 @profile_key: %(doc_profile)s
461 """
462 def initialised(result):
463 profile, client = result
464 service, node = self.host.plugins["XEP-0277"].parseCommentUrl(node_url)
465 mblog_data = {'content': message}
466 for attr in ['content_rich', 'title', 'title_rich']:
467 if attr in extra and extra[attr]:
468 mblog_data[attr] = extra[attr]
469 if 'allow_comments' in extra:
470 raise NotImplementedError # TODO
471 entry_d = self.host.plugins["XEP-0277"].data2entry(mblog_data, profile)
472 entry_d.addCallback(lambda mblog_item: self.host.plugins["XEP-0060"].publish(service, node, items=[mblog_item], profile_key=profile))
473 return entry_d
474
475 return self._initialise(profile_key).addCallback(initialised)
476
477 def _itemsConstruction(self, items, pub_jid, client):
478 """ Transforms items to group blog data and manage comments node
479
480 @param items: iterable of items
481 @param pub_jid: jid of the publisher or None to use items data
482 @param client: SatXMPPClient instance
483 @return: deferred which fire list of group blog data """
484 # TODO: use items data when pub_jid is None
485 d_list = []
486
487 @defer.inlineCallbacks
488 def cb(gbdata):
489 try:
490 gbdata['service'] = client.item_access_pubsub.full()
491 except AttributeError:
492 log.warning(_(u"Pubsub service is unknown for blog entry %s") % gbdata['id'])
493 # every comments node must be subscribed, except if we are the publisher (we are already subscribed in this case)
494 if "comments_node" in gbdata and pub_jid.userhostJID() != client.jid.userhostJID():
495 try:
496 service = jid.JID(gbdata["comments_service"])
497 node = gbdata["comments_node"]
498 except KeyError:
499 log.error(_(u"Missing key for blog comment %s") % gbdata['id'])
500 defer.returnValue(gbdata)
501 # TODO: see if it is really needed to check for not subscribing twice to the node
502 # It previously worked without this check, but the pubsub service logs were polluted
503 # or, if in debug mode, it made sat-pubsub very difficult to debug.
504 subscribed_nodes = yield self.host.plugins['XEP-0060'].listSubscribedNodes(service, profile=client.profile)
505 if node not in subscribed_nodes: # avoid sat-pubsub "SubscriptionExists" error
506 self.host.plugins["XEP-0060"].subscribe(service, node, profile_key=client.profile)
507 defer.returnValue(gbdata)
508
509 for item in items:
510 d_list.append(self.item2gbdata(item).addCallback(cb))
511 return defer.DeferredList(d_list, consumeErrors=True).addCallback(lambda result: [value for (success, value) in result if success])
512
513 ## modify ##
514
515 def updateGroupBlog(self, pub_data, comments, message, extra, profile_key=C.PROF_KEY_NONE):
516 """Modify a microblog node
517
518 @param pub_data: a tuple (service, node identifier, item identifier)
519 @param comments: comments node identifier (for main item) or empty string
520 @param message: new message
521 @param extra: dict which option name as key, which can be:
522 - allow_comments: True to accept an other level of comments, False else (default: False)
523 - rich: if present, contain rich text in currently selected syntax
524 @param profile_key: %(doc_profile)
525 """
526
527 def initialised(result):
528 profile, client = result
529 mblog_data = {'content': message}
530 for attr in ['content_rich', 'title', 'title_rich']:
531 if attr in extra and extra[attr]:
532 mblog_data[attr] = extra[attr]
533 service, node, item_id = pub_data
534 service_jid = jid.JID(service) if service else client.item_access_pubsub
535 if comments or not node: # main item
536 node = self.getNodeName(client.jid)
537 mblog_data['id'] = unicode(item_id)
538 if 'published' in extra:
539 mblog_data['published'] = extra['published']
540 if extra.get('allow_comments', 'False').lower() == 'true':
541 comments_service, comments_node = self.host.plugins["XEP-0277"].parseCommentUrl(comments)
542 # we could use comments_node directly but it's safer to rebuild it
543 # XXX: use the item identifier? http://bugs.goffi.org/show_bug.cgi?id=63
544 entry_id = comments_node.split('_')[1].split('__')[0]
545 self._fillCommentsElement(mblog_data, entry_id, node, service_jid)
546 entry_d = self.host.plugins["XEP-0277"].data2entry(mblog_data, profile)
547 entry_d.addCallback(lambda mblog_item: self.host.plugins["XEP-0060"].publish(service_jid, node, items=[mblog_item], profile_key=profile))
548 entry_d.addErrback(lambda failure: log.error(u"Modification of %s failed: %s" % (pub_data, failure.getErrorMessage())))
549 return entry_d
550
551 return self._initialise(profile_key).addCallback(initialised)
552
553 ## get ##
554
555 def _getOrCountComments(self, items, max_=0, profile_key=C.PROF_KEY_NONE):
556 """Get and/or count the comments of the given items.
557
558 @param items (list): items to consider.
559 @param max_ (int): maximum number of comments to get, if 0 only count
560 them. The count is set to the item data of key "comments_count".
561 @param profile_key (str): %(doc_profile_key)s
562 @return: a deferred list of:
563 - if max_ == 0: microblog data
564 - else: couple (dict, (list[dict], dict)) containing:
565 - microblog data (main item)
566 - couple (comments data, RSM response data for the comments)
567 """
568 def comments_cb(comments_data, entry):
569 try:
570 entry['comments_count'] = comments_data[1]['count']
571 except KeyError: # target pubsub server probably doesn't handle RSM
572 pass
573 return (entry, comments_data) if max_ > 0 else entry
574
575 assert max_ >= 0
576 d_list = []
577 for entry in items:
578 if entry.get('comments', False):
579 comments_rsm = {'max_': max_}
580 d = self.getGroupBlogComments(entry['comments_service'], entry['comments_node'], rsm_data=comments_rsm, profile_key=profile_key)
581 d.addCallback(comments_cb, entry)
582 d_list.append(d)
583 else:
584 if max_ > 0:
585 d_list.append(defer.succeed((entry, ([], {}))))
586 else:
587 d_list.append(defer.succeed(entry))
588 deferred_list = defer.DeferredList(d_list)
589 deferred_list.addCallback(lambda result: [value for (success, value) in result if success])
590 return deferred_list
591
592 def _getGroupBlogs(self, pub_jid_s, item_ids=None, rsm_data=None, max_comments=0, profile_key=C.PROF_KEY_NONE):
593 """Retrieve previously published items from a publish subscribe node.
594
595 @param pub_jid_s: jid of the publisher
596 @param item_ids: list of microblogs items IDs
597 @param rsm_data (dict): RSM request data
598 @param max_comments (int): maximum number of comments to retrieve
599 @param profile_key (str): %(doc_profile_key)s
600 @return: a deferred couple (list, dict) containing:
601 - list of:
602 - if max_comments == 0: microblog data
603 - else: couple (dict, (list[dict], dict)) containing:
604 - microblog data (main item)
605 - couple (comments data, RSM response data for the comments)
606 - RSM response data
607 """
608 pub_jid = jid.JID(pub_jid_s)
609
610 def cb(items, client):
611 d = self._itemsConstruction(items, pub_jid, client)
612 if max_comments == DO_NOT_COUNT_COMMENTS:
613 return d
614 return d.addCallback(self._getOrCountComments, max_comments, profile_key)
615
616 return DeferredItems(self, cb, None, profile_key).get(self.getNodeName(pub_jid), item_ids, rsm_data=rsm_data)
617
618 # def getGroupBlogs(self, pub_jid_s, item_ids=None, rsm_data=None, count_comments=True, profile_key=C.PROF_KEY_NONE):
619 # """Get the published microblogs of the specified IDs. If item_ids is
620 # None, the result would be the same than calling getGroupBlogs
621 # with the default value for the attribute max_items.
622
623 # @param pub_jid_s: jid of the publisher
624 # @param item_ids: list of microblogs items IDs
625 # @param rsm_data (dict): RSM request data
626 # @param count_comments (bool): also count the comments if True
627 # @param profile_key (str): %(doc_profile_key)s
628 # @return: a deferred couple (list, dict) containing:
629 # - list of microblog data
630 # - RSM response data
631 # """
632 # max_comments = 0 if count_comments else DO_NOT_COUNT_COMMENTS
633 # return self._getGroupBlogs(pub_jid_s, item_ids=item_ids, rsm_data=rsm_data, max_comments=max_comments, profile_key=profile_key)
634
635 def getGroupBlogsWithComments(self, pub_jid_s, item_ids=None, rsm_data=None, max_comments=None, profile_key=C.PROF_KEY_NONE):
636 """Get the published microblogs of the specified IDs and their comments. If
637 item_ids is None, returns the last published microblogs and their comments.
638
639 @param pub_jid_s: jid of the publisher
640 @param item_ids: list of microblogs items IDs
641 @param rsm (dict): RSM request data
642 @param max_comments (int): maximum number of comments to retrieve
643 @param profile_key (str): %(doc_profile_key)s
644 @return: a deferred couple (list, dict) containing:
645 - list of couple (dict, (list[dict], dict)) containing:
646 - microblog data (main item)
647 - couple (comments data, RSM response data for the comments)
648 - RSM response data
649 """
650 if max_comments is None:
651 max_comments = MAX_COMMENTS
652 assert max_comments > 0 # otherwise the return signature is not the same
653 return self._getGroupBlogs(pub_jid_s, item_ids=item_ids, rsm_data=rsm_data, max_comments=max_comments, profile_key=profile_key)
654
655 # def _getMassiveGroupBlogs(self, publishers_type, publishers, rsm_data=None, profile_key=C.PROF_KEY_NONE):
656 # if publishers_type == 'JID':
657 # publishers_jids = [jid.JID(publisher) for publisher in publishers]
658 # else:
659 # publishers_jids = publishers
660 # return self.getMassiveGroupBlogs(publishers_type, publishers_jids, rsm_data, profile_key)
661
662 # def _getPublishersJIDs(self, publishers_type, publishers, client):
663 # #TODO: custom exception
664 # if publishers_type not in ["GROUP", "JID", "ALL"]:
665 # raise Exception("Bad call, unknown publishers_type")
666 # if publishers_type == "ALL" and publishers:
667 # raise Exception("Publishers list must be empty when getting microblogs for all contacts")
668
669 # if publishers_type == "ALL":
670 # contacts = client.roster.getItems()
671 # jids = [contact.jid.userhostJID() for contact in contacts]
672 # elif publishers_type == "GROUP":
673 # jids = []
674 # for _group in publishers:
675 # jids.extend(client.roster.getJidsFromGroup(_group))
676 # elif publishers_type == 'JID':
677 # jids = publishers
678 # else:
679 # raise UnknownType
680 # return jids
681
682 # def getMassiveGroupBlogs(self, publishers_type, publishers, rsm_data=None, profile_key=C.PROF_KEY_NONE):
683 # """Get the last published microblogs for a list of groups or jids
684 # @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL")
685 # @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids)
686 # @param rsm_data (dict): RSM request data, common to all publishers
687 # @param profile_key: profile key
688 # @return: a deferred dict with:
689 # - key: publisher (unicode)
690 # - value: couple (list[dict], dict) with:
691 # - the microblogs data
692 # - RSM response data
693 # """
694 # def cb(items, publisher, client):
695 # d = self._itemsConstruction(items, publisher, client)
696 # return d.addCallback(self._getOrCountComments, False, profile_key)
697
698 # #TODO: we need to use the server corresponding to the host of the jid
699 # return DeferredItemsFromMany(self, cb, profile_key).get(publishers_type, publishers, rsm_data=rsm_data)
700
701 ## subscribe ##
702
703 # def subscribeGroupBlog(self, pub_jid, profile_key=C.PROF_KEY_NONE):
704 # def initialised(result):
705 # profile, client = result
706 # d = self.host.plugins["XEP-0060"].subscribe(client.item_access_pubsub, self.getNodeName(jid.JID(pub_jid)),
707 # profile_key=profile_key)
708 # return d
709
710 # #TODO: we need to use the server corresponding the the host of the jid
711 # return self._initialise(profile_key).addCallback(initialised)
712
713
714 ## delete ##
715
716 def deleteGroupBlog(self, pub_data, comments, profile_key=C.PROF_KEY_NONE):
717 """Delete a microblog item from a node.
718
719 @param pub_data: a tuple (service, node identifier, item identifier)
720 @param comments: comments node identifier (for main item) or empty string
721 @param profile_key: %(doc_profile_key)s
722 """
723
724 def initialised(result):
725 profile, client = result
726 service, node, item_id = pub_data
727 service_jid = jid.JID(service) if service else client.item_access_pubsub
728 if comments or not node: # main item
729 node = self.getNodeName(client.jid)
730 if comments:
731 # remove the associated comments node
732 comments_service, comments_node = self.host.plugins["XEP-0277"].parseCommentUrl(comments)
733 d = self.host.plugins["XEP-0060"].deleteNode(comments_service, comments_node, profile_key=profile)
734 d.addErrback(lambda failure: log.error(u"Deletion of node %s failed: %s" % (comments_node, failure.getErrorMessage())))
735 # remove the item itself
736 d = self.host.plugins["XEP-0060"].retractItems(service_jid, node, [item_id], profile_key=profile)
737 d.addErrback(lambda failure: log.error(u"Deletion of item %s from %s failed: %s" % (item_id, node, failure.getErrorMessage())))
738 return d
739
740 def notify(d):
741 # TODO: this works only on the same host, and notifications for item deletion should be
742 # implemented according to http://xmpp.org/extensions/xep-0060.html#publisher-delete-success-notify
743 # instead. The notification mechanism implemented in sat_pubsub and wokkel have apriori
744 # a problem with retrieving the subscriptions, or something else.
745 service, node, item_id = pub_data
746 publisher = self.host.getJidNStream(profile_key)[0]
747 profile = self.host.memory.getProfileName(profile_key)
748 gbdatum = {'id': item_id, 'type': 'main_item' if (comments or not node) else 'comment'}
749 self.host.bridge.personalEvent(publisher.full(), "MICROBLOG_DELETE", gbdatum, profile)
750 return d
751
752 return self._initialise(profile_key).addCallback(initialised).addCallback(notify)
753
754 def deleteAllGroupBlogsAndComments(self, profile_key=C.PROF_KEY_NONE):
755 """Delete absolutely all the microblog data that the user has posted"""
756 calls = [self.deleteAllGroupBlogs(profile_key), self.deleteAllGroupBlogsComments(profile_key)]
757 return defer.DeferredList(calls)
758
759 def deleteAllGroupBlogs(self, profile_key=C.PROF_KEY_NONE):
760 """Delete all the main items that the user has posted and their comments.
761 """
762 def initialised(result):
763 profile, client = result
764 service = client.item_access_pubsub
765 jid_ = client.jid
766 main_node = self.getNodeName(jid_)
767
768 def cb(nodes):
769 d_list = []
770 for node in [node for node in nodes if node.endswith(main_node)]:
771 d = self.host.plugins["XEP-0060"].deleteNode(service, node, profile_key=profile)
772 d.addErrback(lambda failure: log.error(_(u"Deletion of node %(node)s failed: %(message)s") %
773 {'node': node, 'message': failure.getErrorMessage()}))
774 d_list.append(d)
775 return defer.DeferredList(d_list)
776
777 d = self.host.plugins["XEP-0060"].listNodes(service, profile=profile)
778 d.addCallback(cb)
779 d.addCallback(lambda dummy: log.info(_(u"All microblog's main items from %s have been deleted!") % jid_.userhost()))
780 return d
781
782 return self._initialise(profile_key).addCallback(initialised)
783
784 def deleteAllGroupBlogsComments(self, profile_key=C.PROF_KEY_NONE):
785 """Delete all the comments that the user posted on other's main items.
786 We avoid the conversions from item to microblog data as we only need
787 to retrieve some attributes, no need to convert text syntax...
788 """
789 def initialised(result):
790 """Get all the main items from our contact list
791 @return: a DeferredList
792 """
793 profile, client = result
794 service = client.item_access_pubsub
795 jids = [contact.jid.userhostJID() for contact in client.roster.getItems()]
796 blogs = []
797 for jid_ in jids:
798 if jid_ == client.jid.userhostJID():
799 continue # do not remove the comments on our own node
800 main_node = self.getNodeName(jid_)
801 d = self.host.plugins["XEP-0060"].getItems(service, main_node, profile_key=profile)
802 d.addCallback(lambda res: getComments(res[0], client))
803 d.addErrback(lambda failure, main_node: log.error(_(u"Retrieval of items for node %(node)s failed: %(message)s") %
804 {'node': main_node, 'message': failure.getErrorMessage()}), main_node)
805 blogs.append(d)
806
807 return defer.DeferredList(blogs)
808
809 def getComments(items, client):
810 """Get all the comments for a list of items
811 @param items: a list of main items for one user
812 @param client: the client of the user
813 @return: a DeferredList
814 """
815 comments = []
816 for item in items:
817 try:
818 entry = generateElementsNamed(item.elements(), 'entry').next()
819 link = generateElementsNamed(entry.elements(), 'link').next()
820 except StopIteration:
821 continue
822 href = link.getAttribute('href')
823 service, node = self.host.plugins['XEP-0277'].parseCommentUrl(href)
824 d = self.host.plugins["XEP-0060"].getItems(service, node, profile_key=profile_key)
825 d.addCallback(lambda items: (service, node, items[0]))
826 d.addErrback(lambda failure, node: log.error(_(u"Retrieval of comments for node %(node)s failed: %(message)s") %
827 {'node': node, 'message': failure.getErrorMessage()}), node)
828 comments.append(d)
829 dlist = defer.DeferredList(comments)
830 dlist.addCallback(deleteComments, client)
831 return dlist
832
833 def deleteComments(result, client):
834 """Delete all the comments of the user that are found in result
835 @param result: a list of couple (success, value) with success a
836 boolean and value a tuple (service as JID, node_id, comment_items)
837 @param client: the client of the user
838 @return: a DeferredList with the deletions result
839 """
840 user_jid_s = client.jid.userhost()
841 for (success, value) in result:
842 if not success:
843 continue
844 service, node_id, comment_items = value
845 item_ids = []
846 for comment_item in comment_items: # for all the comments on one post
847 try:
848 entry = generateElementsNamed(comment_item.elements(), 'entry').next()
849 author = generateElementsNamed(entry.elements(), 'author').next()
850 name = generateElementsNamed(author.elements(), 'name').next()
851 except StopIteration:
852 continue
853 if name.children[0] == user_jid_s:
854 item_ids.append(comment_item.getAttribute('id'))
855 deletions = []
856 if item_ids: # remove the comments of the user on the given post
857 d = self.host.plugins['XEP-0060'].retractItems(service, node_id, item_ids, profile_key=profile_key)
858 d.addCallback(lambda dummy, node_id: log.debug(_(u'Comments of user %(user)s in node %(node)s have been retracted') %
859 {'user': user_jid_s, 'node': node_id}), node_id)
860 d.addErrback(lambda failure, node_id: log.error(_(u"Retraction of comments from %(user)s in node %(node)s failed: %(message)s") %
861 {'user': user_jid_s, 'node': node_id, 'message': failure.getErrorMessage()}), node_id)
862 deletions.append(d)
863 return defer.DeferredList(deletions)
864
865 return self._initialise(profile_key).addCallback(initialised)
866
867 ## helper classes to manipulate items ##
868
869 class DeferredItems():
870 """Retrieve items using XEP-0060"""
871
872 def __init__(self, parent, cb, eb=None, profile_key=C.PROF_KEY_NONE):
873 """
874 @param parent (GroupBlog): GroupBlog instance
875 @param cb (callable): callback method to be applied on items
876 @param eb (callable): errback method to be applied on items
877 @param profile_key (str): %(doc_profile_key)s
878 """
879 self.parent = parent
880 self.cb = cb
881 self.eb = (lambda dummy: ([], {})) if eb is None else eb
882 self.profile_key = profile_key
883
884 def get(self, node, item_ids=None, sub_id=None, rsm_data=None):
885 """Retrieve and process a page of pubsub items
886
887 @param node (str): node identifier.
888 @param item_ids (list[str]): list of items identifiers.
889 @param sub_id (str): optional subscription identifier.
890 @param rsm_data (dict): RSM request data
891 @return: a deferred couple (list, dict) containing:
892 - list of microblog data
893 - RSM response data
894 """
895 if rsm_data is None:
896 rsm_data = {'max_': (len(item_ids) if item_ids else MAX_ITEMS)}
897
898 def initialised(result):
899 profile, client = result
900 rsm_request = rsm.RSMRequest(**rsm_data)
901 d = self.parent.host.plugins["XEP-0060"].getItems(client.item_access_pubsub,
902 node, rsm_request.max,
903 item_ids, sub_id, rsm_request,
904 profile_key=profile)
905
906 def cb(result):
907 d = defer.maybeDeferred(self.cb, result[0], client)
908 return d.addCallback(lambda items: (items, result[1]))
909
910 d.addCallbacks(cb, self.eb)
911 return d
912
913 #TODO: we need to use the server corresponding to the host of the jid
914 return self.parent._initialise(self.profile_key).addCallback(initialised)
915
916
917 class DeferredItemsFromMany():
918 def __init__(self, parent, cb, profile_key=C.PROF_KEY_NONE):
919 """
920 @param parent (GroupBlog): GroupBlog instance
921 @param cb (callable): callback method to be applied on items
922 @param profile_key (str): %(doc_profile_key)s
923 """
924 self.parent = parent
925 self.cb = cb
926 self.profile_key = profile_key
927
928 def _buildData(self, publishers_type, publishers, client):
929 jids = self.parent._getPublishersJIDs(publishers_type, publishers, client)
930 return {publisher: self.parent.getNodeName(publisher) for publisher in jids}
931
932 def get(self, publishers_type, publishers, sub_id=None, rsm_data=None):
933 """Retrieve and process a page of pubsub items
934
935 @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL")
936 @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids)
937 @param sub_id (str): optional subscription identifier.
938 @param rsm_data (dict): RSM request data
939 @return: a deferred dict with:
940 - key: publisher (unicode)
941 - value: couple (list[dict], dict) with:
942 - the microblogs data
943 - RSM response data
944 """
945 if rsm_data is None:
946 rsm_data = {'max_': MAX_ITEMS}
947
948 def initialised(result):
949 profile, client = result
950
951 data = self._buildData(publishers_type, publishers, client)
952 rsm_request = rsm.RSMRequest(**rsm_data)
953 d = self.parent.host.plugins["XEP-0060"].getItemsFromMany(client.item_access_pubsub,
954 data, rsm_request.max, sub_id,
955 rsm_request, profile_key=profile)
956
957 def cb(publisher):
958 def callback(result):
959 d = defer.maybeDeferred(self.cb, result[0], publisher, client)
960 d.addCallback(lambda items: (publisher.full(), (items, result[1])))
961 return d
962 return callback
963
964 def cb_list(result):
965 return {value[0]: value[1] for success, value in result if success}
966
967 def main_cb(result):
968 d_list = []
969 for publisher, d_items in result.items():
970 # XXX: trick needed as publisher is a loop variable
971 d_list.append(d_items.addCallback(cb(publisher)))
972 return defer.DeferredList(d_list, consumeErrors=False).addCallback(cb_list)
973
974 d.addCallback(main_cb)
975 return d
976
977 #TODO: we need to use the server corresponding to the host of the jid
978 return self.parent._initialise(self.profile_key).addCallback(initialised)
979
980 137
981 class GroupBlog_handler(XMPPHandler): 138 class GroupBlog_handler(XMPPHandler):
982 implements(iwokkel.IDisco) 139 implements(iwokkel.IDisco)
983 140
984 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 141 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):