types.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. # Copyright (c) 2020 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. # Only for outgoing
  181. outgoing_filename: Optional[str] = field(default=None, json="filename")
  182. @dataclass
  183. class Quote(SerializableAttrs):
  184. id: int
  185. author: Address
  186. text: Optional[str] = None
  187. # TODO: attachments, mentions
  188. @dataclass(kw_only=True)
  189. class Reaction(SerializableAttrs):
  190. emoji: str
  191. remove: bool = False
  192. target_author: Address = field(json="targetAuthor")
  193. target_sent_timestamp: int = field(json="targetSentTimestamp")
  194. @dataclass
  195. class Sticker(SerializableAttrs):
  196. attachment: Attachment
  197. pack_id: str = field(json="packID")
  198. pack_key: str = field(json="packKey")
  199. sticker_id: int = field(json="stickerID")
  200. @dataclass
  201. class RemoteDelete(SerializableAttrs):
  202. target_sent_timestamp: int = field(json="targetSentTimestamp")
  203. @dataclass
  204. class Mention(SerializableAttrs):
  205. uuid: UUID
  206. length: int
  207. start: int = 0
  208. @dataclass
  209. class MessageData(SerializableAttrs):
  210. timestamp: int
  211. body: Optional[str] = None
  212. quote: Optional[Quote] = None
  213. reaction: Optional[Reaction] = None
  214. attachments: List[Attachment] = field(factory=lambda: [])
  215. sticker: Optional[Sticker] = None
  216. mentions: List[Mention] = field(factory=lambda: [])
  217. group: Optional[Group] = None
  218. group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
  219. end_session: bool = field(default=False, json="endSession")
  220. expires_in_seconds: int = field(default=0, json="expiresInSeconds")
  221. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  222. view_once: bool = field(default=False, json="viewOnce")
  223. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  224. @dataclass
  225. class SentSyncMessage(SerializableAttrs):
  226. message: MessageData
  227. timestamp: int
  228. expiration_start_timestamp: Optional[int] = field(
  229. default=None, json="expirationStartTimestamp"
  230. )
  231. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  232. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  233. destination: Optional[Address] = None
  234. class TypingAction(SerializableEnum):
  235. UNKNOWN = "UNKNOWN"
  236. STARTED = "STARTED"
  237. STOPPED = "STOPPED"
  238. @dataclass
  239. class TypingNotification(SerializableAttrs):
  240. action: TypingAction
  241. timestamp: int
  242. group_id: Optional[GroupID] = field(default=None, json="groupId")
  243. @dataclass
  244. class OwnReadReceipt(SerializableAttrs):
  245. sender: Address
  246. timestamp: int
  247. class ReceiptType(SerializableEnum):
  248. UNKNOWN = "UNKNOWN"
  249. DELIVERY = "DELIVERY"
  250. READ = "READ"
  251. VIEWED = "VIEWED"
  252. @dataclass
  253. class Receipt(SerializableAttrs):
  254. type: ReceiptType
  255. timestamps: List[int]
  256. when: int
  257. @dataclass
  258. class ContactSyncMeta(SerializableAttrs):
  259. id: Optional[str] = None
  260. @dataclass
  261. class ConfigItem(SerializableAttrs):
  262. present: bool = False
  263. @dataclass
  264. class ClientConfiguration(SerializableAttrs):
  265. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  266. typing_indicators: Optional[ConfigItem] = field(
  267. factory=lambda: ConfigItem(), json="typingIndicators"
  268. )
  269. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  270. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  271. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  272. )
  273. class StickerPackOperation(ExtensibleEnum):
  274. INSTALL = "INSTALL"
  275. # there are very likely others
  276. @dataclass
  277. class StickerPackOperations(SerializableAttrs):
  278. type: StickerPackOperation
  279. pack_id: str = field(json="packID")
  280. pack_key: str = field(json="packKey")
  281. @dataclass
  282. class SyncMessage(SerializableAttrs):
  283. sent: Optional[SentSyncMessage] = None
  284. typing: Optional[TypingNotification] = None
  285. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  286. contacts: Optional[ContactSyncMeta] = None
  287. groups: Optional[ContactSyncMeta] = None
  288. configuration: Optional[ClientConfiguration] = None
  289. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  290. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  291. default=None, json="stickerPackOperations"
  292. )
  293. contacts_complete: bool = field(default=False, json="contactsComplete")
  294. class MessageType(SerializableEnum):
  295. CIPHERTEXT = "CIPHERTEXT"
  296. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  297. RECEIPT = "RECEIPT"
  298. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  299. KEY_EXCHANGE = "KEY_EXCHANGE"
  300. UNKNOWN = "UNKNOWN"
  301. @dataclass(kw_only=True)
  302. class Message(SerializableAttrs):
  303. username: str
  304. source: Address
  305. timestamp: int
  306. timestamp_iso: str = field(json="timestampISO")
  307. type: MessageType
  308. source_device: Optional[int] = field(json="sourceDevice", default=None)
  309. server_timestamp: Optional[int] = field(json="serverTimestamp", default=None)
  310. server_delivered_timestamp: int = field(json="serverDeliveredTimestamp")
  311. has_content: bool = field(json="hasContent", default=False)
  312. is_unidentified_sender: Optional[bool] = field(json="isUnidentifiedSender", default=None)
  313. has_legacy_message: bool = field(default=False, json="hasLegacyMessage")
  314. data_message: Optional[MessageData] = field(default=None, json="dataMessage")
  315. sync_message: Optional[SyncMessage] = field(default=None, json="syncMessage")
  316. typing: Optional[TypingNotification] = None
  317. receipt: Optional[Receipt] = None
  318. class WebsocketConnectionState(SerializableEnum):
  319. # States from signald itself
  320. DISCONNECTED = "DISCONNECTED"
  321. CONNECTING = "CONNECTING"
  322. CONNECTED = "CONNECTED"
  323. RECONNECTING = "RECONNECTING"
  324. DISCONNECTING = "DISCONNECTING"
  325. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  326. FAILED = "FAILED"
  327. # Socket disconnect state
  328. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  329. @dataclass
  330. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  331. state: WebsocketConnectionState
  332. account: str
  333. exception: Optional[str] = None