Coverage for slidge / db / models.py: 98%

207 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-20 19:56 +0000

1import warnings 

2from datetime import datetime 

3from enum import IntEnum 

4from typing import Any 

5 

6import sqlalchemy as sa 

7from slixmpp import JID 

8from slixmpp.types import MucAffiliation, MucRole 

9from sqlalchemy import JSON, Column, ForeignKey, Index, Table, UniqueConstraint 

10from sqlalchemy.orm import Mapped, mapped_column, relationship 

11 

12from ..util.types import ClientType, Hat, MucType 

13from .meta import Base, JSONSerializable, JSONSerializableTypes 

14 

15 

16class ArchivedMessageSource(IntEnum): 

17 """ 

18 Whether an archived message comes from ``LegacyMUC.backfill()`` or was received 

19 as a "live" message. 

20 """ 

21 

22 LIVE = 1 

23 BACKFILL = 2 

24 

25 

26class GatewayUser(Base): 

27 """ 

28 A user, registered to the gateway component. 

29 """ 

30 

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 ) 

37 

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 """ 

48 

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") 

56 spaces: Mapped[list["Space"]] = relationship(back_populates="user") 

57 

58 def __repr__(self) -> str: 

59 return f"User(id={self.id!r}, jid={self.jid!r})" 

60 

61 def get(self, field: str, default: str = "") -> JSONSerializableTypes: 

62 # """ 

63 # Get fields from the registration form (required to comply with slixmpp backend protocol) 

64 # 

65 # :param field: Name of the field 

66 # :param default: Default value to return if the field is not present 

67 # 

68 # :return: Value of the field 

69 # """ 

70 return self.legacy_module_data.get(field, default) 

71 

72 @property 

73 def registration_form(self) -> dict[str, Any]: 

74 # Kept for retrocompat, should be 

75 # FIXME: delete me 

76 warnings.warn( 

77 "GatewayUser.registration_form is deprecated.", DeprecationWarning 

78 ) 

79 return self.legacy_module_data 

80 

81 

82class Avatar(Base): 

83 """ 

84 Avatars of contacts, rooms and participants. 

85 

86 To comply with XEPs, we convert them all to PNG before storing them. 

87 """ 

88 

89 __tablename__ = "avatar" 

90 

91 id: Mapped[int] = mapped_column(primary_key=True) 

92 

93 hash: Mapped[str] = mapped_column(unique=True) 

94 height: Mapped[int] = mapped_column() 

95 width: Mapped[int] = mapped_column() 

96 

97 legacy_id: Mapped[str | None] = mapped_column(unique=True, nullable=True) 

98 

99 # this is only used when avatars are available as HTTP URLs and do not 

100 # have a legacy_id 

101 url: Mapped[str | None] = mapped_column(unique=True, default=None) 

102 etag: Mapped[str | None] = mapped_column(default=None) 

103 last_modified: Mapped[str | None] = mapped_column(default=None) 

104 

105 contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar") 

106 rooms: Mapped[list["Room"]] = relationship(back_populates="avatar") 

107 

108 

109space_owner_association = Table( 

110 "space_owner_association", 

111 Base.metadata, 

112 Column("space_id", ForeignKey("space.id"), primary_key=True), 

113 Column("contact_id", ForeignKey("contact.id"), primary_key=True), 

114) 

115 

116 

117class Contact(Base): 

118 """ 

119 Legacy contacts 

120 """ 

121 

122 __tablename__ = "contact" 

123 __table_args__ = ( 

124 UniqueConstraint( 

125 "user_account_id", "legacy_id", name="uq_contact_user_account_id_legacy_id" 

126 ), 

127 UniqueConstraint( 

128 "user_account_id", "jid", name="uq_contact_user_account_id_jid" 

129 ), 

130 ) 

131 

132 id: Mapped[int] = mapped_column(primary_key=True) 

133 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) 

134 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="contacts") 

135 legacy_id: Mapped[str] = mapped_column(nullable=False) 

136 

137 jid: Mapped[JID] = mapped_column() 

138 

139 avatar_id: Mapped[int | None] = mapped_column( 

140 ForeignKey("avatar.id"), nullable=True 

141 ) 

142 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="contacts") 

143 

144 nick: Mapped[str | None] = mapped_column(nullable=True) 

145 

