slidge 0.2.0a8__py3-none-any.whl → 0.2.0a10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. slidge/__version__.py +1 -1
  2. slidge/command/adhoc.py +1 -1
  3. slidge/command/base.py +4 -4
  4. slidge/contact/contact.py +3 -2
  5. slidge/contact/roster.py +7 -0
  6. slidge/core/dispatcher/__init__.py +3 -0
  7. slidge/core/{gateway → dispatcher}/caps.py +6 -4
  8. slidge/core/{gateway → dispatcher}/disco.py +11 -17
  9. slidge/core/dispatcher/message/__init__.py +10 -0
  10. slidge/core/dispatcher/message/chat_state.py +40 -0
  11. slidge/core/dispatcher/message/marker.py +67 -0
  12. slidge/core/dispatcher/message/message.py +397 -0
  13. slidge/core/dispatcher/muc/__init__.py +12 -0
  14. slidge/core/dispatcher/muc/admin.py +98 -0
  15. slidge/core/{gateway → dispatcher/muc}/mam.py +26 -15
  16. slidge/core/dispatcher/muc/misc.py +118 -0
  17. slidge/core/dispatcher/muc/owner.py +96 -0
  18. slidge/core/{gateway → dispatcher/muc}/ping.py +10 -15
  19. slidge/core/dispatcher/presence.py +177 -0
  20. slidge/core/{gateway → dispatcher}/registration.py +23 -2
  21. slidge/core/{gateway → dispatcher}/search.py +9 -14
  22. slidge/core/dispatcher/session_dispatcher.py +84 -0
  23. slidge/core/dispatcher/util.py +174 -0
  24. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +26 -12
  25. slidge/core/{gateway/base.py → gateway.py} +42 -137
  26. slidge/core/mixins/attachment.py +7 -2
  27. slidge/core/mixins/base.py +2 -2
  28. slidge/core/mixins/message.py +10 -4
  29. slidge/core/pubsub.py +2 -1
  30. slidge/core/session.py +28 -2
  31. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  32. slidge/db/models.py +13 -0
  33. slidge/db/store.py +128 -2
  34. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  35. slidge/util/test.py +5 -1
  36. slidge/util/types.py +6 -0
  37. slidge/util/util.py +5 -2
  38. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/METADATA +2 -1
  39. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/RECORD +42 -33
  40. slidge/core/gateway/__init__.py +0 -3
  41. slidge/core/gateway/muc_admin.py +0 -35
  42. slidge/core/gateway/presence.py +0 -95
  43. slidge/core/gateway/session_dispatcher.py +0 -895
  44. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/LICENSE +0 -0
  45. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/WHEEL +0 -0
  46. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/entry_points.txt +0 -0
