Mercurial > libervia-backend
annotate sat/plugins/plugin_misc_forums.py @ 3942:a92eef737703
plugin XEP-0373: download public keys if they are not found in local storage:
public keys were only obtained from PEP notifications, however this wasn't working if the
entity was not in our roster.
Now if no public key is retrieved from local storage, the public key node is requested,
and an error is raised if nothing is found. This allows the use of OX with entities which
are not in roster.
rel 380
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 15 Oct 2022 20:38:33 +0200 |
parents | 6c5f0fbc519b |
children | 524856bd7b19 |
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_DELIVER_PAYLOADS: 1, | |
62 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, | |
63 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, | |
64 } | |
65 host.registerNamespace('forums', NS_FORUMS) | |
66 host.bridge.addMethod("forumsGet", ".plugin", | |
67 in_sign='ssss', out_sign='s', | |
68 method=self._get, | |
3028 | 69 async_=True) |
2484 | 70 host.bridge.addMethod("forumsSet", ".plugin", |
71 in_sign='sssss', out_sign='', | |
72 method=self._set, | |
3028 | 73 async_=True) |
2484 | 74 host.bridge.addMethod("forumTopicsGet", ".plugin", |
3549
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
75 in_sign='ssa{ss}s', out_sign='(aa{ss}s)', |
2484 | 76 method=self._getTopics, |
3028 | 77 async_=True) |
2484 | 78 host.bridge.addMethod("forumTopicCreate", ".plugin", |
79 in_sign='ssa{ss}s', out_sign='', | |
80 method=self._createTopic, | |
3028 | 81 async_=True) |
2484 | 82 |
83 @defer.inlineCallbacks | |
84 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
|
85 """Recursively create <forums> element(s) |
2484 | 86 |
87 @param forums(list): forums which may have subforums | |
88 @param service(jid.JID): service where the new nodes will be created | |
89 @param node(unicode): node of the forums | |
90 will be used as basis for the newly created nodes | |
91 @param parent_elt(domish.Element, None): element where the forum must be added | |
92 if None, the root <forums> element will be created | |
93 @return (domish.Element): created forums | |
94 """ | |
95 if not isinstance(forums, list): | |
3028 | 96 raise ValueError(_("forums arguments must be a list of forums")) |
2484 | 97 if forums_elt is None: |
3028 | 98 forums_elt = domish.Element((NS_FORUMS, 'forums')) |
2484 | 99 assert names is None |
100 names = set() | |
101 else: | |
3028 | 102 if names is None or forums_elt.name != 'forums': |
103 raise exceptions.InternalError('invalid forums or names') | |
2484 | 104 assert names is not None |
105 | |
106 for forum in forums: | |
107 if not isinstance(forum, dict): | |
3028 | 108 raise ValueError(_("A forum item must be a dictionary")) |
2484 | 109 forum_elt = forums_elt.addElement('forum') |
110 | |
3028 | 111 for key, value in forum.items(): |
112 if key == 'name' and key in names: | |
113 raise exceptions.ConflictError(_("following forum name is not unique: {name}").format(name=key)) | |
114 if key == 'uri' and not value.strip(): | |
115 log.info(_("creating missing forum node")) | |
2484 | 116 forum_node = FORUM_TOPICS_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) |
117 yield self._p.createNode(client, service, forum_node, self._node_options) | |
3028 | 118 value = uri.buildXMPPUri('pubsub', |
2484 | 119 path=service.full(), |
120 node=forum_node) | |
121 if key in FORUM_ATTR: | |
122 forum_elt[key] = value.strip() | |
123 elif key in FORUM_SUB_ELTS: | |
124 forum_elt.addElement(key, content=value) | |
3028 | 125 elif key == 'sub-forums': |
126 sub_forums_elt = forum_elt.addElement('forums') | |
2484 | 127 yield self._createForums(client, value, service, node, sub_forums_elt, names=names) |
128 else: | |
3028 | 129 log.warning(_("Unknown forum attribute: {key}").format(key=key)) |
130 if not forum_elt.getAttribute('title'): | |
131 name = forum_elt.getAttribute('name') | |
2484 | 132 if name: |
3028 | 133 forum_elt['title'] = name |
2484 | 134 else: |
3028 | 135 raise ValueError(_("forum need a title or a name")) |
136 if not forum_elt.getAttribute('uri') and not forum_elt.children: | |
137 raise ValueError(_("forum need uri or sub-forums")) | |
2484 | 138 defer.returnValue(forums_elt) |
139 | |
140 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
|
141 """Recursivly parse a <forums> elements and return corresponding forums data |
2484 | 142 |
143 @param item(domish.Element): item with <forums> element | |
144 @param parent_elt(domish.Element, None): element to parse | |
145 @return (list): parsed data | |
146 @raise ValueError: item is invalid | |
147 """ | |
3028 | 148 if parent_elt.name == 'item': |
2484 | 149 forums = [] |
150 try: | |
3028 | 151 forums_elt = next(parent_elt.elements(NS_FORUMS, 'forums')) |
2484 | 152 except StopIteration: |
3028 | 153 raise ValueError(_("missing <forums> element")) |
2484 | 154 else: |
155 forums_elt = parent_elt | |
156 if forums is None: | |
3028 | 157 raise exceptions.InternalError('expected forums') |
2484 | 158 if forums_elt.name != 'forums': |
3028 | 159 raise ValueError(_('Unexpected element: {xml}').format(xml=forums_elt.toXml())) |
2484 | 160 for forum_elt in forums_elt.elements(): |
161 if forum_elt.name == 'forum': | |
162 data = {} | |
163 for attrib in FORUM_ATTR.intersection(forum_elt.attributes): | |
164 data[attrib] = forum_elt[attrib] | |
165 unknown = set(forum_elt.attributes).difference(FORUM_ATTR) | |
166 if unknown: | |
3028 | 167 log.warning(_("Following attributes are unknown: {unknown}").format(unknown=unknown)) |
2484 | 168 for elt in forum_elt.elements(): |
169 if elt.name in FORUM_SUB_ELTS: | |
3028 | 170 data[elt.name] = str(elt) |
171 elif elt.name == 'forums': | |
172 sub_forums = data['sub-forums'] = [] | |
2484 | 173 self._parseForums(elt, sub_forums) |
3028 | 174 if not 'title' in data or not {'uri', 'sub-forums'}.intersection(data): |
175 log.warning(_("invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml())) | |
2484 | 176 else: |
177 forums.append(data) | |
178 else: | |
3028 | 179 log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt)) |
2484 | 180 |
181 return forums | |
182 | |
183 def _get(self, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE): | |
184 client = self.host.getClient(profile_key) | |
185 if service.strip(): | |
186 service = jid.JID(service) | |
187 else: | |
188 service = None | |
189 if not node.strip(): | |
190 node = None | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
191 d = defer.ensureDeferred(self.get(client, service, node, forums_key or None)) |
2484 | 192 d.addCallback(lambda data: json.dumps(data)) |
193 return d | |
194 | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
195 async def get(self, client, service=None, node=None, forums_key=None): |
2484 | 196 if service is None: |
197 service = client.pubsub_service | |
198 if node is None: | |
199 node = NS_FORUMS | |
200 if forums_key is None: | |
3028 | 201 forums_key = 'default' |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
202 items_data = await self._p.getItems(client, service, node, item_ids=[forums_key]) |
2484 | 203 item = items_data[0][0] |
204 # we have the item and need to convert it to json | |
205 forums = self._parseForums(item) | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
206 return forums |
2484 | 207 |
208 def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE): | |
209 client = self.host.getClient(profile_key) | |
210 forums = json.loads(forums) | |
211 if service.strip(): | |
212 service = jid.JID(service) | |
213 else: | |
214 service = None | |
215 if not node.strip(): | |
216 node = None | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
217 return defer.ensureDeferred( |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
218 self.set(client, forums, service, node, forums_key or None) |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
219 ) |
2484 | 220 |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
221 async 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
|
222 """Create or replace forums structure |
2484 | 223 |
224 @param forums(list): list of dictionary as follow: | |
225 a dictionary represent a forum metadata, with the following keys: | |
226 - title: title of the forum | |
227 - name: short name (unique in those forums) for the forum | |
228 - main-language: main language to be use in the forums | |
229 - uri: XMPP uri to the microblog node hosting the forum | |
230 - short-desc: short description of the forum (in main-language) | |
231 - desc: long description of the forum (in main-language) | |
232 - sub-forums: a list of sub-forums with the same structure | |
233 title or name is needed, and uri or sub-forums | |
234 @param forums_key(unicode, None): key (i.e. item id) of the forums | |
235 may be used to store different forums structures for different languages | |
236 None to use "default" | |
237 """ | |
238 if service is None: | |
239 service = client.pubsub_service | |
240 if node is None: | |
241 node = NS_FORUMS | |
242 if forums_key is None: | |
3028 | 243 forums_key = 'default' |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
244 forums_elt = await self._createForums(client, forums, service, node) |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
245 return await self._p.sendItem( |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
246 client, service, node, forums_elt, item_id=forums_key |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
247 ) |
2484 | 248 |
249 def _getTopics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE): | |
250 client = self.host.getClient(profile_key) | |
251 extra = self._p.parseExtra(extra) | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
252 d = defer.ensureDeferred( |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
253 self.getTopics( |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
254 client, jid.JID(service), node, rsm_request=extra.rsm_request, |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
255 extra=extra.extra |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
256 ) |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
257 ) |
3549
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
258 d.addCallback( |
3fd60beb9b92
plugin forums: use serialised data for extra in forumTopicsGet
Goffi <goffi@goffi.org>
parents:
3515
diff
changeset
|
259 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
|
260 ) |
2484 | 261 return d |
262 | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
263 async 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
|
264 """Retrieve topics data |
2484 | 265 |
266 Topics are simple microblog URIs with some metadata duplicated from first post | |
267 """ | |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
268 topics_data = await self._p.getItems( |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
269 client, service, node, rsm_request=rsm_request, extra=extra |
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
270 ) |
2484 | 271 topics = [] |
272 item_elts, metadata = topics_data | |
273 for item_elt in item_elts: | |
3028 | 274 topic_elt = next(item_elt.elements(NS_FORUMS, 'topic')) |
275 title_elt = next(topic_elt.elements(NS_FORUMS, 'title')) | |
276 topic = {'uri': topic_elt['uri'], | |
277 'author': topic_elt['author'], | |
278 'title': str(title_elt)} | |
2484 | 279 topics.append(topic) |
3584
edc79cefe968
plugin XEP-0060: `getItem(s)`, `publish` and `(un)subscribe` are now coroutines
Goffi <goffi@goffi.org>
parents:
3549
diff
changeset
|
280 return (topics, metadata) |
2484 | 281 |
282 def _createTopic(self, service, node, mb_data, profile_key): | |
283 client = self.host.getClient(profile_key) | |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
284 return defer.ensureDeferred( |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
285 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
|
286 ) |
2484 | 287 |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
288 async def createTopic(self, client, service, node, mb_data): |
2484 | 289 try: |
3028 | 290 title = mb_data['title'] |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
291 content = mb_data.pop('content') |
2484 | 292 except KeyError as e: |
3028 | 293 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
|
294 else: |
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
295 mb_data["content_rich"] = content |
2484 | 296 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
|
297 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
|
298 await self._m.send(client, mb_data, service, topic_node) |
3028 | 299 topic_uri = uri.buildXMPPUri('pubsub', |
300 subtype='microblog', | |
2484 | 301 path=service.full(), |
302 node=topic_node) | |
303 topic_elt = domish.Element((NS_FORUMS, 'topic')) | |
3028 | 304 topic_elt['uri'] = topic_uri |
305 topic_elt['author'] = client.jid.userhost() | |
306 topic_elt.addElement('title', content = title) | |
3515
2dce411c2647
plugin misc forums: use rich content in createTopic
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
307 await self._p.sendItem(client, service, node, topic_elt) |