diff options
author | wurstsalat <mailtrash@posteo.de> | 2022-12-04 19:03:03 +0300 |
---|---|---|
committer | Philipp Hörist <philipp@hoerist.com> | 2023-02-03 18:50:35 +0300 |
commit | 8883336f27cff82dc847acf1fc2f266a69475b1c (patch) | |
tree | 47e26046df5018209961ec9f917e37e44b51a0b4 | |
parent | 345fbe0dacb7ac25dd531744df5cf9b8060e13bd (diff) |
feat: Add support for XEP-0461: Message Replies
-rw-r--r-- | nbxmpp/dispatcher.py | 2 | ||||
-rw-r--r-- | nbxmpp/modules/replies.py | 98 | ||||
-rw-r--r-- | nbxmpp/namespaces.py | 2 | ||||
-rw-r--r-- | nbxmpp/protocol.py | 24 | ||||
-rw-r--r-- | nbxmpp/structs.py | 8 | ||||
-rw-r--r-- | python-nbxmpp.doap | 7 |
6 files changed, 141 insertions, 0 deletions
diff --git a/nbxmpp/dispatcher.py b/nbxmpp/dispatcher.py index 0a232b2..b7c5450 100644 --- a/nbxmpp/dispatcher.py +++ b/nbxmpp/dispatcher.py @@ -72,6 +72,7 @@ from nbxmpp.modules.chat_markers import ChatMarkers from nbxmpp.modules.receipts import Receipts from nbxmpp.modules.oob import OOB from nbxmpp.modules.correction import Correction +from nbxmpp.modules.replies import Replies from nbxmpp.modules.attention import Attention from nbxmpp.modules.security_labels import SecurityLabels from nbxmpp.modules.chatstates import Chatstates @@ -185,6 +186,7 @@ class StanzaDispatcher(Observable): self._modules['Discovery'] = Discovery(self._client) self._modules['ChatMarkers'] = ChatMarkers(self._client) self._modules['Receipts'] = Receipts(self._client) + self._modules['Replies'] = Replies(self._client) self._modules['OOB'] = OOB(self._client) self._modules['Correction'] = Correction(self._client) self._modules['Attention'] = Attention(self._client) diff --git a/nbxmpp/modules/replies.py b/nbxmpp/modules/replies.py new file mode 100644 index 0000000..8f91b5a --- /dev/null +++ b/nbxmpp/modules/replies.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 <http://www.gnu.org/licenses/>. + +# XEP-0461: Message Replies + +from __future__ import annotations + +from typing import Optional +from typing import TYPE_CHECKING + +from nbxmpp.modules.base import BaseModule +from nbxmpp.namespaces import Namespace +from nbxmpp.protocol import Message +from nbxmpp.structs import MessageProperties +from nbxmpp.structs import ReplyData +from nbxmpp.structs import StanzaHandler + +if TYPE_CHECKING: + from nbxmpp.client import Client + + +class Replies(BaseModule): + def __init__(self, client: Client) -> None: + BaseModule.__init__(self, client) + + self._client = client + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_message, + ns=Namespace.REPLY, + priority=15) + ] + + def _process_message(self, + _client: Client, + stanza: Message, + properties: MessageProperties + ) -> None: + + reply = stanza.getTag('reply', namespace=Namespace.REPLY) + if reply is None: + return + + reply_to = reply.getAttr('to') + if reply_to is None: + self._log.warning('Received reply without "to" attribute') + return + + reply_to_id = reply.getAttr('id') + if reply_to_id is None: + self._log.warning('Received reply without "id"') + return + + fallback_start, fallback_end = None, None + fallback_data = self._get_fallback_data(stanza) + if fallback_data is not None: + fallback_start, fallback_end = fallback_data + + properties.reply_data = ReplyData( + to=reply_to, + id=reply_to_id, + fallback_start=fallback_start, + fallback_end=fallback_end) + + def _get_fallback_data(self, stanza: Message) -> Optional[tuple[int, int]]: + fallback = stanza.getTag('fallback', namespace=Namespace.FALLBACK) + if fallback is None or fallback.getAttr('for') != Namespace.REPLY: + return None + + fallback_data = fallback.getTag('body') + if fallback_data is None: + return None + + start = fallback_data.getAttr('start') + end = fallback_data.getAttr('end') + if start is None or end is None: + return None + + try: + start = int(start) + end = int(end) + except ValueError: + self._log.warning('Could not get fallback start/end') + return None + + return start, end diff --git a/nbxmpp/namespaces.py b/nbxmpp/namespaces.py index c5bd822..dc8a661 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' + FALLBACK: str = 'urn:xmpp:fallback:0' FASTEN: str = 'urn:xmpp:fasten:0' FILE_METADATA: str = 'urn:xmpp:file:metadata:0' FORWARD: str = 'urn:xmpp:forward:0' @@ -138,6 +139,7 @@ class _Namespaces: RECEIPTS: str = 'urn:xmpp:receipts' REGISTER: str = 'jabber:iq:register' REGISTER_FEATURE: str = 'http://jabber.org/features/iq-register' + REPLY: str = 'urn:xmpp:reply:0' REPORTING: str = 'urn:xmpp:reporting:0' ROSTER: str = 'jabber:iq:roster' ROSTERNOTES: str = 'storage:rosternotes' diff --git a/nbxmpp/protocol.py b/nbxmpp/protocol.py index a6822c8..7ff4595 100644 --- a/nbxmpp/protocol.py +++ b/nbxmpp/protocol.py @@ -1258,6 +1258,30 @@ class Message(Protocol): def setReceiptReceived(self, id_): self.setTag('received', namespace=Namespace.RECEIPTS, attrs={'id': id_}) + def setReply(self, + recipient_jid: str, + reply_to_id: str, + fallback_start: int, + fallback_end: int + ) -> None: + + self.setTag( + 'reply', + namespace=Namespace.REPLY, + attrs={ + 'id': reply_to_id, + 'to': recipient_jid}) + + fallback_tag = self.setTag( + 'fallback', + namespace=Namespace.FALLBACK, + attrs={'for': Namespace.REPLY}) + fallback_tag.addChild( + 'body', + attrs={ + 'start': str(fallback_start), + 'end': str(fallback_end)}) + def setOOB(self, url, desc=None): oob = self.setTag('x', namespace=Namespace.X_OOB) oob.setTagData('url', url) diff --git a/nbxmpp/structs.py b/nbxmpp/structs.py index a54ecdd..663b49b 100644 --- a/nbxmpp/structs.py +++ b/nbxmpp/structs.py @@ -290,6 +290,13 @@ class CorrectionData(NamedTuple): id: str +class ReplyData(NamedTuple): + to: str + id: str + fallback_start: Optional[int] + fallback_end: Optional[int] + + class ModerationData(NamedTuple): stanza_id: str moderator_jid: str @@ -993,6 +1000,7 @@ class MessageProperties: receipt: Optional[ReceiptData] = None oob: Optional[OOBData] = None correction: Optional[CorrectionData] = None + reply_data: Optional[ReplyData] = None moderation: Optional[ModerationData] = None attention: bool = False forms = None diff --git a/python-nbxmpp.doap b/python-nbxmpp.doap index 65135bb..9e95da2 100644 --- a/python-nbxmpp.doap +++ b/python-nbxmpp.doap @@ -422,5 +422,12 @@ <xmpp:version>0.1.0</xmpp:version> </xmpp:SupportedXep> </implements> + <implements> + <xmpp:SupportedXep> + <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html"/> + <xmpp:status>complete</xmpp:status> + <xmpp:version>0.1.0</xmpp:version> + </xmpp:SupportedXep> + </implements> </Project> </rdf:RDF> |