@@ -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",)
@@ -0,0 +1,98 @@
1
+ from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
2
+ from slixmpp.exceptions import XMPPError
3
+ from slixmpp.xmlstream import StanzaBase
4
+
5
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
6
+
7
+
8
+ class MucAdminMixin(DispatcherMixin):
9
+ def __init__(self, xmpp) -> None:
10
+ super().__init__(xmpp)
11
+ self.xmpp.register_handler(
12
+ CoroutineCallback(
13
+ "MUCModerate",
14
+ StanzaPath("iq/apply_to/moderate"),
15
+ self.on_user_moderation,
16
+ )
17
+ )
18
+ self.xmpp.register_handler(
19
+ CoroutineCallback(
20
+ "MUCSetAffiliation",
21
+ StanzaPath("iq@type=set/mucadmin_query"),
22
+ self.on_user_set_affiliation,
23
+ )
24
+ )
25
+ self.xmpp.register_handler(
26
+ CoroutineCallback(
27
+ "MUCGetAffiliation",
28
+ StanzaPath("iq@type=get/mucadmin_query"),
29
+ self.on_muc_admin_query_get,
30
+ )
31
+ )
32
+
33
+ @exceptions_to_xmpp_errors
34
+ async def on_user_moderation(self, iq: StanzaBase) -> None:
35
+ assert isinstance(iq, Iq)
36
+ muc = await self.get_muc_from_stanza(iq)
37
+
38
+ apply_to = iq["apply_to"]
39
+ xmpp_id = apply_to["id"]
40
+ if not xmpp_id:
41
+ raise XMPPError("bad-request", "Missing moderated message ID")
42
+
43
+ moderate = apply_to["moderate"]
44
+ if not moderate["retract"]:
45
+ raise XMPPError(
46
+ "feature-not-implemented",
47
+ "Slidge only implements moderation/retraction",
48
+ )
49
+
50
+ legacy_id = self._xmpp_msg_id_to_legacy(muc.session, xmpp_id)
51
+ await muc.session.on_moderate(muc, legacy_id, moderate["reason"] or None)
52
+ iq.reply(clear=True).send()
53
+
54
+ @exceptions_to_xmpp_errors
55
+ async def on_user_set_affiliation(self, iq: StanzaBase) -> None:
56
+ assert isinstance(iq, Iq)
57
+ muc = await self.get_muc_from_stanza(iq)
58
+
59
+ item = iq["mucadmin_query"]["item"]
60
+ if item["jid"]:
61
+ contact = await muc.session.contacts.by_jid(JID(item["jid"]))
62
+ else:
63
+ part = await muc.get_participant(
64
+ item["nick"], fill_first=True, raise_if_not_found=True
65
+ )
66
+ assert part.contact is not None
67
+ contact = part.contact
68
+
69
+ if item["affiliation"]:
70
+ await muc.on_set_affiliation(
71
+ contact,
72
+ item["affiliation"],
73
+ item["reason"] or None,
74
+ item["nick"] or None,
75
+ )
76
+ elif item["role"] == "none":
77
+ await muc.on_kick(contact, item["reason"] or None)
78
+
79
+ iq.reply(clear=True).send()
80
+
81
+ @exceptions_to_xmpp_errors
82
+ async def on_muc_admin_query_get(self, iq: StanzaBase) -> None:
83
+ assert isinstance(iq, Iq)
84
+ affiliation = iq["mucadmin_query"]["item"]["affiliation"]
85
+
86
+ if not affiliation:
87
+ raise XMPPError("bad-request")
88
+
89
+ session = await self._get_session(iq, 1, logged=True)
90
+ muc = await session.bookmarks.by_jid(iq.get_to())
91
+
92
+ reply = iq.reply()
93
+ reply.enable("mucadmin_query")
94
+ async for participant in muc.get_participants():
95
+ if not participant.affiliation == affiliation:
96
+ continue
97
+ reply["mucadmin_query"].append(participant.mucadmin_item())
98
+ reply.send()
@@ -1,42 +1,57 @@
1
+ import asyncio
1
2
  from typing import TYPE_CHECKING
2
3
 
3
4
  from slixmpp import CoroutineCallback, Iq, StanzaPath
4
5
  from slixmpp.exceptions import XMPPError
6
+ from slixmpp.xmlstream import StanzaBase
7
+
8
+ from ... import config
9
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
5
10
 
6
11
  if TYPE_CHECKING:
7
- from .base import BaseGateway
12
+ from slidge.core.gateway import BaseGateway
8
13
 
9
14
 
10
- class Mam:
15
+ class MamMixin(DispatcherMixin):
11
16
  def __init__(self, xmpp: "BaseGateway"):
12
- self.xmpp = xmpp
17
+ super().__init__(xmpp)
18
+ self.__mam_cleanup_task = xmpp.loop.create_task(self.__mam_cleanup())
13
19
  xmpp.register_handler(
14
20
  CoroutineCallback(
15
21
  "MAM_query",
16
22
  StanzaPath("iq@type=set/mam"),
17
- self.__handle_mam, # type:ignore
23
+ self.__handle_mam,
18
24
  )
19
25
  )
