slidge 0.2.12__py3-none-any.whl → 0.3.0a0__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.
Files changed (77) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +119 -209
  8. slidge/contact/roster.py +106 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +117 -92
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +21 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +168 -84
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +163 -148
  31. slidge/core/mixins/avatar.py +100 -177
  32. slidge/core/mixins/db.py +50 -2
  33. slidge/core/mixins/message.py +19 -17
  34. slidge/core/mixins/message_maker.py +29 -15
  35. slidge/core/mixins/message_text.py +38 -30
  36. slidge/core/mixins/presence.py +91 -35
  37. slidge/core/pubsub.py +42 -47
  38. slidge/core/session.py +88 -57
  39. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
  40. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +56 -0
  41. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
  42. slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
  43. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
  44. slidge/db/avatar.py +150 -119
  45. slidge/db/meta.py +33 -22
  46. slidge/db/models.py +68 -117
  47. slidge/db/store.py +412 -1094
  48. slidge/group/archive.py +61 -54
  49. slidge/group/bookmarks.py +74 -55
  50. slidge/group/participant.py +135 -142
  51. slidge/group/room.py +315 -312
  52. slidge/main.py +28 -18
  53. slidge/migration.py +2 -12
  54. slidge/slixfix/__init__.py +20 -4
  55. slidge/slixfix/delivery_receipt.py +6 -4
  56. slidge/slixfix/link_preview/link_preview.py +1 -1
  57. slidge/slixfix/link_preview/stanza.py +1 -1
  58. slidge/slixfix/roster.py +5 -7
  59. slidge/slixfix/xep_0077/register.py +8 -8
  60. slidge/slixfix/xep_0077/stanza.py +7 -7
  61. slidge/slixfix/xep_0100/gateway.py +12 -13
  62. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  63. slidge/slixfix/xep_0292/vcard4.py +1 -1
  64. slidge/util/archive_msg.py +11 -5
  65. slidge/util/conf.py +23 -20
  66. slidge/util/jid_escaping.py +1 -1
  67. slidge/{core/mixins → util}/lock.py +6 -6
  68. slidge/util/test.py +30 -29
  69. slidge/util/types.py +22 -18
  70. slidge/util/util.py +19 -22
  71. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/METADATA +1 -1
  72. slidge-0.3.0a0.dist-info/RECORD +117 -0
  73. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/WHEEL +1 -1
  74. slidge-0.2.12.dist-info/RECORD +0 -112
  75. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/entry_points.txt +0 -0
  76. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
  77. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/top_level.txt +0 -0
slidge/core/session.py CHANGED
@@ -7,18 +7,20 @@ from typing import (
7
7
  Iterable,
8
8
  NamedTuple,
9
9
  Optional,
10
+ Type,
10
11
  Union,
11
12
  cast,
12
13
  )
13
14
 
14
15
  import aiohttp
16
+ import sqlalchemy as sa
15
17
  from slixmpp import JID, Message
16
18
  from slixmpp.exceptions import XMPPError
17
19
  from slixmpp.types import PresenceShows
18
20
 
19
21
  from ..command import SearchResult
20
22
  from ..contact import LegacyContact, LegacyRoster
21
- from ..db.models import GatewayUser
23
+ from ..db.models import Contact, GatewayUser
22
24
  from ..group.bookmarks import LegacyBookmarks
23
25
  from ..group.room import LegacyMUC
24
26
  from ..util import ABCSubclassableOnceAtMost