146 cached_presence: Mapped[bool] = mapped_column(default=False) 

147 last_seen: Mapped[datetime | None] = mapped_column(nullable=True) 

148 ptype: Mapped[str | None] = mapped_column(nullable=True) 

149 pstatus: Mapped[str | None] = mapped_column(nullable=True) 

150 pshow: Mapped[str | None] = mapped_column(nullable=True) 

151 caps_ver: Mapped[str | None] = mapped_column(nullable=True) 

152 

153 is_friend: Mapped[bool] = mapped_column(default=False) 

154 added_to_roster: Mapped[bool] = mapped_column(default=False) 

155 sent_order: Mapped[list["ContactSent"]] = relationship( 

156 back_populates="contact", cascade="all, delete-orphan" 

157 ) 

158 

159 extra_attributes: Mapped[JSONSerializable | None] = mapped_column( 

160 default=None, nullable=True 

161 ) 

162 updated: Mapped[bool] = mapped_column(default=False) 

163 

164 vcard: Mapped[str | None] = mapped_column() 

165 vcard_fetched: Mapped[bool] = mapped_column(default=False) 

166 

167 participants: Mapped[list["Participant"]] = relationship(back_populates="contact") 

168 

169 client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc") 

170 

171 messages: Mapped[list["DirectMessages"]] = relationship( 

172 cascade="all, delete-orphan" 

173 ) 

174 threads: Mapped[list["DirectThreads"]] = relationship(cascade="all, delete-orphan") 

175 

176 spaces_created: Mapped[list["Space"]] = relationship( 

177 back_populates="creator", 

178 cascade="all, delete-orphan", 

179 ) 

180 spaces_owned: Mapped[list["Space"]] = relationship( 

181 back_populates="owners", 

182 secondary=space_owner_association, 

183 ) 

184 

185 

186class ContactSent(Base): 

187 """ 

188 Keep track of XMPP msg ids sent by a specific contact for networks in which 

189 all messages need to be marked as read. 

190 

191 (XMPP displayed markers convey a "read up to here" semantic.) 

192 """ 

193 

194 __tablename__ = "contact_sent" 

195 __table_args__ = ( 

196 UniqueConstraint( 

197 "contact_id", "msg_id", name="uq_contact_sent_contact_id_msg_id" 

198 ), 

199 ) 

200 

201 id: Mapped[int] = mapped_column(primary_key=True) 

202 contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id")) 

203 contact: Mapped[Contact] = relationship(back_populates="sent_order") 

204 msg_id: Mapped[str] = mapped_column() 

205 

206 

207class Room(Base): 

208 """ 

209 Legacy room 

210 """ 

211 

212 __table_args__ = ( 

213 UniqueConstraint( 

214 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id" 

215 ), 

216 UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"), 

217 ) 

218 

219 __tablename__ = "room" 

220 id: Mapped[int] = mapped_column(primary_key=True) 

221 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) 

222 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="rooms") 

223 legacy_id: Mapped[str] = mapped_column(nullable=False) 

224 

225 jid: Mapped[JID] = mapped_column(nullable=False) 

226 

227 avatar_id: Mapped[int | None] = mapped_column( 

228 ForeignKey("avatar.id"), nullable=True 

229 ) 

230 avatar: Mapped[Avatar | None] = relationship(lazy=False, back_populates="rooms") 

231 

232 name: Mapped[str | None] = mapped_column(nullable=True) 

233 description: Mapped[str | None] = mapped_column(nullable=True) 

234 subject: Mapped[str | None] = mapped_column(nullable=True) 

235 subject_date: Mapped[datetime | None] = mapped_column(nullable=True) 

236 subject_setter: Mapped[str | None] = mapped_column(nullable=True) 

237 

238 n_participants: Mapped[int | None] = mapped_column(default=None) 

239 

240 muc_type: Mapped[MucType] = mapped_column(default=MucType.CHANNEL) 

241 

242 user_nick: Mapped[str | None] = mapped_column() 

243 user_resources: Mapped[str | None] = mapped_column(nullable=True) 

244 

245 participants_filled: Mapped[bool] = mapped_column(default=False) 

246 history_filled: Mapped[bool] = mapped_column(default=False) 

247 

248 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None) 

249 updated: Mapped[bool] = mapped_column(default=False) 

250 