20
26
  xmpp.register_handler(
21
27
  CoroutineCallback(
22
28
  "MAM_get_from",
23
29
  StanzaPath("iq@type=get/mam"),
24
- self.__handle_mam_get_form, # type:ignore
30
+ self.__handle_mam_get_form,
25
31
  )
26
32
  )
27
33
  xmpp.register_handler(
28
34
  CoroutineCallback(
29
35
  "MAM_get_meta",
30
36
  StanzaPath("iq@type=get/mam_metadata"),
31
- self.__handle_mam_metadata, # type:ignore
37
+ self.__handle_mam_metadata,
32
38
  )
33
39
  )
34
40
 
41
+ async def __mam_cleanup(self):
42
+ if not config.MAM_MAX_DAYS:
43
+ return
44
+ while True:
45
+ await asyncio.sleep(3600 * 6)
46
+ self.xmpp.store.mam.nuke_older_than(config.MAM_MAX_DAYS)
47
+
48
+ @exceptions_to_xmpp_errors
35
49
  async def __handle_mam(self, iq: Iq):
36
- muc = await self.xmpp.get_muc_from_stanza(iq)
50
+ muc = await self.get_muc_from_stanza(iq)
37
51
  await muc.send_mam(iq)
38
52
 
39
- async def __handle_mam_get_form(self, iq: Iq):
53
+ async def __handle_mam_get_form(self, iq: StanzaBase):
54
+ assert isinstance(iq, Iq)
40
55
  ito = iq.get_to()
41
56
 
42
57
  if ito == self.xmpp.boundjid.bare:
@@ -44,12 +59,7 @@ class Mam:
44
59
  text="No MAM on the component itself, use a JID with a resource"
45
60
  )
46
61
 
47
- user = self.xmpp.store.users.get(iq.get_from())
48
- if user is None:
49
- raise XMPPError("registration-required")
50
-
51
- session = self.xmpp.get_session_from_user(user)
52
-
62
+ session = await self._get_session(iq, 0, logged=True)
53
63
  await session.bookmarks.by_jid(ito)
54
64
 
55
65
  reply = iq.reply()
@@ -67,6 +77,7 @@ class Mam:
67
77
  reply["mam"].append(form)
68
78
  reply.send()
69
79
 
80
+ @exceptions_to_xmpp_errors
70
81
  async def __handle_mam_metadata(self, iq: Iq):
71
- muc = await self.xmpp.get_muc_from_stanza(iq)
82
+ muc = await self.get_muc_from_stanza(iq)
72
83
  await muc.send_mam_metadata(iq)
