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

1import logging 

2 

3from slixmpp import JID, Presence 

4from slixmpp.exceptions import XMPPError 

5 

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 

11 

12 

13class _IsDirectedAtComponent(Exception): 

14 def __init__(self, session: BaseSession) -> None: 

15 self.session = session 

16 

17 

18class PresenceHandlerMixin(DispatcherMixin): 

19 __slots__: list[str] = [] 

20 

21 def __init__(self, xmpp) -> None: 

22 super().__init__(xmpp) 

23 

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) 

30 

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) 

38 

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 

46 

47 if contact.is_friend: 

48 pres.reply().send() 

49 else: 

50 await contact.on_friend_request(pres["status"]) 

51 

52 @exceptions_to_xmpp_errors 

53 async def _handle_unsubscribe(self, pres: Presence) -> None: 

54 pres.reply().send() 

55 

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 

62 

63 contact.is_friend = False 

64 await contact.on_friend_delete(pres["status"]) 

65 

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 

72 

73 await contact.on_friend_accept() 

74 contact.send_last_presence(force=True) 

75 

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 

82 

83 if contact.is_friend: 

84 contact.is_friend = False 

85 await contact.on_friend_delete(pres["status"]) 

86 

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() 

101 

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 

112 

113 session = await self._get_session(p) 

114 

115 pto = p.get_to() 

116 if pto == self.xmpp.boundjid.bare: 

117 await self._on_presence_to_component(session, p) 

118 return 

119 

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 

132 

133 muc = session.bookmarks.by_jid_only_if_exists(JID(pto.bare)) 

134 

135 if muc is not None and p.get_type() == "unavailable": 

136 return muc.on_presence_unavailable(p) 

137 

138 if muc is None or p.get_from().resource not in muc.get_user_resources(): 

139 return 

140 

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 

146 

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 

149 

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() 

162 

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) 

189 

190 

191_USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"} 

192 

193log = logging.getLogger(__name__)