slidge 0.2.0a2__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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 ###
|