Mercurial > libervia-backend
comparison src/plugins/plugin_misc_groupblog.py @ 615:6f4c31192c7c
plugins XEP-0060, XEP-0277, groupblog: comments implementation (first draft, not finished yet):
- PubSub options constants are moved to XEP-0060
- comments url are generated/parsed according to XEP-0277
- microblog data can now have the following keys:
- "comments", with the url as given in the <link> tag
- "comments_service", with the jid of the PubSub service hosting the comments
- "comments_node", with the parsed node
- comments nodes use different access_model according to parent microblog item access
- publisher is not verified yet, see FIXME warning
- so far, comments node are automatically subscribed
- some bug fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 20 May 2013 23:21:29 +0200 |
parents | 84a6e83157c2 |
children | 8782f94e761e |
comparison
equal
deleted
inserted
replaced
614:bef0f893482f | 615:6f4c31192c7c |
---|---|
23 | 23 |
24 from wokkel import disco, data_form, iwokkel | 24 from wokkel import disco, data_form, iwokkel |
25 | 25 |
26 from zope.interface import implements | 26 from zope.interface import implements |
27 | 27 |
28 import uuid | |
29 import urllib | |
30 | |
28 try: | 31 try: |
29 from twisted.words.protocols.xmlstream import XMPPHandler | 32 from twisted.words.protocols.xmlstream import XMPPHandler |
30 except ImportError: | 33 except ImportError: |
31 from wokkel.subprotocols import XMPPHandler | 34 from wokkel.subprotocols import XMPPHandler |
32 | 35 |
33 NS_PUBSUB = 'http://jabber.org/protocol/pubsub' | 36 NS_PUBSUB = 'http://jabber.org/protocol/pubsub' |
34 NS_GROUPBLOG = 'http://goffi.org/protocol/groupblog' | 37 NS_GROUPBLOG = 'http://goffi.org/protocol/groupblog' |
35 NS_NODE_PREFIX = 'urn:xmpp:groupblog:' | 38 NS_NODE_PREFIX = 'urn:xmpp:groupblog:' |
39 NS_COMMENT_PREFIX = 'urn:xmpp:comments:' | |
36 #NS_PUBSUB_EXP = 'http://goffi.org/protocol/pubsub' #for non official features | 40 #NS_PUBSUB_EXP = 'http://goffi.org/protocol/pubsub' #for non official features |
37 NS_PUBSUB_EXP = NS_PUBSUB # XXX: we can't use custom namespace as Wokkel's PubSubService use official NS | 41 NS_PUBSUB_EXP = NS_PUBSUB # XXX: we can't use custom namespace as Wokkel's PubSubService use official NS |
38 NS_PUBSUB_ITEM_ACCESS = NS_PUBSUB_EXP + "#item-access" | 42 NS_PUBSUB_ITEM_ACCESS = NS_PUBSUB_EXP + "#item-access" |
39 NS_PUBSUB_CREATOR_JID_CHECK = NS_PUBSUB_EXP + "#creator-jid-check" | 43 NS_PUBSUB_CREATOR_JID_CHECK = NS_PUBSUB_EXP + "#creator-jid-check" |
40 NS_PUBSUB_ITEM_CONFIG = NS_PUBSUB_EXP + "#item-config" | 44 NS_PUBSUB_ITEM_CONFIG = NS_PUBSUB_EXP + "#item-config" |
41 NS_PUBSUB_AUTO_CREATE = NS_PUBSUB + "#auto-create" | 45 NS_PUBSUB_AUTO_CREATE = NS_PUBSUB + "#auto-create" |
42 OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed' | |
43 OPT_ACCESS_MODEL = 'pubsub#access_model' | |
44 OPT_PERSIST_ITEMS = 'pubsub#persist_items' | |
45 OPT_MAX_ITEMS = 'pubsub#max_items' | |
46 OPT_NODE_TYPE = 'pubsub#node_type' | |
47 OPT_SUBSCRIPTION_TYPE = 'pubsub#subscription_type' | |
48 OPT_SUBSCRIPTION_DEPTH = 'pubsub#subscription_depth' | |
49 TYPE_COLLECTION = 'collection' | 46 TYPE_COLLECTION = 'collection' |
47 ACCESS_TYPE_MAP = { 'PUBLIC': 'open', | |
48 'GROUP': 'roster', | |
49 'JID': None, #JID is not yet managed | |
50 } | |
50 | 51 |
51 PLUGIN_INFO = { | 52 PLUGIN_INFO = { |
52 "name": "Group blogging throught collections", | 53 "name": "Group blogging throught collections", |
53 "import_name": "groupblog", | 54 "import_name": "groupblog", |
54 "type": "MISC", | 55 "type": "MISC", |
81 | 82 |
82 def __init__(self, host): | 83 def __init__(self, host): |
83 info(_("Group blog plugin initialization")) | 84 info(_("Group blog plugin initialization")) |
84 self.host = host | 85 self.host = host |
85 | 86 |
86 host.bridge.addMethod("sendGroupBlog", ".plugin", in_sign='sasss', out_sign='', | 87 host.bridge.addMethod("sendGroupBlog", ".plugin", in_sign='sassa{ss}s', out_sign='', |
87 method=self.sendGroupBlog) | 88 method=self.sendGroupBlog) |
89 | |
90 host.bridge.addMethod("sendGroupBlogComment", ".plugin", in_sign='sss', out_sign='', | |
91 method=self.sendGroupBlogComment, | |
92 async=True) | |
88 | 93 |
89 host.bridge.addMethod("getLastGroupBlogs", ".plugin", | 94 host.bridge.addMethod("getLastGroupBlogs", ".plugin", |
90 in_sign='sis', out_sign='aa{ss}', | 95 in_sign='sis', out_sign='aa{ss}', |
91 method=self.getLastGroupBlogs, | 96 method=self.getLastGroupBlogs, |
92 async=True) | 97 async=True) |
137 if set([NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK]).issubset(_disco.features): | 142 if set([NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK]).issubset(_disco.features): |
138 info(_("item-access powered pubsub service found: [%s]") % entity.full()) | 143 info(_("item-access powered pubsub service found: [%s]") % entity.full()) |
139 client.item_access_pubsub = entity | 144 client.item_access_pubsub = entity |
140 client._item_access_pubsub_pending.callback(None) | 145 client._item_access_pubsub_pending.callback(None) |
141 | 146 |
142 if hasattr(client, "_item_access_pubsub_pending"): | 147 if "_item_access_pubsub_pending" in client: |
143 #XXX: we need to wait for item access pubsub service check | 148 #XXX: we need to wait for item access pubsub service check |
144 yield client._item_access_pubsub_pending | 149 yield client._item_access_pubsub_pending |
145 del client._item_access_pubsub_pending | 150 del client._item_access_pubsub_pending |
146 | 151 |
147 if not client.item_access_pubsub: | 152 if not client.item_access_pubsub: |
150 | 155 |
151 defer.returnValue((profile, client)) | 156 defer.returnValue((profile, client)) |
152 | 157 |
153 def pubSubItemsReceivedTrigger(self, event, profile): | 158 def pubSubItemsReceivedTrigger(self, event, profile): |
154 """"Trigger which catch groupblogs events""" | 159 """"Trigger which catch groupblogs events""" |
160 | |
155 if event.nodeIdentifier.startswith(NS_NODE_PREFIX): | 161 if event.nodeIdentifier.startswith(NS_NODE_PREFIX): |
162 # Microblog | |
156 publisher = jid.JID(event.nodeIdentifier[len(NS_NODE_PREFIX):]) | 163 publisher = jid.JID(event.nodeIdentifier[len(NS_NODE_PREFIX):]) |
157 origin_host = publisher.host.split('.') | 164 origin_host = publisher.host.split('.') |
158 event_host = event.sender.host.split('.') | 165 event_host = event.sender.host.split('.') |
159 #FIXME: basic origin check, must be improved | 166 #FIXME: basic origin check, must be improved |
160 #TODO: automatic security test | 167 #TODO: automatic security test |
167 for item in event.items: | 174 for item in event.items: |
168 microblog_data = self.item2gbdata(item) | 175 microblog_data = self.item2gbdata(item) |
169 | 176 |
170 self.host.bridge.personalEvent(publisher.full(), "MICROBLOG", microblog_data, profile) | 177 self.host.bridge.personalEvent(publisher.full(), "MICROBLOG", microblog_data, profile) |
171 return False | 178 return False |
179 elif event.nodeIdentifier.startswith(NS_COMMENT_PREFIX): | |
180 # Comment | |
181 for item in event.items: | |
182 publisher = "" # FIXME: publisher attribute for item in SàT pubsub is not managed yet, so | |
183 # publisher is not checked and can be easily spoofed. This need to be fixed | |
184 # quickly. | |
185 microblog_data = self.item2gbdata(item) | |
186 microblog_data["comments_service"] = event.sender.userhost() | |
187 microblog_data["comments_node"] = event.nodeIdentifier | |
188 microblog_data["verified_publisher"] = "true" if publisher else "false" | |
189 | |
190 self.host.bridge.personalEvent(publisher.full() if publisher else microblog_data["author"], "MICROBLOG_COMMENT", microblog_data, profile) | |
191 return False | |
172 return True | 192 return True |
173 | 193 |
174 def _parseAccessData(self, microblog_data, item): | 194 def _parseAccessData(self, microblog_data, item): |
195 P = self.host.plugins["XEP-0060"] | |
175 form_elts = filter(lambda elt: elt.name == "x", item.children) | 196 form_elts = filter(lambda elt: elt.name == "x", item.children) |
176 for form_elt in form_elts: | 197 for form_elt in form_elts: |
177 form = data_form.Form.fromElement(form_elt) | 198 form = data_form.Form.fromElement(form_elt) |
178 | 199 |
179 if (form.formNamespace == NS_PUBSUB_ITEM_CONFIG): | 200 if (form.formNamespace == NS_PUBSUB_ITEM_CONFIG): |
180 access_model = form.get(OPT_ACCESS_MODEL, 'open') | 201 access_model = form.get(P.OPT_ACCESS_MODEL, 'open') |
181 if access_model == "roster": | 202 if access_model == "roster": |
182 try: | 203 try: |
183 microblog_data["groups"] = '\n'.join(form.fields[OPT_ROSTER_GROUPS_ALLOWED].values) | 204 microblog_data["groups"] = '\n'.join(form.fields[P.OPT_ROSTER_GROUPS_ALLOWED].values) |
184 except KeyError: | 205 except KeyError: |
185 warning("No group found for roster access-model") | 206 warning("No group found for roster access-model") |
186 microblog_data["groups"] = '' | 207 microblog_data["groups"] = '' |
187 | 208 |
188 break | 209 break |
197 """Retrieve the name of publisher's node | 218 """Retrieve the name of publisher's node |
198 @param publisher: publisher's jid | 219 @param publisher: publisher's jid |
199 @return: node's name (string)""" | 220 @return: node's name (string)""" |
200 return NS_NODE_PREFIX + publisher.userhost() | 221 return NS_NODE_PREFIX + publisher.userhost() |
201 | 222 |
202 def _publishMblog(self, service, client, access_type, access_list, message): | 223 def _publishMblog(self, service, client, access_type, access_list, message, options): |
203 """Actually publish the message on the group blog | 224 """Actually publish the message on the group blog |
204 @param service: jid of the item-access pubsub service | 225 @param service: jid of the item-access pubsub service |
205 @param client: SatXMPPClient of the published | 226 @param client: SatXMPPClient of the published |
206 @param access_type: one of "PUBLIC", "GROUP", "JID" | 227 @param access_type: one of "PUBLIC", "GROUP", "JID" |
207 @param access_list: set of entities (empty list for all, groups or jids) allowed to see the item | 228 @param access_list: set of entities (empty list for all, groups or jids) allowed to see the item |
208 @param message: message to publish | 229 @param message: message to publish |
209 """ | 230 @param options: dict which option name as key, which can be: |
210 mblog_item = self.host.plugins["XEP-0277"].data2entry({'content': message}, client.profile) | 231 - allow_comments: True to accept comments, False else (default: False) |
232 """ | |
233 node_name = self.getNodeName(client.jid) | |
234 mblog_data = {'content': message} | |
235 P = self.host.plugins["XEP-0060"] | |
236 access_model_value = ACCESS_TYPE_MAP[access_type] | |
237 | |
238 if options.get('allow_comments', 'False').lower() == 'true': | |
239 comments_node = "%s_%s__%s" % (NS_COMMENT_PREFIX, str(uuid.uuid4()), node_name) | |
240 mblog_data['comments'] = "xmpp:%(service)s?%(query)s" % {'service': service.userhost(), | |
241 'query': urllib.urlencode([('node',comments_node.encode('utf-8'))])} | |
242 _options = {P.OPT_ACCESS_MODEL: access_model_value, | |
243 P.OPT_PERSIST_ITEMS: 1, | |
244 P.OPT_MAX_ITEMS: -1, | |
245 P.OPT_DELIVER_PAYLOADS: 1, | |
246 P.OPT_SEND_ITEM_SUBSCRIBE: 1, | |
247 P.OPT_PUBLISH_MODEL: "subscribers", #TODO: should be open if *both* node and item access_model are open (public node and item) | |
248 } | |
249 if access_model_value == 'roster': | |
250 _options[P.OPT_ROSTER_GROUPS_ALLOWED] = list(access_list) | |
251 | |
252 # FIXME: check comments node creation success, at the moment this is a potential security risk (if the node | |
253 # already exists, the creation will silently fail, but the comments link will stay the same, linking to a | |
254 # node owned by somebody else) | |
255 defer_blog = self.host.plugins["XEP-0060"].createNode(service, comments_node, _options, profile_key=client.profile) | |
256 | |
257 mblog_item = self.host.plugins["XEP-0277"].data2entry(mblog_data, client.profile) | |
211 form = data_form.Form('submit', formNamespace=NS_PUBSUB_ITEM_CONFIG) | 258 form = data_form.Form('submit', formNamespace=NS_PUBSUB_ITEM_CONFIG) |
259 | |
212 if access_type == "PUBLIC": | 260 if access_type == "PUBLIC": |
213 if access_list: | 261 if access_list: |
214 raise BadAccessListError("access_list must be empty for PUBLIC access") | 262 raise BadAccessListError("access_list must be empty for PUBLIC access") |
215 access = data_form.Field(None, OPT_ACCESS_MODEL, value="open") | 263 access = data_form.Field(None, P.OPT_ACCESS_MODEL, value=access_model_value) |
216 form.addField(access) | 264 form.addField(access) |
217 elif access_type == "GROUP": | 265 elif access_type == "GROUP": |
218 access = data_form.Field(None, OPT_ACCESS_MODEL, value="roster") | 266 access = data_form.Field(None, P.OPT_ACCESS_MODEL, value=access_model_value) |
219 allowed = data_form.Field(None, OPT_ROSTER_GROUPS_ALLOWED, values=access_list) | 267 allowed = data_form.Field(None, P.OPT_ROSTER_GROUPS_ALLOWED, values=access_list) |
220 form.addField(access) | 268 form.addField(access) |
221 form.addField(allowed) | 269 form.addField(allowed) |
222 mblog_item.addChild(form.toElement()) | 270 mblog_item.addChild(form.toElement()) |
223 elif access_type == "JID": | 271 elif access_type == "JID": |
224 raise NotImplementedError | 272 raise NotImplementedError |
225 else: | 273 else: |
226 error(_("Unknown access_type")) | 274 error(_("Unknown access_type")) |
227 raise BadAccessTypeError | 275 raise BadAccessTypeError |
228 defer_blog = self.host.plugins["XEP-0060"].publish(service, self.getNodeName(client.jid), items=[mblog_item], profile_key=client.profile) | 276 |
277 defer_blog = self.host.plugins["XEP-0060"].publish(service, node_name, items=[mblog_item], profile_key=client.profile) | |
229 defer_blog.addErrback(self._mblogPublicationFailed) | 278 defer_blog.addErrback(self._mblogPublicationFailed) |
230 | 279 |
231 def _mblogPublicationFailed(self, failure): | 280 def _mblogPublicationFailed(self, failure): |
232 #TODO | 281 #TODO |
233 return failure | 282 return failure |
234 | 283 |
235 def sendGroupBlog(self, access_type, access_list, message, profile_key='@DEFAULT@'): | 284 def sendGroupBlog(self, access_type, access_list, message, options, profile_key='@NONE@'): |
236 """Publish a microblog with given item access | 285 """Publish a microblog with given item access |
237 @param access_type: one of "PUBLIC", "GROUP", "JID" | 286 @param access_type: one of "PUBLIC", "GROUP", "JID" |
238 @param access_list: list of authorized entity (empty list for PUBLIC ACCESS, | 287 @param access_list: list of authorized entity (empty list for PUBLIC ACCESS, |
239 list of groups or list of jids) for this item | 288 list of groups or list of jids) for this item |
240 @param message: microblog | 289 @param message: microblog |
290 @param options: dict which option name as key, which can be: | |
291 - allow_comments: True to accept comments, False else (default: False) | |
241 @profile_key: %(doc_profile)s | 292 @profile_key: %(doc_profile)s |
242 """ | 293 """ |
243 print "sendGroupBlog" | |
244 | 294 |
245 def initialised(result): | 295 def initialised(result): |
246 profile, client = result | 296 profile, client = result |
247 if access_type == "PUBLIC": | 297 if access_type == "PUBLIC": |
248 if access_list: | 298 if access_list: |
249 raise Exception("Publishers list must be empty when getting microblogs for all contacts") | 299 raise Exception("Publishers list must be empty when getting microblogs for all contacts") |
250 self._publishMblog(client.item_access_pubsub, client, "PUBLIC", [], message) | 300 self._publishMblog(client.item_access_pubsub, client, "PUBLIC", [], message, options) |
251 elif access_type == "GROUP": | 301 elif access_type == "GROUP": |
252 _groups = set(access_list).intersection(client.roster.getGroups()) # We only keep group which actually exist | 302 _groups = set(access_list).intersection(client.roster.getGroups()) # We only keep group which actually exist |
253 if not _groups: | 303 if not _groups: |
254 raise BadAccessListError("No valid group") | 304 raise BadAccessListError("No valid group") |
255 self._publishMblog(client.item_access_pubsub, client, "GROUP", _groups, message) | 305 self._publishMblog(client.item_access_pubsub, client, "GROUP", _groups, message, options) |
256 elif access_type == "JID": | 306 elif access_type == "JID": |
257 raise NotImplementedError | 307 raise NotImplementedError |
258 else: | 308 else: |
259 error(_("Unknown access type")) | 309 error(_("Unknown access type")) |
260 raise BadAccessTypeError | 310 raise BadAccessTypeError |
261 | 311 |
262 self.initialise(profile_key).addCallback(initialised) | 312 self.initialise(profile_key).addCallback(initialised) |
263 | 313 |
264 def getLastGroupBlogs(self, pub_jid, max_items=10, profile_key='@DEFAULT@'): | 314 def sendGroupBlogComment(self, node_url, message, profile_key='@DEFAULT@'): |
315 """Publish a comment in the given node | |
316 @param node url: link to the comments node as specified in XEP-0277 and given in microblog data's comments key | |
317 @param message: comment | |
318 @profile_key: %(doc_profile)s | |
319 """ | |
320 profile = self.host.memory.getProfileName(profile_key) | |
321 if not profile: | |
322 error(_("Unknown profile")) | |
323 raise Exception("Unknown profile") | |
324 | |
325 service, node = self.host.plugins["XEP-0277"].parseCommentUrl(node_url) | |
326 | |
327 mblog_data = {'content': message} | |
328 mblog_item = self.host.plugins["XEP-0277"].data2entry(mblog_data, profile) | |
329 | |
330 return self.host.plugins["XEP-0060"].publish(service, node, items=[mblog_item], profile_key=profile) | |
331 | |
332 def _itemsConstruction(self, items, pub_jid, client): | |
333 """ Transforms items to group blog data and manage comments node | |
334 @param items: iterable of items | |
335 @return: list of group blog data """ | |
336 ret = [] | |
337 for item in items: | |
338 gbdata = self.item2gbdata(item) | |
339 ret.append(gbdata) | |
340 # if there is a comments node, we subscribe to it | |
341 if "comments_node" in gbdata and pub_jid.userhostJID() != client.jid.userhostJID(): | |
342 try: | |
343 self.host.plugins["XEP-0060"].subscribe(gbdata["comments_service"], gbdata["comments_node"], | |
344 profile_key=client.profile) | |
345 self.host.plugins["XEP-0060"].getItems(gbdata["comments_service"], gbdata["comments_node"], profile_key=client.profile) | |
346 except KeyError: | |
347 warning("Missing key for comments") | |
348 return ret | |
349 | |
350 def getLastGroupBlogs(self, pub_jid_s, max_items=10, profile_key='@DEFAULT@'): | |
265 """Get the last published microblogs | 351 """Get the last published microblogs |
266 @param pub_jid: jid of the publisher | 352 @param pub_jid_s: jid of the publisher |
267 @param max_items: how many microblogs we want to get (see XEP-0060 #6.5.7) | 353 @param max_items: how many microblogs we want to get (see XEP-0060 #6.5.7) |
268 @param profile_key: profile key | 354 @param profile_key: profile key |
269 @return: list of microblog data (dict) | 355 @return: list of microblog data (dict) |
270 """ | 356 """ |
357 pub_jid = jid.JID(pub_jid_s) | |
271 | 358 |
272 def initialised(result): | 359 def initialised(result): |
273 profile, client = result | 360 profile, client = result |
274 d = self.host.plugins["XEP-0060"].getItems(client.item_access_pubsub, self.getNodeName(jid.JID(pub_jid)), | 361 d = self.host.plugins["XEP-0060"].getItems(client.item_access_pubsub, self.getNodeName(pub_jid), |
275 max_items=max_items, profile_key=profile_key) | 362 max_items=max_items, profile_key=profile_key) |
276 d.addCallback(lambda items: map(self.item2gbdata, items)) | 363 d.addCallback(self._itemsConstruction, pub_jid, client) |
277 d.addErrback(lambda ignore: {}) # TODO: more complete error management (log !) | 364 d.addErrback(lambda ignore: {}) # TODO: more complete error management (log !) |
278 return d | 365 return d |
279 | 366 |
280 #TODO: we need to use the server corresponding the the host of the jid | 367 #TODO: we need to use the server corresponding the the host of the jid |
281 return self.initialise(profile_key).addCallback(initialised) | 368 return self.initialise(profile_key).addCallback(initialised) |
315 else: | 402 else: |
316 raise UnknownType | 403 raise UnknownType |
317 | 404 |
318 mblogs = [] | 405 mblogs = [] |
319 | 406 |
320 for _jid in jids: | 407 for jid_s in jids: |
321 d = self.host.plugins["XEP-0060"].getItems(client.item_access_pubsub, self.getNodeName(jid.JID(_jid)), | 408 _jid = jid.JID(jid_s) |
409 d = self.host.plugins["XEP-0060"].getItems(client.item_access_pubsub, self.getNodeName(_jid), | |
322 max_items=max_items, profile_key=profile_key) | 410 max_items=max_items, profile_key=profile_key) |
323 d.addCallback(lambda items, source_jid: (source_jid, map(self.item2gbdata, items)), _jid) | 411 d.addCallback(lambda items, source_jid: (source_jid, self._itemsConstruction(items, _jid, client)), jid_s) |
412 | |
324 mblogs.append(d) | 413 mblogs.append(d) |
325 dlist = defer.DeferredList(mblogs) | 414 dlist = defer.DeferredList(mblogs) |
326 dlist.addCallback(sendResult) | 415 dlist.addCallback(sendResult) |
327 | 416 |
328 return dlist | 417 return dlist |
333 if publishers_type == "ALL" and publishers: | 422 if publishers_type == "ALL" and publishers: |
334 raise Exception("Publishers list must be empty when getting microblogs for all contacts") | 423 raise Exception("Publishers list must be empty when getting microblogs for all contacts") |
335 return self.initialise(profile_key).addCallback(initialised) | 424 return self.initialise(profile_key).addCallback(initialised) |
336 #TODO: we need to use the server corresponding the the host of the jid | 425 #TODO: we need to use the server corresponding the the host of the jid |
337 | 426 |
338 def subscribeGroupBlog(self, pub_jid, profile_key='@DEFAULT'): | 427 def subscribeGroupBlog(self, pub_jid, profile_key='@DEFAULT@'): |
339 def initialised(result): | 428 def initialised(result): |
340 profile, client = result | 429 profile, client = result |
341 d = self.host.plugins["XEP-0060"].subscribe(client.item_access_pubsub, self.getNodeName(jid.JID(pub_jid)), | 430 d = self.host.plugins["XEP-0060"].subscribe(client.item_access_pubsub, self.getNodeName(jid.JID(pub_jid)), |
342 profile_key=profile_key) | 431 profile_key=profile_key) |
343 return d | 432 return d |