Coverage for slidge / db / models.py: 98%
185 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-02-15 09:02 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-02-15 09:02 +0000
1import warnings
2from datetime import datetime
3from enum import IntEnum
5import sqlalchemy as sa
6from slixmpp import JID
7from slixmpp.types import MucAffiliation, MucRole
8from sqlalchemy import JSON, ForeignKey, Index, UniqueConstraint
9from sqlalchemy.orm import Mapped, mapped_column, relationship
11from ..util.types import ClientType, Hat, MucType
12from .meta import Base, JSONSerializable, JSONSerializableTypes
15class ArchivedMessageSource(IntEnum):
16 """
17 Whether an archived message comes from ``LegacyMUC.backfill()`` or was received
18 as a "live" message.
19 """
21 LIVE = 1
22 BACKFILL = 2
25class GatewayUser(Base):
26 """
27 A user, registered to the gateway component.
28 """
30 __tablename__ = "user_account"
31 id: Mapped[int] = mapped_column(primary_key=True)
32 jid: Mapped[JID] = mapped_column(unique=True)
33 registration_date: Mapped[datetime] = mapped_column(
34 sa.DateTime, server_default=sa.func.now()
35 )
37 legacy_module_data: Mapped[JSONSerializable] = mapped_column(default={})
38 """
39 Arbitrary non-relational data that legacy modules can use
40 """
41 preferences: Mapped[JSONSerializable] = mapped_column(default={})
42 avatar_hash: Mapped[str | None] = mapped_column(default=None)
43 """
44 Hash of the user's avatar, to avoid re-publishing the same avatar on the
45 legacy network
46 """
48 contacts: Mapped[list["Contact"]] = relationship(
49 back_populates="user", cascade="all, delete-orphan"
50 )
51 rooms: Mapped[list["Room"]] = relationship(
52 back_populates="user", cascade="all, delete-orphan"
53 )
54 attachments: Mapped[list["Attachment"]] = relationship(cascade="all, delete-orphan")
56 def __repr__(self) -> str:
57 return f"User(id={self.id!r}, jid={self.jid!r})"
59 def get(self, field: str, default: str = "") -> JSONSerializableTypes:
60 # """
61 # Get fields from the registration form (required to comply with slixmpp backend protocol)
62 #
63 # :param field: Name of the field
64 # :param default: Default value to return if the field is not present
65 #
66 # :return: Value of the field
67 # """
68 return self.legacy_module_data.get(field, default)
70 @property
71 def registration_form(self) -> dict:
72 # Kept for retrocompat, should be
73 # FIXME: delete me
74 warnings.warn(
75 "GatewayUser.registration_form is deprecated.", DeprecationWarning
76 )
77 return self.legacy_module_data
80class Avatar(Base):
81 """
82 Avatars of contacts, rooms and participants.
84 To comply with XEPs, we convert them all to PNG before storing them.
85 """
87 __tablename__ = "avatar"
89 id: Mapped[int] = mapped_column(primary_key=True)
91 hash: Mapped[str] = mapped_column(unique=True)
92 height: Mapped[int] = mapped_column()
93 width: Mapped[int] = mapped_column()
95 legacy_id: Mapped[str | None] = mapped_column(unique=True, nullable=True)
97 # this is only used when avatars are available as HTTP URLs and do not
98 # have a legacy_id
99 url: Mapped[str | None] = mapped_column(unique=True, default=None)
100 etag: Mapped[str | None] = mapped_column(default=None)
101 last_modified: Mapped[str | None] = mapped_column(default=None)
103 contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar")
104 rooms: Mapped[list["Room"]] = relationship(back_populates="avatar")
107class Contact(Base):
108 """
109 Legacy contacts
110 """
112 __tablename__ = "contact"
113 __table_args__ = (
114 UniqueConstraint(
115 "user_account_id", "legacy_id", name="uq_contact_user_account_id_legacy_id"
116 ),
117 UniqueConstraint(
118 "user_account_id", "jid", name="uq_contact_user_account_id_jid"
119 ),
120 )
122 id: Mapped[int] = mapped_column(primary_key=True)
123 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
124 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="contacts")
125 legacy_id: Mapped[str] = mapped_column(nullable=False)
127 jid: Mapped[JID] = mapped_column()
129 avatar_id: Mapped[int | None] = mapped_column(
130 ForeignKey("avatar.id"), nullable=True
131 )
132 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="contacts")
134 nick: Mapped[str | None] = mapped_column(nullable=True)
136 cached_presence: Mapped[bool] = mapped_column(default=False)
137 last_seen: Mapped[datetime | None] = mapped_column(nullable=True)
138 ptype: Mapped[str | None] = mapped_column(nullable=True)
139 pstatus: Mapped[str | None] = mapped_column(nullable=True)
140 pshow: Mapped[str | None] = mapped_column(nullable=True)
141 caps_ver: Mapped[str | None] = mapped_column(nullable=True)
143 is_friend: Mapped[bool] = mapped_column(default=False)
144 added_to_roster: Mapped[bool] = mapped_column(default=False)
145 sent_order: Mapped[list["ContactSent"]] = relationship(
146 back_populates="contact", cascade="all, delete-orphan"
147 )
149 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(
150 default=None, nullable=True
151 )
152 updated: Mapped[bool] = mapped_column(default=False)
154 vcard: Mapped[str | None] = mapped_column()
155 vcard_fetched: Mapped[bool] = mapped_column(default=False)
157 participants: Mapped[list["Participant"]] = relationship(back_populates="contact")
159 client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc")
161 messages: Mapped[list["DirectMessages"]] = relationship(
162 cascade="all, delete-orphan"
163 )
164 threads: Mapped[list["DirectThreads"]] = relationship(cascade="all, delete-orphan")
167class ContactSent(Base):
168 """
169 Keep track of XMPP msg ids sent by a specific contact for networks in which
170 all messages need to be marked as read.
172 (XMPP displayed markers convey a "read up to here" semantic.)
173 """
175 __tablename__ = "contact_sent"
176 __table_args__ = (
177 UniqueConstraint(
178 "contact_id", "msg_id", name="uq_contact_sent_contact_id_msg_id"
179 ),
180 )
182 id: Mapped[int] = mapped_column(primary_key=True)
183 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
184 contact: Mapped[Contact] = relationship(back_populates="sent_order")
185 msg_id: Mapped[str] = mapped_column()
188class Room(Base):
189 """
190 Legacy room
191 """
193 __table_args__ = (
194 UniqueConstraint(
195 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
196 ),
197 UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"),
198 )
200 __tablename__ = "room"
201 id: Mapped[int] = mapped_column(primary_key=True)
202 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
203 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="rooms")
204 legacy_id: Mapped[str] = mapped_column(nullable=False)
206 jid: Mapped[JID] = mapped_column(nullable=False)
208 avatar_id: Mapped[int | None] = mapped_column(
209 ForeignKey("avatar.id"), nullable=True
210 )
211 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="rooms")
213 name: Mapped[str | None] = mapped_column(nullable=True)
214 description: Mapped[str | None] = mapped_column(nullable=True)
215 subject: Mapped[str | None] = mapped_column(nullable=True)
216 subject_date: Mapped[datetime | None] = mapped_column(nullable=True)
217 subject_setter: Mapped[str | None] = mapped_column(nullable=True)
219 n_participants: Mapped[int | None] = mapped_column(default=None)
221 muc_type: Mapped[MucType] = mapped_column(default=MucType.CHANNEL)
223 user_nick: Mapped[str | None] = mapped_column()
224 user_resources: Mapped[str | None] = mapped_column(nullable=True)
226 participants_filled: Mapped[bool] = mapped_column(default=False)
227 history_filled: Mapped[bool] = mapped_column(default=False)
229 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None)
230 updated: Mapped[bool] = mapped_column(default=False)
232 participants: Mapped[list["Participant"]] = relationship(
233 back_populates="room",
234 primaryjoin="Participant.room_id == Room.id",
235 cascade="all, delete-orphan",
236 )
238 archive: Mapped[list["ArchivedMessage"]] = relationship(
239 cascade="all, delete-orphan"
240 )
242 messages: Mapped[list["GroupMessages"]] = relationship(cascade="all, delete-orphan")
243 threads: Mapped[list["GroupThreads"]] = relationship(cascade="all, delete-orphan")
246class ArchivedMessage(Base):
247 """
248 Messages of rooms, that we store to act as a MAM server
249 """
251 __tablename__ = "mam"
252 __table_args__ = (
253 UniqueConstraint("room_id", "stanza_id", name="uq_mam_room_id_stanza_id"),
254 )
256 id: Mapped[int] = mapped_column(primary_key=True)
257 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
258 room: Mapped[Room] = relationship(lazy=True, back_populates="archive")
260 stanza_id: Mapped[str] = mapped_column(nullable=False)
261 timestamp: Mapped[datetime] = mapped_column(nullable=False)
262 author_jid: Mapped[JID] = mapped_column(nullable=False)
263 source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False)
264 legacy_id: Mapped[str | None] = mapped_column(nullable=True)
266 stanza: Mapped[str] = mapped_column(nullable=False)
268 displayed_by_user: Mapped[bool] = mapped_column(default=False, nullable=True)
271class _LegacyToXmppIdsBase:
272 """
273 XMPP-client generated IDs, and mapping to the corresponding legacy IDs.
275 A single legacy ID can map to several XMPP ids.
276 """
278 id: Mapped[int] = mapped_column(primary_key=True)
279 legacy_id: Mapped[str] = mapped_column(nullable=False)
280 xmpp_id: Mapped[str] = mapped_column(nullable=False)
283class DirectMessages(_LegacyToXmppIdsBase, Base):
284 __tablename__ = "direct_msg"
285 __table_args__ = (Index("ix_direct_msg_legacy_id", "legacy_id", "foreign_key"),)
286 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
289class GroupMessages(_LegacyToXmppIdsBase, Base):
290 __tablename__ = "group_msg"
291 __table_args__ = (Index("ix_group_msg_legacy_id", "legacy_id", "foreign_key"),)
292 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
295class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base):
296 """
297 This maps "origin ids" <message id=XXX> to legacy message IDs
298 We need that for message corrections and retractions, which do not reference
299 messages by their "Unique and Stable Stanza IDs (XEP-0359)"
300 """
302 __tablename__ = "group_msg_origin"
303 __table_args__ = (
304 Index("ix_group_msg_origin_legacy_id", "legacy_id", "foreign_key"),
305 )
306 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
309class DirectThreads(_LegacyToXmppIdsBase, Base):
310 __tablename__ = "direct_thread"
311 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),)
312 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
315class GroupThreads(_LegacyToXmppIdsBase, Base):
316 __tablename__ = "group_thread"
317 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),)
318 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
321class Attachment(Base):
322 """
323 Legacy attachments
324 """
326 __tablename__ = "attachment"
327 __table_args__ = (
328 UniqueConstraint(
329 "user_account_id",
330 "legacy_file_id",
331 name="uq_attachment_user_account_id_legacy_file_id",
332 ),
333 )
335 id: Mapped[int] = mapped_column(primary_key=True)
336 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
337 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
339 legacy_file_id: Mapped[str | None] = mapped_column(index=True, nullable=True)
340 url: Mapped[str] = mapped_column(index=True, nullable=False)
341 sims: Mapped[str | None] = mapped_column()
342 sfs: Mapped[str | None] = mapped_column()
345class Participant(Base):
346 __tablename__ = "participant"
347 __table_args__ = (
348 UniqueConstraint("room_id", "resource", name="uq_participant_room_id_resource"),
349 UniqueConstraint(
350 "room_id", "contact_id", name="uq_participant_room_id_contact_id"
351 ),
352 UniqueConstraint(
353 "room_id", "occupant_id", name="uq_participant_room_id_occupant_id"
354 ),
355 )
357 id: Mapped[int] = mapped_column(primary_key=True)
359 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
360 room: Mapped[Room] = relationship(
361 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id
362 )
364 contact_id: Mapped[int | None] = mapped_column(
365 ForeignKey("contact.id"), nullable=True
366 )
367 contact: Mapped[Contact | None] = relationship(
368 lazy=False, back_populates="participants"
369 )
371 occupant_id: Mapped[str] = mapped_column(nullable=False)
373 is_user: Mapped[bool] = mapped_column(default=False)
375 affiliation: Mapped[MucAffiliation] = mapped_column(
376 default="member", nullable=False
377 )
378 role: Mapped[MucRole] = mapped_column(default="participant", nullable=False)
380 presence_sent: Mapped[bool] = mapped_column(default=False)
382 resource: Mapped[str] = mapped_column(nullable=False)
383 nickname: Mapped[str] = mapped_column(nullable=False, default=None)
384 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
386 hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
388 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None)
390 def __init__(self, *args, **kwargs):
391 super().__init__(*args, **kwargs)
392 self.role = "participant"
393 self.affiliation = "member"
396class Bob(Base):
397 __tablename__ = "bob"
399 id: Mapped[int] = mapped_column(primary_key=True)
400 file_name: Mapped[str] = mapped_column(nullable=False)
402 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
403 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
404 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
406 content_type: Mapped[str | None] = mapped_column(nullable=False)