types.py 18 KB

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