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