diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/plugins/plugin_misc_forums.py	Fri Jun 02 11:49:51 2023 +0200
@@ -0,0 +1,307 @@
+#!/usr/bin/env python3
+
+
+# SAT plugin for pubsub forums
+# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from libervia.backend.core.i18n import _
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core import exceptions
+from libervia.backend.core.log import getLogger
+from libervia.backend.tools.common import uri, data_format
+from twisted.words.protocols.jabber import jid
+from twisted.words.xish import domish
+from twisted.internet import defer
+import shortuuid
+import json
+log = getLogger(__name__)
+
+NS_FORUMS = 'org.salut-a-toi.forums:0'
+NS_FORUMS_TOPICS = NS_FORUMS + '#topics'
+
+PLUGIN_INFO = {
+    C.PI_NAME: _("forums management"),
+    C.PI_IMPORT_NAME: "forums",
+    C.PI_TYPE: "EXP",
+    C.PI_PROTOCOLS: [],
+    C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0277"],
+    C.PI_MAIN: "forums",
+    C.PI_HANDLER: "no",
+    C.PI_DESCRIPTION: _("""forums management plugin""")
+}
+FORUM_ATTR = {'title', 'name', 'main-language', 'uri'}
+FORUM_SUB_ELTS = ('short-desc', 'desc')
+FORUM_TOPICS_NODE_TPL = '{node}#topics_{uuid}'
+FORUM_TOPIC_NODE_TPL = '{node}_{uuid}'
+
+
+class forums(object):
+
+    def __init__(self, host):
+        log.info(_("forums plugin initialization"))
+        self.host = host
+        self._m = self.host.plugins['XEP-0277']
+        self._p = self.host.plugins['XEP-0060']
+        self._node_options = {
+            self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN,
+            self._p.OPT_PERSIST_ITEMS: 1,
+            self._p.OPT_DELIVER_PAYLOADS: 1,
+            self._p.OPT_SEND_ITEM_SUBSCRIBE: 1,
+            self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN,
+            }
+        host.register_namespace('forums', NS_FORUMS)
+        host.bridge.add_method("forums_get", ".plugin",
+                              in_sign='ssss', out_sign='s',
+                              method=self._get,
+                              async_=True)
+        host.bridge.add_method("forums_set", ".plugin",
+                              in_sign='sssss', out_sign='',
+                              method=self._set,
+                              async_=True)
+        host.bridge.add_method("forum_topics_get", ".plugin",
+                              in_sign='ssa{ss}s', out_sign='(aa{ss}s)',
+                              method=self._get_topics,
+                              async_=True)
+        host.bridge.add_method("forum_topic_create", ".plugin",
+                              in_sign='ssa{ss}s', out_sign='',
+                              method=self._create_topic,
+                              async_=True)
+
+    @defer.inlineCallbacks
+    def _create_forums(self, client, forums, service, node, forums_elt=None, names=None):
+        """Recursively create <forums> element(s)
+
+        @param forums(list): forums which may have subforums
+        @param service(jid.JID): service where the new nodes will be created
+        @param node(unicode): node of the forums
+            will be used as basis for the newly created nodes
+        @param parent_elt(domish.Element, None): element where the forum must be added
+            if None, the root <forums> element will be created
+        @return (domish.Element): created forums
+        """
+        if not isinstance(forums, list):
+            raise ValueError(_("forums arguments must be a list of forums"))
+        if forums_elt is None:
+            forums_elt = domish.Element((NS_FORUMS, 'forums'))
+            assert names is None
+            names = set()
+        else:
+            if names is None or forums_elt.name != 'forums':
+                raise exceptions.InternalError('invalid forums or names')
+            assert names is not None
+
+        for forum in forums:
+            if not isinstance(forum, dict):
+                raise ValueError(_("A forum item must be a dictionary"))
+            forum_elt = forums_elt.addElement('forum')
+
+            for key, value in forum.items():
+                if key == 'name' and key in names:
+                    raise exceptions.ConflictError(_("following forum name is not unique: {name}").format(name=key))
+                if key == 'uri' and not value.strip():
+                    log.info(_("creating missing forum node"))
+                    forum_node = FORUM_TOPICS_NODE_TPL.format(node=node, uuid=shortuuid.uuid())
+                    yield self._p.createNode(client, service, forum_node, self._node_options)
+                    value = uri.build_xmpp_uri('pubsub',
+                                             path=service.full(),
+                                             node=forum_node)
+                if key in FORUM_ATTR:
+                    forum_elt[key] = value.strip()
+                elif key in FORUM_SUB_ELTS:
+                    forum_elt.addElement(key, content=value)
+                elif key == 'sub-forums':
+                    sub_forums_elt = forum_elt.addElement('forums')
+                    yield self._create_forums(client, value, service, node, sub_forums_elt, names=names)
+                else:
+                    log.warning(_("Unknown forum attribute: {key}").format(key=key))
+            if not forum_elt.getAttribute('title'):
+                name = forum_elt.getAttribute('name')
+                if name:
+                    forum_elt['title'] = name
+                else:
+                    raise ValueError(_("forum need a title or a name"))
+            if not forum_elt.getAttribute('uri') and not forum_elt.children:
+                raise ValueError(_("forum need uri or sub-forums"))
+        defer.returnValue(forums_elt)
+
+    def _parse_forums(self, parent_elt=None, forums=None):
+        """Recursivly parse a <forums> elements and return corresponding forums data
+
+        @param item(domish.Element): item with <forums> element
+        @param parent_elt(domish.Element, None): element to parse
+        @return (list): parsed data
+        @raise ValueError: item is invalid
+        """
+        if parent_elt.name == 'item':
+            forums = []
+            try:
+                forums_elt = next(parent_elt.elements(NS_FORUMS, 'forums'))
+            except StopIteration:
+                raise ValueError(_("missing <forums> element"))
+        else:
+            forums_elt = parent_elt
+            if forums is None:
+                raise exceptions.InternalError('expected forums')
+            if forums_elt.name != 'forums':
+                raise ValueError(_('Unexpected element: {xml}').format(xml=forums_elt.toXml()))
+        for forum_elt in forums_elt.elements():
+            if forum_elt.name == 'forum':
+                data = {}
+                for attrib in FORUM_ATTR.intersection(forum_elt.attributes):
+                    data[attrib] = forum_elt[attrib]
+                unknown = set(forum_elt.attributes).difference(FORUM_ATTR)
+                if unknown:
+                    log.warning(_("Following attributes are unknown: {unknown}").format(unknown=unknown))
+                for elt in forum_elt.elements():
+                    if elt.name in FORUM_SUB_ELTS:
+                        data[elt.name] = str(elt)
+                    elif elt.name == 'forums':
+                        sub_forums = data['sub-forums'] = []
+                        self._parse_forums(elt, sub_forums)
+                if not 'title' in data or not {'uri', 'sub-forums'}.intersection(data):
+                    log.warning(_("invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml()))
+                else:
+                    forums.append(data)
+            else:
+                log.warning(_("unkown forums sub element: {xml}").format(xml=forum_elt))
+
+        return forums
+
+    def _get(self, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE):
+        client = self.host.get_client(profile_key)
+        if service.strip():
+            service = jid.JID(service)
+        else:
+            service = None
+        if not node.strip():
+            node = None
+        d = defer.ensureDeferred(self.get(client, service, node, forums_key or None))
+        d.addCallback(lambda data: json.dumps(data))
+        return d
+
+    async def get(self, client, service=None, node=None, forums_key=None):
+        if service is None:
+            service = client.pubsub_service
+        if node is None:
+            node = NS_FORUMS
+        if forums_key is None:
+            forums_key = 'default'
+        items_data = await self._p.get_items(client, service, node, item_ids=[forums_key])
+        item = items_data[0][0]
+        # we have the item and need to convert it to json
+        forums = self._parse_forums(item)
+        return forums
+
+    def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE):
+        client = self.host.get_client(profile_key)
+        forums = json.loads(forums)
+        if service.strip():
+            service = jid.JID(service)
+        else:
+            service = None
+        if not node.strip():
+            node = None
+        return defer.ensureDeferred(
+            self.set(client, forums, service, node, forums_key or None)
+        )
+
+    async def set(self, client, forums, service=None, node=None, forums_key=None):
+        """Create or replace forums structure
+
+        @param forums(list): list of dictionary as follow:
+            a dictionary represent a forum metadata, with the following keys:
+                - title: title of the forum
+                - name: short name (unique in those forums) for the forum
+                - main-language: main language to be use in the forums
+                - uri: XMPP uri to the microblog node hosting the forum
+                - short-desc: short description of the forum (in main-language)
+                - desc: long description of the forum (in main-language)
+                - sub-forums: a list of sub-forums with the same structure
+            title or name is needed, and uri or sub-forums
+        @param forums_key(unicode, None): key (i.e. item id) of the forums
+            may be used to store different forums structures for different languages
+            None to use "default"
+        """
+        if service is None:
+             service = client.pubsub_service
+        if node is None:
+            node = NS_FORUMS
+        if forums_key is None:
+            forums_key = 'default'
+        forums_elt = await self._create_forums(client, forums, service, node)
+        return await self._p.send_item(
+            client, service, node, forums_elt, item_id=forums_key
+        )
+
+    def _get_topics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE):
+        client = self.host.get_client(profile_key)
+        extra = self._p.parse_extra(extra)
+        d = defer.ensureDeferred(
+            self.get_topics(
+                client, jid.JID(service), node, rsm_request=extra.rsm_request,
+                extra=extra.extra
+            )
+        )
+        d.addCallback(
+            lambda topics_data: (topics_data[0], data_format.serialise(topics_data[1]))
+        )
+        return d
+
+    async def get_topics(self, client, service, node, rsm_request=None, extra=None):
+        """Retrieve topics data
+
+        Topics are simple microblog URIs with some metadata duplicated from first post
+        """
+        topics_data = await self._p.get_items(
+            client, service, node, rsm_request=rsm_request, extra=extra
+        )
+        topics = []
+        item_elts, metadata = topics_data
+        for item_elt in item_elts:
+            topic_elt = next(item_elt.elements(NS_FORUMS, 'topic'))
+            title_elt = next(topic_elt.elements(NS_FORUMS, 'title'))
+            topic = {'uri': topic_elt['uri'],
+                     'author': topic_elt['author'],
+                     'title': str(title_elt)}
+            topics.append(topic)
+        return (topics, metadata)
+
+    def _create_topic(self, service, node, mb_data, profile_key):
+        client = self.host.get_client(profile_key)
+        return defer.ensureDeferred(
+            self.create_topic(client, jid.JID(service), node, mb_data)
+        )
+
+    async def create_topic(self, client, service, node, mb_data):
+        try:
+            title = mb_data['title']
+            content = mb_data.pop('content')
+        except KeyError as e:
+            raise exceptions.DataError("missing mandatory data: {key}".format(key=e.args[0]))
+        else:
+            mb_data["content_rich"] = content
+        topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid())
+        await self._p.createNode(client, service, topic_node, self._node_options)
+        await self._m.send(client, mb_data, service, topic_node)
+        topic_uri = uri.build_xmpp_uri('pubsub',
+                                     subtype='microblog',
+                                     path=service.full(),
+                                     node=topic_node)
+        topic_elt = domish.Element((NS_FORUMS, 'topic'))
+        topic_elt['uri'] = topic_uri
+        topic_elt['author'] = client.jid.userhost()
+        topic_elt.addElement('title', content = title)
+        await self._p.send_item(client, service, node, topic_elt)