slidge 0.1.3__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.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +107 -40
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.3.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.bare_jid)
93
+ self.log = logging.getLogger(user.jid.bare)
96
94
 
97
- self.user = user
98
- self.sent = SQLBiDict[LegacyMessageType, str](
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) -> None:
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.user}>"
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._from_user_or_none(user_store.get_by_stanza(s))
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
- return cls._from_user_or_none(user_store.get_by_jid(jid))
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 user, session in _sessions.items():
619
- if user.jid == jid.bare:
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
- del _sessions[user]
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.user.bare_jid, pstatus=status, pshow=show, **kwargs
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.user.jid, **msg_kwargs)
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.user.jid)
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.user.jid, text, **msg_kwargs)
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.user.jid)
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 jid.bare in (contacts := self.contacts.known_contacts(only_friends=False)):
720
- return contacts[jid.bare]
721
- if jid.bare in (mucs := self.bookmarks._mucs_by_bare_jid):
722
- return await self.__get_muc_or_participant(mucs[jid.bare], jid)
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
- _sessions: dict[GatewayUser, BaseSession] = {}
827
+ # keys = user.jid.bare
828
+ _sessions: dict[str, BaseSession] = {}
768
829
  log = logging.getLogger(__name__)
slidge/db/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .models import GatewayUser
2
+ from .store import SlidgeStore
3
+
4
+ __all__ = ("GatewayUser", "SlidgeStore")
File without changes
@@ -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 ###