Mercurial > libervia-backend
annotate sat/plugins/plugin_misc_forums.py @ 3652:6e34307319c0
plugin XEP-0353: fix jingle initiation on disco "Service Unavailable" error:
When requesting disco info on a bare jid which is not in our roster, server may return
"Service Unavailable" (to avoid leaking valid JIDs). In this case, the initiation was
failing, this is now fixed by using empty categories in this case.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 08 Sep 2021 11:16:52 +0200 |
parents | 3fd60beb9b92 |
children | edc79cefe968 |
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 |
3479 | 5 # Copyright (C) 2009-2021 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 | |
3549
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
24 from sat.tools.common import uri, data_format |
2484 | 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", |
3549
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
76 in_sign='ssa{ss}s', out_sign='(aa{ss}s)', |
2484 | 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) | |
3549
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
252 d.addCallback( |
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
253 lambda topics_data: (topics_data[0], data_format.serialise(topics_data[1])) |
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
254 ) |
2484 | 255 return d |
256 | |
257 @defer.inlineCallbacks | |
258 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
|
259 """Retrieve topics data |
2484 | 260 |
261 Topics are simple microblog URIs with some metadata duplicated from first post | |
262 """ | |
263 topics_data = yield self._p.getItems(client, service, node, rsm_request=rsm_request, extra=extra) | |
264 topics = [] | |
265 item_elts, metadata = topics_data | |
266 for item_elt in item_elts: | |
3028 | 267 topic_elt = next(item_elt.elements(NS_FORUMS, 'topic')) |
268 title_elt = next(topic_elt.elements(NS_FORUMS, 'title')) | |
269 topic = {'uri': topic_elt['uri'], | |
270 'author': topic_elt['author'], | |
271 'title': str(title_elt)} | |
2484 | 272 topics.append(topic) |
273 defer.returnValue((topics, metadata)) | |
274 | |
275 def _createTopic(self, service, node, mb_data, profile_key): | |
276 client = self.host.getClient(profile_key) | |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
277 return defer.ensureDeferred( |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
278 self.createTopic(client, jid.JID(service), node, mb_data) |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
279 ) |
2484 | 280 |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
281 async def createTopic(self, client, service, node, mb_data): |
2484 | 282 try: |
3028 | 283 title = mb_data['title'] |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
284 content = mb_data.pop('content') |
2484 | 285 except KeyError as e: |
3028 | 286 raise exceptions.DataError("missing mandatory data: {key}".format(key=e.args[0])) |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
287 else: |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
288 mb_data["content_rich"] = content |
2484 | 289 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
290 await self._p.createNode(client, service, topic_node, self._node_options) |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
291 await self._m.send(client, mb_data, service, topic_node) |
3028 | 292 topic_uri = uri.buildXMPPUri('pubsub', |
293 subtype='microblog', | |
2484 | 294 path=service.full(), |
295 node=topic_node) | |
296 topic_elt = domish.Element((NS_FORUMS, 'topic')) | |
3028 | 297 topic_elt['uri'] = topic_uri |
298 topic_elt['author'] = client.jid.userhost() | |
299 topic_elt.addElement('title', content = title) | |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
300 await self._p.sendItem(client, service, node, topic_elt) |