@@ -89,22 +91,21 @@ class BaseSession(
89
91
  This can be used to implement voting in polls in a hacky way.
90
92
  """
91
93
 
92
- def __init__(self, user: GatewayUser):
93
- self.log = logging.getLogger(user.jid.bare)
94
+ _roster_cls: Type[LegacyRoster]
95
+ _bookmarks_cls: Type[LegacyBookmarks]
94
96
 
95
- self.user_jid = user.jid
96
- self.user_pk = user.id
97
+ def __init__(self, user: GatewayUser) -> None:
98
+ self.user = user
99
+ self.log = logging.getLogger(user.jid.bare)
97
100
 
98
101
  self.ignore_messages = set[str]()
99
102
 
100
- self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self)
103
+ self.contacts = self._roster_cls(self)
101
104
  self.is_logging_in = False
102
105
  self._logged = False
103
106
  self.__reset_ready()
104
107
 
105
- self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()(
106
- self
107
- )
108
+ self.bookmarks = self._bookmarks_cls(self)
108
109
 
109
110
  self.thread_creation_lock = asyncio.Lock()
110
111
 
@@ -113,14 +114,18 @@ class BaseSession(
113
114
  self.__tasks = set[asyncio.Task]()
114
115
 
115
116
  @property
116
- def user(self) -> GatewayUser:
117
- return self.xmpp.store.users.get(self.user_jid) # type:ignore
117
+ def user_jid(self) -> JID:
118
+ return self.user.jid
119
+
120
+ @property
121
+ def user_pk(self) -> int:
122
+ return self.user.id
118
123
 
119
124
  @property
120
125
  def http(self) -> aiohttp.ClientSession:
121
126
  return self.xmpp.http
122
127
 
123
- def __remove_task(self, fut):
128
+ def __remove_task(self, fut) -> None:
124
129
  self.log.debug("Removing fut %s", fut)
125
130
  self.__tasks.remove(fut)
126
131
 
@@ -131,7 +136,7 @@ class BaseSession(
131
136
  task.add_done_callback(lambda _: self.__remove_task(task))
132
137
  return task
133
138
 
134
- def cancel_all_tasks(self):
139
+ def cancel_all_tasks(self) -> None:
135
140
  for task in self.__tasks:
136
141
  task.cancel()
137
142
 
@@ -148,7 +153,7 @@ class BaseSession(
148
153
  """
149
154
  raise NotImplementedError
150
155
 
151
- async def logout(self):
156
+ async def logout(self) -> None:
152
157
  """
153
158
  Logs out the gateway user from the legacy network.
154
159
 
@@ -304,6 +309,17 @@ class BaseSession(
304
309
 
305
310
  paused = deprecated("BaseSession.paused", on_paused)
306
311
 
312
+ async def on_gone(
313
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
314
+ ):
315
+ """
316
+ Triggered when the user is "gone" in a legacy chat (:xep:`0085`)
317
+
318
+ :param chat: See :meth:`.BaseSession.on_text`
319
+ :param thread:
320
+ """
321
+ raise NotImplementedError
322
+
307
323
  async def on_displayed(
308
324
  self,
309
325
  chat: RecipientType,
@@ -493,7 +509,7 @@ class BaseSession(
493
509
 
494
510
  async def on_invitation(
495
511
  self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
496
- ):
512
+ ) -> None:
497
513
  """
498
514
  Triggered when the user invites a :term:`Contact` to a legacy MUC via
499
515
  :xep:`0249`.
@@ -519,7 +535,7 @@ class BaseSession(
519
535
  """
520
536
  raise NotImplementedError
521
537
 
522
- def __reset_ready(self):
538
+ def __reset_ready(self) -> None:
523
539
  self.ready = self.xmpp.loop.create_future()
524
540
 
525
541
  @property
@@ -527,7 +543,7 @@ class BaseSession(
527
543
  return self._logged
528
544
 
529
545
  @logged.setter
530
- def logged(self, v: bool):
546
+ def logged(self, v: bool) -> None:
531
547
  self.is_logging_in = False
532
548
  self._logged = v
533
549
  if self.ready.done():
@@ -539,19 +555,34 @@ class BaseSession(
539
555
  if v:
540
556
  self.ready.set_result(True)
541
557
 
542
- def __repr__(self):
558
+ def __repr__(self) -> str:
543
559
  return f"<Session of {self.user_jid}>"
544
560
 
545
- def shutdown(self, logout=True) -> asyncio.Task:
561
+ def shutdown(self, logout: bool = True) -> asyncio.Task:
546
562
  for m in self.bookmarks:
547
563
  m.shutdown()
548
- for c in self.contacts:
549
- c.offline()
564
+ with self.xmpp.store.session() as orm:
565
+ for jid in orm.execute(
566
+ sa.select(Contact.jid).filter_by(user=self.user, is_friend=True)
567
+ ).scalars():
568
+ pres = self.xmpp.make_presence(
569
+ pfrom=jid,
570
+ pto=self.user_jid,
571
+ ptype="unavailable",
572
+ pstatus="Gateway has shut down.",
573
+ )
574
+ pres.send()
550
575
  if logout:
551
- return self.xmpp.loop.create_task(self.logout())
576
+ return self.xmpp.loop.create_task(self.__logout())
552
577
  else:
553
578
  return self.xmpp.loop.create_task(noop_coro())
554
579
 
580
+ async def __logout(self) -> None:
581
+ try:
582
+ await self.logout()
583
+ except NotImplementedError:
584
+ pass
585
+
555
586
  @staticmethod
556
587
  def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
557
588
  """
@@ -604,7 +635,7 @@ class BaseSession(
604
635
  @classmethod
605
636
  def _from_user_or_none(cls, user):
606
637
  if user is None:
607
- log.debug("user not found", stack_info=True)
638
+ log.debug("user not found")
608
639
  raise XMPPError(text="User not found", condition="subscription-required")
609
640
 
610
641
  session = _sessions.get(user.jid.bare)
@@ -641,11 +672,12 @@ class BaseSession(
641
672
  session = _sessions.get(jid.bare)
642
673
  if session is not None:
643
674
  return session
644
- user = cls.xmpp.store.users.get(jid)
675
+ with cls.xmpp.store.session() as orm:
676
+ user = orm.query(GatewayUser).filter_by(jid=jid.bare).one_or_none()
645
677
  return cls._from_user_or_none(user)
646
678
 
647
679
  @classmethod
648
- async def kill_by_jid(cls, jid: JID):
680
+ async def kill_by_jid(cls, jid: JID) -> None:
649
681
  # """
650
682
  # Terminate a user session.
651
683
  #
@@ -665,17 +697,19 @@ class BaseSession(
665
697
  c.unsubscribe()
666
698
  for m in session.bookmarks:
667
699
  m.shutdown()
668
- user = cls.xmpp.store.users.get(jid)
669
- if user is None:
700
+
701
+ try:
702
+ session = _sessions.pop(jid.bare)
703
+ except KeyError:
670
704
  log.warning("User not found during unregistration")
671
705
  return
672
- await cls.xmpp.unregister(user)
673
- cls.xmpp.store.users.delete(user.jid)
674
- del _sessions[user.jid.bare]
675
- del user
676
- del session
677
706
 
678
- def __ack(self, msg: Message):
707
+ with cls.xmpp.store.session() as orm:
708
+ await cls.xmpp.unregister(session.user)
709
+ orm.delete(session.user)
710
+ orm.commit()
711
+
712
+ def __ack(self, msg: Message) -> None:
679
713
  if not self.xmpp.PROPER_RECEIPTS:
680
714
  self.xmpp.delivery_receipt.ack(msg)
681
715
 
@@ -684,7 +718,7 @@ class BaseSession(
684
718
  status: Optional[str] = None,
685
719
  show=Optional[PresenceShows],
686
720
  **kwargs,
687
- ):
721
+ ) -> None:
688
722
  """
689
723
  Send a presence from the gateway to the user.
690
724
 
@@ -699,7 +733,7 @@ class BaseSession(
699
733
  pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
700
734
  )
701
735
 
702
- def send_cached_presence(self, to: JID):
736
+ def send_cached_presence(self, to: JID) -> None:
703
737
  if not self.__cached_presence:
704
738
  self.xmpp.send_presence(pto=to, ptype="unavailable")
705
739
  return
@@ -710,7 +744,7 @@ class BaseSession(
710
744
  **self.__cached_presence.kwargs,
711
745
  )
712
746
 
713
- def send_gateway_message(self, text: str, **msg_kwargs):
747
+ def send_gateway_message(self, text: str, **msg_kwargs) -> None:
714
748
  """
715
749
  Send a message from the gateway component to the user.
716
750
 
@@ -725,7 +759,7 @@ class BaseSession(
725
759
  muc: LegacyMUC,
726
760
  reason: Optional[str] = None,
727
761
  password: Optional[str] = None,
728
- ):
762
+ ) -> None:
729
763
  """
730
764
  Send an invitation to join a MUC, emanating from the gateway component.
731
765
 
@@ -747,7 +781,7 @@ class BaseSession(
747
781
  """
748
782
  return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
749
783
 
750
- async def send_qr(self, text: str):
784
+ async def send_qr(self, text: str) -> None:
751
785
  """
752
786
  Sends a QR code generated from 'text' via HTTP Upload and send the URL to
753
787
  ``self.user``
@@ -756,13 +790,13 @@ class BaseSession(
756
790
  """
757
791
  await self.xmpp.send_qr(text, mto=self.user_jid)
758
792
 
759
- def re_login(self):
793
+ def re_login(self) -> None:
760
794
  # Logout then re-login
761
795
  #
762
796
  # No reason to override this
763
797
  self.xmpp.re_login(self)
764
798
 
765
- async def get_contact_or_group_or_participant(self, jid: JID, create=True):
799
+ async def get_contact_or_group_or_participant(self, jid: JID, create: bool = True):
766
800
  if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None:
767
801
  return contact
768
802
  if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
@@ -813,23 +847,20 @@ class BaseSession(
813
847
  "Legacy session is not fully initialized, retry later",
814
848
  )
815
849
 
816
- def legacy_module_data_update(self, data: dict):
817
- with self.xmpp.store.session():
818
- user = self.user
819
- user.legacy_module_data.update(data)
820
- self.xmpp.store.users.update(user)
821
-
822
- def legacy_module_data_set(self, data: dict):
823
- with self.xmpp.store.session():
824
- user = self.user
825
- user.legacy_module_data = data
826
- self.xmpp.store.users.update(user)
827
-
828
- def legacy_module_data_clear(self):
829
- with self.xmpp.store.session():
830
- user = self.user
831
- user.legacy_module_data.clear()
832
- self.xmpp.store.users.update(user)
850
+ def legacy_module_data_update(self, data: dict) -> None:
851
+ user = self.user
852
+ user.legacy_module_data.update(data)
853
+ self.xmpp.store.users.update(user)
854
+
855
+ def legacy_module_data_set(self, data: dict) -> None:
856
+ user = self.user
857
+ user.legacy_module_data = data
858
+ self.xmpp.store.users.update(user)
859
+
860
+ def legacy_module_data_clear(self) -> None:
861
+ user = self.user
862
+ user.legacy_module_data.clear()
863
+ self.xmpp.store.users.update(user)
833
864
 
834
865
 
835
866
  # keys = user.jid.bare
@@ -0,0 +1,183 @@
1
+ """Unify legacy/XMPP id mappings
2
+
3
+ Revision ID: 0337c90c0b96
4
+ Revises: 58b98dacf819
5
+ Create Date: 2025-04-25 20:22:47.612652
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 = "0337c90c0b96"
16
+ down_revision: Union[str, None] = "58b98dacf819"
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
+ op.create_table(
24
+ "direct_msg",
25
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
26
+ sa.Column("id", sa.Integer(), nullable=False),
27
+ sa.Column("legacy_id", sa.String(), nullable=False),
28
+ sa.Column("xmpp_id", sa.String(), nullable=False),
29
+ sa.ForeignKeyConstraint(
30
+ ["foreign_key"],
31
+ ["contact.id"],
32
+ name=op.f("fk_direct_msg_foreign_key_contact"),
33
+ ),
34
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_direct_msg")),
35
+ )
36
+ with op.batch_alter_table("direct_msg", schema=None) as batch_op:
37
+ batch_op.create_index(
38
+ "ix_direct_msg_legacy_id", ["legacy_id", "foreign_key"], unique=False
39
+ )
40
+
41
+ op.create_table(
42
+ "direct_thread",
43
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
44
+ sa.Column("id", sa.Integer(), nullable=False),
45
+ sa.Column("legacy_id", sa.String(), nullable=False),
46
+ sa.Column("xmpp_id", sa.String(), nullable=False),
47
+ sa.ForeignKeyConstraint(
48
+ ["foreign_key"],
49
+ ["contact.id"],
50
+ name=op.f("fk_direct_thread_foreign_key_contact"),
51
+ ),
52
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_direct_thread")),
53
+ )
54
+ with op.batch_alter_table("direct_thread", schema=None) as batch_op:
55
+ batch_op.create_index(
56
+ "ix_direct_direct_thread_id", ["legacy_id", "foreign_key"], unique=False
57
+ )
58
+
59
+ op.create_table(
60
+ "group_msg",
61
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
62
+ sa.Column("id", sa.Integer(), nullable=False),
63
+ sa.Column("legacy_id", sa.String(), nullable=False),
64
+ sa.Column("xmpp_id", sa.String(), nullable=False),
65
+ sa.ForeignKeyConstraint(
66
+ ["foreign_key"], ["room.id"], name=op.f("fk_group_msg_foreign_key_room")
67
+ ),
68
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_group_msg")),
69
+ )
70
+ with op.batch_alter_table("group_msg", schema=None) as batch_op:
71
+ batch_op.create_index(
72
+ "ix_group_msg_legacy_id", ["legacy_id", "foreign_key"], unique=False
73
+ )
74
+
75
+ op.create_table(
76
+ "group_thread",
77
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
78
+ sa.Column("id", sa.Integer(), nullable=False),
79
+ sa.Column("legacy_id", sa.String(), nullable=False),
80
+ sa.Column("xmpp_id", sa.String(), nullable=False),
81
+ sa.ForeignKeyConstraint(
82
+ ["foreign_key"], ["room.id"], name=op.f("fk_group_thread_foreign_key_room")
83
+ ),
84
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_group_thread")),
85
+ )
86
+ with op.batch_alter_table("group_thread", schema=None) as batch_op:
87
+ batch_op.create_index(
88
+ "ix_direct_group_thread_id", ["legacy_id", "foreign_key"], unique=False
89
+ )
90
+
91
+ with op.batch_alter_table("xmpp_ids_multi", schema=None) as batch_op:
92
+ batch_op.drop_index("legacy_ids_multi_user_account_id_xmpp_id")
93
+
94
+ op.drop_table("xmpp_ids_multi")
95
+ with op.batch_alter_table("xmpp_to_legacy_ids", schema=None) as batch_op:
96
+ batch_op.drop_index("xmpp_legacy")
97
+
98
+ op.drop_table("xmpp_to_legacy_ids")
99
+ with op.batch_alter_table("legacy_ids_multi", schema=None) as batch_op:
100
+ batch_op.drop_index("legacy_ids_multi_user_account_id_legacy_id")
101
+
102
+ op.drop_table("legacy_ids_multi")
103
+ # ### end Alembic commands ###
104
+
105
+
106
+ def downgrade() -> None:
107
+ # ### commands auto generated by Alembic - please adjust! ###
108
+ op.create_table(
109
+ "legacy_ids_multi",
110
+ sa.Column("id", sa.INTEGER(), nullable=False),
111
+ sa.Column("user_account_id", sa.INTEGER(), nullable=False),
112
+ sa.Column("legacy_id", sa.VARCHAR(), nullable=False),
113
+ sa.ForeignKeyConstraint(
114
+ ["user_account_id"],
115
+ ["user_account.id"],
116
+ ),
117
+ sa.PrimaryKeyConstraint("id"),
118
+ )
119
+ with op.batch_alter_table("legacy_ids_multi", schema=None) as batch_op:
120
+ batch_op.create_index(
121
+ "legacy_ids_multi_user_account_id_legacy_id",
122
+ ["user_account_id", "legacy_id"],
123
+ unique=1,
124
+ )
125
+
126
+ op.create_table(
127
+ "xmpp_to_legacy_ids",
128
+ sa.Column("id", sa.INTEGER(), nullable=False),
129
+ sa.Column("user_account_id", sa.INTEGER(), nullable=False),
130
+ sa.Column("xmpp_id", sa.VARCHAR(), nullable=False),
131
+ sa.Column("legacy_id", sa.VARCHAR(), nullable=False),
132
+ sa.Column("type", sa.VARCHAR(length=10), nullable=False),
133
+ sa.ForeignKeyConstraint(
134
+ ["user_account_id"],
135
+ ["user_account.id"],
136
+ ),
137
+ sa.PrimaryKeyConstraint("id"),
138
+ )
139
+ with op.batch_alter_table("xmpp_to_legacy_ids", schema=None) as batch_op:
140
+ batch_op.create_index(
141
+ "xmpp_legacy", ["user_account_id", "xmpp_id", "legacy_id"], unique=1
142
+ )
143
+
144
+ op.create_table(
145
+ "xmpp_ids_multi",
146
+ sa.Column("id", sa.INTEGER(), nullable=False),
147
+ sa.Column("user_account_id", sa.INTEGER(), nullable=False),
148
+ sa.Column("xmpp_id", sa.VARCHAR(), nullable=False),
149
+ sa.Column("legacy_ids_multi_id", sa.INTEGER(), nullable=False),
150
+ sa.ForeignKeyConstraint(
151
+ ["legacy_ids_multi_id"],
152
+ ["legacy_ids_multi.id"],
153
+ ),
154
+ sa.ForeignKeyConstraint(
155
+ ["user_account_id"],
156
+ ["user_account.id"],
157
+ ),
158
+ sa.PrimaryKeyConstraint("id"),
159
+ )
160
+ with op.batch_alter_table("xmpp_ids_multi", schema=None) as batch_op:
161
+ batch_op.create_index(
162
+ "legacy_ids_multi_user_account_id_xmpp_id",
163
+ ["user_account_id", "xmpp_id"],
164
+ unique=1,
165
+ )
166
+
167
+ with op.batch_alter_table("group_thread", schema=None) as batch_op:
168
+ batch_op.drop_index("ix_direct_group_thread_id")
169
+
170
+ op.drop_table("group_thread")
171
+ with op.batch_alter_table("group_msg", schema=None) as batch_op:
172
+ batch_op.drop_index("ix_group_msg_legacy_id")
173
+
174
+ op.drop_table("group_msg")
175
+ with op.batch_alter_table("direct_thread", schema=None) as batch_op:
176
+ batch_op.drop_index("ix_direct_direct_thread_id")
177
+
178
+ op.drop_table("direct_thread")
179
+ with op.batch_alter_table("direct_msg", schema=None) as batch_op:
180
+ batch_op.drop_index("ix_direct_msg_legacy_id")
181
+
182
+ op.drop_table("direct_msg")
183
+ # ### end Alembic commands ###
@@ -0,0 +1,56 @@
1
+ """New avatar store
2
+
3
+ Revision ID: 4dbd23a3f868
4
+ Revises: 04cf35e3cf85
5
+ Create Date: 2025-04-14 21:57:49.030430
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 = "4dbd23a3f868"
16
+ down_revision: Union[str, None] = "04cf35e3cf85"
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
+ with op.batch_alter_table("avatar", schema=None) as batch_op:
23
+ batch_op.add_column(sa.Column("legacy_id", sa.String(), nullable=True))
24
+ batch_op.create_unique_constraint("avatar_unique_legacy_id", ["legacy_id"])
25
+
26
+ batch_op.execute("""
27
+ UPDATE avatar
28
+ SET legacy_id = contact.avatar_legacy_id
29
+ FROM contact
30
+ WHERE avatar.id = contact.avatar_id
31
+ """)
32
+
33
+ batch_op.execute("""
34
+ UPDATE avatar
35
+ SET legacy_id = room.avatar_legacy_id
36
+ FROM room
37
+ WHERE avatar.id = room.avatar_id
38
+ """)
39
+
40
+ with op.batch_alter_table("contact", schema=None) as batch_op:
41
+ batch_op.drop_column("avatar_legacy_id")
42
+
43
+ with op.batch_alter_table("room", schema=None) as batch_op:
44
+ batch_op.drop_column("avatar_legacy_id")
45
+
46
+
47
+ def downgrade() -> None:
48
+ with op.batch_alter_table("room", schema=None) as batch_op:
49
+ batch_op.add_column(sa.Column("avatar_legacy_id", sa.VARCHAR(), nullable=True))
50
+
51
+ with op.batch_alter_table("contact", schema=None) as batch_op:
52
+ batch_op.add_column(sa.Column("avatar_legacy_id", sa.VARCHAR(), nullable=True))
53
+
54
+ with op.batch_alter_table("avatar", schema=None) as batch_op:
55
+ batch_op.drop_constraint("avatar_unique_legacy_id", type_="unique")
56
+ batch_op.drop_column("legacy_id")
@@ -0,0 +1,50 @@
1
+ """Use hash for avatar filenames
2
+
3
+ Revision ID: 54ce3cde350c
4
+ Revises: 4dbd23a3f868
5
+ Create Date: 2025-04-14 23:52:38.520744
6
+
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Sequence, Union
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "54ce3cde350c"
17
+ down_revision: Union[str, None] = "4dbd23a3f868"
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
+ from slidge.core import config
24
+
25
+ try:
26
+ avatars_root = config.HOME_DIR / "slidge_avatars_v3"
27
+ except AttributeError:
28
+ avatars_root = Path(".")
29
+
30
+ connection = op.get_bind()
31
+ avatars = connection.execute(sa.text("SELECT id, filename, hash FROM avatar"))
32
+ for avatar in avatars:
33
+ filename = avatar[1]
34
+ hash_value = avatar[2]
35
+ if filename and hash_value:
36
+ old_file_path = avatars_root / filename
37
+ new_file_path = (avatars_root / hash_value).with_suffix(".png")
38
+ if old_file_path.exists():
39
+ old_file_path.rename(new_file_path)
40
+
41
+ with op.batch_alter_table("avatar", schema=None) as batch_op:
42
+ batch_op.drop_column("filename")
43
+
44
+
45
+ def downgrade() -> None:
46
+ # ### commands auto generated by Alembic - please adjust! ###
47
+ with op.batch_alter_table("avatar", schema=None) as batch_op:
48
+ batch_op.add_column(sa.Column("filename", sa.VARCHAR(), nullable=False))
49
+
50
+ # ### end Alembic commands ###