types.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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. class AnnouncementsMode(SerializableEnum):
  136. UNKNOWN = "UNKNOWN"
  137. ENABLED = "ENABLED"
  138. DISABLED = "DISABLED"
  139. @dataclass
  140. class GroupAccessControl(SerializableAttrs):
  141. attributes: AccessControlMode = AccessControlMode.UNKNOWN
  142. link: AccessControlMode = AccessControlMode.UNKNOWN
  143. members: AccessControlMode = AccessControlMode.UNKNOWN
  144. class GroupMemberRole(SerializableEnum):
  145. UNKNOWN = "UNKNOWN"
  146. DEFAULT = "DEFAULT"
  147. ADMINISTRATOR = "ADMINISTRATOR"
  148. UNRECOGNIZED = "UNRECOGNIZED"
  149. @dataclass
  150. class GroupMember(SerializableAttrs):
  151. uuid: UUID
  152. joined_revision: int = 0
  153. role: GroupMemberRole = GroupMemberRole.UNKNOWN
  154. @dataclass(kw_only=True)
  155. class GroupV2(GroupV2ID, SerializableAttrs):
  156. title: str
  157. avatar: Optional[str] = None
  158. timer: Optional[int] = None
  159. master_key: Optional[str] = field(default=None, json="masterKey")
  160. invite_link: Optional[str] = field(default=None, json="inviteLink")
  161. access_control: GroupAccessControl = field(
  162. factory=lambda: GroupAccessControl(), json="accessControl"
  163. )
  164. members: List[Address]
  165. member_detail: List[GroupMember] = field(factory=lambda: [], json="memberDetail")
  166. pending_members: List[Address] = field(factory=lambda: [], json="pendingMembers")
  167. pending_member_detail: List[GroupMember] = field(
  168. factory=lambda: [], json="pendingMemberDetail"
  169. )
  170. requesting_members: List[Address] = field(factory=lambda: [], json="requestingMembers")
  171. announcements: AnnouncementsMode = field(default=AnnouncementsMode.UNKNOWN)
  172. @dataclass
  173. class Attachment(SerializableAttrs):
  174. width: int = 0
  175. height: int = 0
  176. caption: Optional[str] = None
  177. preview: Optional[str] = None
  178. blurhash: Optional[str] = None
  179. voice_note: bool = field(default=False, json="voiceNote")
  180. content_type: Optional[str] = field(default=None, json="contentType")
  181. custom_filename: Optional[str] = field(default=None, json="customFilename")
  182. # Only for incoming
  183. id: Optional[str] = None
  184. incoming_filename: Optional[str] = field(default=None, json="storedFilename")
  185. digest: Optional[str] = None
  186. size: Optional[int] = None
  187. # Only for outgoing
  188. outgoing_filename: Optional[str] = field(default=None, json="filename")
  189. @dataclass
  190. class Mention(SerializableAttrs):
  191. uuid: UUID
  192. length: int
  193. start: int = 0
  194. @dataclass
  195. class QuotedAttachment(SerializableAttrs):
  196. content_type: Optional[str] = field(default=None, json="contentType")
  197. filename: Optional[str] = field(default=None, json="fileName")
  198. @dataclass
  199. class Quote(SerializableAttrs):
  200. id: int
  201. author: Address
  202. text: Optional[str] = None
  203. attachments: Optional[List[QuotedAttachment]] = None
  204. mentions: Optional[List[Mention]] = None
  205. @dataclass(kw_only=True)
  206. class Reaction(SerializableAttrs):
  207. emoji: str
  208. remove: bool = False
  209. target_author: Address = field(json="targetAuthor")
  210. target_sent_timestamp: int = field(json="targetSentTimestamp")
  211. @dataclass
  212. class Sticker(SerializableAttrs):
  213. attachment: Attachment
  214. pack_id: str = field(json="packID")
  215. pack_key: str = field(json="packKey")
  216. sticker_id: int = field(json="stickerID")
  217. @dataclass
  218. class RemoteDelete(SerializableAttrs):
  219. target_sent_timestamp: int
  220. class SharedContactDetailType(SerializableEnum):
  221. HOME = "HOME"
  222. WORK = "WORK"
  223. MOBILE = "MOBILE"
  224. CUSTOM = "CUSTOM"
  225. @dataclass
  226. class SharedContactDetail(SerializableAttrs):
  227. type: SharedContactDetailType
  228. value: str
  229. label: Optional[str] = None
  230. @property
  231. def type_or_label(self) -> str:
  232. if self.type != SharedContactDetailType.CUSTOM:
  233. return self.type.value.title()
  234. return self.label
  235. @dataclass
  236. class SharedContactAvatar(SerializableAttrs):
  237. attachment: Attachment
  238. is_profile: bool
  239. @dataclass
  240. class SharedContactName(SerializableAttrs):
  241. display: Optional[str] = None
  242. given: Optional[str] = None
  243. middle: Optional[str] = None
  244. family: Optional[str] = None
  245. prefix: Optional[str] = None
  246. suffix: Optional[str] = None
  247. @property
  248. def parts(self) -> List[str]:
  249. return [self.prefix, self.given, self.middle, self.family, self.suffix]
  250. def __str__(self) -> str:
  251. if self.display:
  252. return self.display
  253. return " ".join(part for part in self.parts if part)
  254. @dataclass
  255. class SharedContactAddress(SerializableAttrs):
  256. type: SharedContactDetailType
  257. label: Optional[str] = None
  258. street: Optional[str] = None
  259. pobox: Optional[str] = None
  260. neighborhood: Optional[str] = None
  261. city: Optional[str] = None
  262. region: Optional[str] = None
  263. postcode: Optional[str] = None
  264. country: Optional[str] = None
  265. @dataclass
  266. class SharedContact(SerializableAttrs):
  267. name: SharedContactName
  268. organization: Optional[str] = None
  269. avatar: Optional[SharedContactAvatar] = None
  270. email: List[SharedContactDetail] = field(factory=lambda: [])
  271. phone: List[SharedContactDetail] = field(factory=lambda: [])
  272. address: Optional[SharedContactAddress] = None
  273. @dataclass
  274. class LinkPreview(SerializableAttrs):
  275. url: str
  276. title: str
  277. description: str
  278. attachment: Optional[Attachment] = None
  279. @dataclass
  280. class MessageData(SerializableAttrs):
  281. timestamp: int
  282. body: Optional[str] = None
  283. quote: Optional[Quote] = None
  284. reaction: Optional[Reaction] = None
  285. attachments: List[Attachment] = field(factory=lambda: [])
  286. sticker: Optional[Sticker] = None
  287. mentions: List[Mention] = field(factory=lambda: [])
  288. contacts: List[SharedContact] = field(factory=lambda: [])
  289. group: Optional[Group] = None
  290. group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
  291. end_session: bool = field(default=False, json="endSession")
  292. expires_in_seconds: int = field(default=0, json="expiresInSeconds")
  293. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  294. view_once: bool = field(default=False, json="viewOnce")
  295. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  296. previews: List[LinkPreview] = field(factory=lambda: [])
  297. @property
  298. def is_message(self) -> bool:
  299. return bool(self.body or self.attachments or self.sticker or self.contacts)
  300. @dataclass
  301. class SentSyncMessage(SerializableAttrs):
  302. message: MessageData
  303. timestamp: int
  304. expiration_start_timestamp: Optional[int] = field(
  305. default=None, json="expirationStartTimestamp"
  306. )
  307. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  308. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  309. destination: Optional[Address] = None
  310. class TypingAction(SerializableEnum):
  311. UNKNOWN = "UNKNOWN"
  312. STARTED = "STARTED"
  313. STOPPED = "STOPPED"
  314. @dataclass
  315. class TypingMessage(SerializableAttrs):
  316. action: TypingAction
  317. timestamp: int
  318. group_id: Optional[GroupID] = None
  319. @dataclass
  320. class OwnReadReceipt(SerializableAttrs):
  321. sender: Address
  322. timestamp: int
  323. class ReceiptType(SerializableEnum):
  324. UNKNOWN = "UNKNOWN"
  325. DELIVERY = "DELIVERY"
  326. READ = "READ"
  327. VIEWED = "VIEWED"
  328. @dataclass
  329. class ReceiptMessage(SerializableAttrs):
  330. type: ReceiptType
  331. timestamps: List[int]
  332. when: int
  333. @dataclass
  334. class ContactSyncMeta(SerializableAttrs):
  335. id: Optional[str] = None
  336. @dataclass
  337. class ConfigItem(SerializableAttrs):
  338. present: bool = False
  339. @dataclass
  340. class ClientConfiguration(SerializableAttrs):
  341. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  342. typing_indicators: Optional[ConfigItem] = field(
  343. factory=lambda: ConfigItem(), json="typingIndicators"
  344. )
  345. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  346. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  347. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  348. )
  349. class StickerPackOperation(ExtensibleEnum):
  350. INSTALL = "INSTALL"
  351. # there are very likely others
  352. @dataclass
  353. class StickerPackOperations(SerializableAttrs):
  354. type: StickerPackOperation
  355. pack_id: str = field(json="packID")
  356. pack_key: str = field(json="packKey")
  357. @dataclass
  358. class SyncMessage(SerializableAttrs):
  359. sent: Optional[SentSyncMessage] = None
  360. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  361. contacts: Optional[ContactSyncMeta] = None
  362. groups: Optional[ContactSyncMeta] = None
  363. configuration: Optional[ClientConfiguration] = None
  364. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  365. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  366. default=None, json="stickerPackOperations"
  367. )
  368. contacts_complete: bool = field(default=False, json="contactsComplete")
  369. class OfferMessageType(SerializableEnum):
  370. AUDIO_CALL = "AUDIO_CALL"
  371. VIDEO_CALL = "VIDEO_CALL"
  372. @dataclass
  373. class OfferMessage(SerializableAttrs):
  374. id: int
  375. type: OfferMessageType
  376. class HangupMessageType(SerializableEnum):
  377. NORMAL = "NORMAL"
  378. ACCEPTED = "ACCEPTED"
  379. DECLINED = "DECLINED"
  380. BUSY = "BUSY"
  381. NEED_PERMISSION = "NEED_PERMISSION"
  382. @dataclass
  383. class HangupMessage(SerializableAttrs):
  384. id: int
  385. type: HangupMessageType
  386. device_id: int = field(json="deviceId")
  387. @dataclass
  388. class CallMessage(SerializableAttrs):
  389. offer_message: Optional[OfferMessage] = field(default=None, json="offerMessage")
  390. hangup_message: Optional[HangupMessage] = field(default=None, json="hangupMessage")
  391. class MessageType(SerializableEnum):
  392. CIPHERTEXT = "CIPHERTEXT"
  393. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  394. RECEIPT = "RECEIPT"
  395. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  396. KEY_EXCHANGE = "KEY_EXCHANGE"
  397. UNKNOWN = "UNKNOWN"
  398. @dataclass(kw_only=True)
  399. class IncomingMessage(SerializableAttrs):
  400. account: str
  401. source: Address
  402. timestamp: int
  403. type: MessageType
  404. source_device: Optional[int] = None
  405. server_guid: str
  406. server_receiver_timestamp: int
  407. server_deliver_timestamp: int
  408. has_content: bool
  409. unidentified_sender: bool
  410. has_legacy_message: bool
  411. call_message: Optional[CallMessage] = field(default=None)
  412. data_message: Optional[MessageData] = field(default=None)
  413. sync_message: Optional[SyncMessage] = field(default=None)
  414. typing_message: Optional[TypingMessage] = None
  415. receipt_message: Optional[ReceiptMessage] = None
  416. @dataclass(kw_only=True)
  417. class ErrorMessageData(SerializableAttrs):
  418. sender: str
  419. timestamp: int
  420. message: str
  421. sender_device: int
  422. content_hint: int
  423. @dataclass(kw_only=True)
  424. class ErrorMessage(SerializableAttrs):
  425. type: str
  426. version: str
  427. data: ErrorMessageData
  428. error: bool
  429. account: str
  430. class WebsocketConnectionState(SerializableEnum):
  431. # States from signald itself
  432. DISCONNECTED = "DISCONNECTED"
  433. CONNECTING = "CONNECTING"
  434. CONNECTED = "CONNECTED"
  435. RECONNECTING = "RECONNECTING"
  436. DISCONNECTING = "DISCONNECTING"
  437. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  438. FAILED = "FAILED"
  439. # Socket disconnect state
  440. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  441. class WebsocketType(SerializableEnum):
  442. IDENTIFIED = "IDENTIFIED"
  443. UNIDENTIFIED = "UNIDENTIFIED"
  444. @dataclass
  445. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  446. state: WebsocketConnectionState
  447. account: str
  448. socket: Optional[WebsocketType] = None
  449. exception: Optional[str] = None