types.py 17 KB

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