Coverage for slidge/db/models.py: 98%
174 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-04 08:17 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-04 08:17 +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, declared_attr, mapped_column, relationship
12from ..util.types import ClientType, 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 DirectThreads(_LegacyToXmppIdsBase, Base):
286 __tablename__ = "direct_thread"
287 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),)
288 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False)
291class GroupThreads(_LegacyToXmppIdsBase, Base):
292 __tablename__ = "group_thread"
293 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),)
294 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
297class Attachment(Base):
298 """
299 Legacy attachments
300 """
302 __tablename__ = "attachment"
304 id: Mapped[int] = mapped_column(primary_key=True)
305 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
306 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
308 legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
309 url: Mapped[str] = mapped_column(index=True, nullable=False)
310 sims: Mapped[Optional[str]] = mapped_column()
311 sfs: Mapped[Optional[str]] = mapped_column()
314class Participant(Base):
315 __tablename__ = "participant"
316 __table_args__ = (
317 UniqueConstraint("room_id", "resource"),
318 UniqueConstraint("room_id", "contact_id"),
319 )
321 id: Mapped[int] = mapped_column(primary_key=True)
323 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
324 room: Mapped[Room] = relationship(
325 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id
326 )
328 contact_id: Mapped[Optional[int]] = mapped_column(
329 ForeignKey("contact.id"), nullable=True
330 )
331 contact: Mapped[Optional[Contact]] = relationship(
332 lazy=False, back_populates="participants"
333 )
335 is_user: Mapped[bool] = mapped_column(default=False)
337 affiliation: Mapped[MucAffiliation] = mapped_column(default="member")
338 role: Mapped[MucRole] = mapped_column(default="participant")
340 presence_sent: Mapped[bool] = mapped_column(default=False)
342 resource: Mapped[str] = mapped_column(nullable=False)
343 nickname: Mapped[str] = mapped_column(nullable=False, default=None)
344 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
346 hats: Mapped[list[tuple[str, str]]] = mapped_column(JSON, default=list)
348 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
351class Bob(Base):
352 __tablename__ = "bob"
354 id: Mapped[int] = mapped_column(primary_key=True)
355 file_name: Mapped[str] = mapped_column(nullable=False)
357 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
358 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
359 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
361 content_type: Mapped[Optional[str]] = mapped_column(nullable=False)