types.py 15 KB

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