diff --git a/Cargo.lock b/Cargo.lock index 18195c8..018b9dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -196,6 +196,7 @@ version = "0.1.0" dependencies = [ "async-trait", "casual_logger", + "chrono", "dotenv", "futures", "rand", diff --git a/Cargo.toml b/Cargo.toml index 87f8cfa..f4c7751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ rand = { version = "0.8.5", features = [] } futures = "0.3" async-trait = "0.1.77" casual_logger = "0.6.5" +chrono = "0.4.35" [lib] name = "bot_lib" diff --git a/src/core/botinstance.rs b/src/core/botinstance.rs index a668dac..c5b6dca 100644 --- a/src/core/botinstance.rs +++ b/src/core/botinstance.rs @@ -161,6 +161,18 @@ impl BotInstance { while let Some(message) = msglock.recv().await { + + botlog::trace( + format!( + "[TRACE][ServerMessage] > {:?}", + message + ) + .as_str(), + Some("BotInstance > runner()".to_string()), + None, + ); + + match message { ServerMessage::Notice(msg) => { botlog::notice( @@ -169,8 +181,10 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Privmsg(msg) => { + botlog::debug( format!( "[Twitch Chat > {}] > {}: {}", @@ -181,6 +195,18 @@ impl BotInstance { Some(&msg), ); + + botlog::trace( + format!( + "[TRACE][Twitch Chat > {}] > {}: {:?}", + msg.channel_login, msg.sender.name, msg + ) + .as_str(), + Some("BotInstance > runner()".to_string()), + Some(&msg), + ); + Log::flush(); + BotInstance::listener_main_prvmsg(Arc::clone(&bot), &msg).await; } ServerMessage::Whisper(msg) => { @@ -189,6 +215,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Join(msg) => { botlog::notice( @@ -196,6 +223,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Part(msg) => { botlog::notice( @@ -203,6 +231,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } _ => {} }; @@ -243,6 +272,41 @@ impl BotInstance { Some(msg), ); + + /* + [ ] What we should do instead is : + 1. Check if the message is related to a Reply (so we know how many arguments we should skip) + 2. If a reply, skip the first argument + */ + + let mut msgiter= msg + .message_text + .split(' '); + + let arg1 = msgiter.next(); + let arg2 = msgiter.next(); + + let reply = if let Some(Some(replyid)) = msg.source.tags.0.get("reply-thread-parent-msg-id") { + Some(replyid) + } else { None } + ; + + + let inpt = match reply { + None => { // Regular message, use the first arg as the command + match arg1 { + None => return, // return if no argument found + Some(a) => a, + } + }, + Some(_) => { + match arg2 { // A reply message, use the 2nd arg as the command + None => return, // return if no argument found + Some(a) => a, + } + }, + }; + for acts in (*actsdblock).values() { for a in acts { match a { @@ -263,11 +327,7 @@ impl BotInstance { Some(msg), ); - let inpt = msg - .message_text - .split(' ') - .next() - .expect("ERROR during BotCommand"); + // [x] Check if a bot command based on ... // [x] prefix + command diff --git a/src/core/chat.rs b/src/core/chat.rs index 717b485..1ba85e7 100644 --- a/src/core/chat.rs +++ b/src/core/chat.rs @@ -27,6 +27,14 @@ pub struct Chat { pub client: TwitchIRCClient, StaticLoginCredentials>, } + +#[derive(Clone)] +enum BotMsgType<'a> { + SayInReplyTo(&'a PrivmsgMessage,String), + Say(String,String), +} + + impl Chat { pub fn init( ratelimiters: HashMap, @@ -43,8 +51,11 @@ impl Chat { self.ratelimiters.lock().await.insert(chnl, n); } - pub async fn say_in_reply_to(&self, msg: &PrivmsgMessage, mut outmsg: String) { - /* + + + + async fn send_botmsg(&self, msginput: BotMsgType<'_>) { + /* formats message before sending to TwitchIRC - [x] Custom String Formatting (e.g., adding random black spaces) @@ -53,12 +64,38 @@ impl Chat { */ + let (channel_login,mut outmsg) = match msginput.clone() { + BotMsgType::SayInReplyTo(msg, outmsg) => { + (msg.channel_login.clone(),outmsg) + }, + BotMsgType::Say(a,b ) => { + (a.clone(),b.clone()) + }, + }; + + if self.client.get_channel_status(channel_login.clone()).await == (false,false) { + // in the case where the provided channel isn't something we're known to be connected to + + botlog::warn( + &format!("A message attempted to send for a Non-Joined Channel : {}",channel_login.clone()), + Some("Chat > send_botmsg".to_string()), + None, + ); + return ; + } + let rl = Arc::clone(&self.ratelimiters); let mut rllock = rl.lock().await; + botlog::debug( + &format!("Ratelimiter being checked for channel : {}",channel_login.clone()), + Some("Chat > send_botmsg".to_string()), + None, + ); + let contextratelimiter = rllock // .get_mut() - .get_mut(&Channel(String::from(&msg.channel_login))) + .get_mut(&Channel(channel_login.to_lowercase().clone())) .expect("ERROR: Issue with Rate limiters"); // Continue to check the limiter and sleep if required if the minimum is not reached @@ -75,20 +112,38 @@ impl Chat { outmsg.push_str(blankspace); } - self.client.say_in_reply_to(msg, outmsg).await.unwrap(); - + match msginput.clone() { + BotMsgType::SayInReplyTo(msg, _) => { + self.client.say_in_reply_to(msg, outmsg).await.unwrap(); + }, + BotMsgType::Say(a, _) => { + self.client.say(a, outmsg).await.unwrap(); + } + } + contextratelimiter.increment_counter(); let logstr = format!( "(#{}) > {} ; contextratelimiter : {:?}", - msg.channel_login, "rate limit counter increase", contextratelimiter + channel_login.clone(), "rate limit counter increase", contextratelimiter ); - botlog::trace( - logstr.as_str(), - Some("Chat > say_in_reply_to".to_string()), - Some(msg), - ); + if let BotMsgType::SayInReplyTo(msg,_ ) = msginput { + botlog::trace( + logstr.as_str(), + Some("Chat > send_botmsg".to_string()), + Some(msg), + ); + } else { + botlog::trace( + logstr.as_str(), + Some("Chat > send_botmsg".to_string()), + None, + ); + } + + + } ratelimiter::LimiterResp::Skip => { // (); // do nothing otherwise @@ -98,13 +153,22 @@ impl Chat { } } - Log::flush(); + + Log::flush(); } - async fn _say(&self, _: String, _: String) { + + + pub async fn say_in_reply_to(&self, msg: &PrivmsgMessage, outmsg: String) { + + self.send_botmsg(BotMsgType::SayInReplyTo(msg, outmsg)).await; + + } + + pub async fn say(&self, channel_login: String, message: String) { // more info https://docs.rs/twitch-irc/latest/twitch_irc/client/struct.TwitchIRCClient.html#method.say - // self.client.say(msg,outmsg).await.unwrap(); + self.send_botmsg(BotMsgType::Say(channel_login.to_lowercase(), message)).await; } async fn _me(&self, _: String, _: String) { diff --git a/src/custom.rs b/src/custom.rs index fc802e6..6107797 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -1,5 +1,5 @@ /* - `modules` will : + `custom` will : - be a starting refrence point for the bot instance to pull module definitions for */ @@ -11,7 +11,8 @@ pub use crate::core::botmodules::ModulesManager; // [ ] Load submodules -mod experiments; +// mod experiments; +mod experimental; // [ ] init() function that accepts bot instance - this is passed to init() on submodules @@ -19,5 +20,6 @@ pub async fn init(mgr: Arc) { // Modules initializer loads modules into the bot // this is achieved by calling submodules that also have fn init() defined - experiments::init(mgr).await + // experiments::init(mgr).await + experimental::init(mgr).await; } diff --git a/src/custom/experimental.rs b/src/custom/experimental.rs new file mode 100644 index 0000000..409abd1 --- /dev/null +++ b/src/custom/experimental.rs @@ -0,0 +1,24 @@ +/* + `experimental` will : + - be for mostly experimental +*/ + +use std::sync::Arc; + +// pub use crate::core::botinstance::BotInstance; +pub use crate::core::botmodules::ModulesManager; + +// [ ] Load submodules + +mod experiment001; +mod experiment002; + +// [ ] init() function that accepts bot instance - this is passed to init() on submodules + +pub async fn init(mgr: Arc) { + // Modules initializer loads modules into the bot + // this is achieved by calling submodules that also have fn init() defined + + experiment001::init(Arc::clone(&mgr)).await; + experiment002::init(Arc::clone(&mgr)).await; +} diff --git a/src/custom/experiments.rs b/src/custom/experimental/experiment001.rs similarity index 100% rename from src/custom/experiments.rs rename to src/custom/experimental/experiment001.rs diff --git a/src/custom/experimental/experiment002.rs b/src/custom/experimental/experiment002.rs new file mode 100644 index 0000000..2a97b30 --- /dev/null +++ b/src/custom/experimental/experiment002.rs @@ -0,0 +1,228 @@ +/* + Custom Modules - + + Usage : + [ ] within the file's init(), define BotActions & Load them into the ModulesManager + [ ] Define Execution Bodies for these BotActions + [ ] Afterwards, add the following to parent modules.rs file + - mod ; + - within init(), ::init(mgr).await + +*/ + +// use rand::Rng; +use std::sync::Arc; + +use chrono::{TimeZone,Local}; + +use twitch_irc::message::PrivmsgMessage; + +// use crate::core::botinstance::ChType::Channel; +use crate::core::botinstance::Channel; +// use ChType::Channel; +use crate::core::botlog; + +use casual_logger::Log; + +use crate::core::bot_actions::actions_util::{self, BotAR}; +use crate::core::botmodules::{BotActionTrait, BotCommand, BotModule, ModulesManager}; + +use crate::core::identity::UserRole::*; + +// use tokio::time::{sleep, Duration}; + +pub async fn init(mgr: Arc) { + + const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("experiments002")), + command: String::from("say"), // command call name + alias: vec![ + "s".to_string(), + ], // String of alternative names + exec_body: actions_util::asyncbox(sayout), + help: String::from("Test Command tester"), + required_roles: vec![ + BotAdmin, + Mod(OF_CMD_CHANNEL), + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_to_modmgr(Arc::clone(&mgr)).await; + + mgr.set_instance_enabled(BotModule(String::from("experiments002"))).await; + + +} + + +async fn sayout(bot: BotAR, msg: PrivmsgMessage) { + + /* + usage : + + */ + + let reply_parent = if let Some(Some(reply)) = msg.source.tags.0.get("reply-parent-msg-body") { + Some(reply) + } else { None } + ; + + + // let reply_parent_usr = if let Some(Some(reply)) = msg.source.tags.0.get("reply-thread-parent-user-login") { + // Some(reply) + // } else { None } + // ; + + let reply_parent_ts = if let Some(Some(replyts)) = msg.source.tags.0.get("tmi-sent-ts") { + + let a: i64 = replyts.parse().unwrap(); + let b = Local.timestamp_millis_opt(a).unwrap(); + // println!("Output : {}",b.to_string()); + // println!("Formatted : {}",b.format("%m-%d %H:%M") ); + Some(b.format("%m-%d %H:%M")) + } else { None } + ; + + // [x] Unwraps arguments from message + + + let argrslt = + if let Some((_,str1)) = msg.message_text.split_once(' ') { + if reply_parent.is_none() { + if let Some((channelstr,msgstr)) = str1.split_once(' ') { + Some((channelstr,msgstr)) + } + else { None } + } else if let Some((_,str2)) = str1.split_once(' ') { + if let Some((channelstr,msgstr)) = str2.split_once(' ') { + Some((channelstr,msgstr)) + } + else { None } + } else { None } + } + else { None }; + + + + + match argrslt { + Some((trgchnl,outmsg)) => { + + let bot = Arc::clone(&bot); + + let botlock = bot.read().await; + + // [x] Validate first if trgchnl exists + + botlog::trace( + &format!("[TRACE] Evaluated status of {} : {:?}", + trgchnl.to_string().clone(),botlock.botmgrs.chat.client.get_channel_status(trgchnl.to_string().clone()).await), + Some("Chat > send_botmsg".to_string()), + None, + ); + + // if botlock.botmgrs.chat.client.get_channel_status(trgchnl.to_string().clone()).await == (false,false) { + if !botlock.bot_channels.contains(&Channel(trgchnl.to_lowercase().to_string().clone())) { + + // in the case where the provided channel isn't something we're known to be connected to + botlog::warn( + &format!("A message attempted to send for a Non-Joined Channel : {}",trgchnl.to_string().clone()), + Some("Chat > send_botmsg".to_string()), + None, + ); + // return ; + + botlock + .botmgrs + .chat + .say_in_reply_to(&msg, format!("Not a Joined Channel : {}",trgchnl)) + .await; + + + } + + /* + 1. If a Reply , + [ ] Get Parent Content message - reply_parent + [ ] Get Parent Chatter - reply_parent_usr + [ ] Get Parent Channel - msg.channel_login + -> Share this first then + [ ] Get Reply Message (that triggered bot command) - msgstr + [ ] Get Reply Sender - msg.sender.name + [ ] Get Target Channel - trgchnl + + 2. If not a reply + [ ] Get Reply Message (that triggered bot command) - msgstr + [ ] Get Reply Sender - msg.sender.name + [ ] Get Target Channel - trgchnl + */ + + // reply_parent_ts + + let newoutmsg = if let Some(srcmsg) = reply_parent { + + // format!("{} from #{} says {} . Replying to: {} : {}", + // msg.sender.name,msg.channel_login,outmsg, reply_parent_usr.unwrap(),srcmsg) + // format!("{} from #{} says {} @ {} {} : {}", + // msg.sender.name, + // msg.channel_login, + // outmsg, + // reply_parent_ts.unwrap(), + // reply_parent_usr.unwrap(), + // srcmsg) + format!("{} {} @ {} : {}", + reply_parent_ts.unwrap(), + msg.sender.name, + msg.channel_login, + srcmsg) + } else { + // format!("{} from #{} says : {}", + // msg.sender.name, + // msg.channel_login, + // outmsg) + format!("in {} - {} : {}", + msg.channel_login, + msg.sender.name, + outmsg) + }; + + // uses chat.say_in_reply_to() for the bot controls for messages + botlock + .botmgrs + .chat + .say(trgchnl.to_string(), newoutmsg.to_string()) + .await; + + + + }, + None => { + botlog::debug( + "sayout had issues trying to parse arguments", + Some("experiment002 > sayout".to_string()), + Some(&msg), + ); + + let bot = Arc::clone(&bot); + + let botlock = bot.read().await; + + // uses chat.say_in_reply_to() for the bot controls for messages + botlock + .botmgrs + .chat + .say_in_reply_to(&msg, String::from("Invalid arguments")) + .await; + + }, + + } + + + Log::flush(); +} \ No newline at end of file