types.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  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. @property
  23. def number_or_uuid(self) -> str:
  24. return self.number or str(self.uuid)
  25. def __eq__(self, other: "Address") -> bool:
  26. if not isinstance(other, Address):
  27. return False
  28. if self.uuid and other.uuid:
  29. return self.uuid == other.uuid
  30. elif self.number and other.number:
  31. return self.number == other.number
  32. return False
  33. def __hash__(self) -> int:
  34. if self.uuid:
  35. return hash(self.uuid)
  36. return hash(self.number)
  37. @classmethod
  38. def parse(cls, value: str) -> "Address":
  39. return Address(number=value) if value.startswith("+") else Address(uuid=UUID(value))
  40. @dataclass
  41. class Account(SerializableAttrs):
  42. account_id: str
  43. device_id: int
  44. address: Address
  45. pending: bool = False
  46. pni: Optional[str] = None
  47. def pluralizer(val: int) -> str:
  48. if val == 1:
  49. return ""
  50. return "s"
  51. @dataclass
  52. class DeviceInfo(SerializableAttrs):
  53. id: int
  54. created: int
  55. last_seen: int = field(json="lastSeen")
  56. name: Optional[str] = None
  57. @property
  58. def name_with_default(self) -> str:
  59. if self.name:
  60. return self.name
  61. return "primary device" if self.id == 1 else "unnamed device"
  62. @property
  63. def created_fmt(self) -> str:
  64. return datetime.utcfromtimestamp(self.created / 1000).strftime("%Y-%m-%d %H:%M:%S UTC")
  65. @property
  66. def last_seen_fmt(self) -> str:
  67. dt = datetime.utcfromtimestamp(self.last_seen / 1000)
  68. now = datetime.utcnow()
  69. if dt.date() == now.date():
  70. return "today"
  71. elif (dt + timedelta(days=1)).date() == now.date():
  72. return "yesterday"
  73. day_diff = (now - dt).days
  74. if day_diff < 30:
  75. return f"{day_diff} day{pluralizer(day_diff)} ago"
  76. return dt.strftime("%Y-%m-%d")
  77. @dataclass
  78. class LinkSession(SerializableAttrs):
  79. uri: str
  80. session_id: str
  81. class TrustLevel(SerializableEnum):
  82. TRUSTED_UNVERIFIED = "TRUSTED_UNVERIFIED"
  83. TRUSTED_VERIFIED = "TRUSTED_VERIFIED"
  84. UNTRUSTED = "UNTRUSTED"
  85. @property
  86. def human_str(self) -> str:
  87. if self == TrustLevel.TRUSTED_VERIFIED:
  88. return "trusted"
  89. elif self == TrustLevel.TRUSTED_UNVERIFIED:
  90. return "trusted (unverified)"
  91. elif self == TrustLevel.UNTRUSTED:
  92. return "untrusted"
  93. return "unknown"
  94. @dataclass
  95. class Identity(SerializableAttrs):
  96. trust_level: TrustLevel
  97. added: int
  98. safety_number: str
  99. qr_code_data: str
  100. @dataclass
  101. class GetIdentitiesResponse(SerializableAttrs):
  102. address: Address
  103. identities: List[Identity]
  104. @dataclass
  105. class Capabilities(SerializableAttrs):
  106. gv2: bool = False
  107. storage: bool = False
  108. gv1_migration: bool = field(default=False, json="gv1-migration")
  109. announcement_group: bool = False
  110. change_number: bool = False
  111. sender_key: bool = False
  112. stories: bool = False
  113. @dataclass
  114. class Profile(SerializableAttrs):
  115. address: Optional[Address] = None
  116. name: str = ""
  117. contact_name: str = ""
  118. profile_name: str = ""
  119. about: str = ""
  120. avatar: str = ""
  121. color: str = ""
  122. emoji: str = ""
  123. inbox_position: Optional[int] = None
  124. mobilecoin_address: Optional[str] = None
  125. expiration_time: Optional[int] = None
  126. capabilities: Optional[Capabilities] = None
  127. # visible_badge_ids: List[str]
  128. @dataclass
  129. class Group(SerializableAttrs):
  130. group_id: GroupID = field(json="groupId")
  131. name: str = "Unknown group"
  132. # Sometimes "UPDATE"
  133. type: Optional[str] = None
  134. # Not always present
  135. members: List[Address] = field(factory=lambda: [])
  136. avatar_id: int = field(default=0, json="avatarId")
  137. @dataclass(kw_only=True)
  138. class GroupV2ID(SerializableAttrs):
  139. id: GroupID
  140. revision: Optional[int] = None
  141. removed: Optional[bool] = False
  142. class AccessControlMode(SerializableEnum):
  143. UNKNOWN = "UNKNOWN"
  144. ANY = "ANY"
  145. MEMBER = "MEMBER"
  146. ADMINISTRATOR = "ADMINISTRATOR"
  147. UNSATISFIABLE = "UNSATISFIABLE"
  148. UNRECOGNIZED = "UNRECOGNIZED"
  149. class AnnouncementsMode(SerializableEnum):
  150. UNKNOWN = "UNKNOWN"
  151. ENABLED = "ENABLED"
  152. DISABLED = "DISABLED"
  153. @dataclass
  154. class GroupAccessControl(SerializableAttrs):
  155. attributes: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
  156. link: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
  157. members: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
  158. class GroupMemberRole(SerializableEnum):
  159. UNKNOWN = "UNKNOWN"
  160. DEFAULT = "DEFAULT"
  161. ADMINISTRATOR = "ADMINISTRATOR"
  162. UNRECOGNIZED = "UNRECOGNIZED"
  163. @dataclass
  164. class GroupMember(SerializableAttrs):
  165. uuid: UUID
  166. joined_revision: int = 0
  167. role: GroupMemberRole = GroupMemberRole.UNKNOWN
  168. @dataclass
  169. class BannedGroupMember(SerializableAttrs):
  170. uuid: UUID
  171. timestamp: int
  172. @dataclass(kw_only=True)
  173. class GroupV2(GroupV2ID, SerializableAttrs):
  174. title: str
  175. description: Optional[str] = None
  176. avatar: Optional[str] = None
  177. timer: Optional[int] = None
  178. master_key: Optional[str] = field(default=None, json="masterKey")
  179. invite_link: Optional[str] = field(default=None, json="inviteLink")
  180. access_control: GroupAccessControl = field(
  181. factory=lambda: GroupAccessControl(), json="accessControl"
  182. )
  183. members: List[Address]
  184. member_detail: List[GroupMember] = field(factory=lambda: [], json="memberDetail")
  185. pending_members: List[Address] = field(factory=lambda: [], json="pendingMembers")
  186. pending_member_detail: List[GroupMember] = field(
  187. factory=lambda: [], json="pendingMemberDetail"
  188. )
  189. requesting_members: List[Address] = field(factory=lambda: [], json="requestingMembers")
  190. announcements: AnnouncementsMode = field(default=AnnouncementsMode.UNKNOWN)
  191. banned_members: Optional[List[BannedGroupMember]] = None
  192. @dataclass
  193. class Attachment(SerializableAttrs):
  194. width: int = 0
  195. height: int = 0
  196. caption: Optional[str] = None
  197. preview: Optional[str] = None
  198. blurhash: Optional[str] = None
  199. voice_note: bool = field(default=False, json="voiceNote")
  200. content_type: Optional[str] = field(default=None, json="contentType")
  201. custom_filename: Optional[str] = field(default=None, json="customFilename")
  202. # Only for incoming
  203. id: Optional[str] = None
  204. incoming_filename: Optional[str] = field(default=None, json="storedFilename")
  205. digest: Optional[str] = None
  206. size: Optional[int] = None
  207. # Only for outgoing
  208. outgoing_filename: Optional[str] = field(default=None, json="filename")
  209. @dataclass
  210. class Mention(SerializableAttrs):
  211. uuid: UUID
  212. length: int
  213. start: int = 0
  214. @dataclass
  215. class QuotedAttachment(SerializableAttrs):
  216. content_type: Optional[str] = field(default=None, json="contentType")
  217. filename: Optional[str] = field(default=None, json="fileName")
  218. @dataclass
  219. class Quote(SerializableAttrs):
  220. id: int
  221. author: Address
  222. text: Optional[str] = None
  223. attachments: Optional[List[QuotedAttachment]] = None
  224. mentions: Optional[List[Mention]] = None
  225. @dataclass(kw_only=True)
  226. class Reaction(SerializableAttrs):
  227. emoji: str
  228. remove: bool = False
  229. target_author: Address = field(json="targetAuthor")
  230. target_sent_timestamp: int = field(json="targetSentTimestamp")
  231. @dataclass
  232. class Sticker(SerializableAttrs):
  233. attachment: Attachment
  234. pack_id: str = field(json="packID")
  235. pack_key: str = field(json="packKey")
  236. sticker_id: int = field(json="stickerID")
  237. @dataclass
  238. class RemoteDelete(SerializableAttrs):
  239. target_sent_timestamp: int
  240. class SharedContactDetailType(SerializableEnum):
  241. HOME = "HOME"
  242. WORK = "WORK"
  243. MOBILE = "MOBILE"
  244. CUSTOM = "CUSTOM"
  245. @dataclass
  246. class SharedContactDetail(SerializableAttrs):
  247. type: SharedContactDetailType
  248. value: str
  249. label: Optional[str] = None
  250. @property
  251. def type_or_label(self) -> str:
  252. if self.type != SharedContactDetailType.CUSTOM:
  253. return self.type.value.title()
  254. return self.label
  255. @dataclass
  256. class SharedContactAvatar(SerializableAttrs):
  257. attachment: Attachment
  258. is_profile: bool
  259. @dataclass
  260. class SharedContactName(SerializableAttrs):
  261. display: Optional[str] = None
  262. given: Optional[str] = None
  263. middle: Optional[str] = None
  264. family: Optional[str] = None
  265. prefix: Optional[str] = None
  266. suffix: Optional[str] = None
  267. @property
  268. def parts(self) -> List[str]:
  269. return [self.prefix, self.given, self.middle, self.family, self.suffix]
  270. def __str__(self) -> str:
  271. if self.display:
  272. return self.display
  273. return " ".join(part for part in self.parts if part)
  274. @dataclass
  275. class SharedContactAddress(SerializableAttrs):
  276. type: SharedContactDetailType
  277. label: Optional[str] = None
  278. street: Optional[str] = None
  279. pobox: Optional[str] = None
  280. neighborhood: Optional[str] = None
  281. city: Optional[str] = None
  282. region: Optional[str] = None
  283. postcode: Optional[str] = None
  284. country: Optional[str] = None
  285. @dataclass
  286. class SharedContact(SerializableAttrs):
  287. name: SharedContactName
  288. organization: Optional[str] = None
  289. avatar: Optional[SharedContactAvatar] = None
  290. email: List[SharedContactDetail] = field(factory=lambda: [])
  291. phone: List[SharedContactDetail] = field(factory=lambda: [])
  292. address: Optional[SharedContactAddress] = None
  293. @dataclass
  294. class LinkPreview(SerializableAttrs):
  295. url: str
  296. title: str
  297. description: str
  298. attachment: Optional[Attachment] = None
  299. @dataclass
  300. class MessageData(SerializableAttrs):
  301. timestamp: int
  302. body: Optional[str] = None
  303. quote: Optional[Quote] = None
  304. reaction: Optional[Reaction] = None
  305. attachments: List[Attachment] = field(factory=lambda: [])
  306. sticker: Optional[Sticker] = None
  307. mentions: List[Mention] = field(factory=lambda: [])
  308. contacts: List[SharedContact] = field(factory=lambda: [])
  309. group: Optional[Group] = None
  310. group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
  311. end_session: bool = field(default=False, json="endSession")
  312. expires_in_seconds: int = field(default=0, json="expiresInSeconds")
  313. is_expiration_update: bool = field(default=False)
  314. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  315. view_once: bool = field(default=False, json="viewOnce")
  316. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  317. previews: List[LinkPreview] = field(factory=lambda: [])
  318. @property
  319. def is_message(self) -> bool:
  320. return bool(self.body or self.attachments or self.sticker or self.contacts)
  321. @dataclass
  322. class SentSyncMessage(SerializableAttrs):
  323. message: MessageData
  324. timestamp: int
  325. expiration_start_timestamp: Optional[int] = field(
  326. default=None, json="expirationStartTimestamp"
  327. )
  328. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  329. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  330. destination: Optional[Address] = None
  331. class TypingAction(SerializableEnum):
  332. UNKNOWN = "UNKNOWN"
  333. STARTED = "STARTED"
  334. STOPPED = "STOPPED"
  335. @dataclass
  336. class TypingMessage(SerializableAttrs):
  337. action: TypingAction
  338. timestamp: int
  339. group_id: Optional[GroupID] = None
  340. @dataclass
  341. class OwnReadReceipt(SerializableAttrs):
  342. sender: Address
  343. timestamp: int
  344. class ReceiptType(SerializableEnum):
  345. UNKNOWN = "UNKNOWN"
  346. DELIVERY = "DELIVERY"
  347. READ = "READ"
  348. VIEWED = "VIEWED"
  349. @dataclass
  350. class ReceiptMessage(SerializableAttrs):
  351. type: ReceiptType
  352. timestamps: List[int]
  353. when: int
  354. @dataclass
  355. class ContactSyncMeta(SerializableAttrs):
  356. id: Optional[str] = None
  357. @dataclass
  358. class ConfigItem(SerializableAttrs):
  359. present: bool = False
  360. @dataclass
  361. class ClientConfiguration(SerializableAttrs):
  362. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  363. typing_indicators: Optional[ConfigItem] = field(
  364. factory=lambda: ConfigItem(), json="typingIndicators"
  365. )
  366. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  367. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  368. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  369. )
  370. class StickerPackOperation(ExtensibleEnum):
  371. INSTALL = "INSTALL"
  372. # there are very likely others
  373. @dataclass
  374. class StickerPackOperations(SerializableAttrs):
  375. type: StickerPackOperation
  376. pack_id: str = field(json="packID")
  377. pack_key: str = field(json="packKey")
  378. @dataclass
  379. class SyncMessage(SerializableAttrs):
  380. sent: Optional[SentSyncMessage] = None
  381. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  382. contacts: Optional[ContactSyncMeta] = None
  383. groups: Optional[ContactSyncMeta] = None
  384. configuration: Optional[ClientConfiguration] = None
  385. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  386. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  387. default=None, json="stickerPackOperations"
  388. )
  389. contacts_complete: bool = field(default=False, json="contactsComplete")
  390. class OfferMessageType(SerializableEnum):
  391. AUDIO_CALL = "audio_call"
  392. VIDEO_CALL = "video_call"
  393. @dataclass
  394. class OfferMessage(SerializableAttrs):
  395. id: int
  396. type: OfferMessageType
  397. opaque: Optional[str] = None
  398. sdp: Optional[str] = None
  399. @dataclass
  400. class AnswerMessage(SerializableAttrs):
  401. id: int
  402. opaque: Optional[str] = None
  403. sdp: Optional[str] = None
  404. @dataclass
  405. class ICEUpdateMessage(SerializableAttrs):
  406. id: int
  407. opaque: Optional[str] = None
  408. sdp: Optional[str] = None
  409. @dataclass
  410. class BusyMessage(SerializableAttrs):
  411. id: int
  412. class HangupMessageType(SerializableEnum):
  413. NORMAL = "normal"
  414. ACCEPTED = "accepted"
  415. DECLINED = "declined"
  416. BUSY = "busy"
  417. NEED_PERMISSION = "need_permission"
  418. @dataclass
  419. class HangupMessage(SerializableAttrs):
  420. id: int
  421. type: HangupMessageType
  422. device_id: int
  423. legacy: bool = False
  424. @dataclass
  425. class CallMessage(SerializableAttrs):
  426. offer_message: Optional[OfferMessage] = None
  427. hangup_message: Optional[HangupMessage] = None
  428. answer_message: Optional[AnswerMessage] = None
  429. busy_message: Optional[BusyMessage] = None
  430. ice_update_message: Optional[List[ICEUpdateMessage]] = None
  431. multi_ring: bool = False
  432. destination_device_id: Optional[int] = None
  433. class MessageType(SerializableEnum):
  434. CIPHERTEXT = "CIPHERTEXT"
  435. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  436. RECEIPT = "RECEIPT"
  437. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  438. KEY_EXCHANGE = "KEY_EXCHANGE"
  439. UNKNOWN = "UNKNOWN"
  440. @dataclass(kw_only=True)
  441. class IncomingMessage(SerializableAttrs):
  442. account: str
  443. source: Address
  444. timestamp: int
  445. type: MessageType
  446. source_device: Optional[int] = None
  447. server_guid: str
  448. server_receiver_timestamp: int
  449. server_deliver_timestamp: int
  450. has_content: bool
  451. unidentified_sender: bool
  452. has_legacy_message: bool
  453. call_message: Optional[CallMessage] = field(default=None)
  454. data_message: Optional[MessageData] = field(default=None)
  455. sync_message: Optional[SyncMessage] = field(default=None)
  456. typing_message: Optional[TypingMessage] = None
  457. receipt_message: Optional[ReceiptMessage] = None
  458. @dataclass(kw_only=True)
  459. class ErrorMessageData(SerializableAttrs):
  460. sender: str
  461. timestamp: int
  462. message: str
  463. sender_device: int
  464. content_hint: int
  465. @dataclass(kw_only=True)
  466. class ErrorMessage(SerializableAttrs):
  467. type: str
  468. version: str
  469. data: ErrorMessageData
  470. error: bool
  471. account: str
  472. @dataclass(kw_only=True)
  473. class StorageChangeData(SerializableAttrs):
  474. version: int
  475. @dataclass(kw_only=True)
  476. class StorageChange(SerializableAttrs):
  477. type: str
  478. version: str
  479. data: StorageChangeData
  480. account: str
  481. class WebsocketConnectionState(SerializableEnum):
  482. # States from signald itself
  483. DISCONNECTED = "DISCONNECTED"
  484. CONNECTING = "CONNECTING"
  485. CONNECTED = "CONNECTED"
  486. RECONNECTING = "RECONNECTING"
  487. DISCONNECTING = "DISCONNECTING"
  488. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  489. FAILED = "FAILED"
  490. # Socket disconnect state
  491. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  492. class WebsocketType(SerializableEnum):
  493. IDENTIFIED = "IDENTIFIED"
  494. UNIDENTIFIED = "UNIDENTIFIED"
  495. @dataclass
  496. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  497. state: WebsocketConnectionState
  498. account: str
  499. socket: Optional[WebsocketType] = None
  500. exception: Optional[str] = None
  501. @dataclass
  502. class JoinGroupResponse(SerializableAttrs):
  503. group_id: str = field(json="groupID")
  504. pending_admin_approval: bool = field(json="pendingAdminApproval")
  505. member_count: Optional[int] = field(json="memberCount", default=None)
  506. revision: Optional[int] = None
  507. title: Optional[str] = None
  508. description: Optional[str] = None
  509. class ProofRequiredType(SerializableEnum):
  510. RECAPTCHA = "RECAPTCHA"
  511. PUSH_CHALLENGE = "PUSH_CHALLENGE"
  512. @dataclass
  513. class ProofRequiredError(SerializableAttrs):
  514. options: List[ProofRequiredType] = field(factory=lambda: [])
  515. message: Optional[str] = None
  516. retry_after: Optional[int] = None
  517. token: Optional[str] = None
  518. @dataclass
  519. class SendSuccessData(SerializableAttrs):
  520. devices: List[int] = field(factory=lambda: [])
  521. duration: Optional[int] = None
  522. needs_sync: bool = field(json="needsSync", default=False)
  523. unidentified: bool = field(json="unidentified", default=False)
  524. @dataclass
  525. class SendMessageResult(SerializableAttrs):
  526. address: Address
  527. success: Optional[SendSuccessData] = None
  528. proof_required_failure: Optional[ProofRequiredError] = None
  529. identity_failure: Optional[str] = field(json="identityFailure", default=None)
  530. network_failure: bool = field(json="networkFailure", default=False)
  531. unregistered_failure: bool = field(json="unregisteredFailure", default=False)
  532. @dataclass
  533. class SendMessageResponse(SerializableAttrs):
  534. results: List[SendMessageResult]
  535. timestamp: int