Compare commits

..

No commits in common. "main" and "imp-fixes" have entirely different histories.

6 changed files with 236 additions and 239 deletions

View File

@ -1,10 +1,11 @@
[package]
name = "cwtch-imp"
version = "0.2.0"
name = "imp"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libcwtch = "0.4.0"
libcwtch = "0.3.0"
serde_json = "1.0"
chrono = "0.4.19"

View File

@ -12,15 +12,6 @@ It is in the very early prototype stage with one prototype use in the Cwtch [upd
Start with creating a `Behaviour` struct and populating it with your desired set of bot behaviours, then `imp::spawn` your bot with the behaviour.
To handle Cwtch events you can either
- Define a struct fulfilling the `imp::EventHandler::event_loop` function which has the capacity to support all the events libCwtch can emit
- Override specific `on_x_event` functions in `imp::EventHandler` such as `on_new_message_from_contact`
- This is newer and more will be defined in later versions
Finally, run the imp `my_imp.event_loop::<MyEventHandlerType>(update_bot.borrow_mut());`
## Examples
- Private [rust-bert](https://github.com/guillaume-be/rust-bert) chat bot - [rust-bert-bot](https://git.openprivacy.ca/dan/rust-bert-bot)
- Public utility bot - [Cwtch Update Bot](https://git.openprivacy.ca/dan/update_bot/)
Define a struct fulfilling the `imp::EventHandler` trait with all your custom event handling code.
Finally, run the imp `my_imp.event_loop(Box::new(custom_event_handler));`

View File

@ -1,110 +0,0 @@
use libcwtch::event::{ContactIdentity, GroupID};
/// defines a locked list of allowed peers and groups the bot may communicate with
/// others will be blocked, and peers listed here will be peered with actively
pub struct AllowListMembers {
/// list of peers to allow by handle
pub peers: Vec<ContactIdentity>,
/// list of groups to join and listen for peers in peer list from
pub groups: Vec<GroupID>,
}
impl AllowListMembers {
/// constructs a new AllowListMembers struct
pub fn new(peers: Vec<ContactIdentity>, groups: Vec<GroupID>) -> Self {
AllowListMembers {peers: peers, groups: groups}
}
}
/// How new contacts should be treated
pub enum NewContactPolicy {
/// Do not react, leave it for the custom event handler
Ignore,
/// Block all new contacts
Block,
/// Accept all new contacts
Accept,
/// AllowList is a list of handles that connections will be allowed from and connected to, and will be accepted
/// everything else will be ignored
AllowList(AllowListMembers)
}
/// Settings for the bot on how it should automatically behave
pub struct Behaviour {
/// The bot will enable experimental feautres (required for any experiments to be used)
pub proto_experiments: bool,
/// The bot will enable the file sharing experiment
pub proto_experiment_fileshare: bool,
/// The bot will enable the groups experiment
pub proto_experiment_groups: bool,
/// The profile name the bot will share with accepted conversations
pub profile_name: String,
/// The profile pic the bot with share with accepted conversations IF the file share exoeriment is enabled
pub profile_pic_path: Option<String>,
/// Policy dictacting how the bot should automatically handle ContactCreated events
pub new_contant_policy: NewContactPolicy,
}
/// intermediary struct for building a Behaviour using builder patern
pub struct BehaviourBuilder {
behaviour: Behaviour,
}
impl BehaviourBuilder {
/// Returns a new empty default off for features behaviour builder
pub fn new() -> Self {
return BehaviourBuilder {
behaviour: Behaviour {
proto_experiments: false,
proto_experiment_fileshare: false,
proto_experiment_groups: false,
new_contant_policy: NewContactPolicy::Ignore,
profile_name: "".to_string(),
profile_pic_path: None,
},
};
}
/// Build the defined behaviours into a Behaviour struct
pub fn build(self) -> Behaviour {
self.behaviour
}
/// Control if the Behaviour of the bot should include groups (enabling experiments and the group experiment)
pub fn groups(mut self, val: bool) -> Self {
self.behaviour.proto_experiment_groups = val;
self.behaviour.proto_experiments = true;
self
}
/// Control if the Behaviour of the bot should include filesharing (enabling experiments and the filesharing experiment)
pub fn fileshare(mut self, val: bool) -> Self {
self.behaviour.proto_experiment_fileshare = val;
self.behaviour.proto_experiments = true;
self
}
/// Set a profile pic for the bot and enable the filesharing experiment
pub fn profile_pic_path(mut self, val: String) -> Self {
self.behaviour.profile_pic_path = Some(val);
self.behaviour.proto_experiment_fileshare = true;
self.behaviour.proto_experiments = true;
self
}
/// Set a name for the behaviour
pub fn name(mut self, val: String) -> Self {
self.behaviour.profile_name = val;
self
}
/// Set a new contact policy for the behaviour
pub fn new_contact_policy(mut self, val: NewContactPolicy) -> Self {
self.behaviour.new_contant_policy = val;
self
}
}

72
src/event.rs Normal file
View File

@ -0,0 +1,72 @@
use chrono::{DateTime, FixedOffset};
use libcwtch::structs::CwtchEvent;
use std::collections::HashMap;
#[derive(Debug)]
pub enum Event {
CwtchStarted {
data: HashMap<String, String>,
},
NewPeer {
data: HashMap<String, String>,
},
NewMessageFromPeer {
conversation_id: i32,
handle: String,
timestamp_received: DateTime<FixedOffset>,
message: String,
},
AppError {
data: HashMap<String, String>,
},
ContactCreated {
data: HashMap<String, String>,
},
PeerStateChange {
data: HashMap<String, String>,
},
UpdateGlobalSettings {
data: HashMap<String, String>,
},
ErrUnhandled {
name: String,
data: HashMap<String, String>,
},
}
impl From<&CwtchEvent> for Event {
fn from(cwtch_event: &CwtchEvent) -> Self {
match cwtch_event.event_type.as_str() {
"CwtchStarted" => Event::CwtchStarted {
data: cwtch_event.data.clone(),
},
"NewPeer" => Event::NewPeer {
data: cwtch_event.data.clone(),
},
"NewMessageFromPeer" => Event::NewMessageFromPeer {
conversation_id: cwtch_event.data["ConversationID"].parse().unwrap_or(-2),
handle: cwtch_event.data["RemotePeer"].clone(),
timestamp_received: DateTime::parse_from_rfc3339(cwtch_event.data["TimestampReceived"].as_str()).unwrap(),
message: cwtch_event.data["Data"].clone(),
},
"AppError" => Event::AppError {
data: cwtch_event.data.clone(),
},
"ContactCreated" => Event::ContactCreated {
data: cwtch_event.data.clone(),
},
"PeerStateChange" => Event::PeerStateChange {
data: cwtch_event.data.clone(),
},
"UpdateGlobalSettings" => Event::UpdateGlobalSettings {
data: cwtch_event.data.clone(),
},
x => Event::ErrUnhandled {
name: x.to_string(),
data: cwtch_event.data.clone(),
},
}
}
}

View File

@ -1,28 +1,100 @@
use chrono::{DateTime, FixedOffset};
use crate::event::Event;
use libcwtch::structs::*;
use libcwtch::CwtchLib;
use libcwtch::event::{ConversationID, Event};
use crate::behaviour::{Behaviour, NewContactPolicy};
use crate::behaviour::NewContactPolicy::AllowList;
use serde_json;
/// How new contacts should be treated
pub enum NewContactPolicy {
/// Do not react, leave it for the custom event handler
Ignore,
/// Block all new contacts
Block,
/// Accept all new contacts
Accept,
}
/// Settings for the bot on how it should automatically behave
pub struct Behaviour {
/// The bot will enable experimental feautres (required for any experiments to be used)
pub proto_experiments: bool,
/// The bot will enable the file sharing experiment
pub proto_experiment_fileshare: bool,
/// The bot will enable the groups experiment
pub proto_experiment_groups: bool,
/// The profile name the bot will share with accepted conversations
pub profile_name: String,
/// The profile pic the bot with share with accepted conversations IF the file share exoeriment is enabled
pub profile_pic_path: Option<String>,
/// Policy dictacting how the bot should automatically handle ContactCreated events
pub new_contant_policy: NewContactPolicy,
}
pub struct BehaviourBuilder {
behaviour: Behaviour,
}
impl BehaviourBuilder {
pub fn new() -> Self {
return BehaviourBuilder {
behaviour: Behaviour {
proto_experiments: false,
proto_experiment_fileshare: false,
proto_experiment_groups: false,
new_contant_policy: NewContactPolicy::Ignore,
profile_name: "".to_string(),
profile_pic_path: None,
},
};
}
pub fn build(self) -> Behaviour {
self.behaviour
}
pub fn groups(mut self, val: bool) -> Self {
self.behaviour.proto_experiment_groups = val;
self.behaviour.proto_experiments = true;
self
}
pub fn fileshare(mut self, val: bool) -> Self {
self.behaviour.proto_experiment_fileshare = val;
self.behaviour.proto_experiments = true;
self
}
pub fn profile_pic_path(mut self, val: String) -> Self {
self.behaviour.profile_pic_path = Some(val);
self.behaviour.proto_experiment_fileshare = true;
self.behaviour.proto_experiments = true;
self
}
pub fn name(mut self, val: String) -> Self {
self.behaviour.profile_name = val;
self
}
pub fn new_contact_policy(mut self, val: NewContactPolicy) -> Self {
self.behaviour.new_contant_policy = val;
self
}
}
/// Trait to be used by implementors of imp bots to supply their custom event handling
/// the handle function is called after the default imp automatic event handling has run on each new event
pub trait EventHandler {
#[allow(unused_variables)]
fn handle(&mut self, cwtch: &dyn libcwtch::CwtchLib, profile: Option<&Profile>, event: &Event) {}
#[allow(unused_variables)]
fn on_contact_online(&self, cwtch: &dyn libcwtch::CwtchLib, profile: &Profile, convo_id: ConversationID) {}
#[allow(unused_variables)]
fn on_new_contact(&self, cwtch: &dyn libcwtch::CwtchLib, profile: &Profile, convo_id: ConversationID) {}
#[allow(unused_variables)]
fn on_new_message_from_contact(&self, cwtch: &dyn libcwtch::CwtchLib, profile: &Profile, conversation_id: ConversationID, handle: String, timestamp_received: DateTime<FixedOffset>, message: Message) {}
fn handle(&mut self, cwtch: &dyn CwtchLib, profile: Option<&Profile>, event: Event);
}
/// Cwtch bot
pub struct Imp {
cwtch: Box<dyn libcwtch::CwtchLib>,
cwtch: Box<dyn CwtchLib>,
behaviour: Behaviour,
password: String,
home_dir: String,
@ -51,7 +123,6 @@ impl Imp {
}
/// The main event loop handler for the bot, supply your own customer handler to handle events after the imp's automatic handling has processed the event
#[allow(unused_variables, unused_mut)]
pub fn event_loop<T>(&mut self, handler: &mut T)
where
T: EventHandler,
@ -59,10 +130,13 @@ impl Imp {
let mut initialized: bool = false;
loop {
let event = self.cwtch.get_appbus_event();
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 = Event::from(&event);
match &event {
Event::CwtchStarted => {
Event::CwtchStarted { data } => {
println!("event CwtchStarted!");
initialized = true;
@ -74,24 +148,27 @@ impl Imp {
Some(_) => (),
}
}
Event::UpdateGlobalSettings { settings } => {
let mut local_settings = settings.clone();
println!("Loading settings froms {:?}", local_settings);
Event::UpdateGlobalSettings { data } => {
println!("Loading settings froms {}", &data["Data"]);
let mut settings: Settings = match serde_json::from_str(&data["Data"]) {
Ok(s) => s,
Err(e) => panic!("invalid json: {:?}", e),
};
if self.behaviour.proto_experiments {
local_settings.ExperimentsEnabled = true;
settings.ExperimentsEnabled = true;
}
if self.behaviour.proto_experiment_fileshare {
local_settings
settings
.Experiments
.insert(Experiments::FileSharingExperiment.to_key_string(), true);
}
if self.behaviour.proto_experiment_groups {
local_settings
settings
.Experiments
.insert(Experiments::GroupExperiment.to_key_string(), true);
}
match local_settings.save(self.cwtch.as_ref()) {
match settings.save(self.cwtch.as_ref()) {
Ok(_) => (),
Err(e) => println!("ERROR: could not save settings: {}", e),
};
@ -99,139 +176,105 @@ impl Imp {
match self.profile.as_ref() {
Some(profile) => {
if let Some(profile_pic_path) = &self.behaviour.profile_pic_path {
self.cwtch.share_file(&profile.profile_id, ConversationID(-1), profile_pic_path);
self.cwtch.share_file(&profile.handle, -1, profile_pic_path);
}
}
None => (),
};
self.settings = Some(local_settings);
self.settings = Some(settings);
}
Event::NewPeer { profile_id, tag, created, name, default_picture, picture, online, profile_data} => {
if let Err(e) = profile_data {
panic!("error parsing profile: {}", e);
}
Event::NewPeer { data } => {
println!("\n***** {} at {} *****\n", data["name"], data["Identity"]);
// process json for profile, conversations and servers...else {
let profile = match Profile::new(
&data["Identity"],
&data["name"],
&data["picture"],
&data["ContactsJson"],
&data["ServerList"],
) {
Ok(p) => p,
Err(e) => panic!("error parsing profile: {}", e),
};
// Share profile image
match self.settings.as_ref() {
Some(_settings) => {
self.cwtch.share_file(&profile_id, ConversationID(-1), "build_bot.png");
self.cwtch.share_file(&profile.handle, -1, "build_bot.png");
}
None => (),
};
self.cwtch.set_profile_attribute(
&profile_id,
&profile.handle,
"profile.name",
&self.behaviour.profile_name,
);
if let Ok(ok_profile) = profile_data {
for (_id, conversation) in &ok_profile.conversations {
self.process_contact(conversation.identifier);
}
// Allow list should add all people in the list
if let AllowList(allow_list) = &self.behaviour.new_contant_policy {
for contact_id in &allow_list.peers {
if let None = ok_profile.find_conversation_id_by_handle(contact_id.clone()) {
self.cwtch.import_bundle(&profile_id, contact_id.clone().as_str());
}
for (_id, conversation) in &profile.conversations {
match self.behaviour.new_contant_policy {
NewContactPolicy::Accept => {
self.cwtch
.accept_conversation(&profile.handle.clone(), conversation.identifier);
}
NewContactPolicy::Block => self
.cwtch
.block_contact(&profile.handle.clone(), conversation.identifier),
NewContactPolicy::Ignore => (),
}
self.profile = Some(ok_profile.clone());
}
self.profile = Some(profile);
}
Event::AppError { error } => {
if initialized && error == "Loaded 0 profiles" {
Event::AppError { data } => {
if initialized && data["Error"] == "Loaded 0 profiles" {
self.cwtch
.create_profile(&self.behaviour.profile_name, &self.password);
}
}
Event::ContactCreated {profile_id, conversation_id, contact_id, nick, status, unread, picture, default_picture, num_messages, accepted, access_control_list, blocked, loading, last_msg_time, .. } => {
Event::ContactCreated { data } => {
println!("Contact Created");
let convo_handle = data["RemotePeer"].to_string();
let convo_id = data["ConversationID"].parse::<i32>().unwrap();
let acl: ACL = serde_json::from_str(&data["accessControlList"]).expect("Error parsing conversation");
let conversation = Conversation {
contact_id: contact_id.clone(),
identifier: conversation_id.clone(),
name: nick.clone(),
status: status.clone(),
blocked: blocked.clone(),
accepted: accepted.clone(),
access_control_list: access_control_list.clone(),
handle: convo_handle.clone(),
identifier: data["ConversationID"].parse::<i32>().unwrap(),
name: data["nick"].to_string(),
status: ConnectionState::new(&data["status"]),
blocked: data["blocked"] == "true",
accepted: data["accepted"] == "true",
access_control_list: acl,
is_group: false, // by definition
};
self.process_contact(conversation.identifier);
match self.profile.as_mut() {
Some(profile) => {
profile
.conversations
.insert(conversation.identifier, conversation);
handler.on_new_contact(self.cwtch.as_ref(), profile, conversation_id.clone());
handler.on_contact_online(self.cwtch.as_ref(), profile, conversation_id.clone());
.insert(data["RemotePeer"].to_string(), conversation);
match self.behaviour.new_contant_policy {
NewContactPolicy::Accept => {
self.cwtch
.accept_conversation(&profile.handle.clone(), convo_id);
}
NewContactPolicy::Block => self.cwtch.block_contact(&profile.handle.clone(), convo_id),
NewContactPolicy::Ignore => (),
}
}
None => (),
};
}
Event::PeerStateChange { profile_id, contact_id, connection_state } => {
if *connection_state == ConnectionState::Authenticated {
match self.profile.as_ref() {
Some(profile) => {
match profile.find_conversation_id_by_handle(contact_id.clone()) {
Some(conversation_id) => handler.on_contact_online(self.cwtch.as_ref(), profile,conversation_id),
None => {}
}
}
None => (),
};
}
}
Event::NewMessageFromPeer {profile_id, conversation_id,contact_id, nick, timestamp_received, message, notification, picture } => {
match self.profile.as_ref() {
Some(profile) => handler.on_new_message_from_contact(self.cwtch.as_ref(), profile, conversation_id.clone(), nick.clone(), timestamp_received.clone(), message.clone()),
None => {},
}
}
Event::PeerStateChange { data } => {}
Event::ErrUnhandled { name, data } => eprintln!("unhandled event: {}!", name),
_ => (),
};
handler.handle(self.cwtch.as_ref(), self.profile.as_ref(), &event);
}
}
fn process_contact(&self, conversation_id: ConversationID) {
match &self.profile {
Some(profile) => {
let profile_handle = profile.profile_id.clone();
match &self.behaviour.new_contant_policy {
NewContactPolicy::Accept => {
self.cwtch
.accept_conversation(&profile.profile_id, conversation_id);
}
NewContactPolicy::Block => self.cwtch.block_contact(&profile_handle.clone(), conversation_id),
NewContactPolicy::AllowList(allow_list) => {
match profile.conversations.get(&conversation_id) {
Some(conversation) => {
if allow_list.peers.contains(&conversation.contact_id) {
self.cwtch
.accept_conversation(&profile_handle.clone(), conversation_id);
} else {
self.cwtch.block_contact(&profile_handle.clone(), conversation_id);
}
},
None => {},
}
}
NewContactPolicy::Ignore => (),
}
},
None => {},
handler.handle(self.cwtch.as_ref(), self.profile.as_ref(), event);
}
}
}

View File

@ -1,2 +1,2 @@
pub mod event;
pub mod imp;
pub mod behaviour;