slidge 0.1.0__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 (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,63 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING
3
+
4
+ from slixmpp import Presence
5
+ from slixmpp.exceptions import XMPPError
6
+ from slixmpp.xmlstream import StanzaBase
7
+
8
+ from ...contact import LegacyContact
9
+
10
+ if TYPE_CHECKING:
11
+ from .base import BaseGateway
12
+
13
+
14
+ class Caps:
15
+ def __init__(self, xmpp: "BaseGateway"):
16
+ self.xmpp = xmpp
17
+ xmpp.del_filter("out", xmpp.plugin["xep_0115"]._filter_add_caps)
18
+ xmpp.add_filter("out", self._filter_add_caps) # type:ignore
19
+
20
+ async def _filter_add_caps(self, stanza: StanzaBase):
21
+ # we rolled our own "add caps on presences" filter because
22
+ # there is too much magic happening in slixmpp
23
+ # anyway, we probably want to roll our own "dynamic disco"/caps
24
+ # module in the long run, so it's a step in this direction
25
+ if not isinstance(stanza, Presence):
26
+ return stanza
27
+
28
+ if stanza["type"] not in ("available", "chat", "away", "dnd", "xa"):
29
+ return stanza
30
+
31
+ pfrom = stanza.get_from()
32
+
33
+ caps = self.xmpp.plugin["xep_0115"]
34
+
35
+ if pfrom != self.xmpp.boundjid.bare:
36
+ try:
37
+ session = self.xmpp.get_session_from_jid(stanza.get_to())
38
+ except XMPPError:
39
+ log.debug("not adding caps 1")
40
+ return stanza
41
+
42
+ if session is None:
43
+ return stanza
44
+
45
+ await session.ready
46
+
47
+ entity = await session.get_contact_or_group_or_participant(pfrom)
48
+ if not isinstance(entity, LegacyContact):
49
+ return stanza
50
+ ver = await entity.get_caps_ver(pfrom)
51
+ else:
52
+ ver = await caps.get_verstring(pfrom)
53
+
54
+ log.debug("Ver: %s", ver)
55
+
56
+ if ver:
57
+ stanza["caps"]["node"] = caps.caps_node
58
+ stanza["caps"]["hash"] = caps.hash
59
+ stanza["caps"]["ver"] = ver
60
+ return stanza
61
+
62
+
63
+ log = logging.getLogger(__name__)
@@ -0,0 +1,52 @@
1
+ """
2
+ XEP-0184 Delivery Receipts
3
+
4
+ The corresponding slixmpp module is a bit too rigid, this is our implementation
5
+ to selectively choose when we send delivery receipts
6
+ """
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ from slixmpp import JID, Message
11
+ from slixmpp.types import MessageTypes
12
+
13
+ if TYPE_CHECKING:
14
+ from .base import BaseGateway
15
+
16
+
17
+ class DeliveryReceipt:
18
+ def __init__(self, xmpp: "BaseGateway"):
19
+ self.xmpp = xmpp
20
+
21
+ def ack(self, msg: Message):
22
+ """
23
+ Send a XEP-0184 (delivery receipt) in response to a message,
24
+ if appropriate.
25
+
26
+ :param msg:
27
+ """
28
+ if not self.requires_receipt(msg):
29
+ return
30
+ ack = self.make_ack(msg["id"], msg["to"], msg["from"].bare, msg["type"])
31
+ ack.send()
32
+
33
+ def make_ack(self, msg_id: str, mfrom: JID, mto: JID, mtype: MessageTypes = "chat"):
34
+ ack = self.xmpp.Message()
35
+ ack["type"] = mtype
36
+ ack["to"] = mto
37
+ ack["from"] = mfrom
38
+ ack["receipt"] = msg_id
39
+ return ack
40
+
41
+ def requires_receipt(self, msg: Message):
42
+ """
43
+ Check if a message is eligible for a delivery receipt.
44
+
45
+ :param msg:
46
+ :return:
47
+ """
48
+ return (
49
+ msg["request_receipt"]
50
+ and msg["type"] in self.xmpp.plugin["xep_0184"].ack_types
51
+ and not msg["receipt"]
52
+ )
@@ -0,0 +1,80 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Any, Optional
3
+
4
+ from slixmpp.exceptions import XMPPError
5
+ from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
6
+ from slixmpp.types import OptJid
7
+
8
+ from ...util.db import user_store
9
+
10
+ if TYPE_CHECKING:
11
+ from .base import BaseGateway
12
+
13
+
14
+ class Disco:
15
+ def __init__(self, xmpp: "BaseGateway"):
16
+ self.xmpp = xmpp
17
+
18
+ xmpp.plugin["xep_0030"].set_node_handler(
19
+ "get_info",
20
+ jid=None,
21
+ node=None,
22
+ handler=self.get_info,
23
+ )
24
+
25
+ xmpp.plugin["xep_0030"].set_node_handler(
26
+ "get_items",
27
+ jid=None,
28
+ node=None,
29
+ handler=self.get_items,
30
+ )
31
+
32
+ async def get_info(
33
+ self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any
34
+ ):
35
+ if ifrom == self.xmpp.boundjid.bare or jid in (self.xmpp.boundjid.bare, None):
36
+ return self.xmpp.plugin["xep_0030"].static.get_info(jid, node, ifrom, data)
37
+
38
+ if ifrom is None:
39
+ raise XMPPError("subscription-required")
40
+
41
+ user = user_store.get_by_jid(ifrom)
42
+ if user is None:
43
+ raise XMPPError("registration-required")
44
+ session = self.xmpp.get_session_from_user(user)
45
+ await session.wait_for_ready()
46
+
47
+ log.debug("Looking for entity: %s", jid)
48
+
49
+ assert jid is not None
50
+ entity = await session.get_contact_or_group_or_participant(jid)
51
+
52
+ if entity is None:
53
+ raise XMPPError("item-not-found")
54
+
55
+ return await entity.get_disco_info(jid, node)
56
+
57
+ async def get_items(
58
+ self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any
59
+ ):
60
+ if ifrom is None:
61
+ raise XMPPError("bad-request")
62
+
63
+ if jid != self.xmpp.boundjid.bare:
64
+ return DiscoItems()
65
+
66
+ user = user_store.get_by_jid(ifrom)
67
+ if user is None:
68
+ raise XMPPError("registration-required")
69
+
70
+ session = self.xmpp.get_session_from_user(user)
71
+ await session.wait_for_ready()
72
+
73
+ d = DiscoItems()
74
+ for muc in sorted(session.bookmarks, key=lambda m: m.name):
75
+ d.add_item(muc.jid, name=muc.name)
76
+
77
+ return d
78
+
79
+
80
+ log = logging.getLogger(__name__)
@@ -0,0 +1,75 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from slixmpp import CoroutineCallback, Iq, StanzaPath
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ from ...util.db import user_store
7
+
8
+ if TYPE_CHECKING:
9
+ from .base import BaseGateway
10
+
11
+
12
+ class Mam:
13
+ def __init__(self, xmpp: "BaseGateway"):
14
+ self.xmpp = xmpp
15
+ xmpp.register_handler(
16
+ CoroutineCallback(
17
+ "MAM_query",
18
+ StanzaPath("iq@type=set/mam"),
19
+ self.__handle_mam, # type:ignore
20
+ )
21
+ )
22
+ xmpp.register_handler(
23
+ CoroutineCallback(
24
+ "MAM_get_from",
25
+ StanzaPath("iq@type=get/mam"),
26
+ self.__handle_mam_get_form, # type:ignore
27
+ )
28
+ )
29
+ xmpp.register_handler(
30
+ CoroutineCallback(
31
+ "MAM_get_meta",
32
+ StanzaPath("iq@type=get/mam_metadata"),
33
+ self.__handle_mam_metadata, # type:ignore
34
+ )
35
+ )
36
+
37
+ async def __handle_mam(self, iq: Iq):
38
+ muc = await self.xmpp.get_muc_from_stanza(iq)
39
+ await muc.send_mam(iq)
40
+
41
+ async def __handle_mam_get_form(self, iq: Iq):
42
+ ito = iq.get_to()
43
+
44
+ if ito == self.xmpp.boundjid.bare:
45
+ raise XMPPError(
46
+ text="No MAM on the component itself, use a JID with a resource"
47
+ )
48
+
49
+ ifrom = iq.get_from()
50
+ user = user_store.get_by_jid(ifrom)
51
+ if user is None:
52
+ raise XMPPError("registration-required")
53
+
54
+ session = self.xmpp.get_session_from_user(user)
55
+
56
+ await session.bookmarks.by_jid(ito)
57
+
58
+ reply = iq.reply()
59
+ form = self.xmpp.plugin["xep_0004"].make_form()
60
+ form.add_field(ftype="hidden", var="FORM_TYPE", value="urn:xmpp:mam:2")
61
+ form.add_field(ftype="jid-single", var="with")
62
+ form.add_field(ftype="text-single", var="start")
63
+ form.add_field(ftype="text-single", var="end")
64
+ form.add_field(ftype="text-single", var="before-id")
65
+ form.add_field(ftype="text-single", var="after-id")
66
+ form.add_field(ftype="boolean", var="include-groupchat")
67
+ field = form.add_field(ftype="list-multi", var="ids")
68
+ field["validate"]["datatype"] = "xs:string"
69
+ field["validate"]["open"] = True
70
+ reply["mam"].append(form)
71
+ reply.send()
72
+
73
+ async def __handle_mam_metadata(self, iq: Iq):
74
+ muc = await self.xmpp.get_muc_from_stanza(iq)
75
+ await muc.send_mam_metadata(iq)
@@ -0,0 +1,35 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from slixmpp import CoroutineCallback, Iq, StanzaPath
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ if TYPE_CHECKING:
7
+ from .base import BaseGateway
8
+
9
+
10
+ class MucAdmin:
11
+ def __init__(self, xmpp: "BaseGateway"):
12
+ self.xmpp = xmpp
13
+ xmpp.register_handler(
14
+ CoroutineCallback(
15
+ "muc#admin",
16
+ StanzaPath("iq@type=get/mucadmin_query"),
17
+ self._handle_admin, # type: ignore
18
+ )
19
+ )
20
+
21
+ async def _handle_admin(self, iq: Iq):
22
+ muc = await self.xmpp.get_muc_from_stanza(iq)
23
+
24
+ affiliation = iq["mucadmin_query"]["item"]["affiliation"]
25
+
26
+ if not affiliation:
27
+ raise XMPPError("bad-request")
28
+
29
+ reply = iq.reply()
30
+ reply.enable("mucadmin_query")
31
+ for participant in await muc.get_participants():
32
+ if not participant.affiliation == affiliation:
33
+ continue
34
+ reply["mucadmin_query"].append(participant.mucadmin_item())
35
+ reply.send()
@@ -0,0 +1,66 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from slixmpp import CoroutineCallback, Iq, StanzaPath
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ from ...group import LegacyMUC
7
+ from ...util.db import user_store
8
+
9
+ if TYPE_CHECKING:
10
+ from .base import BaseGateway
11
+
12
+
13
+ class Ping:
14
+ def __init__(self, xmpp: "BaseGateway"):
15
+ self.xmpp = xmpp
16
+
17
+ xmpp.remove_handler("Ping")
18
+ xmpp.register_handler(
19
+ CoroutineCallback(
20
+ "Ping",
21
+ StanzaPath("iq@type=get/ping"),
22
+ self.__handle_ping, # type:ignore
23
+ )
24
+ )
25
+ xmpp.plugin["xep_0030"].add_feature("urn:xmpp:ping")
26
+
27
+ async def __handle_ping(self, iq: Iq):
28
+ ito = iq.get_to()
29
+
30
+ if ito == self.xmpp.boundjid.bare:
31
+ iq.reply().send()
32
+
33
+ ifrom = iq.get_from()
34
+ user = user_store.get_by_jid(ifrom)
35
+ if user is None:
36
+ raise XMPPError("registration-required")
37
+
38
+ session = self.xmpp.get_session_from_user(user)
39
+ session.raise_if_not_logged()
40
+
41
+ try:
42
+ muc = await session.bookmarks.by_jid(ito)
43
+ except XMPPError:
44
+ pass
45
+ else:
46
+ self.__handle_muc_ping(muc, iq)
47
+ return
48
+
49
+ try:
50
+ await session.contacts.by_jid(ito)
51
+ except XMPPError:
52
+ pass
53
+ else:
54
+ iq.reply().send()
55
+ return
56
+
57
+ raise XMPPError(
58
+ "item-not-found", f"This JID does not match anything slidge knows: {ito}"
59
+ )
60
+
61
+ @staticmethod
62
+ def __handle_muc_ping(muc: LegacyMUC, iq: Iq):
63
+ if iq.get_from().resource in muc.user_resources:
64
+ iq.reply().send()
65
+ else:
66
+ raise XMPPError("not-acceptable", etype="cancel", by=muc.jid)
@@ -0,0 +1,95 @@
1
+ import logging
2
+
3
+ from slixmpp import JID, Presence
4
+
5
+ from ..session import BaseSession
6
+
7
+
8
+ class _IsDirectedAtComponent(Exception):
9
+ def __init__(self, session: BaseSession):
10
+ self.session = session
11
+
12
+
13
+ class PresenceHandlerMixin:
14
+ boundjid: JID
15
+
16
+ def get_session_from_stanza(self, s) -> BaseSession:
17
+ raise NotImplementedError
18
+
19
+ async def __get_contact(self, pres: Presence):
20
+ sess = await self.__get_session(pres)
21
+ pto = pres.get_to()
22
+ if pto == self.boundjid.bare:
23
+ raise _IsDirectedAtComponent(sess)
24
+ await sess.contacts.ready
25
+ return await sess.contacts.by_jid(pto)
26
+
27
+ async def __get_session(self, p: Presence):
28
+ sess = self.get_session_from_stanza(p)
29
+ return sess
30
+
31
+ async def _handle_subscribe(self, pres: Presence):
32
+ try:
33
+ contact = await self.__get_contact(pres)
34
+ except _IsDirectedAtComponent:
35
+ pres.reply().send()
36
+ return
37
+
38
+ if contact.is_friend:
39
+ pres.reply().send()
40
+ else:
41
+ await contact.on_friend_request(pres["status"])
42
+
43
+ async def _handle_unsubscribe(self, pres: Presence):
44
+ pres.reply().send()
45
+
46
+ try:
47
+ contact = await self.__get_contact(pres)
48
+ except _IsDirectedAtComponent as e:
49
+ e.session.send_gateway_message("Bye bye!")
50
+ await e.session.kill_by_jid(e.session.user.jid)
51
+ return
52
+
53
+ contact.is_friend = False
54
+ await contact.on_friend_delete(pres["status"])
55
+
56
+ async def _handle_subscribed(self, pres: Presence):
57
+ try:
58
+ contact = await self.__get_contact(pres)
59
+ except _IsDirectedAtComponent:
60
+ return
61
+
62
+ await contact.on_friend_accept()
63
+
64
+ async def _handle_unsubscribed(self, pres: Presence):
65
+ try:
66
+ contact = await self.__get_contact(pres)
67
+ except _IsDirectedAtComponent:
68
+ return
69
+
70
+ if contact.is_friend:
71
+ contact.is_friend = False
72
+ await contact.on_friend_delete(pres["status"])
73
+
74
+ async def _handle_probe(self, pres: Presence):
75
+ try:
76
+ contact = await self.__get_contact(pres)
77
+ except _IsDirectedAtComponent:
78
+ session = await self.__get_session(pres)
79
+ session.send_cached_presence(pres.get_from())
80
+ return
81
+ if contact.is_friend:
82
+ contact.send_last_presence(force=True)
83
+ else:
84
+ reply = pres.reply()
85
+ reply["type"] = "unsubscribed"
86
+ reply.send()
87
+
88
+ async def _handle_new_subscription(self, pres: Presence):
89
+ pass
90
+
91
+ async def _handle_removed_subscription(self, pres: Presence):
92
+ pass
93
+
94
+
95
+ log = logging.getLogger(__name__)
@@ -0,0 +1,53 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from slixmpp import JID, Iq
5
+
6
+ from ...util.db import user_store
7
+
8
+ if TYPE_CHECKING:
9
+ from .base import BaseGateway
10
+
11
+
12
+ class Registration:
13
+ def __init__(self, xmpp: "BaseGateway"):
14
+ self.xmpp = xmpp
15
+ xmpp["xep_0077"].api.register(
16
+ user_store.get,
17
+ "user_get",
18
+ )
19
+ xmpp["xep_0077"].api.register(
20
+ user_store.remove,
21
+ "user_remove",
22
+ )
23
+ xmpp["xep_0077"].api.register(
24
+ self.xmpp.make_registration_form, "make_registration_form"
25
+ )
26
+ xmpp["xep_0077"].api.register(self._user_validate, "user_validate")
27
+ xmpp["xep_0077"].api.register(self._user_modify, "user_modify")
28
+
29
+ async def _user_validate(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
30
+ """
31
+ SliXMPP internal API stuff
32
+ """
33
+ xmpp = self.xmpp
34
+ log.debug("User validate: %s", ifrom.bare)
35
+ form_dict = {f.var: iq.get(f.var) for f in xmpp.REGISTRATION_FIELDS}
36
+ xmpp.raise_if_not_allowed_jid(ifrom)
37
+ await xmpp.user_prevalidate(ifrom, form_dict)
38
+ log.info("New user: %s", ifrom.bare)
39
+ user_store.add(ifrom, form_dict)
40
+
41
+ async def _user_modify(
42
+ self, _gateway_jid, _node, ifrom: JID, form_dict: dict[str, Optional[str]]
43
+ ):
44
+ """
45
+ SliXMPP internal API stuff
46
+ """
47
+ user = user_store.get_by_jid(ifrom)
48
+ log.debug("Modify user: %s", user)
49
+ await self.xmpp.user_prevalidate(ifrom, form_dict)
50
+ user_store.add(ifrom, form_dict)
51
+
52
+
53
+ log = logging.getLogger(__name__)
@@ -0,0 +1,102 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
4
+ from slixmpp.exceptions import XMPPError
5
+
6
+ from ...util.db import user_store
7
+
8
+ if TYPE_CHECKING:
9
+ from .base import BaseGateway
10
+
11
+
12
+ class Search:
13
+ def __init__(self, xmpp: "BaseGateway"):
14
+ self.xmpp = xmpp
15
+
16
+ xmpp["xep_0055"].api.register(self.search_get_form, "search_get_form")
17
+ xmpp["xep_0055"].api.register(self._search_query, "search_query")
18
+
19
+ xmpp.plugin["xep_0030"].add_feature("jabber:iq:gateway")
20
+ xmpp.register_handler(
21
+ CoroutineCallback(
22
+ "iq:gateway",
23
+ StanzaPath("iq/gateway"),
24
+ self._handle_gateway_iq, # type: ignore
25
+ )
26
+ )
27
+
28
+ async def search_get_form(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
29
+ """
30
+ Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS`
31
+ """
32
+ user = user_store.get_by_jid(ifrom)
33
+ if user is None:
34
+ raise XMPPError(text="Search is only allowed for registered users")
35
+
36
+ xmpp = self.xmpp
37
+
38
+ reply = iq.reply()
39
+ form = reply["search"]["form"]
40
+ form["title"] = xmpp.SEARCH_TITLE
41
+ form["instructions"] = xmpp.SEARCH_INSTRUCTIONS
42
+ for field in xmpp.SEARCH_FIELDS:
43
+ form.append(field.get_xml())
44
+ return reply
45
+
46
+ async def _search_query(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
47
+ """
48
+ Handles a search request
49
+ """
50
+ user = user_store.get_by_jid(ifrom)
51
+ if user is None:
52
+ raise XMPPError(text="Search is only allowed for registered users")
53
+
54
+ result = await self.xmpp.get_session_from_stanza(iq).on_search(
55
+ iq["search"]["form"].get_values()
56
+ )
57
+
58
+ if not result:
59
+ raise XMPPError("item-not-found", text="Nothing was found")
60
+
61
+ reply = iq.reply()
62
+ form = reply["search"]["form"]
63
+ for field in result.fields:
64
+ form.add_reported(field.var, label=field.label, type=field.type)
65
+ for item in result.items:
66
+ form.add_item(item)
67
+ return reply
68
+
69
+ async def _handle_gateway_iq(self, iq: Iq):
70
+ if iq.get_to() != self.xmpp.boundjid.bare:
71
+ raise XMPPError("bad-request", "This can only be used on the component JID")
72
+
73
+ user = user_store.get_by_jid(iq.get_from())
74
+ if user is None:
75
+ raise XMPPError("not-authorized", "Register to the gateway first")
76
+
77
+ if len(self.xmpp.SEARCH_FIELDS) > 1:
78
+ raise XMPPError(
79
+ "feature-not-implemented", "Use jabber search for this gateway"
80
+ )
81
+
82
+ field = self.xmpp.SEARCH_FIELDS[0]
83
+
84
+ reply = iq.reply()
85
+ if iq["type"] == "get":
86
+ reply["gateway"]["desc"] = self.xmpp.SEARCH_TITLE
87
+ reply["gateway"]["prompt"] = field.label
88
+ elif iq["type"] == "set":
89
+ prompt = iq["gateway"]["prompt"]
90
+ session = self.xmpp.session_cls.from_user(user)
91
+ result = await session.on_search({field.var: prompt})
92
+ if result is None or not result.items:
93
+ raise XMPPError(
94
+ "item-not-found", "No contact was found with the info you provided."
95
+ )
96
+ if len(result.items) > 1:
97
+ raise XMPPError(
98
+ "bad-request", "Your search yielded more than one result."
99
+ )
100
+ reply["gateway"]["jid"] = result.items[0]["jid"]
101
+
102
+ reply.send()