2484
|
1 #!/usr/bin/env python2 |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # SAT plugin for Pubsub Schemas |
2771
|
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) |
2484
|
6 |
|
7 # This program is free software: you can redistribute it and/or modify |
|
8 # it under the terms of the GNU Affero General Public License as published by |
|
9 # the Free Software Foundation, either version 3 of the License, or |
|
10 # (at your option) any later version. |
|
11 |
|
12 # This program is distributed in the hope that it will be useful, |
|
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15 # GNU Affero General Public License for more details. |
|
16 |
|
17 # You should have received a copy of the GNU Affero General Public License |
|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
19 |
|
20 from sat.core.i18n import _ |
|
21 from sat.core.constants import Const as C |
|
22 from sat.core import exceptions |
|
23 from sat.core.log import getLogger |
|
24 from sat.tools.common import uri |
|
25 from twisted.words.protocols.jabber import jid |
|
26 from twisted.words.xish import domish |
|
27 from twisted.internet import defer |
|
28 import shortuuid |
|
29 import json |
|
30 log = getLogger(__name__) |
|
31 |
|
32 NS_FORUMS = u'org.salut-a-toi.forums:0' |
|
33 NS_FORUMS_TOPICS = NS_FORUMS + u'#topics' |
|
34 |
|
35 PLUGIN_INFO = { |
|
36 C.PI_NAME: _("forums management"), |
|
37 C.PI_IMPORT_NAME: "forums", |
|
38 C.PI_TYPE: "EXP", |
|
39 C.PI_PROTOCOLS: [], |
|
40 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0277"], |
|
41 C.PI_MAIN: "forums", |
|
42 C.PI_HANDLER: "no", |
|
43 C.PI_DESCRIPTION: _("""forums management plugin""") |
|
44 } |
|
45 FORUM_ATTR = {u'title', u'name', u'main-language', u'uri'} |
|
46 FORUM_SUB_ELTS = (u'short-desc', u'desc') |
|
47 FORUM_TOPICS_NODE_TPL = u'{node}#topics_{uuid}' |
|
48 FORUM_TOPIC_NODE_TPL = u'{node}_{uuid}' |
|
49 |
|
50 |
|
51 class forums(object): |
|
52 |
|
53 def __init__(self, host): |
|
54 log.info(_(u"forums plugin initialization")) |
|
55 self.host = host |
|
56 self._m = self.host.plugins['XEP-0277'] |
|
57 self._p = self.host.plugins['XEP-0060'] |
|
58 self._node_options = { |
|
59 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, |
|
60 self._p.OPT_PERSIST_ITEMS: 1, |
|
61 self._p.OPT_MAX_ITEMS: -1, |
|
62 self._p.OPT_DELIVER_PAYLOADS: 1, |
|
63 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, |
|
64 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, |
|
65 } |
|
66 host.registerNamespace('forums', NS_FORUMS) |
|
67 host.bridge.addMethod("forumsGet", ".plugin", |
|
68 in_sign='ssss', out_sign='s', |
|
69 method=self._get, |
|
70 async=True) |
|
71 host.bridge.addMethod("forumsSet", ".plugin", |
|
72 in_sign='sssss', out_sign='', |
|
73 method=self._set, |
|
74 async=True) |
|
75 host.bridge.addMethod("forumTopicsGet", ".plugin", |
|
76 in_sign='ssa{ss}s', out_sign='(aa{ss}a{ss})', |
|
77 method=self._getTopics, |
|
78 async=True) |
|
79 host.bridge.addMethod("forumTopicCreate", ".plugin", |
|
80 in_sign='ssa{ss}s', out_sign='', |
|
81 method=self._createTopic, |
|
82 async=True) |
|
83 |
|
84 @defer.inlineCallbacks |
|
85 def _createForums(self, client, forums, service, node, forums_elt=None, names=None): |
|
86 """recursively create <forums> element(s) |
|
87 |
|
88 @param forums(list): forums which may have subforums |
|
89 @param service(jid.JID): service where the new nodes will be created |
|
90 @param node(unicode): node of the forums |
|
91 will be used as basis for the newly created nodes |
|
92 @param parent_elt(domish.Element, None): element where the forum must be added |
|
93 if None, the root <forums> element will be created |
|
94 @return (domish.Element): created forums |
|
95 """ |
|
96 if not isinstance(forums, list): |
|
97 raise ValueError(_(u"forums arguments must be a list of forums")) |
|
98 if forums_elt is None: |
|
99 forums_elt = domish.Element((NS_FORUMS, u'forums')) |
|
100 assert names is None |
|
101 names = set() |
|
102 else: |
|
103 if names is None or forums_elt.name != u'forums': |
|
104 raise exceptions.InternalError(u'invalid forums or names') |
|
105 assert names is not None |
|
106 |
|
107 for forum in forums: |
|
108 if not isinstance(forum, dict): |
|
109 raise ValueError(_(u"A forum item must be a dictionary")) |
|
110 forum_elt = forums_elt.addElement('forum') |
|
111 |
|
112 for key, value in forum.iteritems(): |
|
113 if key == u'name' and key in names: |
|
114 raise exceptions.ConflictError(_(u"following forum name is not unique: {name}").format(name=key)) |
|
115 if key == u'uri' and not value.strip(): |
|
116 log.info(_(u"creating missing forum node")) |
|
117 forum_node = FORUM_TOPICS_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) |
|
118 yield self._p.createNode(client, service, forum_node, self._node_options) |
|
119 value = uri.buildXMPPUri(u'pubsub', |
|
120 path=service.full(), |
|
121 node=forum_node) |
|
122 if key in FORUM_ATTR: |
|
123 forum_elt[key] = value.strip() |
|
124 elif key in FORUM_SUB_ELTS: |
|
125 forum_elt.addElement(key, content=value) |
|
126 elif key == u'sub-forums': |
|
127 sub_forums_elt = forum_elt.addElement(u'forums') |
|
128 yield self._createForums(client, value, service, node, sub_forums_elt, names=names) |
|
129 else: |
|
130 log.warning(_(u"Unknown forum attribute: {key}").format(key=key)) |
|
131 if not forum_elt.getAttribute(u'title'): |
|
132 name = forum_elt.getAttribute(u'name') |
|
133 if name: |
|
134 forum_elt[u'title'] = name |
|
135 else: |
|
136 raise ValueError(_(u"forum need a title or a name")) |
|
137 if not forum_elt.getAttribute(u'uri') and not forum_elt.children: |
|
138 raise ValueError(_(u"forum need uri or sub-forums")) |
|
139 defer.returnValue(forums_elt) |
|
140 |
|
141 def _parseForums(self, parent_elt=None, forums=None): |
|
142 """recursivly parse a <forums> elements and return corresponding forums data |
|
143 |
|
144 @param item(domish.Element): item with <forums> element |
|
145 @param parent_elt(domish.Element, None): element to parse |
|
146 @return (list): parsed data |
|
147 @raise ValueError: item is invalid |
|
148 """ |
|
149 if parent_elt.name == u'item': |
|
150 forums = [] |
|
151 try: |
|
152 forums_elt = next(parent_elt.elements(NS_FORUMS, u'forums')) |
|
153 except StopIteration: |
|
154 raise ValueError(_(u"missing <forums> element")) |
|
155 else: |
|
156 forums_elt = parent_elt |
|
157 if forums is None: |
|
158 raise exceptions.InternalError(u'expected forums') |
|
159 if forums_elt.name != 'forums': |
|
160 raise ValueError(_(u'Unexpected element: {xml}').format(xml=forums_elt.toXml())) |
|
161 for forum_elt in forums_elt.elements(): |
|
162 if forum_elt.name == 'forum': |
|
163 data = {} |
|
164 for attrib in FORUM_ATTR.intersection(forum_elt.attributes): |
|
165 data[attrib] = forum_elt[attrib] |
|
166 unknown = set(forum_elt.attributes).difference(FORUM_ATTR) |
|
167 if unknown: |
|
168 log.warning(_(u"Following attributes are unknown: {unknown}").format(unknown=unknown)) |
|
169 for elt in forum_elt.elements(): |
|
170 if elt.name in FORUM_SUB_ELTS: |
|
171 data[elt.name] = unicode(elt) |
|
172 elif elt.name == u'forums': |
|
173 sub_forums = data[u'sub-forums'] = [] |
|
174 self._parseForums(elt, sub_forums) |
|
175 if not u'title' in data or not {u'uri', u'sub-forums'}.intersection(data): |
|
176 log.warning(_(u"invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml())) |
|
177 else: |
|
178 forums.append(data) |
|
179 else: |
|
180 log.warning(_(u"unkown forums sub element: {xml}").format(xml=forum_elt)) |
|
181 |
|
182 return forums |
|
183 |
|
184 def _get(self, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE): |
|
185 client = self.host.getClient(profile_key) |
|
186 if service.strip(): |
|
187 service = jid.JID(service) |
|
188 else: |
|
189 service = None |
|
190 if not node.strip(): |
|
191 node = None |
|
192 d=self.get(client, service, node, forums_key or None) |
|
193 d.addCallback(lambda data: json.dumps(data)) |
|
194 return d |
|
195 |
|
196 @defer.inlineCallbacks |
|
197 def get(self, client, service=None, node=None, forums_key=None): |
|
198 if service is None: |
|
199 service = client.pubsub_service |
|
200 if node is None: |
|
201 node = NS_FORUMS |
|
202 if forums_key is None: |
|
203 forums_key = u'default' |
|
204 items_data = yield self._p.getItems(client, service, node, item_ids=[forums_key]) |
|
205 item = items_data[0][0] |
|
206 # we have the item and need to convert it to json |
|
207 forums = self._parseForums(item) |
|
208 defer.returnValue(forums) |
|
209 |
|
210 def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE): |
|
211 client = self.host.getClient(profile_key) |
|
212 forums = json.loads(forums) |
|
213 if service.strip(): |
|
214 service = jid.JID(service) |
|
215 else: |
|
216 service = None |
|
217 if not node.strip(): |
|
218 node = None |
|
219 return self.set(client, forums, service, node, forums_key or None) |
|
220 |
|
221 @defer.inlineCallbacks |
|
222 def set(self, client, forums, service=None, node=None, forums_key=None): |
|
223 """create or replace forums structure |
|
224 |
|
225 @param forums(list): list of dictionary as follow: |
|
226 a dictionary represent a forum metadata, with the following keys: |
|
227 - title: title of the forum |
|
228 - name: short name (unique in those forums) for the forum |
|
229 - main-language: main language to be use in the forums |
|
230 - uri: XMPP uri to the microblog node hosting the forum |
|
231 - short-desc: short description of the forum (in main-language) |
|
232 - desc: long description of the forum (in main-language) |
|
233 - sub-forums: a list of sub-forums with the same structure |
|
234 title or name is needed, and uri or sub-forums |
|
235 @param forums_key(unicode, None): key (i.e. item id) of the forums |
|
236 may be used to store different forums structures for different languages |
|
237 None to use "default" |
|
238 """ |
|
239 if service is None: |
|
240 service = client.pubsub_service |
|
241 if node is None: |
|
242 node = NS_FORUMS |
|
243 if forums_key is None: |
|
244 forums_key = u'default' |
|
245 forums_elt = yield self._createForums(client, forums, service, node) |
|
246 yield self._p.sendItem(client, service, node, forums_elt, item_id=forums_key) |
|
247 |
|
248 def _getTopics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE): |
|
249 client = self.host.getClient(profile_key) |
|
250 extra = self._p.parseExtra(extra) |
|
251 d = self.getTopics(client, jid.JID(service), node, rsm_request=extra.rsm_request, extra=extra.extra) |
|
252 d.addCallback(lambda(topics, metadata): (topics, {k: unicode(v) for k,v in metadata.iteritems()})) |
|
253 return d |
|
254 |
|
255 @defer.inlineCallbacks |
|
256 def getTopics(self, client, service, node, rsm_request=None, extra=None): |
|
257 """retrieve topics data |
|
258 |
|
259 Topics are simple microblog URIs with some metadata duplicated from first post |
|
260 """ |
|
261 topics_data = yield self._p.getItems(client, service, node, rsm_request=rsm_request, extra=extra) |
|
262 topics = [] |
|
263 item_elts, metadata = topics_data |
|
264 for item_elt in item_elts: |
|
265 topic_elt = next(item_elt.elements(NS_FORUMS, u'topic')) |
|
266 title_elt = next(topic_elt.elements(NS_FORUMS, u'title')) |
|
267 topic = {u'uri': topic_elt[u'uri'], |
|
268 u'author': topic_elt[u'author'], |
|
269 u'title': unicode(title_elt)} |
|
270 topics.append(topic) |
|
271 defer.returnValue((topics, metadata)) |
|
272 |
|
273 def _createTopic(self, service, node, mb_data, profile_key): |
|
274 client = self.host.getClient(profile_key) |
|
275 return self.createTopic(client, jid.JID(service), node, mb_data) |
|
276 |
|
277 @defer.inlineCallbacks |
|
278 def createTopic(self, client, service, node, mb_data): |
|
279 try: |
|
280 title = mb_data[u'title'] |
|
281 if not u'content' in mb_data: |
|
282 raise KeyError(u'content') |
|
283 except KeyError as e: |
|
284 raise exceptions.DataError(u"missing mandatory data: {key}".format(key=e.args[0])) |
|
285 |
|
286 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) |
|
287 yield self._p.createNode(client, service, topic_node, self._node_options) |
|
288 self._m.send(client, mb_data, service, topic_node) |
|
289 topic_uri = uri.buildXMPPUri(u'pubsub', |
|
290 subtype=u'microblog', |
|
291 path=service.full(), |
|
292 node=topic_node) |
|
293 topic_elt = domish.Element((NS_FORUMS, 'topic')) |
|
294 topic_elt[u'uri'] = topic_uri |
|
295 topic_elt[u'author'] = client.jid.userhost() |
|
296 topic_elt.addElement(u'title', content = title) |
|
297 yield self._p.sendItem(client, service, node, topic_elt) |