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,167 @@
1
+ from asyncio import Task, create_task
2
+ from hashlib import sha1
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from slixmpp import JID
7
+
8
+ from ...util.types import (
9
+ URL,
10
+ AnyBaseSession,
11
+ AvatarIdType,
12
+ AvatarType,
13
+ LegacyFileIdType,
14
+ )
15
+ from ..cache import avatar_cache
16
+
17
+ if TYPE_CHECKING:
18
+ from ..pubsub import PepAvatar
19
+
20
+
21
+ class AvatarMixin:
22
+ """
23
+ Mixin for XMPP entities that have avatars that represent them.
24
+
25
+ Both :py:class:`slidge.LegacyContact` and :py:class:`slidge.LegacyMUC` use
26
+ :py:class:`.AvatarMixin`.
27
+ """
28
+
29
+ jid: JID = NotImplemented
30
+ session: AnyBaseSession = NotImplemented
31
+ _avatar_pubsub_broadcast: bool = NotImplemented
32
+ _avatar_bare_jid: bool = NotImplemented
33
+
34
+ def __init__(self) -> None:
35
+ super().__init__()
36
+ self._set_avatar_task: Optional[Task] = None
37
+ self.__avatar_unique_id: Optional[AvatarIdType] = None
38
+
39
+ @property
40
+ def __avatar_jid(self):
41
+ return JID(self.jid.bare) if self._avatar_bare_jid else self.jid
42
+
43
+ @property
44
+ def avatar_id(self) -> Optional[AvatarIdType]:
45
+ """
46
+ The unique ID of this entity's avatar.
47
+ """
48
+ return self.__avatar_unique_id
49
+
50
+ @property
51
+ def avatar(self) -> Optional[AvatarIdType]:
52
+ """
53
+ This property can be used to set the avatar, but
54
+ :py:meth:`~.AvatarMixin.set_avatar()` should be preferred because you can
55
+ provide a unique ID for the avatar for efficient caching.
56
+ Setting this is OKish in case the avatar type is a URL or a local path
57
+ that can act as a legacy ID.
58
+
59
+ Python's ``property`` is abused here to maintain backwards
60
+ compatibility, but when getting it you actually get the avatar legacy
61
+ ID.
62
+ """
63
+ return self.__avatar_unique_id
64
+
65
+ @avatar.setter
66
+ def avatar(self, a: Optional[AvatarType]):
67
+ if self._set_avatar_task:
68
+ self._set_avatar_task.cancel()
69
+ self.session.log.debug("Setting avatar with property")
70
+ self._set_avatar_task = self.session.xmpp.loop.create_task(
71
+ self.set_avatar(a, None, blocking=True, cancel=False),
72
+ name=f"Set avatar of {self} from property",
73
+ )
74
+
75
+ @staticmethod
76
+ def __get_uid(a: Optional[AvatarType]) -> Optional[AvatarIdType]:
77
+ if isinstance(a, str):
78
+ return URL(a)
79
+ elif isinstance(a, Path):
80
+ return str(a)
81
+ elif isinstance(a, bytes):
82
+ return sha1(a).hexdigest()
83
+ elif a is None:
84
+ return None
85
+ raise TypeError("Bad avatar", a)
86
+
87
+ async def __set_avatar(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]):
88
+ self.__avatar_unique_id = uid
89
+ await self.session.xmpp.pubsub.set_avatar(
90
+ jid=self.__avatar_jid,
91
+ avatar=a,
92
+ unique_id=None if isinstance(uid, URL) else uid,
93
+ broadcast_to=self.session.user.jid.bare,
94
+ broadcast=self._avatar_pubsub_broadcast,
95
+ )
96
+ self._post_avatar_update()
97
+
98
+ async def _no_change(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]):
99
+ if a is None:
100
+ return self.__avatar_unique_id is None
101
+ if not self.__avatar_unique_id:
102
+ return False
103
+ if isinstance(uid, URL):
104
+ if self.__avatar_unique_id != uid:
105
+ return False
106
+ return not await avatar_cache.url_has_changed(uid)
107
+ return self.__avatar_unique_id == uid
108
+
109
+ async def set_avatar(
110
+ self,
111
+ a: Optional[AvatarType],
112
+ avatar_unique_id: Optional[LegacyFileIdType] = None,
113
+ blocking=False,
114
+ cancel=True,
115
+ ) -> None:
116
+ """
117
+ Set an avatar for this entity
118
+
119
+ :param a:
120
+ :param avatar_unique_id:
121
+ :param blocking:
122
+ :param cancel:
123
+ """
124
+ if avatar_unique_id is None and a is not None:
125
+ avatar_unique_id = self.__get_uid(a)
126
+ if await self._no_change(a, avatar_unique_id):
127
+ return
128
+ if cancel and self._set_avatar_task:
129
+ self._set_avatar_task.cancel()
130
+ awaitable = create_task(
131
+ self.__set_avatar(a, avatar_unique_id),
132
+ name=f"Set pubsub avatar of {self}",
133
+ )
134
+ if not self._set_avatar_task or self._set_avatar_task.done():
135
+ self._set_avatar_task = awaitable
136
+ if blocking:
137
+ await awaitable
138
+
139
+ def get_avatar(self) -> Optional["PepAvatar"]:
140
+ if not self.__avatar_unique_id:
141
+ return None
142
+ return self.session.xmpp.pubsub.get_avatar(self.__avatar_jid)
143
+
144
+ def _post_avatar_update(self) -> None:
145
+ return
146
+
147
+ async def avatar_wrap_update_info(self):
148
+ cached_id = avatar_cache.get_cached_id_for(self.__avatar_jid)
149
+ self.__avatar_unique_id = cached_id
150
+ try:
151
+ await self.update_info() # type:ignore
152
+ except NotImplementedError:
153
+ return
154
+ new_id = self.avatar
155
+ if isinstance(new_id, URL) and not await avatar_cache.url_has_changed(new_id):
156
+ return
157
+ elif new_id != cached_id:
158
+ # at this point it means that update_info set the avatar, and we don't
159
+ # need to do anything else
160
+ return
161
+
162
+ await self.session.xmpp.pubsub.set_avatar_from_cache(
163
+ self.__avatar_jid,
164
+ new_id is None and cached_id is not None,
165
+ self.session.user.jid.bare,
166
+ self._avatar_pubsub_broadcast,
167
+ )
@@ -0,0 +1,31 @@
1
+ from abc import ABCMeta
2
+ from typing import TYPE_CHECKING
3
+
4
+ from slixmpp import JID
5
+
6
+ from ...util.types import MessageOrPresenceTypeVar
7
+
8
+ if TYPE_CHECKING:
9
+ from slidge.core.gateway import BaseGateway
10
+ from slidge.core.session import BaseSession
11
+ from slidge.util.db import GatewayUser
12
+
13
+
14
+ class MetaBase(ABCMeta):
15
+ pass
16
+
17
+
18
+ class Base:
19
+ session: "BaseSession" = NotImplemented
20
+ xmpp: "BaseGateway" = NotImplemented
21
+ user: "GatewayUser" = NotImplemented
22
+
23
+ jid: JID = NotImplemented
24
+ name: str = NotImplemented
25
+
26
+
27
+ class BaseSender(Base):
28
+ def _send(
29
+ self, stanza: MessageOrPresenceTypeVar, **send_kwargs
30
+ ) -> MessageOrPresenceTypeVar:
31
+ raise NotImplementedError
@@ -0,0 +1,130 @@
1
+ from typing import Optional
2
+
3
+ from slixmpp.plugins.xep_0004 import Form
4
+ from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
5
+ from slixmpp.types import OptJid
6
+
7
+ from .base import Base
8
+
9
+
10
+ class BaseDiscoMixin(Base):
11
+ DISCO_TYPE: str = NotImplemented
12
+ DISCO_CATEGORY: str = NotImplemented
13
+ DISCO_NAME: str = NotImplemented
14
+ DISCO_LANG = None
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.__caps_cache: Optional[str] = None
19
+
20
+ def _get_disco_name(self):
21
+ if self.DISCO_NAME is NotImplemented:
22
+ return self.xmpp.COMPONENT_NAME
23
+ return self.DISCO_NAME or self.xmpp.COMPONENT_NAME
24
+
25
+ def features(self):
26
+ return []
27
+
28
+ async def extended_features(self) -> Optional[list[Form]]:
29
+ return None
30
+
31
+ async def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None):
32
+ info = DiscoInfo()
33
+ for feature in self.features():
34
+ info.add_feature(feature)
35
+ info.add_identity(
36
+ category=self.DISCO_CATEGORY,
37
+ itype=self.DISCO_TYPE,
38
+ name=self._get_disco_name(),
39
+ lang=self.DISCO_LANG,
40
+ )
41
+ if forms := await self.extended_features():
42
+ for form in forms:
43
+ info.append(form)
44
+ return info
45
+
46
+ async def get_caps_ver(self, jid: OptJid = None, node: Optional[str] = None):
47
+ if self.__caps_cache:
48
+ return self.__caps_cache
49
+ info = await self.get_disco_info(jid, node)
50
+ caps = self.xmpp.plugin["xep_0115"]
51
+ ver = caps.generate_verstring(info, caps.hash)
52
+ self.__caps_cache = ver
53
+ return ver
54
+
55
+ def reset_caps_cache(self):
56
+ self.__caps_cache = None
57
+
58
+
59
+ class ChatterDiscoMixin(BaseDiscoMixin):
60
+ AVATAR = True
61
+ RECEIPTS = True
62
+ MARKS = True
63
+ CHAT_STATES = True
64
+ UPLOAD = True
65
+ CORRECTION = True
66
+ REACTION = True
67
+ RETRACTION = True
68
+ REPLIES = True
69
+ INVITATION_RECIPIENT = False
70
+
71
+ DISCO_TYPE = "pc"
72
+ DISCO_CATEGORY = "client"
73
+ DISCO_NAME = ""
74
+
75
+ def features(self):
76
+ features = []
77
+ if self.CHAT_STATES:
78
+ features.append("http://jabber.org/protocol/chatstates")
79
+ if self.RECEIPTS:
80
+ features.append("urn:xmpp:receipts")
81
+ if self.CORRECTION:
82
+ features.append("urn:xmpp:message-correct:0")
83
+ if self.MARKS:
84
+ features.append("urn:xmpp:chat-markers:0")
85
+ if self.UPLOAD:
86
+ features.append("jabber:x:oob")
87
+ if self.REACTION:
88
+ features.append("urn:xmpp:reactions:0")
89
+ if self.RETRACTION:
90
+ features.append("urn:xmpp:message-retract:0")
91
+ if self.REPLIES:
92
+ features.append("urn:xmpp:reply:0")
93
+ if self.INVITATION_RECIPIENT:
94
+ features.append("jabber:x:conference")
95
+ features.append("urn:ietf:params:xml:ns:vcard-4.0")
96
+ return features
97
+
98
+ async def extended_features(self):
99
+ f = getattr(self, "restricted_emoji_extended_feature", None)
100
+ if f is None:
101
+ return
102
+
103
+ e = await f()
104
+ if not e:
105
+ return
106
+
107
+ return [e]
108
+
109
+
110
+ class ContactAccountDiscoMixin(BaseDiscoMixin):
111
+ async def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None):
112
+ if jid and jid.resource:
113
+ return await super().get_disco_info()
114
+ info = DiscoInfo()
115
+ info.add_feature("http://jabber.org/protocol/pubsub")
116
+ info.add_feature("http://jabber.org/protocol/pubsub#retrieve-items")
117
+ info.add_feature("http://jabber.org/protocol/pubsub#subscribe")
118
+ info.add_identity(
119
+ category="account",
120
+ itype="registered",
121
+ name=self._get_disco_name(),
122
+ lang=self.DISCO_LANG,
123
+ )
124
+ info.add_identity(
125
+ category="pubsub",
126
+ itype="pep",
127
+ name=self._get_disco_name(),
128
+ lang=self.DISCO_LANG,
129
+ )
130
+ return info
@@ -0,0 +1,31 @@
1
+ import asyncio
2
+ import logging
3
+ from contextlib import asynccontextmanager
4
+ from typing import Hashable
5
+
6
+
7
+ class NamedLockMixin:
8
+ def __init__(self, *a, **k):
9
+ super().__init__(*a, **k)
10
+ self.__locks = dict[Hashable, asyncio.Lock]()
11
+
12
+ @asynccontextmanager
13
+ async def lock(self, id_: Hashable):
14
+ log.trace("getting %s", id_) # type:ignore
15
+ locks = self.__locks
16
+ if not locks.get(id_):
17
+ locks[id_] = asyncio.Lock()
18
+ async with locks[id_]:
19
+ log.trace("acquired %s", id_) # type:ignore
20
+ yield
21
+ log.trace("releasing %s", id_) # type:ignore
22
+ waiters = locks[id_]._waiters # type:ignore
23
+ if not waiters:
24
+ del locks[id_]
25
+ log.trace("erasing %s", id_) # type:ignore
26
+
27
+ def get_lock(self, id_: Hashable):
28
+ return self.__locks.get(id_)
29
+
30
+
31
+ log = logging.getLogger(__name__) # type:ignore