types.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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: AccessControlMode = AccessControlMode.UNKNOWN
  155. link: AccessControlMode = AccessControlMode.UNKNOWN
  156. members: 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. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  308. view_once: bool = field(default=False, json="viewOnce")
  309. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  310. previews: List[LinkPreview] = field(factory=lambda: [])
  311. @property
  312. def is_message(self) -> bool:
  313. return bool(self.body or self.attachments or self.sticker or self.contacts)
  314. @dataclass
  315. class SentSyncMessage(SerializableAttrs):
  316. message: MessageData
  317. timestamp: int
  318. expiration_start_timestamp: Optional[int] = field(
  319. default=None, json="expirationStartTimestamp"
  320. )
  321. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  322. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  323. destination: Optional[Address] = None
  324. class TypingAction(SerializableEnum):
  325. UNKNOWN = "UNKNOWN"
  326. STARTED = "STARTED"
  327. STOPPED = "STOPPED"
  328. @dataclass
  329. class TypingMessage(SerializableAttrs):
  330. action: TypingAction
  331. timestamp: int
  332. group_id: Optional[GroupID] = None
  333. @dataclass
  334. class OwnReadReceipt(SerializableAttrs):
  335. sender: Address
  336. timestamp: int
  337. class ReceiptType(SerializableEnum):
  338. UNKNOWN = "UNKNOWN"
  339. DELIVERY = "DELIVERY"
  340. READ = "READ"
  341. VIEWED = "VIEWED"
  342. @dataclass
  343. class ReceiptMessage(SerializableAttrs):
  344. type: ReceiptType
  345. timestamps: List[int]
  346. when: int
  347. @dataclass
  348. class ContactSyncMeta(SerializableAttrs):
  349. id: Optional[str] = None
  350. @dataclass
  351. class ConfigItem(SerializableAttrs):
  352. present: bool = False
  353. @dataclass
  354. class ClientConfiguration(SerializableAttrs):
  355. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  356. typing_indicators: Optional[ConfigItem] = field(
  357. factory=lambda: ConfigItem(), json="typingIndicators"
  358. )
  359. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  360. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  361. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  362. )
  363. class StickerPackOperation(ExtensibleEnum):
  364. INSTALL = "INSTALL"
  365. # there are very likely others
  366. @dataclass
  367. class StickerPackOperations(SerializableAttrs):
  368. type: StickerPackOperation
  369. pack_id: str = field(json="packID")
  370. pack_key: str = field(json="packKey")
  371. @dataclass
  372. class SyncMessage(SerializableAttrs):
  373. sent: Optional[SentSyncMessage] = None
  374. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  375. contacts: Optional[ContactSyncMeta] = None
  376. groups: Optional[ContactSyncMeta] = None
  377. configuration: Optional[ClientConfiguration] = None
  378. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  379. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  380. default=None, json="stickerPackOperations"
  381. )
  382. contacts_complete: bool = field(default=False, json="contactsComplete")
  383. class OfferMessageType(SerializableEnum):
  384. AUDIO_CALL = "AUDIO_CALL"
  385. VIDEO_CALL = "VIDEO_CALL"
  386. @dataclass
  387. class OfferMessage(SerializableAttrs):
  388. id: int
  389. type: OfferMessageType
  390. class HangupMessageType(SerializableEnum):
  391. NORMAL = "NORMAL"
  392. ACCEPTED = "ACCEPTED"
  393. DECLINED = "DECLINED"
  394. BUSY = "BUSY"
  395. NEED_PERMISSION = "NEED_PERMISSION"
  396. @dataclass
  397. class HangupMessage(SerializableAttrs):
  398. id: int
  399. type: HangupMessageType
  400. device_id: int = field(json="deviceId")
  401. @dataclass
  402. class CallMessage(SerializableAttrs):
  403. offer_message: Optional[OfferMessage] = field(default=None, json="offerMessage")
  404. hangup_message: Optional[HangupMessage] = field(default=None, json="hangupMessage")
  405. class MessageType(SerializableEnum):
  406. CIPHERTEXT = "CIPHERTEXT"
  407. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  408. RECEIPT = "RECEIPT"
  409. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  410. KEY_EXCHANGE = "KEY_EXCHANGE"
  411. UNKNOWN = "UNKNOWN"
  412. @dataclass(kw_only=True)
  413. class IncomingMessage(SerializableAttrs):
  414. account: str
  415. source: Address
  416. timestamp: int
  417. type: MessageType
  418. source_device: Optional[int] = None
  419. server_guid: str
  420. server_receiver_timestamp: int
  421. server_deliver_timestamp: int
  422. has_content: bool
  423. unidentified_sender: bool
  424. has_legacy_message: bool
  425. call_message: Optional[CallMessage] = field(default=None)
  426. data_message: Optional[MessageData] = field(default=None)
  427. sync_message: Optional[SyncMessage] = field(default=None)
  428. typing_message: Optional[TypingMessage] = None
  429. receipt_message: Optional[ReceiptMessage] = None
  430. @dataclass(kw_only=True)
  431. class ErrorMessageData(SerializableAttrs):
  432. sender: str
  433. timestamp: int
  434. message: str
  435. sender_device: int
  436. content_hint: int
  437. @dataclass(kw_only=True)
  438. class ErrorMessage(SerializableAttrs):
  439. type: str
  440. version: str
  441. data: ErrorMessageData
  442. error: bool
  443. account: str
  444. class WebsocketConnectionState(SerializableEnum):
  445. # States from signald itself
  446. DISCONNECTED = "DISCONNECTED"
  447. CONNECTING = "CONNECTING"
  448. CONNECTED = "CONNECTED"
  449. RECONNECTING = "RECONNECTING"
  450. DISCONNECTING = "DISCONNECTING"
  451. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  452. FAILED = "FAILED"
  453. # Socket disconnect state
  454. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  455. class WebsocketType(SerializableEnum):
  456. IDENTIFIED = "IDENTIFIED"
  457. UNIDENTIFIED = "UNIDENTIFIED"
  458. @dataclass
  459. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  460. state: WebsocketConnectionState
  461. account: str
  462. socket: Optional[WebsocketType] = None
  463. exception: Optional[str] = None
  464. @dataclass
  465. class JoinGroupResponse(SerializableAttrs):
  466. group_id: str = field(json="groupID")
  467. pending_admin_approval: bool = field(json="pendingAdminApproval")
  468. member_count: Optional[int] = field(json="memberCount", default=None)
  469. revision: Optional[int] = None
  470. title: Optional[str] = None
  471. description: Optional[str] = None
  472. class ProofRequiredType(SerializableEnum):
  473. RECAPTCHA = "RECAPTCHA"
  474. PUSH_CHALLENGE = "PUSH_CHALLENGE"
  475. @dataclass
  476. class ProofRequiredError(SerializableAttrs):
  477. options: List[ProofRequiredType] = field(factory=lambda: [])
  478. message: Optional[str] = None
  479. retry_after: Optional[int] = None
  480. token: Optional[str] = None
  481. @dataclass
  482. class SendSuccessData(SerializableAttrs):
  483. devices: List[int] = field(factory=lambda: [])
  484. duration: Optional[int] = None
  485. needs_sync: bool = field(json="needsSync", default=False)
  486. unidentified: bool = field(json="unidentified", default=False)
  487. @dataclass
  488. class SendMessageResult(SerializableAttrs):
  489. address: Address
  490. success: Optional[SendSuccessData] = None
  491. proof_required_failure: Optional[ProofRequiredError] = None
  492. identity_failure: Optional[str] = field(json="identityFailure", default=None)
  493. network_failure: bool = field(json="networkFailure", default=False)
  494. unregistered_failure: bool = field(json="unregisteredFailure", default=False)
  495. @dataclass
  496. class SendMessageResponse(SerializableAttrs):
  497. results: List[SendMessageResult]
  498. timestamp: int