Coverage for slidge/db/models.py: 98%
189 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-07 05:11 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-07 05:11 +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 ForeignKey, Index, UniqueConstraint
10from sqlalchemy.orm import Mapped, mapped_column, relationship
12from ..util.types import ClientType, MucType
13from .meta import Base, JSONSerializable, JSONSerializableTypes
16class XmppToLegacyEnum(IntEnum):
17 """
18 XMPP-client generated IDs, used in the XmppToLegacyIds table to keep track
19 of corresponding legacy IDs
20 """
22 DM = 1
23 GROUP_CHAT = 2
24 THREAD = 3
27class ArchivedMessageSource(IntEnum):
28 """
29 Whether an archived message comes from ``LegacyMUC.backfill()`` or was received
30 as a "live" message.
31 """
33 LIVE = 1
34 BACKFILL = 2
37class GatewayUser(Base):
38 """
39 A user, registered to the gateway component.
40 """
42 __tablename__ = "user_account"
43 id: Mapped[int] = mapped_column(primary_key=True)
44 jid: Mapped[JID] = mapped_column(unique=True)
45 registration_date: Mapped[datetime] = mapped_column(
46 sa.DateTime, server_default=sa.func.now()
47 )
49 legacy_module_data: Mapped[JSONSerializable] = mapped_column(default={})
50 """
51 Arbitrary non-relational data that legacy modules can use
52 """
53 preferences: Mapped[JSONSerializable] = mapped_column(default={})
54 avatar_hash: Mapped[Optional[str]] = mapped_column(default=None)
55 """
56 Hash of the user's avatar, to avoid re-publishing the same avatar on the
57 legacy network
58 """
60 contacts: Mapped[list["Contact"]] = relationship(
61 back_populates="user", cascade="all, delete-orphan"
62 )
63 rooms: Mapped[list["Room"]] = relationship(
64 back_populates="user", cascade="all, delete-orphan"
65 )
66 xmpp_to_legacy: Mapped[list["XmppToLegacyIds"]] = relationship(
67 cascade="all, delete-orphan"
68 )
69 attachments: Mapped[list["Attachment"]] = relationship(cascade="all, delete-orphan")
70 multi_legacy: Mapped[list["LegacyIdsMulti"]] = relationship(
71 cascade="all, delete-orphan"
72 )
73 multi_xmpp: Mapped[list["XmppIdsMulti"]] = relationship(
74 cascade="all, delete-orphan"
75 )
77 def __repr__(self) -> str:
78 return f"User(id={self.id!r}, jid={self.jid!r})"
80 def get(self, field: str, default: str = "") -> JSONSerializableTypes:
81 # """
82 # Get fields from the registration form (required to comply with slixmpp backend protocol)
83 #
84 # :param field: Name of the field
85 # :param default: Default value to return if the field is not present
86 #
87 # :return: Value of the field
88 # """
89 return self.legacy_module_data.get(field, default)
91 @property
92 def registration_form(self) -> dict:
93 # Kept for retrocompat, should be
94 # FIXME: delete me
95 warnings.warn(
96 "GatewayUser.registration_form is deprecated.", DeprecationWarning
97 )
98 return self.legacy_module_data
101class Avatar(Base):
102 """
103 Avatars of contacts, rooms and participants.
105 To comply with XEPs, we convert them all to PNG before storing them.
106 """
108 __tablename__ = "avatar"
110 id: Mapped[int] = mapped_column(primary_key=True)
112 filename: Mapped[str] = mapped_column(unique=True)
113 hash: Mapped[str] = mapped_column(unique=True)
114 height: Mapped[int] = mapped_column()
115 width: Mapped[int] = mapped_column()
117 # this is only used when avatars are available as HTTP URLs and do not
118 # have a legacy_id
119 url: Mapped[Optional[str]] = mapped_column(default=None)
120 etag: Mapped[Optional[str]] = mapped_column(default=None)
121 last_modified: Mapped[Optional[str]] = mapped_column(default=None)
123 contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar")
124 rooms: Mapped[list["Room"]] = relationship(back_populates="avatar")
127class Contact(Base):
128 """
129 Legacy contacts
130 """
132 __tablename__ = "contact"
133 __table_args__ = (
134 UniqueConstraint("user_account_id", "legacy_id"),
135 UniqueConstraint("user_account_id", "jid"),
136 )
138 id: Mapped[int] = mapped_column(primary_key=True)
139 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
140 user: Mapped[GatewayUser] = relationship(back_populates="contacts")
141 legacy_id: Mapped[str] = mapped_column(nullable=False)
143 jid: Mapped[JID] = mapped_column()
145 avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True)
146 avatar: Mapped[Avatar] = relationship(back_populates="contacts")
148 nick: Mapped[Optional[str]] = mapped_column(nullable=True)
150 cached_presence: Mapped[bool] = mapped_column(default=False)
151 last_seen: Mapped[Optional[datetime]] = mapped_column(nullable=True)
152 ptype: Mapped[Optional[str]] = mapped_column(nullable=True)
153 pstatus: Mapped[Optional[str]] = mapped_column(nullable=True)
154 pshow: Mapped[Optional[str]] = mapped_column(nullable=True)
155 caps_ver: Mapped[Optional[str]] = mapped_column(nullable=True)
157 is_friend: Mapped[bool] = mapped_column(default=False)
158 added_to_roster: Mapped[bool] = mapped_column(default=False)
159 sent_order: Mapped[list["ContactSent"]] = relationship(
160 back_populates="contact", cascade="all, delete-orphan"
161 )
163 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(
164 default=None, nullable=True
165 )
166 updated: Mapped[bool] = mapped_column(default=False)
168 vcard: Mapped[Optional[str]] = mapped_column()
169 vcard_fetched: Mapped[bool] = mapped_column(default=False)
171 participants: Mapped[list["Participant"]] = relationship(back_populates="contact")
173 avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
175 client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc")
178class ContactSent(Base):
179 """
180 Keep track of XMPP msg ids sent by a specific contact for networks in which
181 all messages need to be marked as read.
183 (XMPP displayed markers convey a "read up to here" semantic.)
184 """
186 __tablename__ = "contact_sent"
187 __table_args__ = (UniqueConstraint("contact_id", "msg_id"),)
189 id: Mapped[int] = mapped_column(primary_key=True)
190 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
191 contact: Mapped[Contact] = relationship(back_populates="sent_order")
192 msg_id: Mapped[str] = mapped_column()
195class Room(Base):
196 """
197 Legacy room
198 """
200 __table_args__ = (
201 UniqueConstraint(
202 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
203 ),
204 UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"),
205 )
207 __tablename__ = "room"
208 id: Mapped[int] = mapped_column(primary_key=True)
209 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
210 user: Mapped[GatewayUser] = relationship(back_populates="rooms")
211 legacy_id: Mapped[str] = mapped_column(nullable=False)
213 jid: Mapped[JID] = mapped_column(nullable=False)
215 avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True)
216 avatar: Mapped[Avatar] = relationship(back_populates="rooms")
218 name: Mapped[Optional[str]] = mapped_column(nullable=True)
219 description: Mapped[Optional[str]] = mapped_column(nullable=True)
220 subject: Mapped[Optional[str]] = mapped_column(nullable=True)
221 subject_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
222 subject_setter: Mapped[Optional[str]] = mapped_column(nullable=True)
224 n_participants: Mapped[Optional[int]] = mapped_column(default=None)
226 muc_type: Mapped[Optional[MucType]] = mapped_column(default=MucType.GROUP)
228 user_nick: Mapped[Optional[str]] = mapped_column()
229 user_resources: Mapped[Optional[str]] = mapped_column(nullable=True)
231 participants_filled: Mapped[bool] = mapped_column(default=False)
232 history_filled: Mapped[bool] = mapped_column(default=False)
234 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
235 updated: Mapped[bool] = mapped_column(default=False)
237 participants: Mapped[list["Participant"]] = relationship(
238 back_populates="room",
239 primaryjoin="Participant.room_id == Room.id",
240 cascade="all, delete-orphan",
241 )
243 avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
245 archive: Mapped[list["ArchivedMessage"]] = relationship(
246 cascade="all, delete-orphan"
247 )
250class ArchivedMessage(Base):
251 """
252 Messages of rooms, that we store to act as a MAM server
253 """
255 __tablename__ = "mam"
256 __table_args__ = (UniqueConstraint("room_id", "stanza_id"),)
258 id: Mapped[int] = mapped_column(primary_key=True)
259 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
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[Optional[str]] = mapped_column(nullable=True)
267 stanza: Mapped[str] = mapped_column(nullable=False)
270class XmppToLegacyIds(Base):
271 """
272 XMPP-client generated IDs, and mapping to the corresponding legacy IDs
273 """
275 __tablename__ = "xmpp_to_legacy_ids"
276 __table_args__ = (
277 Index("xmpp_legacy", "user_account_id", "xmpp_id", "legacy_id", unique=True),
278 )
279 id: Mapped[int] = mapped_column(primary_key=True)
280 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
281 user: Mapped[GatewayUser] = relationship(back_populates="xmpp_to_legacy")
283 xmpp_id: Mapped[str] = mapped_column(nullable=False)
284 legacy_id: Mapped[str] = mapped_column(nullable=False)
286 type: Mapped[XmppToLegacyEnum] = mapped_column(nullable=False)
289class Attachment(Base):
290 """
291 Legacy attachments
292 """
294 __tablename__ = "attachment"
296 id: Mapped[int] = mapped_column(primary_key=True)
297 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
298 user: Mapped[GatewayUser] = relationship(back_populates="attachments")
300 legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
301 url: Mapped[str] = mapped_column(index=True, nullable=False)
302 sims: Mapped[Optional[str]] = mapped_column()
303 sfs: Mapped[Optional[str]] = mapped_column()
306class LegacyIdsMulti(Base):
307 """
308 Legacy messages with multiple attachments are split as several XMPP messages,
309 this table and the next maps a single legacy ID to multiple XMPP IDs.
310 """
312 __tablename__ = "legacy_ids_multi"
313 __table_args__ = (
314 Index(
315 "legacy_ids_multi_user_account_id_legacy_id",
316 "user_account_id",
317 "legacy_id",
318 unique=True,
319 ),
320 )
321 id: Mapped[int] = mapped_column(primary_key=True)
322 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
324 legacy_id: Mapped[str] = mapped_column(nullable=False)
325 xmpp_ids: Mapped[list["XmppIdsMulti"]] = relationship(
326 back_populates="legacy_ids_multi", cascade="all, delete-orphan"
327 )
330class XmppIdsMulti(Base):
331 __tablename__ = "xmpp_ids_multi"
332 __table_args__ = (
333 Index(
334 "legacy_ids_multi_user_account_id_xmpp_id",
335 "user_account_id",
336 "xmpp_id",
337 unique=True,
338 ),
339 )
340 id: Mapped[int] = mapped_column(primary_key=True)
341 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
343 xmpp_id: Mapped[str] = mapped_column(nullable=False)
345 legacy_ids_multi_id: Mapped[int] = mapped_column(ForeignKey("legacy_ids_multi.id"))
346 legacy_ids_multi: Mapped[LegacyIdsMulti] = relationship(back_populates="xmpp_ids")
349participant_hats = sa.Table(
350 "participant_hats",
351 Base.metadata,
352 sa.Column("participant_id", ForeignKey("participant.id"), primary_key=True),
353 sa.Column("hat_id", ForeignKey("hat.id"), primary_key=True),
354)
357class Hat(Base):
358 __tablename__ = "hat"
359 __table_args__ = (UniqueConstraint("title", "uri"),)
361 id: Mapped[int] = mapped_column(primary_key=True)
362 title: Mapped[str] = mapped_column()
363 uri: Mapped[str] = mapped_column()
364 participants: Mapped[list["Participant"]] = relationship(
365 secondary=participant_hats, back_populates="hats"
366 )
369class Participant(Base):
370 __tablename__ = "participant"
372 id: Mapped[int] = mapped_column(primary_key=True)
374 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
375 room: Mapped[Room] = relationship(
376 back_populates="participants", primaryjoin=Room.id == room_id
377 )
379 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=True)
380 contact: Mapped[Contact] = relationship(lazy=False, back_populates="participants")
382 is_user: Mapped[bool] = mapped_column(default=False)
384 affiliation: Mapped[MucAffiliation] = mapped_column(default="member")
385 role: Mapped[MucRole] = mapped_column(default="participant")
387 presence_sent: Mapped[bool] = mapped_column(default=False)
389 resource: Mapped[Optional[str]] = mapped_column(default=None)
390 nickname: Mapped[str] = mapped_column(nullable=True, default=None)
392 hats: Mapped[list["Hat"]] = relationship(
393 secondary=participant_hats, back_populates="participants"
394 )
396 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
399class Bob(Base):
400 __tablename__ = "bob"
402 id: Mapped[int] = mapped_column(primary_key=True)
403 file_name: Mapped[str] = mapped_column(nullable=False)
405 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
406 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
407 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
409 content_type: Mapped[Optional[str]] = mapped_column(nullable=False)