types.py 18 KB

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