From 22aaefb2b36eb46a5306092496c2be680b167f61 Mon Sep 17 00:00:00 2001 From: wurstsalat Date: Mon, 8 Nov 2021 23:14:11 +0100 Subject: Add XEP-0425: Message Moderation --- nbxmpp/dispatcher.py | 2 + nbxmpp/modules/muc/moderation.py | 98 ++++++++++++++++++++++++++++++++++++++++ nbxmpp/modules/muc/muc.py | 6 +++ nbxmpp/namespaces.py | 3 ++ nbxmpp/structs.py | 16 +++++++ python-nbxmpp.doap | 7 +++ 6 files changed, 132 insertions(+) create mode 100644 nbxmpp/modules/muc/moderation.py diff --git a/nbxmpp/dispatcher.py b/nbxmpp/dispatcher.py index 4b57a7b..b52bbb7 100644 --- a/nbxmpp/dispatcher.py +++ b/nbxmpp/dispatcher.py @@ -44,6 +44,7 @@ from nbxmpp.modules.iq import BaseIq from nbxmpp.modules.nickname import Nickname from nbxmpp.modules.delay import Delay from nbxmpp.modules.muc import MUC +from nbxmpp.modules.muc.moderation import Moderation from nbxmpp.modules.idle import Idle from nbxmpp.modules.pgplegacy import PGPLegacy from nbxmpp.modules.vcard_avatar import VCardAvatar @@ -156,6 +157,7 @@ class StanzaDispatcher(Observable): self._modules['HTTPAuth'] = HTTPAuth(self._client) self._modules['Nickname'] = Nickname(self._client) self._modules['MUC'] = MUC(self._client) + self._modules['Moderation'] = Moderation(self._client) self._modules['Delay'] = Delay(self._client) self._modules['Captcha'] = Captcha(self._client) self._modules['Idle'] = Idle(self._client) diff --git a/nbxmpp/modules/muc/moderation.py b/nbxmpp/modules/muc/moderation.py new file mode 100644 index 0000000..bf4772a --- /dev/null +++ b/nbxmpp/modules/muc/moderation.py @@ -0,0 +1,98 @@ +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see . + +# XEP-0425: Message Moderation + +from typing import Optional + +from nbxmpp.modules.base import BaseModule +from nbxmpp.modules.util import process_response + +from nbxmpp import JID +from nbxmpp.namespaces import Namespace +from nbxmpp.protocol import Iq +from nbxmpp.structs import StanzaHandler +from nbxmpp.structs import MessageProperties +from nbxmpp.structs import ModerationData +from nbxmpp.simplexml import Node +from nbxmpp.task import iq_request_task + + +class Moderation(BaseModule): + def __init__(self, client): + BaseModule.__init__(self, client) + + self._client = client + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_message, + typ='groupchat', + ns=Namespace.FASTEN, + priority=20), + ] + + @iq_request_task + def send_retract_request(self, muc_jid: JID, stanza_id: str, + reason: Optional[str] = None): + _task = yield + + response = yield _make_retract_request(muc_jid, stanza_id, reason) + + yield process_response(response) + + @staticmethod + def _process_message(_client, stanza: Node, + properties: MessageProperties) -> None: + if not properties.jid.is_bare: + return + + apply_to = stanza.getTag( + 'apply-to', namespace=Namespace.FASTEN) + if apply_to is None: + return + + moderated = apply_to.getTag( + 'moderated', namespace=Namespace.MESSAGE_MODERATE) + if moderated is None: + return + + retract = moderated.getTag( + 'retract', namespace=Namespace.MESSAGE_RETRACT) + if retract is None: + # Tag can be 'retract' or 'retracted', depending on whether the + # server applies a tombstone for MAM messages or not. + retract = moderated.getTag( + 'retracted', namespace=Namespace.MESSAGE_RETRACT) + if retract is None: + return + + properties.moderation = ModerationData( + stanza_id=apply_to.getAttr('id'), + moderator_jid=moderated.getAttr('by'), + reason=moderated.getTagData('reason'), + timestamp=retract.getAttr('stamp')) + + +def _make_retract_request(muc_jid: JID, stanza_id: str, + reason: Optional[str]) -> Iq: + iq = Iq('set', Namespace.FASTEN, to=muc_jid) + query = iq.setQuery(name='apply-to') + query.setAttr('id', stanza_id) + moderate = query.addChild(name='moderate', + namespace=Namespace.MESSAGE_MODERATE) + moderate.addChild(name='retract', namespace=Namespace.MESSAGE_RETRACT) + if reason is not None: + moderate.addChild(name='reason', payload=[reason]) + return iq diff --git a/nbxmpp/modules/muc/muc.py b/nbxmpp/modules/muc/muc.py index c596761..0d94375 100644 --- a/nbxmpp/modules/muc/muc.py +++ b/nbxmpp/modules/muc/muc.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program; If not, see . +from typing import Optional from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_NOT_ACCEPTABLE @@ -61,6 +62,7 @@ class MUC(BaseModule): _depends = { 'disco_info': 'Discovery', 'request_vcard': 'VCardTemp', + 'send_retract_request': 'Moderation', } def __init__(self, client): @@ -468,6 +470,10 @@ class MUC(BaseModule): response = yield make_set_role_request(room_jid, nick, role, reason) yield process_response(response) + def retract_message(self, room_jid: JID, stanza_id: str, + reason: Optional[str] = None) -> None: + self.send_retract_request(room_jid, stanza_id, reason) + def set_subject(self, room_jid, subject): message = Message(room_jid, typ='groupchat', subject=subject) self._log.info('Set subject for %s', room_jid) diff --git a/nbxmpp/namespaces.py b/nbxmpp/namespaces.py index fd8464b..b2c52e9 100644 --- a/nbxmpp/namespaces.py +++ b/nbxmpp/namespaces.py @@ -62,6 +62,7 @@ class _Namespaces: DOMAIN_BASED_NAME: str = 'urn:xmpp:domain-based-name:1' EME: str = 'urn:xmpp:eme:0' ENCRYPTED: str = 'jabber:x:encrypted' + FASTEN: str = 'urn:xmpp:fasten:0' FILE_METADATA: str = 'urn:xmpp:file:metadata:0' FORWARD: str = 'urn:xmpp:forward:0' FRAMING: str = 'urn:ietf:params:xml:ns:xmpp-framing' @@ -99,6 +100,8 @@ class _Namespaces: LOCATION: str = 'http://jabber.org/protocol/geoloc' MAM_1: str = 'urn:xmpp:mam:1' MAM_2: str = 'urn:xmpp:mam:2' + MESSAGE_MODERATE: str = 'urn:xmpp:message-moderate:0' + MESSAGE_RETRACT: str = 'urn:xmpp:message-retract:0' MOOD: str = 'http://jabber.org/protocol/mood' MSG_HINTS: str = 'urn:xmpp:hints' MUCLUMBUS: str = 'https://xmlns.zombofant.net/muclumbus/search/1.0' diff --git a/nbxmpp/structs.py b/nbxmpp/structs.py index 80e5831..29b48d9 100644 --- a/nbxmpp/structs.py +++ b/nbxmpp/structs.py @@ -284,6 +284,13 @@ class CorrectionData(NamedTuple): id: str +class ModerationData(NamedTuple): + stanza_id: str + moderator_jid: str + reason: Optional[str] = None + timestamp: Optional[str] = None + + class DiscoItems(NamedTuple): jid: JID node: str @@ -468,6 +475,10 @@ class DiscoInfo(NamedTuple): def has_httpupload(self) -> bool: return Namespace.HTTPUPLOAD_0 in self.features + @property + def has_message_moderation(self) -> bool: + return Namespace.MESSAGE_MODERATE in self.features + @property def is_muc(self) -> bool: for identity in self.identities: @@ -960,6 +971,7 @@ class MessageProperties: receipt: Optional[ReceiptData] = None oob: Optional[OOBData] = None correction: Optional[CorrectionData] = None + moderation: Optional[ModerationData] = None attention: bool = False forms = None xhtml: Optional[str] = None @@ -1069,6 +1081,10 @@ class MessageProperties: def is_correction(self) -> bool: return self.correction is not None + @property + def is_moderation(self) -> bool: + return self.moderation is not None + @property def has_attention(self) -> bool: return self.attention diff --git a/python-nbxmpp.doap b/python-nbxmpp.doap index 0a02787..b6f87b4 100644 --- a/python-nbxmpp.doap +++ b/python-nbxmpp.doap @@ -392,5 +392,12 @@ 0.3.0 + + + + complete + 0.2.1 + + -- cgit v1.2.3