Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_misc_forums.py @ 4192:1d24ff583794
plugin forums: parsing fix + formatting:
- reformatted with black
- fix parsing of forums
- minor improvements
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 12 Dec 2023 12:17:15 +0100 |
parents | 5d056d524298 |
children |
comparison
equal
deleted
inserted
replaced
4191:5d056d524298 | 4192:1d24ff583794 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 | 3 # Libervia plugin for pubsub forums |
4 # SAT plugin for pubsub forums | 4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
6 | 5 |
7 # This program is free software: you can redistribute it and/or modify | 6 # 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 | 7 # 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 | 8 # the Free Software Foundation, either version 3 of the License, or |
10 # (at your option) any later version. | 9 # (at your option) any later version. |
27 from twisted.words.protocols.jabber import jid | 26 from twisted.words.protocols.jabber import jid |
28 from twisted.words.xish import domish | 27 from twisted.words.xish import domish |
29 from twisted.internet import defer | 28 from twisted.internet import defer |
30 import shortuuid | 29 import shortuuid |
31 import json | 30 import json |
31 | |
32 log = getLogger(__name__) | 32 log = getLogger(__name__) |
33 | 33 |
34 NS_FORUMS = 'org.salut-a-toi.forums:0' | 34 NS_FORUMS = "org.salut-a-toi.forums:0" |
35 NS_FORUMS_TOPICS = NS_FORUMS + '#topics' | 35 NS_FORUMS_TOPICS = NS_FORUMS + "#topics" |
36 | 36 |
37 PLUGIN_INFO = { | 37 PLUGIN_INFO = { |
38 C.PI_NAME: _("forums management"), | 38 C.PI_NAME: _("forums management"), |
39 C.PI_IMPORT_NAME: "forums", | 39 C.PI_IMPORT_NAME: "forums", |
40 C.PI_TYPE: "EXP", | 40 C.PI_TYPE: "EXP", |
41 C.PI_PROTOCOLS: [], | 41 C.PI_PROTOCOLS: [], |
42 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0277"], | 42 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0277"], |
43 C.PI_MAIN: "forums", | 43 C.PI_MAIN: "forums", |
44 C.PI_HANDLER: "no", | 44 C.PI_HANDLER: "no", |
45 C.PI_DESCRIPTION: _("""forums management plugin""") | 45 C.PI_DESCRIPTION: _("""forums management plugin"""), |
46 } | 46 } |
47 FORUM_ATTR = {'title', 'name', 'main-language', 'uri'} | 47 FORUM_ATTR = {"title", "name", "main-language", "uri"} |
48 FORUM_SUB_ELTS = ('short-desc', 'desc') | 48 FORUM_SUB_ELTS = ("short-desc", "desc") |
49 FORUM_TOPICS_NODE_TPL = '{node}#topics_{uuid}' | 49 FORUM_TOPICS_NODE_TPL = "{node}#topics_{uuid}" |
50 FORUM_TOPIC_NODE_TPL = '{node}_{uuid}' | 50 FORUM_TOPIC_NODE_TPL = "{node}_{uuid}" |
51 | 51 |
52 | 52 |
53 class forums(object): | 53 class forums: |
54 | |
55 def __init__(self, host): | 54 def __init__(self, host): |
56 log.info(_("forums plugin initialization")) | 55 log.info(_("forums plugin initialization")) |
57 self.host = host | 56 self.host = host |
58 self._m = self.host.plugins['XEP-0277'] | 57 self._m = self.host.plugins["XEP-0277"] |
59 self._p = self.host.plugins['XEP-0060'] | 58 self._p = self.host.plugins["XEP-0060"] |
60 self._node_options = { | 59 self._node_options = { |
61 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, | 60 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, |
62 self._p.OPT_PERSIST_ITEMS: 1, | 61 self._p.OPT_PERSIST_ITEMS: 1, |
63 self._p.OPT_DELIVER_PAYLOADS: 1, | 62 self._p.OPT_DELIVER_PAYLOADS: 1, |
64 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, | 63 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, |
65 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, | 64 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, |
66 } | 65 } |
67 host.register_namespace('forums', NS_FORUMS) | 66 host.register_namespace("forums", NS_FORUMS) |
68 host.bridge.add_method("forums_get", ".plugin", | 67 host.bridge.add_method( |
69 in_sign='ssss', out_sign='s', | 68 "forums_get", |
70 method=self._get, | 69 ".plugin", |
71 async_=True) | 70 in_sign="ssss", |
72 host.bridge.add_method("forums_set", ".plugin", | 71 out_sign="s", |
73 in_sign='sssss', out_sign='', | 72 method=self._get, |
74 method=self._set, | 73 async_=True, |
75 async_=True) | 74 ) |
76 host.bridge.add_method("forum_topics_get", ".plugin", | 75 host.bridge.add_method( |
77 in_sign='ssa{ss}s', out_sign='(aa{ss}s)', | 76 "forums_set", |
78 method=self._get_topics, | 77 ".plugin", |
79 async_=True) | 78 in_sign="sssss", |
80 host.bridge.add_method("forum_topic_create", ".plugin", | 79 out_sign="", |
81 in_sign='ssa{ss}s', out_sign='', | 80 method=self._set, |
82 method=self._create_topic, | 81 async_=True, |
83 async_=True) | 82 ) |
83 host.bridge.add_method( | |
84 "forum_topics_get", | |
85 ".plugin", | |
86 in_sign="ssa{ss}s", | |
87 out_sign="(aa{ss}s)", | |
88 method=self._get_topics, | |
89 async_=True, | |
90 ) | |
91 host.bridge.add_method( | |
92 "forum_topic_create", | |
93 ".plugin", | |
94 in_sign="ssa{ss}s", | |
95 out_sign="", | |
96 method=self._create_topic, | |
97 async_=True, | |
98 ) | |
84 | 99 |
85 async def _create_forums( | 100 async def _create_forums( |
86 self, | 101 self, |
87 client: SatXMPPEntity, | 102 client: SatXMPPEntity, |
88 forums: list[dict], | 103 forums: list[dict], |
89 service: jid.JID, | 104 service: jid.JID, |
90 node: str, | 105 node: str, |
91 forums_elt: domish.Element|None = None, | 106 forums_elt: domish.Element | None = None, |
92 names: Iterable = None | 107 names: Iterable = None, |
93 ) -> domish.Element: | 108 ) -> domish.Element: |
94 """Recursively create <forums> element(s) | 109 """Recursively create <forums> element(s) |
95 | 110 |
96 @param forums(list): forums which may have subforums | 111 @param forums(list): forums which may have subforums |
97 @param service(jid.JID): service where the new nodes will be created | 112 @param service(jid.JID): service where the new nodes will be created |
102 @return (domish.Element): created forums | 117 @return (domish.Element): created forums |
103 """ | 118 """ |
104 if not isinstance(forums, list): | 119 if not isinstance(forums, list): |
105 raise ValueError(_("forums arguments must be a list of forums")) | 120 raise ValueError(_("forums arguments must be a list of forums")) |
106 if forums_elt is None: | 121 if forums_elt is None: |
107 forums_elt = domish.Element((NS_FORUMS, 'forums')) | 122 forums_elt = domish.Element((NS_FORUMS, "forums")) |
108 assert names is None | 123 assert names is None |
109 names = set() | 124 names = set() |
110 else: | 125 else: |
111 if names is None or forums_elt.name != 'forums': | 126 if names is None or forums_elt.name != "forums": |
112 raise exceptions.InternalError('invalid forums or names') | 127 raise exceptions.InternalError("invalid forums or names") |
113 assert names is not None | 128 assert names is not None |
114 | 129 |
115 for forum in forums: | 130 for forum in forums: |
116 if not isinstance(forum, dict): | 131 if not isinstance(forum, dict): |
117 raise ValueError(_("A forum item must be a dictionary")) | 132 raise ValueError(_("A forum item must be a dictionary")) |
118 forum_elt = forums_elt.addElement('forum') | 133 forum_elt = forums_elt.addElement("forum") |
119 | 134 |
120 for key, value in forum.items(): | 135 for key, value in forum.items(): |
121 if key == 'name' and key in names: | 136 if key == "name" and key in names: |
122 raise exceptions.ConflictError(_("following forum name is not unique: {name}").format(name=key)) | 137 raise exceptions.ConflictError( |
123 if key == 'uri' and value is None or not value.strip(): | 138 _("following forum name is not unique: {name}").format(name=key) |
139 ) | |
140 if key == "uri" and (value is None or not value.strip()): | |
124 log.info(_("creating missing forum node")) | 141 log.info(_("creating missing forum node")) |
125 forum_node = FORUM_TOPICS_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) | 142 forum_node = FORUM_TOPICS_NODE_TPL.format( |
126 await self._p.createNode(client, service, forum_node, self._node_options) | 143 node=node, uuid=shortuuid.uuid() |
127 value = uri.build_xmpp_uri('pubsub', | 144 ) |
128 path=service.full(), | 145 await self._p.createNode( |
129 node=forum_node) | 146 client, service, forum_node, self._node_options |
147 ) | |
148 value = uri.build_xmpp_uri( | |
149 "pubsub", path=service.full(), node=forum_node | |
150 ) | |
130 if key in FORUM_ATTR: | 151 if key in FORUM_ATTR: |
131 forum_elt[key] = value.strip() | 152 forum_elt[key] = value.strip() |
132 elif key in FORUM_SUB_ELTS: | 153 elif key in FORUM_SUB_ELTS: |
133 forum_elt.addElement(key, content=value) | 154 forum_elt.addElement(key, content=value) |
134 elif key == 'sub-forums': | 155 elif key == "sub-forums": |
135 assert isinstance(value, list) | 156 assert isinstance(value, list) |
136 sub_forums_elt = forum_elt.addElement('forums') | 157 sub_forums_elt = forum_elt.addElement("forums") |
137 await self._create_forums(client, value, service, node, sub_forums_elt, names=names) | 158 await self._create_forums( |
159 client, value, service, node, sub_forums_elt, names=names | |
160 ) | |
138 else: | 161 else: |
139 log.warning(_("Unknown forum attribute: {key}").format(key=key)) | 162 log.warning(_("Unknown forum attribute: {key}").format(key=key)) |
140 if not forum_elt.getAttribute('title'): | 163 if not forum_elt.getAttribute("title"): |
141 name = forum_elt.getAttribute('name') | 164 name = forum_elt.getAttribute("name") |
142 if name: | 165 if name: |
143 forum_elt['title'] = name | 166 forum_elt["title"] = name |
144 else: | 167 else: |
145 raise ValueError(_("forum need a title or a name")) | 168 raise ValueError(_("forum need a title or a name")) |
146 if not forum_elt.getAttribute('uri') and not forum_elt.children: | 169 if not forum_elt.getAttribute("uri") and not forum_elt.children: |
147 raise ValueError(_("forum need uri or sub-forums")) | 170 raise ValueError(_("forum need uri or sub-forums")) |
148 return forums_elt | 171 return forums_elt |
149 | 172 |
150 def _parse_forums(self, parent_elt=None, forums=None): | 173 def _parse_forums(self, parent_elt=None, forums=None): |
151 """Recursivly parse a <forums> elements and return corresponding forums data | 174 """Recursivly parse a <forums> elements and return corresponding forums data |
153 @param item(domish.Element): item with <forums> element | 176 @param item(domish.Element): item with <forums> element |
154 @param parent_elt(domish.Element, None): element to parse | 177 @param parent_elt(domish.Element, None): element to parse |
155 @return (list): parsed data | 178 @return (list): parsed data |
156 @raise ValueError: item is invalid | 179 @raise ValueError: item is invalid |
157 """ | 180 """ |
158 if parent_elt.name == 'item': | 181 if parent_elt.name == "item": |
159 forums = [] | 182 forums = [] |
160 try: | 183 try: |
161 forums_elt = next(parent_elt.elements(NS_FORUMS, 'forums')) | 184 forums_elt = next(parent_elt.elements(NS_FORUMS, "forums")) |
162 except StopIteration: | 185 except StopIteration: |
163 raise ValueError(_("missing <forums> element")) | 186 raise ValueError(_("missing <forums> element")) |
164 else: | 187 else: |
165 forums_elt = parent_elt | 188 forums_elt = parent_elt |
166 if forums is None: | 189 if forums is None: |
167 raise exceptions.InternalError('expected forums') | 190 raise exceptions.InternalError("expected forums") |
168 if forums_elt.name != 'forums': | 191 if forums_elt.name != "forums": |
169 raise ValueError(_('Unexpected element: {xml}').format(xml=forums_elt.toXml())) | 192 raise ValueError( |
193 _("Unexpected element: {xml}").format(xml=forums_elt.toXml()) | |
194 ) | |
170 for forum_elt in forums_elt.elements(): | 195 for forum_elt in forums_elt.elements(): |
171 if forum_elt.name == 'forum': | 196 if forum_elt.name == "forum": |
172 data = {} | 197 data = {} |
173 for attrib in FORUM_ATTR.intersection(forum_elt.attributes): | 198 for attrib in FORUM_ATTR.intersection(forum_elt.attributes): |
174 data[attrib] = forum_elt[attrib] | 199 data[attrib] = forum_elt[attrib] |
175 unknown = set(forum_elt.attributes).difference(FORUM_ATTR) | 200 unknown = set(forum_elt.attributes).difference(FORUM_ATTR) |
176 if unknown: | 201 if unknown: |
177 log.warning(_("Following attributes are unknown: {unknown}").format(unknown=unknown)) | 202 log.warning( |
203 _("Following attributes are unknown: {unknown}").format( | |
204 unknown=unknown | |
205 ) | |
206 ) | |
178 for elt in forum_elt.elements(): | 207 for elt in forum_elt.elements(): |
179 if elt.name in FORUM_SUB_ELTS: | 208 if elt.name in FORUM_SUB_ELTS: |
180 data[elt.name] = str(elt) | 209 data[elt.name] = str(elt) |
181 elif elt.name == 'forums': | 210 elif elt.name == "forums": |
182 sub_forums = data['sub-forums'] = [] | 211 sub_forums = data["sub-forums"] = [] |
183 self._parse_forums(elt, sub_forums) | 212 self._parse_forums(elt, sub_forums) |
184 if not 'title' in data or not {'uri', 'sub-forums'}.intersection(data): | 213 if not "title" in data or not {"uri", "sub-forums"}.intersection(data): |
185 log.warning(_("invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml())) | 214 log.warning( |
215 _("invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml()) | |
216 ) | |
186 else: | 217 else: |
187 forums.append(data) | 218 forums.append(data) |
188 else: | 219 else: |
189 log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt)) | 220 log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt)) |
190 | 221 |
206 if service is None: | 237 if service is None: |
207 service = client.pubsub_service | 238 service = client.pubsub_service |
208 if node is None: | 239 if node is None: |
209 node = NS_FORUMS | 240 node = NS_FORUMS |
210 if forums_key is None: | 241 if forums_key is None: |
211 forums_key = 'default' | 242 forums_key = "default" |
212 items_data = await self._p.get_items(client, service, node, item_ids=[forums_key]) | 243 items_data = await self._p.get_items(client, service, node, item_ids=[forums_key]) |
213 item = items_data[0][0] | 244 item = items_data[0][0] |
214 # we have the item and need to convert it to json | 245 # we have the item and need to convert it to json |
215 forums = self._parse_forums(item) | 246 forums = self._parse_forums(item) |
216 return forums | 247 return forums |
217 | 248 |
218 def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE): | 249 def _set( |
250 self, | |
251 forums: str, | |
252 service_s: str = "", | |
253 node_s: str = "", | |
254 forums_key: str = "", | |
255 profile_key: str = C.PROF_KEY_NONE, | |
256 ) -> defer.Deferred: | |
219 client = self.host.get_client(profile_key) | 257 client = self.host.get_client(profile_key) |
220 forums = json.loads(forums) | 258 forums = json.loads(forums) |
221 if service.strip(): | 259 if not service_s.strip(): |
222 service = jid.JID(service) | |
223 else: | |
224 service = None | 260 service = None |
225 if not node.strip(): | 261 else: |
226 node = None | 262 service = jid.JID(service_s) |
263 node = None if not node_s.strip() else node_s | |
227 return defer.ensureDeferred( | 264 return defer.ensureDeferred( |
228 self.set(client, forums, service, node, forums_key or None) | 265 self.set(client, forums, service, node, forums_key or None) |
229 ) | 266 ) |
230 | 267 |
231 async def set(self, client, forums, service=None, node=None, forums_key=None): | 268 async def set(self, client, forums, service=None, node=None, forums_key=None): |
244 @param forums_key(unicode, None): key (i.e. item id) of the forums | 281 @param forums_key(unicode, None): key (i.e. item id) of the forums |
245 may be used to store different forums structures for different languages | 282 may be used to store different forums structures for different languages |
246 None to use "default" | 283 None to use "default" |
247 """ | 284 """ |
248 if service is None: | 285 if service is None: |
249 service = client.pubsub_service | 286 service = client.pubsub_service |
250 if node is None: | 287 if node is None: |
251 node = NS_FORUMS | 288 node = NS_FORUMS |
252 if forums_key is None: | 289 if forums_key is None: |
253 forums_key = 'default' | 290 forums_key = "default" |
254 forums_elt = await self._create_forums(client, forums, service, node) | 291 forums_elt = await self._create_forums(client, forums, service, node) |
255 return await self._p.send_item( | 292 return await self._p.send_item( |
256 client, service, node, forums_elt, item_id=forums_key | 293 client, service, node, forums_elt, item_id=forums_key |
257 ) | 294 ) |
258 | 295 |
259 def _get_topics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE): | 296 def _get_topics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE): |
260 client = self.host.get_client(profile_key) | 297 client = self.host.get_client(profile_key) |
261 extra = self._p.parse_extra(extra) | 298 extra = self._p.parse_extra(extra) |
262 d = defer.ensureDeferred( | 299 d = defer.ensureDeferred( |
263 self.get_topics( | 300 self.get_topics( |
264 client, jid.JID(service), node, rsm_request=extra.rsm_request, | 301 client, |
265 extra=extra.extra | 302 jid.JID(service), |
303 node, | |
304 rsm_request=extra.rsm_request, | |
305 extra=extra.extra, | |
266 ) | 306 ) |
267 ) | 307 ) |
268 d.addCallback( | 308 d.addCallback( |
269 lambda topics_data: (topics_data[0], data_format.serialise(topics_data[1])) | 309 lambda topics_data: (topics_data[0], data_format.serialise(topics_data[1])) |
270 ) | 310 ) |
279 client, service, node, rsm_request=rsm_request, extra=extra | 319 client, service, node, rsm_request=rsm_request, extra=extra |
280 ) | 320 ) |
281 topics = [] | 321 topics = [] |
282 item_elts, metadata = topics_data | 322 item_elts, metadata = topics_data |
283 for item_elt in item_elts: | 323 for item_elt in item_elts: |
284 topic_elt = next(item_elt.elements(NS_FORUMS, 'topic')) | 324 topic_elt = next(item_elt.elements(NS_FORUMS, "topic")) |
285 title_elt = next(topic_elt.elements(NS_FORUMS, 'title')) | 325 title_elt = next(topic_elt.elements(NS_FORUMS, "title")) |
286 topic = {'uri': topic_elt['uri'], | 326 topic = { |
287 'author': topic_elt['author'], | 327 "uri": topic_elt["uri"], |
288 'title': str(title_elt)} | 328 "author": topic_elt["author"], |
329 "title": str(title_elt), | |
330 } | |
289 topics.append(topic) | 331 topics.append(topic) |
290 return (topics, metadata) | 332 return (topics, metadata) |
291 | 333 |
292 def _create_topic(self, service, node, mb_data, profile_key): | 334 def _create_topic(self, service, node, mb_data, profile_key): |
293 client = self.host.get_client(profile_key) | 335 client = self.host.get_client(profile_key) |
295 self.create_topic(client, jid.JID(service), node, mb_data) | 337 self.create_topic(client, jid.JID(service), node, mb_data) |
296 ) | 338 ) |
297 | 339 |
298 async def create_topic(self, client, service, node, mb_data): | 340 async def create_topic(self, client, service, node, mb_data): |
299 try: | 341 try: |
300 title = mb_data['title'] | 342 title = mb_data["title"] |
301 content = mb_data.pop('content') | 343 content = mb_data.pop("content") |
302 except KeyError as e: | 344 except KeyError as e: |
303 raise exceptions.DataError("missing mandatory data: {key}".format(key=e.args[0])) | 345 raise exceptions.DataError( |
346 "missing mandatory data: {key}".format(key=e.args[0]) | |
347 ) | |
304 else: | 348 else: |
305 mb_data["content_rich"] = content | 349 mb_data["content_rich"] = content |
306 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) | 350 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid()) |
307 await self._p.createNode(client, service, topic_node, self._node_options) | 351 await self._p.createNode(client, service, topic_node, self._node_options) |
308 await self._m.send(client, mb_data, service, topic_node) | 352 await self._m.send(client, mb_data, service, topic_node) |
309 topic_uri = uri.build_xmpp_uri('pubsub', | 353 topic_uri = uri.build_xmpp_uri( |
310 subtype='microblog', | 354 "pubsub", subtype="microblog", path=service.full(), node=topic_node |
311 path=service.full(), | 355 ) |
312 node=topic_node) | 356 topic_elt = domish.Element((NS_FORUMS, "topic")) |
313 topic_elt = domish.Element((NS_FORUMS, 'topic')) | 357 topic_elt["uri"] = topic_uri |
314 topic_elt['uri'] = topic_uri | 358 topic_elt["author"] = client.jid.userhost() |
315 topic_elt['author'] = client.jid.userhost() | 359 topic_elt.addElement("title", content=title) |
316 topic_elt.addElement('title', content = title) | |
317 await self._p.send_item(client, service, node, topic_elt) | 360 await self._p.send_item(client, service, node, topic_elt) |