comparison sat/plugins/plugin_xep_0444.py @ 3886:1f88ca90c3de

plugin XEP-0444: Message Reactions implementation: rel 371
author Goffi <goffi@goffi.org>
date Wed, 31 Aug 2022 17:07:03 +0200
parents
children 524856bd7b19
comparison
equal deleted inserted replaced
3885:18ff4f75f0e6 3886:1f88ca90c3de
1 #!/usr/bin/env python3
2
3 # Libervia plugin for XEP-0444
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from typing import List, Iterable
20 from copy import deepcopy
21
22 from twisted.words.protocols.jabber import jid, xmlstream
23 from twisted.words.xish import domish
24 from twisted.internet import defer
25 from wokkel import disco, iwokkel
26 from zope.interface import implementer
27
28 from sat.core.constants import Const as C
29 from sat.core.i18n import _
30 from sat.core.log import getLogger
31 from sat.core import exceptions
32 from sat.core.core_types import SatXMPPEntity
33 from sat.memory.sqla_mapping import History
34
35 log = getLogger(__name__)
36
37 PLUGIN_INFO = {
38 C.PI_NAME: "Message Reactions",
39 C.PI_IMPORT_NAME: "XEP-0444",
40 C.PI_TYPE: C.PLUG_TYPE_XEP,
41 C.PI_MODES: C.PLUG_MODE_BOTH,
42 C.PI_PROTOCOLS: ["XEP-0444"],
43 C.PI_DEPENDENCIES: ["XEP-0334"],
44 C.PI_MAIN: "XEP_0444",
45 C.PI_HANDLER: "yes",
46 C.PI_DESCRIPTION: _("""Message Reactions implementation"""),
47 }
48
49 NS_REACTIONS = "urn:xmpp:reactions:0"
50
51
52 class XEP_0444:
53
54 def __init__(self, host):
55 log.info(_("Message Reactions initialization"))
56 host.registerNamespace("reactions", NS_REACTIONS)
57 self.host = host
58 self._h = host.plugins["XEP-0334"]
59 host.bridge.addMethod(
60 "messageReactionsSet",
61 ".plugin",
62 in_sign="ssas",
63 out_sign="",
64 method=self._reactionsSet,
65 async_=True,
66 )
67 host.trigger.add("messageReceived", self._messageReceivedTrigger)
68
69 def getHandler(self, client):
70 return XEP_0444_Handler()
71
72 async def _messageReceivedTrigger(
73 self,
74 client: SatXMPPEntity,
75 message_elt: domish.Element,
76 post_treat: defer.Deferred
77 ) -> bool:
78 return True
79
80 def _reactionsSet(self, message_id: str, profile: str, reactions: List[str]) -> None:
81 client = self.host.getClient(profile)
82 return defer.ensureDeferred(
83 self.setReactions(client, message_id)
84 )
85
86 def sendReactions(
87 self,
88 client: SatXMPPEntity,
89 dest_jid: jid.JID,
90 message_id: str,
91 reactions: Iterable[str]
92 ) -> None:
93 """Send the <message> stanza containing the reactions
94
95 @param dest_jid: recipient of the reaction
96 @param message_id: either <origin-id> or message's ID
97 see https://xmpp.org/extensions/xep-0444.html#business-id
98 """
99 message_elt = domish.Element((None, "message"))
100 message_elt["from"] = client.jid.full()
101 message_elt["to"] = dest_jid.full()
102 reactions_elt = message_elt.addElement((NS_REACTIONS, "reactions"))
103 reactions_elt["id"] = message_id
104 for r in set(reactions):
105 reactions_elt.addElement("reaction", content=r)
106 self._h.addHintElements(message_elt, [self._h.HINT_STORE])
107 client.send(message_elt)
108
109 async def addReactionsToHistory(
110 self,
111 history: History,
112 from_jid: jid.JID,
113 reactions: Iterable[str]
114 ) -> None:
115 """Update History instance with given reactions
116
117 @param history: storage History instance
118 will be updated in DB
119 "summary" field of history.extra["reactions"] will also be updated
120 @param from_jid: author of the reactions
121 @param reactions: list of reactions
122 """
123 history.extra = deepcopy(history.extra) if history.extra else {}
124 h_reactions = history.extra.setdefault("reactions", {})
125 # reactions mapped by originating JID
126 by_jid = h_reactions.setdefault("by_jid", {})
127 # reactions are sorted to in summary to keep a consistent order
128 h_reactions["by_jid"][from_jid.userhost()] = sorted(list(set(reactions)))
129 h_reactions["summary"] = sorted(list(set().union(*by_jid.values())))
130 await self.host.memory.storage.session_add(history)
131
132 async def setReactions(
133 self,
134 client: SatXMPPEntity,
135 message_id: str,
136 reactions: Iterable[str]
137 ) -> None:
138 """Set and replace reactions to a message
139
140 @param message_id: internal ID of the message
141 @param rections: lsit of emojis to used to react to the message
142 use empty list to remove all reactions
143 """
144 if not message_id:
145 raise ValueError("message_id can't be empty")
146 history = await self.host.memory.storage.get(
147 client, History, History.uid, message_id,
148 joined_loads=[History.messages, History.subjects]
149 )
150 if history is None:
151 raise exceptions.NotFound(
152 f"message to retract not found in database ({message_id})"
153 )
154 mess_id = history.origin_id or history.stanza_id
155 if not mess_id:
156 raise exceptions.DataError(
157 "target message has neither origin-id nor message-id, we can't send a "
158 "reaction"
159 )
160 await self.addReactionsToHistory(history, client.jid, reactions)
161 self.sendReactions(client, history.dest_jid, mess_id, reactions)
162
163
164 @implementer(iwokkel.IDisco)
165 class XEP_0444_Handler(xmlstream.XMPPHandler):
166
167 def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
168 return [disco.DiscoFeature(NS_REACTIONS)]
169
170 def getDiscoItems(self, requestor, service, nodeIdentifier=""):
171 return []