diff --git a/Cargo.lock b/Cargo.lock index 91faa43..6b100ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "block-buffer" version = "0.10.2" @@ -17,6 +23,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "cpufeatures" version = "0.2.1" @@ -125,14 +144,35 @@ checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" name = "libcwtch" version = "0.3.2" dependencies = [ + "chrono", "hex-literal", "libc", "serde", "serde_json", + "serde_repr", "serde_with", "sha2", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -194,6 +234,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_with" version = "1.11.0" @@ -245,6 +296,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "typenum" version = "1.15.0" @@ -262,3 +324,31 @@ name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index fc9b081..2962911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ libc = "0.2" serde_json = "1.0" serde = { version = "1.0.127", features = ["derive"] } serde_with = { version = "1.10.0" } +serde_repr = "0.1" +chrono = "0.4.19" diff --git a/examples/echobot.rs b/examples/echobot.rs index 93f8b40..92770b3 100644 --- a/examples/echobot.rs +++ b/examples/echobot.rs @@ -3,6 +3,7 @@ use std::thread; use libcwtch; use libcwtch::structs::*; use libcwtch::CwtchLib; +use libcwtch::event::Event; fn main() { let bot_home = "example_cwtch_dir"; @@ -19,46 +20,30 @@ fn main() { let event_loop_handle = thread::spawn(move || { loop { - let event_str = cwtch.get_appbus_event(); - println!("event: {}", event_str); + let event = cwtch.get_appbus_event(); + println!("event: {:?}", event); - let event: CwtchEvent = - serde_json::from_str(&event_str).expect("Error parsing Cwtch event"); - match event.event_type.as_str() { - "CwtchStarted" => { + match event { + Event::CwtchStarted => { println!("event CwtchStarted!"); println!("Creating bot"); cwtch.create_profile("Echobot", "be gay do crime"); } - "NewPeer" => { + Event::NewPeer { profile_id, tag, created, name, default_picture, picture, online, profile_data } => { println!( "\n***** {} at {} *****\n", - event.data["name"], event.data["Identity"] + name, profile_id.as_str() ); // process json for profile, contacts and servers...else { - let profile = match Profile::new( - &event.data["Identity"], - &event.data["name"], - &event.data["picture"], - &event.data["ContactsJson"], - &event.data["ServerList"], - ) { - Ok(p) => p, - Err(e) => panic!("error parsing profile: {}", e), - }; + let profile = profile_data; print!("profile: {:?}", profile); } - "NewMessageFromPeer" => { - let to = &event.data["ProfileOnion"]; - let conversation_id = event.data["ConversationID"].parse::().unwrap(); - let message: Message = - serde_json::from_str(&event.data["Data"]).expect("Error parsing message"); - - let response = Message { o: 1, d: message.d }; + Event::NewMessageFromPeer { profile_id, conversation_id, contact_id: contact, nick, timestamp_received, message, notification, picture } => { + let response = Message { o: message.o.into(), d: message.d }; let response_json = serde_json::to_string(&response).expect("Error parsing json response"); - cwtch.send_message(&to, conversation_id, &response_json); + cwtch.send_message( &profile_id, conversation_id, &response_json); } _ => eprintln!("unhandled event!"), }; diff --git a/src/bindings_go.rs b/src/bindings_go.rs index f3ad8f7..10e5141 100644 --- a/src/bindings_go.rs +++ b/src/bindings_go.rs @@ -8,7 +8,8 @@ use std::ffi::CString; use super::CwtchLib; use crate::cwtchlib_go::bindings; -use crate::{CwtchError, structs::*}; +use crate::{ConversationID, CwtchError, FileKey, ProfileIdentity, ServerIdentity, structs::*}; +use crate::event::Event; struct c_str_wrap { raw: *mut i8, @@ -86,42 +87,113 @@ pub struct CwtchLibGo {} impl CwtchLibGo { c_bind!(_get_profile_attribute(profile: &str, key: &str;;) c_GetProfileAttribute -> String); c_bind!(_get_conversation_attribute(profile: &str; conversation_id: i32; key: &str) c_GetConversationAttribute -> String); + c_bind!(_get_appbus_event(;;) c_GetAppBusEvent -> String); + c_bind!(_send_profile_event(profile: &str, event_jason: &str;;) c_SendProfileEvent); + c_bind!(_accept_conversation(profile: &str ; conversation_id: i32 ;) c_AcceptConversation); + c_bind!(_block_contact(profile: &str ; conversation_id: i32; ) c_BlockContact); + c_bind!(_unblock_contact(profile: &str ; conversation_id: i32; ) c_UnblockContact); + c_bind!(_get_message(profile: &str; conversation_id: i32, message_index: i32 ;) c_GetMessage -> String); + c_bind!(_get_message_by_id(profile: &str ; conversation_id: i32, message_id: i32 ;) c_GetMessageByID -> String); + c_bind!(_get_message_by_content_hash(profile: &str ; conversation_id: i32 ; hash: &str) c_GetMessagesByContentHash -> String); + c_bind!(_get_messages(profile: &str; conversation_id: i32, message_index: i32, count: i32 ;) c_GetMessages -> String); + c_bind!(_send_message(profile: &str; conversation_id: i32; msg: &str) c_SendMessage -> String); + c_bind!(_send_invitation(profile: &str; conversation_id: i32, target_id: i32;) c_SendInvitation -> String); + c_bind!(_share_file(profile: &str; conversation_id: i32; file_path: &str) c_ShareFile -> String); + c_bind!(_download_file(profile: &str; conversation_id: i32; file_path: &str, manifest_path: &str, file_key: &str) c_DownloadFile); + c_bind!(_check_download_status(profile: &str, file_key: &str;;) c_CheckDownloadStatus); + c_bind!(_verify_or_resume_download(profile: &str; conversation_id: i32; file_key: &str) c_VerifyOrResumeDownload); + c_bind!(_create_group(profile: &str, server: &str, name: &str;;) c_CreateGroup); + c_bind!(_delete_profile(profile: &str, pass: &str;;) c_DeleteProfile); + c_bind!(_archive_conversation(profile: &str; conversation_id: i32;) c_ArchiveConversation); + c_bind!(_delete_contact(profile: &str; conversation_id: i32;) c_DeleteContact); + c_bind!(_import_bundle(profile: &str, bundle: &str;;) c_ImportBundle); + c_bind!(_set_profile_attribute(profile: &str, key: &str, val: &str;;) c_SetProfileAttribute); + c_bind!(_set_conversation_attribute(profile: &str; conversation_id: i32; key: &str, val: &str) c_SetConversationAttribute); + c_bind!(_set_message_attribute(profile: &str; conversation_id: i32, channel_id: i32, message_id: i32; key: &str, val: &str) c_SetMessageAttribute); + c_bind!(_change_password(profile: &str, old_pass: &str, new_pass: &str, new_pass_again: &str;;) c_ChangePassword); + c_bind!(_export_profile(profile: &str, filename: &str;;) c_ExportProfile); + c_bind!(_delete_server(server: &str, current_password: &str;;) c_DeleteServer); + c_bind!(_launch_server(server: &str;;) c_LaunchServer); + c_bind!(_stop_server(server: &str;;) c_StopServer); + c_bind!(_set_server_attribute(server: &str, key: &str, val: &str;;) c_SetServerAttribute); } impl CwtchLib for CwtchLibGo { c_bind!(start_cwtch(app_dir: &str, tor_path: &str;;) c_StartCwtch -> i32); c_bind!(started(;;) c_Started -> i32); c_bind!(send_app_event(event_json: &str;;) c_SendAppEvent); - c_bind!(send_profile_event(profile: &str, event_jason: &str;;) c_SendProfileEvent); - c_bind!(get_appbus_event(;;) c_GetAppBusEvent -> String); + fn send_profile_event(&self, profile: &ProfileIdentity, event_jason: &str) { + self._send_profile_event(profile.as_str(), event_jason) + + } c_bind!(create_profile(nick: &str, pass: &str;;) c_CreateProfile); c_bind!(load_profiles(pass: &str;;) c_LoadProfiles); - c_bind!(accept_conversation(profile: &str ; conversation_id: i32 ;) c_AcceptConversation); - c_bind!(block_contact(profile: &str ; conversation_id: i32; ) c_BlockContact); - c_bind!(unblock_contact(profile: &str ; conversation_id: i32; ) c_UnblockContact); - c_bind!(get_message(profile: &str; conversation_id: i32, message_index: i32 ;) c_GetMessage -> String); - c_bind!(get_message_by_id(profile: &str ; conversation_id: i32, message_id: i32 ;) c_GetMessageByID -> String); - c_bind!(get_message_by_content_hash(profile: &str ; conversation_id: i32 ; hash: &str) c_GetMessagesByContentHash -> String); - c_bind!(get_messages(profile: &str; conversation_id: i32, message_index: i32, count: i32 ;) c_GetMessages -> String); - c_bind!(send_message(profile: &str; conversation_id: i32; msg: &str) c_SendMessage -> String); - c_bind!(send_invitation(profile: &str; conversation_id: i32, target_id: i32;) c_SendInvitation -> String); - c_bind!(share_file(profile: &str; conversation_id: i32; file_path: &str) c_ShareFile -> String); - c_bind!(download_file(profile: &str; conversation_id: i32; file_path: &str, manifest_path: &str, file_key: &str) c_DownloadFile); - c_bind!(check_download_status(profile: &str, file_key: &str;;) c_CheckDownloadStatus); - c_bind!(verify_or_resume_download(profile: &str; conversation_id: i32; file_key: &str) c_VerifyOrResumeDownload); + fn accept_conversation(&self, profile: &ProfileIdentity, conversation_id: ConversationID) { + self._accept_conversation(profile.as_str(), conversation_id.into()) + } + fn block_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID) { + self._block_contact(String::from(profile).as_str(), conversation_id.into()) + } + fn unblock_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID) { + self._unblock_contact(String::from(profile).as_str(), conversation_id.into()) + } + fn get_message(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_index: i32) -> String { + self._get_message(String::from(profile).as_str(), conversation_id.into(), message_index) + } + fn get_message_by_id(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_id: i32) -> String { + self._get_message_by_id(String::from(profile).as_str(), conversation_id.into(), message_id) + } + fn get_message_by_content_hash(&self, profile: &ProfileIdentity, conversation_id: ConversationID, hash: &str) -> String { + self._get_message_by_content_hash(String::from(profile).as_str(), conversation_id.into(), hash) + } + fn get_messages(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_index: i32, count: i32) -> String { + self._get_messages(String::from(profile).as_str(), conversation_id.into(), message_index, count) + } + fn send_message(&self, profile: &ProfileIdentity, conversation_id: ConversationID, msg: &str) -> String { + self._send_message(String::from(profile).as_str(), conversation_id.into(), msg) + } + fn send_invitation(&self, profile: &ProfileIdentity, conversation_id: ConversationID, target_id: i32) -> String { + self._send_invitation(String::from(profile).as_str(), conversation_id.into(), target_id) + } + fn share_file(&self, profile: &ProfileIdentity, conversation_id: ConversationID, file_path: &str) -> String { + self._share_file(String::from(profile).as_str(), conversation_id.into(), file_path) + } + fn download_file(&self, profile: &ProfileIdentity, conversation_id: ConversationID, file_path: &str, manifest_path: &str, file_key: FileKey) { + self._download_file(String::from(profile).as_str(), conversation_id.into(), file_path, manifest_path, String::from(file_key).as_str()) + } + fn check_download_status(&self, profile: &ProfileIdentity, file_key: FileKey) { + self._check_download_status(String::from(profile).as_str(), String::from(file_key).as_str()) + } + fn verify_or_resume_download(&self, profile: &ProfileIdentity, conversation_id: ConversationID, file_key: FileKey) { + self._verify_or_resume_download(String::from(profile).as_str(), conversation_id.into(), String::from(file_key).as_str()) + } + fn reset_tor(&self) { unsafe { bindings::c_ResetTor(); } } - c_bind!(create_group(profile: &str, server: &str, name: &str;;) c_CreateGroup); - c_bind!(delete_profile(profile: &str, pass: &str;;) c_DeleteProfile); - c_bind!(archive_conversation(profile: &str; conversation_id: i32;) c_ArchiveConversation); - c_bind!(delete_contact(profile: &str; conversation_id: i32;) c_DeleteContact); - c_bind!(import_bundle(profile: &str, bundle: &str;;) c_ImportBundle); - c_bind!(set_profile_attribute(profile: &str, key: &str, val: &str;;) c_SetProfileAttribute); - fn get_profile_attribute(&self, profile: &str, key: &str) -> Result, CwtchError> { - let resp = self._get_profile_attribute(profile, key); + fn create_group(&self, profile: &ProfileIdentity, server: &str, name: &str) { + self._create_group(String::from(profile).as_str(), server, name) + } + fn delete_profile(&self, profile: &ProfileIdentity, pass: &str) { + self._delete_profile(String::from(profile).as_str(), pass) + } + fn archive_conversation(&self, profile: &ProfileIdentity, conversation_id: ConversationID) { + self._archive_conversation(String::from(profile).as_str(), conversation_id.into()) + } + fn delete_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID) { + self._delete_contact(profile.as_str(), conversation_id.into()) + } + fn import_bundle(&self, profile: &ProfileIdentity, bundle: &str) { + self._import_bundle(String::from(profile).as_str(), bundle) + } + fn set_profile_attribute(&self, profile: &ProfileIdentity, key: &str, val: &str) { + self._set_profile_attribute(String::from(profile).as_str(), key, val) + } + + fn get_profile_attribute(&self, profile: &ProfileIdentity, key: &str) -> Result, CwtchError> { + let resp = self._get_profile_attribute(String::from(profile).as_str(), key); let attr: Attribute = match serde_json::from_str(&resp) { Ok(attr) => attr, Err(e) => return Err(e.to_string()), @@ -131,9 +203,11 @@ impl CwtchLib for CwtchLibGo { false => Ok(None), } } - c_bind!(set_conversation_attribute(profile: &str; conversation_id: i32; key: &str, val: &str) c_SetConversationAttribute); - fn get_conversation_attribute(&self, profile: &str, conversation_id: i32, key: &str) -> Result, CwtchError> { - let resp = self._get_conversation_attribute(profile, conversation_id, key); + fn set_conversation_attribute(&self, profile: &ProfileIdentity, conversation_id: ConversationID, key: &str, val: &str) { + self._set_conversation_attribute(String::from(profile).as_str(), conversation_id.into(), key, val) + } + fn get_conversation_attribute(&self, profile: &ProfileIdentity, conversation_id: ConversationID, key: &str) -> Result, CwtchError> { + let resp = self._get_conversation_attribute(String::from(profile).as_str(), conversation_id.into(), key); let attr: Attribute = match serde_json::from_str(&resp) { Ok(attr) => attr, Err(e) => return Err(e.to_string()), @@ -143,11 +217,19 @@ impl CwtchLib for CwtchLibGo { false => Ok(None), } } - c_bind!(set_message_attribute(profile: &str; conversation_id: i32, channel_id: i32, message_id: i32; key: &str, val: &str) c_SetMessageAttribute); - c_bind!(change_password(profile: &str, old_pass: &str, new_pass: &str, new_pass_again: &str;;) c_ChangePassword); - c_bind!(export_profile(profile: &str, filename: &str;;) c_ExportProfile); + fn set_message_attribute(&self, profile: &ProfileIdentity, conversation_id: ConversationID, channel_id: i32, message_id: i32, key: &str, val: &str) { + self._set_message_attribute(String::from(profile).as_str(), conversation_id.into(), channel_id, message_id, key, val) + } + fn change_password(&self, profile: &ProfileIdentity, old_pass: &str, new_pass: &str, new_pass_again: &str) { + self._change_password(String::from(profile).as_str(), old_pass, new_pass, new_pass_again) + } + fn export_profile(&self, profile: &ProfileIdentity, filename: &str) { + self._export_profile(String::from(profile).as_str(), filename) + } + c_bind!(import_profile(filename: &str, password: &str;;) c_ImportProfile -> String); + fn shutdown_cwtch(&self) { unsafe { bindings::c_ShutdownCwtch(); @@ -156,12 +238,26 @@ impl CwtchLib for CwtchLibGo { c_bind!(load_servers(password: &str;;) c_LoadServers); c_bind!(create_server(password: &str, description: &str; autostart: i8;) c_CreateServer); - c_bind!(delete_server(onion: &str, current_password: &str;;) c_DeleteServer); + fn delete_server(&self, server: ServerIdentity, current_password: &str) { + self._delete_server(String::from(server).as_str(), current_password) + } c_bind!(launch_servers(;;) c_LaunchServers); - c_bind!(launch_server(onion: &str;;) c_LaunchServer); - c_bind!(stop_server(onion: &str;;) c_StopServer); + fn launch_server(&self, server: ServerIdentity) { + self._launch_server(String::from(server).as_str()) + } + fn stop_server(&self, server: ServerIdentity) { + self._stop_server(String::from(server).as_str()) + } c_bind!(stop_servers(;;) c_StopServers); c_bind!(destroy_servers(;;) c_DestroyServers); - c_bind!(set_server_attribute(onion: &str, key: &str, val: &str;;) c_SetServerAttribute); + fn set_server_attribute(&self, server: ServerIdentity, key: &str, val: &str) { + self._set_server_attribute(String::from(server).as_str(), key, val) + } c_bind!(get_debug_info(;;) c_GetDebugInfo -> String); + + fn get_appbus_event(&self) -> Event { + let event_json = self._get_appbus_event(); + let cwtch_event: CwtchEvent = serde_json::from_str(&event_json).expect("Error parsing Cwtch event"); + Event::from(&cwtch_event) + } } diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..6987b09 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,822 @@ +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, Message, 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) + } +} + + +#[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() + } +} + +#[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() + } +} + +#[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 + }, + /// 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: Message, + /// 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: Message, + /// hash of the message + content_hash: String, + /// path to picture for sender + picture: String, + /// name of sender + nick: 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, + /// group id + group_id: GroupID, + /// server the group is on + group_server: String, + /// invite string + group_invite: String, + /// group name + group_name: String, + /// path to group picture + picture: String, + /// Access Control List for group + access_control_list: ACL, + }, + /// 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: Message::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: cwtch_event.data["Error"].clone(), + }, + "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(), + nick: cwtch_event.data["Nick"].clone(), + message: Message::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_id: cwtch_event.data["GroupID"].clone().into(), + group_server: cwtch_event.data["GroupServer"].clone(), + group_invite: cwtch_event.data["GroupInvite"].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()), + }, + "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(), + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b84271e..ee92cc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,15 @@ #![doc(html_root_url = "https://git.openprivacy.ca/cwtch.im/libcwtch-rs")] #![deny(missing_docs)] +use crate::event::{ConversationID, Event, FileKey, ProfileIdentity, ServerIdentity}; + mod bindings_go; mod cwtchlib_go; /// Basic structs using data from Cwtch and for deserializing JSON and serializing to JSON to communicate with Cwtch pub mod structs; +/// Additional structs for advnaced event handling and converstion helpers +pub mod event; /// Error type for Cwtch lib related errors, intended for use with Result pub type CwtchError = String; @@ -24,10 +28,10 @@ pub trait CwtchLib { fn send_app_event(&self, event_json: &str); /// Send json of a structs::CwtchEvent to a cwtch profile - fn send_profile_event(&self, profile: &str, event_json: &str); + fn send_profile_event(&self, profile: &ProfileIdentity, event_json: &str); /// Pull json of a structs::CwtchEvent off the appbus for responding to - fn get_appbus_event(&self) -> String; + fn get_appbus_event(&self) -> Event; /// Create a new profile encrypted with pass fn create_profile(&self, nick: &str, pass: &str); @@ -36,88 +40,88 @@ pub trait CwtchLib { fn load_profiles(&self, pass: &str); /// Cause profile to accept conversation - fn accept_conversation(&self, profile: &str, conversation_id: i32); + fn accept_conversation(&self, profile: &ProfileIdentity, conversation_id: ConversationID); /// Cause profile to block conversation - fn block_contact(&self, profile: &str, conversation_id: i32); + fn block_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID); /// Cause profile to unblock contact - fn unblock_contact(&self, profile: &str, conversation_id: i32); + fn unblock_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID); /// Get a specific message for conversation of profile by index - fn get_message(&self, profile: &str, conversation_id: i32, message_index: i32) -> String; + fn get_message(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_index: i32) -> String; /// Get a specific message for a conversation by its id - fn get_message_by_id(&self, profile: &str, conversation_id: i32, message_id: i32) -> String; + fn get_message_by_id(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_id: i32) -> String; /// Get a specific message for conversation of profile by hash fn get_message_by_content_hash( &self, - profile: &str, - conversation_id: i32, + profile: &ProfileIdentity, + conversation_id: ConversationID, hash: &str, ) -> String; /// Bulk get messages starting at message index and of count amoung - fn get_messages(&self, profile: &str, conversation_id: i32, message_index: i32, count: i32) -> String; + fn get_messages(&self, profile: &ProfileIdentity, conversation_id: ConversationID, message_index: i32, count: i32) -> String; /// Send json of a structs::Message from profile to contact. Returns computed sent message (including index and hash values) - fn send_message(&self, profile: &str, conversation_id: i32, msg: &str) -> String; + fn send_message(&self, profile: &ProfileIdentity, conversation_id: ConversationID, msg: &str) -> String; /// Send profile's contact an invite for/to target. Returns computed sent message (including index and hash values) - fn send_invitation(&self, profile: &str, conversation_id: i32, target_id: i32) -> String; + fn send_invitation(&self, profile: &ProfileIdentity, conversation_id: ConversationID, target_id: i32) -> String; /// share a file file_path with a conersation. Returns computed sent message (including index and hash values) - fn share_file(&self, profile: &str, conversation_id: i32, file_path: &str) -> String; + fn share_file(&self, profile: &ProfileIdentity, conversation_id: ConversationID, file_path: &str) -> String; /// download a file from a conversation to the file_path fn download_file( &self, - profile: &str, - conversation_id: i32, + profile: &ProfileIdentity, + conversation_id: ConversationID, file_path: &str, manifest_path: &str, - file_key: &str, + file_key: FileKey, ); /// Query the status of a download - fn check_download_status(&self, profile: &str, file_key: &str); + fn check_download_status(&self, profile: &ProfileIdentity, file_key: FileKey); /// Verufy a download is done, and if not, resume it - fn verify_or_resume_download(&self, profile: &str, conversation_id: i32, file_key: &str); + fn verify_or_resume_download(&self, profile: &ProfileIdentity, conversation_id: ConversationID, file_key: FileKey); /// Ask the ACN inside the Cwtch app to restart the tor connection fn reset_tor(&self); /// Cause profile to create a group on server with name - fn create_group(&self, profile: &str, server: &str, name: &str); + fn create_group(&self, profile: &ProfileIdentity, server: &str, name: &str); /// Delete profile with encryption/password check of pass - fn delete_profile(&self, profile: &str, pass: &str); + fn delete_profile(&self, profile: &ProfileIdentity, pass: &str); /// Cause profile to archive conversation with contact - fn archive_conversation(&self, profile: &str, conversation_id: i32); + fn archive_conversation(&self, profile: &ProfileIdentity, conversation_id: ConversationID); /// Cause profile to delete contact/group identified by handle - fn delete_contact(&self, profile: &str, conversation_id: i32); + fn delete_contact(&self, profile: &ProfileIdentity, conversation_id: ConversationID); /// Cuase profile to attempt to import a contact/group/keybundle identified by bundle - fn import_bundle(&self, profile: &str, bundle: &str); + fn import_bundle(&self, profile: &ProfileIdentity, bundle: &str); /// Set a profile attribute key to val - fn set_profile_attribute(&self, profile: &str, key: &str, val: &str); + fn set_profile_attribute(&self, profile: &ProfileIdentity, key: &str, val: &str); /// Get a profile attribute - fn get_profile_attribute(&self, profile: &str, key: &str) -> Result, CwtchError>; + fn get_profile_attribute(&self, profile: &ProfileIdentity, key: &str) -> Result, CwtchError>; /// Set a profile's contact's attribute of key to val - fn set_conversation_attribute(&self, profile: &str, conversation_id: i32, key: &str, val: &str); + fn set_conversation_attribute(&self, profile: &ProfileIdentity, conversation_id: ConversationID, key: &str, val: &str); /// Set an attribute on a message in a conversation fn set_message_attribute( &self, - profile: &str, - conversation_id: i32, + profile: &ProfileIdentity, + conversation_id: ConversationID, channel_id: i32, message_id: i32, attribute_key: &str, @@ -125,13 +129,13 @@ pub trait CwtchLib { ); /// Get an attribute for a conversation - fn get_conversation_attribute(&self, profile: &str, conversation_id: i32, key: &str) -> Result, CwtchError>; + fn get_conversation_attribute(&self, profile: &ProfileIdentity, conversation_id: ConversationID, key: &str) -> Result, CwtchError>; /// Change a profile's password to new_pass if old_pass is correct - fn change_password(&self, profile: &str, old_pass: &str, new_pass: &str, new_pass_again: &str); + fn change_password(&self, profile: &ProfileIdentity, old_pass: &str, new_pass: &str, new_pass_again: &str); /// Export a profile to filename - fn export_profile(&self, profile: &str, filename: &str); + fn export_profile(&self, profile: &ProfileIdentity, filename: &str); /// Import a profile from a file with supplied password. Json of a profile struct returned on success fn import_profile(&self, filename: &str, password: &str) -> String; @@ -148,16 +152,16 @@ pub trait CwtchLib { fn create_server(&self, password: &str, description: &str, autostart: i8); /// Delete the specified server (if password is correct) - fn delete_server(&self, onion: &str, current_password: &str); + fn delete_server(&self, server: ServerIdentity, current_password: &str); /// Launch all loaded servers fn launch_servers(&self); /// Launch the specified server - fn launch_server(&self, onion: &str); + fn launch_server(&self, server: ServerIdentity); /// Stop the specified server - fn stop_server(&self, onion: &str); + fn stop_server(&self, server: ServerIdentity); /// Stop all running servers fn stop_servers(&self); @@ -166,7 +170,7 @@ pub trait CwtchLib { fn destroy_servers(&self); /// Set the specified server's attribute of key to val - fn set_server_attribute(&self, onion: &str, key: &str, val: &str); + fn set_server_attribute(&self, server: ServerIdentity, key: &str, val: &str); /// Get debug info (mem, goroutine stats) from lcg in json fn get_debug_info(&self) -> String; diff --git a/src/structs.rs b/src/structs.rs index 91b41e6..706c07f 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,10 +1,12 @@ use crate::structs::ConnectionState::Disconnected; -use crate::CwtchLib; +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; -#[derive(Serialize, Deserialize, Debug)] +#[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 @@ -29,9 +31,9 @@ impl Default for ConnectionState { } } -impl ConnectionState { +impl From<&str> for ConnectionState { /// Creates a ConnectionState from a string sent from libcwtch-go - pub fn new(name: &str) -> Self { + fn from(name: &str) -> Self { match name { "Disconnected" => ConnectionState::Disconnected, "Connecting" => ConnectionState::Connecting, @@ -81,7 +83,7 @@ pub struct CwtchEvent { pub data: HashMap, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "PascalCase")] #[allow(non_snake_case)] /// AccessControl is a type determining client assigned authorization to a peer @@ -98,15 +100,15 @@ pub struct AccessControl { pub type ACL = HashMap; #[serde_as] -#[derive(Serialize, Deserialize, Debug)] +#[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 handle: String, + pub contact_id: ContactIdentity, /// unique identifier of the contact/conversation to be used in API access - pub identifier: i32, + 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")] @@ -125,7 +127,7 @@ pub struct Conversation { //attr: HashMap, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] /// Struct to serialize/deserialize servers coming from libcwtch-go pub struct Server { /// onion address of the server @@ -134,13 +136,11 @@ pub struct Server { pub status: ConnectionState, } -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone)] /// Struct to serialize/deserialize profiles coming from libcwtch-go pub struct Profile { /// onion address / ID of the profile - #[serde(alias = "onion")] - pub handle: String, + 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 @@ -148,22 +148,66 @@ pub struct Profile { /// all profile attributes pub attr: HashMap, /// map of conversation [ onion => conversation ] - pub conversations: HashMap, + pub conversations: HashMap, /// map of servers [ onion => server ] pub servers: HashMap, } -#[derive(Debug, Serialize, Deserialize)] +#[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 pub struct Message { /// overlay id that the message is targeting as defined in cwtch/model/overlay.go /// [ OverlayChat = 1, OverlayInviteContact = 100, OverlayInviteGroup = 101, OverlayFileSharing = 200 ] - pub o: i64, + pub o: MessageType, /// data of the message pub d: String, } -#[derive(Debug, Serialize, Deserialize)] +impl Message { + /// parse json into a Message + pub fn from_json(json: &str) -> Self { + match serde_json::from_str(json) { + Ok(m) => m, + Err(e) => Message{o: MessageType::MalformedMessage, d: e.to_string()} + } + } +} + +#[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 { @@ -265,7 +309,7 @@ impl Profile { /// 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: &str, + identity: ProfileIdentity, name: &str, picture: &str, conversations_json: &str, @@ -280,7 +324,7 @@ impl Profile { Err(e) => return Err(e), }; Ok(Profile { - handle: identity.to_string(), + profile_id: identity, nick: name.to_string(), image_path: picture.to_string(), attr: Default::default(), @@ -289,8 +333,8 @@ impl Profile { }) } - fn process_conversations(conversations_json: &str) -> Result, String> { - let mut conversations: HashMap = HashMap::new(); + fn process_conversations(conversations_json: &str) -> Result, String> { + let mut conversations: HashMap = HashMap::new(); if conversations_json == "null" { return Ok(conversations); } @@ -320,8 +364,8 @@ impl Profile { } /// 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, handle: String) -> Option { - match self.conversations.values().filter(|c| c.handle == handle).next() { + 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 }