comparison libervia/backend/plugins/plugin_misc_forums.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_misc_forums.py@524856bd7b19
children 5d056d524298
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for pubsub forums
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
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 libervia.backend.core.i18n import _
21 from libervia.backend.core.constants import Const as C
22 from libervia.backend.core import exceptions
23 from libervia.backend.core.log import getLogger
24 from libervia.backend.tools.common import uri, data_format
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
32 NS_FORUMS = 'org.salut-a-toi.forums:0'
33 NS_FORUMS_TOPICS = NS_FORUMS + '#topics'
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 }
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}'
49
50
51 class forums(object):
52
53 def __init__(self, host):
54 log.info(_("forums plugin initialization"))
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.register_namespace('forums', NS_FORUMS)
66 host.bridge.add_method("forums_get", ".plugin",
67 in_sign='ssss', out_sign='s',
68 method=self._get,
69 async_=True)
70 host.bridge.add_method("forums_set", ".plugin",
71 in_sign='sssss', out_sign='',
72 method=self._set,
73 async_=True)
74 host.bridge.add_method("forum_topics_get", ".plugin",
75 in_sign='ssa{ss}s', out_sign='(aa{ss}s)',
76 method=self._get_topics,
77 async_=True)
78 host.bridge.add_method("forum_topic_create", ".plugin",
79 in_sign='ssa{ss}s', out_sign='',
80 method=self._create_topic,
81 async_=True)
82
83 @defer.inlineCallbacks
84 def _create_forums(self, client, forums, service, node, forums_elt=None, names=None):
85 """Recursively create <forums> element(s)
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):
96 raise ValueError(_("forums arguments must be a list of forums"))
97 if forums_elt is None:
98 forums_elt = domish.Element((NS_FORUMS, 'forums'))
99 assert names is None
100 names = set()
101 else:
102 if names is None or forums_elt.name != 'forums':
103 raise exceptions.InternalError('invalid forums or names')
104 assert names is not None
105
106 for forum in forums:
107 if not isinstance(forum, dict):
108 raise ValueError(_("A forum item must be a dictionary"))
109 forum_elt = forums_elt.addElement('forum')
110
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"))
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)
118 value = uri.build_xmpp_uri('pubsub',
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)
125 elif key == 'sub-forums':
126 sub_forums_elt = forum_elt.addElement('forums')
127 yield self._create_forums(client, value, service, node, sub_forums_elt, names=names)
128 else:
129 log.warning(_("Unknown forum attribute: {key}").format(key=key))
130 if not forum_elt.getAttribute('title'):
131 name = forum_elt.getAttribute('name')
132 if name:
133 forum_elt['title'] = name
134 else:
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"))
138 defer.returnValue(forums_elt)
139
140 def _parse_forums(self, parent_elt=None, forums=None):
141 """Recursivly parse a <forums> elements and return corresponding forums data
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 """
148 if parent_elt.name == 'item':
149 forums = []
150 try:
151 forums_elt = next(parent_elt.elements(NS_FORUMS, 'forums'))
152 except StopIteration:
153 raise ValueError(_("missing <forums> element"))
154 else:
155 forums_elt = parent_elt
156 if forums is None:
157 raise exceptions.InternalError('expected forums')
158 if forums_elt.name != 'forums':
159 raise ValueError(_('Unexpected element: {xml}').format(xml=forums_elt.toXml()))
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:
167 log.warning(_("Following attributes are unknown: {unknown}").format(unknown=unknown))
168 for elt in forum_elt.elements():
169 if elt.name in FORUM_SUB_ELTS:
170 data[elt.name] = str(elt)
171 elif elt.name == 'forums':
172 sub_forums = data['sub-forums'] = []
173 self._parse_forums(elt, sub_forums)
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()))
176 else:
177 forums.append(data)
178 else:
179 log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt))
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.get_client(profile_key)
185 if service.strip():
186 service = jid.JID(service)
187 else:
188 service = None
189 if not node.strip():
190 node = None
191 d = defer.ensureDeferred(self.get(client, service, node, forums_key or None))
192 d.addCallback(lambda data: json.dumps(data))
193 return d
194
195 async def get(self, client, service=None, node=None, forums_key=None):
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:
201 forums_key = 'default'
202 items_data = await self._p.get_items(client, service, node, item_ids=[forums_key])
203 item = items_data[0][0]
204 # we have the item and need to convert it to json
205 forums = self._parse_forums(item)
206 return forums
207
208 def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE):
209 client = self.host.get_client(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
217 return defer.ensureDeferred(
218 self.set(client, forums, service, node, forums_key or None)
219 )
220
221 async def set(self, client, forums, service=None, node=None, forums_key=None):
222 """Create or replace forums structure
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:
243 forums_key = 'default'
244 forums_elt = await self._create_forums(client, forums, service, node)
245 return await self._p.send_item(
246 client, service, node, forums_elt, item_id=forums_key
247 )
248
249 def _get_topics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE):
250 client = self.host.get_client(profile_key)
251 extra = self._p.parse_extra(extra)
252 d = defer.ensureDeferred(
253 self.get_topics(
254 client, jid.JID(service), node, rsm_request=extra.rsm_request,
255 extra=extra.extra
256 )
257 )
258 d.addCallback(
259 lambda topics_data: (topics_data[0], data_format.serialise(topics_data[1]))
260 )
261 return d
262
263 async def get_topics(self, client, service, node, rsm_request=None, extra=None):
264 """Retrieve topics data
265
266 Topics are simple microblog URIs with some metadata duplicated from first post
267 """
268 topics_data = await self._p.get_items(
269 client, service, node, rsm_request=rsm_request, extra=extra
270 )
271 topics = []
272 item_elts, metadata = topics_data
273 for item_elt in item_elts:
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)}
279 topics.append(topic)
280 return (topics, metadata)
281
282 def _create_topic(self, service, node, mb_data, profile_key):
283 client = self.host.get_client(profile_key)
284 return defer.ensureDeferred(
285 self.create_topic(client, jid.JID(service), node, mb_data)
286 )
287
288 async def create_topic(self, client, service, node, mb_data):
289 try:
290 title = mb_data['title']
291 content = mb_data.pop('content')
292 except KeyError as e:
293 raise exceptions.DataError("missing mandatory data: {key}".format(key=e.args[0]))
294 else:
295 mb_data["content_rich"] = content
296 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid())
297 await self._p.createNode(client, service, topic_node, self._node_options)
298 await self._m.send(client, mb_data, service, topic_node)
299 topic_uri = uri.build_xmpp_uri('pubsub',
300 subtype='microblog',
301 path=service.full(),
302 node=topic_node)
303 topic_elt = domish.Element((NS_FORUMS, 'topic'))
304 topic_elt['uri'] = topic_uri
305 topic_elt['author'] = client.jid.userhost()
306 topic_elt.addElement('title', content = title)
307 await self._p.send_item(client, service, node, topic_elt)