use crate::structs::ConnectionState::Disconnected; use crate::{ConversationID, CwtchLib, ProfileIdentity}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DefaultOnError}; use serde_repr::*; use std::collections::HashMap; use crate::event::{ContactIdentity, FileKey}; #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] /// Defines the states a Cwtch connection can be in pub enum ConnectionState { /// The Cwtch connection is not conected at all Disconnected, /// Cwtch is attempting to connect to the address, it may or may not be online Connecting, /// Cwtch has made a basic connection to the address, but not done authorization. The address is online but unverified as the desired target Connected, /// Cwtch has authenticated the desired connection Authenticated, /// In the case of a server connection, Cwtch has finished syncing messages from the server and is caught up Synced, /// The connection attempt failed Failed, /// The connection has been killed Killed, } impl Default for ConnectionState { fn default() -> ConnectionState { Disconnected } } impl From<&str> for ConnectionState { /// Creates a ConnectionState from a string sent from libcwtch-go fn from(name: &str) -> Self { match name { "Disconnected" => ConnectionState::Disconnected, "Connecting" => ConnectionState::Connecting, "Connected" => ConnectionState::Connected, "Authenticated" => ConnectionState::Authenticated, "Synced" => ConnectionState::Synced, "Failed" => ConnectionState::Failed, "Killed" => ConnectionState::Killed, _ => ConnectionState::Disconnected, } } } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "lowercase")] /// Defines the various authorization modes a contact can be in pub enum ContactAuthorization { /// This is an unknown (new?) contact. The user has not approved them Unknown, /// The contact is approved by the user (manual action) Approved, /// The contact is blocked by the user, should be ignored Blocked, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] #[allow(non_snake_case)] /// Struct to deserialize the results of Get*Attribute requests into pub struct Attribute { /// Was the attribute value found in storage pub exists: bool, /// The value of the requested attribute pub value: String, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] #[allow(non_snake_case)] /// Struct to serialize/deserialize events coming off the Cwtch appbus pub struct CwtchEvent { /// the type of event, as defined in https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/event/common.go pub event_type: String, /// event ID that can be used to respond to some events and keep them differentiated (event_ID because golang naming conventions in libCwtch-go) pub event_ID: String, /// a map of keys and values of arguments for the event as defined in https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/event/common.go pub data: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "PascalCase")] #[allow(non_snake_case)] /// AccessControl is a type determining client assigned authorization to a peer pub struct AccessControl { /// Any attempts from this handle to connect are blocked pub blocked: bool, /// Allows a handle to access the conversation pub read: bool, /// Allows a handle to append new messages to the conversation pub append: bool, /// Profile should automatically try to connect with peer pub auto_connect: bool, /// Profile should automatically exchange attributes like Name, Profile Image, etc. pub exchange_attributes: bool, /// Allows a handle to share files to a conversation pub share_files: bool, /// Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact pub render_images: bool } /// represents an access control list for a conversation. Mapping handles to conversation functions pub type ACL = HashMap; #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] /// Struct to serialize/deserialize conversations coming from libcwtch-go pub struct Conversation { /// onion address / id of the conversation #[serde(alias = "onion")] pub contact_id: ContactIdentity, /// unique identifier of the contact/conversation to be used in API access pub identifier: ConversationID, // TODO TEST does this work here for serde deserializtion /// display name of the conversation, as determined in libcwtch-go from name specified by contact pub name: String, #[serde_as(deserialize_as = "DefaultOnError")] // cwtch loads profile/conversation from storage and leaves status blank, it's filled in "soon" by events... /// contact connection status pub status: ConnectionState, /// has the conversation been manually accpted pub accepted: bool, ///represents an access control list for a conversation. Mapping handles to conversation functions pub access_control_list: ACL, /// has the conversation been manually blocked pub blocked: bool, /// is this conversation a group? if so "onion" will be a group ID /// FIXME: deprecate pub is_group: bool, //attr: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone)] /// Struct to serialize/deserialize servers coming from libcwtch-go pub struct Server { /// onion address of the server pub onion: String, /// server connection status pub status: ConnectionState, } #[derive(Debug, Clone)] /// Struct to serialize/deserialize profiles coming from libcwtch-go pub struct Profile { /// onion address / ID of the profile pub profile_id: ProfileIdentity, /// nick name of the onion as supplied by libcwtch-go based on "name" attribute pub nick: String, /// path to a profile image, controled by "picture" attribute pub image_path: String, /// all profile attributes pub attr: HashMap, /// map of conversation [ onion => conversation ] pub conversations: HashMap, /// map of servers [ onion => server ] pub servers: HashMap, } #[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Clone)] #[repr(i32)] /// Enum matching message types and their overlays as defined in Cwtch /// - https://git.openprivacy.ca/cwtch.im/cwtch/src/commit/907a7ca638fbcbaac5d652608d455d4217597fa9/model/overlay.go /// - https://git.openprivacy.ca/cwtch.im/cwtch-ui/src/commit/3a752b73972cbfc53b6da26116fe018ee2ac2343/lib/models/message.dart pub enum MessageType { /// A standard Cwtch message of text TextMessage = 1, /// This message is a text message but it also contains a reference to the message it is quoting QuotedMessage = 10, /// A shared contact message SuggestContact = 100, /// A group invite message InviteGroup = 101, /// a share file message FileShare = 200, /// This message is of an unsupported type so malformed MalformedMessage, } impl From for MessageType { fn from(x: i32) -> Self { match x { 1 => MessageType::TextMessage, 10 => MessageType::QuotedMessage, 100 => MessageType::SuggestContact, 101 => MessageType::InviteGroup, 200 => MessageType::FileShare, _ => MessageType::MalformedMessage, } } } #[derive(Debug, Serialize, Deserialize, Clone)] /// Struct to serialize/deserialize messages sent over Cwtch between profiles / conversation /// Overlay => data /// 1 TextMessage: d = plain text message /// 10 QuotedMessage: d = json of QuotedMessageStructure, body = plain text of message pub struct MessageWrapper { /// overlay id that the message is targeting as defined in cwtch/model/overlay.go /// [ OverlayChat = 1, OverlayInviteContact = 100, OverlayInviteGroup = 101, OverlayFileSharing = 200 ] pub o: MessageType, /// data of the message, for OverlayChat, a pub d: String, } impl MessageWrapper { /// parse json into a Message pub fn from_json(json: &str) -> Self { match serde_json::from_str(json) { Ok(m) => m, Err(e) => MessageWrapper {o: MessageType::MalformedMessage, d: e.to_string()} } } } #[derive(Debug, Serialize, Deserialize, Clone)] /// defined: cwtch-ui/lib/model/messages/quotedmessage.dart ~ln 15 #[allow(non_snake_case)] pub struct QuotedMessageStructure { /// hash message id of message being quoted pub quotedHash: String, /// plain text of the reply message pub body: String, } #[derive(Debug, Serialize, Deserialize, Clone)] /// LocallyIndexedMessage is a type wrapper around a Message and a TimeLine Index that is local to this /// instance of the timeline. /// defined in: cwtch/model/message.go ~ln 31 #[allow(non_snake_case)] pub struct LocallyIndexedMessage { /// The message pub Message: LocalMessage, /// the local index in this timeline instance pub LocalIndex: i32, } #[derive(Debug, Serialize, Deserialize, Clone)] /// Message is a local representation of a given message sent over a group chat channel. /// defined in: cwtch/model/message.go ~ln 38 #[allow(non_snake_case)] pub struct LocalMessage { /// Timestamp of the message from sender pub Timestamp: String, //time.Time /// Timestamp message was received at pub Received: String, //time.Time /// ContactId of the sender pub PeerID: String, /// Message contents pub Message: MessageWrapper, /// author's signature verifying the message pub Signature: String, /// the signature of hte previous message in the sender's timeline pub PreviousMessageSig: String, /// was message confirmed by server pub ReceivedByServer: bool, /// was the message ack'ed by the recipient peer pub Acknowledged: bool, // peer to peer /// message if there was an error, empty if fine pub Error: String, /// Application specific flags, useful for storing small amounts of metadata pub Flags: u64 } // Only for internal use? /* #[derive(Debug, Serialize, Deserialize, Clone)] /// FilesharingOverlayMessage presents the canonical format of the File Sharing functionality Overlay Message /// This is the format that the UI will parse to display the message /// defined: cwtch/functionality/filesharing/filesharing_functionality.go ~ln 156 pub struct FilesharingOverlayMessage { #[serde(alias = "f")] pub name: String, #[serde(alias = "h")] pub hash: String, #[serde(alias = "n")] pub nonce: String, #[serde(alias = "s")] pub size: u64, } */ #[derive(Debug, Serialize, Deserialize, Clone)] /// SharedFile struct from cwtch defined in cwtch/functionality/filesharing/filesharing_functionality.go ~ln 464 #[allow(non_snake_case)] pub struct SharedFile { /// The roothash.nonce identifier derived for this file share pub FileKey: FileKey, /// Path is the OS specific location of the file pub Path: String, /// DateShared is the original datetime the file was shared /// go time.Time, => DateTime /// DateTime::parse_from_rfc3339(cwtch_event.data["TimestampSent"].as_str()).unwrap_or( DateTime::from(Utc::now())), pub DateShared: String, /// Active is true if the file is currently being shared, false otherwise pub Active: bool, /// Expired is true if the file is not eligible to be shared (because e.g. it has been too long since the file was originally shared, /// or the file no longer exists). pub Expired: bool } #[derive(Debug, Serialize, Deserialize, Clone)] /// Settings as defined in libcwtch-go/utils/settings.go and should be populated by handeling the UpdateGlobalSettings event emited during cwtch.start() #[allow(non_snake_case)] pub struct Settings { /// locale for the UI to use pub Locale: String, /// theme of the UI pub Theme: String, /// Theme mode of the ui, light or dark pub ThemeMode: String, /// previous pid of the run, managed by libcwtch-go pub PreviousPid: i64, /// controls if subsequent experiments are enabled at all pub ExperimentsEnabled: bool, /// map of experiment names and their enabled status pub Experiments: HashMap, /// Should the app block unknown conversations pub BlockUnknownConnections: bool, // Notification policy of a UI app //pub NotificationPolicy: String, //Todo: NotificationPolicy struct // Notification content to show for a UI app //pub NotificationContent: String, /// Should the UI hide conversation IDs pub StreamerMode: bool, /// Unused? pub StateRootPane: i32, /// is this the first run pub FirstTime: bool, /// UI column mode pub UIColumnModePortrait: String, /// UI column mode pub UIColumnModeLandscape: String, /// Path to download files to pub DownloadPath: String, // Turn on advanced tor config in the UI //pub AllowAdvancedTorConfig: bool, // Custom torrc value //pub CustomTorrc: String, // Use the value of CustomTorrc with tor //pub UseCustomTorrc: bool, // Unused? delete //pub UseExternalTor: bool, // Tor socks port, if not default //pub CustomSocksPort: i32, // Tor control port if not default //pub CustomControlPort: i32, // Use tor cache for faster start //pub UseTorCache: bool, // Tor config dir //pub TorCacheDir: String, } /// Enum of experiment types that can be managed in Settings pub enum Experiments { /// experiment enabling in app management and running of Cwtch servers ServersExperiment, /// experiment enabling use of Cwtch groups GroupExperiment, /// experiment enabling filesharing FileSharingExperiment, /// experiment enabling auto downloading of image files to Settings::DownloadPath ImagePreviewsExperiment, } impl Experiments { /// returns the experiment settings key pub fn to_key_string(self) -> String { match self { Experiments::ServersExperiment => "servers-experiment".to_string(), Experiments::GroupExperiment => "tapir-groups-experiment".to_string(), Experiments::FileSharingExperiment => "filesharing".to_string(), Experiments::ImagePreviewsExperiment => "filesharing-images".to_string(), } } } impl Settings { /// Given a CwtchLib, handles sending an event to it with updated settings represented by this struct for saving pub fn save(&self, cwtch: &dyn CwtchLib) -> Result<(), String> { cwtch.update_settings(&self); return Ok(()); } } impl Profile { /// Create a new profile populated from supplied data /// contacts_json as supplied by libcwtch-go, a map of conversations /// server_list as supplied by libcwtch-go, a map of servers pub fn new( identity: ProfileIdentity, name: &str, picture: &str, conversations_json: &str, server_list: &str, ) -> Result { let conversations = match Profile::process_conversations(conversations_json) { Ok(c) => c, Err(e) => return Err(e), }; let servers = match Profile::process_servers(server_list) { Ok(s) => s, Err(e) => return Err(e), }; Ok(Profile { profile_id: identity, nick: name.to_string(), image_path: picture.to_string(), attr: Default::default(), conversations, servers: servers, }) } fn process_conversations(conversations_json: &str) -> Result, String> { let mut conversations: HashMap = HashMap::new(); if conversations_json == "null" { return Ok(conversations); } let conversations_map: Vec = match serde_json::from_str(conversations_json) { Ok(cm) => cm, Err(e) => return Err(format!("invalid json: {:?}", e)), }; for conversation in conversations_map { conversations.insert(conversation.identifier, conversation); } Ok(conversations) } fn process_servers(servers_json: &str) -> Result, String> { let mut servers: HashMap = HashMap::new(); if servers_json == "null" { return Ok(servers); } let servers_map: Vec = match serde_json::from_str(servers_json) { Ok(sm) => sm, Err(e) => return Err(format!("invalid json: {:?}", e)), }; for server in servers_map { servers.insert(server.onion.clone(), server); } Ok(servers) } /// Find a conversation_id associated with a remote handle (used for some events with no conversation id like PeerStateChange) pub fn find_conversation_id_by_handle(&self, contact_id: ContactIdentity) -> Option { match self.conversations.values().filter(|c| c.contact_id == contact_id).next() { Some(conversation) => Some(conversation.identifier), None => None } } }