slidge 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- slidge/__init__.py +3 -5
- slidge/__main__.py +2 -197
- slidge/__version__.py +5 -0
- slidge/command/adhoc.py +40 -17
- slidge/command/admin.py +24 -12
- slidge/command/base.py +10 -8
- slidge/command/categories.py +13 -3
- slidge/command/chat_command.py +29 -2
- slidge/command/register.py +32 -16
- slidge/command/user.py +106 -13
- slidge/contact/contact.py +254 -50
- slidge/contact/roster.py +124 -53
- slidge/core/config.py +19 -13
- slidge/core/dispatcher/__init__.py +3 -0
- slidge/core/{gateway → dispatcher}/caps.py +12 -8
- slidge/core/{gateway → dispatcher}/disco.py +10 -18
- slidge/core/dispatcher/message/__init__.py +10 -0
- slidge/core/dispatcher/message/chat_state.py +40 -0
- slidge/core/dispatcher/message/marker.py +62 -0
- slidge/core/dispatcher/message/message.py +397 -0
- slidge/core/dispatcher/muc/__init__.py +12 -0
- slidge/core/dispatcher/muc/admin.py +98 -0
- slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
- slidge/core/dispatcher/muc/misc.py +121 -0
- slidge/core/dispatcher/muc/owner.py +96 -0
- slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
- slidge/core/dispatcher/presence.py +176 -0
- slidge/core/dispatcher/registration.py +85 -0
- slidge/core/{gateway → dispatcher}/search.py +9 -16
- slidge/core/dispatcher/session_dispatcher.py +84 -0
- slidge/core/dispatcher/util.py +174 -0
- slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
- slidge/core/{gateway/base.py → gateway.py} +176 -153
- slidge/core/mixins/__init__.py +11 -1
- slidge/core/mixins/attachment.py +106 -67
- slidge/core/mixins/avatar.py +94 -25
- slidge/core/mixins/base.py +10 -4
- slidge/core/mixins/db.py +18 -0
- slidge/core/mixins/disco.py +0 -10
- slidge/core/mixins/lock.py +10 -8
- slidge/core/mixins/message.py +11 -195
- slidge/core/mixins/message_maker.py +17 -9
- slidge/core/mixins/message_text.py +211 -0
- slidge/core/mixins/presence.py +17 -4
- slidge/core/pubsub.py +114 -288
- slidge/core/session.py +101 -40
- slidge/db/__init__.py +4 -0
- slidge/db/alembic/__init__.py +0 -0
- slidge/db/alembic/env.py +64 -0
- slidge/db/alembic/old_user_store.py +183 -0
- slidge/db/alembic/script.py.mako +26 -0
- slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
- slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
- slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
- slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
- slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
- slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
- slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
- slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
- slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
- slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
- slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
- slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
- slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
- slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
- slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
- slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
- slidge/db/avatar.py +205 -0
- slidge/db/meta.py +72 -0
- slidge/db/models.py +405 -0
- slidge/db/store.py +1257 -0
- slidge/group/archive.py +58 -14
- slidge/group/bookmarks.py +89 -65
- slidge/group/participant.py +111 -44
- slidge/group/room.py +402 -213
- slidge/main.py +202 -0
- slidge/migration.py +45 -1
- slidge/slixfix/__init__.py +31 -1
- slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
- slidge/slixfix/roster.py +13 -4
- slidge/slixfix/xep_0292/vcard4.py +1 -87
- slidge/util/archive_msg.py +2 -1
- slidge/util/db.py +4 -228
- slidge/util/test.py +91 -4
- slidge/util/types.py +39 -4
- slidge/util/util.py +45 -2
- {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
- slidge-0.2.0.dist-info/RECORD +131 -0
- slidge-0.2.0.dist-info/entry_points.txt +3 -0
- slidge/core/cache.py +0 -183
- slidge/core/gateway/__init__.py +0 -3
- slidge/core/gateway/muc_admin.py +0 -35
- slidge/core/gateway/presence.py +0 -95
- slidge/core/gateway/registration.py +0 -53
- slidge/core/gateway/session_dispatcher.py +0 -795
- slidge/util/schema.sql +0 -126
- slidge/util/sql.py +0 -508
- slidge-0.1.2.dist-info/RECORD +0 -96
- slidge-0.1.2.dist-info/entry_points.txt +0 -3
- {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
- {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
slidge/core/session.py
CHANGED
@@ -18,11 +18,10 @@ from slixmpp.types import PresenceShows
|
|
18
18
|
|
19
19
|
from ..command import SearchResult
|
20
20
|
from ..contact import LegacyContact, LegacyRoster
|
21
|
+
from ..db.models import GatewayUser
|
21
22
|
from ..group.bookmarks import LegacyBookmarks
|
22
23
|
from ..group.room import LegacyMUC
|
23
24
|
from ..util import ABCSubclassableOnceAtMost
|
24
|
-
from ..util.db import GatewayUser, user_store
|
25
|
-
from ..util.sql import SQLBiDict
|
26
25
|
from ..util.types import (
|
27
26
|
LegacyGroupIdType,
|
28
27
|
LegacyMessageType,
|
@@ -32,6 +31,7 @@ from ..util.types import (
|
|
32
31
|
PseudoPresenceShow,
|
33
32
|
RecipientType,
|
34
33
|
ResourceDict,
|
34
|
+
Sticker,
|
35
35
|
)
|
36
36
|
from ..util.util import deprecated
|
37
37
|
|
@@ -74,8 +74,6 @@ class BaseSession(
|
|
74
74
|
session-specific.
|
75
75
|
"""
|
76
76
|
|
77
|
-
http: aiohttp.ClientSession
|
78
|
-
|
79
77
|
MESSAGE_IDS_ARE_THREAD_IDS = False
|
80
78
|
"""
|
81
79
|
Set this to True if the legacy service uses message IDs as thread IDs,
|
@@ -92,16 +90,10 @@ class BaseSession(
|
|
92
90
|
"""
|
93
91
|
|
94
92
|
def __init__(self, user: GatewayUser):
|
95
|
-
self.log = logging.getLogger(user.
|
93
|
+
self.log = logging.getLogger(user.jid.bare)
|
96
94
|
|
97
|
-
self.
|
98
|
-
self.
|
99
|
-
"session_message_sent", "legacy_id", "xmpp_id", self.user
|
100
|
-
)
|
101
|
-
# message ids (*not* stanza-ids), needed for last msg correction
|
102
|
-
self.muc_sent_msg_ids = SQLBiDict[LegacyMessageType, str](
|
103
|
-
"session_message_sent_muc", "legacy_id", "xmpp_id", self.user
|
104
|
-
)
|
95
|
+
self.user_jid = user.jid
|
96
|
+
self.user_pk = user.id
|
105
97
|
|
106
98
|
self.ignore_messages = set[str]()
|
107
99
|
|
@@ -113,28 +105,30 @@ class BaseSession(
|
|
113
105
|
self
|
114
106
|
)
|
115
107
|
|
116
|
-
self.http = self.xmpp.http
|
117
|
-
|
118
|
-
self.threads = SQLBiDict[str, LegacyThreadType]( # type:ignore
|
119
|
-
"session_thread_sent_muc", "legacy_id", "xmpp_id", self.user
|
120
|
-
)
|
121
108
|
self.thread_creation_lock = asyncio.Lock()
|
122
109
|
|
123
110
|
self.__cached_presence: Optional[CachedPresence] = None
|
124
111
|
|
125
|
-
self.avatar_hash: Optional[str] = None
|
126
|
-
|
127
112
|
self.__tasks = set[asyncio.Task]()
|
128
113
|
|
114
|
+
@property
|
115
|
+
def user(self) -> GatewayUser:
|
116
|
+
return self.xmpp.store.users.get(self.user_jid) # type:ignore
|
117
|
+
|
118
|
+
@property
|
119
|
+
def http(self) -> aiohttp.ClientSession:
|
120
|
+
return self.xmpp.http
|
121
|
+
|
129
122
|
def __remove_task(self, fut):
|
130
123
|
self.log.debug("Removing fut %s", fut)
|
131
124
|
self.__tasks.remove(fut)
|
132
125
|
|
133
|
-
def create_task(self, coro) ->
|
126
|
+
def create_task(self, coro) -> asyncio.Task:
|
134
127
|
task = self.xmpp.loop.create_task(coro)
|
135
128
|
self.__tasks.add(task)
|
136
129
|
self.log.debug("Creating task %s", task)
|
137
130
|
task.add_done_callback(lambda _: self.__remove_task(task))
|
131
|
+
return task
|
138
132
|
|
139
133
|
def cancel_all_tasks(self):
|
140
134
|
for task in self.__tasks:
|
@@ -232,6 +226,31 @@ class BaseSession(
|
|
232
226
|
|
233
227
|
send_file = deprecated("BaseSession.send_file", on_file)
|
234
228
|
|
229
|
+
async def on_sticker(
|
230
|
+
self,
|
231
|
+
chat: RecipientType,
|
232
|
+
sticker: Sticker,
|
233
|
+
*,
|
234
|
+
reply_to_msg_id: Optional[LegacyMessageType] = None,
|
235
|
+
reply_to_fallback_text: Optional[str] = None,
|
236
|
+
reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
|
237
|
+
thread: Optional[LegacyThreadType] = None,
|
238
|
+
) -> Optional[LegacyMessageType]:
|
239
|
+
"""
|
240
|
+
Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
|
241
|
+
|
242
|
+
:param chat: See :meth:`.BaseSession.on_text`
|
243
|
+
:param sticker: The sticker sent by the user.
|
244
|
+
:param reply_to_msg_id: See :meth:`.BaseSession.on_text`
|
245
|
+
:param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
|
246
|
+
:param reply_to: See :meth:`.BaseSession.on_text`
|
247
|
+
:param thread:
|
248
|
+
|
249
|
+
:return: An ID of some sort that can be used later to ack and mark the message
|
250
|
+
as read by the user
|
251
|
+
"""
|
252
|
+
raise NotImplementedError
|
253
|
+
|
235
254
|
async def on_active(
|
236
255
|
self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
|
237
256
|
):
|
@@ -488,6 +507,17 @@ class BaseSession(
|
|
488
507
|
"""
|
489
508
|
await muc.on_set_affiliation(contact, "member", reason, None)
|
490
509
|
|
510
|
+
async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType):
|
511
|
+
"""
|
512
|
+
Triggered when the user leaves a group via the dedicated slidge command
|
513
|
+
or the :xep:`0077` ``<remove />`` mechanism.
|
514
|
+
|
515
|
+
This should be interpreted as definitely leaving the group.
|
516
|
+
|
517
|
+
:param muc_legacy_id: The legacy ID of the group to leave
|
518
|
+
"""
|
519
|
+
raise NotImplementedError
|
520
|
+
|
491
521
|
def __reset_ready(self):
|
492
522
|
self.ready = self.xmpp.loop.create_future()
|
493
523
|
|
@@ -507,7 +537,7 @@ class BaseSession(
|
|
507
537
|
self.ready.set_result(True)
|
508
538
|
|
509
539
|
def __repr__(self):
|
510
|
-
return f"<Session of {self.
|
540
|
+
return f"<Session of {self.user_jid}>"
|
511
541
|
|
512
542
|
def shutdown(self) -> asyncio.Task:
|
513
543
|
for c in self.contacts:
|
@@ -571,9 +601,9 @@ class BaseSession(
|
|
571
601
|
log.debug("user not found", stack_info=True)
|
572
602
|
raise XMPPError(text="User not found", condition="subscription-required")
|
573
603
|
|
574
|
-
session = _sessions.get(user)
|
604
|
+
session = _sessions.get(user.jid.bare)
|
575
605
|
if session is None:
|
576
|
-
_sessions[user] = session = cls(user)
|
606
|
+
_sessions[user.jid.bare] = session = cls(user)
|
577
607
|
return session
|
578
608
|
|
579
609
|
@classmethod
|
@@ -590,7 +620,7 @@ class BaseSession(
|
|
590
620
|
# :param s:
|
591
621
|
# :return:
|
592
622
|
# """
|
593
|
-
return cls.
|
623
|
+
return cls.from_jid(s.get_from())
|
594
624
|
|
595
625
|
@classmethod
|
596
626
|
def from_jid(cls, jid: JID) -> "BaseSession":
|
@@ -602,7 +632,11 @@ class BaseSession(
|
|
602
632
|
# :param jid:
|
603
633
|
# :return:
|
604
634
|
# """
|
605
|
-
|
635
|
+
session = _sessions.get(jid.bare)
|
636
|
+
if session is not None:
|
637
|
+
return session
|
638
|
+
user = cls.xmpp.store.users.get(jid)
|
639
|
+
return cls._from_user_or_none(user)
|
606
640
|
|
607
641
|
@classmethod
|
608
642
|
async def kill_by_jid(cls, jid: JID):
|
@@ -615,16 +649,21 @@ class BaseSession(
|
|
615
649
|
# :return:
|
616
650
|
# """
|
617
651
|
log.debug("Killing session of %s", jid)
|
618
|
-
for
|
619
|
-
if
|
652
|
+
for user_jid, session in _sessions.items():
|
653
|
+
if user_jid == jid.bare:
|
620
654
|
break
|
621
655
|
else:
|
622
656
|
log.debug("Did not find a session for %s", jid)
|
623
657
|
return
|
624
658
|
for c in session.contacts:
|
625
659
|
c.unsubscribe()
|
660
|
+
user = cls.xmpp.store.users.get(jid)
|
661
|
+
if user is None:
|
662
|
+
log.warning("User not found during unregistration")
|
663
|
+
return
|
626
664
|
await cls.xmpp.unregister(user)
|
627
|
-
|
665
|
+
cls.xmpp.store.users.delete(user.jid)
|
666
|
+
del _sessions[user.jid.bare]
|
628
667
|
del user
|
629
668
|
del session
|
630
669
|
|
@@ -649,7 +688,7 @@ class BaseSession(
|
|
649
688
|
"""
|
650
689
|
self.__cached_presence = CachedPresence(status, show, kwargs)
|
651
690
|
self.xmpp.send_presence(
|
652
|
-
pto=self.
|
691
|
+
pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
|
653
692
|
)
|
654
693
|
|
655
694
|
def send_cached_presence(self, to: JID):
|
@@ -671,7 +710,7 @@ class BaseSession(
|
|
671
710
|
|
672
711
|
:param text: A text
|
673
712
|
"""
|
674
|
-
self.xmpp.send_text(text, mto=self.
|
713
|
+
self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs)
|
675
714
|
|
676
715
|
def send_gateway_invite(
|
677
716
|
self,
|
@@ -686,7 +725,7 @@ class BaseSession(
|
|
686
725
|
:param reason:
|
687
726
|
:param password:
|
688
727
|
"""
|
689
|
-
self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.
|
728
|
+
self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid)
|
690
729
|
|
691
730
|
async def input(self, text: str, **msg_kwargs):
|
692
731
|
"""
|
@@ -698,7 +737,7 @@ class BaseSession(
|
|
698
737
|
:param msg_kwargs: Extra attributes
|
699
738
|
:return:
|
700
739
|
"""
|
701
|
-
return await self.xmpp.input(self.
|
740
|
+
return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
|
702
741
|
|
703
742
|
async def send_qr(self, text: str):
|
704
743
|
"""
|
@@ -707,7 +746,7 @@ class BaseSession(
|
|
707
746
|
|
708
747
|
:param text: Text to encode as a QR code
|
709
748
|
"""
|
710
|
-
await self.xmpp.send_qr(text, mto=self.
|
749
|
+
await self.xmpp.send_qr(text, mto=self.user_jid)
|
711
750
|
|
712
751
|
def re_login(self):
|
713
752
|
# Logout then re-login
|
@@ -715,14 +754,17 @@ class BaseSession(
|
|
715
754
|
# No reason to override this
|
716
755
|
self.xmpp.re_login(self)
|
717
756
|
|
718
|
-
async def get_contact_or_group_or_participant(self, jid: JID):
|
719
|
-
if
|
720
|
-
return
|
721
|
-
if
|
722
|
-
return await self.__get_muc_or_participant(
|
757
|
+
async def get_contact_or_group_or_participant(self, jid: JID, create=True):
|
758
|
+
if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None:
|
759
|
+
return contact
|
760
|
+
if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
|
761
|
+
return await self.__get_muc_or_participant(muc, jid)
|
723
762
|
else:
|
724
763
|
muc = None
|
725
764
|
|
765
|
+
if not create:
|
766
|
+
return None
|
767
|
+
|
726
768
|
try:
|
727
769
|
return await self.contacts.by_jid(jid)
|
728
770
|
except XMPPError:
|
@@ -763,6 +805,25 @@ class BaseSession(
|
|
763
805
|
"Legacy session is not fully initialized, retry later",
|
764
806
|
)
|
765
807
|
|
808
|
+
def legacy_module_data_update(self, data: dict):
|
809
|
+
with self.xmpp.store.session():
|
810
|
+
user = self.user
|
811
|
+
user.legacy_module_data.update(data)
|
812
|
+
self.xmpp.store.users.update(user)
|
813
|
+
|
814
|
+
def legacy_module_data_set(self, data: dict):
|
815
|
+
with self.xmpp.store.session():
|
816
|
+
user = self.user
|
817
|
+
user.legacy_module_data = data
|
818
|
+
self.xmpp.store.users.update(user)
|
819
|
+
|
820
|
+
def legacy_module_data_clear(self):
|
821
|
+
with self.xmpp.store.session():
|
822
|
+
user = self.user
|
823
|
+
user.legacy_module_data.clear()
|
824
|
+
self.xmpp.store.users.update(user)
|
825
|
+
|
766
826
|
|
767
|
-
|
827
|
+
# keys = user.jid.bare
|
828
|
+
_sessions: dict[str, BaseSession] = {}
|
768
829
|
log = logging.getLogger(__name__)
|
slidge/db/__init__.py
ADDED
File without changes
|
slidge/db/alembic/env.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from alembic import context
|
2
|
+
|
3
|
+
from slidge import global_config
|
4
|
+
from slidge.db.meta import Base, get_engine
|
5
|
+
|
6
|
+
config = context.config
|
7
|
+
|
8
|
+
target_metadata = Base.metadata
|
9
|
+
|
10
|
+
|
11
|
+
def run_migrations_offline() -> None:
|
12
|
+
"""Run migrations in 'offline' mode.
|
13
|
+
|
14
|
+
This configures the context with just a URL
|
15
|
+
and not an Engine, though an Engine is acceptable
|
16
|
+
here as well. By skipping the Engine creation
|
17
|
+
we don't even need a DBAPI to be available.
|
18
|
+
|
19
|
+
Calls to context.execute() here emit the given string to the
|
20
|
+
script output.
|
21
|
+
|
22
|
+
"""
|
23
|
+
url = config.get_main_option("sqlalchemy.url")
|
24
|
+
context.configure(
|
25
|
+
url=url,
|
26
|
+
target_metadata=target_metadata,
|
27
|
+
literal_binds=True,
|
28
|
+
dialect_opts={"paramstyle": "named"},
|
29
|
+
render_as_batch=True,
|
30
|
+
)
|
31
|
+
|
32
|
+
with context.begin_transaction():
|
33
|
+
context.run_migrations()
|
34
|
+
|
35
|
+
|
36
|
+
def run_migrations_online() -> None:
|
37
|
+
"""Run migrations in 'online' mode.
|
38
|
+
|
39
|
+
In this scenario we need to create an Engine
|
40
|
+
and associate a connection with the context.
|
41
|
+
|
42
|
+
"""
|
43
|
+
try:
|
44
|
+
# in prod
|
45
|
+
connectable = get_engine(global_config.DB_URL)
|
46
|
+
except AttributeError:
|
47
|
+
# during dev, to generate migrations
|
48
|
+
connectable = get_engine("sqlite+pysqlite:///dev/slidge.sqlite")
|
49
|
+
|
50
|
+
with connectable.connect() as connection:
|
51
|
+
context.configure(
|
52
|
+
connection=connection,
|
53
|
+
target_metadata=target_metadata,
|
54
|
+
render_as_batch=True,
|
55
|
+
)
|
56
|
+
|
57
|
+
with context.begin_transaction():
|
58
|
+
context.run_migrations()
|
59
|
+
|
60
|
+
|
61
|
+
if context.is_offline_mode():
|
62
|
+
run_migrations_offline()
|
63
|
+
else:
|
64
|
+
run_migrations_online()
|
@@ -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,26 @@
|
|
1
|
+
"""${message}
|
2
|
+
|
3
|
+
Revision ID: ${up_revision}
|
4
|
+
Revises: ${down_revision | comma,n}
|
5
|
+
Create Date: ${create_date}
|
6
|
+
|
7
|
+
"""
|
8
|
+
from typing import Sequence, Union
|
9
|
+
|
10
|
+
from alembic import op
|
11
|
+
import sqlalchemy as sa
|
12
|
+
${imports if imports else ""}
|
13
|
+
|
14
|
+
# revision identifiers, used by Alembic.
|
15
|
+
revision: str = ${repr(up_revision)}
|
16
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
19
|
+
|
20
|
+
|
21
|
+
def upgrade() -> None:
|
22
|
+
${upgrades if upgrades else "pass"}
|
23
|
+
|
24
|
+
|
25
|
+
def downgrade() -> None:
|
26
|
+
${downgrades if downgrades else "pass"}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"""Add n_participants attributes to Room
|
2
|
+
|
3
|
+
Should have been part of another commit, but I messed up some rebase
|
4
|
+
|
5
|
+
Revision ID: 09f27f098baa
|
6
|
+
Revises: 29f5280c61aa
|
7
|
+
Create Date: 2024-07-11 10:54:21.155871
|
8
|
+
|
9
|
+
"""
|
10
|
+
|
11
|
+
from typing import Sequence, Union
|
12
|
+
|
13
|
+
import sqlalchemy as sa
|
14
|
+
from alembic import op
|
15
|
+
|
16
|
+
# revision identifiers, used by Alembic.
|
17
|
+
revision: str = "09f27f098baa"
|
18
|
+
down_revision: Union[str, None] = "29f5280c61aa"
|
19
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
20
|
+
depends_on: Union[str, Sequence[str], None] = None
|
21
|
+
|
22
|
+
|
23
|
+
def upgrade() -> None:
|
24
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
25
|
+
with op.batch_alter_table("room", schema=None) as batch_op:
|
26
|
+
batch_op.add_column(sa.Column("n_participants", sa.Integer(), nullable=True))
|
27
|
+
|
28
|
+
# ### end Alembic commands ###
|
29
|
+
|
30
|
+
|
31
|
+
def downgrade() -> None:
|
32
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
33
|
+
with op.batch_alter_table("room", schema=None) as batch_op:
|
34
|
+
batch_op.drop_column("n_participants")
|
35
|
+
|
36
|
+
# ### end Alembic commands ###
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Remove bogus unique constraints on room table
|
2
|
+
|
3
|
+
Revision ID: 15b0bd83407a
|
4
|
+
Revises: 45c24cc73c91
|
5
|
+
Create Date: 2024-08-28 06:57:25.022994
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Sequence, Union
|
10
|
+
|
11
|
+
import sqlalchemy as sa
|
12
|
+
from alembic import op
|
13
|
+
|
14
|
+
import slidge.db.meta
|
15
|
+
|
16
|
+
# revision identifiers, used by Alembic.
|
17
|
+
revision: str = "15b0bd83407a"
|
18
|
+
down_revision: Union[str, None] = "45c24cc73c91"
|
19
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
20
|
+
depends_on: Union[str, Sequence[str], None] = None
|
21
|
+
|
22
|
+
meta = sa.MetaData()
|
23
|
+
room_table = sa.Table(
|
24
|
+
"room",
|
25
|
+
meta,
|
26
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
27
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
28
|
+
sa.Column("legacy_id", sa.String(), nullable=False),
|
29
|
+
sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
|
30
|
+
sa.Column("avatar_id", sa.Integer(), nullable=True),
|
31
|
+
sa.Column("name", sa.String(), nullable=True),
|
32
|
+
sa.Column("description", sa.String(), nullable=True),
|
33
|
+
sa.Column("subject", sa.String(), nullable=True),
|
34
|
+
sa.Column("subject_date", sa.DateTime(), nullable=True),
|
35
|
+
sa.Column("subject_setter", sa.String(), nullable=True),
|
36
|
+
sa.Column("n_participants", sa.Integer(), nullable=True),
|
37
|
+
sa.Column(
|
38
|
+
"muc_type",
|
39
|
+
sa.Enum("GROUP", "CHANNEL", "CHANNEL_NON_ANONYMOUS", name="muctype"),
|
40
|
+
nullable=True,
|
41
|
+
),
|
42
|
+
sa.Column("user_nick", sa.String(), nullable=True),
|
43
|
+
sa.Column("user_resources", sa.String(), nullable=True),
|
44
|
+
sa.Column("participants_filled", sa.Boolean(), nullable=False),
|
45
|
+
sa.Column("history_filled", sa.Boolean(), nullable=False),
|
46
|
+
sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True),
|
47
|
+
sa.Column("updated", sa.Boolean(), nullable=False),
|
48
|
+
sa.Column("avatar_legacy_id", sa.String(), nullable=True),
|
49
|
+
sa.ForeignKeyConstraint(
|
50
|
+
["avatar_id"],
|
51
|
+
["avatar.id"],
|
52
|
+
),
|
53
|
+
sa.ForeignKeyConstraint(
|
54
|
+
["user_account_id"],
|
55
|
+
["user_account.id"],
|
56
|
+
),
|
57
|
+
sa.PrimaryKeyConstraint("id"),
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
def upgrade() -> None:
|
62
|
+
if op.get_bind().engine.name == "postgresql":
|
63
|
+
return
|
64
|
+
with op.batch_alter_table(
|
65
|
+
"room",
|
66
|
+
schema=None,
|
67
|
+
# without copy_from, the newly created table keeps the constraints
|
68
|
+
# we actually want to ditch.
|
69
|
+
copy_from=room_table,
|
70
|
+
) as batch_op:
|
71
|
+
batch_op.create_unique_constraint(
|
72
|
+
"uq_room_user_account_id_jid", ["user_account_id", "jid"]
|
73
|
+
)
|
74
|
+
batch_op.create_unique_constraint(
|
75
|
+
"uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"]
|
76
|
+
)
|
77
|
+
|
78
|
+
|
79
|
+
def downgrade() -> None:
|
80
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
81
|
+
with op.batch_alter_table("room", schema=None) as batch_op:
|
82
|
+
batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique")
|
83
|
+
batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique")
|
84
|
+
|
85
|
+
# ### end Alembic commands ###
|