Coverage for slidge / db / models.py: 98%
186 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 05:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 05:07 +0000
1import warnings
2from datetime import datetime
3from enum import IntEnum
4from typing import Any
6import sqlalchemy as sa
7from slixmpp import JID
8from slixmpp.types import MucAffiliation, MucRole
9from sqlalchemy import JSON, ForeignKey, Index, UniqueConstraint
10from sqlalchemy.orm import Mapped, mapped_column, relationship
12from ..util.types import ClientType, Hat, MucType
13from .meta import Base, JSONSerializable, JSONSerializableTypes
16class ArchivedMessageSource(IntEnum):
17 """
18 Whether an archived message comes from ``LegacyMUC.backfill()`` or was received
19 as a "live" message.
20 """
22 LIVE = 1
23 BACKFILL = 2
26class GatewayUser(Base):
27 """
28 A user, registered to the gateway component.
29 """
31 __tablename__ = "user_account"
32 id: Mapped[int] = mapped_column(primary_key=True)
33 jid: Mapped[JID] = mapped_column(unique=True)
34 registration_date: Mapped[datetime] = mapped_column(
35 sa.DateTime, server_default=sa.func.now()
36 )
38 legacy_module_data: Mapped[JSONSerializable] = mapped_column(default={})
39 """
40 Arbitrary non-relational data that legacy modules can use
41 """
42 preferences: Mapped[JSONSerializable] = mapped_column(default={})
43 avatar_hash: Mapped[str | None] = mapped_column(default=None)
44 """
45 Hash of the user's avatar, to avoid re-publishing the same avatar on the
46 legacy network
47 """
49 contacts: Mapped[list["Contact"]] = relationship(
50 back_populates="user", cascade="all, delete-orphan"
51 )
52 rooms: Mapped[list["Room"]] = relationship(
53 back_populates="user", cascade="all, delete-orphan"
54 )
55 attachments: Mapped[list["Attachment"]] = relationship(cascade="all, delete-orphan")
57 def __repr__(self) -> str:
58 return f"User(id={self.id!r}, jid={self.jid!r})"
60 def get(self, field: str, default: str = "") -> JSONSerializableTypes:
61 # """
62 # Get fields from the registration form (required to comply with slixmpp backend protocol)
63 #
64 # :param field: Name of the field
65 # :param default: Default value to return if the field is not present
66 #
67 # :return: Value of the field
68 # """
69 return self.legacy_module_data.get(field, default)
71 @property
72 def registration_form(self) -> dict[str, Any]:
73 # Kept for retrocompat, should be
74 # FIXME: delete me
75 warnings.warn(
76 "GatewayUser.registration_form is deprecated.", DeprecationWarning
77 )
78 return self.legacy_module_data
81class Avatar(Base):
82 """
83 Avatars of contacts, rooms and participants.
85 To comply with XEPs, we convert them all to PNG before storing them.
86 """
88 __tablename__ = "avatar"
90 id: Mapped[int] = mapped_column(primary_key=True)
92 hash: Mapped[str] = mapped_column(unique=True)
93 height: Mapped[int] = mapped_column()
94 width: Mapped[int] = mapped_column()
96 legacy_id: Mapped[str | None] = mapped_column(unique=True, nullable=True)
98 # this is only used when avatars are available as HTTP URLs and do not
99 # have a legacy_id
100 url: Mapped[str | None] = mapped_column(unique=True, default=None)
101 etag: Mapped[str | None] = mapped_column(default=None)
102 last_modified: Mapped[str | None] = mapped_column(default=None)
104 contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar")
105 rooms: Mapped[list["Room"]] = relationship(back_populates="avatar")
108class Contact(Base):
109 """
110 Legacy contacts
111 """
113 __tablename__ = "contact"
114 __table_args__ = (
115 UniqueConstraint(
116 "user_account_id", "legacy_id", name="uq_contact_user_account_id_legacy_id"
117 ),
118 UniqueConstraint(
119 "user_account_id", "jid", name="uq_contact_user_account_id_jid"
120 ),
121 )
123 id: Mapped[int] = mapped_column(primary_key=True)
124 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
125 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="contacts")
126 legacy_id: Mapped[str] = mapped_column(nullable=False)
128 jid: Mapped[JID] = mapped_column()
130 avatar_id: Mapped[int | None] = mapped_column(
131 ForeignKey("avatar.id"), nullable=True
132 )
133 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="contacts")
135 nick: Mapped[str | None] = mapped_column(nullable=True)
137 cached_presence: Mapped[bool] = mapped_column(default=False)
138 last_seen: Mapped[datetime | None] = mapped_column(nullable=True)
139 ptype: Mapped[str | None] = mapped_column(nullable=True)
140 pstatus: Mapped[str | None] = mapped_column(nullable=True)
141 pshow: Mapped[str | None] = mapped_column(nullable=True)
142 caps_ver: Mapped[str | None] = mapped_column(nullable=True)
144 is_friend: Mapped[bool] = mapped_column(default=False)
145 added_to_roster: Mapped[bool] = mapped_column(default=False)
146 sent_order: Mapped[list["ContactSent"]] = relationship(
147 back_populates="contact", cascade="all, delete-orphan"
148 )
150 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(
151 default=None, nullable=True
152 )
153 updated: Mapped[bool] = mapped_column(default=False)
155 vcard: Mapped[str | None] = mapped_column()
156 vcard_fetched: Mapped[bool] = mapped_column(default=False)
158 participants: Mapped[list["Participant"]] = relationship(back_populates="contact")
160 client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc")
162 messages: Mapped[list["DirectMessages"]] = relationship(
163 cascade="all, delete-orphan"
164 )
165 threads: Mapped[list["DirectThreads"]] = relationship(cascade="all, delete-orphan")
168class ContactSent(Base):
169 """
170 Keep track of XMPP msg ids sent by a specific contact for networks in which
171 all messages need to be marked as read.
173 (XMPP displayed markers convey a "read up to here" semantic.)
174 """
176 __tablename__ = "contact_sent"
177 __table_args__ = (
178 UniqueConstraint(
179 "contact_id", "msg_id", name="uq_contact_sent_contact_id_msg_id"
180 ),
181 )
183 id: Mapped[int] = mapped_column(primary_key=True)
184 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
185 contact: Mapped[Contact] = relationship(back_populates="sent_order")
186 msg_id: Mapped[str] = mapped_column()
189class Room(Base):
190 """
191 Legacy room
192 """
194 __table_args__ = (
195 UniqueConstraint(
196 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
197 ),
198 UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"),
199 )
201 __tablename__ = "room"
202 id: Mapped[int] = mapped_column(primary_key=True)
203 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
204 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="rooms")
205 legacy_id: Mapped[str] = mapped_column(nullable=False)
207 jid: Mapped[JID] = mapped_column(nullable=False)
209 avatar_id: Mapped[int | None] = mapped_column(
210 ForeignKey("avatar.id"), nullable=True
211 )
212 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="rooms")
214 name: Mapped[str | None] = mapped_column(nullable=True)
215 description: Mapped[str | None] = mapped_column(nullable=True)
216 subject: Mapped[str | None] = mapped_column(nullable=True)
217 subject_date: Mapped[datetime | None] = mapped_column(nullable=True)
218 subject_setter: Mapped[str | None] = mapped_column(nullable=True)
220 n_participants: Mapped[int | None] = mapped_column(default=None)
222 muc_type: Mapped[MucType] = mapped_column(default=MucType.CHANNEL)
224 user_nick: Mapped[str | None] = mapped_column()
225 user_resources: Mapped[str | None] = mapped_column(nullable=True)
227 participants_filled: Mapped[bool] = mapped_column(default=False)
228 history_filled: Mapped[bool] = mapped_column(default=False)
230 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None)
231 updated: Mapped[bool] = mapped_column(default=False)
233 participants: Mapped[list["Participant"]] = relationship(
234 back_populates="room",
235 primaryjoin="Participant.room_id == Room.id",
236 cascade="all, delete-orphan",
237 )
239 archive: Mapped[list["ArchivedMessage"]] = relationship(
240 cascade="all, delete-orphan"
241 )
243 messages: Mapped[list["GroupMessages"]] = relationship(cascade="all, delete-orphan")
244 threads: Mapped[list["GroupThreads"]] = relationship(cascade="all, delete-orphan")
247class ArchivedMessage(Base):
248 """
249 Messages of rooms, that we store to act as a MAM server
250 """
252 __tablename__ = "mam"
253 __table_args__ = (
254 UniqueConstraint("room_id", "stanza_id", name="uq_mam_room_id_stanza_id"),
255 )
257 id: Mapped[int] = mapped_column(primary_key=True)
258 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
259 room: Mapped[Room] = relationship(lazy=True, back_populates="archive")
261 stanza_id: Mapped[str] = mapped_column(nullable=False)
262 timestamp: Mapped[datetime] = mapped_column(nullable=False)
263 author_jid: Mapped[JID] = mapped_column(nullable=False)
264 source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False)
265 legacy_id: Mapped[str | None] = mapped_column(nullable=True)
267 stanza: Mapped[str] = mapped_column(nullable=False)
269 displayed_by_user: Mapped[bool] = mapped_column(default=False, nullable=True)
272class _LegacyToXmppIdsBase:
273 """
274 XMPP-client generated IDs, and mapping to the corresponding legacy IDs.
276 A single legacy ID can map to several XMPP ids.
277 """
279 id: Mapped[int] = mapped_column(primary_key=True)
280 legacy_id: Mapped[str] = mapped_column(nullable=False)
281 xmpp_id: Mapped[str] = mapped_column(nullable=False)
284class DirectMessages(_LegacyToXmppIdsBase, Base):
285 __tablename__ = "direct_msg"
286 __table_args__ = (Index("ix_direct_msg_legacy_id", "legacy_id", "foreign_key"),)
287 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
290class GroupMessages(_LegacyToXmppIdsBase, Base):
291 __tablename__ = "group_msg"
292 __table_args__ = (Index("ix_group_msg_legacy_id", "legacy_id", "foreign_key"),)
293 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
296class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base):
297 """
298 This maps "origin ids" <message id=XXX> to legacy message IDs
299 We need that for message corrections and retractions, which do not reference
300 messages by their "Unique and Stable Stanza IDs (XEP-0359)"
301 """
303 __tablename__ = "group_msg_origin"
304 __table_args__ = (
305 Index("ix_group_msg_origin_legacy_id", "legacy_id", "foreign_key"),
306 )
307 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
310class DirectThreads(_LegacyToXmppIdsBase, Base):
311 __tablename__ = "direct_thread"
312 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),)
313 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
316class GroupThreads(_LegacyToXmppIdsBase, Base):
317 __tablename__ = "group_thread"
318 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),)
319 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
322class Attachment(Base):
323 """
324 Legacy attachments
325 """
327 __tablename__ = "attachment"
328 __table_args__ = (
329 UniqueConstraint(
330 "user_account_id",
331 "legacy_file_id",
332 name="uq_attachment_user_account_id_legacy_file_id",
333 ),
334 )
336 id: Mapped[int] = mapped_column(primary_key=True)
337 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
338 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
340 legacy_file_id: Mapped[str | None] = mapped_column(index=True, nullable=True)
341 url: Mapped[str] = mapped_column(index=True, nullable=False)
342 sims: Mapped[str | None] = mapped_column()
343 sfs: Mapped[str | None] = mapped_column()
346class Participant(Base):
347 __tablename__ = "participant"
348 __table_args__ = (
349 UniqueConstraint("room_id", "resource", name="uq_participant_room_id_resource"),
350 UniqueConstraint(
351 "room_id", "contact_id", name="uq_participant_room_id_contact_id"
352 ),
353 UniqueConstraint(
354 "room_id", "occupant_id", name="uq_participant_room_id_occupant_id"
355 ),
356 )
358 id: Mapped[int] = mapped_column(primary_key=True)
360 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
361 room: Mapped[Room] = relationship(
362 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id
363 )
365 contact_id: Mapped[int | None] = mapped_column(
366 ForeignKey("contact.id"), nullable=True
367 )
368 contact: Mapped[Contact | None] = relationship(
369 lazy=False, back_populates="participants"
370 )
372 occupant_id: Mapped[str] = mapped_column(nullable=False)
374 is_user: Mapped[bool] = mapped_column(default=False)
376 affiliation: Mapped[MucAffiliation] = mapped_column(
377 default="member", nullable=False
378 )
379 role: Mapped[MucRole] = mapped_column(default="participant", nullable=False)
381 presence_sent: Mapped[bool] = mapped_column(default=False)
383 resource: Mapped[str] = mapped_column(nullable=False)
384 nickname: Mapped[str] = mapped_column(nullable=False, default=None)
385 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
387 hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
389 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None)
391 def __init__(self, *args: object, **kwargs: object) -> None:
392 super().__init__(*args, **kwargs)
393 self.role = "participant"
394 self.affiliation = "member"
397class Bob(Base):
398 __tablename__ = "bob"
400 id: Mapped[int] = mapped_column(primary_key=True)
401 file_name: Mapped[str] = mapped_column(nullable=False)
403 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
404 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
405 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
407 content_type: Mapped[str | None] = mapped_column(nullable=False)