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)