Coverage for slidge/db/models.py: 98%
183 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +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)
252 stanza_id: Mapped[str] = mapped_column(nullable=False)
253 timestamp: Mapped[datetime] = mapped_column(nullable=False)
254 author_jid: Mapped[JID] = mapped_column(nullable=False)
255 source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False)
256 legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
258 stanza: Mapped[str] = mapped_column(nullable=False)
261class _LegacyToXmppIdsBase:
262 """
263 XMPP-client generated IDs, and mapping to the corresponding legacy IDs.
265 A single legacy ID can map to several XMPP ids.
266 """
268 id: Mapped[int] = mapped_column(primary_key=True)
269 legacy_id: Mapped[str] = mapped_column(nullable=False)
270 xmpp_id: Mapped[str] = mapped_column(nullable=False)
273class DirectMessages(_LegacyToXmppIdsBase, Base):
274 __tablename__ = "direct_msg"
275 __table_args__ = (Index("ix_direct_msg_legacy_id", "legacy_id", "foreign_key"),)
276 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
279class GroupMessages(_LegacyToXmppIdsBase, Base):
280 __tablename__ = "group_msg"
281 __table_args__ = (Index("ix_group_msg_legacy_id", "legacy_id", "foreign_key"),)
282 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
285class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base):
286 """
287 This maps "origin ids" <message id=XXX> to legacy message IDs
288 We need that for message corrections and retractions, which do not reference
289 messages by their "Unique and Stable Stanza IDs (XEP-0359)"
290 """
292 __tablename__ = "group_msg_origin"
293 __table_args__ = (
294 Index("ix_group_msg_origin_legacy_id", "legacy_id", "foreign_key"),
295 )
296 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
299class DirectThreads(_LegacyToXmppIdsBase, Base):
300 __tablename__ = "direct_thread"
301 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),)
302 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
305class GroupThreads(_LegacyToXmppIdsBase, Base):
306 __tablename__ = "group_thread"
307 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),)
308 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
311class Attachment(Base):
312 """
313 Legacy attachments
314 """
316 __tablename__ = "attachment"
317 __table_args__ = (UniqueConstraint("user_account_id", "legacy_file_id"),)
319 id: Mapped[int] = mapped_column(primary_key=True)
320 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
321 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
323 legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
324 url: Mapped[str] = mapped_column(index=True, nullable=False)
325 sims: Mapped[Optional[str]] = mapped_column()
326 sfs: Mapped[Optional[str]] = mapped_column()
329class Participant(Base):
330 __tablename__ = "participant"
331 __table_args__ = (
332 UniqueConstraint("room_id", "resource"),
333 UniqueConstraint("room_id", "contact_id"),
334 )
336 id: Mapped[int] = mapped_column(primary_key=True)
338 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
339 room: Mapped[Room] = relationship(
340 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id
341 )
343 contact_id: Mapped[Optional[int]] = mapped_column(
344 ForeignKey("contact.id"), nullable=True
345 )
346 contact: Mapped[Optional[Contact]] = relationship(
347 lazy=False, back_populates="participants"
348 )
350 is_user: Mapped[bool] = mapped_column(default=False)
352 affiliation: Mapped[MucAffiliation] = mapped_column(
353 default="member", nullable=False
354 )
355 role: Mapped[MucRole] = mapped_column(default="participant", nullable=False)
357 presence_sent: Mapped[bool] = mapped_column(default=False)
359 resource: Mapped[str] = mapped_column(nullable=False)
360 nickname: Mapped[str] = mapped_column(nullable=False, default=None)
361 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
363 hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
365 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
367 def __init__(self, *args, **kwargs):
368 super().__init__(*args, **kwargs)
369 self.role = "participant"
370 self.affiliation = "member"
373class Bob(Base):
374 __tablename__ = "bob"
376 id: Mapped[int] = mapped_column(primary_key=True)
377 file_name: Mapped[str] = mapped_column(nullable=False)
379 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
380 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
381 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
383 content_type: Mapped[Optional[str]] = mapped_column(nullable=False)