slidge 0.3.0a2__py3-none-any.whl → 0.3.0b1__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 (39) hide show
  1. slidge/contact/contact.py +2 -2
  2. slidge/core/gateway.py +3 -4
  3. slidge/core/mixins/attachment.py +36 -2
  4. slidge/core/mixins/avatar.py +22 -5
  5. slidge/core/session.py +6 -2
  6. slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +361 -0
  7. slidge/db/models.py +1 -0
  8. slidge/group/participant.py +1 -1
  9. slidge/group/room.py +26 -52
  10. slidge/migration.py +14 -5
  11. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/METADATA +1 -1
  12. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/RECORD +16 -38
  13. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/WHEEL +1 -1
  14. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +0 -183
  15. slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py +0 -33
  16. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -36
  17. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +0 -85
  18. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -36
  19. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -37
  20. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -41
  21. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +0 -52
  22. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +0 -42
  23. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +0 -105
  24. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +0 -50
  25. slidge/db/alembic/versions/58b98dacf819_refactor.py +0 -118
  26. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +0 -61
  27. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +0 -74
  28. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -48
  29. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -43
  30. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -139
  31. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -50
  32. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +0 -79
  33. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -214
  34. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -52
  35. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -34
  36. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -26
  37. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/entry_points.txt +0 -0
  38. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/licenses/LICENSE +0 -0
  39. {slidge-0.3.0a2.dist-info → slidge-0.3.0b1.dist-info}/top_level.txt +0 -0
