Mercurial > libervia-backend
annotate sat/plugins/plugin_misc_forums.py @ 3317:83f25da66bec
core (memory): files are now public by default except for those in root directory:
using public permissions by default makes permissions change more easy as we don't have to
recursively change all permissions of sub files when changing directory. Root directories
are private by default.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 01 Aug 2020 16:02:41 +0200 |
parents | 559a625a236b |
children | be6d91572633 |
rev | line source |
---|---|
3028 | 1 #!/usr/bin/env python3 |
3137 | 2 |
2484 | 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 |
3136 | 5 # Copyright (C) 2009-2020 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 | |
3028 | 32 NS_FORUMS = 'org.salut-a-toi.forums:0' |
33 NS_FORUMS_TOPICS = NS_FORUMS + '#topics' | |
2484 | 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 } | |
3028 | 45 FORUM_ATTR = {'title', 'name', 'main-language', 'uri'} |
46 FORUM_SUB_ELTS = ('short-desc', 'desc') | |
47 FORUM_TOPICS_NODE_TPL = '{node}#topics_{uuid}' | |
48 FORUM_TOPIC_NODE_TPL = '{node}_{uuid}' | |
2484 | 49 |
50 | |
51 class forums(object): | |
52 | |
53 def __init__(self, host): | |
3028 | 54 log.info(_("forums plugin initialization")) |
2484 | 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, | |
3028 | 70 async_=True) |
2484 | 71 host.bridge.addMethod("forumsSet", ".plugin", |
72 in_sign='sssss', out_sign='', | |
73 method=self._set, | |
3028 | 74 async_=True) |
2484 | 75 host.bridge.addMethod("forumTopicsGet", ".plugin", |
76 in_sign='ssa{ss}s', out_sign='(aa{ss}a{ss})', | |
77 method=self._getTopics, | |
3028 | 78 async_=True) |
2484 | 79 host.bridge.addMethod("forumTopicCreate", ".plugin", |
80 in_sign='ssa{ss}s', out_sign='', | |
81 method=self._createTopic, | |
3028 | 82 async_=True) |
2484 | 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): | |
3028 | 97 raise ValueError(_("forums arguments must be a list of forums")) |
2484 | 98 if forums_elt is None: |
3028 | 99 forums_elt = domish.Element((NS_FORUMS, 'forums')) |
2484 | 100 assert names is None |
101 names = set() | |
102 else: | |
3028 | 103 if names is None or forums_elt.name != 'forums': |
104 raise exceptions.InternalError('invalid forums or names') | |
2484 | 105 assert names is not None |
106 | |
107 for forum in forums: | |
108 if not isinstance(forum, dict): | |
3028 | 109 raise ValueError(_("A forum item must be a dictionary")) |
2484 | 110 forum_elt = forums_elt.addElement('forum') |
111 | |
3028 | 112 for key, value in forum.items(): |
113 if key == 'name' and key in names: | |
114 raise exceptions.ConflictError(_("following forum name is not unique: {name}").format(name=key)) | |
115 if key == 'uri' and not value.strip(): | |
116 log.info(_("creating missing forum node")) | |
2484 | 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) | |
3028 | 119 value = uri.buildXMPPUri('pubsub', |
2484 | 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) | |
3028 | 126 elif key == 'sub-forums': |
127 sub_forums_elt = forum_elt.addElement('forums') | |
2484 | 128 yield self._createForums(client, value, service, node, sub_forums_elt, names=names) |
129 else: | |
3028 | 130 log.warning(_("Unknown forum attribute: {key}").format(key=key)) |
131 if not forum_elt.getAttribute('title'): | |
132 name = forum_elt.getAttribute('name') | |
2484 | 133 if name: |
3028 | 134 forum_elt['title'] = name |
2484 | 135 else: |
3028 | 136 raise ValueError(_("forum need a title or a name")) |
137 if not forum_elt.getAttribute('uri') and not forum_elt.children: | |
138 raise ValueError(_("forum need uri or sub-forums")) | |
2484 | 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 """ | |
3028 | 149 if parent_elt.name == 'item': |
2484 | 150 forums = [] |
151 try: | |
3028 | 152 forums_elt = next(parent_elt.elements(NS_FORUMS, 'forums')) |
2484 | 153 except StopIteration: |
3028 | 154 raise ValueError(_("missing <forums> element")) |
2484 | 155 else: |
156 forums_elt = parent_elt | |
157 if forums is None: | |
3028 | 158 raise exceptions.InternalError('expected forums') |
2484 | 159 if forums_elt.name != 'forums': |
3028 | 160 raise ValueError(_('Unexpected element: {xml}').format(xml=forums_elt.toXml())) |
2484 | 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: | |
3028 | 168 log.warning(_("Following attributes are unknown: {unknown}").format(unknown=unknown)) |
2484 | 169 for elt in forum_elt.elements(): |
170 if elt.name in FORUM_SUB_ELTS: | |
3028 | 171 data[elt.name] = str(elt) |
172 elif elt.name == 'forums': | |
173 sub_forums = data['sub-forums'] = [] | |
2484 | 174 self._parseForums(elt, sub_forums) |
3028 | 175 if not 'title' in data or not {'uri', 'sub-forums'}.intersection(data): |
176 log.warning(_("invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml())) | |
2484 | 177 else: |
178 forums.append(data) | |
179 else: | |
3028 | 180 log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt)) |
2484 | 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: | |
3028 | 203 forums_key = 'default' |
2484 | 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: | |
3028 | 244 forums_key = 'default' |
2484 | 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) | |
3028 | 252 d.addCallback(lambda topics_metadata: (topics_metadata[0], {k: str(v) for k,v in topics_metadata[1].items()})) |
2484 | 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: | |
3028 | 265 topic_elt = next(item_elt.elements(NS_FORUMS, 'topic')) |
266 title_elt = next(topic_elt.elements(NS_FORUMS, 'title')) | |
267 topic = {'uri': topic_elt['uri'], | |
268 'author': topic_elt['author'], | |
269 'title': str(title_elt)} | |
2484 | 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: | |
3028 | 280 title = mb_data['title'] |
281 if not 'content' in mb_data: | |
282 raise KeyError('content') | |
2484 | 283 except KeyError as e: |
3028 | 284 raise exceptions.DataError("missing mandatory data: {key}".format(key=e.args[0])) |
2484 | 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) | |
3028 | 289 topic_uri = uri.buildXMPPUri('pubsub', |
290 subtype='microblog', | |
2484 | 291 path=service.full(), |
292 node=topic_node) | |
293 topic_elt = domish.Element((NS_FORUMS, 'topic')) | |
3028 | 294 topic_elt['uri'] = topic_uri |
295 topic_elt['author'] = client.jid.userhost() | |
296 topic_elt.addElement('title', content = title) | |
2484 | 297 yield self._p.sendItem(client, service, node, topic_elt) |