types.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  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. uuid: Optional[UUID] = None
  15. number: Optional[str] = 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 Capabilities(SerializableAttrs):
  106. gv2: bool = False
  107. storage: bool = False
  108. gv1_migration: bool = field(default=False, json="gv1-migration")
  109. announcement_group: bool = False
  110. change_number: bool = False
  111. sender_key: bool = False
  112. stories: bool = False
  113. @dataclass
  114. class Profile(SerializableAttrs):
  115. address: Optional[Address] = None
  116. name: str = ""
  117. contact_name: str = ""
  118. profile_name: str = ""
  119. about: str = ""
  120. avatar: str = ""
  121. color: str = ""
  122. emoji: str = ""
  123. inbox_position: Optional[int] = None
  124. mobilecoin_address: Optional[str] = None
  125. expiration_time: Optional[int] = None
  126. capabilities: Optional[Capabilities] = None
  127. # visible_badge_ids: List[str]
  128. @dataclass
  129. class Group(SerializableAttrs):
  130. group_id: GroupID = field(json="groupId")
  131. name: str = "Unknown group"
  132. # Sometimes "UPDATE"
  133. type: Optional[str] = None
  134. # Not always present
  135. members: List[Address] = field(factory=lambda: [])
  136. avatar_id: int = field(default=0, json="avatarId")
  137. class AccessControlMode(SerializableEnum):
  138. UNKNOWN = "UNKNOWN"
  139. ANY = "ANY"
  140. MEMBER = "MEMBER"
  141. ADMINISTRATOR = "ADMINISTRATOR"
  142. UNSATISFIABLE = "UNSATISFIABLE"
  143. UNRECOGNIZED = "UNRECOGNIZED"
  144. class AnnouncementsMode(SerializableEnum):
  145. UNKNOWN = "UNKNOWN"
  146. ENABLED = "ENABLED"
  147. DISABLED = "DISABLED"
  148. @dataclass
  149. class GroupAccessControl(SerializableAttrs):
  150. attributes: Optional[AccessControlMode] = None
  151. link: Optional[AccessControlMode] = None
  152. members: Optional[AccessControlMode] = None
  153. class GroupMemberRole(SerializableEnum):
  154. UNKNOWN = "UNKNOWN"
  155. DEFAULT = "DEFAULT"
  156. ADMINISTRATOR = "ADMINISTRATOR"
  157. UNRECOGNIZED = "UNRECOGNIZED"
  158. @dataclass
  159. class GroupMember(SerializableAttrs):
  160. uuid: UUID
  161. joined_revision: int = 0
  162. role: GroupMemberRole = GroupMemberRole.UNKNOWN
  163. @property
  164. def address(self) -> Address:
  165. return Address(uuid=self.uuid)
  166. @dataclass
  167. class BannedGroupMember(SerializableAttrs):
  168. uuid: UUID
  169. timestamp: int
  170. @dataclass
  171. class GroupChange(SerializableAttrs):
  172. revision: int
  173. editor: Address
  174. delete_members: Optional[List[Address]] = None
  175. delete_pending_members: Optional[List[Address]] = None
  176. delete_requesting_members: Optional[List[Address]] = None
  177. modified_profile_keys: Optional[List[GroupMember]] = None
  178. modify_member_roles: Optional[List[GroupMember]] = None
  179. new_access_control: Optional[GroupAccessControl] = None
  180. new_avatar: bool = False
  181. new_banned_members: Optional[List[GroupMember]] = None
  182. new_description: Optional[str] = None
  183. new_invite_link_password: bool = False
  184. new_is_announcement_group: Optional[AnnouncementsMode] = None
  185. new_members: Optional[List[GroupMember]] = None
  186. new_pending_members: Optional[List[GroupMember]] = None
  187. new_requesting_members: Optional[List[GroupMember]] = None
  188. new_timer: Optional[int] = None
  189. new_title: Optional[str] = None
  190. new_unbanned_members: Optional[List[GroupMember]] = None
  191. promote_pending_members: Optional[List[GroupMember]] = None
  192. promote_requesting_members: Optional[List[GroupMember]] = None
  193. @dataclass(kw_only=True)
  194. class GroupV2ID(SerializableAttrs):
  195. id: GroupID
  196. revision: Optional[int] = None
  197. removed: Optional[bool] = False
  198. group_change: Optional[GroupChange] = None
  199. @dataclass(kw_only=True)
  200. class GroupV2(GroupV2ID, SerializableAttrs):
  201. title: str = None
  202. description: Optional[str] = None
  203. avatar: Optional[str] = None
  204. timer: Optional[int] = None
  205. master_key: Optional[str] = field(default=None, json="masterKey")
  206. invite_link: Optional[str] = field(default=None, json="inviteLink")
  207. access_control: GroupAccessControl = field(
  208. factory=lambda: GroupAccessControl(), json="accessControl"
  209. )
  210. members: List[Address] = None
  211. member_detail: List[GroupMember] = field(factory=lambda: [], json="memberDetail")
  212. pending_members: List[Address] = field(factory=lambda: [], json="pendingMembers")
  213. pending_member_detail: List[GroupMember] = field(
  214. factory=lambda: [], json="pendingMemberDetail"
  215. )
  216. requesting_members: List[Address] = field(factory=lambda: [], json="requestingMembers")
  217. announcements: AnnouncementsMode = field(default=AnnouncementsMode.UNKNOWN)
  218. banned_members: Optional[List[BannedGroupMember]] = None
  219. @dataclass
  220. class Attachment(SerializableAttrs):
  221. width: int = 0
  222. height: int = 0
  223. caption: Optional[str] = None
  224. preview: Optional[str] = None
  225. blurhash: Optional[str] = None
  226. voice_note: bool = field(default=False, json="voiceNote")
  227. content_type: Optional[str] = field(default=None, json="contentType")
  228. custom_filename: Optional[str] = field(default=None, json="customFilename")
  229. # Only for incoming
  230. id: Optional[str] = None
  231. incoming_filename: Optional[str] = field(default=None, json="storedFilename")
  232. digest: Optional[str] = None
  233. size: Optional[int] = None
  234. # Only for outgoing
  235. outgoing_filename: Optional[str] = field(default=None, json="filename")
  236. @dataclass
  237. class Mention(SerializableAttrs):
  238. uuid: UUID
  239. length: int
  240. start: int = 0
  241. @dataclass
  242. class QuotedAttachment(SerializableAttrs):
  243. content_type: Optional[str] = field(default=None, json="contentType")
  244. filename: Optional[str] = field(default=None, json="fileName")
  245. @dataclass
  246. class Quote(SerializableAttrs):
  247. id: int
  248. author: Address
  249. text: Optional[str] = None
  250. attachments: Optional[List[QuotedAttachment]] = None
  251. mentions: Optional[List[Mention]] = None
  252. @dataclass(kw_only=True)
  253. class Reaction(SerializableAttrs):
  254. emoji: str
  255. remove: bool = False
  256. target_author: Address = field(json="targetAuthor")
  257. target_sent_timestamp: int = field(json="targetSentTimestamp")
  258. @dataclass
  259. class Sticker(SerializableAttrs):
  260. attachment: Attachment
  261. pack_id: str = field(json="packID")
  262. pack_key: str = field(json="packKey")
  263. sticker_id: int = field(json="stickerID")
  264. @dataclass
  265. class RemoteDelete(SerializableAttrs):
  266. target_sent_timestamp: int
  267. class SharedContactDetailType(SerializableEnum):
  268. HOME = "HOME"
  269. WORK = "WORK"
  270. MOBILE = "MOBILE"
  271. CUSTOM = "CUSTOM"
  272. @dataclass
  273. class SharedContactDetail(SerializableAttrs):
  274. type: SharedContactDetailType
  275. value: str
  276. label: Optional[str] = None
  277. @property
  278. def type_or_label(self) -> str:
  279. if self.type != SharedContactDetailType.CUSTOM:
  280. return self.type.value.title()
  281. return self.label
  282. @dataclass
  283. class SharedContactAvatar(SerializableAttrs):
  284. attachment: Attachment
  285. is_profile: bool
  286. @dataclass
  287. class SharedContactName(SerializableAttrs):
  288. display: Optional[str] = None
  289. given: Optional[str] = None
  290. middle: Optional[str] = None
  291. family: Optional[str] = None
  292. prefix: Optional[str] = None
  293. suffix: Optional[str] = None
  294. @property
  295. def parts(self) -> List[str]:
  296. return [self.prefix, self.given, self.middle, self.family, self.suffix]
  297. def __str__(self) -> str:
  298. if self.display:
  299. return self.display
  300. return " ".join(part for part in self.parts if part)
  301. @dataclass
  302. class SharedContactAddress(SerializableAttrs):
  303. type: SharedContactDetailType
  304. label: Optional[str] = None
  305. street: Optional[str] = None
  306. pobox: Optional[str] = None
  307. neighborhood: Optional[str] = None
  308. city: Optional[str] = None
  309. region: Optional[str] = None
  310. postcode: Optional[str] = None
  311. country: Optional[str] = None
  312. @dataclass
  313. class SharedContact(SerializableAttrs):
  314. name: SharedContactName
  315. organization: Optional[str] = None
  316. avatar: Optional[SharedContactAvatar] = None
  317. email: List[SharedContactDetail] = field(factory=lambda: [])
  318. phone: List[SharedContactDetail] = field(factory=lambda: [])
  319. address: Optional[SharedContactAddress] = None
  320. @dataclass
  321. class LinkPreview(SerializableAttrs):
  322. url: str
  323. title: str
  324. description: str
  325. attachment: Optional[Attachment] = None
  326. @dataclass
  327. class MessageData(SerializableAttrs):
  328. timestamp: int
  329. body: Optional[str] = None
  330. quote: Optional[Quote] = None
  331. reaction: Optional[Reaction] = None
  332. attachments: List[Attachment] = field(factory=lambda: [])
  333. sticker: Optional[Sticker] = None
  334. mentions: List[Mention] = field(factory=lambda: [])
  335. contacts: List[SharedContact] = field(factory=lambda: [])
  336. group: Optional[Group] = None
  337. group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
  338. end_session: bool = field(default=False, json="endSession")
  339. expires_in_seconds: int = field(default=0, json="expiresInSeconds")
  340. is_expiration_update: bool = field(default=False)
  341. profile_key_update: bool = field(default=False, json="profileKeyUpdate")
  342. view_once: bool = field(default=False, json="viewOnce")
  343. remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
  344. previews: List[LinkPreview] = field(factory=lambda: [])
  345. @property
  346. def is_message(self) -> bool:
  347. return bool(self.body or self.attachments or self.sticker or self.contacts)
  348. @dataclass
  349. class SentSyncMessage(SerializableAttrs):
  350. message: MessageData
  351. timestamp: int
  352. expiration_start_timestamp: Optional[int] = field(
  353. default=None, json="expirationStartTimestamp"
  354. )
  355. is_recipient_update: bool = field(default=False, json="isRecipientUpdate")
  356. unidentified_status: Dict[str, bool] = field(factory=lambda: {})
  357. destination: Optional[Address] = None
  358. class TypingAction(SerializableEnum):
  359. UNKNOWN = "UNKNOWN"
  360. STARTED = "STARTED"
  361. STOPPED = "STOPPED"
  362. @dataclass
  363. class TypingMessage(SerializableAttrs):
  364. action: TypingAction
  365. timestamp: int
  366. group_id: Optional[GroupID] = None
  367. @dataclass
  368. class OwnReadReceipt(SerializableAttrs):
  369. sender: Address
  370. timestamp: int
  371. class ReceiptType(SerializableEnum):
  372. UNKNOWN = "UNKNOWN"
  373. DELIVERY = "DELIVERY"
  374. READ = "READ"
  375. VIEWED = "VIEWED"
  376. @dataclass
  377. class ReceiptMessage(SerializableAttrs):
  378. type: ReceiptType
  379. timestamps: List[int]
  380. when: int
  381. @dataclass
  382. class ContactSyncMeta(SerializableAttrs):
  383. id: Optional[str] = None
  384. @dataclass
  385. class ConfigItem(SerializableAttrs):
  386. present: bool = False
  387. @dataclass
  388. class ClientConfiguration(SerializableAttrs):
  389. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  390. typing_indicators: Optional[ConfigItem] = field(
  391. factory=lambda: ConfigItem(), json="typingIndicators"
  392. )
  393. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  394. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  395. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  396. )
  397. class StickerPackOperation(ExtensibleEnum):
  398. INSTALL = "INSTALL"
  399. # there are very likely others
  400. @dataclass
  401. class StickerPackOperations(SerializableAttrs):
  402. type: StickerPackOperation
  403. pack_id: str = field(json="packID")
  404. pack_key: str = field(json="packKey")
  405. @dataclass
  406. class SyncMessage(SerializableAttrs):
  407. sent: Optional[SentSyncMessage] = None
  408. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  409. contacts: Optional[ContactSyncMeta] = None
  410. groups: Optional[ContactSyncMeta] = None
  411. configuration: Optional[ClientConfiguration] = None
  412. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  413. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  414. default=None, json="stickerPackOperations"
  415. )
  416. contacts_complete: bool = field(default=False, json="contactsComplete")
  417. class OfferMessageType(SerializableEnum):
  418. AUDIO_CALL = "audio_call"
  419. VIDEO_CALL = "video_call"
  420. @dataclass
  421. class OfferMessage(SerializableAttrs):
  422. id: int
  423. type: OfferMessageType
  424. opaque: Optional[str] = None
  425. sdp: Optional[str] = None
  426. @dataclass
  427. class AnswerMessage(SerializableAttrs):
  428. id: int
  429. opaque: Optional[str] = None
  430. sdp: Optional[str] = None
  431. @dataclass
  432. class ICEUpdateMessage(SerializableAttrs):
  433. id: int
  434. opaque: Optional[str] = None
  435. sdp: Optional[str] = None
  436. @dataclass
  437. class BusyMessage(SerializableAttrs):
  438. id: int
  439. class HangupMessageType(SerializableEnum):
  440. NORMAL = "normal"
  441. ACCEPTED = "accepted"
  442. DECLINED = "declined"
  443. BUSY = "busy"
  444. NEED_PERMISSION = "need_permission"
  445. @dataclass
  446. class HangupMessage(SerializableAttrs):
  447. id: int
  448. type: HangupMessageType
  449. device_id: int
  450. legacy: bool = False
  451. @dataclass
  452. class CallMessage(SerializableAttrs):
  453. offer_message: Optional[OfferMessage] = None
  454. hangup_message: Optional[HangupMessage] = None
  455. answer_message: Optional[AnswerMessage] = None
  456. busy_message: Optional[BusyMessage] = None
  457. ice_update_message: Optional[List[ICEUpdateMessage]] = None
  458. multi_ring: bool = False
  459. destination_device_id: Optional[int] = None
  460. class MessageType(SerializableEnum):
  461. CIPHERTEXT = "CIPHERTEXT"
  462. PLAINTEXT_CONTENT = "PLAINTEXT_CONTENT"
  463. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  464. RECEIPT = "RECEIPT"
  465. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  466. KEY_EXCHANGE = "KEY_EXCHANGE"
  467. UNKNOWN = "UNKNOWN"
  468. @dataclass(kw_only=True)
  469. class IncomingMessage(SerializableAttrs):
  470. account: str
  471. source: Address
  472. timestamp: int
  473. type: MessageType
  474. source_device: Optional[int] = None
  475. server_guid: str
  476. server_receiver_timestamp: int
  477. server_deliver_timestamp: int
  478. has_content: bool
  479. unidentified_sender: bool
  480. has_legacy_message: bool
  481. call_message: Optional[CallMessage] = field(default=None)
  482. data_message: Optional[MessageData] = field(default=None)
  483. sync_message: Optional[SyncMessage] = field(default=None)
  484. typing_message: Optional[TypingMessage] = None
  485. receipt_message: Optional[ReceiptMessage] = None
  486. @dataclass(kw_only=True)
  487. class ErrorMessageData(SerializableAttrs):
  488. sender: str
  489. timestamp: int
  490. message: str
  491. sender_device: int
  492. content_hint: int
  493. @dataclass(kw_only=True)
  494. class ErrorMessage(SerializableAttrs):
  495. type: str
  496. version: str
  497. data: ErrorMessageData
  498. error: bool
  499. account: str
  500. @dataclass(kw_only=True)
  501. class StorageChangeData(SerializableAttrs):
  502. version: int
  503. @dataclass(kw_only=True)
  504. class StorageChange(SerializableAttrs):
  505. type: str
  506. version: str
  507. data: StorageChangeData
  508. account: str
  509. class WebsocketConnectionState(SerializableEnum):
  510. # States from signald itself
  511. DISCONNECTED = "DISCONNECTED"
  512. CONNECTING = "CONNECTING"
  513. CONNECTED = "CONNECTED"
  514. RECONNECTING = "RECONNECTING"
  515. DISCONNECTING = "DISCONNECTING"
  516. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  517. FAILED = "FAILED"
  518. # Socket disconnect state
  519. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  520. class WebsocketType(SerializableEnum):
  521. IDENTIFIED = "IDENTIFIED"
  522. UNIDENTIFIED = "UNIDENTIFIED"
  523. @dataclass
  524. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  525. state: WebsocketConnectionState
  526. account: str
  527. socket: Optional[WebsocketType] = None
  528. exception: Optional[str] = None
  529. @dataclass
  530. class JoinGroupResponse(SerializableAttrs):
  531. group_id: str = field(json="groupID")
  532. pending_admin_approval: bool = field(json="pendingAdminApproval")
  533. member_count: Optional[int] = field(json="memberCount", default=None)
  534. revision: Optional[int] = None
  535. title: Optional[str] = None
  536. description: Optional[str] = None
  537. class ProofRequiredType(SerializableEnum):
  538. RECAPTCHA = "RECAPTCHA"
  539. PUSH_CHALLENGE = "PUSH_CHALLENGE"
  540. @dataclass
  541. class ProofRequiredError(SerializableAttrs):
  542. options: List[ProofRequiredType] = field(factory=lambda: [])
  543. message: Optional[str] = None
  544. retry_after: Optional[int] = None
  545. token: Optional[str] = None
  546. @dataclass
  547. class SendSuccessData(SerializableAttrs):
  548. devices: List[int] = field(factory=lambda: [])
  549. duration: Optional[int] = None
  550. needs_sync: bool = field(json="needsSync", default=False)
  551. unidentified: bool = field(json="unidentified", default=False)
  552. @dataclass
  553. class SendMessageResult(SerializableAttrs):
  554. address: Address
  555. success: Optional[SendSuccessData] = None
  556. proof_required_failure: Optional[ProofRequiredError] = None
  557. identity_failure: Optional[str] = field(json="identityFailure", default=None)
  558. network_failure: bool = field(json="networkFailure", default=False)
  559. unregistered_failure: bool = field(json="unregisteredFailure", default=False)
  560. @dataclass
  561. class SendMessageResponse(SerializableAttrs):
  562. results: List[SendMessageResult]
  563. timestamp: int