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

1import warnings 

2from datetime import datetime 

3from enum import IntEnum 

4from typing import Optional 

5 

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 

11 

12from ..util.types import ClientType, 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[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 """ 

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 

57 def __repr__(self) -> str: 

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

59 

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) 

70 

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 

79 

80 

81class Avatar(Base): 

82 """ 

83 Avatars of contacts, rooms and participants. 

84 

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

86 """ 

87 

88 __tablename__ = "avatar" 

89 

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

91 

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

93 height: Mapped[int] = mapped_column() 

94 width: Mapped[int] = mapped_column() 

95 

96 legacy_id: Mapped[Optional[str]] = mapped_column(unique=True, nullable=True) 

97 

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) 

103 

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

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

106 

107 

108class Contact(Base): 

109 """ 

110 Legacy contacts 

111 """ 

112 

113 __tablename__ = "contact" 

114 __table_args__ = ( 

115 UniqueConstraint("user_account_id", "legacy_id"), 

116 UniqueConstraint("user_account_id", "jid"), 

117 ) 

118 

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) 

123 

124 jid: Mapped[JID] = mapped_column() 

125 

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 ) 

132 

133 nick: Mapped[Optional[str]] = mapped_column(nullable=True) 

134 

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) 

141 

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 ) 

147 

148 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column( 

149 default=None, nullable=True 

150 ) 

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

152 

153 vcard: Mapped[Optional[str]] = mapped_column() 

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

155 

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

157 

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

159 

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

161 cascade="all, delete-orphan" 

162 ) 

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

164 

165 

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. 

170 

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

172 """ 

173 

174 __tablename__ = "contact_sent" 

175 __table_args__ = (UniqueConstraint("contact_id", "msg_id"),) 

176 

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

181 

182 

183class Room(Base): 

184 """ 

185 Legacy room 

186 """ 

187 

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 ) 

194 

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) 

200 

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

202 

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

207 

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) 

213 

214 n_participants: Mapped[Optional[int]] = mapped_column(default=None) 

215 

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

217 

218 user_nick: Mapped[Optional[str]] = mapped_column() 

219 user_resources: Mapped[Optional[str]] = mapped_column(nullable=True) 

220 

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

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

223 

224 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None) 

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

226 

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

228 back_populates="room", 

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

230 cascade="all, delete-orphan", 

231 ) 

232 

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

234 cascade="all, delete-orphan" 

235 ) 

236 

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

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

239 

240 

241class ArchivedMessage(Base): 

242 """ 

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

244 """ 

245 

246 __tablename__ = "mam" 

247 __table_args__ = (UniqueConstraint("room_id", "stanza_id"),) 

248 

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

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

251 

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) 

257 

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

259 

260 

261class _LegacyToXmppIdsBase: 

262 """ 

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

264 

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

266 """ 

267 

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) 

271 

272 

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) 

277 

278 

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) 

283 

284 

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) 

289 

290 

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) 

295 

296 

297class Attachment(Base): 

298 """ 

299 Legacy attachments 

300 """ 

301 

302 __tablename__ = "attachment" 

303 

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

307 

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

312 

313 

314class Participant(Base): 

315 __tablename__ = "participant" 

316 __table_args__ = ( 

317 UniqueConstraint("room_id", "resource"), 

318 UniqueConstraint("room_id", "contact_id"), 

319 ) 

320 

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

322 

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 ) 

327 

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 ) 

334 

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

336 

337 affiliation: Mapped[MucAffiliation] = mapped_column(default="member") 

338 role: Mapped[MucRole] = mapped_column(default="participant") 

339 

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

341 

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) 

345 

346 hats: Mapped[list[tuple[str, str]]] = mapped_column(JSON, default=list) 

347 

348 extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None) 

349 

350 

351class Bob(Base): 

352 __tablename__ = "bob" 

353 

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

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

356 

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) 

360 

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