slidge 0.2.0a9__py3-none-any.whl → 0.2.0a10__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- slidge/__version__.py +1 -1
- slidge/command/adhoc.py +1 -1
- slidge/command/base.py +4 -4
- slidge/contact/roster.py +7 -0
- slidge/core/dispatcher/__init__.py +3 -0
- slidge/core/{gateway → dispatcher}/caps.py +6 -4
- slidge/core/{gateway → dispatcher}/disco.py +11 -17
- slidge/core/dispatcher/message/__init__.py +10 -0
- slidge/core/dispatcher/message/chat_state.py +40 -0
- slidge/core/dispatcher/message/marker.py +67 -0
- slidge/core/dispatcher/message/message.py +397 -0
- slidge/core/dispatcher/muc/__init__.py +12 -0
- slidge/core/dispatcher/muc/admin.py +98 -0
- slidge/core/{gateway → dispatcher/muc}/mam.py +26 -15
- slidge/core/dispatcher/muc/misc.py +118 -0
- slidge/core/dispatcher/muc/owner.py +96 -0
- slidge/core/{gateway → dispatcher/muc}/ping.py +10 -15
- slidge/core/dispatcher/presence.py +177 -0
- slidge/core/{gateway → dispatcher}/registration.py +23 -2
- slidge/core/{gateway → dispatcher}/search.py +9 -14
- slidge/core/dispatcher/session_dispatcher.py +84 -0
- slidge/core/dispatcher/util.py +174 -0
- slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +26 -12
- slidge/core/{gateway/base.py → gateway.py} +42 -137
- slidge/core/mixins/base.py +2 -2
- slidge/core/mixins/message.py +10 -4
- slidge/core/pubsub.py +2 -1
- slidge/core/session.py +28 -2
- slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
- slidge/db/models.py +13 -0
- slidge/db/store.py +128 -2
- slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
- slidge/util/test.py +5 -1
- slidge/util/types.py +6 -0
- slidge/util/util.py +5 -2
- {slidge-0.2.0a9.dist-info → slidge-0.2.0a10.dist-info}/METADATA +2 -1
- {slidge-0.2.0a9.dist-info → slidge-0.2.0a10.dist-info}/RECORD +40 -31
- slidge/core/gateway/__init__.py +0 -3
- slidge/core/gateway/muc_admin.py +0 -35
- slidge/core/gateway/presence.py +0 -95
- slidge/core/gateway/session_dispatcher.py +0 -895
- {slidge-0.2.0a9.dist-info → slidge-0.2.0a10.dist-info}/LICENSE +0 -0
- {slidge-0.2.0a9.dist-info → slidge-0.2.0a10.dist-info}/WHEEL +0 -0
- {slidge-0.2.0a9.dist-info → slidge-0.2.0a10.dist-info}/entry_points.txt +0 -0
slidge/__version__.py
CHANGED
slidge/command/adhoc.py
CHANGED
@@ -13,7 +13,7 @@ from . import Command, CommandResponseType, Confirmation, Form, TableResult
|
|
13
13
|
from .base import FormField
|
14
14
|
|
15
15
|
if TYPE_CHECKING:
|
16
|
-
from ..core.gateway
|
16
|
+
from ..core.gateway import BaseGateway
|
17
17
|
from ..core.session import BaseSession
|
18
18
|
|
19
19
|
|
slidge/command/base.py
CHANGED
@@ -6,9 +6,9 @@ from typing import (
|
|
6
6
|
Any,
|
7
7
|
Awaitable,
|
8
8
|
Callable,
|
9
|
-
Collection,
|
10
9
|
Iterable,
|
11
10
|
Optional,
|
11
|
+
Sequence,
|
12
12
|
Type,
|
13
13
|
TypedDict,
|
14
14
|
Union,
|
@@ -54,11 +54,11 @@ class TableResult:
|
|
54
54
|
Structured data as the result of a command
|
55
55
|
"""
|
56
56
|
|
57
|
-
fields:
|
57
|
+
fields: Sequence["FormField"]
|
58
58
|
"""
|
59
59
|
The 'columns names' of the table.
|
60
60
|
"""
|
61
|
-
items:
|
61
|
+
items: Sequence[dict[str, Union[str, JID]]]
|
62
62
|
"""
|
63
63
|
The rows of the table. Each row is a dict where keys are the fields ``var``
|
64
64
|
attribute.
|
@@ -149,7 +149,7 @@ class Form:
|
|
149
149
|
|
150
150
|
title: str
|
151
151
|
instructions: str
|
152
|
-
fields:
|
152
|
+
fields: Sequence["FormField"]
|
153
153
|
handler: FormHandlerType
|
154
154
|
handler_args: Iterable[Any] = field(default_factory=list)
|
155
155
|
handler_kwargs: dict[str, Any] = field(default_factory=dict)
|
slidge/contact/roster.py
CHANGED
@@ -92,6 +92,13 @@ class LegacyRoster(
|
|
92
92
|
stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
|
93
93
|
return await self.__update_contact(stored, legacy_id, username)
|
94
94
|
|
95
|
+
def by_jid_only_if_exists(self, contact_jid: JID) -> LegacyContactType | None:
|
96
|
+
with self.__store.session():
|
97
|
+
stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
|
98
|
+
if stored is not None and stored.updated:
|
99
|
+
return self._contact_cls.from_store(self.session, stored)
|
100
|
+
return None
|
101
|
+
|
95
102
|
async def by_legacy_id(
|
96
103
|
self, legacy_id: LegacyUserIdType, *args, **kwargs
|
97
104
|
) -> LegacyContactType:
|
@@ -5,17 +5,19 @@ from slixmpp import Presence
|
|
5
5
|
from slixmpp.exceptions import XMPPError
|
6
6
|
from slixmpp.xmlstream import StanzaBase
|
7
7
|
|
8
|
+
from .util import DispatcherMixin
|
9
|
+
|
8
10
|
if TYPE_CHECKING:
|
9
|
-
from .
|
11
|
+
from slidge.core.gateway import BaseGateway
|
10
12
|
|
11
13
|
|
12
|
-
class
|
14
|
+
class CapsMixin(DispatcherMixin):
|
13
15
|
def __init__(self, xmpp: "BaseGateway"):
|
14
|
-
|
16
|
+
super().__init__(xmpp)
|
15
17
|
xmpp.del_filter("out", xmpp.plugin["xep_0115"]._filter_add_caps)
|
16
18
|
xmpp.add_filter("out", self._filter_add_caps) # type:ignore
|
17
19
|
|
18
|
-
async def _filter_add_caps(self, stanza: StanzaBase):
|
20
|
+
async def _filter_add_caps(self, stanza: StanzaBase) -> StanzaBase:
|
19
21
|
# we rolled our own "add caps on presences" filter because
|
20
22
|
# there is too much magic happening in slixmpp
|
21
23
|
# anyway, we probably want to roll our own "dynamic disco"/caps
|
@@ -5,13 +5,15 @@ from slixmpp.exceptions import XMPPError
|
|
5
5
|
from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
|
6
6
|
from slixmpp.types import OptJid
|
7
7
|
|
8
|
+
from .util import DispatcherMixin
|
9
|
+
|
8
10
|
if TYPE_CHECKING:
|
9
|
-
from .
|
11
|
+
from slidge.core.gateway import BaseGateway
|
10
12
|
|
11
13
|
|
12
|
-
class
|
14
|
+
class DiscoMixin(DispatcherMixin):
|
13
15
|
def __init__(self, xmpp: "BaseGateway"):
|
14
|
-
|
16
|
+
super().__init__(xmpp)
|
15
17
|
|
16
18
|
xmpp.plugin["xep_0030"].set_node_handler(
|
17
19
|
"get_info",
|
@@ -36,15 +38,11 @@ class Disco:
|
|
36
38
|
if ifrom is None:
|
37
39
|
raise XMPPError("subscription-required")
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
raise XMPPError("registration-required")
|
42
|
-
session = self.xmpp.get_session_from_user(user)
|
43
|
-
await session.wait_for_ready()
|
41
|
+
assert jid is not None
|
42
|
+
session = await self._get_session_from_jid(jid=ifrom)
|
44
43
|
|
45
44
|
log.debug("Looking for entity: %s", jid)
|
46
45
|
|
47
|
-
assert jid is not None
|
48
46
|
entity = await session.get_contact_or_group_or_participant(jid)
|
49
47
|
|
50
48
|
if entity is None:
|
@@ -61,16 +59,12 @@ class Disco:
|
|
61
59
|
if jid != self.xmpp.boundjid.bare:
|
62
60
|
return DiscoItems()
|
63
61
|
|
64
|
-
|
65
|
-
|
66
|
-
raise XMPPError("registration-required")
|
67
|
-
|
68
|
-
session = self.xmpp.get_session_from_user(user)
|
69
|
-
await session.wait_for_ready()
|
62
|
+
assert ifrom is not None
|
63
|
+
session = await self._get_session_from_jid(ifrom)
|
70
64
|
|
71
65
|
d = DiscoItems()
|
72
|
-
for
|
73
|
-
d.add_item(
|
66
|
+
for room in self.xmpp.store.rooms.get_all_jid_and_names(session.user_pk):
|
67
|
+
d.add_item(room.jid, name=room.name)
|
74
68
|
|
75
69
|
return d
|
76
70
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from slixmpp import Message
|
2
|
+
from slixmpp.xmlstream import StanzaBase
|
3
|
+
|
4
|
+
from ..util import DispatcherMixin, exceptions_to_xmpp_errors
|
5
|
+
|
6
|
+
|
7
|
+
class ChatStateMixin(DispatcherMixin):
|
8
|
+
def __init__(self, xmpp) -> None:
|
9
|
+
super().__init__(xmpp)
|
10
|
+
xmpp.add_event_handler("chatstate_active", self.on_chatstate_active)
|
11
|
+
xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive)
|
12
|
+
xmpp.add_event_handler("chatstate_composing", self.on_chatstate_composing)
|
13
|
+
xmpp.add_event_handler("chatstate_paused", self.on_chatstate_paused)
|
14
|
+
|
15
|
+
@exceptions_to_xmpp_errors
|
16
|
+
async def on_chatstate_active(self, msg: StanzaBase) -> None:
|
17
|
+
assert isinstance(msg, Message)
|
18
|
+
if msg["body"]:
|
19
|
+
# if there is a body, it's handled in on_legacy_message()
|
20
|
+
return
|
21
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
22
|
+
await session.on_active(entity, thread)
|
23
|
+
|
24
|
+
@exceptions_to_xmpp_errors
|
25
|
+
async def on_chatstate_inactive(self, msg: StanzaBase) -> None:
|
26
|
+
assert isinstance(msg, Message)
|
27
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
28
|
+
await session.on_inactive(entity, thread)
|
29
|
+
|
30
|
+
@exceptions_to_xmpp_errors
|
31
|
+
async def on_chatstate_composing(self, msg: StanzaBase) -> None:
|
32
|
+
assert isinstance(msg, Message)
|
33
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
34
|
+
await session.on_composing(entity, thread)
|
35
|
+
|
36
|
+
@exceptions_to_xmpp_errors
|
37
|
+
async def on_chatstate_paused(self, msg: StanzaBase) -> None:
|
38
|
+
assert isinstance(msg, Message)
|
39
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
40
|
+
await session.on_paused(entity, thread)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from slixmpp import JID, Message
|
2
|
+
from slixmpp.xmlstream import StanzaBase
|
3
|
+
|
4
|
+
from ....group.room import LegacyMUC
|
5
|
+
from ....util.types import Recipient
|
6
|
+
from ..util import (
|
7
|
+
DispatcherMixin,
|
8
|
+
_get_entity,
|
9
|
+
_xmpp_to_legacy_thread,
|
10
|
+
exceptions_to_xmpp_errors,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class MarkerMixin(DispatcherMixin):
|
15
|
+
def __init__(self, xmpp) -> None:
|
16
|
+
super().__init__(xmpp)
|
17
|
+
xmpp.add_event_handler("marker_displayed", self.on_marker_displayed)
|
18
|
+
xmpp.add_event_handler(
|
19
|
+
"message_displayed_synchronization_publish",
|
20
|
+
self.on_message_displayed_synchronization_publish,
|
21
|
+
)
|
22
|
+
|
23
|
+
@exceptions_to_xmpp_errors
|
24
|
+
async def on_marker_displayed(self, msg: StanzaBase) -> None:
|
25
|
+
assert isinstance(msg, Message)
|
26
|
+
session = await self._get_session(msg)
|
27
|
+
|
28
|
+
e: Recipient = await _get_entity(session, msg)
|
29
|
+
legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
|
30
|
+
displayed_msg_id = msg["displayed"]["id"]
|
31
|
+
if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
|
32
|
+
to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
|
33
|
+
if to_mark is None:
|
34
|
+
session.log.debug("Can't mark all messages up to %s", displayed_msg_id)
|
35
|
+
to_mark = [displayed_msg_id]
|
36
|
+
else:
|
37
|
+
to_mark = [displayed_msg_id]
|
38
|
+
for xmpp_id in to_mark:
|
39
|
+
await session.on_displayed(
|
40
|
+
e, self._xmpp_msg_id_to_legacy(session, xmpp_id), legacy_thread
|
41
|
+
)
|
42
|
+
if isinstance(e, LegacyMUC):
|
43
|
+
await e.echo(msg, None)
|
44
|
+
|
45
|
+
@exceptions_to_xmpp_errors
|
46
|
+
async def on_message_displayed_synchronization_publish(
|
47
|
+
self, msg: StanzaBase
|
48
|
+
) -> None:
|
49
|
+
assert isinstance(msg, Message)
|
50
|
+
chat_jid = JID(msg["pubsub_event"]["items"]["item"]["id"])
|
51
|
+
if chat_jid.server != self.xmpp.boundjid.bare:
|
52
|
+
return
|
53
|
+
|
54
|
+
session = await self._get_session(msg, timeout=None)
|
55
|
+
|
56
|
+
if chat_jid == self.xmpp.boundjid.bare:
|
57
|
+
return
|
58
|
+
|
59
|
+
chat = await session.get_contact_or_group_or_participant(chat_jid)
|
60
|
+
if not isinstance(chat, LegacyMUC):
|
61
|
+
session.log.debug("Ignoring non-groupchat MDS event")
|
62
|
+
return
|
63
|
+
|
64
|
+
stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"]
|
65
|
+
await session.on_displayed(
|
66
|
+
chat, self._xmpp_msg_id_to_legacy(session, stanza_id)
|
67
|
+
)
|
@@ -0,0 +1,397 @@
|
|
1
|
+
import logging
|
2
|
+
from copy import copy
|
3
|
+
from xml.etree import ElementTree
|
4
|
+
|
5
|
+
from slixmpp import JID, Message
|
6
|
+
from slixmpp.exceptions import XMPPError
|
7
|
+
|
8
|
+
from ....contact.contact import LegacyContact
|
9
|
+
from ....group.participant import LegacyParticipant
|
10
|
+
from ....group.room import LegacyMUC
|
11
|
+
from ....util.types import LinkPreview, Recipient
|
12
|
+
from ....util.util import dict_to_named_tuple, remove_emoji_variation_selector_16
|
13
|
+
from ... import config
|
14
|
+
from ...session import BaseSession
|
15
|
+
from ..util import DispatcherMixin, exceptions_to_xmpp_errors
|
16
|
+
|
17
|
+
|
18
|
+
class MessageContentMixin(DispatcherMixin):
|
19
|
+
def __init__(self, xmpp):
|
20
|
+
super().__init__(xmpp)
|
21
|
+
xmpp.add_event_handler("legacy_message", self.on_legacy_message)
|
22
|
+
xmpp.add_event_handler("message_correction", self.on_message_correction)
|
23
|
+
xmpp.add_event_handler("message_retract", self.on_message_retract)
|
24
|
+
xmpp.add_event_handler("groupchat_message", self.on_groupchat_message)
|
25
|
+
xmpp.add_event_handler("reactions", self.on_reactions)
|
26
|
+
|
27
|
+
async def on_groupchat_message(self, msg: Message) -> None:
|
28
|
+
await self.on_legacy_message(msg)
|
29
|
+
|
30
|
+
@exceptions_to_xmpp_errors
|
31
|
+
async def on_legacy_message(self, msg: Message):
|
32
|
+
"""
|
33
|
+
Meant to be called from :class:`BaseGateway` only.
|
34
|
+
|
35
|
+
:param msg:
|
36
|
+
:return:
|
37
|
+
"""
|
38
|
+
# we MUST not use `if m["replace"]["id"]` because it adds the tag if not
|
39
|
+
# present. this is a problem for MUC echoed messages
|
40
|
+
if msg.get_plugin("replace", check=True) is not None:
|
41
|
+
# ignore last message correction (handled by a specific method)
|
42
|
+
return
|
43
|
+
if msg.get_plugin("apply_to", check=True) is not None:
|
44
|
+
# ignore message retraction (handled by a specific method)
|
45
|
+
return
|
46
|
+
if msg.get_plugin("reactions", check=True) is not None:
|
47
|
+
# ignore message reaction fallback.
|
48
|
+
# the reaction itself is handled by self.react_from_msg().
|
49
|
+
return
|
50
|
+
if msg.get_plugin("retract", check=True) is not None:
|
51
|
+
# ignore message retraction fallback.
|
52
|
+
# the retraction itself is handled by self.on_retract
|
53
|
+
return
|
54
|
+
cid = None
|
55
|
+
if msg.get_plugin("html", check=True) is not None:
|
56
|
+
body = ElementTree.fromstring("<body>" + msg["html"].get_body() + "</body>")
|
57
|
+
p = body.findall("p")
|
58
|
+
if p is not None and len(p) == 1:
|
59
|
+
if p[0].text is None or not p[0].text.strip():
|
60
|
+
images = p[0].findall("img")
|
61
|
+
if len(images) == 1:
|
62
|
+
# no text, single img ⇒ this is a sticker
|
63
|
+
# other cases should be interpreted as "custom emojis" in text
|
64
|
+
src = images[0].get("src")
|
65
|
+
if src is not None and src.startswith("cid:"):
|
66
|
+
cid = src.removeprefix("cid:")
|
67
|
+
|
68
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
69
|
+
|
70
|
+
if msg.get_plugin("oob", check=True) is not None:
|
71
|
+
url = msg["oob"]["url"]
|
72
|
+
else:
|
73
|
+
url = None
|
74
|
+
|
75
|
+
if msg.get_plugin("reply", check=True):
|
76
|
+
text, reply_to_msg_id, reply_to, reply_fallback = await self.__get_reply(
|
77
|
+
msg, session, entity
|
78
|
+
)
|
79
|
+
else:
|
80
|
+
text = msg["body"]
|
81
|
+
reply_to_msg_id = None
|
82
|
+
reply_to = None
|
83
|
+
reply_fallback = None
|
84
|
+
|
85
|
+
if msg.get_plugin("link_previews", check=True):
|
86
|
+
link_previews = [
|
87
|
+
dict_to_named_tuple(p, LinkPreview) for p in msg["link_previews"]
|
88
|
+
]
|
89
|
+
else:
|
90
|
+
link_previews = []
|
91
|
+
|
92
|
+
if url:
|
93
|
+
legacy_msg_id = await self.__send_url(
|
94
|
+
url,
|
95
|
+
session,
|
96
|
+
entity,
|
97
|
+
reply_to_msg_id=reply_to_msg_id,
|
98
|
+
reply_to_fallback_text=reply_fallback,
|
99
|
+
reply_to=reply_to,
|
100
|
+
thread=thread,
|
101
|
+
)
|
102
|
+
elif cid:
|
103
|
+
legacy_msg_id = await self.__send_bob(
|
104
|
+
msg.get_from(),
|
105
|
+
cid,
|
106
|
+
session,
|
107
|
+
entity,
|
108
|
+
reply_to_msg_id=reply_to_msg_id,
|
109
|
+
reply_to_fallback_text=reply_fallback,
|
110
|
+
reply_to=reply_to,
|
111
|
+
thread=thread,
|
112
|
+
)
|
113
|
+
elif text:
|
114
|
+
if isinstance(entity, LegacyMUC):
|
115
|
+
mentions = {"mentions": await entity.parse_mentions(text)}
|
116
|
+
else:
|
117
|
+
mentions = {}
|
118
|
+
legacy_msg_id = await session.on_text(
|
119
|
+
entity,
|
120
|
+
text,
|
121
|
+
reply_to_msg_id=reply_to_msg_id,
|
122
|
+
reply_to_fallback_text=reply_fallback,
|
123
|
+
reply_to=reply_to,
|
124
|
+
thread=thread,
|
125
|
+
link_previews=link_previews,
|
126
|
+
**mentions,
|
127
|
+
)
|
128
|
+
else:
|
129
|
+
log.debug("Ignoring %s", msg.get_id())
|
130
|
+
return
|
131
|
+
|
132
|
+
if isinstance(entity, LegacyMUC):
|
133
|
+
await entity.echo(msg, legacy_msg_id)
|
134
|
+
if legacy_msg_id is not None:
|
135
|
+
self.xmpp.store.sent.set_group_message(
|
136
|
+
session.user_pk, str(legacy_msg_id), msg.get_id()
|
137
|
+
)
|
138
|
+
else:
|
139
|
+
self.__ack(msg)
|
140
|
+
if legacy_msg_id is not None:
|
141
|
+
self.xmpp.store.sent.set_message(
|
142
|
+
session.user_pk, str(legacy_msg_id), msg.get_id()
|
143
|
+
)
|
144
|
+
if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]):
|
145
|
+
self.xmpp.store.sent.set_thread(
|
146
|
+
session.user_pk, t, str(legacy_msg_id)
|
147
|
+
)
|
148
|
+
|
149
|
+
@exceptions_to_xmpp_errors
|
150
|
+
async def on_message_correction(self, msg: Message):
|
151
|
+
if msg.get_plugin("retract", check=True) is not None:
|
152
|
+
# ignore message retraction fallback (fallback=last msg correction)
|
153
|
+
return
|
154
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
155
|
+
xmpp_id = msg["replace"]["id"]
|
156
|
+
if isinstance(entity, LegacyMUC):
|
157
|
+
legacy_id_str = self.xmpp.store.sent.get_group_legacy_id(
|
158
|
+
session.user_pk, xmpp_id
|
159
|
+
)
|
160
|
+
if legacy_id_str is None:
|
161
|
+
legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
|
162
|
+
else:
|
163
|
+
legacy_id = self.xmpp.LEGACY_MSG_ID_TYPE(legacy_id_str)
|
164
|
+
else:
|
165
|
+
legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
|
166
|
+
|
167
|
+
if isinstance(entity, LegacyMUC):
|
168
|
+
mentions = await entity.parse_mentions(msg["body"])
|
169
|
+
else:
|
170
|
+
mentions = None
|
171
|
+
|
172
|
+
if previews := msg["link_previews"]:
|
173
|
+
link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews]
|
174
|
+
else:
|
175
|
+
link_previews = []
|
176
|
+
|
177
|
+
if legacy_id is None:
|
178
|
+
log.debug("Did not find legacy ID to correct")
|
179
|
+
new_legacy_msg_id = await session.on_text(
|
180
|
+
entity,
|
181
|
+
"Correction:" + msg["body"],
|
182
|
+
thread=thread,
|
183
|
+
mentions=mentions,
|
184
|
+
link_previews=link_previews,
|
185
|
+
)
|
186
|
+
elif (
|
187
|
+
not msg["body"].strip()
|
188
|
+
and config.CORRECTION_EMPTY_BODY_AS_RETRACTION
|
189
|
+
and entity.RETRACTION
|
190
|
+
):
|
191
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
192
|
+
new_legacy_msg_id = None
|
193
|
+
elif entity.CORRECTION:
|
194
|
+
new_legacy_msg_id = await session.on_correct(
|
195
|
+
entity,
|
196
|
+
msg["body"],
|
197
|
+
legacy_id,
|
198
|
+
thread=thread,
|
199
|
+
mentions=mentions,
|
200
|
+
link_previews=link_previews,
|
201
|
+
)
|
202
|
+
else:
|
203
|
+
session.send_gateway_message(
|
204
|
+
"Last message correction is not supported by this legacy service. "
|
205
|
+
"Slidge will send your correction as new message."
|
206
|
+
)
|
207
|
+
if (
|
208
|
+
config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND
|
209
|
+
and entity.RETRACTION
|
210
|
+
and legacy_id is not None
|
211
|
+
):
|
212
|
+
if legacy_id is not None:
|
213
|
+
session.send_gateway_message(
|
214
|
+
"Slidge will attempt to retract the original message you wanted"
|
215
|
+
" to edit."
|
216
|
+
)
|
217
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
218
|
+
|
219
|
+
new_legacy_msg_id = await session.on_text(
|
220
|
+
entity,
|
221
|
+
"Correction: " + msg["body"],
|
222
|
+
thread=thread,
|
223
|
+
mentions=mentions,
|
224
|
+
link_previews=link_previews,
|
225
|
+
)
|
226
|
+
|
227
|
+
if isinstance(entity, LegacyMUC):
|
228
|
+
if new_legacy_msg_id is not None:
|
229
|
+
self.xmpp.store.sent.set_group_message(
|
230
|
+
session.user_pk, new_legacy_msg_id, msg.get_id()
|
231
|
+
)
|
232
|
+
await entity.echo(msg, new_legacy_msg_id)
|
233
|
+
else:
|
234
|
+
self.__ack(msg)
|
235
|
+
if new_legacy_msg_id is not None:
|
236
|
+
self.xmpp.store.sent.set_message(
|
237
|
+
session.user_pk, new_legacy_msg_id, msg.get_id()
|
238
|
+
)
|
239
|
+
|
240
|
+
@exceptions_to_xmpp_errors
|
241
|
+
async def on_message_retract(self, msg: Message):
|
242
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
243
|
+
if not entity.RETRACTION:
|
244
|
+
raise XMPPError(
|
245
|
+
"bad-request",
|
246
|
+
"This legacy service does not support message retraction.",
|
247
|
+
)
|
248
|
+
xmpp_id: str = msg["retract"]["id"]
|
249
|
+
legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
|
250
|
+
if legacy_id:
|
251
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
252
|
+
if isinstance(entity, LegacyMUC):
|
253
|
+
await entity.echo(msg, None)
|
254
|
+
else:
|
255
|
+
log.debug("Ignored retraction from user")
|
256
|
+
self.__ack(msg)
|
257
|
+
|
258
|
+
@exceptions_to_xmpp_errors
|
259
|
+
async def on_reactions(self, msg: Message):
|
260
|
+
session, entity, thread = await self._get_session_entity_thread(msg)
|
261
|
+
react_to: str = msg["reactions"]["id"]
|
262
|
+
|
263
|
+
special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith(
|
264
|
+
session.SPECIAL_MSG_ID_PREFIX
|
265
|
+
)
|
266
|
+
|
267
|
+
if special_msg:
|
268
|
+
legacy_id = react_to
|
269
|
+
else:
|
270
|
+
legacy_id = self._xmpp_msg_id_to_legacy(session, react_to)
|
271
|
+
|
272
|
+
if not legacy_id:
|
273
|
+
log.debug("Ignored reaction from user")
|
274
|
+
raise XMPPError(
|
275
|
+
"internal-server-error",
|
276
|
+
"Could not convert the XMPP msg ID to a legacy ID",
|
277
|
+
)
|
278
|
+
|
279
|
+
emojis = [
|
280
|
+
remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"]
|
281
|
+
]
|
282
|
+
error_msg = None
|
283
|
+
entity = entity
|
284
|
+
|
285
|
+
if not special_msg:
|
286
|
+
if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
|
287
|
+
error_msg = "Maximum 1 emoji/message"
|
288
|
+
|
289
|
+
if not error_msg and (subset := await entity.available_emojis(legacy_id)):
|
290
|
+
if not set(emojis).issubset(subset):
|
291
|
+
error_msg = f"You can only react with the following emojis: {''.join(subset)}"
|
292
|
+
|
293
|
+
if error_msg:
|
294
|
+
session.send_gateway_message(error_msg)
|
295
|
+
if not isinstance(entity, LegacyMUC):
|
296
|
+
# no need to carbon for groups, we just don't echo the stanza
|
297
|
+
entity.react(legacy_id, carbon=True) # type: ignore
|
298
|
+
await session.on_react(entity, legacy_id, [], thread=thread)
|
299
|
+
raise XMPPError("not-acceptable", text=error_msg)
|
300
|
+
|
301
|
+
await session.on_react(entity, legacy_id, emojis, thread=thread)
|
302
|
+
if isinstance(entity, LegacyMUC):
|
303
|
+
await entity.echo(msg, None)
|
304
|
+
else:
|
305
|
+
self.__ack(msg)
|
306
|
+
|
307
|
+
multi = self.xmpp.store.multi.get_xmpp_ids(session.user_pk, react_to)
|
308
|
+
if not multi:
|
309
|
+
return
|
310
|
+
multi = [m for m in multi if react_to != m]
|
311
|
+
|
312
|
+
if isinstance(entity, LegacyMUC):
|
313
|
+
for xmpp_id in multi:
|
314
|
+
mc = copy(msg)
|
315
|
+
mc["reactions"]["id"] = xmpp_id
|
316
|
+
await entity.echo(mc)
|
317
|
+
elif isinstance(entity, LegacyContact):
|
318
|
+
for xmpp_id in multi:
|
319
|
+
entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True)
|
320
|
+
|
321
|
+
def __ack(self, msg: Message):
|
322
|
+
if not self.xmpp.PROPER_RECEIPTS:
|
323
|
+
self.xmpp.delivery_receipt.ack(msg)
|
324
|
+
|
325
|
+
async def __get_reply(
|
326
|
+
self, msg: Message, session: BaseSession, entity: Recipient
|
327
|
+
) -> tuple[
|
328
|
+
str, str | int | None, LegacyContact | LegacyParticipant | None, str | None
|
329
|
+
]:
|
330
|
+
try:
|
331
|
+
reply_to_msg_id = self._xmpp_msg_id_to_legacy(session, msg["reply"]["id"])
|
332
|
+
except XMPPError:
|
333
|
+
session.log.debug(
|
334
|
+
"Could not determine reply-to legacy msg ID, sending quote instead."
|
335
|
+
)
|
336
|
+
return msg["body"], None, None, None
|
337
|
+
|
338
|
+
reply_to_jid = JID(msg["reply"]["to"])
|
339
|
+
reply_to = None
|
340
|
+
if msg["type"] == "chat":
|
341
|
+
if reply_to_jid.bare != session.user_jid.bare:
|
342
|
+
try:
|
343
|
+
reply_to = await session.contacts.by_jid(reply_to_jid)
|
344
|
+
except XMPPError:
|
345
|
+
pass
|
346
|
+
elif msg["type"] == "groupchat":
|
347
|
+
nick = reply_to_jid.resource
|
348
|
+
try:
|
349
|
+
muc = await session.bookmarks.by_jid(reply_to_jid)
|
350
|
+
except XMPPError:
|
351
|
+
pass
|
352
|
+
else:
|
353
|
+
if nick != muc.user_nick:
|
354
|
+
reply_to = await muc.get_participant(
|
355
|
+
reply_to_jid.resource, store=False
|
356
|
+
)
|
357
|
+
|
358
|
+
if msg.get_plugin("fallback", check=True) and (
|
359
|
+
isinstance(entity, LegacyMUC) or entity.REPLIES
|
360
|
+
):
|
361
|
+
text = msg["fallback"].get_stripped_body(self.xmpp["xep_0461"].namespace)
|
362
|
+
try:
|
363
|
+
reply_fallback = msg["reply"].get_fallback_body()
|
364
|
+
except AttributeError:
|
365
|
+
reply_fallback = None
|
366
|
+
else:
|
367
|
+
text = msg["body"]
|
368
|
+
reply_fallback = None
|
369
|
+
|
370
|
+
return text, reply_to_msg_id, reply_to, reply_fallback
|
371
|
+
|
372
|
+
async def __send_url(
|
373
|
+
self, url: str, session: BaseSession, entity: Recipient, **kwargs
|
374
|
+
) -> int | str | None:
|
375
|
+
async with self.xmpp.http.get(url) as response:
|
376
|
+
if response.status >= 400:
|
377
|
+
session.log.warning(
|
378
|
+
"OOB url cannot be downloaded: %s, sending the URL as text"
|
379
|
+
" instead.",
|
380
|
+
response,
|
381
|
+
)
|
382
|
+
return await session.on_text(entity, url, **kwargs)
|
383
|
+
|
384
|
+
return await session.on_file(entity, url, http_response=response, **kwargs)
|
385
|
+
|
386
|
+
async def __send_bob(
|
387
|
+
self, from_: JID, cid: str, session: BaseSession, entity: Recipient, **kwargs
|
388
|
+
) -> int | str | None:
|
389
|
+
sticker = self.xmpp.store.bob.get_sticker(cid)
|
390
|
+
if sticker is None:
|
391
|
+
await self.xmpp.plugin["xep_0231"].get_bob(from_, cid)
|
392
|
+
sticker = self.xmpp.store.bob.get_sticker(cid)
|
393
|
+
assert sticker is not None
|
394
|
+
return await session.on_sticker(entity, sticker, **kwargs)
|
395
|
+
|
396
|
+
|
397
|
+
log = logging.getLogger(__name__)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from .admin import MucAdminMixin
|
2
|
+
from .mam import MamMixin
|
3
|
+
from .misc import MucMiscMixin
|
4
|
+
from .owner import MucOwnerMixin
|
5
|
+
from .ping import PingMixin
|
6
|
+
|
7
|
+
|
8
|
+
class MucMixin(PingMixin, MamMixin, MucAdminMixin, MucOwnerMixin, MucMiscMixin):
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
__all__ = ("MucMixin",)
|