Mercurial > libervia-backend
annotate sat/plugins/plugin_misc_forums.py @ 2970:9a47a8caa280
core (constants): fixed Flatpak config path:
trailing "/" was missing in flatpak config path, resulting in the configuration not being read properly on this platform.
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 27 Jun 2019 20:26:00 +0200 |
parents | 989b622faff6 |
children | ab2696e34d29 |
rev | line source |
---|---|
2484 | 1 #!/usr/bin/env python2 |
2 # -*- coding: utf-8 -*- | |
3 | |
2959
989b622faff6
plugins schema, tickets, merge_requests: use serialised data for extra dict + some cosmetic changes
Goffi <goffi@goffi.org>
parents:
2771
diff
changeset
|
4 # SAT plugin for pubsub forums |
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): | |
2959
989b622faff6
plugins schema, tickets, merge_requests: use serialised data for extra dict + some cosmetic changes
Goffi <goffi@goffi.org>
parents:
2771
diff
changeset
|
86 """Recursively create <forums> element(s) |
2484 | 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): | |
2959
989b622faff6
plugins schema, tickets, merge_requests: use serialised data for extra dict + some cosmetic changes
Goffi <goffi@goffi.org>
parents:
2771
diff
changeset
|
142 """Recursivly parse a <forums> elements and return corresponding forums data |
2484 | 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): | |
2959
989b622faff6
plugins schema, tickets, merge_requests: use serialised data for extra dict + some cosmetic changes
Goffi <goffi@goffi.org>
parents:
2771
diff
changeset
|
223 """Create or replace forums structure |
2484 | 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): | |
2959
989b622faff6
plugins schema, tickets, merge_requests: use serialised data for extra dict + some cosmetic changes
Goffi <goffi@goffi.org>
parents:
2771
diff
changeset
|
257 """Retrieve topics data |
2484 | 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) |