slidge 0.2.0a2__py3-none-any.whl → 0.2.0a4__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/__version__.py +1 -1
- slidge/command/admin.py +7 -1
- slidge/contact/contact.py +24 -1
- slidge/contact/roster.py +41 -51
- slidge/core/gateway/base.py +1 -3
- slidge/core/gateway/session_dispatcher.py +19 -0
- slidge/core/gateway/vcard_temp.py +2 -2
- slidge/core/pubsub.py +22 -18
- slidge/db/alembic/__init__.py +0 -0
- slidge/db/alembic/old_user_store.py +183 -0
- slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +44 -0
- slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +13 -1
- slidge/db/avatar.py +5 -0
- slidge/db/meta.py +7 -0
- slidge/db/models.py +12 -2
- slidge/db/store.py +37 -7
- slidge/group/bookmarks.py +34 -47
- slidge/group/participant.py +15 -7
- slidge/group/room.py +3 -1
- slidge/main.py +0 -4
- slidge/slixfix/roster.py +4 -2
- slidge/slixfix/xep_0292/vcard4.py +1 -90
- slidge/util/db.py +4 -182
- slidge/util/test.py +1 -1
- {slidge-0.2.0a2.dist-info → slidge-0.2.0a4.dist-info}/METADATA +1 -1
- {slidge-0.2.0a2.dist-info → slidge-0.2.0a4.dist-info}/RECORD +30 -26
- {slidge-0.2.0a2.dist-info → slidge-0.2.0a4.dist-info}/LICENSE +0 -0
- {slidge-0.2.0a2.dist-info → slidge-0.2.0a4.dist-info}/WHEEL +0 -0
- {slidge-0.2.0a2.dist-info → slidge-0.2.0a4.dist-info}/entry_points.txt +0 -0
slidge/__version__.py
CHANGED
slidge/command/admin.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Commands only accessible for slidge admins
|
2
2
|
import functools
|
3
|
+
import importlib
|
3
4
|
import logging
|
4
5
|
from datetime import datetime
|
5
6
|
from typing import Any, Optional
|
@@ -7,6 +8,7 @@ from typing import Any, Optional
|
|
7
8
|
from slixmpp import JID
|
8
9
|
from slixmpp.exceptions import XMPPError
|
9
10
|
|
11
|
+
from ..core import config
|
10
12
|
from ..util.types import AnyBaseSession
|
11
13
|
from .base import (
|
12
14
|
Command,
|
@@ -90,8 +92,12 @@ class SlidgeInfo(AdminCommand):
|
|
90
92
|
[a for a in (days_ago, hours_ago, minutes_ago, seconds_ago) if a]
|
91
93
|
)
|
92
94
|
|
95
|
+
legacy_module = importlib.import_module(config.LEGACY_MODULE)
|
96
|
+
version = getattr(legacy_module, "__version__", "No version")
|
97
|
+
|
93
98
|
return (
|
94
|
-
f"{self.xmpp.COMPONENT_NAME}
|
99
|
+
f"{self.xmpp.COMPONENT_NAME} (slidge core {__version__},"
|
100
|
+
f" {config.LEGACY_MODULE} {version})\n"
|
95
101
|
f"Up since {start:%Y-%m-%d %H:%M} ({ago} ago)"
|
96
102
|
)
|
97
103
|
|
slidge/contact/contact.py
CHANGED
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
import warnings
|
4
4
|
from datetime import date
|
5
5
|
from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union
|
6
|
+
from xml.etree import ElementTree as ET
|
6
7
|
|
7
8
|
from slixmpp import JID, Message, Presence
|
8
9
|
from slixmpp.exceptions import IqError
|
@@ -125,6 +126,16 @@ class LegacyContact(
|
|
125
126
|
self._is_friend: bool = False
|
126
127
|
self._added_to_roster = False
|
127
128
|
self._caps_ver: str | None = None
|
129
|
+
self._vcard_fetched = False
|
130
|
+
self._vcard: str | None = None
|
131
|
+
|
132
|
+
async def get_vcard(self, fetch=True) -> VCard4 | None:
|
133
|
+
if fetch and not self._vcard_fetched:
|
134
|
+
await self.fetch_vcard()
|
135
|
+
if self._vcard is None:
|
136
|
+
return None
|
137
|
+
|
138
|
+
return VCard4(xml=ET.fromstring(self._vcard))
|
128
139
|
|
129
140
|
@property
|
130
141
|
def is_friend(self):
|
@@ -373,7 +384,17 @@ class LegacyContact(
|
|
373
384
|
elif country:
|
374
385
|
vcard.add_address(country, locality)
|
375
386
|
|
376
|
-
self.
|
387
|
+
self._vcard = str(vcard)
|
388
|
+
self._vcard_fetched = True
|
389
|
+
self.session.create_task(
|
390
|
+
self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard)
|
391
|
+
)
|
392
|
+
|
393
|
+
if self._updating_info:
|
394
|
+
return
|
395
|
+
|
396
|
+
assert self.contact_pk is not None
|
397
|
+
self.xmpp.store.contacts.set_vcard(self.contact_pk, self._vcard)
|
377
398
|
|
378
399
|
def get_roster_item(self):
|
379
400
|
item = {
|
@@ -584,6 +605,8 @@ class LegacyContact(
|
|
584
605
|
contact._caps_ver = stored.caps_ver
|
585
606
|
contact._set_logger_name()
|
586
607
|
contact._set_avatar_from_store(stored)
|
608
|
+
contact._vcard = stored.vcard
|
609
|
+
contact._vcard_fetched = stored.vcard_fetched
|
587
610
|
return contact
|
588
611
|
|
589
612
|
|
slidge/contact/roster.py
CHANGED
@@ -8,6 +8,7 @@ from slixmpp.exceptions import IqError, XMPPError
|
|
8
8
|
from slixmpp.jid import JID_UNESCAPE_TRANSFORMATIONS, _unescape_node
|
9
9
|
|
10
10
|
from ..core.mixins.lock import NamedLockMixin
|
11
|
+
from ..db.models import Contact
|
11
12
|
from ..db.store import ContactStore
|
12
13
|
from ..util import SubclassableOnce
|
13
14
|
from ..util.types import LegacyContactType, LegacyUserIdType
|
@@ -63,37 +64,6 @@ class LegacyRoster(
|
|
63
64
|
for stored in self.__store.get_all(user_pk=self.session.user_pk):
|
64
65
|
yield self._contact_cls.from_store(self.session, stored)
|
65
66
|
|
66
|
-
async def __finish_init_contact(
|
67
|
-
self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
|
68
|
-
):
|
69
|
-
async with self.lock(("finish", legacy_id)):
|
70
|
-
with self.__store.session():
|
71
|
-
stored = self.__store.get_by_legacy_id(
|
72
|
-
self.session.user_pk, str(legacy_id)
|
73
|
-
)
|
74
|
-
if stored is not None:
|
75
|
-
if stored.updated:
|
76
|
-
return self._contact_cls.from_store(self.session, stored)
|
77
|
-
c: LegacyContact = self._contact_cls(
|
78
|
-
self.session, legacy_id, jid_username, *args, **kwargs
|
79
|
-
)
|
80
|
-
c.contact_pk = stored.id
|
81
|
-
else:
|
82
|
-
c = self._contact_cls(
|
83
|
-
self.session, legacy_id, jid_username, *args, **kwargs
|
84
|
-
)
|
85
|
-
try:
|
86
|
-
with c.updating_info():
|
87
|
-
await c.avatar_wrap_update_info()
|
88
|
-
except Exception as e:
|
89
|
-
raise XMPPError("internal-server-error", str(e))
|
90
|
-
c._caps_ver = await c.get_caps_ver(c.jid)
|
91
|
-
need_avatar = c.contact_pk is None
|
92
|
-
c.contact_pk = self.__store.update(c, commit=not self.__filling)
|
93
|
-
if need_avatar:
|
94
|
-
c._post_avatar_update()
|
95
|
-
return c
|
96
|
-
|
97
67
|
def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
|
98
68
|
if only_friends:
|
99
69
|
return {c.jid.bare: c for c in self if c.is_friend}
|
@@ -112,17 +82,15 @@ class LegacyRoster(
|
|
112
82
|
# """
|
113
83
|
username = contact_jid.node
|
114
84
|
async with self.lock(("username", username)):
|
115
|
-
with self.__store.session():
|
116
|
-
stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
|
117
|
-
if stored is not None and stored.updated:
|
118
|
-
return self._contact_cls.from_store(self.session, stored)
|
119
|
-
|
120
85
|
legacy_id = await self.jid_username_to_legacy_id(username)
|
121
86
|
log.debug("Contact %s not found", contact_jid)
|
122
87
|
if self.get_lock(("legacy_id", legacy_id)):
|
123
88
|
log.debug("Already updating %s", contact_jid)
|
124
89
|
return await self.by_legacy_id(legacy_id)
|
125
|
-
|
90
|
+
|
91
|
+
with self.__store.session():
|
92
|
+
stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
|
93
|
+
return await self.__update_contact(stored, legacy_id, username)
|
126
94
|
|
127
95
|
async def by_legacy_id(
|
128
96
|
self, legacy_id: LegacyUserIdType, *args, **kwargs
|
@@ -145,26 +113,48 @@ class LegacyRoster(
|
|
145
113
|
if legacy_id == self.user_legacy_id:
|
146
114
|
raise ContactIsUser
|
147
115
|
async with self.lock(("legacy_id", legacy_id)):
|
148
|
-
with self.__store.session():
|
149
|
-
stored = self.__store.get_by_legacy_id(
|
150
|
-
self.session.user_pk, str(legacy_id)
|
151
|
-
)
|
152
|
-
if stored is not None and stored.updated:
|
153
|
-
return self._contact_cls.from_store(
|
154
|
-
self.session, stored, *args, **kwargs
|
155
|
-
)
|
156
|
-
|
157
116
|
username = await self.legacy_id_to_jid_username(legacy_id)
|
158
|
-
log.debug("Contact %s not found", legacy_id)
|
159
117
|
if self.get_lock(("username", username)):
|
160
118
|
log.debug("Already updating %s", username)
|
161
119
|
jid = JID()
|
162
120
|
jid.node = username
|
163
121
|
jid.domain = self.session.xmpp.boundjid.bare
|
164
122
|
return await self.by_jid(jid)
|
165
|
-
|
166
|
-
|
167
|
-
|
123
|
+
|
124
|
+
with self.__store.session():
|
125
|
+
stored = self.__store.get_by_legacy_id(
|
126
|
+
self.session.user_pk, str(legacy_id)
|
127
|
+
)
|
128
|
+
return await self.__update_contact(
|
129
|
+
stored, legacy_id, username, *args, **kwargs
|
130
|
+
)
|
131
|
+
|
132
|
+
async def __update_contact(
|
133
|
+
self,
|
134
|
+
stored: Contact | None,
|
135
|
+
legacy_id: LegacyUserIdType,
|
136
|
+
username: str,
|
137
|
+
*a,
|
138
|
+
**kw,
|
139
|
+
) -> LegacyContactType:
|
140
|
+
if stored is None:
|
141
|
+
contact = self._contact_cls(self.session, legacy_id, username, *a, **kw)
|
142
|
+
else:
|
143
|
+
contact = self._contact_cls.from_store(self.session, stored, *a, **kw)
|
144
|
+
if stored.updated:
|
145
|
+
return contact
|
146
|
+
|
147
|
+
try:
|
148
|
+
with contact.updating_info():
|
149
|
+
await contact.avatar_wrap_update_info()
|
150
|
+
except Exception as e:
|
151
|
+
raise XMPPError("internal-server-error", str(e))
|
152
|
+
contact._caps_ver = await contact.get_caps_ver(contact.jid)
|
153
|
+
need_avatar = contact.contact_pk is None
|
154
|
+
contact.contact_pk = self.__store.update(contact, commit=not self.__filling)
|
155
|
+
if need_avatar:
|
156
|
+
contact._post_avatar_update()
|
157
|
+
return contact
|
168
158
|
|
169
159
|
async def by_stanza(self, s) -> LegacyContact:
|
170
160
|
# """
|
@@ -225,7 +215,7 @@ class LegacyRoster(
|
|
225
215
|
item = contact.get_roster_item()
|
226
216
|
old = user_roster.get(contact.jid.bare)
|
227
217
|
if old is not None and all(
|
228
|
-
old[k] == item[contact.jid.bare]
|
218
|
+
old[k] == item[contact.jid.bare].get(k)
|
229
219
|
for k in ("subscription", "groups", "name")
|
230
220
|
):
|
231
221
|
self.log.debug("No need to update roster")
|
slidge/core/gateway/base.py
CHANGED
@@ -36,7 +36,6 @@ from ...command.register import RegistrationType
|
|
36
36
|
from ...db import GatewayUser, SlidgeStore
|
37
37
|
from ...db.avatar import avatar_cache
|
38
38
|
from ...slixfix.roster import RosterBackend
|
39
|
-
from ...slixfix.xep_0292.vcard4 import VCard4Provider
|
40
39
|
from ...util import ABCSubclassableOnceAtMost
|
41
40
|
from ...util.types import AvatarType, MessageOrPresenceTypeVar
|
42
41
|
from ...util.util import timeit
|
@@ -316,7 +315,7 @@ class BaseGateway(
|
|
316
315
|
|
317
316
|
from ...group.room import LegacyMUC
|
318
317
|
|
319
|
-
LegacyMUC.
|
318
|
+
LegacyMUC.get_self_or_unique_subclass().xmpp = self
|
320
319
|
|
321
320
|
self.get_session_from_stanza: Callable[
|
322
321
|
[Union[Message, Presence, Iq]], BaseSession
|
@@ -331,7 +330,6 @@ class BaseGateway(
|
|
331
330
|
|
332
331
|
self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
|
333
332
|
self.pubsub: PubSubComponent = self["pubsub"]
|
334
|
-
self.vcard: VCard4Provider = self["xep_0292_provider"]
|
335
333
|
self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self)
|
336
334
|
|
337
335
|
# with this we receive user avatar updates
|
@@ -68,6 +68,13 @@ class SessionDispatcher:
|
|
68
68
|
_exceptions_to_xmpp_errors(self.on_ibr_remove), # type: ignore
|
69
69
|
)
|
70
70
|
)
|
71
|
+
self.xmpp.register_handler(
|
72
|
+
CoroutineCallback(
|
73
|
+
"get_vcard",
|
74
|
+
StanzaPath("iq@type=get/vcard"),
|
75
|
+
_exceptions_to_xmpp_errors(self.on_get_vcard), # type:ignore
|
76
|
+
)
|
77
|
+
)
|
71
78
|
|
72
79
|
for event in (
|
73
80
|
"legacy_message",
|
@@ -778,6 +785,18 @@ class SessionDispatcher:
|
|
778
785
|
|
779
786
|
raise XMPPError("feature-not-implemented")
|
780
787
|
|
788
|
+
async def on_get_vcard(self, iq: Iq):
|
789
|
+
session = await self.__get_session(iq)
|
790
|
+
session.raise_if_not_logged()
|
791
|
+
contact = await session.contacts.by_jid(iq.get_to())
|
792
|
+
vcard = await contact.get_vcard()
|
793
|
+
reply = iq.reply()
|
794
|
+
if vcard:
|
795
|
+
reply.append(vcard)
|
796
|
+
else:
|
797
|
+
reply.enable("vcard")
|
798
|
+
reply.send()
|
799
|
+
|
781
800
|
def _xmpp_msg_id_to_legacy(self, session: "BaseSession", xmpp_id: str):
|
782
801
|
sent = self.xmpp.store.sent.get_legacy_id(session.user_pk, xmpp_id)
|
783
802
|
if sent is not None:
|
@@ -79,14 +79,14 @@ class VCardTemp:
|
|
79
79
|
elif not (contact := entity.contact):
|
80
80
|
raise XMPPError("item-not-found", "This participant has no contact")
|
81
81
|
else:
|
82
|
-
vcard = await
|
82
|
+
vcard = await contact.get_vcard()
|
83
83
|
avatar = contact.get_avatar()
|
84
84
|
type_ = "image/png"
|
85
85
|
else:
|
86
86
|
avatar = entity.get_avatar()
|
87
87
|
type_ = "image/png"
|
88
88
|
if isinstance(entity, LegacyContact):
|
89
|
-
vcard = await
|
89
|
+
vcard = await entity.get_vcard(fetch=False)
|
90
90
|
else:
|
91
91
|
vcard = None
|
92
92
|
v = self.xmpp.plugin["xep_0054"].make_vcard()
|
slidge/core/pubsub.py
CHANGED
@@ -22,6 +22,7 @@ from slixmpp.types import JidStr, OptJidStr
|
|
22
22
|
|
23
23
|
from ..db.avatar import CachedAvatar, avatar_cache
|
24
24
|
from ..db.store import ContactStore, SlidgeStore
|
25
|
+
from ..util.types import URL
|
25
26
|
from .mixins.lock import NamedLockMixin
|
26
27
|
|
27
28
|
if TYPE_CHECKING:
|
@@ -135,6 +136,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
|
|
135
136
|
|
136
137
|
to = p.get_to()
|
137
138
|
|
139
|
+
contact = None
|
138
140
|
# we don't want to push anything for contacts that are not in the user's roster
|
139
141
|
if to != self.xmpp.boundjid.bare:
|
140
142
|
session = self.xmpp.get_session_from_stanza(p)
|
@@ -170,14 +172,13 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
|
|
170
172
|
except XMPPError:
|
171
173
|
pass
|
172
174
|
else:
|
173
|
-
if pep_avatar.metadata is None:
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
)
|
175
|
+
if pep_avatar.metadata is not None:
|
176
|
+
await self.__broadcast(
|
177
|
+
data=pep_avatar.metadata,
|
178
|
+
from_=p.get_to(),
|
179
|
+
to=from_,
|
180
|
+
id=pep_avatar.metadata["info"]["id"],
|
181
|
+
)
|
181
182
|
if UserNick.namespace + "+notify" in features:
|
182
183
|
try:
|
183
184
|
pep_nick = await self._get_authorized_nick(p)
|
@@ -186,17 +187,19 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
|
|
186
187
|
else:
|
187
188
|
await self.__broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
|
188
189
|
|
189
|
-
if VCARD4_NAMESPACE + "+notify" in features:
|
190
|
-
await self.broadcast_vcard_event(
|
190
|
+
if contact is not None and VCARD4_NAMESPACE + "+notify" in features:
|
191
|
+
await self.broadcast_vcard_event(
|
192
|
+
p.get_to(), from_, await contact.get_vcard()
|
193
|
+
)
|
191
194
|
|
192
|
-
async def broadcast_vcard_event(self, from_, to):
|
195
|
+
async def broadcast_vcard_event(self, from_: JID, to: JID, vcard: VCard4 | None):
|
193
196
|
item = Item()
|
194
197
|
item.namespace = VCARD4_NAMESPACE
|
195
198
|
item["id"] = "current"
|
196
|
-
vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to)
|
199
|
+
# vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to)
|
197
200
|
# The vcard content should NOT be in this event according to the spec:
|
198
201
|
# https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
|
199
|
-
# but movim expects it to be here, and I guess
|
202
|
+
# but movim expects it to be here, and I guess it does not hurt
|
200
203
|
|
201
204
|
log.debug("Broadcast vcard4 event: %s", vcard)
|
202
205
|
await self.__broadcast(
|
@@ -219,7 +222,9 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
|
|
219
222
|
item = PepAvatar()
|
220
223
|
avatar_id = entity.avatar_id
|
221
224
|
if avatar_id is not None:
|
222
|
-
stored = avatar_cache.get(
|
225
|
+
stored = avatar_cache.get(
|
226
|
+
avatar_id if isinstance(avatar_id, URL) else str(avatar_id)
|
227
|
+
)
|
223
228
|
assert stored is not None
|
224
229
|
item.set_avatar_from_cache(stored)
|
225
230
|
return item
|
@@ -268,10 +273,9 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
|
|
268
273
|
# this is not the proper way that clients should retrieve VCards, but
|
269
274
|
# gajim does it this way.
|
270
275
|
# https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
|
271
|
-
|
272
|
-
|
273
|
-
)
|
274
|
-
log.debug("VCARD: %s -- %s -- %s", iq.get_to().bare, iq.get_from().bare, vcard)
|
276
|
+
session = self.xmpp.get_session_from_stanza(iq)
|
277
|
+
contact = await session.contacts.by_jid(iq.get_to())
|
278
|
+
vcard = await contact.get_vcard()
|
275
279
|
if vcard is None:
|
276
280
|
raise XMPPError("item-not-found")
|
277
281
|
self._reply_with_payload(iq, vcard, "current", VCARD4_NAMESPACE)
|
File without changes
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""
|
2
|
+
This module covers a backend for storing user data persistently and managing a
|
3
|
+
pseudo-roster for the gateway component.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import dataclasses
|
7
|
+
import datetime
|
8
|
+
import logging
|
9
|
+
import os.path
|
10
|
+
import shelve
|
11
|
+
from io import BytesIO
|
12
|
+
from os import PathLike
|
13
|
+
from typing import Iterable, Optional, Union
|
14
|
+
|
15
|
+
from pickle_secure import Pickler, Unpickler
|
16
|
+
from slixmpp import JID, Iq, Message, Presence
|
17
|
+
|
18
|
+
|
19
|
+
# noinspection PyUnresolvedReferences
|
20
|
+
class EncryptedShelf(shelve.DbfilenameShelf):
|
21
|
+
cache: dict
|
22
|
+
dict: dict
|
23
|
+
writeback: bool
|
24
|
+
keyencoding: str
|
25
|
+
_protocol: int
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self, filename: PathLike, key: str, flag="c", protocol=None, writeback=False
|
29
|
+
):
|
30
|
+
super().__init__(str(filename), flag, protocol, writeback)
|
31
|
+
self.secret_key = key
|
32
|
+
|
33
|
+
def __getitem__(self, key):
|
34
|
+
try:
|
35
|
+
value = self.cache[key]
|
36
|
+
except KeyError:
|
37
|
+
f = BytesIO(self.dict[key.encode(self.keyencoding)])
|
38
|
+
value = Unpickler(f, key=self.secret_key).load() # type:ignore
|
39
|
+
if self.writeback:
|
40
|
+
self.cache[key] = value
|
41
|
+
return value
|
42
|
+
|
43
|
+
def __setitem__(self, key, value):
|
44
|
+
if self.writeback:
|
45
|
+
self.cache[key] = value
|
46
|
+
f = BytesIO()
|
47
|
+
p = Pickler(f, self._protocol, key=self.secret_key) # type:ignore
|
48
|
+
p.dump(value)
|
49
|
+
self.dict[key.encode(self.keyencoding)] = f.getvalue()
|
50
|
+
|
51
|
+
|
52
|
+
@dataclasses.dataclass
|
53
|
+
class GatewayUser:
|
54
|
+
"""
|
55
|
+
A gateway user
|
56
|
+
"""
|
57
|
+
|
58
|
+
bare_jid: str
|
59
|
+
"""Bare JID of the user"""
|
60
|
+
registration_form: dict[str, Optional[str]]
|
61
|
+
"""Content of the registration form, as a dict"""
|
62
|
+
plugin_data: Optional[dict] = None
|
63
|
+
registration_date: Optional[datetime.datetime] = None
|
64
|
+
|
65
|
+
def __hash__(self):
|
66
|
+
return hash(self.bare_jid)
|
67
|
+
|
68
|
+
def __repr__(self):
|
69
|
+
return f"<User {self.bare_jid}>"
|
70
|
+
|
71
|
+
def __post_init__(self):
|
72
|
+
if self.registration_date is None:
|
73
|
+
self.registration_date = datetime.datetime.now()
|
74
|
+
|
75
|
+
@property
|
76
|
+
def jid(self) -> JID:
|
77
|
+
"""
|
78
|
+
The user's (bare) JID
|
79
|
+
|
80
|
+
:return:
|
81
|
+
"""
|
82
|
+
return JID(self.bare_jid)
|
83
|
+
|
84
|
+
def get(self, field: str, default: str = "") -> Optional[str]:
|
85
|
+
# """
|
86
|
+
# Get fields from the registration form (required to comply with slixmpp backend protocol)
|
87
|
+
#
|
88
|
+
# :param field: Name of the field
|
89
|
+
# :param default: Default value to return if the field is not present
|
90
|
+
#
|
91
|
+
# :return: Value of the field
|
92
|
+
# """
|
93
|
+
return self.registration_form.get(field, default)
|
94
|
+
|
95
|
+
|
96
|
+
class UserStore:
|
97
|
+
"""
|
98
|
+
Basic user store implementation using shelve from the python standard library
|
99
|
+
|
100
|
+
Set_file must be called before it is usable
|
101
|
+
"""
|
102
|
+
|
103
|
+
def __init__(self):
|
104
|
+
self._users: shelve.Shelf[GatewayUser] = None # type: ignore
|
105
|
+
|
106
|
+
def set_file(self, filename: PathLike, secret_key: Optional[str] = None):
|
107
|
+
"""
|
108
|
+
Set the file to use to store user data
|
109
|
+
|
110
|
+
:param filename: Path to the shelf file
|
111
|
+
:param secret_key: Secret key to store files encrypted on disk
|
112
|
+
"""
|
113
|
+
if self._users is not None:
|
114
|
+
raise RuntimeError("Shelf file already set!")
|
115
|
+
if os.path.exists(filename):
|
116
|
+
log.info("Using existing slidge DB: %s", filename)
|
117
|
+
else:
|
118
|
+
log.info("Creating a new slidge DB: %s", filename)
|
119
|
+
if secret_key:
|
120
|
+
self._users = EncryptedShelf(filename, key=secret_key)
|
121
|
+
else:
|
122
|
+
self._users = shelve.open(str(filename))
|
123
|
+
log.info("Registered users in the DB: %s", list(self._users.keys()))
|
124
|
+
|
125
|
+
def get_all(self) -> Iterable[GatewayUser]:
|
126
|
+
"""
|
127
|
+
Get all users in the store
|
128
|
+
|
129
|
+
:return: An iterable of GatewayUsers
|
130
|
+
"""
|
131
|
+
return self._users.values()
|
132
|
+
|
133
|
+
def commit(self, user: GatewayUser):
|
134
|
+
self._users[user.jid.bare] = user
|
135
|
+
self._users.sync()
|
136
|
+
|
137
|
+
def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
|
138
|
+
"""
|
139
|
+
Get a user from the store
|
140
|
+
|
141
|
+
NB: there is no reason to call this, it is used by SliXMPP internal API
|
142
|
+
|
143
|
+
:param _gateway_jid:
|
144
|
+
:param _node:
|
145
|
+
:param ifrom:
|
146
|
+
:param iq:
|
147
|
+
:return:
|
148
|
+
"""
|
149
|
+
if ifrom is None: # bug in SliXMPP's XEP_0100 plugin
|
150
|
+
ifrom = iq["from"]
|
151
|
+
log.debug("Getting user %s", ifrom.bare)
|
152
|
+
return self._users.get(ifrom.bare)
|
153
|
+
|
154
|
+
def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
|
155
|
+
"""
|
156
|
+
Convenience function to get a user from their JID.
|
157
|
+
|
158
|
+
:param jid: JID of the gateway user
|
159
|
+
:return:
|
160
|
+
"""
|
161
|
+
return self._users.get(jid.bare)
|
162
|
+
|
163
|
+
def get_by_stanza(self, s: Union[Presence, Message, Iq]) -> Optional[GatewayUser]:
|
164
|
+
"""
|
165
|
+
Convenience function to get a user from a stanza they sent.
|
166
|
+
|
167
|
+
:param s: A stanza sent by the gateway user
|
168
|
+
:return:
|
169
|
+
"""
|
170
|
+
return self.get_by_jid(s.get_from())
|
171
|
+
|
172
|
+
def close(self):
|
173
|
+
self._users.sync()
|
174
|
+
self._users.close()
|
175
|
+
|
176
|
+
|
177
|
+
user_store = UserStore()
|
178
|
+
"""
|
179
|
+
A persistent store for slidge users. Not public, but I didn't find how to hide
|
180
|
+
it from the docs!
|
181
|
+
"""
|
182
|
+
|
183
|
+
log = logging.getLogger(__name__)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""Lift room legacy ID constraint
|
2
|
+
|
3
|
+
Revision ID: 5bd48bfdffa2
|
4
|
+
Revises: b64b1a793483
|
5
|
+
Create Date: 2024-07-24 10:29:23.467851
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Sequence, Union
|
10
|
+
|
11
|
+
from alembic import op
|
12
|
+
|
13
|
+
from slidge.db.models import Room
|
14
|
+
|
15
|
+
# revision identifiers, used by Alembic.
|
16
|
+
revision: str = "5bd48bfdffa2"
|
17
|
+
down_revision: Union[str, None] = "b64b1a793483"
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
20
|
+
|
21
|
+
|
22
|
+
def upgrade() -> None:
|
23
|
+
with op.batch_alter_table(
|
24
|
+
"room",
|
25
|
+
schema=None,
|
26
|
+
# without copy_from, the newly created table keeps the constraints
|
27
|
+
# we actually want to ditch.
|
28
|
+
copy_from=Room.__table__, # type:ignore
|
29
|
+
) as batch_op:
|
30
|
+
batch_op.create_unique_constraint(
|
31
|
+
"uq_room_user_account_id_jid", ["user_account_id", "jid"]
|
32
|
+
)
|
33
|
+
batch_op.create_unique_constraint(
|
34
|
+
"uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"]
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
def downgrade() -> None:
|
39
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
40
|
+
with op.batch_alter_table("room", schema=None) as batch_op:
|
41
|
+
batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique")
|
42
|
+
batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique")
|
43
|
+
|
44
|
+
# ### end Alembic commands ###
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Add vcard content to contact table
|
2
|
+
|
3
|
+
Revision ID: 8b993243a536
|
4
|
+
Revises: 5bd48bfdffa2
|
5
|
+
Create Date: 2024-07-24 07:02:47.770894
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Sequence, Union
|
10
|
+
|
11
|
+
import sqlalchemy as sa
|
12
|
+
from alembic import op
|
13
|
+
|
14
|
+
# revision identifiers, used by Alembic.
|
15
|
+
revision: str = "8b993243a536"
|
16
|
+
down_revision: Union[str, None] = "5bd48bfdffa2"
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
19
|
+
|
20
|
+
|
21
|
+
def upgrade() -> None:
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
23
|
+
with op.batch_alter_table("contact", schema=None) as batch_op:
|
24
|
+
batch_op.add_column(sa.Column("vcard", sa.String(), nullable=True))
|
25
|
+
batch_op.add_column(
|
26
|
+
sa.Column(
|
27
|
+
"vcard_fetched",
|
28
|
+
sa.Boolean(),
|
29
|
+
nullable=False,
|
30
|
+
server_default=sa.sql.true(),
|
31
|
+
)
|
32
|
+
)
|
33
|
+
|
34
|
+
# ### end Alembic commands ###
|
35
|
+
|
36
|
+
|
37
|
+
def downgrade() -> None:
|
38
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
39
|
+
with op.batch_alter_table("contact", schema=None) as batch_op:
|
40
|
+
batch_op.drop_column("vcard_fetched")
|
41
|
+
batch_op.drop_column("vcard")
|
42
|
+
|
43
|
+
# ### end Alembic commands ###
|