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