slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -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 +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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 +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -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
+ )
@@ -1,9 +1,9 @@
1
1
  from abc import ABCMeta
2
- from typing import TYPE_CHECKING, Optional, Union
2
+ from typing import TYPE_CHECKING
3
3
 
4
- from slixmpp import JID, Message, Presence
4
+ from slixmpp import JID
5
5
 
6
- from slidge.util.types import LegacyMessageType
6
+ from ...util.types import MessageOrPresenceTypeVar
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from slidge.core.gateway import BaseGateway
@@ -25,20 +25,7 @@ class Base:
25
25
 
26
26
 
27
27
  class BaseSender(Base):
28
- def _send(self, stanza: Union[Message, Presence], **send_kwargs):
28
+ def _send(
29
+ self, stanza: MessageOrPresenceTypeVar, **send_kwargs
30
+ ) -> MessageOrPresenceTypeVar:
29
31
  raise NotImplementedError
30
-
31
-
32
- class ReactionRecipientMixin:
33
- REACTIONS_SINGLE_EMOJI = False
34
-
35
- async def available_emojis(
36
- self, legacy_msg_id: LegacyMessageType
37
- ) -> Optional[set[str]]:
38
- """
39
- Override this to restrict the subset of reactions this recipient
40
- can handle.
41
-
42
- :return: A set of emojis or None if any emoji is allowed
43
- """
44
- return None
@@ -1,6 +1,8 @@
1
- import functools
1
+ from typing import Optional
2
2
 
3
- from slidge.util.xep_0030.stanza.info import DiscoInfo
3
+ from slixmpp.plugins.xep_0004 import Form
4
+ from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
5
+ from slixmpp.types import OptJid
4
6
 
5
7
  from .base import Base
6
8
 
@@ -11,26 +13,48 @@ class BaseDiscoMixin(Base):
11
13
  DISCO_NAME: str = NotImplemented
12
14
  DISCO_LANG = None
13
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
+
14
25
  def features(self):
15
26
  return []
16
27
 
17
- def extended_features(self):
18
- return
28
+ async def extended_features(self) -> Optional[list[Form]]:
29
+ return None
19
30
 
20
- def get_disco_info(self):
31
+ async def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None):
21
32
  info = DiscoInfo()
22
33
  for feature in self.features():
23
34
  info.add_feature(feature)
24
35
  info.add_identity(
25
36
  category=self.DISCO_CATEGORY,
26
37
  itype=self.DISCO_TYPE,
27
- name=self.DISCO_NAME,
38
+ name=self._get_disco_name(),
28
39
  lang=self.DISCO_LANG,
29
40
  )
30
- if x := self.extended_features():
31
- info.append(x)
41
+ if forms := await self.extended_features():
42
+ for form in forms:
43
+ info.append(form)
32
44
  return info
33
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
+
34
58
 
35
59
  class ChatterDiscoMixin(BaseDiscoMixin):
36
60
  AVATAR = True
@@ -42,6 +66,7 @@ class ChatterDiscoMixin(BaseDiscoMixin):
42
66
  REACTION = True
43
67
  RETRACTION = True
44
68
  REPLIES = True
69
+ INVITATION_RECIPIENT = False
45
70
 
46
71
  DISCO_TYPE = "pc"
47
72
  DISCO_CATEGORY = "client"
@@ -65,15 +90,41 @@ class ChatterDiscoMixin(BaseDiscoMixin):
65
90
  features.append("urn:xmpp:message-retract:0")
66
91
  if self.REPLIES:
67
92
  features.append("urn:xmpp:reply:0")
93
+ if self.INVITATION_RECIPIENT:
94
+ features.append("jabber:x:conference")
68
95
  features.append("urn:ietf:params:xml:ns:vcard-4.0")
69
96
  return features
70
97
 
71
- async def update_caps(self):
72
- jid = self.jid
73
- xmpp = self.xmpp
98
+ async def extended_features(self):
99
+ f = getattr(self, "restricted_emoji_extended_feature", None)
100
+ if f is None:
101
+ return
74
102
 
75
- add_feature = functools.partial(xmpp["xep_0030"].add_feature, jid=jid)
76
- for f in self.features():
77
- await add_feature(f)
103
+ e = await f()
104
+ if not e:
105
+ return
78
106
 
79
- await xmpp["xep_0115"].update_caps(jid=jid)
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