slidge/contact/contact.py CHANGED
@@ -95,7 +95,7 @@ class LegacyContact(
95
95
  super().__init__()
96
96
 
97
97
  @property
98
- def jid(self):
98
+ def jid(self): # type:ignore[override]
99
99
  jid = JID(self.stored.jid)
100
100
  jid.resource = self.RESOURCE
101
101
  return jid
@@ -298,7 +298,7 @@ class LegacyContact(
298
298
  )
299
299
  self.commit()
300
300
  for p in self.participants:
301
- p.nickname = n
301
+ p.nickname = n or str(self.legacy_id)
302
302
 
303
303
  def _post_avatar_update(self, cached_avatar) -> None:
304
304
  if self.is_friend and self.added_to_roster:
slidge/core/gateway.py CHANGED
@@ -472,7 +472,7 @@ class BaseGateway(
472
472
  )
473
473
 
474
474
  @property # type: ignore
475
- def jid(self):
475
+ def jid(self): # type:ignore[override]
476
476
  # Override to avoid slixmpp deprecation warnings.
477
477
  return self.boundjid
478
478
 
@@ -906,16 +906,15 @@ class BaseGateway(
906
906
  await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid)
907
907
  await self.xmpp._session_cls.kill_by_jid(user.jid)
908
908
 
909
- async def unregister(self, user: GatewayUser) -> None:
909
+ async def unregister(self, session: BaseSession) -> None:
910
910
  """
911
911
  Optionally override this if you need to clean additional
912
912
  stuff after a user has been removed from the persistent user store.
913
913
 
914
914
  By default, this just calls :meth:`BaseSession.logout`.
915
915
 
916
- :param user:
916
+ :param session: The session of the user who just unregistered
917
917
  """
918
- session = self.get_session_from_user(user)
919
918
  try:
920
919
  await session.logout()
921
920
  except NotImplementedError:
@@ -141,11 +141,14 @@ class AttachmentMixin(TextMessageMixin):
141
141
  return r.status < 400
142
142
 
143
143
  async def __get_stored(self, attachment: LegacyAttachment) -> Attachment:
144
- if attachment.legacy_file_id is not None:
144
+ if attachment.legacy_file_id is not None and self.session is not NotImplemented:
145
145
  with self.xmpp.store.session() as orm:
146
146
  stored = (
147
147
  orm.query(Attachment)
148
- .filter_by(legacy_file_id=str(attachment.legacy_file_id))
148
+ .filter_by(
149
+ legacy_file_id=str(attachment.legacy_file_id),
150
+ user_account_id=self.session.user_pk,
151
+ )
149
152
  .one_or_none()
150
153
  )
151
154
  if stored is not None:
@@ -400,6 +403,37 @@ class AttachmentMixin(TextMessageMixin):
400
403
  :param when: when the file was sent, for a "delay" tag (:xep:`0203`)
401
404
  :param thread:
402
405
  """
406
+ coro = self.__send_file(
407
+ attachment,
408
+ legacy_msg_id,
409
+ reply_to=reply_to,
410
+ when=when,
411
+ thread=thread,
412
+ **kwargs,
413
+ )
414
+ if self.session is NotImplemented:
415
+ return await coro
416
+ elif not isinstance(attachment, LegacyAttachment):
417
+ return await coro
418
+ elif attachment.legacy_file_id is None:
419
+ return await coro
420
+ else:
421
+ # prevents race conditions where we download the same thing several time
422
+ # and end up attempting to insert it twice in the DB, raising an
423
+ # IntegrityError.
424
+ async with self.session.lock(("attachment", attachment.legacy_file_id)):
425
+ return await coro
426
+
427
+ async def __send_file(
428
+ self,
429
+ attachment: LegacyAttachment | Path | str,
430
+ legacy_msg_id: Optional[LegacyMessageType] = None,
431
+ *,
432
+ reply_to: Optional[MessageReference] = None,
433
+ when: Optional[datetime] = None,
434
+ thread: Optional[LegacyThreadType] = None,
435
+ **kwargs,
436
+ ) -> tuple[Optional[str], list[Message]]:
403
437
  store_multi = kwargs.pop("store_multi", True)
404
438
  carbon = kwargs.pop("carbon", False)
405
439
  mto = kwargs.pop("mto", None)
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Optional
4
4
 
5
5
  from PIL import UnidentifiedImageError
6
6
  from slixmpp import JID
7
+ from sqlalchemy.exc import IntegrityError
7
8
  from sqlalchemy.orm.exc import DetachedInstanceError
8
9
 
9
10
  from ...db.avatar import CachedAvatar, avatar_cache
@@ -122,11 +123,27 @@ class AvatarMixin(UpdateInfoMixin):
122
123
  else:
123
124
  avatar.path.unlink()
124
125
 
125
- if cached_avatar is None:
126
- self.stored.avatar = None
127
- else:
128
- self.stored.avatar = cached_avatar.stored
129
- self.commit(merge=True)
126
+ stored_avatar = None if cached_avatar is None else cached_avatar.stored
127
+ self.stored.avatar = stored_avatar
128
+
129
+ try:
130
+ self.commit(merge=True)
131
+ except IntegrityError as e:
132
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
133
+ if orm.object_session(self.stored):
134
+ self.log.debug(
135
+ "Hit integrity error, attempting to fix by refreshing participants"
136
+ )
137
+ orm.refresh(self.stored, ["participants"])
138
+ else:
139
+ self.log.debug(
140
+ "Hit integrity error, attempting to fix by merging contact.stored"
141
+ )
142
+ self.stored = orm.merge(self.stored)
143
+ self.stored.avatar = stored_avatar
144
+ orm.add(self.stored)
145
+ orm.commit()
146
+
130
147
  self._post_avatar_update(cached_avatar)
131
148
 
132
149
  def get_cached_avatar(self) -> Optional["CachedAvatar"]:
slidge/core/session.py CHANGED
@@ -24,6 +24,7 @@ from ..db.models import Contact, GatewayUser
24
24
  from ..group.bookmarks import LegacyBookmarks
25
25
  from ..group.room import LegacyMUC
26
26
  from ..util import ABCSubclassableOnceAtMost
27
+ from ..util.lock import NamedLockMixin
27
28
  from ..util.types import (
28
29
  LegacyGroupIdType,
29
30
  LegacyMessageType,
@@ -50,7 +51,9 @@ class CachedPresence(NamedTuple):
50
51
 
51
52
 
52
53
  class BaseSession(
53
- Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost
54
+ Generic[LegacyMessageType, RecipientType],
55
+ NamedLockMixin,
56
+ metaclass=ABCSubclassableOnceAtMost,
54
57
  ):
55
58
  """
56
59
  The session of a registered :term:`User`.
@@ -95,6 +98,7 @@ class BaseSession(
95
98
  _bookmarks_cls: Type[LegacyBookmarks]
96
99
 
97
100
  def __init__(self, user: GatewayUser) -> None:
101
+ super().__init__()
98
102
  self.user = user
99
103
  self.log = logging.getLogger(user.jid.bare)
100
104
 
@@ -704,8 +708,8 @@ class BaseSession(
704
708
  log.warning("User not found during unregistration")
705
709
  return
706
710
 
711
+ await cls.xmpp.unregister(session)
707
712
  with cls.xmpp.store.session() as orm:
708
- await cls.xmpp.unregister(session.user)
709
713
  orm.delete(session.user)
710
714
  orm.commit()
711
715
 
@@ -0,0 +1,361 @@
1
+ """Initial schema
2
+
3
+ Revision ID: cef02a8b1451
4
+ Revises:
5
+ Create Date: 2025-08-28 12:48:16.890606
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ import slidge
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "cef02a8b1451"
18
+ down_revision: Union[str, None] = None
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
+ op.create_table(
26
+ "avatar",
27
+ sa.Column("id", sa.Integer(), nullable=False),
28
+ sa.Column("hash", sa.String(), nullable=False),
29
+ sa.Column("height", sa.Integer(), nullable=False),
30
+ sa.Column("width", sa.Integer(), nullable=False),
31
+ sa.Column("legacy_id", sa.String(), nullable=True),
32
+ sa.Column("url", sa.String(), nullable=True),
33
+ sa.Column("etag", sa.String(), nullable=True),
34
+ sa.Column("last_modified", sa.String(), nullable=True),
35
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_avatar")),
36
+ sa.UniqueConstraint("hash", name=op.f("uq_avatar_hash")),
37
+ sa.UniqueConstraint("legacy_id", name=op.f("uq_avatar_legacy_id")),
38
+ )
39
+ op.create_table(
40
+ "bob",
41
+ sa.Column("id", sa.Integer(), nullable=False),
42
+ sa.Column("file_name", sa.String(), nullable=False),
43
+ sa.Column("sha_1", sa.String(), nullable=False),
44
+ sa.Column("sha_256", sa.String(), nullable=False),
45
+ sa.Column("sha_512", sa.String(), nullable=False),
46
+ sa.Column("content_type", sa.String(), nullable=False),
47
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_bob")),
48
+ sa.UniqueConstraint("sha_1", name=op.f("uq_bob_sha_1")),
49
+ sa.UniqueConstraint("sha_256", name=op.f("uq_bob_sha_256")),
50
+ sa.UniqueConstraint("sha_512", name=op.f("uq_bob_sha_512")),
51
+ )
52
+ op.create_table(
53
+ "user_account",
54
+ sa.Column("id", sa.Integer(), nullable=False),
55
+ sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
56
+ sa.Column(
57
+ "registration_date",
58
+ sa.DateTime(),
59
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
60
+ nullable=False,
61
+ ),
62
+ sa.Column(
63
+ "legacy_module_data", slidge.db.meta.JSONEncodedDict(), nullable=False
64
+ ),
65
+ sa.Column("preferences", slidge.db.meta.JSONEncodedDict(), nullable=False),
66
+ sa.Column("avatar_hash", sa.String(), nullable=True),
67
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_user_account")),
68
+ sa.UniqueConstraint("jid", name=op.f("uq_user_account_jid")),
69
+ )
70
+ op.create_table(
71
+ "attachment",
72
+ sa.Column("id", sa.Integer(), nullable=False),
73
+ sa.Column("user_account_id", sa.Integer(), nullable=False),
74
+ sa.Column("legacy_file_id", sa.String(), nullable=True),
75
+ sa.Column("url", sa.String(), nullable=False),
76
+ sa.Column("sims", sa.String(), nullable=True),
77
+ sa.Column("sfs", sa.String(), nullable=True),
78
+ sa.ForeignKeyConstraint(
79
+ ["user_account_id"],
80
+ ["user_account.id"],
81
+ name=op.f("fk_attachment_user_account_id_user_account"),
82
+ ),
83
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_attachment")),
84
+ sa.UniqueConstraint(
85
+ "user_account_id",
86
+ "legacy_file_id",
87
+ name=op.f("uq_attachment_user_account_id"),
88
+ ),
89
+ )
90
+ with op.batch_alter_table("attachment", schema=None) as batch_op:
91
+ batch_op.create_index(
92
+ batch_op.f("ix_attachment_legacy_file_id"), ["legacy_file_id"], unique=False
93
+ )
94
+ batch_op.create_index(batch_op.f("ix_attachment_url"), ["url"], unique=False)
95
+
96
+ op.create_table(
97
+ "contact",
98
+ sa.Column("id", sa.Integer(), nullable=False),
99
+ sa.Column("user_account_id", sa.Integer(), nullable=False),
100
+ sa.Column("legacy_id", sa.String(), nullable=False),
101
+ sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
102
+ sa.Column("avatar_id", sa.Integer(), nullable=True),
103
+ sa.Column("nick", sa.String(), nullable=True),
104
+ sa.Column("cached_presence", sa.Boolean(), nullable=False),
105
+ sa.Column("last_seen", sa.DateTime(), nullable=True),
106
+ sa.Column("ptype", sa.String(), nullable=True),
107
+ sa.Column("pstatus", sa.String(), nullable=True),
108
+ sa.Column("pshow", sa.String(), nullable=True),
109
+ sa.Column("caps_ver", sa.String(), nullable=True),
110
+ sa.Column("is_friend", sa.Boolean(), nullable=False),
111
+ sa.Column("added_to_roster", sa.Boolean(), nullable=False),
112
+ sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True),
113
+ sa.Column("updated", sa.Boolean(), nullable=False),
114
+ sa.Column("vcard", sa.String(), nullable=True),
115
+ sa.Column("vcard_fetched", sa.Boolean(), nullable=False),
116
+ sa.Column(
117
+ "client_type",
118
+ sa.Enum(
119
+ "bot",
120
+ "console",
121
+ "game",
122
+ "handheld",
123
+ "pc",
124
+ "phone",
125
+ "sms",
126
+ "tablet",
127
+ "web",
128
+ native_enum=False,
129
+ ),
130
+ nullable=False,
131
+ ),
132
+ sa.ForeignKeyConstraint(
133
+ ["avatar_id"], ["avatar.id"], name=op.f("fk_contact_avatar_id_avatar")
134
+ ),
135
+ sa.ForeignKeyConstraint(
136
+ ["user_account_id"],
137
+ ["user_account.id"],
138
+ name=op.f("fk_contact_user_account_id_user_account"),
139
+ ),
140
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_contact")),
141
+ sa.UniqueConstraint(
142
+ "user_account_id", "jid", name=op.f("uq_contact_user_account_id")
143
+ ),
144
+ sa.UniqueConstraint(
145
+ "user_account_id", "legacy_id", name=op.f("uq_contact_user_account_id")
146
+ ),
147
+ )
148
+ op.create_table(
149
+ "room",
150
+ sa.Column("id", sa.Integer(), nullable=False),
151
+ sa.Column("user_account_id", sa.Integer(), nullable=False),
152
+ sa.Column("legacy_id", sa.String(), nullable=False),
153
+ sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
154
+ sa.Column("avatar_id", sa.Integer(), nullable=True),
155
+ sa.Column("name", sa.String(), nullable=True),
156
+ sa.Column("description", sa.String(), nullable=True),
157
+ sa.Column("subject", sa.String(), nullable=True),
158
+ sa.Column("subject_date", sa.DateTime(), nullable=True),
159
+ sa.Column("subject_setter", sa.String(), nullable=True),
160
+ sa.Column("n_participants", sa.Integer(), nullable=True),
161
+ sa.Column(
162
+ "muc_type",
163
+ sa.Enum("GROUP", "CHANNEL", "CHANNEL_NON_ANONYMOUS", name="muctype"),
164
+ nullable=False,
165
+ ),
166
+ sa.Column("user_nick", sa.String(), nullable=True),
167
+ sa.Column("user_resources", sa.String(), nullable=True),
168
+ sa.Column("participants_filled", sa.Boolean(), nullable=False),
169
+ sa.Column("history_filled", sa.Boolean(), nullable=False),
170
+ sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True),
171
+ sa.Column("updated", sa.Boolean(), nullable=False),
172
+ sa.ForeignKeyConstraint(
173
+ ["avatar_id"], ["avatar.id"], name=op.f("fk_room_avatar_id_avatar")
174
+ ),
175
+ sa.ForeignKeyConstraint(
176
+ ["user_account_id"],
177
+ ["user_account.id"],
178
+ name=op.f("fk_room_user_account_id_user_account"),
179
+ ),
180
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_room")),
181
+ sa.UniqueConstraint(
182
+ "user_account_id", "jid", name="uq_room_user_account_id_jid"
183
+ ),
184
+ sa.UniqueConstraint(
185
+ "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
186
+ ),
187
+ )
188
+ op.create_table(
189
+ "contact_sent",
190
+ sa.Column("id", sa.Integer(), nullable=False),
191
+ sa.Column("contact_id", sa.Integer(), nullable=False),
192
+ sa.Column("msg_id", sa.String(), nullable=False),
193
+ sa.ForeignKeyConstraint(
194
+ ["contact_id"],
195
+ ["contact.id"],
196
+ name=op.f("fk_contact_sent_contact_id_contact"),
197
+ ),
198
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_contact_sent")),
199
+ sa.UniqueConstraint(
200
+ "contact_id", "msg_id", name=op.f("uq_contact_sent_contact_id")
201
+ ),
202
+ )
203
+ op.create_table(
204
+ "direct_msg",
205
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
206
+ sa.Column("id", sa.Integer(), nullable=False),
207
+ sa.Column("legacy_id", sa.String(), nullable=False),
208
+ sa.Column("xmpp_id", sa.String(), nullable=False),
209
+ sa.ForeignKeyConstraint(
210
+ ["foreign_key"],
211
+ ["contact.id"],
212
+ name=op.f("fk_direct_msg_foreign_key_contact"),
213
+ ),
214
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_direct_msg")),
215
+ )
216
+ with op.batch_alter_table("direct_msg", schema=None) as batch_op:
217
+ batch_op.create_index(
218
+ "ix_direct_msg_legacy_id", ["legacy_id", "foreign_key"], unique=False
219
+ )
220
+
221
+ op.create_table(
222
+ "direct_thread",
223
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
224
+ sa.Column("id", sa.Integer(), nullable=False),
225
+ sa.Column("legacy_id", sa.String(), nullable=False),
226
+ sa.Column("xmpp_id", sa.String(), nullable=False),
227
+ sa.ForeignKeyConstraint(
228
+ ["foreign_key"],
229
+ ["contact.id"],
230
+ name=op.f("fk_direct_thread_foreign_key_contact"),
231
+ ),
232
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_direct_thread")),
233
+ )
234
+ with op.batch_alter_table("direct_thread", schema=None) as batch_op:
235
+ batch_op.create_index(
236
+ "ix_direct_direct_thread_id", ["legacy_id", "foreign_key"], unique=False
237
+ )
238
+
239
+ op.create_table(
240
+ "group_msg",
241
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
242
+ sa.Column("id", sa.Integer(), nullable=False),
243
+ sa.Column("legacy_id", sa.String(), nullable=False),
244
+ sa.Column("xmpp_id", sa.String(), nullable=False),
245
+ sa.ForeignKeyConstraint(
246
+ ["foreign_key"], ["room.id"], name=op.f("fk_group_msg_foreign_key_room")
247
+ ),
248
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_group_msg")),
249
+ )
250
+ with op.batch_alter_table("group_msg", schema=None) as batch_op:
251
+ batch_op.create_index(
252
+ "ix_group_msg_legacy_id", ["legacy_id", "foreign_key"], unique=False
253
+ )
254
+
255
+ op.create_table(
256
+ "group_thread",
257
+ sa.Column("foreign_key", sa.Integer(), nullable=False),
258
+ sa.Column("id", sa.Integer(), nullable=False),
259
+ sa.Column("legacy_id", sa.String(), nullable=False),
260
+ sa.Column("xmpp_id", sa.String(), nullable=False),
261
+ sa.ForeignKeyConstraint(
262
+ ["foreign_key"], ["room.id"], name=op.f("fk_group_thread_foreign_key_room")
263
+ ),
264
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_group_thread")),
265
+ )
266
+ with op.batch_alter_table("group_thread", schema=None) as batch_op:
267
+ batch_op.create_index(
268
+ "ix_direct_group_thread_id", ["legacy_id", "foreign_key"], unique=False
269
+ )
270
+
271
+ op.create_table(
272
+ "mam",
273
+ sa.Column("id", sa.Integer(), nullable=False),
274
+ sa.Column("room_id", sa.Integer(), nullable=False),
275
+ sa.Column("stanza_id", sa.String(), nullable=False),
276
+ sa.Column("timestamp", sa.DateTime(), nullable=False),
277
+ sa.Column("author_jid", slidge.db.meta.JIDType(), nullable=False),
278
+ sa.Column(
279
+ "source",
280
+ sa.Enum("LIVE", "BACKFILL", name="archivedmessagesource"),
281
+ nullable=False,
282
+ ),
283
+ sa.Column("legacy_id", sa.String(), nullable=True),
284
+ sa.Column("stanza", sa.String(), nullable=False),
285
+ sa.ForeignKeyConstraint(
286
+ ["room_id"], ["room.id"], name=op.f("fk_mam_room_id_room")
287
+ ),
288
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_mam")),
289
+ sa.UniqueConstraint("room_id", "stanza_id", name=op.f("uq_mam_room_id")),
290
+ )
291
+ op.create_table(
292
+ "participant",
293
+ sa.Column("id", sa.Integer(), nullable=False),
294
+ sa.Column("room_id", sa.Integer(), nullable=False),
295
+ sa.Column("contact_id", sa.Integer(), nullable=True),
296
+ sa.Column("is_user", sa.Boolean(), nullable=False),
297
+ sa.Column(
298
+ "affiliation",
299
+ sa.Enum("outcast", "member", "admin", "owner", "none", native_enum=False),
300
+ nullable=False,
301
+ ),
302
+ sa.Column(
303
+ "role",
304
+ sa.Enum("moderator", "participant", "visitor", "none", native_enum=False),
305
+ nullable=False,
306
+ ),
307
+ sa.Column("presence_sent", sa.Boolean(), nullable=False),
308
+ sa.Column("resource", sa.String(), nullable=False),
309
+ sa.Column("nickname", sa.String(), nullable=False),
310
+ sa.Column("nickname_no_illegal", sa.String(), nullable=False),
311
+ sa.Column("hats", sa.JSON(), nullable=False),
312
+ sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True),
313
+ sa.ForeignKeyConstraint(
314
+ ["contact_id"],
315
+ ["contact.id"],
316
+ name=op.f("fk_participant_contact_id_contact"),
317
+ ),
318
+ sa.ForeignKeyConstraint(
319
+ ["room_id"], ["room.id"], name=op.f("fk_participant_room_id_room")
320
+ ),
321
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_participant")),
322
+ sa.UniqueConstraint(
323
+ "room_id", "contact_id", name=op.f("uq_participant_room_id")
324
+ ),
325
+ sa.UniqueConstraint("room_id", "resource", name=op.f("uq_participant_room_id")),
326
+ )
327
+ # ### end Alembic commands ###
328
+
329
+
330
+ def downgrade() -> None:
331
+ # ### commands auto generated by Alembic - please adjust! ###
332
+ op.drop_table("participant")
333
+ op.drop_table("mam")
334
+ with op.batch_alter_table("group_thread", schema=None) as batch_op:
335
+ batch_op.drop_index("ix_direct_group_thread_id")
336
+
337
+ op.drop_table("group_thread")
338
+ with op.batch_alter_table("group_msg", schema=None) as batch_op:
339
+ batch_op.drop_index("ix_group_msg_legacy_id")
340
+
341
+ op.drop_table("group_msg")
342
+ with op.batch_alter_table("direct_thread", schema=None) as batch_op:
343
+ batch_op.drop_index("ix_direct_direct_thread_id")
344
+
345
+ op.drop_table("direct_thread")
346
+ with op.batch_alter_table("direct_msg", schema=None) as batch_op:
347
+ batch_op.drop_index("ix_direct_msg_legacy_id")
348
+
349
+ op.drop_table("direct_msg")
350
+ op.drop_table("contact_sent")
351
+ op.drop_table("room")
352
+ op.drop_table("contact")
353
+ with op.batch_alter_table("attachment", schema=None) as batch_op:
354
+ batch_op.drop_index(batch_op.f("ix_attachment_url"))
355
+ batch_op.drop_index(batch_op.f("ix_attachment_legacy_file_id"))
356
+
357
+ op.drop_table("attachment")
358
+ op.drop_table("user_account")
359
+ op.drop_table("bob")
360
+ op.drop_table("avatar")
361
+ # ### end Alembic commands ###
slidge/db/models.py CHANGED
@@ -300,6 +300,7 @@ class Attachment(Base):
300
300
  """
301
301
 
302
302
  __tablename__ = "attachment"
303
+ __table_args__ = (UniqueConstraint("user_account_id", "legacy_file_id"),)
303
304
 
304
305
  id: Mapped[int] = mapped_column(primary_key=True)
305
306
  user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
@@ -302,7 +302,7 @@ class LegacyParticipant(
302
302
  return p
303
303
 
304
304
  @property
305
- def DISCO_NAME(self):
305
+ def DISCO_NAME(self): # type:ignore[override]
306
306
  return self.nickname
307
307
 
308
308
  def __send_presence_if_needed(