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

207 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-06-13 04:38 +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( 

57 back_populates="user", cascade="all, delete-orphan" 

58 ) 

59 

60 def __repr__(self) -> str: 

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

62 

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

64 # """ 

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

66 # 

67 # :param field: Name of the field 

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

69 # 

70 # :return: Value of the field 

71 # """ 

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

73 

74 @property 

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

76 # Kept for retrocompat, should be 

77 # FIXME: delete me 

78 warnings.warn( 

79 "GatewayUser.registration_form is deprecated.", DeprecationWarning 

80 ) 

81 return self.legacy_module_data 

82 

83 

84class Avatar(Base): 

85 """ 

86 Avatars of contacts, rooms and participants. 

87 

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

89 """ 

90 

91 __tablename__ = "avatar" 

92 

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

94 

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

96 height: Mapped[int] = mapped_column() 

97 width: Mapped[int] = mapped_column() 

98 

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

100 

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

102 # have a legacy_id 

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

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

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

106 

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

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

109 

110 

111space_owner_association = Table( 

112 "space_owner_association", 

113 Base.metadata, 

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

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

116) 

117 

118 

119class Contact(Base): 

120 """ 

121 Legacy contacts 

122 """ 

123 

124 __tablename__ = "contact" 

125 __table_args__ = ( 

126 UniqueConstraint( 

127 "user_account_id", "legacy_id", name="uq_contact_user_account_id_legacy_id" 

128 ), 

129 UniqueConstraint( 

130 "user_account_id", "jid", name="uq_contact_user_account_id_jid" 

131 ), 

132 ) 

133 

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

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

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

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

138 

139 jid: Mapped[JID] = mapped_column() 

140 

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

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

143 ) 

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

145 

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

147 

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

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

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

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

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

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

154 

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

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

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

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

159 ) 

160 

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

162 default=None, nullable=True 

163 ) 

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

165 

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

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

168 

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

170 

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

172 

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

174 cascade="all, delete-orphan" 

175 ) 

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

177 

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

179 back_populates="creator", 

180 cascade="all, delete-orphan", 

181 ) 

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

183 back_populates="owners", 

184 secondary=space_owner_association, 

185 ) 

186 

187 

188class ContactSent(Base): 

189 """ 

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

191 all messages need to be marked as read. 

192 

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

194 """ 

195 

196 __tablename__ = "contact_sent" 

197 __table_args__ = ( 

198 UniqueConstraint( 

199 "contact_id", "msg_id", name="uq_contact_sent_contact_id_msg_id" 

200 ), 

201 ) 

202 

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

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

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

206 msg_id: Mapped[str] = mapped_column() 

207 

208 

209class Room(Base): 

210 """ 

211 Legacy room 

212 """ 

213 

214 __table_args__ = ( 

215 UniqueConstraint( 

216 "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id" 

217 ), 

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

219 ) 

220 

221 __tablename__ = "room" 

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

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

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

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

226 

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

228 

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

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

231 ) 

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

233 

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

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

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

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

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

239 

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

241 

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

243 

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

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

246 

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

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

249 

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

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

252 

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

254 back_populates="room", 

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

256 cascade="all, delete-orphan", 

257 ) 

258 

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

260 cascade="all, delete-orphan" 

261 ) 

262 

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

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

265 

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

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

268 

269 

270class ArchivedMessage(Base): 

271 """ 

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

273 """ 

274 

275 __tablename__ = "mam" 

276 __table_args__ = ( 

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

278 ) 

279 

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

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

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

283 

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

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

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

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

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

289 

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

291 

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

293 

294 

295class _LegacyToXmppIdsBase: 

296 """ 

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

298 

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

300 """ 

301 

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

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

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

305 

306 

307class DirectMessages(_LegacyToXmppIdsBase, Base): 

308 __tablename__ = "direct_msg" 

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

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

311 

312 

313class GroupMessages(_LegacyToXmppIdsBase, Base): 

314 __tablename__ = "group_msg" 

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

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

317 

318 

319class GroupMessagesOrigin(_LegacyToXmppIdsBase, Base): 

320 """ 

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

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

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

324 """ 

325 

326 __tablename__ = "group_msg_origin" 

327 __table_args__ = ( 

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

329 ) 

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

331 

332 

333class DirectThreads(_LegacyToXmppIdsBase, Base): 

334 __tablename__ = "direct_thread" 

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

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

337 

338 

339class GroupThreads(_LegacyToXmppIdsBase, Base): 

340 __tablename__ = "group_thread" 

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

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

343 

344 

345class Attachment(Base): 

346 """ 

347 Legacy attachments 

348 """ 

349 

350 __tablename__ = "attachment" 

351 __table_args__ = ( 

352 UniqueConstraint( 

353 "user_account_id", 

354 "legacy_file_id", 

355 name="uq_attachment_user_account_id_legacy_file_id", 

356 ), 

357 ) 

358 

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

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

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

362 

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

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

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

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

367 

368 

369class Participant(Base): 

370 __tablename__ = "participant" 

371 __table_args__ = ( 

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

373 UniqueConstraint( 

374 "room_id", "contact_id", name="uq_participant_room_id_contact_id" 

375 ), 

376 UniqueConstraint( 

377 "room_id", "occupant_id", name="uq_participant_room_id_occupant_id" 

378 ), 

379 ) 

380 

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

382 

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

384 room: Mapped[Room] = relationship( 

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

386 ) 

387 

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

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

390 ) 

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

392 lazy=False, back_populates="participants" 

393 ) 

394 

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

396 

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

398 

399 affiliation: Mapped[MucAffiliation] = mapped_column( 

400 default="member", nullable=False 

401 ) 

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

403 

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

405 

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

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

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

409 

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

411 

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

413 

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

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

416 self.role = "participant" 

417 self.affiliation = "member" 

418 

419 

420class Bob(Base): 

421 __tablename__ = "bob" 

422 

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

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

425 

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

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

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

429 

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

431 

432 

433class Space(Base): 

434 __tablename__ = "space" 

435 __table_args__ = ( 

436 UniqueConstraint( 

437 "user_account_id", "legacy_id", name="uq_space_user_account_id_legacy_id" 

438 ), 

439 ) 

440 

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

442 

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

444 

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

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

447 

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

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

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

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

452 

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

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

455 ) 

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

457 

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

459 back_populates="spaces_owned", 

460 secondary=space_owner_association, 

461 ) 

462 

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