use std::collections::HashMap; use serde::{Deserialize, Serialize}; use chrono::{DateTime, FixedOffset}; use chrono::prelude::*; use std::convert::From; use crate::structs::{ACL, ConnectionState, CwtchEvent, MessageWrapper, Profile, Settings}; #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Profile ID used to refer to profiles in Cwtch pub struct ProfileIdentity(String); impl From for ProfileIdentity { fn from(x: String) -> Self { ProfileIdentity(x) } } impl From for String { fn from(x: ProfileIdentity) -> Self { x.into() } } impl From<&str> for ProfileIdentity { fn from(x: &str) -> Self { ProfileIdentity(x.to_string()) } } impl From<&ProfileIdentity> for String { fn from(x: &ProfileIdentity) -> Self { x.0.clone() } } impl ProfileIdentity { /// Get &str of ProfileIdentity String pub fn as_str(&self) -> &str { self.0.as_str() } } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Contact ID used to refer to contacts in Cwtch pub struct ContactIdentity(String); impl From for ContactIdentity { fn from(x: String) -> Self { ContactIdentity(x) } } impl ContactIdentity { /// Get &str of ContactIdentity String pub fn as_str(&self) -> &str { self.0.as_str() } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Conversation ID user to refer to a conversation with a Contact or Group in Cwtch pub struct ConversationID(pub i32) ; impl From for ConversationID { fn from(x: i32) -> Self { ConversationID(x) } } impl From for i32 { fn from(x: ConversationID) -> Self { x.0 } } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Group ID used to refer to a Group in Cwtch pub struct GroupID(String) ; impl From for GroupID { fn from(x: String) -> Self { GroupID(x) } } impl GroupID { /// Get &str of GroupID String pub fn as_str(&self) -> &str { self.0.as_str() } } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Server ID user to refer to a server in Cwtch pub struct ServerIdentity(String); impl From for ServerIdentity { fn from(x: String) -> Self { ServerIdentity(x) } } impl From<&str> for ServerIdentity { fn from(x: &str) -> Self { ServerIdentity(x.to_string()) } } impl From for String { fn from(x: ServerIdentity) -> Self { x.into() } } impl ServerIdentity { /// Get &str of ServerIdentity String pub fn as_str(&self) -> &str { self.0.as_str() } } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// FileKey ID user to refer to a file share in Cwtch pub struct FileKey(String); impl From for FileKey { fn from(x: String) -> Self { FileKey(x) } } impl From for String { fn from(x: FileKey) -> Self { x.into() } } impl From<&FileKey> for String { fn from(x: &FileKey) -> Self { x.into() } } impl FileKey { /// Get &str of FileKey String pub fn as_str(&self) -> &str { self.0.as_str() } } #[derive(Debug, Clone)] /// Enum for type of notification a UI/client should emit for a new message pub enum MessageNotification { /// means no notification None, /// means emit a notification that a message event happened only SimpleEvent, /// means emit a notification event with Conversation handle included ContactInfo, } impl From for MessageNotification { fn from(str: String) -> MessageNotification { match str.as_str() { "None" => MessageNotification::None, "SimpleEvent" => MessageNotification::SimpleEvent, "ContactInfo" => MessageNotification::ContactInfo, _ => MessageNotification::None, } } } #[derive(Debug, Clone)] /// Enum for results from NetworkCheck plugin on profiles pub enum NetworkCheckStatus { /// There was an error, this profile cannot connect to itself Error, /// Success connecting to self, profile is "online" Success } impl From for NetworkCheckStatus { fn from(str: String) -> NetworkCheckStatus { match str.as_str() { "Error" => NetworkCheckStatus::Error, "Success" => NetworkCheckStatus::Success, _ => NetworkCheckStatus::Error, } } } #[derive(Debug, Clone)] /// Enum denoting server storage mode pub enum ServerStorageType { /// indicates some app supplied default password is being used on this server for storage encryption DefaultPassword, /// indicates a user supplied password is being used on this server for storage encryption Password, /// indicates no password and no encryption is being used on server storage NoPassword } impl From for ServerStorageType { fn from(str: String) -> ServerStorageType { match str.as_str() { "storage-default-password" => ServerStorageType::DefaultPassword, "storage-password" => ServerStorageType::Password, "storage-no-password" => ServerStorageType::NoPassword, _ => ServerStorageType::DefaultPassword, } } } #[derive(Debug, Clone)] /// Enum used by servers to declare their intent to be running or stopped pub enum ServerIntent { /// a server intends to be running Running, /// a server intends to be stopped Stopped } impl From for ServerIntent { fn from(str: String) -> ServerIntent { match str.as_str() { "running" => ServerIntent::Running, "stopped" => ServerIntent::Stopped, _ => ServerIntent::Stopped, } } } #[derive(Debug, Clone)] /// Enum for events from Cwtch appbus pub enum Event { // App events /// Emited by libcwtch once Cwtch.Start() has been completed successfully, you can start making API calls, lcg is initialized CwtchStarted, /// A new peer has been loaded, details of peer. Identity should be stored so further API calls can be made NewPeer { /// identity field profile_id: ProfileIdentity, /// optional client specified tag tag: String, /// is this a newly created profile event, or a load created: bool, /// user supplied name of the profile name: String, /// default picture path default_picture: String, /// user supplied picture path picture: String, /// is the profile online online: String, /// The deserialized profile with contacts and server info profile_data: Result, }, /// Cwtch had an error at the app level (not profile level), usually in response to an API call AppError { /// details of the app error that occured error: String, /// possible data about the error data: String }, /// Global settings being emited from lcg, usually in response to them being sent to be saved by client UpdateGlobalSettings { /// map of setting names to values (https://git.openprivacy.ca/cwtch.im/libcwtch-go/src/branch/trunk/utils/settings.go) settings: Settings, }, /// A profile has an error, usually emited in response to an API call PeerError { /// details of the peer error that occured error: String }, /// A profile was successfully deleted, it is no longer usable PeerDeleted { /// identity of deleted peer profile_id: ProfileIdentity }, /// Cwtch is shutting down, stop making API calls Shutdown, /// indicates status of the ACN (tor), usually during boot. ACNStatus { /// the percent of ACN boot (-1 to 100) progress: i8, /// an associated status message from the ACN status: String, }, /// Version of the ACN (currently tor) ACNVersion { /// version string from ACN app version: String, }, /// Notice from libCwtch that before completing load of a profile a storage migration is occuring so Start will take longer StartingStorageMiragtion, /// Notice from libCwtch that the storage migration is complete, profile being loaded resuming DoneStorageMigration, // app server events /// A new server has been loaded NewServer { /// identity of server server: ServerIdentity, /// sharable / importable server bundle server_bundle: String, /// user supplied description description: String, /// storage mode the server is using storage_type: ServerStorageType, /// does/should the server auto start on cwtch start autostart: bool, /// is the server running running: bool, }, /// Response to request for server intent change, indicating the server is indending the new state ServerIntentUpdate { /// identity of server server: ServerIdentity, /// intent of the server to be running or not intent: ServerIntent, }, /// Notice a server was deleted (in response to an API call) and is no longer usable ServerDeleted { /// identity of server server: ServerIdentity, /// was deletion a success success: bool, /// optional error string in case of failure to delete error: Option, }, /// Stats info for a server, periodically emited ServerStatsUpdate { /// identity of server server: ServerIdentity, /// count of total messages on the server total_messages: i32, /// count of total current connections to the server connections: i32, }, // profile events /// A new message was received NewMessageFromPeer { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// contact id contact_id: ContactIdentity, /// name of contact nick: String, /// time message was received timestamp_received: DateTime, /// the message message: MessageWrapper, /// notification instructions (based on settings) notification: MessageNotification, /// path to picture for the contact picture: String, }, /// A new contact has been created (imported, added, or contacted by) ContactCreated { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// contact id contact_id: ContactIdentity, /// name of group nick: String, /// connection status to group server status: ConnectionState, /// number of unread messages in group unread: i32, /// path to picture for group picture: String, /// path to default picture for group default_picture: String, /// total number of messages in group num_messages: i32, /// has the user accepted the group accepted: bool, /// ACL for the group access_control_list: ACL, /// is the group blocked blocked: bool, /// is the group syncing loading: bool, /// time of last message from the group last_msg_time: DateTime, }, /// A peer has changed state PeerStateChange { /// identity field profile_id: ProfileIdentity, /// contact id contact_id: ContactIdentity, /// connection status to contact connection_state: ConnectionState, }, /// Notice from the network check plugin, a profile self check test by attempting to connecting to itself NetworkStatus { /// profile the check was performed on profile_id: ProfileIdentity, // it's technically a profile self check /// error if there was one (can be empty) error: String, /// status of profile self connection check status: NetworkCheckStatus, }, /// Information from the ACN about a peer ACNInfo { /// identity field profile_id: ProfileIdentity, /// contact id contact_id: ContactIdentity, /// key of info key: String, /// data of info data: String, }, /// a profile attribute has been updated with a new value UpdatedProfileAttribute { /// identity field profile_id: ProfileIdentity, /// attribute key key: String, /// attribute new value value: String, }, /// emited to confirm ack of a message succeeded IndexedAcknowledgement { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: i32, /// index of message acked index: i32, }, /// emited to signal failure to ack a message IndexedFailure { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// index of failure of message to ack index: i32, /// contact id contact_id: ContactIdentity, /// error string error: String, }, /// a peer has acked a message PeerAcknowledgement { /// identity field profile_id: ProfileIdentity, /// message id this is an ack to event_id: String, /// contact id contact_id: ContactIdentity, /// conversation id conversation_id: i32, }, /// New message received on a group NewMessageFromGroup { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// time of message timestamp_sent: DateTime, /// contact id contact_id: ContactIdentity, /// message index index: i32, /// the message message: MessageWrapper, /// hash of the message content_hash: String, /// path to picture for sender picture: String, /// notification policy (based on settings) notification: MessageNotification, }, /// notice a group has been created GroupCreated { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// group id group_id: GroupID, /// server the group is on group_server: String, /// name of group group_name: String, /// path to picture for group picture: String, /// Access Control List for group access_control_list: ACL, }, /// notice a new group exists NewGroup { /// identity field profile_id: ProfileIdentity, /// conversation id conversation_id: ConversationID, /// server the group is on group_server: String, /// invite string group_invite: String, /// group name group_name: String, }, /// a server connection state has changed ServerStateChange { /// identity field profile_id: ProfileIdentity, /// server the group is on group_server: String, /// state of connection to server state: ConnectionState, }, /// A getval call to a peer has returned a response NewRetValMessageFromPeer { /// identity field profile_id: ProfileIdentity, /// conversation id contact_id: ContactIdentity, /// scope of the val scope: String, /// path of the val (zone.key) path: String, /// does the queried for value exist exists: bool, /// value data: String, /// optional filepath if there was a downloaded component file_path: Option, }, /// result of a call to share a file, the filekey and manifest to operate on it ShareManifest { /// identity field profile_id: ProfileIdentity, /// filekey filekey: FileKey, /// serialized manifest of the share serialized_manifest: String, }, /// Information on a peer fileshare has been received ManifestSizeReceived { /// identity field profile_id: ProfileIdentity, /// filekey filekey: FileKey, /// size of manifest received for a share manifest_size: i32, /// contact id contact_id: ContactIdentity, }, /// An error has occured while trying to parse a peer sharefile ManifestError { /// identity field profile_id: ProfileIdentity, /// contact id contact_id: ContactIdentity, /// filekey filekey: FileKey, }, /// A peer message about a shared file has been received ManifestReceived { /// identity field profile_id: ProfileIdentity, /// contact id contact_id: ContactIdentity, /// filekey filekey: FileKey, /// serialized manifest serialized_manifest: String, }, /// a received manfiest has been saved ManifestSaved { /// identity field profile_id: ProfileIdentity, /// contact id contact_id: ContactIdentity, /// filekey filekey: FileKey, /// serialized manifest serialized_manifest: String, /// temporary storage path for share download temp_file: String, /// contact suggested share file name name_suggestion: String, }, /// periodically emited status updates about an active download of a shared file FileDownloadProgressUpdate { /// identity field profile_id: ProfileIdentity, /// filekey filekey: FileKey, /// progress of download of share in chunks progress: i32, /// size of share in chunks filesize_in_chunks: i32, /// contact suggested name for file name_suggestion: String, }, // FileDownloaded, ?? /// Indicates an event was sent from libCwtch that we don't handle/didn't anticipate /// still passing it on giving the user a chance to react, but should be reported so we can handle it properly ErrUnhandled { /// name of unhandled event name: String, /// map of key:val attributes of unhandled event data: HashMap, }, } impl From<&CwtchEvent> for Event { fn from(cwtch_event: &CwtchEvent) -> Self { println!("EVENT: {:?}", cwtch_event); match cwtch_event.event_type.as_str() { "CwtchStarted" => Event::CwtchStarted, "NewPeer" => Event::NewPeer { profile_id: cwtch_event.data["Identity"].clone().into(), tag: cwtch_event.data["tag"].clone(), created: cwtch_event.data["Created"] == "true", name: cwtch_event.data["name"].clone(), default_picture: cwtch_event.data["defaultPicture"].clone(), picture: cwtch_event.data["picture"].clone(), online: cwtch_event.data["Online"].clone(), profile_data: Profile::new( cwtch_event.data["Identity"].clone().into(), &cwtch_event.data["name"], &cwtch_event.data["picture"], &cwtch_event.data["ContactsJson"], &cwtch_event.data["ServerList"], ) }, "NewMessageFromPeer" => Event::NewMessageFromPeer { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-2).into(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), nick: cwtch_event.data["Nick"].clone(), timestamp_received: DateTime::parse_from_rfc3339(cwtch_event.data["TimestampReceived"].as_str()).unwrap_or( DateTime::from(Utc::now())), message: MessageWrapper::from_json(&cwtch_event.data["Data"]), notification: MessageNotification::from(cwtch_event.data["notification"].clone()), picture: cwtch_event.data["picture"].clone(), }, "PeerError" => Event::PeerError { error: cwtch_event.data["Error"].clone() }, "AppError" => Event::AppError { error: match cwtch_event.data.contains_key("Error") { true => cwtch_event.data["Error"].clone(), false => "".to_string() }, data: match cwtch_event.data.contains_key("Data") { true => cwtch_event.data["Data"].clone(), false => "".to_string() } }, "ContactCreated" => Event::ContactCreated { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), conversation_id: cwtch_event.data["ConversationID"].clone().parse().unwrap_or(-2).into(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), unread: cwtch_event.data["unread"].clone().parse().unwrap_or(0), picture: cwtch_event.data["picture"].clone(), default_picture: cwtch_event.data["defaultPicture"].clone(), num_messages: cwtch_event.data["numMessages"].clone().parse().unwrap_or(0), nick: cwtch_event.data["nick"].clone(), accepted: cwtch_event.data["accepted"].clone().parse().unwrap_or(false), status: ConnectionState::from(cwtch_event.data["status"].as_str()), access_control_list: serde_json::from_str(cwtch_event.data["accessControlList"].as_str()).unwrap_or(ACL::new()), blocked: cwtch_event.data["blocked"].clone().parse().unwrap_or(false), loading: cwtch_event.data["loading"].clone().parse().unwrap_or(false), last_msg_time: DateTime::parse_from_rfc3339(cwtch_event.data["lastMsgTime"].as_str()).unwrap_or(DateTime::from(Utc::now())), }, "PeerStateChange" => Event::PeerStateChange { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), connection_state: ConnectionState::from(cwtch_event.data["ConnectionState"].as_str()), }, "UpdateGlobalSettings" => Event::UpdateGlobalSettings { settings: serde_json::from_str(cwtch_event.data["Data"].as_str()).expect("could not parse settings from libCwtch-go"), }, "PeerDeleted" => Event::PeerDeleted { profile_id: cwtch_event.data["Identity"].clone().into() }, "ACNStatus" => Event::ACNStatus { progress: cwtch_event.data["Progress"].parse().unwrap_or(0), status: cwtch_event.data["Status"].clone(), }, "ACNVersion" => Event::ACNVersion { version: cwtch_event.data["Data"].clone()}, "NetworkError" => Event::NetworkStatus { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), error: cwtch_event.data["Error"].clone(), status: NetworkCheckStatus::from(cwtch_event.data["Status"].clone()) }, "ACNInfo" => Event::ACNInfo { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["Handle"].clone().into(), key: cwtch_event.data["Key"].clone(), data: cwtch_event.data["Data"].clone(), }, "UpdatedProfileAttribute" => Event::UpdatedProfileAttribute { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), key: cwtch_event.data["Key"].clone(), value: cwtch_event.data["Data"].clone(), }, "IndexedAcknowledgement" => Event::IndexedAcknowledgement { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-1).into(), index: cwtch_event.data["Index"].parse().unwrap_or(-1), }, "ShareManifest" => Event::ShareManifest { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), serialized_manifest: cwtch_event.data["SerializedManifest"].clone(), }, "NewServer" => Event::NewServer { server: cwtch_event.data["Onion"].clone().into(), server_bundle: cwtch_event.data["ServerBundle"].clone(), description: cwtch_event.data["Description"].clone(), storage_type: ServerStorageType::from(cwtch_event.data["StorageType"].clone()), autostart: cwtch_event.data["Autostart"].parse().unwrap_or(false), running: cwtch_event.data["Running"].parse().unwrap_or(false), }, "ServerIntentUpdate" => Event::ServerIntentUpdate { server: cwtch_event.data["Identity"].clone().into(), intent: ServerIntent::from(cwtch_event.data["Intent"].clone()) }, "ServerDeleted" => Event::ServerDeleted { server: cwtch_event.data["Identity"].clone().into(), success: cwtch_event.data["Status"].clone() == "success", error: match cwtch_event.data.get("Error") { Some(e) => Some(e.clone()), None => None, } }, "ServerStatsUpdate" => Event::ServerStatsUpdate { server: cwtch_event.data["Identity"].clone().into(), total_messages: cwtch_event.data["TotalMessages"].parse().unwrap_or(0), connections: cwtch_event.data["Connections"].parse().unwrap_or(0), }, "ManifestSizeReceived" => Event::ManifestSizeReceived { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), manifest_size: cwtch_event.data["ManifestSize"].parse().unwrap_or(0), contact_id: cwtch_event.data["Handle"].clone().into(), }, "ManifestError" => Event::ManifestError { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["Handle"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), }, "ManifestReceived" => Event::ManifestReceived { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["Handle"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), serialized_manifest: cwtch_event.data["SerializedManifest"].clone(), }, "ManifestSaved" => Event::ManifestSaved { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["Handle"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), serialized_manifest: cwtch_event.data["SerializedManifest"].clone(), temp_file: cwtch_event.data["TempFile"].clone(), name_suggestion: cwtch_event.data["NameSuggestion"].clone(), }, "FileDownloadProgressUpdate" => Event::FileDownloadProgressUpdate { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), filekey: cwtch_event.data["FileKey"].clone().into(), progress: cwtch_event.data["Progress"].parse().unwrap_or(0), filesize_in_chunks: cwtch_event.data["FileSizeInChunks"].parse().unwrap_or(0), name_suggestion: cwtch_event.data["NameSuggestion"].clone(), }, "PeerAcknowledgement" => Event::PeerAcknowledgement { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), event_id: cwtch_event.data["EventID"].clone(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), conversation_id: cwtch_event.data.get("ConversationID").unwrap_or(&"0".to_string()).parse().unwrap_or(0) }, "NewMessageFromGroup" => Event::NewMessageFromGroup { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), timestamp_sent: DateTime::parse_from_rfc3339(cwtch_event.data["TimestampSent"].as_str()).unwrap_or( DateTime::from(Utc::now())), index: cwtch_event.data["RemotePeer"].parse().unwrap_or(-1), content_hash: cwtch_event.data["ContentHash"].clone(), conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-2).into(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), message: MessageWrapper::from_json(&cwtch_event.data["Data"]), notification: MessageNotification::from(cwtch_event.data["notification"].clone()), picture: cwtch_event.data["picture"].clone(), }, "GroupCreated" => Event::GroupCreated { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-2).into(), group_id: cwtch_event.data["GroupID"].clone().into(), group_server: cwtch_event.data["GroupServer"].clone(), group_name: cwtch_event.data["GroupName"].clone(), picture: cwtch_event.data["picture"].clone(), access_control_list: serde_json::from_str(cwtch_event.data["accessControlList"].as_str()).unwrap_or(ACL::new()), }, "NewGroup" => Event::NewGroup { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-2).into(), group_server: cwtch_event.data["GroupServer"].clone(), group_name: cwtch_event.data["GroupName"].clone(), group_invite: cwtch_event.data["GroupInvite"].clone(), }, "ServerStateChange" => Event::ServerStateChange { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), group_server: cwtch_event.data["GroupServer"].clone(), state: ConnectionState::from(cwtch_event.data["ConnectionState"].as_str()), }, "NewRetValMessageFromPeer" => Event::NewRetValMessageFromPeer { profile_id: cwtch_event.data["ProfileOnion"].clone().into(), contact_id: cwtch_event.data["RemotePeer"].clone().into(), scope: cwtch_event.data["Scope"].clone(), path: cwtch_event.data["Path"].clone(), exists: cwtch_event.data["Exists"].parse().unwrap_or(false), data: cwtch_event.data["Data"].clone(), file_path: match cwtch_event.data.get("FilePath") { Some(fp) => Some(fp.to_string()), None => None, }, }, _ => Event::ErrUnhandled { name: cwtch_event.event_type.to_string(), data: cwtch_event.data.clone(), }, } } }