types.py 15 KB

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