types.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  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 ContactSyncMeta(SerializableAttrs):
  373. id: Optional[str] = None
  374. @dataclass
  375. class ConfigItem(SerializableAttrs):
  376. present: bool = False
  377. @dataclass
  378. class ClientConfiguration(SerializableAttrs):
  379. read_receipts: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="readReceipts")
  380. typing_indicators: Optional[ConfigItem] = field(
  381. factory=lambda: ConfigItem(), json="typingIndicators"
  382. )
  383. link_previews: Optional[ConfigItem] = field(factory=lambda: ConfigItem(), json="linkPreviews")
  384. unidentified_delivery_indicators: Optional[ConfigItem] = field(
  385. factory=lambda: ConfigItem(), json="unidentifiedDeliveryIndicators"
  386. )
  387. class StickerPackOperation(ExtensibleEnum):
  388. INSTALL = "INSTALL"
  389. # there are very likely others
  390. @dataclass
  391. class StickerPackOperations(SerializableAttrs):
  392. type: StickerPackOperation
  393. pack_id: str = field(json="packID")
  394. pack_key: str = field(json="packKey")
  395. @dataclass
  396. class SyncMessage(SerializableAttrs):
  397. sent: Optional[SentSyncMessage] = None
  398. read_messages: Optional[List[OwnReadReceipt]] = field(default=None, json="readMessages")
  399. contacts: Optional[ContactSyncMeta] = None
  400. groups: Optional[ContactSyncMeta] = None
  401. configuration: Optional[ClientConfiguration] = None
  402. # blocked_list: Optional[???] = field(default=None, json="blockedList")
  403. sticker_pack_operations: Optional[List[StickerPackOperations]] = field(
  404. default=None, json="stickerPackOperations"
  405. )
  406. contacts_complete: bool = field(default=False, json="contactsComplete")
  407. class OfferMessageType(SerializableEnum):
  408. AUDIO_CALL = "audio_call"
  409. VIDEO_CALL = "video_call"
  410. @dataclass
  411. class OfferMessage(SerializableAttrs):
  412. id: int
  413. type: OfferMessageType
  414. opaque: Optional[str] = None
  415. sdp: Optional[str] = None
  416. @dataclass
  417. class AnswerMessage(SerializableAttrs):
  418. id: int
  419. opaque: Optional[str] = None
  420. sdp: Optional[str] = None
  421. @dataclass
  422. class ICEUpdateMessage(SerializableAttrs):
  423. id: int
  424. opaque: Optional[str] = None
  425. sdp: Optional[str] = None
  426. @dataclass
  427. class BusyMessage(SerializableAttrs):
  428. id: int
  429. class HangupMessageType(SerializableEnum):
  430. NORMAL = "normal"
  431. ACCEPTED = "accepted"
  432. DECLINED = "declined"
  433. BUSY = "busy"
  434. NEED_PERMISSION = "need_permission"
  435. @dataclass
  436. class HangupMessage(SerializableAttrs):
  437. id: int
  438. type: HangupMessageType
  439. device_id: int
  440. legacy: bool = False
  441. @dataclass
  442. class CallMessage(SerializableAttrs):
  443. offer_message: Optional[OfferMessage] = None
  444. hangup_message: Optional[HangupMessage] = None
  445. answer_message: Optional[AnswerMessage] = None
  446. busy_message: Optional[BusyMessage] = None
  447. ice_update_message: Optional[List[ICEUpdateMessage]] = None
  448. multi_ring: bool = False
  449. destination_device_id: Optional[int] = None
  450. class MessageType(SerializableEnum):
  451. CIPHERTEXT = "CIPHERTEXT"
  452. PLAINTEXT_CONTENT = "PLAINTEXT_CONTENT"
  453. UNIDENTIFIED_SENDER = "UNIDENTIFIED_SENDER"
  454. RECEIPT = "RECEIPT"
  455. PREKEY_BUNDLE = "PREKEY_BUNDLE"
  456. KEY_EXCHANGE = "KEY_EXCHANGE"
  457. UNKNOWN = "UNKNOWN"
  458. @dataclass(kw_only=True)
  459. class IncomingMessage(SerializableAttrs):
  460. account: str
  461. source: Address
  462. timestamp: int
  463. type: MessageType
  464. source_device: Optional[int] = None
  465. server_guid: str
  466. server_receiver_timestamp: int
  467. server_deliver_timestamp: int
  468. has_content: bool
  469. unidentified_sender: bool
  470. has_legacy_message: bool
  471. call_message: Optional[CallMessage] = field(default=None)
  472. data_message: Optional[MessageData] = field(default=None)
  473. sync_message: Optional[SyncMessage] = field(default=None)
  474. typing_message: Optional[TypingMessage] = None
  475. receipt_message: Optional[ReceiptMessage] = None
  476. @dataclass(kw_only=True)
  477. class ErrorMessageData(SerializableAttrs):
  478. sender: str
  479. timestamp: int
  480. message: str
  481. sender_device: int
  482. content_hint: int
  483. @dataclass(kw_only=True)
  484. class ErrorMessage(SerializableAttrs):
  485. type: str
  486. version: str
  487. data: ErrorMessageData
  488. error: bool
  489. account: str
  490. @dataclass(kw_only=True)
  491. class StorageChangeData(SerializableAttrs):
  492. version: int
  493. @dataclass(kw_only=True)
  494. class StorageChange(SerializableAttrs):
  495. type: str
  496. version: str
  497. data: StorageChangeData
  498. account: str
  499. class WebsocketConnectionState(SerializableEnum):
  500. # States from signald itself
  501. DISCONNECTED = "DISCONNECTED"
  502. CONNECTING = "CONNECTING"
  503. CONNECTED = "CONNECTED"
  504. RECONNECTING = "RECONNECTING"
  505. DISCONNECTING = "DISCONNECTING"
  506. AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
  507. FAILED = "FAILED"
  508. # Socket disconnect state
  509. SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"
  510. class WebsocketType(SerializableEnum):
  511. IDENTIFIED = "IDENTIFIED"
  512. UNIDENTIFIED = "UNIDENTIFIED"
  513. @dataclass
  514. class WebsocketConnectionStateChangeEvent(SerializableAttrs):
  515. state: WebsocketConnectionState
  516. account: str
  517. socket: Optional[WebsocketType] = None
  518. exception: Optional[str] = None
  519. @dataclass
  520. class JoinGroupResponse(SerializableAttrs):
  521. group_id: str = field(json="groupID")
  522. pending_admin_approval: bool = field(json="pendingAdminApproval")
  523. member_count: Optional[int] = field(json="memberCount", default=None)
  524. revision: Optional[int] = None
  525. title: Optional[str] = None
  526. description: Optional[str] = None
  527. class ProofRequiredType(SerializableEnum):
  528. RECAPTCHA = "RECAPTCHA"
  529. PUSH_CHALLENGE = "PUSH_CHALLENGE"
  530. @dataclass
  531. class ProofRequiredError(SerializableAttrs):
  532. options: List[ProofRequiredType] = field(factory=lambda: [])
  533. message: Optional[str] = None
  534. retry_after: Optional[int] = None
  535. token: Optional[str] = None
  536. @dataclass
  537. class SendSuccessData(SerializableAttrs):
  538. devices: List[int] = field(factory=lambda: [])
  539. duration: Optional[int] = None
  540. needs_sync: bool = field(json="needsSync", default=False)
  541. unidentified: bool = field(json="unidentified", default=False)
  542. @dataclass
  543. class SendMessageResult(SerializableAttrs):
  544. address: Address
  545. success: Optional[SendSuccessData] = None
  546. proof_required_failure: Optional[ProofRequiredError] = None
  547. identity_failure: Optional[str] = field(json="identityFailure", default=None)
  548. network_failure: bool = field(json="networkFailure", default=False)
  549. unregistered_failure: bool = field(json="unregisteredFailure", default=False)
  550. @dataclass
  551. class SendMessageResponse(SerializableAttrs):
  552. results: List[SendMessageResult]
  553. timestamp: int