@@ -0,0 +1,118 @@
1
+ import logging
2
+
3
+ from slixmpp import JID, CoroutineCallback, Iq, Message, Presence, StanzaPath
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
7
+
8
+
9
+ class MucMiscMixin(DispatcherMixin):
10
+ def __init__(self, xmpp):
11
+ super().__init__(xmpp)
12
+ xmpp.register_handler(
13
+ CoroutineCallback(
14
+ "ibr_remove", StanzaPath("/iq/register"), self.on_ibr_remove
15
+ )
16
+ )
17
+
18
+ xmpp.add_event_handler("groupchat_join", self.on_groupchat_join)
19
+ xmpp.add_event_handler(
20
+ "groupchat_direct_invite", self.on_groupchat_direct_invite
21
+ )
22
+ xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject)
23
+ xmpp.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
24
+
25
+ async def __on_group_chat_error(self, msg: Message):
26
+ condition = msg["error"].get_condition()
27
+ if condition not in KICKABLE_ERRORS:
28
+ return
29
+
30
+ try:
31
+ muc = await self.get_muc_from_stanza(msg)
32
+ except XMPPError as e:
33
+ log.debug("Not removing resource", exc_info=e)
34
+ return
35
+ mfrom = msg.get_from()
36
+ resource = mfrom.resource
37
+ try:
38
+ muc.remove_user_resource(resource)
39
+ except KeyError:
40
+ # this actually happens quite frequently on for both beagle and monal
41
+ # (not sure why?), but is of no consequence
42
+ log.debug("%s was not in the resources of %s", resource, muc)
43
+ else:
44
+ log.info(
45
+ "Removed %s from the resources of %s because of error", resource, muc
46
+ )
47
+
48
+ @exceptions_to_xmpp_errors
49
+ async def on_ibr_remove(self, iq: Iq):
50
+ if iq.get_to() == self.xmpp.boundjid.bare:
51
+ return
52
+
53
+ if iq["type"] == "set" and iq["register"]["remove"]:
54
+ muc = await self.get_muc_from_stanza(iq)
55
+ await muc.session.on_leave_group(muc.legacy_id)
56
+ iq.reply().send()
57
+ return
58
+
59
+ raise XMPPError("feature-not-implemented")
60
+
61
+ @exceptions_to_xmpp_errors
62
+ async def on_groupchat_join(self, p: Presence):
63
+ if not self.xmpp.GROUPS:
64
+ raise XMPPError(
65
+ "feature-not-implemented",
66
+ "This gateway does not implement multi-user chats.",
67
+ )
68
+ muc = await self.get_muc_from_stanza(p)
69
+ await muc.join(p)
70
+
71
+ @exceptions_to_xmpp_errors
72
+ async def on_groupchat_direct_invite(self, msg: Message):
73
+ invite = msg["groupchat_invite"]
74
+ jid = JID(invite["jid"])
75
+
76
+ if jid.domain != self.xmpp.boundjid.bare:
77
+ raise XMPPError(
78
+ "bad-request",
79
+ "Legacy contacts can only be invited to legacy groups, not standard XMPP MUCs.",
80
+ )
81
+
82
+ if invite["password"]:
83
+ raise XMPPError(
84
+ "bad-request", "Password-protected groups are not supported"
85
+ )
86
+
87
+ session = await self._get_session(msg, logged=True)
88
+ contact = await session.contacts.by_jid(msg.get_to())
89
+ muc = await session.bookmarks.by_jid(jid)
90
+
91
+ await session.on_invitation(contact, muc, invite["reason"] or None)
92
+
93
+ @exceptions_to_xmpp_errors
94
+ async def on_groupchat_subject(self, msg: Message):
95
+ muc = await self.get_muc_from_stanza(msg)
96
+ if not muc.HAS_SUBJECT:
97
+ raise XMPPError(
98
+ "bad-request",
99
+ "There are no room subject in here. "
100
+ "Use the room configuration to update its name or description",
101
+ )
102
+ await muc.on_set_subject(msg["subject"])
103
+
104
+
105
+ KICKABLE_ERRORS = {
106
+ "gone",
107
+ "internal-server-error",
108
+ "item-not-found",
109
+ "jid-malformed",
110
+ "recipient-unavailable",
111
+ "redirect",
112
+ "remote-server-not-found",
113
+ "remote-server-timeout",
114
+ "service-unavailable",
115
+ "malformed error",
116
+ }
117
+
118
+ log = logging.getLogger(__name__)
@@ -0,0 +1,96 @@
1
+ from slixmpp import CoroutineCallback, Iq, StanzaPath
2
+ from slixmpp.exceptions import XMPPError
3
+ from slixmpp.plugins.xep_0004 import Form
4
+ from slixmpp.xmlstream import StanzaBase
5
+
6
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
7
+
8
+
9
+ class MucOwnerMixin(DispatcherMixin):
10
+ def __init__(self, xmpp):
11
+ super().__init__(xmpp)
12
+ xmpp.register_handler(
13
+ CoroutineCallback(
14
+ "MUCOwnerGet",
15
+ StanzaPath("iq@type=get/mucowner_query"),
16
+ self.on_muc_owner_query,
17
+ )
18
+ )
19
+ xmpp.register_handler(
20
+ CoroutineCallback(
21
+ "MUCOwnerSet",
22
+ StanzaPath("iq@type=set/mucowner_query"),
23
+ self.on_muc_owner_set,
24
+ )
25
+ )
26
+
27
+ @exceptions_to_xmpp_errors
28
+ async def on_muc_owner_query(self, iq: StanzaBase) -> None:
29
+ assert isinstance(iq, Iq)
30
+ muc = await self.get_muc_from_stanza(iq)
31
+
32
+ reply = iq.reply()
33
+
34
+ form = Form(title="Slidge room configuration")
35
+ form["instructions"] = (
36
+ "Complete this form to modify the configuration of your room."
37
+ )
38
+ form.add_field(
39
+ var="FORM_TYPE",
40
+ type="hidden",
41
+ value="http://jabber.org/protocol/muc#roomconfig",
42
+ )
43
+ form.add_field(
44
+ var="muc#roomconfig_roomname",
45
+ label="Natural-Language Room Name",
46
+ type="text-single",
47
+ value=muc.name,
48
+ )
49
+ if muc.HAS_DESCRIPTION:
50
+ form.add_field(
51
+ var="muc#roomconfig_roomdesc",
52
+ label="Short Description of Room",
53
+ type="text-single",
54
+ value=muc.description,
55
+ )
56
+
57
+ muc_owner = iq["mucowner_query"]
58
+ muc_owner.append(form)
59
+ reply.append(muc_owner)
60
+ reply.send()
61
+
62
+ @exceptions_to_xmpp_errors
63
+ async def on_muc_owner_set(self, iq: StanzaBase) -> None:
64
+ assert isinstance(iq, Iq)
65
+ muc = await self.get_muc_from_stanza(iq)
66
+ query = iq["mucowner_query"]
67
+
68
+ if form := query.get_plugin("form", check=True):
69
+ values = form.get_values()
70
+ await muc.on_set_config(
71
+ name=values.get("muc#roomconfig_roomname"),
72
+ description=(
73
+ values.get("muc#roomconfig_roomdesc")
74
+ if muc.HAS_DESCRIPTION
75
+ else None
76
+ ),
77
+ )
78
+ form["type"] = "result"
79
+ clear = False
80
+ elif destroy := query.get_plugin("destroy", check=True):
81
+ reason = destroy["reason"] or None
82
+ await muc.on_destroy_request(reason)
83
+ user_participant = await muc.get_user_participant()
84
+ user_participant._affiliation = "none"
85
+ user_participant._role = "none"
86
+ presence = user_participant._make_presence(ptype="unavailable", force=True)
87
+ presence["muc"].enable("destroy")
88
+ if reason is not None:
89
+ presence["muc"]["destroy"]["reason"] = reason
90
+ user_participant._send(presence)
91
+ muc.session.bookmarks.remove(muc)
92
+ clear = True
93
+ else:
94
+ raise XMPPError("bad-request")
95
+
96
+ iq.reply(clear=clear).send()
@@ -3,39 +3,34 @@ from typing import TYPE_CHECKING
3
3
  from slixmpp import CoroutineCallback, Iq, StanzaPath
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
- from ...group import LegacyMUC
6
+ from ....group import LegacyMUC
7
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
7
8
 
