Coverage for slidge/core/dispatcher/presence.py: 83%
133 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
1import logging
3from slixmpp import JID, Presence
4from slixmpp.exceptions import XMPPError
6from ...contact.roster import ContactIsUser
7from ...util.types import AnyBaseSession
8from ...util.util import merge_resources
9from ..session import BaseSession
10from .util import DispatcherMixin, exceptions_to_xmpp_errors
13class _IsDirectedAtComponent(Exception):
14 def __init__(self, session: BaseSession) -> None:
15 self.session = session
18class PresenceHandlerMixin(DispatcherMixin):
19 __slots__: list[str] = []
21 def __init__(self, xmpp) -> None:
22 super().__init__(xmpp)
24 xmpp.add_event_handler("presence_subscribe", self._handle_subscribe)
25 xmpp.add_event_handler("presence_subscribed", self._handle_subscribed)
26 xmpp.add_event_handler("presence_unsubscribe", self._handle_unsubscribe)
27 xmpp.add_event_handler("presence_unsubscribed", self._handle_unsubscribed)
28 xmpp.add_event_handler("presence_probe", self._handle_probe)
29 xmpp.add_event_handler("presence", self.on_presence)
31 async def __get_contact(self, pres: Presence):
32 sess = await self._get_session(pres)
33 pto = pres.get_to()
34 if pto == self.xmpp.boundjid.bare:
35 raise _IsDirectedAtComponent(sess)
36 await sess.contacts.ready
37 return await sess.contacts.by_jid(pto)
39 @exceptions_to_xmpp_errors
40 async def _handle_subscribe(self, pres: Presence) -> None:
41 try:
42 contact = await self.__get_contact(pres)
43 except _IsDirectedAtComponent:
44 pres.reply().send()
45 return
47 if contact.is_friend:
48 pres.reply().send()
49 else:
50 await contact.on_friend_request(pres["status"])
52 @exceptions_to_xmpp_errors
53 async def _handle_unsubscribe(self, pres: Presence) -> None:
54 pres.reply().send()
56 try:
57 contact = await self.__get_contact(pres)
58 except _IsDirectedAtComponent as e:
59 e.session.send_gateway_message("Bye bye!")
60 await e.session.kill_by_jid(e.session.user_jid)
61 return
63 contact.is_friend = False
64 await contact.on_friend_delete(pres["status"])
66 @exceptions_to_xmpp_errors
67 async def _handle_subscribed(self, pres: Presence) -> None:
68 try:
69 contact = await self.__get_contact(pres)
70 except _IsDirectedAtComponent:
71 return
73 await contact.on_friend_accept()
74 contact.send_last_presence(force=True)
76 @exceptions_to_xmpp_errors
77 async def _handle_unsubscribed(self, pres: Presence) -> None:
78 try:
79 contact = await self.__get_contact(pres)
80 except _IsDirectedAtComponent:
81 return
83 if contact.is_friend:
84 contact.is_friend = False
85 await contact.on_friend_delete(pres["status"])
87 @exceptions_to_xmpp_errors
88 async def _handle_probe(self, pres: Presence) -> None:
89 try:
90 contact = await self.__get_contact(pres)
91 except _IsDirectedAtComponent:
92 session = await self._get_session(pres)
93 session.send_cached_presence(pres.get_from())
94 return
95 if contact.is_friend:
96 contact.send_last_presence(force=True)
97 else:
98 reply = pres.reply()
99 reply["type"] = "unsubscribed"
100 reply.send()
102 @exceptions_to_xmpp_errors
103 async def on_presence(self, p: Presence) -> None:
104 if p.get_plugin("muc_join", check=True):
105 # handled in on_groupchat_join
106 # without this early return, since we switch from and to in this
107 # presence stanza, on_groupchat_join ends up trying to instantiate
108 # a MUC with the user's JID, which in turn leads to slidge sending
109 # a (error) presence from=the user's JID, which terminates the
110 # XML stream.
111 return
113 session = await self._get_session(p)
115 pto = p.get_to()
116 if pto == self.xmpp.boundjid.bare:
117 await self._on_presence_to_component(session, p)
118 return
120 if p.get_type() == "available":
121 try:
122 contact = await session.contacts.by_jid(pto)
123 except XMPPError:
124 contact = None
125 except ContactIsUser:
126 raise XMPPError(
127 "bad-request", "Actions with yourself are not supported."
128 )
129 if contact is not None:
130 await self.xmpp.pubsub.on_presence_available(p, contact)
131 return
133 muc = session.bookmarks.by_jid_only_if_exists(JID(pto.bare))
135 if muc is not None and p.get_type() == "unavailable":
136 return muc.on_presence_unavailable(p)
138 if muc is None or p.get_from().resource not in muc.get_user_resources():
139 return
141 if pto.resource == muc.user_nick:
142 # Ignore presence stanzas with the valid nick.
143 # even if joined to the group, we might receive those from clients,
144 # when setting a status message, or going away, etc.
145 return
147 # We can't use XMPPError here because XMPPError does not have a way to
148 # add the <x xmlns="http://jabber.org/protocol/muc" /> element
150 error_stanza = p.error()
151 error_stanza.set_to(p.get_from())
152 error_stanza.set_from(pto)
153 error_stanza.enable("muc_join") # <x xmlns="http://jabber.org/protocol/muc" />
154 error_stanza.enable("error")
155 error_stanza["error"]["type"] = "cancel"
156 error_stanza["error"]["by"] = muc.jid
157 error_stanza["error"]["condition"] = "not-acceptable"
158 error_stanza["error"]["text"] = (
159 "Slidge does not let you change your nickname in groups."
160 )
161 error_stanza.send()
163 async def _on_presence_to_component(
164 self, session: AnyBaseSession, p: Presence
165 ) -> None:
166 session.log.debug("Received a presence from %s", p.get_from())
167 if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
168 return
169 if not session.user.preferences.get("sync_presence", False):
170 session.log.debug("User does not want to sync their presence")
171 return
172 # NB: get_type() returns either a proper presence type or
173 # a presence show if available. Weird, weird, weird slix.
174 resources = self.xmpp.roster[self.xmpp.boundjid.bare][p.get_from()].resources
175 try:
176 await session.on_presence(
177 p.get_from().resource,
178 ptype, # type: ignore
179 p["status"],
180 resources,
181 merge_resources(resources),
182 )
183 except NotImplementedError:
184 pass
185 if p.get_type() == "available":
186 await self.xmpp.pubsub.on_presence_available(p, None)
187 for contact in session.contacts:
188 await self.xmpp.pubsub.on_presence_available(p, contact)
191_USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
193log = logging.getLogger(__name__)