307
|
1 #!/usr/bin/python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 """ |
|
5 SAT plugin for microbloging with roster access |
|
6 Copyright (C) 2009, 2010, 2011 Jérôme Poisson (goffi@goffi.org) |
|
7 |
|
8 This program is free software: you can redistribute it and/or modify |
|
9 it under the terms of the GNU General Public License as published by |
|
10 the Free Software Foundation, either version 3 of the License, or |
|
11 (at your option) any later version. |
|
12 |
|
13 This program is distributed in the hope that it will be useful, |
|
14 but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
16 GNU General Public License for more details. |
|
17 |
|
18 You should have received a copy of the GNU General Public License |
|
19 along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
20 """ |
|
21 |
|
22 from logging import debug, info, error |
|
23 from twisted.internet import protocol |
|
24 from twisted.words.protocols.jabber import jid |
|
25 from twisted.words.protocols.jabber import error as jab_error |
|
26 import twisted.internet.error |
|
27 from twisted.words.xish import domish |
|
28 from sat.tools.xml_tools import ElementParser |
|
29 |
|
30 from wokkel import disco,pubsub |
|
31 from feed.atom import Entry, Author |
|
32 import uuid |
|
33 from time import time |
|
34 |
|
35 NS_BLOG_COLLECTION = 'urn:xmpp:blogcollection:0' |
|
36 MBLOG_COLLECTION = 'MBLOGCOLLECTION' |
|
37 CONFIG_NODE = 'CONFIG' |
|
38 NS_ACCESS_MODEL = 'pubsub#access_model' |
|
39 NS_PERSIST_ITEMS = 'pubsub#persist_items' |
|
40 NS_MAX_ITEMS = 'pubsub#max_items' |
|
41 NS_NODE_TYPE = 'pubsub#node_type' |
|
42 TYPE_COLLECTION = 'collection' |
|
43 |
|
44 PLUGIN_INFO = { |
|
45 "name": "Group blogging throught collections", |
|
46 "import_name": "groupblog", |
|
47 "type": "MISC", |
|
48 "protocols": [], |
|
49 "dependencies": ["XEP-0277"], |
|
50 "main": "GroupBlog", |
|
51 "handler": "no", |
|
52 "description": _("""Implementation of microblogging with roster access""") |
|
53 } |
|
54 |
|
55 class NodeCreationError(Exception): |
|
56 pass |
|
57 |
|
58 class GroupBlog(): |
|
59 """This class use a PubSub Collection to manage roster access on microblog""" |
|
60 |
|
61 def __init__(self, host): |
|
62 info(_("Group blog plugin initialization")) |
|
63 self.host = host |
|
64 self._blog_nodes={} |
|
65 """host.bridge.addMethod("getLastMicroblogs", ".communication", |
|
66 in_sign='sis', out_sign='aa{ss}', |
|
67 method=self.getLastMicroblogs, |
|
68 async = True, |
|
69 doc = { 'summary':'retrieve items', |
|
70 'param_0':'jid: publisher of wanted microblog', |
|
71 'param_1':'max_items: see XEP-0060 #6.5.7', |
|
72 'param_2':'%(doc_profile)s', |
|
73 'return':'list of microblog data (dict)' |
|
74 }) |
|
75 host.bridge.addMethod("setMicroblogAccess", ".communication", in_sign='ss', out_sign='', |
|
76 method=self.setMicroblogAccess, |
|
77 doc = { |
|
78 })""" |
|
79 |
|
80 host.bridge.addMethod("initBlogCollection", ".communication", in_sign='s', out_sign='', |
|
81 method=self.initBlogCollection, |
|
82 doc = { |
|
83 }) |
|
84 |
|
85 host.bridge.addMethod("getMblogNodes", ".communication", in_sign='s', out_sign='a{sas}', |
|
86 method=self.getMblogNodes, |
|
87 async = True, |
|
88 doc = { 'summary':"retrieve mblog node, and their association with roster's groups", |
|
89 'param_0':'%(doc_profile)s', |
|
90 'return':'list of microblog data (dict)' |
|
91 }) |
|
92 |
|
93 host.bridge.addMethod("sendGroupBlog", ".communication", in_sign='asss', out_sign='', |
|
94 method=self.sendGroupBlog, |
|
95 doc = { 'summary':"Send a microblog to a list of groups", |
|
96 'param_0':'list of groups which can read the microblog', |
|
97 'param_1':'text to send', |
|
98 'param_2':'%(doc_profile)s' |
|
99 }) |
|
100 |
|
101 def _getRootNode(self, entity): |
|
102 return "%(entity)s_%(root_suff)s" % {'entity':entity.userhost(), 'root_suff':MBLOG_COLLECTION} |
|
103 |
|
104 def _getConfigNode(self, entity): |
|
105 return "%(entity)s_%(root_suff)s" % {'entity':entity.userhost(), 'root_suff':CONFIG_NODE} |
|
106 |
|
107 def _configNodeCb(self, result, callback, profile): |
|
108 self._blog_nodes[profile] = {} |
|
109 for item in result: |
|
110 node_ass = item.firstChildElement() |
|
111 assert(node_ass.name == "node_association") |
|
112 node = node_ass['node'] |
|
113 groups = [unicode(group) for group in node_ass.children] |
|
114 self._blog_nodes[profile][node] = groups |
|
115 callback(self._blog_nodes[profile]) |
|
116 |
|
117 def _configNodeFail(self, failure, errback): |
|
118 import pdb |
|
119 pdb.set_trace() |
|
120 errback() #FIXME |
|
121 |
|
122 def _configNodeErr(self, failure, user_jid, pubsub_ent, callback, errback, profile): |
|
123 if failure.value.condition == 'item-not-found': |
|
124 debug(_('Multiblog config node not found, creating it')) |
|
125 _options = {NS_ACCESS_MODEL:"whitelist", NS_PERSIST_ITEMS:1, NS_MAX_ITEMS:-1} |
|
126 d = self.host.plugins["XEP-0060"].createNode(pubsub_ent, self._getConfigNode(user_jid), _options, profile_key=profile) |
|
127 d.addCallback(self._configNodeCb, callback, profile) |
|
128 d.addErrback(self._configNodeFail, errback) |
|
129 else: |
|
130 self._configNodeFail(failure, errback) |
|
131 |
|
132 def getMblogNodes(self, profile_key='@DEFAULT@', callback=None, errback=None): |
|
133 debug(_('Getting mblog nodes')) |
|
134 profile = self.host.memory.getProfileName(profile_key) |
|
135 if not profile: |
|
136 error(_("Unknown profile")) |
|
137 return {} |
|
138 |
|
139 def after_init(ignore): |
|
140 pubsub_ent = self.host.memory.getServerServiceEntity("pubsub", "service", profile) |
|
141 _jid, xmlstream = self.host.getJidNStream(profile_key) |
|
142 d = self.host.plugins["XEP-0060"].getItems(pubsub_ent, self._getConfigNode(_jid), profile_key=profile_key) |
|
143 d.addCallbacks(self._configNodeCb, self._configNodeErr, callbackArgs=(callback, profile), errbackArgs=(_jid, pubsub_ent, callback, errback, profile)) |
|
144 |
|
145 client = self.host.getClient(profile) |
|
146 if not client: |
|
147 error(_('No client for this profile key: %s') % profile_key) |
|
148 return |
|
149 client.client_initialized.addCallback(after_init) |
|
150 |
|
151 |
|
152 def initBlogCollection(self, profile_key="@DEFAULT@"): |
|
153 _jid, xmlstream = self.host.getJidNStream(profile_key) |
|
154 _options = {NS_NODE_TYPE:TYPE_COLLECTION} |
|
155 def cb(result): |
|
156 #Node is created with right permission |
|
157 debug(_("Microblog node collection created")) |
|
158 |
|
159 def fatal_err(s_error): |
|
160 #Something went wrong |
|
161 error(_("Can't create node collection")) |
|
162 |
|
163 def err_cb(s_error): |
|
164 #If the node already exists, the condition is "conflict", |
|
165 #else we have an unmanaged error |
|
166 if s_error.value.condition=='conflict': |
|
167 fatal_err(s_error) |
|
168 else: |
|
169 fatal_err(s_error) |
|
170 |
|
171 def create_node(): |
|
172 #return self.host.plugins["XEP-0060"].createNode(_jid.userhostJID(), NS_BLOG_COLLECTION, _options, profile_key=profile_key) |
|
173 return self.host.plugins["XEP-0060"].createNode(jid.JID("pubsub.tazar.int"), self._getRootNode(_jid), _options, profile_key=profile_key) |
|
174 |
|
175 create_node().addCallback(cb).addErrback(err_cb) |
|
176 |
|
177 def _publishMblog(self, name, message, pubsub_ent, profile): |
|
178 """Actually publish the message on the group blog |
|
179 @param name: name of the node where we publish |
|
180 @param message: message to publish |
|
181 @param pubsub_ent: entity of the publish-subscribe service |
|
182 @param profile: profile of the owner of the group""" |
|
183 mblog_item = self.host.plugins["XEP-0277"].data2entry({'content':message}, profile) |
|
184 defer_blog = self.host.plugins["XEP-0060"].publish(pubsub_ent, name, items=[mblog_item], profile_key=profile) |
|
185 defer_blog.addErrback(self._mblogPublicationFailed) |
|
186 |
|
187 def _groupNodeCreated(self, ignore, groups, name, message, user_jid, pubsub_ent, profile): |
|
188 """A group node as been created, we need to add it to the configure node, and send the message to it |
|
189 @param groups: list of groups authorized to subscribe to the node |
|
190 @param name: unique name of the group |
|
191 @param message: message to publish to the group |
|
192 @param user_jid: jid of the owner of the node |
|
193 @param pubsub_ent: entity of the publish-subscribe service |
|
194 @param profile: profile of the owner of the group""" |
|
195 config_node = self._getConfigNode(user_jid) |
|
196 _payload = domish.Element(('','node_association')) |
|
197 _payload['node'] = name |
|
198 for group in groups: |
|
199 _payload.addElement('group',content=group) |
|
200 config_item = pubsub.Item(payload=_payload) |
|
201 defer_config = self.host.plugins["XEP-0060"].publish(pubsub_ent, config_node, items=[config_item], profile_key=profile) |
|
202 defer_config.addCallback(lambda x: debug(_("Configuration node updated"))) |
|
203 defer_config.addErrback(self._configUpdateFailed) |
|
204 |
|
205 #Finally, we publish the message |
|
206 self._publishMblog(name, message, pubsub_ent, profile) |
|
207 |
|
208 |
|
209 def _mblogPublicationFailed(self, failure): |
|
210 #TODO |
|
211 import pdb |
|
212 pdb.set_trace() |
|
213 |
|
214 def _configUpdateFailed(self, failure): |
|
215 #TODO |
|
216 import pdb |
|
217 pdb.set_trace() |
|
218 |
|
219 def _nodeCreationFailed(self, failure, name, user_jid, groups, pubsub_ent, message, profile): |
|
220 #TODO |
|
221 if failure.value.condition == "item-not-found": |
|
222 #The root node doesn't exists |
|
223 def err_creating_root_node(failure): |
|
224 msg = _("Can't create Root node") |
|
225 error(msg) |
|
226 raise NodeCreationError(msg) |
|
227 |
|
228 _options = {NS_NODE_TYPE:TYPE_COLLECTION} |
|
229 d = self.host.plugins["XEP-0060"].createNode(pubsub_ent, self._getRootNode(user_jid), _options, profile_key=profile) |
|
230 d.addCallback(self._createNode, name, user_jid, groups, pubsub_ent, message, profile) |
|
231 d.addErrback(err_creating_root_node) |
|
232 else: |
|
233 import pdb |
|
234 pdb.set_trace() |
|
235 |
|
236 def _createNode(self, ignore, name, user_jid, groups, pubsub_ent, message, profile): |
|
237 """create a group microblog node |
|
238 @param ignore: ignored param, necessary to be added as a deferred callback |
|
239 @param name: name of the node |
|
240 @param user_jid: jid of the user creating the node |
|
241 @param groups: list of group than can subscribe to the node |
|
242 @param pubsub_ent: publish/subscribe service's entity |
|
243 @param message: message to publish |
|
244 @param profile: profile of the user creating the node""" |
|
245 _options = {NS_ACCESS_MODEL:"roster", NS_PERSIST_ITEMS:1, NS_MAX_ITEMS:-1, |
|
246 'pubsub#node_type':'leaf', 'pubsub#collection':self._getRootNode(user_jid), |
|
247 'pubsub#roster_groups_allowed':groups} |
|
248 d = self.host.plugins["XEP-0060"].createNode(pubsub_ent, name, _options, profile_key=profile) |
|
249 d.addCallback(self._groupNodeCreated, groups, name, message, user_jid, pubsub_ent, profile) |
|
250 d.addErrback(self._nodeCreationFailed, name, user_jid, groups, pubsub_ent, message, profile) |
|
251 |
|
252 def _getNodeForGroups(self, groups, profile): |
|
253 """Return node associated with the given list of groups |
|
254 @param groups: list of groups |
|
255 @param profile: profile of publisher""" |
|
256 for node in self._blog_nodes[profile]: |
|
257 node_groups = self._blog_nodes[profile][node] |
|
258 if set(node_groups) == set(groups): |
|
259 return node |
|
260 return None |
|
261 |
|
262 def sendGroupBlog(self, groups, message, profile_key='@DEFAULT@'): |
|
263 """Publish a microblog to the node associated to the groups |
|
264 If the node doesn't exist, it is created, then the message is posted |
|
265 @param groups: list of groups allowed to retrieve the microblog |
|
266 @param message: microblog |
|
267 @profile_key: %(doc_profile)s |
|
268 """ |
|
269 profile = self.host.memory.getProfileName(profile_key) |
|
270 if not profile: |
|
271 error(_("Unknown profile")) |
|
272 return |
|
273 |
|
274 def after_init(ignore): |
|
275 _groups = list(set(groups).intersection(client.roster.getGroups())) #We only keep group which actually exist |
|
276 #TODO: send an error signal if user want to post to non existant groups |
|
277 _groups.sort() |
|
278 pubsub_ent = self.host.memory.getServerServiceEntity("pubsub", "service", profile) |
|
279 for group in _groups: |
|
280 _node = self._getNodeForGroups([group], profile) |
|
281 if not _node: |
|
282 _node_name = unicode(uuid.uuid4()) |
|
283 self._createNode(None, _node_name, client.jid, [group], pubsub_ent, message, profile) |
|
284 else: |
|
285 self._publishMblog(_node, message, pubsub_ent, profile) |
|
286 |
|
287 client = self.host.getClient(profile) |
|
288 if not client: |
|
289 error(_('No client for this profile key: %s') % profile_key) |
|
290 return |
|
291 client.client_initialized.addCallback(after_init) |
|
292 |