diff --git a/.gitignore b/.gitignore index c77774d..8e6827f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # env .env + +# temp +/tmp \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 11ba1db..3ee755c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ name = "forcebot-rs-v2" version = "0.1.0" dependencies = [ "dotenv", + "lazy_static", "tokio", "twitch-irc", ] @@ -214,6 +215,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" diff --git a/Cargo.toml b/Cargo.toml index 53faa8d..e9d2e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ default-run = "forcebot-rs-v2" [dependencies] dotenv = "0.15.0" +lazy_static = "1.5.0" tokio = { version = "1.33.0", features = ["full"] } twitch-irc = "5.0.1" + # [[bin]] # name = "simple_bot" # path = "src/simple_bot.rs" diff --git a/readme.md b/readme.md index d192a1b..045e712 100644 --- a/readme.md +++ b/readme.md @@ -128,16 +128,19 @@ pub async fn main() { pub mod custom_mod { + use std::sync::Arc; use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module}; use twitch_irc::message::ServerMessage; - /// Module with a loaded command + /// Module definition with a loaded command pub fn new() -> Module { /* 1. Create a new module */ - let mut custom_mod = Module::new("test".to_string(), "".to_string()); + let mut custom_mod = Module::new( + vec!["test".to_string()], + "".to_string()); /* 2. Load the cmd into a new module */ custom_mod.load_command(cmd_test()); @@ -146,9 +149,10 @@ pub mod custom_mod { } + /// Command definition pub fn cmd_test() -> Command { /* 1. Create a new cmd */ - let mut cmd = Command::new("test".to_string(),"".to_string()); + let mut cmd = Command::new(vec!["test".to_string()],"".to_string()); /* 2. Define exec callback */ async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { @@ -165,7 +169,6 @@ pub mod custom_mod { cmd.set_min_badge(Badge::Moderator); cmd - } } ``` @@ -290,7 +293,7 @@ pub async fn main() { let mut bot = Bot::new(); /* 1. Create a new blank cmd */ - let mut cmd = Command::new("test".to_string(),"".to_string()); + let mut cmd = Command::new(vec!["test".to_string()],"".to_string()); /* 2. Define an async fn callback execution */ async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { @@ -308,8 +311,8 @@ pub async fn main() { cmd.set_admin_only(false); /* 5. optionally, set min badge*/ - cmd.set_min_badge("broadcaster".to_string()); -// + cmd.set_min_badge(Badge::Moderator); + /* 6. Load the cmd into the bot */ bot.load_command(cmd); diff --git a/src/bin/fun_bot.rs b/src/bin/fun_bot.rs index eaf0487..196ba05 100644 --- a/src/bin/fun_bot.rs +++ b/src/bin/fun_bot.rs @@ -3,6 +3,7 @@ //! Custom modules that can be managed in chat through `disable` and `enable` commands //! - funbot //! - guests +//! - pyramid //! //! //! Be sure the followig is defined in `.env` @@ -16,7 +17,7 @@ //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> -use forcebot_rs_v2::{custom_mods::guest_badge, Bot}; +use forcebot_rs_v2::{custom_mods::{guest_badge, pyramid}, Bot}; #[tokio::main] pub async fn main() { @@ -29,6 +30,7 @@ pub async fn main() { /* 2. Load Custom Modules */ bot.load_module(guest_badge::create_module()); + bot.load_module(pyramid::create_module()); /* 3. Run the bot */ bot.run().await; diff --git a/src/bin/simple_module.rs b/src/bin/simple_module.rs index 002ca22..1204e3a 100644 --- a/src/bin/simple_module.rs +++ b/src/bin/simple_module.rs @@ -41,7 +41,7 @@ pub mod custom_mod { use twitch_irc::message::ServerMessage; - /// Module with a loaded command + /// Module definition with a loaded command pub fn new() -> Module { /* 1. Create a new module */ let mut custom_mod = Module::new( @@ -55,6 +55,7 @@ pub mod custom_mod { } + /// Command definition pub fn cmd_test() -> Command { /* 1. Create a new cmd */ let mut cmd = Command::new(vec!["test".to_string()],"".to_string()); @@ -74,5 +75,5 @@ pub mod custom_mod { cmd.set_min_badge(Badge::Moderator); cmd - } + } } \ No newline at end of file diff --git a/src/botcore/bot.rs b/src/botcore/bot.rs index ac19db2..b70b09c 100644 --- a/src/botcore/bot.rs +++ b/src/botcore/bot.rs @@ -1,7 +1,7 @@ use tokio::sync::{mpsc::UnboundedReceiver, Mutex}; -use twitch_irc::{login::StaticLoginCredentials, message::ServerMessage, SecureTCPTransport, TwitchIRCClient}; +use twitch_irc::{login::StaticLoginCredentials, message::{PrivmsgMessage, ServerMessage}, SecureTCPTransport, TwitchIRCClient}; use dotenv::dotenv; use std::{env, sync::Arc, time::{Duration, Instant}}; @@ -32,7 +32,10 @@ pub struct Bot /// channel module status channel_module_status: Mutex<Vec<(String,String,modules::Status)>>, /// chatter guest badges - chatter,channel,Badge,start_time,duration - chatter_guest_badges: Mutex<Vec<(String,String,Badge,Instant,Duration)>> + chatter_guest_badges: Mutex<Vec<(String,String,Badge,Instant,Duration)>>, + /// Message cache + message_cache: Mutex<Vec<PrivmsgMessage>>, + // message_cache: Vec<PrivmsgMessage> } @@ -112,6 +115,7 @@ impl Bot modules: vec![], channel_module_status: Mutex::new(vec![]), chatter_guest_badges: Mutex::new(vec![]), + message_cache : Mutex::new(vec![]), }; for cmd in built_in_objects::create_commands() { @@ -135,7 +139,10 @@ impl Bot let a = bot.clone(); let mut in_msgs_lock = a.incoming_msgs.lock().await; - while let Some(message) = in_msgs_lock.recv().await { + while let Some(message) = in_msgs_lock.recv().await { + + + for listener in &(*bot).listeners { let a = listener.clone(); @@ -147,6 +154,15 @@ impl Bot if let ServerMessage::Privmsg(msg) = message.clone() { + // let mut cache_lock = bot.message_cache.lock().await; + let mut cache_lock = bot.message_cache.lock().await; + cache_lock.push(msg.clone()); + // dbg!(cache_lock.clone()); + drop(cache_lock); + + // let a = bot.clone(); + // dbg!(bot.get_message_cache()); + for cmd in &(*bot).commands { let a = cmd.clone(); @@ -324,6 +340,9 @@ impl Bot } + pub fn get_message_cache(&self) -> &Mutex<Vec<PrivmsgMessage>> { + &self.message_cache + } } diff --git a/src/custom_mods.rs b/src/custom_mods.rs index cd79787..099cee1 100644 --- a/src/custom_mods.rs +++ b/src/custom_mods.rs @@ -1 +1,2 @@ -pub mod guest_badge; \ No newline at end of file +pub mod guest_badge; +pub mod pyramid; \ No newline at end of file diff --git a/src/custom_mods/guest_badge.rs b/src/custom_mods/guest_badge.rs index 175a0e1..9e9807c 100644 --- a/src/custom_mods/guest_badge.rs +++ b/src/custom_mods/guest_badge.rs @@ -14,16 +14,18 @@ use crate::{asyncfn_box, Badge, Bot, Command, Module}; /// /// /// -/// -/// -/// -/// const VIP_GIVEN_DUR_MIN:u64 = 15; const MOD_GIVEN_DUR_MIN:u64 = 30; - +/// Use this function when loading modules into the bot +/// +/// For example +/// ```rust +/// bot.load_module(guest_badge::create_module()); +/// ``` +/// pub fn create_module() -> Module { let mut custom_mod = Module::new( @@ -32,7 +34,7 @@ pub fn create_module() -> Module { custom_mod.load_command(create_cmd_mod()); custom_mod.load_command(create_cmd_vip()); - + custom_mod } diff --git a/src/custom_mods/pyramid.rs b/src/custom_mods/pyramid.rs new file mode 100644 index 0000000..5874a07 --- /dev/null +++ b/src/custom_mods/pyramid.rs @@ -0,0 +1,270 @@ +use std::sync::{Arc, Mutex}; + +use twitch_irc::message::{PrivmsgMessage, ServerMessage}; + +use crate::{asyncfn_box, Bot, Listener, Module}; + +/// pyramid module +/// +/// for detecting & handling pyramids +/// +/// +/// +/// +use lazy_static::lazy_static; + + +/// Use this function when loading modules into the bot +/// +/// For example +/// ```rust +/// bot.load_module(pyramid::create_module()); +/// ``` +/// +pub fn create_module() -> Module { + + let mut custom_mod = Module::new( + vec!["pyramid".to_string(), + "pyramids".to_string()], + "o7 I can handle pyramids".to_string()); + custom_mod.load_listener(create_pyramid_detector()); + + custom_mod + +} + +fn create_pyramid_detector() -> Listener { + + /* 1. Create a new blank Listener */ + let mut listener = Listener::new(); + + /* 2. Set a trigger condition function for listener */ + listener.set_trigger_cond_fn( + |_bot:Arc<Bot>,_message:ServerMessage| + true + ); + + /* 3. Define an async fn callback execution */ + async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { + if let ServerMessage::Privmsg(msg) = message { + if detect_pyramid_complete_ok(bot.clone(), msg.clone()).await { + let _ = bot.client.say_in_reply_to(&msg, "Clap".to_string()).await ; + return Result::Ok("Success".to_string()) ; + } + } + Result::Err("Not Valid message type".to_string()) + } + + /* 4. Set and Store the execution body using `async_box()` */ + listener.set_exec_fn(asyncfn_box(execbody)); + + listener + +} + + +async fn detect_pyramid_complete_ok(_bot:Arc<Bot>,msg:PrivmsgMessage) -> bool { + + let msgtext = msg.message_text.replace("\u{e0000}","").trim().to_string(); + let msgchannel = msg.channel_login; + + // 1. Check if Pyramid started in chat > and recognize pyramid started + if !is_pyramid_started(msgchannel.clone()) & check_start_pyramid(msgchannel.clone(),msgtext.clone(),) { + set_pyramid_started(msgchannel.clone(),true); + push_to_compare(msgchannel.clone(),get_start_pattern(msgchannel.clone())); + + } + + if is_pyramid_started(msgchannel.clone()) { + push_to_compare(msgchannel.clone(),msgtext.clone()); + } + + // 2a. If Pyramid Not Started, Assume message is a potential start pattern + if !is_pyramid_started(msgchannel.clone()) { + set_start_pattern(msgchannel.clone(),msgtext.clone()); + } + + // 2b. If Pyramid is Started, and the latest message is the pattern, check for + // symmetry to determine pyramid + if is_pyramid_started(msgchannel.clone()) && msgtext.clone() == get_start_pattern(msgchannel.clone()) { + if symmetry_ok(msgchannel.clone()) { + return true; + } else { + return false ; + } + } else { + return false; + } + +} + + +lazy_static!{ + /// Message Compare stack per channel (channel:String,msgstack:Vec<String>) + pub static ref COMPARE_MSG_STACK_PER_CHNL: Mutex<Vec<(String,Mutex<Vec<String>>)>> = Mutex::new(vec![]); + #[derive(Debug)] + /// Pyramid Started per channel (channel:String,started:bool) + pub static ref PYRAMID_STARTED_PER_CHNL: Mutex<Vec<(String,Mutex<bool>)>> = Mutex::new(vec![]); + /// Start patterns per channel (channel:String,pattern:String) + pub static ref START_PATTERNS_PER_CHNL: Mutex<Vec<(String,Mutex<String>)>> = Mutex::new(vec![]); + /// temp message stack checker + pub static ref TEMP_MSG_STACK: Mutex<Vec<String>> = Mutex::new(vec![]); +} + +fn read_top_of_compare(channel:String) -> Option<String> { + + let comp_perchnl = COMPARE_MSG_STACK_PER_CHNL.lock().unwrap(); + + for rec in comp_perchnl.iter() { + if rec.0 == channel { + let msg_stack = rec.1.lock().unwrap(); + + return msg_stack.last().cloned(); + } + } + + None + +} + +fn pop_top_of_compare(channel:String) -> Option<String> { + let comp_perchnl = COMPARE_MSG_STACK_PER_CHNL.lock().unwrap(); + + for rec in comp_perchnl.iter() { + if rec.0 == channel { + let mut msg_stack = rec.1.lock().unwrap(); + + let popped = msg_stack.pop(); + return popped; + } + } + + None +} + +fn set_pyramid_started(channel:String,started:bool) { + let mut start_perchnl = PYRAMID_STARTED_PER_CHNL.lock().unwrap(); + let mut found = false; + for rec in start_perchnl.iter() { + if rec.0 == channel { + found = true; + let mut rec_started = rec.1.lock().unwrap(); + *rec_started = started; + } + } + if !found { + start_perchnl.push((channel,Mutex::new(started))); + } +} + +fn is_pyramid_started(channel:String) -> bool { + let start_perchnl = PYRAMID_STARTED_PER_CHNL.lock().unwrap(); + for rec in start_perchnl.iter() { + if rec.0 == channel { + let rec_started = rec.1.lock().unwrap(); + return *rec_started; + } + } + false +} + +fn set_start_pattern(channel:String,pattern:String) { + let mut start_patterns = START_PATTERNS_PER_CHNL.lock().unwrap(); + + let mut found = false; + for rec in start_patterns.iter() { + + if rec.0 == channel { + found = true; + let mut patternlock = rec.1.lock().unwrap(); + *patternlock = pattern.clone(); + } + + } + if !found { + start_patterns.push((channel.clone(),Mutex::new(pattern.clone()))); + } +} + + +fn get_start_pattern(channel:String) -> String { + let start_patterns = START_PATTERNS_PER_CHNL.lock().unwrap(); + + for rec in start_patterns.iter() { + + if rec.0 == channel { + let patternlock = rec.1.lock().unwrap(); + return patternlock.clone(); + } + } + + return "".to_string(); +} + + +/// pushes message to compare stack +fn push_to_compare(channel:String,message:String) { + let mut comp_perchnl = COMPARE_MSG_STACK_PER_CHNL.lock().unwrap(); + + let mut found = false; + for rec in comp_perchnl.iter() { + if rec.0 == channel { + found = true; + let mut msg_stack = rec.1.lock().unwrap(); + msg_stack.push(message.clone()); + // dbg!("Push message to cmp stack ; result last cmp_pchnl - ",comp_perchnl.last()); + } + } + if !found { + comp_perchnl.push((channel,Mutex::new(vec![message]))); + } + +} + + +/// checks latest and next latest messages for potential start +fn check_start_pyramid(channel:String,msgtext: String) -> bool { + msgtext == format!("{} {}",get_start_pattern(channel.clone()),get_start_pattern(channel.clone())) +} + + +/// pops the compare stack to determine symmetry +fn symmetry_ok(channel:String) -> bool { + let mut temp_stack = TEMP_MSG_STACK.lock().unwrap(); + let mut checking_started = false; + if !(read_top_of_compare(channel.clone()).unwrap_or("".to_string()) == get_start_pattern(channel.clone())) { + return false; + } + loop { + + if !checking_started && read_top_of_compare(channel.clone()).unwrap_or("".to_string()) == get_start_pattern(channel.clone()) { + checking_started = true; + } + if temp_stack.last().is_none() || read_top_of_compare(channel.clone()).unwrap_or("".to_string()).len() > temp_stack.last().unwrap_or(&"".to_string()).len() { + temp_stack.push(pop_top_of_compare(channel.clone()).unwrap_or("".to_string())); + + } else if temp_stack.last().is_some() && read_top_of_compare(channel.clone()).unwrap_or("".to_string()).len() < temp_stack.last().unwrap_or(&"".to_string()).len() { + + temp_stack.pop(); + if temp_stack.last().unwrap_or(&"".to_string()).clone() == read_top_of_compare(channel.clone()).unwrap_or("".to_string()) { + temp_stack.pop(); + + continue; + } else { + + temp_stack.clear(); + return false; + } + + } else { /* dbg!("failed catchall"); */ return false; } + if checking_started && read_top_of_compare(channel.clone()).unwrap() == get_start_pattern(channel.clone()) { + + temp_stack.clear(); + return true; + } + + } + + +} + diff --git a/src/lib.rs b/src/lib.rs index 3bb703a..cf5fdc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,10 +59,12 @@ //! use twitch_irc::message::ServerMessage; //! //! -//! /// Module with a loaded command +//! /// Module definition with a loaded command //! pub fn new() -> Module { //! /* 1. Create a new module */ -//! let mut custom_mod = Module::new("test".to_string(), "".to_string()); +//! let mut custom_mod = Module::new( +//! vec!["test".to_string()], +//! "".to_string()); //! //! /* 2. Load the cmd into a new module */ //! custom_mod.load_command(cmd_test()); @@ -71,9 +73,10 @@ //! //! } //! +//! /// Command definition //! pub fn cmd_test() -> Command { //! /* 1. Create a new cmd */ -//! let mut cmd = Command::new("test".to_string(),"".to_string()); +//! let mut cmd = Command::new(vec!["test".to_string()],"".to_string()); //! //! /* 2. Define exec callback */ //! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { @@ -83,7 +86,7 @@ //! } //! Result::Err("Not Valid message type".to_string()) //! } -//! +//! z //! /* 3. Set Command flags */ //! cmd.set_exec_fn(asyncfn_box(execbody)); //! cmd.set_admin_only(false);