8
9
  if TYPE_CHECKING:
9
- from .base import BaseGateway
10
+ from slidge.core.gateway import BaseGateway
10
11
 
11
12
 
12
- class Ping:
13
+ class PingMixin(DispatcherMixin):
13
14
  def __init__(self, xmpp: "BaseGateway"):
14
- self.xmpp = xmpp
15
+ super().__init__(xmpp)
15
16
 
16
17
  xmpp.remove_handler("Ping")
17
18
  xmpp.register_handler(
18
19
  CoroutineCallback(
19
20
  "Ping",
20
21
  StanzaPath("iq@type=get/ping"),
21
- self.__handle_ping, # type:ignore
22
+ self.__handle_ping,
22
23
  )
23
24
  )
24
25
  xmpp.plugin["xep_0030"].add_feature("urn:xmpp:ping")
25
26
 
26
- async def __handle_ping(self, iq: Iq):
27
+ @exceptions_to_xmpp_errors
28
+ async def __handle_ping(self, iq: Iq) -> None:
27
29
  ito = iq.get_to()
28
-
29
30
  if ito == self.xmpp.boundjid.bare:
30
31
  iq.reply().send()
31
32
 
32
- ifrom = iq.get_from()
33
- user = self.xmpp.store.users.get(ifrom)
34
- if user is None:
35
- raise XMPPError("registration-required")
36
-
37
- session = self.xmpp.get_session_from_user(user)
38
- session.raise_if_not_logged()
33
+ session = await self._get_session(iq)
39
34
 
