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