251 participants: Mapped[list["Participant"]] = relationship( 

252 back_populates="room", 

253 primaryjoin="Participant.room_id == Room.id", 

254 cascade="all, delete-orphan", 

255 ) 

256 

257 archive: Mapped[list["ArchivedMessage"]] = relationship( 

258 cascade="all, delete-orphan" 

259 ) 

260 

261 messages: Mapped[list["GroupMessages"]] = relationship(cascade="all, delete-orphan") 

262 threads: Mapped[list["GroupThreads"]] = relationship(cascade="all, delete-orphan") 

263 

264 space_id: Mapped[int | None] = mapped_column(ForeignKey("space.id"), nullable=True) 

265 space: Mapped["Space"] = relationship(back_populates="rooms", lazy=False) 

266 

267 

268class ArchivedMessage(Base): 

269 """ 

270 Messages of rooms, that we store to act as a MAM server 

271 """ 

272 

273 __tablename__ = "mam" 

274 __table_args__ = ( 

275 UniqueConstraint("room_id", "stanza_id", name="uq_mam_room_id_stanza_id"), 

276 ) 

277 

278 id: Mapped[int] = mapped_column(primary_key=True) 

279 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) 

280 room: Mapped[Room] = relationship(lazy=True, back_populates="archive") 

281 

282 stanza_id: Mapped[str] = mapped_column(nullable=False) 

283 timestamp: Mapped[datetime] = mapped_column(nullable=False) 

284 author_jid: Mapped[JID] = mapped_column(nullable=False) 

285 source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False) 

286 legacy_id: Mapped[str | None] = mapped_column(nullable=True) 

287 

288 stanza: Mapped[str] = mapped_column(nullable=False) 

289 

290 displayed_by_user: Mapped[bool] = mapped_column(default=False, nullable=True) 

291 

292 

293class _LegacyToXmppIdsBase: 

294 """ 

295 XMPP-client generated IDs, and mapping to the corresponding legacy IDs. 

296 

297 A single legacy ID can map to several XMPP ids. 

298 """ 

299 

300 id: Mapped[int] = mapped_column(primary_key=True) 

301 legacy_id: Mapped[str] = mapped_column(nullable=False) 

302 xmpp_id: Mapped[str] = mapped_column(nullable=False) 

303 

304 

305class DirectMessages(_LegacyToXmppIdsBase, Base): 

306 __tablename__ = "direct_msg" 

307 __table_args__ = (Index("ix_direct_msg_legacy_id", "legacy_id", "foreign_key"),) 

308 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False) 

309 

310 

311class GroupMessages(_LegacyToXmppIdsBase, Base): 

312 __tablename__ = "group_msg" 

313 __table_args__ = (Index("ix_group_msg_legacy_id", "legacy_id", "foreign_key"),) 

314 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) 

315 

316 

317class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base): 

318 """ 

319 This maps "origin ids" <message id=XXX> to legacy message IDs 

320 We need that for message corrections and retractions, which do not reference 

321 messages by their "Unique and Stable Stanza IDs (XEP-0359)" 

322 """ 

323 

324 __tablename__ = "group_msg_origin" 

325 __table_args__ = ( 

326 Index("ix_group_msg_origin_legacy_id", "legacy_id", "foreign_key"), 

327 ) 

328 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) 

329 

330 

331class DirectThreads(_LegacyToXmppIdsBase, Base): 

332 __tablename__ = "direct_thread" 

333 __table_args__ = (Index("ix_direct_direct_thread_id", "legacy_id", "foreign_key"),) 

334 foreign_key: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=False) 

335 

336 

337class GroupThreads(_LegacyToXmppIdsBase, Base): 

338 __tablename__ = "group_thread" 

339 __table_args__ = (Index("ix_direct_group_thread_id", "legacy_id", "foreign_key"),) 

340 foreign_key: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) 

341 

342 

343class Attachment(Base): 

344 """ 

345 Legacy attachments 

346 """ 

347 

348 __tablename__ = "attachment" 

349 __table_args__ = ( 

350 UniqueConstraint( 

351 "user_account_id", 

352 "legacy_file_id", 

353 name="uq_attachment_user_account_id_legacy_file_id", 

354 ), 

355 ) 

356 

357 id: Mapped[int] = mapped_column(primary_key=True) 

