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.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- 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
|