types.py 18 KB

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