Coverage for slidge / db / models.py: 98%
186 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-06 15:18 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-06 15:18 +0000
1import warnings
2from datetime import datetime
3from enum import IntEnum
4from typing import Optional
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[Optional[str]] = 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:
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[Optional[str]] = 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[Optional[str]] = mapped_column(default=None)
101 etag: Mapped[Optional[str]] = mapped_column(default=None)
102 last_modified: Mapped[Optional[str]] = 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("user_account_id", "legacy_id"),
116 UniqueConstraint("user_account_id", "jid"),
117 )
119 id: Mapped[int] = mapped_column(primary_key=True)
120 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
121 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="contacts")
122 legacy_id: Mapped[str] = mapped_column(nullable=False)
124 jid: Mapped[JID] = mapped_column()
126 avatar_id: Mapped[Optional[int]] = mapped_column(
127 ForeignKey("avatar.id"), nullable=True
128 )
129 avatar: Mapped[Optional[Avatar]] = relationship(
130 lazy=False, back_populates="contacts"
131 )
133 nick: Mapped[Optional[str]] = mapped_column(nullable=True)
135 cached_presence: Mapped[bool] = mapped_column(default=False)
136 last_seen: Mapped[Optional[datetime]] = mapped_column(nullable=True)
137 ptype: Mapped[Optional[str]] = mapped_column(nullable=True)
138 pstatus: Mapped[Optional[str]] = mapped_column(nullable=True)
139 pshow: Mapped[Optional[str]] = mapped_column(nullable=True)
140 caps_ver: Mapped[Optional[str]] = mapped_column(nullable=True)
142 is_friend: Mapped[bool] = mapped_column(default=False)
143 added_to_roster: Mapped[bool] = mapped_column(default=False)
144 sent_order: Mapped[list["ContactSent"]] = relationship(
145 back_populates="contact", cascade="all, delete-orphan"
146 )
148 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(
149 default=None, nullable=True
150 )
151 updated: Mapped[bool] = mapped_column(default=False)
153 vcard: Mapped[Optional[str]] = mapped_column()
154 vcard_fetched: Mapped[bool] = mapped_column(default=False)
156 participants: Mapped[list["Participant"]] = relationship(back_populates="contact")
158 client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc")
160 messages: Mapped[list["DirectMessages"]] = relationship(
161 cascade="all, delete-orphan"
162 )
163 threads: Mapped[list["DirectThreads"]] = relationship(cascade="all, delete-orphan")
166class ContactSent(Base):
167 """
168 Keep track of XMPP msg ids sent by a specific contact for networks in which
169 all messages need to be marked as read.
171 (XMPP displayed markers convey a "read up to here" semantic.)
172 """
174 __tablename__ = "contact_sent"
175 __table_args__ = (UniqueConstraint("contact_id", "msg_id"),)
177 id: Mapped[int] = mapped_column(primary_key=True)
178 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
179 contact: Mapped[Contact] = relationship(back_populates="sent_order")
180 msg_id: Mapped[str] = mapped_column()
183class Room(Base):
184 """
185 Legacy room
186 """
188 __table_args__ = (
189 UniqueConstraint(
190 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
191 ),
192 UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"),
193 )
195 __tablename__ = "room"
196 id: Mapped[int] = mapped_column(primary_key=True)
197 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
198 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="rooms")
199 legacy_id: Mapped[str] = mapped_column(nullable=False)
201 jid: Mapped[JID] = mapped_column(nullable=False)
203 avatar_id: Mapped[Optional[int]] = mapped_column(
204 ForeignKey("avatar.id"), nullable=True
205 )
206 avatar: Mapped[Optional[Avatar]] = relationship(lazy=False, back_populates="rooms")
208 name: Mapped[Optional[str]] = mapped_column(nullable=True)
209 description: Mapped[Optional[str]] = mapped_column(nullable=True)
210 subject: Mapped[Optional[str]] = mapped_column(nullable=True)
211 subject_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
212 subject_setter: Mapped[Optional[str]] = mapped_column(nullable=True)
214 n_participants: Mapped[Optional[int]] = mapped_column(default=None)
216 muc_type: Mapped[MucType] = mapped_column(default=MucType.CHANNEL)
218 user_nick: Mapped[Optional[str]] = mapped_column()
219 user_resources: Mapped[Optional[str]] = mapped_column(nullable=True)
221 participants_filled: Mapped[bool] = mapped_column(default=False)
222 history_filled: Mapped[bool] = mapped_column(default=False)
224 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
225 updated: Mapped[bool] = mapped_column(default=False)
227 participants: Mapped[list["Participant"]] = relationship(
228 back_populates="room",
229 primaryjoin="Participant.room_id == Room.id",
230 cascade="all, delete-orphan",
231 )
233 archive: Mapped[list["ArchivedMessage"]] = relationship(
234 cascade="all, delete-orphan"
235 )
237 messages: Mapped[list["GroupMessages"]] = relationship(cascade="all, delete-orphan")
238 threads: Mapped[list["GroupThreads"]] = relationship(cascade="all, delete-orphan")
241class ArchivedMessage(Base):
242 """
243 Messages of rooms, that we store to act as a MAM server
244 """
246 __tablename__ = "mam"
247 __table_args__ = (UniqueConstraint("room_id", "stanza_id"),)
249 id: Mapped[int] = mapped_column(primary_key=True)
250 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
251 room: Mapped[Room] = relationship(lazy=True, back_populates="archive")
253 stanza_id: Mapped[str] = mapped_column(nullable=False)
254 timestamp: Mapped[datetime] = mapped_column(nullable=False)
255 author_jid: Mapped[JID] = mapped_column(nullable=False)
256 source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False)
257 legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
259 stanza: Mapped[str] = mapped_column(nullable=False)
261 displayed_by_user: Mapped[bool] = mapped_column(default=False)
264class _LegacyToXmppIdsBase:
265 """
266 XMPP-client generated IDs, and mapping to the corresponding legacy IDs.
268 A single legacy ID can map to several XMPP ids.
269 """
271 id: Mapped[int] = mapped_column(primary_key=True)
272 legacy_id: Mapped[str] = mapped_column(nullable=False)
273 xmpp_id: Mapped[str] = mapped_column(nullable=False)
276class DirectMessages(_LegacyToXmppIdsBase, Base):
277 __tablename__ = "direct_msg"
278 __table_args__ = (Index("ix_direct_msg_legacy_id", "legacy_id", "foreign_key"),)
279 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
282class GroupMessages(_LegacyToXmppIdsBase, Base):
283 __tablename__ = "group_msg"
284 __table_args__ = (Index("ix_group_msg_legacy_id", "legacy_id", "foreign_key"),)
285 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
288class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base):
289 """
290 This maps "origin ids" <message id=XXX> to legacy message IDs
291 We need that for message corrections and retractions, which do not reference
292 messages by their "Unique and Stable Stanza IDs (XEP-0359)"
293 """
295 __tablename__ = "group_msg_origin"
296 __table_args__ = (
297 Index("ix_group_msg_origin_legacy_id", "legacy_id", "foreign_key"),
298 )
299 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
302class DirectThreads(_LegacyToXmppIdsBase, Base):
303 __tablename__ = "direct_thread"
304 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),)
305 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
308class GroupThreads(_LegacyToXmppIdsBase, Base):
309 __tablename__ = "group_thread"
310 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),)
311 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
314class Attachment(Base):
315 """
316 Legacy attachments
317 """
319 __tablename__ = "attachment"
320 __table_args__ = (UniqueConstraint("user_account_id", "legacy_file_id"),)
322 id: Mapped[int] = mapped_column(primary_key=True)
323 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
324 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
326 legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
327 url: Mapped[str] = mapped_column(index=True, nullable=False)
328 sims: Mapped[Optional[str]] = mapped_column()
329 sfs: Mapped[Optional[str]] = mapped_column()
332class Participant(Base):
333 __tablename__ = "participant"
334 __table_args__ = (
335 UniqueConstraint("room_id", "resource"),
336 UniqueConstraint("room_id", "contact_id"),
337 UniqueConstraint("room_id", "occupant_id"),
338 )
340 id: Mapped[int] = mapped_column(primary_key=True)
342 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
343 room: Mapped[Room] = relationship(
344 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id
345 )
347 contact_id: Mapped[Optional[int]] = mapped_column(
348 ForeignKey("contact.id"), nullable=True
349 )
350 contact: Mapped[Optional[Contact]] = relationship(
351 lazy=False, back_populates="participants"
352 )
354 occupant_id: Mapped[str] = mapped_column(nullable=False)
356 is_user: Mapped[bool] = mapped_column(default=False)
358 affiliation: Mapped[MucAffiliation] = mapped_column(
359 default="member", nullable=False
360 )
361 role: Mapped[MucRole] = mapped_column(default="participant", nullable=False)
363 presence_sent: Mapped[bool] = mapped_column(default=False)
365 resource: Mapped[str] = mapped_column(nullable=False)
366 nickname: Mapped[str] = mapped_column(nullable=False, default=None)
367 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
369 hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
371 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
373 def __init__(self, *args, **kwargs):
374 super().__init__(*args, **kwargs)
375 self.role = "participant"
376 self.affiliation = "member"
379class Bob(Base):
380 __tablename__ = "bob"
382 id: Mapped[int] = mapped_column(primary_key=True)
383 file_name: Mapped[str] = mapped_column(nullable=False)
385 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
386 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
387 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
389 content_type: Mapped[Optional[str]] = mapped_column(nullable=False)