diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..8924168 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,193 @@ +use libcwtch::structs::*; +use libcwtch::CwtchLib; +use crate::Event; + +use serde_json; +use crate::bot::DefaultContactPolicy::Accept; + +pub enum DefaultContactPolicy { + Ignore, + Block, + Accept, +} + +pub struct Behaviour { + pub proto_experiments: bool, + pub proto_experiment_fileshare: bool, + + pub profile_name: String, + pub profile_pic_path: Option, + + pub default_contant_policy: DefaultContactPolicy, +} + +impl Behaviour { + pub fn new_default_acceptor(name: String, profile_pic_path: String) -> Self { + return Behaviour{ + proto_experiments: true, + proto_experiment_fileshare: true, + default_contant_policy: Accept, + profile_name: name, + profile_pic_path: Some(profile_pic_path), + } + } +} + +pub trait EventHandler { + fn handle(&self, cwtch: &dyn CwtchLib, profile: Option<&Profile>, event: CwtchEvent); +} + +pub struct Bot { + cwtch: Box, + behaviour: Behaviour, + password: String, + home_dir: String, + + settings: Option, + profile: Option, +} + +impl Bot { + pub fn new(behaviour: Behaviour, password: String, home_dir: String) -> Self { + let cwtch = libcwtch::new_cwtchlib_go(); + println!("start_cwtch"); + let ret = cwtch.start_cwtch(&home_dir, ""); + println!("start_cwtch returned {}", ret); + + return Bot{behaviour, cwtch: Box::new(cwtch), password, home_dir, profile: None, settings: None} + } + + pub fn event_loop(&mut self, handler: Box) { + let mut initialized: bool = false; + + loop { + let event_str = self.cwtch.get_appbus_event(); + println!("bot: event: {}", event_str); + + let event: CwtchEvent = + serde_json::from_str(&event_str).expect("Error parsing Cwtch event"); + let event_type = Event::new(event.event_type.as_str()); + match event_type { + Event::CwtchStarted => { + println!("event CwtchStarted!"); + initialized = true; + + match self.profile { + None => { + println!("Creating bot"); + self.cwtch.load_profiles(&self.password); + } + Some(_) => (), + } + } + Event::UpdateGlobalSettings => { + println!("Loading settings froms {}", &event.data["Data"]); + let mut settings: Settings = match serde_json::from_str(&event.data["Data"]) { + Ok(s) => s, + Err(e) => panic!("invalid json: {:?}", e), + }; + + if self.behaviour.proto_experiments { + settings.ExperimentsEnabled = true; + } + if self.behaviour.proto_experiment_fileshare { + settings + .Experiments + .insert(Experiments::FileSharingExperiment.to_key_string(), true); + } + match settings.save(self.cwtch.as_ref()) { + Ok(_) => (), + Err(e) => println!("ERROR: could not save settings: {}", e), + }; + + match self.profile.as_ref() { + Some(profile) => { + if let Some(profile_pic_path) = &self.behaviour.profile_pic_path { + self.cwtch.share_file(&profile.handle, -1, profile_pic_path); + } + } + None => (), + }; + + self.settings = Some(settings); + } + Event::NewPeer => { + println!( + "\n***** {} at {} *****\n", + event.data["name"], event.data["Identity"] + ); + + // process json for profile, conversations 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), + }; + print!("profile: {:?}", profile); + // Share profile image + match self.settings.as_ref() { + Some(_settings) => { + self.cwtch.share_file(&profile.handle, -1, "build_bot.png"); + } + None => (), + }; + + self.cwtch.set_profile_attribute(&profile.handle, "profile.name", &self.behaviour.profile_name); + + self.profile = Some(profile); + } + Event::AppError => { + if initialized && event.data["Error"] == "Loaded 0 profiles" { + self.cwtch.create_profile(&self.behaviour.profile_name, &self.password); + } + } + Event::ContactCreated => { + if event.data["ConnectionState"] == "Authenticated" { + let profile_onion = event.data["RemotePeer"].to_string(); + let convo_id = event.data["ConversationID"].parse::().unwrap(); + + let acl: ACL = serde_json::from_str(&event.data["accessControlList"]) + .expect("Error parsing conversation"); + + let conversation = Conversation { + handle: profile_onion.clone(), + identifier: event.data["ConversationID"].parse::().unwrap(), + name: event.data["nick"].to_string(), + status: ConnectionState::new(&event.data["status"]), + blocked: event.data["blocked"] == "true", + accepted: event.data["accepted"] == "true", + access_control_list: acl, + is_group: false, // by definition + }; + + match self.behaviour.default_contant_policy { + DefaultContactPolicy::Accept => + self.cwtch.accept_conversation(&conversation.handle.clone(), convo_id), + DefaultContactPolicy::Block => + self.cwtch.block_contact(&conversation.handle.clone(), convo_id), + DefaultContactPolicy::Ignore => (), + } + + match self.profile.as_mut() { + Some(profile) => { + profile + .conversations + .insert(event.data["RemotePeer"].to_string(), conversation); + } + None => (), + }; + } + } + Event::ErrUnhandled(err) => eprintln!("unhandled event: {}!", err), + _ => (), + }; + + handler.handle(self.cwtch.as_ref(), self.profile.as_ref(), event); + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a1213e1..04dd3e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod event; +mod bot; extern crate core; @@ -13,6 +14,7 @@ use serde_json; use libcwtch; use libcwtch::structs::*; use libcwtch::CwtchLib; +use crate::bot::{Behaviour, Bot}; const DIST_DIR: &str = "cwtch_dist"; const BOT_HOME: &str = "~/.cwtch/bots/update_bot"; @@ -23,8 +25,6 @@ const BOT_NAME: &str = "Update Bot"; const LAST_OFFERED_KEY: &str = "profile.last_version_offered"; struct UpdateBot { - settings: Option, - profile: Option, versions_dirs: Vec, latest_version: PathBuf, version: String, @@ -54,8 +54,6 @@ impl UpdateBot { .to_string(); let bot = UpdateBot { versions_dirs: versions_dirs, - profile: None, - settings: None, latest_version: latest_version, version: version, }; @@ -65,261 +63,127 @@ impl UpdateBot { } fn main() { - // load file, parse version if !Path::new(DIST_DIR).exists() { panic!("no '{}' directory with versions to distribute", DIST_DIR) } let mut update_bot = UpdateBot::new(); - - // make cwtch bot - let cwtch = libcwtch::new_cwtchlib_go(); - println!("start_cwtch"); - let ret = cwtch.start_cwtch(BOT_HOME, ""); - println!("start_cwtch returned {}", ret); - - // approve all friends - // offer newest version if none or now newest (question about os followed by file strasfer) - // for all friends, store offered version as attr - // respond to simple commands, include links, help info + let behaviour: Behaviour = Behaviour::new_default_acceptor(BOT_NAME.to_string(), "build_bot.png".to_string()); let event_loop_handle = thread::spawn(move || { - let mut initialized: bool = false; + let mut bot = Bot::new(behaviour, PASSWORD.to_string(), BOT_HOME.to_string()); - loop { - let event_str = cwtch.get_appbus_event(); - println!("event: {}", event_str); - - let event: CwtchEvent = - serde_json::from_str(&event_str).expect("Error parsing Cwtch event"); - let event_type = Event::new(event.event_type.as_str()); - match event_type { - Event::CwtchStarted => { - println!("event CwtchStarted!"); - initialized = true; - - match update_bot.profile { - None => { - println!("Creating bot"); - cwtch.load_profiles(PASSWORD); - } - Some(_) => (), - } - } - Event::UpdateGlobalSettings => { - println!("Loading settings froms {}", &event.data["Data"]); - let mut settings: Settings = match serde_json::from_str(&event.data["Data"]) { - Ok(s) => s, - Err(e) => panic!("invalid json: {:?}", e), - }; - settings.ExperimentsEnabled = true; - settings - .Experiments - .insert(Experiments::FileSharingExperiment.to_key_string(), true); - // TODO delete - settings - .Experiments - .insert(Experiments::ImagePreviewsExperiment.to_key_string(), false); - match settings.save(&cwtch) { - Ok(_) => (), - Err(e) => println!("ERROR: could not save settings: {}", e), - }; - match update_bot.profile.as_ref() { - Some(profile) => { - cwtch.share_file(&profile.handle, -1, "build_bot.png"); - } - None => (), - }; - update_bot.settings = Some(settings); - } - Event::NewPeer => { - println!( - "\n***** {} at {} *****\n", - event.data["name"], event.data["Identity"] - ); - - // process json for profile, conversations 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), - }; - print!("profile: {:?}", profile); - // Share profile image - match update_bot.settings.as_ref() { - Some(_settings) => { - cwtch.share_file(&profile.handle, -1, "build_bot.png"); - } - None => (), - }; - - cwtch.set_profile_attribute(&profile.handle, "profile.name", BOT_NAME); - for (_id, conversation) in &profile.conversations { - if conversation.accepted != true { - cwtch.accept_conversation( - profile.handle.as_str(), - conversation.identifier, - ) - } - } - update_bot.profile = Some(profile); - } - Event::AppError => { - if initialized && event.data["Error"] == "Loaded 0 profiles" { - cwtch.create_profile(BOT_NAME, PASSWORD); - } - } - Event::ContactCreated => { - if event.data["ConnectionState"] == "Authenticated" { - let profile_onion = event.data["RemotePeer"].to_string(); - let convo_id = event.data["ConversationID"].parse::().unwrap(); - - let acl: ACL = serde_json::from_str(&event.data["accessControlList"]) - .expect("Error parsing conversation"); - - let conversation = Conversation { - handle: profile_onion.clone(), - identifier: event.data["ConversationID"].parse::().unwrap(), - name: event.data["nick"].to_string(), - status: ConnectionState::new(&event.data["status"]), - blocked: event.data["blocked"] == "true", - accepted: event.data["accepted"] == "true", - access_control_list: acl, - is_group: false, // by definition - }; - - if conversation.accepted != true { - cwtch.accept_conversation(&conversation.handle.clone(), convo_id) - } - - match update_bot.profile.as_mut() { - Some(profile) => { - profile - .conversations - .insert(event.data["RemotePeer"].to_string(), conversation); - } - None => (), - }; - - update_bot.greet(&cwtch, convo_id); - } - } - - Event::PeerStateChange => { - if event.data["ConnectionState"] == "Authenticated" { - match update_bot.profile.as_ref() { - Some(profile) => { - let conversation = - &profile.conversations[&event.data["RemotePeer"]]; - if conversation.accepted != true { - cwtch.accept_conversation( - profile.handle.as_str(), - conversation.identifier, - ) - } - update_bot.greet(&cwtch, conversation.identifier); - } - None => (), - }; - } - } - Event::NewMessageFromPeer => { - let to = &event.data["ProfileOnion"]; - let conversation_id = event.data["ConversationID"].parse::().unwrap(); - let message_wrapper: Message = - serde_json::from_str(&event.data["Data"]).expect("Error parsing message"); - - let mut message = message_wrapper.d.clone(); - message.make_ascii_lowercase(); - - match message.as_str() { - "windows" => { - let mut windows_path = update_bot.latest_version.clone(); - windows_path.push("cwtch-installer.exe"); - cwtch.share_file(&to, conversation_id, windows_path.to_str().unwrap()); - } - "linux" => { - let mut linux_path = update_bot.latest_version.clone(); - linux_path.push(format!("cwtch-{}.tar.gz", update_bot.version)); - cwtch.share_file(&to, conversation_id, linux_path.to_str().unwrap()); - } - "macos" => { - let mut mac_path = update_bot.latest_version.clone(); - mac_path.push("Cwtch.dmg"); - cwtch.share_file(&to, conversation_id, mac_path.to_str().unwrap()); - } - "android" => { - let mut android_path = update_bot.latest_version.clone(); - android_path.push("app-release.apk"); - cwtch.share_file(&to, conversation_id, android_path.to_str().unwrap()); - } - _ => { - update_bot.offer(&cwtch, conversation_id); - } - } - } - Event::ErrUnhandled(err) => eprintln!("unhandled event: {}!", err), - _ => print!("unhandled event: {:?}!", event_type), - }; - } + bot.event_loop(Box::new(update_bot)); }); - event_loop_handle.join().expect("Error running event loop"); + event_loop_handle.join().expect("Error running event loop"); } + impl UpdateBot { - pub fn greet(&self, cwtch: &dyn CwtchLib, convo_id: i32) { - match self.profile.as_ref() { - Some(profile) => { - let do_offer = match cwtch.get_conversation_attribute( + pub fn greet(&self, cwtch: &dyn CwtchLib, profile_opt: Option<&Profile>, convo_id: i32) { + if let Some(profile) = profile_opt { + let do_offer = match cwtch.get_conversation_attribute( + &profile.handle, + convo_id, + &format!("local.{}", LAST_OFFERED_KEY), + ) { + Ok(ret) => match ret { + Some(last_offered) => last_offered != self.version, + None => true, + }, + Err(e) => { + println!("Error parsing attribute: {}", e); + false + } + }; + if do_offer { + self.offer(cwtch, profile_opt, convo_id); + cwtch.set_conversation_attribute( &profile.handle, convo_id, - &format!("local.{}", LAST_OFFERED_KEY), - ) { - Ok(ret) => match ret { - Some(last_offered) => last_offered != self.version, - None => true, - }, - Err(e) => { - println!("Error parsing attribute: {}", e); - false - } - }; - if do_offer { - self.offer(cwtch, convo_id); - cwtch.set_conversation_attribute( - &profile.handle, - convo_id, - LAST_OFFERED_KEY, - &self.version, - ); - } - } - None => (), - }; - } - - pub fn offer(&self, cwtch: &dyn CwtchLib, convo_id: i32) { - match self.profile.as_ref() { - Some(profile) => { - let resp_message = format!( - "Currently offering Cwtch {}\nPlease respond with the OS you would like a package for:\n- Windows\n- Android\n- MacOS\n- Linux", - self.version + LAST_OFFERED_KEY, + &self.version, ); - let response = Message { - o: 1, - d: resp_message, - }; - let response_json = - serde_json::to_string(&response).expect("Error parsing json response"); - cwtch.send_message(&profile.handle, convo_id, &response_json); } - None => (), - }; + } + } + + pub fn offer(&self, cwtch: &dyn CwtchLib, profile_opt: Option<&Profile>, convo_id: i32) { + if let Some(profile) = profile_opt { + let resp_message = format!( + "Currently offering Cwtch {}\nPlease respond with the OS you would like a package for:\n- Windows\n- Android\n- MacOS\n- Linux", + self.version + ); + let response = Message { + o: 1, + d: resp_message, + }; + let response_json = + serde_json::to_string(&response).expect("Error parsing json response"); + cwtch.send_message(&profile.handle, convo_id, &response_json); + } } } + +impl bot::EventHandler for UpdateBot { + fn handle(&self, cwtch: &dyn CwtchLib, profile_opt: Option<&Profile>, event: CwtchEvent) { + let event_type = Event::new(event.event_type.as_str()); + match event_type { + Event::ContactCreated => { + if event.data["ConnectionState"] == "Authenticated" { + let convo_id = event.data["ConversationID"].parse::().unwrap(); + self.greet(cwtch, profile_opt, convo_id); + } + } + Event::PeerStateChange => { + if event.data["ConnectionState"] == "Authenticated" { + match profile_opt.as_ref() { + Some(profile) => { + let conversation = + &profile.conversations[&event.data["RemotePeer"]]; + self.greet(cwtch, profile_opt, conversation.identifier); + } + None => (), + }; + } + } + Event::NewMessageFromPeer => { + let to = &event.data["ProfileOnion"]; + let conversation_id = event.data["ConversationID"].parse::().unwrap(); + let message_wrapper: Message = + serde_json::from_str(&event.data["Data"]).expect("Error parsing message"); + let mut message = message_wrapper.d.clone(); + message.make_ascii_lowercase(); + + match message.as_str() { + "windows" => { + let mut windows_path = self.latest_version.clone(); + windows_path.push("cwtch-installer.exe"); + cwtch.share_file(&to, conversation_id, windows_path.to_str().unwrap()); + } + "linux" => { + let mut linux_path = self.latest_version.clone(); + linux_path.push(format!("cwtch-{}.tar.gz", self.version)); + cwtch.share_file(&to, conversation_id, linux_path.to_str().unwrap()); + } + "macos" => { + let mut mac_path = self.latest_version.clone(); + mac_path.push("Cwtch.dmg"); + cwtch.share_file(&to, conversation_id, mac_path.to_str().unwrap()); + } + "android" => { + let mut android_path = self.latest_version.clone(); + android_path.push("app-release.apk"); + cwtch.share_file(&to, conversation_id, android_path.to_str().unwrap()); + } + _ => { + self.offer( cwtch, profile_opt, conversation_id); + } + } + } + Event::ErrUnhandled(err) => eprintln!("unhandled event: {}!", err), + _ => () + }; + } +} \ No newline at end of file