358 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) 

359 user: Mapped[GatewayUser] = relationship(back_populates="attachments") 

360 

361 legacy_file_id: Mapped[str | None] = mapped_column(index=True, nullable=True) 

362 url: Mapped[str] = mapped_column(index=True, nullable=False) 

363 sims: Mapped[str | None] = mapped_column() 

364 sfs: Mapped[str | None] = mapped_column() 

365 

366 

367class Participant(Base): 

368 __tablename__ = "participant" 

369 __table_args__ = ( 

370 UniqueConstraint("room_id", "resource", name="uq_participant_room_id_resource"), 

371 UniqueConstraint( 

372 "room_id", "contact_id", name="uq_participant_room_id_contact_id" 

373 ), 

374 UniqueConstraint( 

375 "room_id", "occupant_id", name="uq_participant_room_id_occupant_id" 

376 ), 

377 ) 

378 

379 id: Mapped[int] = mapped_column(primary_key=True) 

380 

381 room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) 

382 room: Mapped[Room] = relationship( 

383 lazy=False, back_populates="participants", primaryjoin=Room.id == room_id 

384 ) 

385 

386 contact_id: Mapped[int | None] = mapped_column( 

387 ForeignKey("contact.id"), nullable=True 

388 ) 

389 contact: Mapped[Contact | None] = relationship( 

390 lazy=False, back_populates="participants" 

391 ) 

392 

393 occupant_id: Mapped[str] = mapped_column(nullable=False) 

394 

395 is_user: Mapped[bool] = mapped_column(default=False) 

396 

397 affiliation: Mapped[MucAffiliation] = mapped_column( 

398 default="member", nullable=False 

399 ) 

400 role: Mapped[MucRole] = mapped_column(default="participant", nullable=False) 

401 

402 presence_sent: Mapped[bool] = mapped_column(default=False) 

403 

404 resource: Mapped[str] = mapped_column(nullable=False) 

405 nickname: Mapped[str] = mapped_column(nullable=False, default=None) 

406 nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None) 

407 

408 hats: Mapped[list[Hat]] = mapped_column(JSON, default=list) 

409 

410 extra_attributes: Mapped[JSONSerializable | None] = mapped_column(default=None) 

411 

412 def __init__(self, *args: object, **kwargs: object) -> None: 

413 super().__init__(*args, **kwargs) 

414 self.role = "participant" 

415 self.affiliation = "member" 

416 

417 

418class Bob(Base): 

419 __tablename__ = "bob" 

420 

421 id: Mapped[int] = mapped_column(primary_key=True) 

422 file_name: Mapped[str] = mapped_column(nullable=False) 

423 

424 sha_1: Mapped[str] = mapped_column(nullable=False, unique=True) 

425 sha_256: Mapped[str] = mapped_column(nullable=False, unique=True) 

426 sha_512: Mapped[str] = mapped_column(nullable=False, unique=True) 

427 

428 content_type: Mapped[str] = mapped_column(nullable=False) 

429 

430 

431class Space(Base): 

432 __tablename__ = "space" 

433 __table_args__ = ( 

434 UniqueConstraint( 

435 "user_account_id", "legacy_id", name="uq_space_user_account_id_legacy_id" 

436 ), 

437 ) 

438 

439 id: Mapped[int] = mapped_column(primary_key=True) 

440 

441 updated: Mapped[bool] = mapped_column(default=False, nullable=False) 

442 

443 user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) 

444 user: Mapped[GatewayUser] = relationship(lazy=True, back_populates="spaces") 

445 

446 legacy_id: Mapped[str] = mapped_column(nullable=False) 

447 name: Mapped[str | None] = mapped_column(nullable=True) 

448 description: Mapped[str | None] = mapped_column(nullable=True) 

449 member_count: Mapped[int | None] = mapped_column(nullable=True) 

450 

451 creator_pk: Mapped[int | None] = mapped_column( 

452 ForeignKey("contact.id"), nullable=True 

453 ) 

454 creator: Mapped[Contact | None] = relationship(back_populates="spaces_created") 

455 

456 owners: Mapped[list[Contact]] = relationship( 

457 back_populates="spaces_owned", 

458 secondary=space_owner_association, 

459 ) 

460 

461 rooms: Mapped[list[Room]] = relationship(cascade="all, delete-orphan")