types.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. # Copyright (c) 2022 Tulir Asokan
  2. #
  3. # This Source Code Form is subject to the terms of the Mozilla Public
  4. # License, v. 2.0. If a copy of the MPL was not distributed with this
  5. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. from typing import Dict, List, NewType, Optional
  7. from datetime import datetime, timedelta
  8. from uuid import UUID
  9. from attr import dataclass
  10. from mautrix.types import ExtensibleEnum, SerializableAttrs, SerializableEnum, field
  11. GroupID = NewType("GroupID", str)
  12. @dataclass(frozen=True, eq=False)
  13. class Address(SerializableAttrs):
  14. number: Optional[str] = None
  15. uuid: Optional[UUID] = None
  16. @property
  17. def is_valid(self) -> bool:
  18. return bool(self.number) or bool(self.uuid)
  19. @property
  20. def best_identifier(self) -> str:
  21. return str(self.uuid) if self.uuid else self.number
  22. def __eq__(self, other: "Address") -> bool:
  23. if not isinstance(other, Address):
  24. return False
  25. if self.uuid and other.uuid:
  26. return self.uuid == other.uuid
  27. elif self.number and other.number:
  28. return self.number == other.number
  29. return False
  30. def __hash__(self) -> int:
  31. if self.uuid:
  32. return hash(self.uuid)
  33. return hash(self.number)
  34. @classmethod
  35. def parse(cls, value: str) -> "Address":
  36. return Address(number=value) if value.startswith("+") else Address(uuid=UUID(value))
  37. @dataclass
  38. class Account(SerializableAttrs):
  39. account_id: str
  40. device_id: int
  41. address: Address
  42. def pluralizer(val: int) -> str:
  43. if val == 1:
  44. return ""
  45. return "s"
  46. @dataclass
  47. class DeviceInfo(SerializableAttrs):
  48. id: int
  49. created: int
  50. last_seen: int = field(json="lastSeen")
  51. name: Optional[str] = None
  52. @property
  53. def name_with_default(self) -> str:
  54. if self.name:
  55. return self.name
  56. return "primary device" if self.id == 1 else "unnamed device"
  57. @property
  58. def created_fmt(self) -> str:
  59. return datetime.utcfromtimestamp(self.created / 1000).strftime("%Y-%m-%d %H:%M:%S UTC")
  60. @property
  61. def last_seen_fmt(self) -> str:
  62. dt = datetime.utcfromtimestamp(self.last_seen / 1000)
  63. now = datetime.utcnow()
  64. if dt.date() == now.date():
  65. return "today"
  66. elif (dt + timedelta(days=1)).date() == now.date():
  67. return "yesterday"
  68. day_diff = (now - dt).days
  69. if day_diff < 30:
  70. return f"{day_diff} day{pluralizer(day_diff)} ago"
  71. return dt.strftime("%Y-%m-%d")
  72. @dataclass
  73. class LinkSession(SerializableAttrs):
  74. uri: str
  75. session_id: str
  76. @dataclass
  77. class TrustLevel(SerializableEnum):
  78. TRUSTED_UNVERIFIED = "TRUSTED_UNVERIFIED"
  79. TRUSTED_VERIFIED = "TRUSTED_VERIFIED"
  80. UNTRUSTED = "UNTRUSTED"
  81. @dataclass
  82. class Identity(SerializableAttrs):
  83. trust_level: TrustLevel
  84. added: int
  85. safety_number: str
  86. qr_code_data: str
  87. @dataclass
  88. class GetIdentitiesResponse(SerializableAttrs):
  89. address: Address
  90. identities: List[Identity]
  91. @dataclass
  92. class Contact(SerializableAttrs):
  93. address: Address
  94. name: Optional[str] = None
  95. color: Optional[str] = None
  96. profile_key: Optional[str] = field(default=None, json="profileKey")
  97. message_expiration_time: int = field(default=0, json="messageExpirationTime")
  98. @dataclass
  99. class Capabilities(SerializableAttrs):
  100. gv2: bool = False
  101. storage: bool = False
  102. gv1_migration: bool = field(default=False, json="gv1-migration")
  103. @dataclass
  104. class Profile(SerializableAttrs):
  105. name: str = ""
  106. profile_name: str = ""
  107. avatar: str = ""
  108. identity_key: str = ""
  109. unidentified_access: str = ""
  110. unrestricted_unidentified_access: bool = False
  111. address: Optional[Address] = None
  112. expiration_time: int = 0
  113. capabilities: Optional[Capabilities] = None
  114. @dataclass
  115. class Group(SerializableAttrs):
  116. group_id: GroupID = field(json="groupId")
  117. name: str = "Unknown group"
  118. # Sometimes "UPDATE"
  119. type: Optional[str] = None
  120. # Not always present
  121. members: List[Address] = field(factory=lambda: [])
  122. avatar_id: int = field(default=0, json="avatarId")
  123. @dataclass(kw_only=True)
  124. class GroupV2ID(SerializableAttrs):
  125. id: GroupID
  126. revision: Optional[int] = None
  127. removed: Optional[bool] = False
  128. class AccessControlMode(SerializableEnum):
  129. UNKNOWN = "UNKNOWN"
  130. ANY = "ANY"
  131. MEMBER = "MEMBER"
  132. ADMINISTRATOR = "ADMINISTRATOR"
  133. UNSATISFIABLE = "UNSATISFIABLE"
  134. UNRECOGNIZED = "UNRECOGNIZED"
  135. class AnnouncementsMode(SerializableEnum):
  136. UNKNOWN = "UNKNOWN"
  137. ENABLED = "ENABLED"
  138. DISABLED = "DISABLED"
  139. @dataclass
  140. class GroupAccessControl(SerializableAttrs):
  141. attributes: AccessControlMode = AccessControlMode.UNKNOWN
  142. link: AccessControlMode = AccessControlMode.UNKNOWN
  143. members: AccessControlMode = AccessControlMode.UNKNOWN
  144. class GroupMemberRole(SerializableEnum):
  145. UNKNOWN = "UNKNOWN"
  146. DEFAULT = "DEFAULT"
  147. ADMINISTRATOR = "ADMINISTRATOR"
  148. UNRECOGNIZED = "UNRECOGNIZED"
  149. @dataclass
  150. class GroupMember(SerializableAttrs):
  151. uuid: UUID
  152. joined_revision: int = 0
  153. role: GroupMemberRole = GroupMemberRole.UNKNOWN
  154. @dataclass(kw_only=True)
  155. class GroupV2(GroupV2ID, SerializableAttrs):
  156. title: str
  157. description: Optional[str] = None
  158. avatar: Optional[str] = None
  159. timer: Optional[int] = None
  160. master_key: Optional[str] = field(default=None, json="masterKey")
  161. invite_link: Optional[str] = field(default=None, json="inviteLink")
  162. access_control: GroupAccessControl = field(
  163. factory=lambda: GroupAccessControl(), json="accessControl"
  164. )
  165. members: List[Address]
  166. member_detail: List[GroupMember] = field(factory=lambda: [], json="memberDetail")
  167. pending_members: List[Address] = field(factory=lambda: [], json="pendingMembers")
  168. pending_member_detail: List[GroupMember] = field(
  169. factory=lambda: [], json="pendingMemberDetail"
  170. )
  171. requesting_members: List[Address] = field(factory=lambda: [], json="requestingMembers")
  172. announcements: AnnouncementsMode = field(default=AnnouncementsMode.UNKNOWN)
  173. @dataclass
  174. class Attachment(SerializableAttrs):
  175. width: int = 0
  176. height: int = 0
  177. caption: Optional[str] = None
  178. preview: Optional[str] = None
  179. blurhash: Optional[str] = None
  180. voice_note: bool = field(default=False, json="voiceNote")
  181. content_type: Optional[str] = field(default=None, json="contentType")
  182. custom_filename: Optional[str] = field(default=None, json="customFilename")
  183. # Only for incoming
  184. id: Optional[str] = None
  185. incoming_filename: Optional[str] = field(default=None, json="storedFilename")
  186. digest: Optional[str] = None
  187. size: Optional[int] = None
  188. # Only for outgoing
  189. outgoing_filename: Optional[str] = field(default=None, json="filename")
  190. @dataclass
  191. class Mention(SerializableAttrs):
  192. uuid: UUID
  193. length: int
  194. start: int = 0
  195. @dataclass
  196. class QuotedAttachment(SerializableAttrs):
  197. content_type: Optional[str] = field(default=None, json="contentType")
  198. filename: Optional[str] = field(default=None, json="fileName")
  199. @dataclass
  200. class Quote(SerializableAttrs):
  201. id: int
  202. author: Address
  203. text: Optional[str] = None
  204. attachments: Optional[List[QuotedAttachment]] = None
  205. mentions: Optional[List[Mention]] = None
  206. @dataclass(kw_only=True)
  207. class Reaction(SerializableAttrs):
  208. emoji: str
  209. remove: bool = False
  210. target_author: Address = field(json="targetAuthor")
  211. target_sent_timestamp: int = field(json="targetSentTimestamp")
  212. @dataclass
  213. class Sticker(SerializableAttrs):
  214. attachment: Attachment
  215. pack_id: str = field(json="packID")
  216. pack_key: str = field(json="packKey")
  217. sticker_id: int = field(json="stickerID")
  218. @dataclass
  219. class RemoteDelete(SerializableAttrs):
  220. target_sent_timestamp: int
  221. class SharedContactDetailType(SerializableEnum):
  222. HOME = "HOME"
  223. WORK = "WORK"
  224. MOBILE = "MOBILE"
  225. CUSTOM = "CUSTOM"
  226. @dataclass
  227. class SharedContactDetail(SerializableAttrs):
  228. type: SharedContactDetailType
  229. value: str
  230. label: Optional[str] = None
  231. @property
  232. def type_or_label(self) -> str:
  233. if self.type != SharedContactDetailType.CUSTOM:
  234. return self.type.value.title()
  235. return self.label
  236. @dataclass
  237. class SharedContactAvatar(SerializableAttrs):
  238. attachment: Attachment
  239. is_profile: bool
  240. @dataclass
  241. class SharedContactName(SerializableAttrs):
  242. display: Optional[str] = None
  243. given: Optional[str] = None
  244. middle: Optional[str] = None
  245. family: Optional[str] = None
  246. prefix: Optional[str] = None
  247. suffix: Optional[str] = None
  248. @property
  249. def parts(self) -> List[str]:
  250. return [self.prefix, self.given, self.middle, self.family, self.suffix]
  251. def __str__(self) -> str:
  252. if self.display:
  253. return self.display
  254. return " ".join(part for part in self.parts if part)
  255. @dataclass
  256. class SharedContactAddress(SerializableAttrs):
  257. type: SharedContactDetailType
  258. label: Optional[str] = None
  259. street: Optional[str] = None
  260. pobox: Optional[str] = None
  261. neighborhood: Optional[str] = None
  262. city: Optional[str] = None
  263. region: Optional[str] = None
  264. postcode: Optional[str] = None
  265. country: Optional[str] = None
  266. @dataclass
  267. class SharedContact(SerializableAttrs):
  268. name: SharedContactName
  269. organization: Optional[str] = None
  270. avatar: Optional[SharedContactAvatar] = None
  271. email: List[SharedContactDetail] = field(factory=lambda: [])
  272. phone: List[SharedContactDetail] = field(factory=lambda: [])
  273. address: Optional[SharedContactAddress] = None
  274. @dataclass
  275. class LinkPreview(SerializableAttrs):
  276. url: str
  277. title: str
  278. description: str
  279. attachment: Optional[Attachment] = None
  280. @dataclass
  281. class MessageData(SerializableAttrs):
  282. timestamp: int
  283. body: Optional[str] = None
  284. quote: Optional[Quote] = None
  285. reaction: Optional[Reaction] = None
  286. attachments: List[Attachment] = field(factory=lambda: [])
  287. sticker: Optional[Sticker] = None
  288. mentions: List[Mention] = field(factory=lambda: [])
  289. contacts: List[SharedContact] = field(factory=lambda: [])
  290. group: Optional[Group] = None
  291. group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
  292. end_session: bool = field(default=False, json="endSession")
  293. expires_in_seconds: int = field(default=0, json="expiresInSeconds")
  294. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  295. view_once: bool = field(default=False, json="viewOnce")
  296. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  297. previews: List[LinkPreview] = field(factory=lambda: [])
  298. @property
  299. def is_message(self) -> bool:
  300. return bool(self.body or self.attachments or self.sticker or self.contacts)
  301. @dataclass
  302. class SentSyncMessage(SerializableAttrs):
  303. message: MessageData
  304. timestamp: int
  305. expiration_start_timestamp: Optional[int] = field(
  306. default=None, json="expirationStartTimestamp"
  307. )
  308. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  309. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  310. destination: Optional[Address] = None
  311. class TypingAction(SerializableEnum):
  312. UNKNOWN = "UNKNOWN"
  313. STARTED = "STARTED"
  314. STOPPED = "STOPPED"
  315. @dataclass
  316. class TypingMessage(SerializableAttrs):
  317. action: TypingAction
  318. timestamp: int
  319. group_id: Optional[GroupID] = None
  320. @dataclass
  321. class OwnReadReceipt(SerializableAttrs):
  322. sender: Address
  323. timestamp: int
  324. class ReceiptType(SerializableEnum):
  325. UNKNOWN = "UNKNOWN"
  326. DELIVERY = "DELIVERY"
  327. READ = "READ"
  328. VIEWED = "VIEWED"
  329. @dataclass
  330. class ReceiptMessage(SerializableAttrs):
  331. type: ReceiptType
  332. timestamps: List[int]
  333. when: int
  334. @dataclass
  335. class ContactSyncMeta(SerializableAttrs):
  336. id: Optional[str] = None
  337. @dataclass
  338. class ConfigItem(SerializableAttrs):
  339. present: bool = False
  340. @dataclass
  341. class ClientConfiguration(SerializableAttrs):
  342. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  343. typing_indicators: Optional[ConfigItem] = field(
  344. factory=lambda: ConfigItem(), json="typingIndicators"
  345. )
  346. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  347. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  348. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  349. )
  350. class StickerPackOperation(ExtensibleEnum):
  351. INSTALL = "INSTALL"
  352. # there are very likely others
  353. @dataclass
  354. class StickerPackOperations(SerializableAttrs):
  355. type: StickerPackOperation
  356. pack_id: str = field(json="packID")
  357. pack_key: str = field(json="packKey")
  358. @dataclass
  359. class SyncMessage(SerializableAttrs):
  360. sent: Optional[SentSyncMessage] = None
  361. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  362. contacts: Optional[ContactSyncMeta] = None
  363. groups: Optional[ContactSyncMeta] = None
  364. configuration: Optional[ClientConfiguration] = None
  365. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  366. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  367. default=None, json="stickerPackOperations"
  368. )
  369. contacts_complete: bool = field(default=False, json="contactsComplete")
  370. class OfferMessageType(SerializableEnum):
  371. AUDIO_CALL = "AUDIO_CALL"
  372. VIDEO_CALL = "VIDEO_CALL"
  373. @dataclass
  374. class OfferMessage(SerializableAttrs):
  375. id: int
  376. type: OfferMessageType
  377. class HangupMessageType(SerializableEnum):
  378. NORMAL = "NORMAL"
  379. ACCEPTED = "ACCEPTED"
  380. DECLINED = "DECLINED"
  381. BUSY = "BUSY"
  382. NEED_PERMISSION = "NEED_PERMISSION"
  383. @dataclass
  384. class HangupMessage(SerializableAttrs):
  385. id: int
  386. type: HangupMessageType
  387. device_id: int = field(json="deviceId")
  388. @dataclass
  389. class CallMessage(SerializableAttrs):
  390. offer_message: Optional[OfferMessage] = field(default=None, json="offerMessage")
  391. hangup_message: Optional[HangupMessage] = field(default=None, json="hangupMessage")
  392. class MessageType(SerializableEnum):
  393. CIPHERTEXT = "CIPHERTEXT"
  394. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  395. RECEIPT = "RECEIPT"
  396. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  397. KEY_EXCHANGE = "KEY_EXCHANGE"
  398. UNKNOWN = "UNKNOWN"
  399. @dataclass(kw_only=True)
  400. class IncomingMessage(SerializableAttrs):
  401. account: str
  402. source: Address
  403. timestamp: int
  404. type: MessageType
  405. source_device: Optional[int] = None
  406. server_guid: str
  407. server_receiver_timestamp: int
  408. server_deliver_timestamp: int
  409. has_content: bool
  410. unidentified_sender: bool
  411. has_legacy_message: bool
  412. call_message: Optional[CallMessage] = field(default=None)
  413. data_message: Optional[MessageData] = field(default=None)
  414. sync_message: Optional[SyncMessage] = field(default=None)
  415. typing_message: Optional[TypingMessage] = None
  416. receipt_message: Optional[ReceiptMessage] = None
  417. @dataclass(kw_only=True)
  418. class ErrorMessageData(SerializableAttrs):
  419. sender: str
  420. timestamp: int
  421. message: str
  422. sender_device: int
  423. content_hint: int
  424. @dataclass(kw_only=True)
  425. class ErrorMessage(SerializableAttrs):
  426. type: str
  427. version: str
  428. data: ErrorMessageData
  429. error: bool
  430. account: str
  431. class WebsocketConnectionState(SerializableEnum):
  432. # States from signald itself
  433. DISCONNECTED = "DISCONNECTED"
  434. CONNECTING = "CONNECTING"
  435. CONNECTED = "CONNECTED"
  436. RECONNECTING = "RECONNECTING"
  437. DISCONNECTING = "DISCONNECTING"
  438. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  439. FAILED = "FAILED"
  440. # Socket disconnect state
  441. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  442. class WebsocketType(SerializableEnum):
  443. IDENTIFIED = "IDENTIFIED"
  444. UNIDENTIFIED = "UNIDENTIFIED"
  445. @dataclass
  446. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  447. state: WebsocketConnectionState
  448. account: str
  449. socket: Optional[WebsocketType] = None
  450. exception: Optional[str] = None
  451. @dataclass
  452. class JoinGroupResponse(SerializableAttrs):
  453. group_id: str = field(json="groupID")
  454. pending_admin_approval: bool = field(json="pendingAdminApproval")
  455. member_count: Optional[int] = field(json="memberCount", default=None)
  456. revision: Optional[int] = None
  457. title: Optional[str] = None
  458. description: Optional[str] = None