40
35
  try:
41
36
  muc = await session.bookmarks.by_jid(ito)
@@ -58,7 +53,7 @@ class Ping:
58
53
  )
59
54
 
60
55
  @staticmethod
61
- def __handle_muc_ping(muc: LegacyMUC, iq: Iq):
56
+ def __handle_muc_ping(muc: LegacyMUC, iq: Iq) -> None:
62
57
  if iq.get_from().resource in muc.get_user_resources():
63
58
  iq.reply().send()
64
59
  else:
@@ -0,0 +1,177 @@
1
+ import logging
2
+
3
+ from slixmpp import JID, Presence
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ from ...util.util import merge_resources
7
+ from ..session import BaseSession
8
+ from .util import DispatcherMixin, exceptions_to_xmpp_errors
9
+
10
+
11
+ class _IsDirectedAtComponent(Exception):
12
+ def __init__(self, session: BaseSession):
13
+ self.session = session
14
+
15
+
16
+ class PresenceHandlerMixin(DispatcherMixin):
17
+ def __init__(self, xmpp):
18
+ super().__init__(xmpp)
19
+
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)
26
+
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)
34
+
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
42
+
43
+ if contact.is_friend:
44
+ pres.reply().send()
45
+ else:
46
+ await contact.on_friend_request(pres["status"])
47
+
48
+ @exceptions_to_xmpp_errors
49
+ async def _handle_unsubscribe(self, pres: Presence):
50
+ pres.reply().send()
51
+
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
58
+
59
+ contact.is_friend = False
60
+ await contact.on_friend_delete(pres["status"])
61
+
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
68
+
69
+ await contact.on_friend_accept()
70
+
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
77
+
78
+ if contact.is_friend:
79
+ contact.is_friend = False
80
+ await contact.on_friend_delete(pres["status"])
81
+
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()
96
+
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
107
+
108
+ session = await self._get_session(p)
109
+
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
133
+
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
142
+
143
+ muc = session.bookmarks.by_jid_only_if_exists(JID(pto.bare))
144
+
145
+ if muc is not None and p.get_type() == "unavailable":
146
+ return muc.on_presence_unavailable(p)
147
+
148
+ if muc is None or p.get_from().resource not in muc.get_user_resources():
149
+ return
150
+
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
156
+
157
+ # We can't use XMPPError here because from must be room@slidge/VALID-USER-NICK
158
+
159
+ error_from = JID(muc.jid)
160
+ error_from.resource = muc.user_nick
161
+ error_stanza = p.error()
162
+ error_stanza.set_to(p.get_from())
163
+ error_stanza.set_from(error_from)
164
+ error_stanza.enable("muc_join")
165
+ error_stanza.enable("error")
166
+ error_stanza["error"]["type"] = "cancel"
167
+ error_stanza["error"]["by"] = muc.jid
168
+ error_stanza["error"]["condition"] = "not-acceptable"
169
+ error_stanza["error"][
170
+ "text"
171
+ ] = "Slidge does not let you change your nickname in groups."
172
+ error_stanza.send()
173
+
174
+
175
+ _USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
176
+
177
+ log = logging.getLogger(__name__)