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