slidge 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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