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..e770a80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -63,9 +63,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "shlex", ] @@ -142,10 +142,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "forcebot-rs-v2" +name = "forcebot_core" version = "0.1.0" dependencies = [ "dotenv", + "lazy_static", "tokio", "twitch-irc", ] @@ -199,13 +200,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -214,6 +216,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" @@ -264,15 +272,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "moderator_reactor" +version = "0.1.0" +dependencies = [ + "dotenv", + "forcebot_core", + "lazy_static", + "tokio", + "twitch-irc", +] + [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -285,6 +304,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_empty_bot" +version = "0.1.0" +dependencies = [ + "dotenv", + "forcebot_core", + "lazy_static", + "tokio", + "twitch-irc", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -311,9 +341,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" dependencies = [ "bitflags", "cfg-if", @@ -337,9 +367,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -429,9 +459,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", @@ -493,6 +523,39 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_command_bot" +version = "0.1.0" +dependencies = [ + "dotenv", + "forcebot_core", + "lazy_static", + "tokio", + "twitch-irc", +] + +[[package]] +name = "simple_debug_listener" +version = "0.1.0" +dependencies = [ + "dotenv", + "forcebot_core", + "lazy_static", + "tokio", + "twitch-irc", +] + +[[package]] +name = "simple_module_example" +version = "0.1.0" +dependencies = [ + "dotenv", + "forcebot_core", + "lazy_static", + "tokio", + "twitch-irc", +] + [[package]] name = "slab" version = "0.4.9" @@ -520,9 +583,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -531,9 +594,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", @@ -680,9 +743,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "vcpkg" @@ -696,6 +759,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -777,3 +849,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml index 53faa8d..7b027e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,7 @@ -[package] -name = "forcebot-rs-v2" -version = "0.1.0" -edition = "2021" -default-run = "forcebot-rs-v2" - -[dependencies] -dotenv = "0.15.0" -tokio = { version = "1.33.0", features = ["full"] } -twitch-irc = "5.0.1" - -# [[bin]] -# name = "simple_bot" -# path = "src/simple_bot.rs" - -# [[bin]] -# name = "simple_bot_listener" -# path = "src/simple_bot_listener.rs" - -# [lib] -# name = "botlib" -# path = "src/lib.rs" \ No newline at end of file +[workspace] +members = [ + "forcebot_core", + "simple_module_example", + "new_empty_bot", + "simple_debug_listener", + "moderator_reactor", "simple_command_bot"] diff --git a/forcebot_core/Cargo.toml b/forcebot_core/Cargo.toml new file mode 100644 index 0000000..70ac147 --- /dev/null +++ b/forcebot_core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "forcebot_core" +version = "0.1.0" +edition = "2021" +default-run = "fun_bot" + +[dependencies] +# async-recursion = "1.1.1" /* has issues when used */ +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/forcebot_core/src/bin/fun_bot.rs b/forcebot_core/src/bin/fun_bot.rs new file mode 100644 index 0000000..b506faf --- /dev/null +++ b/forcebot_core/src/bin/fun_bot.rs @@ -0,0 +1,87 @@ +//! WIP Fun forcebot with catered customizations #todo +//! +//! Custom modules that can be managed in chat through `disable` and `enable` commands +//! - `besty` - uses a custom prefix tp trigger +//! - `guests` +//! - `pyramid` +//! - `quiet` +//! +//! +//! Be sure the followig is defined in `.env` +//! - login_name +//! - access_token +//! - bot_channels +//! - prefix +//! - bot_admins +//! +//! Bot access tokens be generated here - +//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> +//! - More Info - <https://dev.twitch.tv/docs/authentication> + +use forcebot_core::{ + custom_mods::{debug, guest_badge, pyramid}, + Bot, +}; + +#[tokio::main] +pub async fn main() { + /* Create the bot using env */ + let bot = Bot::new().await; + + /* 1. Load the module into the bot */ + bot.load_module(funbot_objs::create_module()).await; + bot.load_module(guest_badge::create_module()).await; + bot.load_module(pyramid::create_module()).await; + bot.load_module(debug::create_module()).await; + + /* 3. Run the bot */ + bot.run().await; +} + +pub mod funbot_objs { + use std::sync::Arc; + + use forcebot_core::{execution_async, Badge, Bot, Command, Module}; + use twitch_irc::message::ServerMessage; + + /// Create a Module with a loaded Command object + pub fn create_module() -> Module { + let mut custom_mod = Module::new( + vec!["besty".to_string()], + "Now Aware of besty xdd666 ".to_string(), + ); + + custom_mod.load_command(create_cmd_test()); + // custom_mod.set_status_by_default(Status::Disabled); + + custom_mod + } + + /// Create a Command Object + fn create_cmd_test() -> Command { + let mut cmd = Command::new( + vec!["remind besty".to_string()], + "annytfYandere ".to_string(), + ); + + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, "annytfYandere he's mine".to_string()) + .await; + return Result::Ok("Success".to_string()); + } + Result::Err("Not Valid message type".to_string()) + } + + cmd.set_exec_fn(execution_async(execbody)); + + cmd.set_admin_only(false); + cmd.set_min_badge(Badge::Vip); + + cmd + } +} diff --git a/src/bin/new_bot.rs b/forcebot_core/src/bin/new_bot.rs similarity index 73% rename from src/bin/new_bot.rs rename to forcebot_core/src/bin/new_bot.rs index cbe682b..856f6b1 100644 --- a/src/bin/new_bot.rs +++ b/forcebot_core/src/bin/new_bot.rs @@ -1,24 +1,22 @@ //! Example simple Binary crate that creates & runs bot based on `.env` -//! Be sure the followig is defined in `.env` +//! Be sure the followig is defined in `.env` //! - login_name //! - access_token //! - bot_channels //! - prefix //! - bot_admins -//! -//! Bot access tokens be generated here - +//! +//! Bot access tokens be generated here - //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> -use forcebot_rs_v2::Bot; +use forcebot_core::Bot; #[tokio::main] pub async fn main() { - /* 1. Create the bot using env */ - let bot = Bot::new(); + let bot = Bot::new().await; /* 2. Run the bot */ bot.run().await; - } diff --git a/src/bin/simple_module.rs b/forcebot_core/src/bin/simple_module.rs similarity index 52% rename from src/bin/simple_module.rs rename to forcebot_core/src/bin/simple_module.rs index 61d66e5..477da78 100644 --- a/src/bin/simple_module.rs +++ b/forcebot_core/src/bin/simple_module.rs @@ -1,76 +1,76 @@ //! Simple Module with a Command -//! -//! Adding objects through packages provides controls , +//! +//! Adding objects through packages provides controls , //! such as moderators, and brodcasters can disable or enable mods -//! -//! Here, moderators or above can enable or disable the `test` +//! +//! Here, moderators or above can enable or disable the `test` //! module with the command `<prefix> disable test` -//! -//! Be sure the followig is defined in `.env` +//! +//! Be sure the followig is defined in `.env` //! - login_name //! - access_token //! - bot_channels //! - prefix //! - bot_admins -//! -//! Bot access tokens be generated here - +//! +//! Bot access tokens be generated here - //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> -use forcebot_rs_v2::Bot; +use forcebot_core::Bot; #[tokio::main] pub async fn main() { - /* Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* load the Module */ - bot.load_module(custom_mod::new()); + bot.load_module(custom_mod::new()).await; /* Run the bot */ bot.run().await; - } - pub mod custom_mod { use std::sync::Arc; - use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module}; + use forcebot_core::{execution_async, 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()); 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> { + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { if let ServerMessage::Privmsg(msg) = message { - let _= bot.client.say_in_reply_to( - &msg, "test return".to_string()).await; + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, "test return".to_string()) + .await; } - Result::Err("Not Valid message type".to_string()) + Result::Err("Not Valid message type".to_string()) } /* 3. Set Command flags */ - cmd.set_exec_fn(asyncfn_box(execbody)); + cmd.set_exec_fn(execution_async(execbody)); cmd.set_admin_only(false); cmd.set_min_badge(Badge::Moderator); cmd - } -} \ No newline at end of file + } +} diff --git a/forcebot_core/src/botcore.rs b/forcebot_core/src/botcore.rs new file mode 100644 index 0000000..a750843 --- /dev/null +++ b/forcebot_core/src/botcore.rs @@ -0,0 +1,5 @@ +pub mod bot; +pub mod bot_objects; +pub mod built_in_mods; +pub mod chat; +pub mod modules; diff --git a/forcebot_core/src/botcore/bot.rs b/forcebot_core/src/botcore/bot.rs new file mode 100644 index 0000000..f3774a1 --- /dev/null +++ b/forcebot_core/src/botcore/bot.rs @@ -0,0 +1,482 @@ +// use async_recursion::async_recursion; +use dotenv::dotenv; +use std::{ + env, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; +use tokio::sync::{mpsc::UnboundedReceiver, Mutex}; +use twitch_irc::{ + login::StaticLoginCredentials, + message::{PrivmsgMessage, ServerMessage}, + SecureTCPTransport, TwitchIRCClient, +}; + +// use crate::{Badge, Command, Listener, Module}; +use super::{bot_objects::command::Command, built_in_mods, chat::Chat}; + +use super::super::botcore::modules::Module; +use crate::botcore::bot_objects::listener::Listener; +use crate::botcore::{bot_objects::Badge, chat}; +// use super:: + +use super::{ + bot_objects::built_in_objects, + modules::{self, Status}, +}; + +/// Twitch chat bot +pub struct Bot { + /// Prefix for commands + prefix: String, + /// inbound chat msg stream + incoming_msgs: Mutex<UnboundedReceiver<ServerMessage>>, + /// outbound chat client msg stream + pub client: TwitchIRCClient<SecureTCPTransport, StaticLoginCredentials>, + /// *preferred* bot enforced outbound chat client msg stream + pub chat: Mutex<Chat>, + /// joined channels + botchannels: Vec<String>, + /// admin chatters + admins: Vec<String>, + /// listeners + listeners: Mutex<Vec<Listener>>, + /// commands + commands: Mutex<Vec<Command>>, + /// modules + modules: RwLock<Vec<Module>>, + /// channel module status + channel_module_status: RwLock<Vec<(String, String, modules::Status)>>, + /// chatter guest badges - chatter,channel,Badge,start_time,duration + chatter_guest_badges: Mutex<Vec<(String, String, Badge, Instant, Duration)>>, + /// Message cache + message_cache: Mutex<Vec<PrivmsgMessage>>, + // /// channel_quiet + // channel_quiet_yn: RwLock<Vec<(String,RwLock<bool>)>>, +} + +impl Bot { + /// Creates a new `Bot` using env variables + /// + /// Be sure the following is defined in an `.env` file + /// - login_name + /// - access_token + /// - bot_channels + /// - prefix + /// - bot_admins + // #[async_recursion] + pub async fn new() -> Arc<Bot> { + dotenv().ok(); + let bot_login_name = env::var("login_name").unwrap().to_owned(); + let oauth_token = env::var("access_token").unwrap().to_owned(); + let prefix = env::var("prefix").unwrap().to_owned(); + + let mut botchannels = Vec::new(); + + for chnl in env::var("bot_channels").unwrap().split(',') { + botchannels.push(chnl.to_owned()); + } + + Bot::new_from(bot_login_name, oauth_token, prefix, botchannels).await + } + + /// Creates a new `Bot` using bot information + /// + /// Bot will join `botchannels` argument + pub async fn new_from( + bot_login_name: String, + oauth_token: String, + prefix: String, + botchannels: Vec<String>, + ) -> Arc<Bot> { + dotenv().ok(); + let bot_login_name = bot_login_name; + + let config = twitch_irc::ClientConfig::new_simple(StaticLoginCredentials::new( + bot_login_name.to_owned(), + Some(oauth_token.to_owned()), + )); + + let (incoming_messages, client) = + TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config); + + let mut botchannels_all = Vec::new(); + botchannels_all.extend(botchannels); + + let mut admins = Vec::new(); + + if let Ok(value) = env::var("bot_admins") { + for admin in value.split(',') { + admins.push(String::from(admin)) + } + } + + let bot = Bot { + prefix, + incoming_msgs: Mutex::new(incoming_messages), + client: client.clone(), + chat: Mutex::new(Chat::new(client).await), + botchannels: botchannels_all, + listeners: Mutex::new(vec![]), + commands: Mutex::new(vec![]), + admins, + modules: RwLock::new(vec![]), + channel_module_status: RwLock::new(vec![]), + chatter_guest_badges: Mutex::new(vec![]), + message_cache: Mutex::new(vec![]), + // channel_quiet_yn : RwLock::new(vec![]), + }; + + async fn load_modules(bot: Bot) -> Bot { + // let mut bot1 = bot; + + // bot1.chat = Some(Chat::new(client, bot1)); + + for cmd in built_in_objects::create_commands() { + bot.load_command(cmd).await; + } + built_in_mods::load_built_in_mods(&bot).await; + + bot + } + + let bot = load_modules(bot).await; + + let bot = Arc::new(bot); + + // let lock = bot.chat.lock().await; + + // *lock = Some(Chat::new(chat, bot.clone())); + + // let cht = Chat::new(chat).await; + + // bot.chat.lock() + + // lock.set_parent_bot(bot.clone()); + + println!("Joined - {:?}", bot.botchannels); + + bot.clone() + } + + /// Runs the bot + pub async fn run(self: Arc<Self>) { + for chnl in &self.botchannels { + self.client.join(chnl.to_owned()).unwrap(); + } + + // let bot = Arc::new(self); + let bot = self; + + let join_handle = tokio::spawn(async move { + let a = bot.clone(); + let mut in_msgs_lock = a.incoming_msgs.lock().await; + + while let Some(message) = in_msgs_lock.recv().await { + // dbg!(message.clone()) ; + + let bot_listener_lock = bot.listeners.lock().await; + for listener in bot_listener_lock.iter() { + let a = listener.clone(); + if a.cond_triggered(bot.clone(), message.clone()).await { + let _ = listener.execute_fn(bot.clone(), message.clone()).await; + } + } + + 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 cmd_lock = bot.commands.lock().await; + for cmd in cmd_lock.iter() { + let a = cmd.clone(); + if a.command_triggered(bot.clone(), msg.clone()).await { + let _ = cmd.execute_fn(bot.clone(), message.clone()).await; + } + } + + fn get_enabled_channel_modules(bot: Arc<Bot>, channel: String) -> Vec<Module> { + let botmodules_lock = bot.modules.read().unwrap(); + let botmodules_cpy = botmodules_lock.clone(); + drop(botmodules_lock); + + let mut enabled_mods = Vec::new(); + + 'module_loop: for module in &*botmodules_cpy { + // dbg!("try cms read"); + let cms_lock = bot.channel_module_status.read().unwrap(); + + for channel_flags in cms_lock.iter() { + if channel_flags.0 == channel { + if module.get_names().contains(&channel_flags.1) + && channel_flags.2 == Status::Disabled + { + continue 'module_loop; + } + } + } + enabled_mods.push(module.clone()); + } + + enabled_mods + } + + for module in + get_enabled_channel_modules(bot.clone(), msg.clone().channel_login) + { + for listener in module.get_listeners() { + let a = listener.clone(); + if a.cond_triggered(bot.clone(), message.clone()).await { + let _ = listener.execute_fn(bot.clone(), message.clone()).await; + } + } + for cmd in module.get_commands() { + let a = cmd.clone(); + if a.command_triggered(bot.clone(), msg.clone()).await { + let _ = cmd.execute_fn(bot.clone(), message.clone()).await; + } + } + } + } else { + }; + } + drop(in_msgs_lock); + }); + + join_handle.await.unwrap(); + } + + /// Loads a `Listener` into the bot + pub async fn load_listener(&self, l: Listener) { + let a = Arc::new(self); + let mut listlock = a.listeners.lock().await; + listlock.push(l); + } + + /// Loads a `Command` into the bot + pub async fn load_command(&self, c: Command) { + let a = Arc::new(self); + let mut cmdlock = a.commands.lock().await; + cmdlock.push(c); + } + + pub async fn get_module(&self, module: String) -> Option<Module> { + let modlock = self.modules.read().unwrap(); + for modl in modlock.iter() { + if modl.get_names().contains(&module) { + return Some(modl.clone()); + } + } + None + } + + pub fn get_prefix(&self) -> String { + self.prefix.clone() + } + + pub fn get_admins(&self) -> Vec<String> { + self.admins.clone() + } + + /// loads a `Module` and its bot objects + pub async fn load_module(&self, m: Module) { + // dbg!("load module - start",m.get_names().first().unwrap()); + let bot = Arc::new(self); + // let bot_lock = bot.lock().await; + // dbg!("bot arc"); + if m.get_status_by_default() == Status::Disabled { + // dbg!("module fund disabled by default"); + // dbg!("inner if"); + for (_index, chnl) in bot.botchannels.iter().enumerate() { + // dbg!("iter - ",index); + bot.disable_module(chnl.clone(), m.get_names().first().unwrap().clone()) + .await + } + } + // dbg!("aftee disable check"); + // dbg!(bot.modules); + let mut botmods = bot.modules.write().unwrap(); + // dbg!(m); + // dbg!("loading module ",m.get_names()); + botmods.push(m); + } + + pub async fn get_channel_module_status(&self, channel: String, module: String) -> Status { + // dbg!("get channel module status"); + let found_disabled: bool = { + // dbg!("try cms read"); + let cms_lock = self.channel_module_status.read().unwrap(); + // dbg!("cms read lock"); + // dbg!(module.clone()); + let mut found = false; + + for channel_flags in cms_lock.iter() { + if channel_flags.0 == channel { + if channel_flags.1 == module && channel_flags.2 == Status::Disabled { + found = true; + } + } + } + found + }; + + let module_loaded: bool = { + let mut loaded_yn = false; + + for loaded_m in self.modules.read().unwrap().iter() { + if loaded_m.get_names().contains(&module) { + loaded_yn = true; + } + } + + loaded_yn + }; + + if found_disabled { + return Status::Disabled; + } else if !module_loaded { + return Status::NotLoaded; + } else { + return Status::Enabled; + }; + } + + pub async fn disable_module(&self, channel: String, module: String) { + // dbg!("disable module called",channel.clone(),module.clone()); + + let found_disabled: bool = { + // dbg!("finding disabled mod"); + // dbg!("try cms read"); + let cms_lock = self.channel_module_status.read().unwrap(); + // dbg!("cms read lock"); + // dbg!(module.clone()); + + let mut found = false; + + for channel_flags in cms_lock.iter() { + if channel_flags.0 == channel { + if channel_flags.1 == module && channel_flags.2 == Status::Disabled { + found = true; + } + } + } + drop(cms_lock); + found + }; + + if !found_disabled { + let mut cms_lock = self.channel_module_status.write().unwrap(); + + cms_lock.push((channel, module.clone(), Status::Disabled)); + + drop(cms_lock); + } + } + + pub async fn enable_module(&self, channel: String, module: String) { + // dbg!("enable module called",channel.clone(),module.clone()); + // dbg!("try cms write"); + let mut lock = self.channel_module_status.write().unwrap(); + // dbg!("cms write lock"); + // dbg!(module.clone()); + while lock.contains(&(channel.clone(), module.clone(), Status::Disabled)) { + let index = lock + .iter() + .position(|x| *x == (channel.clone(), module.clone(), Status::Disabled)) + .unwrap(); + lock.remove(index); + } + drop(lock); + } + + pub async fn get_channel_guest_badges( + &self, + chatter: String, + channel: String, + ) -> Vec<(Badge, Instant, Duration)> { + let bot = Arc::new(self); + let guest_badges_lock = bot.chatter_guest_badges.lock().await; + + let mut badges = vec![]; + for temp_badge in guest_badges_lock.iter() { + if temp_badge.0 == chatter + && temp_badge.1 == channel + && temp_badge.3 + temp_badge.4 > Instant::now() + { + badges.push((temp_badge.2.clone(), temp_badge.3, temp_badge.4)); + } + } + + badges + } + + pub async fn issue_new_guest_badge( + &self, + chatter: String, + channel: String, + badge: Badge, + start: Instant, + dur: Duration, + ) { + let bot = Arc::new(self); + let mut guest_badges_lock = bot.chatter_guest_badges.lock().await; + + guest_badges_lock.push((chatter, channel, badge, start, dur)); + } + + pub fn get_message_cache(&self) -> &Mutex<Vec<PrivmsgMessage>> { + &self.message_cache + } + + /// get message cache newest to oldest for a channel + pub async fn get_message_cache_per_channel(&self, channel: String) -> Vec<PrivmsgMessage> { + let cache = self.message_cache.lock().await; + let mut rslt = vec![]; + for a in cache + .iter() + .rev() + .filter(|x| x.channel_login == channel) + .into_iter() + { + rslt.push(a.clone()); + } + rslt + } + + // /// Get the quiet status of a channel + // pub fn get_channel_quiet(&self,channel:String) -> bool { + // for a in self.channel_quiet_yn.read().unwrap().iter() { + // if a.0 == channel { + // return a.1.read().unwrap().clone(); + // } + // } + // return false; + // } + + // /// Get the quiet status of a channel + // pub fn set_channel_quiet(&self,channel:String,quiet_on:bool) { + // let mut found = false; + + // let chnlquiet = self.channel_quiet_yn.read().unwrap(); + // for rec in chnlquiet.iter() { + // if rec.0 == channel { + // found = true; + // let mut status = rec.1.write().unwrap(); + // *status = quiet_on; + // drop(status); + // } + // } + // drop(chnlquiet); + + // if !found { + // // dbg!("set chn quiet > !found channel quiet status"); + // let mut chnlquiet = self.channel_quiet_yn.write().unwrap(); + // chnlquiet.push((channel,RwLock::new(quiet_on))); + // drop(chnlquiet); + // } + + // } +} diff --git a/forcebot_core/src/botcore/bot_objects.rs b/forcebot_core/src/botcore/bot_objects.rs new file mode 100644 index 0000000..77de39c --- /dev/null +++ b/forcebot_core/src/botcore/bot_objects.rs @@ -0,0 +1,399 @@ +pub mod command; +pub mod listener; + +use std::boxed::Box; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use twitch_irc::message::{PrivmsgMessage, ServerMessage}; + +use super::bot::Bot; + +/// chat badge +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Badge { + Moderator, + Broadcaster, + Vip, +} + +pub type ExecBody = Box< + dyn Fn(Arc<Bot>, ServerMessage) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> + + Send + + Sync, +>; + +/// used to store async execution functions. Primarily used for `Command` +/// +/// call this to store execution functions in `Commands` +/// +/// # example +/// ``` +/// /* 2. Define exec callback */ +/// async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { +/// /* do smth */ +/// } +/// +/// /* 3. Set Command flags */ +/// cmd.set_exec_fn(execution_async(execbody)); +/// ``` +/// +pub fn execution_async<T>(f: fn(Arc<Bot>, ServerMessage) -> T) -> ExecBody +where + T: Future<Output = Result<String, String>> + Send + 'static, +{ + Box::new(move |a, b| Box::pin(f(a, b))) +} + +pub type CommandTrigger = Box< + dyn Fn(Arc<Bot>, PrivmsgMessage) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send + Sync, +>; + +/// used to store async trigger condition callback functions. Primarily used for `Command` +/// +/// # example +/// ``` +/// /* 2. Define condition callback */ +/// async fn condition01(bot:Arc<Bot>,message:ServerMessage) -> bool { +/// /* do smth */ +/// } +/// +/// /* 3. Set Command flags */ +/// cmd.set_custom_cond_async(command_condition_async(condition01)); +/// ``` +/// +pub fn command_condition_async<T>(f: fn(Arc<Bot>, PrivmsgMessage) -> T) -> CommandTrigger +where + T: Future<Output = bool> + Send + 'static, +{ + Box::new(move |a, b| Box::pin(f(a, b))) +} + +pub type ListenerTrigger = Box< + dyn Fn(Arc<Bot>, ServerMessage) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send + Sync, +>; + +/// used to store async trigger condition callback functions. Primarily used for `Listener` +/// +/// # example +/// ``` +/// /* 2. Define condition callback */ +/// async fn condition01(bot:Arc<Bot>,message:ServerMessage) -> bool { +/// /* do smth */ +/// } +/// +/// /* 3. Set Command flags */ +/// cmd.set_custom_cond_async(listener_condition_async(condition01)); +/// ``` +/// +pub fn listener_condition_async<T>(f: fn(Arc<Bot>, ServerMessage) -> T) -> ListenerTrigger +where + T: Future<Output = bool> + Send + 'static, +{ + Box::new(move |a, b| Box::pin(f(a, b))) +} + +/// collection of functions to create built in objects +pub mod built_in_objects { + const TEMP_BADGE_DUR_MIN: u64 = 30; + + use std::{ + sync::Arc, + time::{Duration, Instant}, + }; + + use twitch_irc::message::ServerMessage; + + use super::{super::modules::Status, command::Command, execution_async, Badge, Bot}; + + /// create a vector of command build in objects + pub fn create_commands() -> Vec<Command> { + let mut cmds = vec![]; + + cmds.push(create_disable_cmd()); + cmds.push(create_enable_cmd()); + cmds.push(create_iam_role_cmd()); + + cmds + } + + fn create_disable_cmd() -> Command { + /* 1. Create a new blank cmd */ + let mut cmd = Command::new(vec!["disable".to_string()], "".to_string()); + + /* 2. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let mut action_taken = false; + for (i, arg) in msg + .message_text + .replace("\u{e0000}", "") + .trim() + .split(" ") + .enumerate() + { + if i > 1 { + if bot + .get_channel_module_status(msg.channel_login.clone(), arg.to_string()) + .await + == Status::Enabled + { + action_taken = true; + bot.disable_module(msg.channel_login.clone(), arg.to_string()) + .await; + } + } + } + if action_taken { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, String::from("Disabled!")) + .await; + } + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(execution_async(execbody)); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(false); + + /* 5. optionally, set min badge*/ + cmd.set_min_badge(Badge::Moderator /* ::Moderator */); + cmd + } + + fn create_enable_cmd() -> Command { + /* 1. Create a new blank cmd */ + let mut cmd = Command::new(vec!["enable".to_string()], "".to_string()); + + /* 2. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let mut bot_message = "".to_string(); + let mut re_enabled = false; + + for (i, arg) in msg + .message_text + .replace("\u{e0000}", "") + .trim() + .split(" ") + .enumerate() + { + if i > 1 { + if Status::Disabled + == bot + .get_channel_module_status( + msg.channel_login.clone(), + arg.to_string(), + ) + .await + { + bot.enable_module(msg.channel_login.clone(), arg.to_string()) + .await; + + //bot.get_modules() + if let Some(found_mod) = bot.get_module(arg.to_string()).await { + bot_message = bot_message.to_string() + + found_mod.get_bot_read_description().as_str(); + } + re_enabled = true; + } + } + } + + if re_enabled { + if bot_message.len() > 250 { + bot_message = bot_message[..250].to_string(); + } + + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, format!("Enabled! {}", bot_message)) + .await; + } + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(execution_async(execbody)); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(false); + + /* 5. optionally, set min badge*/ + cmd.set_min_badge(Badge::Moderator); + cmd + } + + /// adminonly command that grants a temporary role + fn create_iam_role_cmd() -> Command { + /* 1. Create a new blank cmd */ + let mut cmd = Command::new( + vec!["I am ".to_string(), "I'm ".to_string(), "Im a ".to_string()], + "".to_string(), + ); + + /* 2. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + for (i, arg) in msg + .message_text + .replace("\u{e0000}", "") + .trim() + .split(" ") + .enumerate() + { + if i > 1 { + // bot.disable_module(msg.channel_login.clone(), arg.to_string()).await; + // #todo + // if not dont have the badge or have a lower priviledge badge + // and they dont have an active guest badge, ths admin can be + // recognzed wth that badge + + if arg == "mod" || arg == "moderator" { + let curr_temp_badges = bot + .get_channel_guest_badges( + msg.sender.login.clone(), + msg.channel_login.clone(), + ) + .await; + let mut found = false; + for temp_badge in curr_temp_badges { + if temp_badge.0 == Badge::Moderator { + found = true; + } + } + if found { + /* do nothing */ + } else { + bot.issue_new_guest_badge( + msg.sender.login.clone(), + msg.channel_login.clone(), + Badge::Moderator, + Instant::now(), + Duration::from_secs(60 * TEMP_BADGE_DUR_MIN), + ) + .await; + + let _ = bot + .chat + .lock() + .await + .say_in_reply_to( + &msg, + format!( + "Temp {:?} issued for {:?} minutes", + Badge::Moderator, + TEMP_BADGE_DUR_MIN + ), + ) + .await; + } + } + if arg == "vip" { + let curr_temp_badges = bot + .get_channel_guest_badges( + msg.sender.login.clone(), + msg.channel_login.clone(), + ) + .await; + let mut found = false; + for temp_badge in curr_temp_badges { + if temp_badge.0 == Badge::Vip { + found = true; + } + } + if found { + /* do nothing */ + } else { + bot.issue_new_guest_badge( + msg.sender.login.clone(), + msg.channel_login.clone(), + Badge::Vip, + Instant::now(), + Duration::from_secs(60 * TEMP_BADGE_DUR_MIN), + ) + .await; + + let _ = bot + .chat + .lock() + .await + .say_in_reply_to( + &msg, + format!( + "Temp {:?} issued for {:?} minutes for the bot admin", + Badge::Vip, + TEMP_BADGE_DUR_MIN + ), + ) + .await; + } + } + if arg == "broadcaster" || arg == "strimmer" || arg == "streamer" { + let curr_temp_badges = bot + .get_channel_guest_badges( + msg.sender.login.clone(), + msg.channel_login.clone(), + ) + .await; + let mut found = false; + for temp_badge in curr_temp_badges { + if temp_badge.0 == Badge::Broadcaster { + found = true; + } + } + if found { + /* do nothing */ + } else { + bot.issue_new_guest_badge( + msg.sender.login.clone(), + msg.channel_login.clone(), + Badge::Broadcaster, + Instant::now(), + Duration::from_secs(60 * TEMP_BADGE_DUR_MIN), + ) + .await; + + let _ = bot + .chat + .lock() + .await + .say_in_reply_to( + &msg, + format!( + "Temp {:?} issued for {:?} minutes for the bot admin", + Badge::Broadcaster, + TEMP_BADGE_DUR_MIN + ), + ) + .await; + } + } + } + } + // let _ = bot.chat.lock().await.say_in_reply_to(&msg, String::from("Disabled!")).await ; + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(execution_async(execbody)); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(true); + + // /* 5. optionally, set min badge*/ + // cmd.set_min_badge(Badge::Moderator); + cmd + } +} diff --git a/forcebot_core/src/botcore/bot_objects/command.rs b/forcebot_core/src/botcore/bot_objects/command.rs new file mode 100644 index 0000000..10a1e6b --- /dev/null +++ b/forcebot_core/src/botcore/bot_objects/command.rs @@ -0,0 +1,226 @@ +use std::sync::Arc; + +use twitch_irc::message::{PrivmsgMessage, ServerMessage}; + +use super::{command_condition_async, execution_async, Badge, Bot}; + +use super::{CommandTrigger, ExecBody}; + +/// Bot `Command` that stores trigger condition callback and a execution functon +/// +/// A prefix character or phrase can be defined for the bot to evaluate a trigger condition +/// +/// A command or command phrase defines the phrase after the prefix phrase +/// +/// If no min badge role is provided, Broadcaster is defaulted. All commands require at least a vip role +/// +/// AdminOnly commands can only be ran by admin +/// +/// Use `execution_async()` on custom async execution bodies +#[derive(Clone)] +pub struct Command { + commands: Vec<String>, + exec_fn: Arc<ExecBody>, + min_badge: Badge, + /// only admins can run - default : `true` + admin_only: bool, + /// admin role overrides channel badge - default : `false` + admin_override: bool, + prefix: String, + custom_cond_fn: fn(Arc<Bot>, PrivmsgMessage) -> bool, + custom_cond_async: Arc<CommandTrigger>, +} + +impl Command { + /// Creates a new empty `Command` using command `String` and prefix `String` + /// Pass an empty string prefix if the bot should use the bot default + /// + /// Call `set_trigger_cond_fn()` and `set_exec_fn()` to trigger & execution function callbacks + /// if a blank prefix is given, the bot will look for the bot prefix instead + /// + /// By default, the new command is admin_only + pub fn new(commands: Vec<String>, prefix: String) -> Command { + async fn execbody(_: Arc<Bot>, _: ServerMessage) -> Result<String, String> { + Result::Ok("success".to_string()) + } + async fn condition01(_: Arc<Bot>, _: PrivmsgMessage) -> bool { + true + } + + Command { + commands, + prefix, + exec_fn: Arc::new(execution_async(execbody)), + min_badge: Badge::Vip, + admin_only: true, + admin_override: false, + custom_cond_fn: |_: Arc<Bot>, _: PrivmsgMessage| true, + custom_cond_async: Arc::new(command_condition_async(condition01)), + } + } + + /// set a trigger condition callback that returns true if the command should trigger + pub fn set_custom_cond_fn(&mut self, cond_fn: fn(Arc<Bot>, PrivmsgMessage) -> bool) { + self.custom_cond_fn = cond_fn; + } + + /// sets the async trigger condition for listener + /// + /// Same as `set_custom_cond_fn()` , but async define + /// + /// Use`execution_async()` on the async fn when storing + /// + /// Example - + /// ```rust + /// /* 1. Create a new blank Listener */ + /// let mut cmd = Command::new(); + /// + /// /* 2. define an async function */ + /// async fn condition01(_:Arc<Bot>,_:ServerMessage) -> bool { true } + /// + /// /* 3. Set and Store the execution body using `execution_async()` */ + /// cmd.set_custom_cond_async(condition_async(condition01)); + /// ``` + /// + pub fn set_custom_cond_async(&mut self, condition: CommandTrigger) { + self.custom_cond_async = Arc::new(condition); + } + + /// sets the execution body of the listener for when it triggers + /// + /// Use`execution_async()` on the async fn when storing + /// + /// + pub fn set_exec_fn(&mut self, exec_fn: ExecBody) { + self.exec_fn = Arc::new(exec_fn); + } + + /// checks if the trigger condition is met + /// specifically if the message is a valid command and min badge roles provided + /// + pub async fn command_triggered(&self, bot: Arc<Bot>, msg: PrivmsgMessage) -> bool { + fn cmd_called(cmd: &Command, bot: Arc<Bot>, message: PrivmsgMessage) -> bool { + let mut prefixed_cmd = "".to_string(); + if cmd.prefix == "" { + prefixed_cmd.push_str(&bot.get_prefix()); + } else { + prefixed_cmd.push_str(&cmd.prefix); + } + for cmd_nm in &cmd.commands { + prefixed_cmd.push_str(cmd_nm); + if message.message_text.starts_with(prefixed_cmd.as_str()) { + return true; + } + } + return false; + } + + async fn caller_badge_ok(cmd: &Command, bot: Arc<Bot>, message: PrivmsgMessage) -> bool { + // senders that are admins skip badge check if the command is adminonly + if cmd.admin_only && bot.get_admins().contains(&message.sender.login) { + return true; + }; + + // adminOnly commands will can only be ran by admins + if cmd.admin_only && bot.get_admins().contains(&message.sender.login) { + return false; + } + + // admin role overrides badge check if enabled + if cmd.admin_override && bot.get_admins().contains(&message.sender.login) { + return true; + } + + for badge in message.badges { + match cmd.min_badge { + Badge::Broadcaster => { + if badge.name == "broadcaster" { + return true; + } else { + return false; + } + } + Badge::Moderator => match badge.name.as_str() { + "moderator" | "broadcaster" => return true, + _ => (), + }, + Badge::Vip => match badge.name.as_str() { + "vip" | "moderator" | "broadcaster" => return true, + _ => (), + }, + } + } + + for temp_badge in bot + .get_channel_guest_badges(message.sender.login, message.channel_login) + .await + { + match (cmd.min_badge.clone(), temp_badge.0) { + (Badge::Broadcaster, Badge::Broadcaster) => return true, + (Badge::Moderator, Badge::Moderator) + | (Badge::Moderator, Badge::Broadcaster) => return true, + (Badge::Vip, Badge::Vip) + | (Badge::Vip, Badge::Moderator) + | (Badge::Vip, Badge::Broadcaster) => return true, + _ => (), + } + } + + return false; + } + + /// determines if the command caller can run the command + /// based on admin_only flag + /// + /// callers who are admins can run admin_only commands + /// callers can run non-admin_only commands + fn admin_only_ok(cmd: &Command, bot: Arc<Bot>, message: PrivmsgMessage) -> bool { + if (cmd.admin_only && bot.get_admins().contains(&message.sender.login)) + || !cmd.admin_only + { + return true; + } else { + return false; + } + } + + async fn custom_cond_ok(cmd: &Command, bot: Arc<Bot>, message: PrivmsgMessage) -> bool { + (cmd.custom_cond_fn)(bot.clone(), message.clone()) + && (cmd.custom_cond_async)(bot, message).await + } + + // async fn quiet_off_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool { + // !bot.chat.lock().await.get_channel_quiet(message.channel_login.clone()) + // || bot.chat.lock().await.get_channel_quiet(message.channel_login.clone()) && cmd.commands.contains(&("quiet off".to_string())) + // } + + cmd_called(self, bot.clone(), msg.clone()) + && caller_badge_ok(self, bot.clone(), msg.clone()).await + && admin_only_ok(self, bot.clone(), msg.clone()) + && custom_cond_ok(self, bot.clone(), msg.clone()).await + // && + // quiet_off_ok(self, bot, msg).await + } + + /// executes the listeners executon body + pub async fn execute_fn(&self, bot: Arc<Bot>, msg: ServerMessage) -> Result<String, String> { + (self.exec_fn)(bot, msg).await + } + + /// sets min_badge to run the cmd + // pub fn set_min_badge(&mut self,min_badge:String) { + pub fn set_min_badge(&mut self, min_badge: Badge) { + self.min_badge = min_badge + } + + /// sets admin_only + pub fn set_admin_only(&mut self, admin_only: bool) { + self.admin_only = admin_only + } + + /// sets admin_override . This lets admins bypass + /// badge restrictions + pub fn set_admin_override(&mut self, admin_override: bool) { + self.admin_override = admin_override + } +} diff --git a/forcebot_core/src/botcore/bot_objects/listener.rs b/forcebot_core/src/botcore/bot_objects/listener.rs new file mode 100644 index 0000000..2633b3b --- /dev/null +++ b/forcebot_core/src/botcore/bot_objects/listener.rs @@ -0,0 +1,147 @@ +use std::sync::Arc; + +use twitch_irc::message::ServerMessage; + +use crate::Module; + +use super::{execution_async, listener_condition_async, Bot}; + +use super::{ExecBody, ListenerTrigger}; + +/// Bot `Listener` that stores trigger condition callback and a execution functon +/// +/// Use `Listener` functions to define the Trigger Condition & Execution callbacks. +/// When the Trigger callback is `true`, the Execution callback runs in the bot loop +/// +/// Create a new empty `Listener` with `new()` +/// +/// Use the following on the empty listener before loading it into the bot to set the callbacks +/// +/// - `set_trigger_cond_fn()` - to define the Trigger condition callback +/// +/// - `set_exec_fn()` - to define the Execution Callback +#[derive(Clone)] +pub struct Listener { + /// trigger condition + trigger_cond_fn: fn(Arc<Bot>, ServerMessage) -> bool, + /// trigger condition for async + trigger_cond_async: Arc<ListenerTrigger>, + /// execution body + exec_fn: Arc<ExecBody>, + parent_module: Arc<Option<Module>>, +} + +impl Listener { + /// Creates a new empty `Listener` + /// + /// Use `Listener` functions to define the Trigger Condition & Execution callbacks. + /// When the Trigger callback is `true`, the Execution callback runs in the bot loop + /// + /// Use the following on the empty listener before loading it into the bot to set the callbacks + /// + /// - `set_trigger_cond_fn()` - to define the Trigger condition callback + /// + /// - `set_exec_fn()` - to define the Execution Callback + pub fn new() -> Listener { + async fn execbody(_: Arc<Bot>, _: ServerMessage) -> Result<String, String> { + Result::Ok("success".to_string()) + } + async fn condition01(_: Arc<Bot>, _: ServerMessage) -> bool { + true + } + Listener { + trigger_cond_fn: |_: Arc<Bot>, _: ServerMessage| true, + trigger_cond_async: Arc::new(listener_condition_async(condition01)), + exec_fn: Arc::new(execution_async(execbody)), + parent_module: Arc::new(None), + } + } + + /// set a trigger conditin callback that returns true if the listener shoud trigger + pub fn set_trigger_cond_fn(&mut self, cond_fn: fn(Arc<Bot>, ServerMessage) -> bool) { + self.trigger_cond_fn = cond_fn; + } + + /// sets the async trigger condition for listener + /// + /// Same as `set_trigger_cond_fn()` , but async define + /// + /// Use`condition_async()` on the async fn when storing + /// + /// Example - + /// ```rust + /// /* 1. Create a new blank Listener */ + /// let mut listener = Listener::new(); + /// + /// /* 2. define an async function */ + /// async fn condition01(_:Arc<Bot>,_:ServerMessage) -> bool { true } + /// + /// /* 3. Set and Store the execution body using `execution_async()` */ + /// listener.set_trigger_cond_async(condition_async(condition01)); + /// ``` + /// + pub fn set_trigger_cond_async(&mut self, condition: ListenerTrigger) { + self.trigger_cond_async = Arc::new(condition); + } + + /// sets the execution body of the listener for when it triggers + /// + /// Use`execution_async()` on the async fn when storing + /// + /// Example - + /// ```rust + /// /* 1. Create a new blank Listener */ + /// let mut listener = Listener::new(); + /// + /// /* 2. define an async function */ + /// async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> {Result::Ok("success".to_string()) } + /// + /// /* 3. Set and Store the execution body using `execution_async()` */ + /// listener.set_exec_fn(execution_async(execbody)); + /// ``` + /// + pub fn set_exec_fn(&mut self, exec_fn: ExecBody) { + self.exec_fn = Arc::new(exec_fn); + } + + /// checks if the trigger condition is met + pub async fn cond_triggered(&self, bot: Arc<Bot>, msg: ServerMessage) -> bool { + let list = Arc::new(self.clone()); + + async fn defined_conditions_ok( + list: Arc<Listener>, + bot: Arc<Bot>, + msg: ServerMessage, + ) -> bool { + // let list = Arc::new(self); + (list.trigger_cond_fn)(bot.clone(), msg.clone()) + && (list.trigger_cond_async)(bot, msg).await + } + + // async fn quiet_off_ok(list:Arc<Listener>,bot:Arc<Bot>,message:ServerMessage) -> bool { + // if let ServerMessage::Privmsg(msg) = message { + + // if let Some(parent_mod) = &*list.parent_module { + // return !bot.chat.lock().await.get_channel_quiet(msg.channel_login) || parent_mod.get_names().contains(&"debug".to_string()); + // } + + // return !bot.chat.lock().await.get_channel_quiet(msg.channel_login) ; + // } + // return true; /* quiet is off for non chat msgs */ + // } + + defined_conditions_ok(list.clone(), bot.clone(), msg.clone()).await + // && + // quiet_off_ok(list, bot, msg).await + } + + /// executes the listeners executon body + pub async fn execute_fn(&self, bot: Arc<Bot>, msg: ServerMessage) -> Result<String, String> { + (self.exec_fn)(bot, msg).await + } + + /// sets parent module + pub fn set_parent_module(&mut self, module: Module) { + self.parent_module = Arc::new(Some(module)); + } +} diff --git a/forcebot_core/src/botcore/built_in_mods.rs b/forcebot_core/src/botcore/built_in_mods.rs new file mode 100644 index 0000000..03b8d28 --- /dev/null +++ b/forcebot_core/src/botcore/built_in_mods.rs @@ -0,0 +1,10 @@ +// use std::sync::Arc; + +use crate::Bot; + +pub mod quiet; + +/// used to internally load internal modules +pub async fn load_built_in_mods(bot: &Bot) { + bot.load_module(quiet::create_module()).await; +} diff --git a/forcebot_core/src/botcore/built_in_mods/quiet.rs b/forcebot_core/src/botcore/built_in_mods/quiet.rs new file mode 100644 index 0000000..7a12cc1 --- /dev/null +++ b/forcebot_core/src/botcore/built_in_mods/quiet.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use twitch_irc::message::ServerMessage; + +use crate::{execution_async, Badge, Bot, Command, Module}; + +/// quiet the bot in a channel +/// +/// use +/// `quiet on` +/// `quiet off` +/// +/// +/// + +/// Use this function when loading modules into the bot +/// +/// For example +/// ```rust +/// bot.load_module(quiet::create_module()); +/// ``` +/// +pub fn create_module() -> Module { + /* 1. Create a new module */ + let mut custom_mod = Module::new(vec!["quiet".to_string()], "".to_string()); + + /* 2. Load the cmd into a new module */ + custom_mod.load_command(cmd_quiet_on()); + custom_mod.load_command(cmd_quiet_off()); + custom_mod +} + +/// Command definition for quiet command +fn cmd_quiet_on() -> Command { + /* 1. Create a new cmd */ + let mut cmd = Command::new(vec!["quiet on".to_string()], "".to_string()); + + /* 2. Define exec callback */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + // dbg!("quiet on called"); + + let chatlock = bot.chat.lock().await; + let _ = chatlock.say_in_reply_to(&msg, "Shush ".to_string()).await; + + chatlock.set_channel_quiet(msg.channel_login.clone(), true); + println!("channel {} set quiet true", msg.channel_login); + + return Result::Ok("Success".to_string()); + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set Command flags */ + cmd.set_exec_fn(execution_async(execbody)); + cmd.set_admin_only(false); + cmd.set_min_badge(Badge::Moderator); + cmd.set_admin_override(true); + + cmd +} + +/// Command definition for quiet command +fn cmd_quiet_off() -> Command { + /* 1. Create a new cmd */ + let mut cmd = Command::new(vec!["quiet off".to_string()], "".to_string()); + + /* 2. Define exec callback */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let chatlock = bot.chat.lock().await; + + chatlock.set_channel_quiet(msg.channel_login.clone(), false); + let _ = chatlock + .say_in_reply_to(&msg, "GoodGirl I'll be good for u chat rar ".to_string()) + .await; + + println!("channel {} set quiet false", msg.channel_login); + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set Command flags */ + cmd.set_exec_fn(execution_async(execbody)); + cmd.set_admin_only(false); + cmd.set_min_badge(Badge::Moderator); + cmd.set_admin_override(true); + + cmd +} diff --git a/forcebot_core/src/botcore/chat.rs b/forcebot_core/src/botcore/chat.rs new file mode 100644 index 0000000..42f1ec6 --- /dev/null +++ b/forcebot_core/src/botcore/chat.rs @@ -0,0 +1,140 @@ +use std::{ + fmt::Error, + ops::Mul, + rc::Rc, + sync::{Arc, Mutex, RwLock}, +}; + +use twitch_irc::{ + login::StaticLoginCredentials, message::ReplyToMessage, SecureTCPTransport, TwitchIRCClient, +}; + +use crate::Bot; + +/// Bot API to send messages to send messages to chat +/// +/// Uses TwitchIRCClient say_in_reply_to() but enforces controls like quiet +/// +/// + +pub struct Chat { + /// outbound chat client msg stream + pub client: TwitchIRCClient<SecureTCPTransport, StaticLoginCredentials>, + /// channel_quiet + channel_quiet_yn: RwLock<Vec<(String, RwLock<bool>)>>, +} + +impl Chat { + pub async fn new(client: TwitchIRCClient<SecureTCPTransport, StaticLoginCredentials>) -> Chat { + Chat { + client, + // parent_bot : Mutex::new(Bot::new().await) , + channel_quiet_yn: RwLock::new(vec![]), + } + } + + // pub fn set_parent_bot(&self,parent_bot_in:Arc<Bot>) + // { + // let mut lock = self.parent_bot.lock().unwrap(); + // *lock = parent_bot_in; + // } + + /// helper + fn ok_to_send(&self, channel_login: String) -> bool { + fn not_quiet_ok(chat: &Chat, channel_login: String) -> bool { + // let lock = chat.parent_bot.lock().unwrap(); + // let a = lock.as_ref(); + // if let Some(bot) = &*lock { + return !chat.get_channel_quiet(channel_login); + // } + // true + } + not_quiet_ok(self, channel_login) + } + + /// Get the quiet status of a channel + pub fn get_channel_quiet(&self, channel: String) -> bool { + for a in self.channel_quiet_yn.read().unwrap().iter() { + if a.0 == channel { + return a.1.read().unwrap().clone(); + } + } + return false; + } + + /// Get the quiet status of a channel + pub fn set_channel_quiet(&self, channel: String, quiet_on: bool) { + let mut found = false; + + let chnlquiet = self.channel_quiet_yn.read().unwrap(); + for rec in chnlquiet.iter() { + if rec.0 == channel { + found = true; + let mut status = rec.1.write().unwrap(); + *status = quiet_on; + drop(status); + } + } + drop(chnlquiet); + + if !found { + // dbg!("set chn quiet > !found channel quiet status"); + let mut chnlquiet = self.channel_quiet_yn.write().unwrap(); + chnlquiet.push((channel, RwLock::new(quiet_on))); + drop(chnlquiet); + } + } + + pub async fn say_in_reply_to( + &self, + reply_to: &impl ReplyToMessage, + message: String, + ) -> Result<(), ()> { + // reply_to.channel_login() + if self.ok_to_send(reply_to.channel_login().to_string()) { + match self.client.say_in_reply_to(reply_to, message).await { + Ok(_) => return Ok(()), + Err(_) => return Err(()), + } + } else { + return Err(()); + } + } + + pub async fn say(&self, channel_login: String, message: String) -> Result<(), ()> { + if self.ok_to_send(channel_login.to_string()) { + match self.client.say(channel_login, message).await { + Ok(_) => return Ok(()), + Err(_) => return Err(()), + } + } else { + return Err(()); + } + } + + pub async fn me(&self, channel_login: String, message: String) -> Result<(), ()> { + if self.ok_to_send(channel_login.to_string()) { + match self.client.me(channel_login, message).await { + Ok(_) => return Ok(()), + Err(_) => return Err(()), + } + } else { + return Err(()); + } + } + + pub async fn me_in_reply_to( + &self, + reply_to: &impl ReplyToMessage, + message: String, + ) -> Result<(), ()> { + if self.ok_to_send(reply_to.channel_login().to_string()) { + match self.client.me_in_reply_to(reply_to, message).await { + Ok(_) => return Ok(()), + Err(_) => return Err(()), + } + } else { + return Err(()); + } + } +} diff --git a/forcebot_core/src/botcore/modules.rs b/forcebot_core/src/botcore/modules.rs new file mode 100644 index 0000000..dd9cc29 --- /dev/null +++ b/forcebot_core/src/botcore/modules.rs @@ -0,0 +1,74 @@ +// use std::sync::{Arc, Mutex}; + +use super::bot_objects::command::Command; +use super::bot_objects::listener::Listener; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Status { + Disabled, + Enabled, + NotLoaded, +} + +/// Bot `Module` that groups a set of `bot_objects` +/// +/// Elevated chatters can disable modules by their name or chat alias +#[derive(Clone)] +pub struct Module { + name: Vec<String>, + // _alias: String, + bot_read_description: String, + listeners: Vec<Listener>, + commands: Vec<Command>, + // disable module at load for bot channels + default_status_per_channel: Status, +} + +impl Module { + /// create a new module + pub fn new(name: Vec<String>, bot_read_description: String) -> Module { + Module { + name, + // _alias: alias, + bot_read_description, + listeners: vec![], + commands: vec![], + default_status_per_channel: Status::Enabled, + } + } + + /// Loads a `Listener` into the module + pub fn load_listener(&mut self, mut l: Listener) { + l.set_parent_module(self.clone()); + self.listeners.push(l); + } + + /// Loads a `Command` into the module + pub fn load_command(&mut self, c: Command) { + self.commands.push(c); + } + + pub fn get_listeners(&self) -> Vec<Listener> { + self.listeners.clone() + } + + pub fn get_commands(&self) -> Vec<Command> { + self.commands.clone() + } + + pub fn get_names(&self) -> Vec<String> { + self.name.clone() + } + + pub fn get_bot_read_description(&self) -> String { + self.bot_read_description.clone() + } + + pub fn set_status_by_default(&mut self, status: Status) { + self.default_status_per_channel = status; + } + + pub fn get_status_by_default(&self) -> Status { + self.default_status_per_channel.clone() + } +} diff --git a/forcebot_core/src/custom_mods.rs b/forcebot_core/src/custom_mods.rs new file mode 100644 index 0000000..fdbd664 --- /dev/null +++ b/forcebot_core/src/custom_mods.rs @@ -0,0 +1,4 @@ +pub mod debug; +pub mod guest_badge; +pub mod pyramid; +// pub mod quiet; diff --git a/forcebot_core/src/custom_mods/debug.rs b/forcebot_core/src/custom_mods/debug.rs new file mode 100644 index 0000000..0457e54 --- /dev/null +++ b/forcebot_core/src/custom_mods/debug.rs @@ -0,0 +1,138 @@ +use std::sync::Arc; + +use crate::{execution_async, modules::Status, Bot, Command, Listener, Module}; +use twitch_irc::message::ServerMessage; + +/// debug module +/// +/// Commands to enable debugging messages in chat +/// +/// `debug on` to start +/// +/// `debug off` to stop +/// +/// + +/// Use this function when loading modules into the bot +/// +/// For example +/// ```rust +/// bot.load_module(debug::create_module()); +/// ``` +/// +pub fn create_module() -> Module { + /* 1. Create a new module */ + let mut custom_mod = Module::new(vec!["debug".to_string()], "".to_string()); + + /* 2. Load the cmd into a new module */ + custom_mod.load_command(cmd_debug_on()); + custom_mod.load_command(cmd_debug_off()); + custom_mod +} + +/// Command definition for debug command +fn cmd_debug_on() -> Command { + /* 1. Create a new cmd */ + let mut cmd = Command::new(vec!["debug on".to_string()], "".to_string()); + + /* 2. Define exec callback */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + // dbg!("debug cmd on executed"); + + let modulename = "debug listener".to_string(); + + if let Status::NotLoaded = bot + .get_channel_module_status(msg.channel_login.clone(), modulename.clone()) + .await + { + let module = create_listener_module(modulename.clone()); + bot.load_module(module.clone()).await; + } + let modl = bot.get_module(modulename).await.unwrap(); + bot.enable_module( + msg.channel_login.clone(), + modl.get_names().first().unwrap().clone(), + ) + .await; + println!("Debug enabled for channel {}", msg.channel_login); + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set Command flags */ + cmd.set_exec_fn(execution_async(execbody)); + cmd.set_admin_only(true); + // cmd.set_min_badge(Badge::Moderator); + cmd.set_admin_override(true); + + cmd +} + +/// Command definition for debug off command +fn cmd_debug_off() -> Command { + /* 1. Create a new cmd */ + let mut cmd = Command::new(vec!["debug off".to_string()], "".to_string()); + + /* 2. Define exec callback */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + // dbg!("debug cmd on executed"); + + let modulename = "debug listener".to_string(); + + // if let Status::NotLoaded = bot.get_channel_module_status(msg.channel_login.clone(), modulename.clone()).await { + // let module = create_listener_module(modulename.clone()); + // bot.load_module(module.clone()).await; + // } + let modl = bot.get_module(modulename).await.unwrap(); + bot.disable_module( + msg.channel_login.clone(), + modl.get_names().first().unwrap().clone(), + ) + .await; + println!("Debug disabled for channel {}", msg.channel_login); + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set Command flags */ + cmd.set_exec_fn(execution_async(execbody)); + cmd.set_admin_only(true); + // cmd.set_min_badge(Badge::Moderator); + cmd.set_admin_override(true); + + cmd +} + +fn create_listener_module(name: String) -> Module { + let mut custom_mod = Module::new(vec![name], "".to_string()); + // dbg!("debug listener module created"); + custom_mod.load_listener(cmd_debug_listener()); + custom_mod.set_status_by_default(Status::Disabled); + + custom_mod +} + +/// Listener for debug +fn cmd_debug_listener() -> Listener { + // dbg!("Creating debug listener"); + /* 2a. Create a new blank Listener */ + let mut listener = Listener::new(); + + /* 2b. Set a trigger condition function for listener */ + listener.set_trigger_cond_fn(|_: Arc<Bot>, _: ServerMessage| true); + + /* 2c. Define an async fn callback execution */ + async fn execbody(_: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + dbg!(msg); /* outputs message to debug */ + } + Result::Err("Not Valid message type".to_string()) + } + + /* 2d. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); + + listener +} diff --git a/forcebot_core/src/custom_mods/guest_badge.rs b/forcebot_core/src/custom_mods/guest_badge.rs new file mode 100644 index 0000000..2cf4ae7 --- /dev/null +++ b/forcebot_core/src/custom_mods/guest_badge.rs @@ -0,0 +1,175 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use twitch_irc::message::ServerMessage; + +use crate::{execution_async, Badge, Bot, Command, Module}; + +/// guest_badge / guest module +/// +/// Temporary badges can be issued to chatters. The bot then opens functionality +/// to that chatter based on the recognized role +/// +/// Chatters with real badge roles will be able to share guest +/// badges based on their role +/// +/// +/// + +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( + vec!["guests".to_string()], + "Temp Guest badges can be given by chatters with badges. ".to_string(), + ); + + custom_mod.load_command(create_cmd_mod()); + custom_mod.load_command(create_cmd_vip()); + + custom_mod +} + +fn create_cmd_vip() -> Command { + let mut cmd = Command::new(vec!["vip".to_string()], "".to_string()); + + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let guest_dur_min = { + let mut result = VIP_GIVEN_DUR_MIN; + for badge in msg.clone().badges { + if badge.name == "vip" { + result = VIP_GIVEN_DUR_MIN; + } + if badge.name == "moderator" { + result = MOD_GIVEN_DUR_MIN; + } + } + result + }; + + let mut badges_issued = false; + for (i, arg) in msg + .message_text + .replace("\u{e0000}", "") + .trim() + .split(" ") + .enumerate() + { + if i > 1 { + let mut already_vip = false; + + for guest_badge in bot + .get_channel_guest_badges(arg.trim().to_string(), msg.channel_login.clone()) + .await + { + if guest_badge.0 == Badge::Vip { + already_vip = true + } + } + if !already_vip { + badges_issued = true; + bot.issue_new_guest_badge( + arg.trim().to_string(), + msg.channel_login.clone(), + Badge::Vip, + Instant::now(), + Duration::from_secs(60 * guest_dur_min), + ) + .await; + } + } + } + if badges_issued { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to( + &msg.clone(), + format!("Guest badges issued for {} min", guest_dur_min), + ) + .await; + return Result::Ok("Success".to_string()); + } + } + Result::Err("Not Valid message type".to_string()) + } + + cmd.set_exec_fn(execution_async(execbody)); + + cmd.set_admin_only(false); + cmd.set_admin_override(true); + cmd.set_min_badge(Badge::Vip); + cmd +} + +fn create_cmd_mod() -> Command { + let mut cmd = Command::new(vec!["mod".to_string()], "".to_string()); + + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let mut badges_issued = false; + for (i, arg) in msg + .message_text + .replace("\u{e0000}", "") + .trim() + .split(" ") + .enumerate() + { + if i > 1 { + let mut already_mod = false; + for guest_badge in bot + .get_channel_guest_badges(arg.trim().to_string(), msg.channel_login.clone()) + .await + { + if guest_badge.0 == Badge::Moderator { + already_mod = true + } + } + if !already_mod { + badges_issued = true; + bot.issue_new_guest_badge( + arg.trim().to_string(), + msg.channel_login.clone(), + Badge::Moderator, + Instant::now(), + Duration::from_secs(60 * MOD_GIVEN_DUR_MIN), + ) + .await; + } + } + } + if badges_issued { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to( + &msg, + format!("Guest badges issued for {} min", MOD_GIVEN_DUR_MIN), + ) + .await; + return Result::Ok("Success".to_string()); + } + } + Result::Err("Not Valid message type".to_string()) + } + + cmd.set_exec_fn(execution_async(execbody)); + + cmd.set_admin_only(false); + cmd.set_admin_override(true); + cmd.set_min_badge(Badge::Moderator); + cmd +} diff --git a/forcebot_core/src/custom_mods/pyramid.rs b/forcebot_core/src/custom_mods/pyramid.rs new file mode 100644 index 0000000..99deb25 --- /dev/null +++ b/forcebot_core/src/custom_mods/pyramid.rs @@ -0,0 +1,485 @@ +use std::sync::{Arc, Mutex}; + +use twitch_irc::message::{PrivmsgMessage, ServerMessage}; + +// use crate::{execution_async, listener_condition_async, Badge, Bot, Command, Listener, Module}; +use super::super::botcore::bot::Bot; +use super::super::botcore::bot_objects::command::Command; +use super::super::botcore::bot_objects::execution_async; +use super::super::botcore::bot_objects::listener::Listener; +use super::super::botcore::bot_objects::listener_condition_async; +use super::super::botcore::bot_objects::Badge; +use super::super::botcore::modules::Module; + +/// pyramid module +/// +/// for detecting & handling pyramids +/// +/// - listener - detects pyramid +/// - cmd & listener - interrupts some chatters temporarily +/// +/// +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. Define an async trigger condition callback */ + async fn condition01(bot: Arc<Bot>, message: ServerMessage) -> bool { + if let ServerMessage::Privmsg(msg) = message { + if detect_pyramid_complete_ok(bot.clone(), msg.clone()).await + && get_pyramid_size(msg.channel_login) > 3 + { + return true; + } + } + false + } + + /* 3. Set a trigger condition function for listener */ + listener.set_trigger_cond_async(listener_condition_async(condition01)); + + /* 4. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + dbg!("enter pyramid listener execution - after pyramid complete"); + // if detect_pyramid_complete_ok(bot.clone(), msg.clone()).await { + + // let _ = bot.chat.lock().await. + dbg!("> get start pattern"); + let pattern = get_start_pattern(msg.channel_login.clone()); + let mut outmsg; + + /* Prefer emote before pattern in case pattern is command */ + if pattern.len() < 50 { + outmsg = format!("Clap {}", pattern); + } else { + outmsg = "Clap".to_string(); + } + + dbg!(get_pyramid_size(msg.channel_login.clone())); + if get_pyramid_size(msg.channel_login.clone()) < 4 { + outmsg = format!("{} annytfMagniGlass", outmsg); + } + + dbg!("> start pattern :", pattern); + + dbg!("> say_in_reply_to completed :", outmsg.clone()); + let _ = bot.chat.lock().await.say_in_reply_to(&msg, outmsg).await; + + dbg!("> set pyramid started - false"); + set_pyramid_started(msg.channel_login.clone(), false); + + return Result::Ok("Success".to_string()); + // } + } + Result::Err("Not Valid message type".to_string()) + } + + /* 5. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(Box::new(move |a, b| Box::pin(execbody(a, b)))); + + listener +} + +/// detect pyramid based on latest message and channel +/// +/// +async fn detect_pyramid_complete_ok(_bot: Arc<Bot>, msg: PrivmsgMessage) -> bool { + dbg!("enter detect_pyramid_complete()"); + + let msgtext = msg + .message_text + .replace("ó €€", "") + .replace("\u{e0000}", "") + .trim() + .to_string(); + let msgchannel = msg.channel_login; + let msgchatter = msg.sender.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()) + { + dbg!("> set pyramid started - true"); + set_pyramid_started(msgchannel.clone(), true); + push_to_compare( + msgchannel.clone(), + msgchatter.clone(), + get_start_pattern(msgchannel.clone()), + ); + } + + if is_pyramid_started(msgchannel.clone()) { + push_to_compare(msgchannel.clone(), msgchatter.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 { + dbg!("> set pyramid started - false"); + set_pyramid_started(msgchannel, false); + + return false; + } + } + + // 2c. if Pyramid is strted but latest message does not ontain pattern + if is_pyramid_started(msgchannel.clone()) + && !msgtext + .clone() + .contains(get_start_pattern(msgchannel.clone()).as_str()) + { + dbg!("> set pyramid started - false"); + set_pyramid_started(msgchannel, false); + + return false; + } else { + return false; + }; +} + +lazy_static! { + /// Message Compare stack per channel (channel:String,msgstack:Vec<(chatter:String,message:String)>) + pub static ref COMPARE_MSG_STACK_PER_CHNL: Mutex<Vec<(String,Mutex<Vec<(String,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![]); + /// Pyramid sze per channel (channel:String,started:bool) + pub static ref PYRAMID_SIZE_PER_CHNL: Mutex<Vec<(String,Mutex<i32>)>> = Mutex::new(vec![]); + /// temp message stack checker + pub static ref TEMP_MSG_STACK: Mutex<Vec<String>> = Mutex::new(vec![]); + + /// interruptor targets - (channel:String,chatters:Vec<String>>) + pub static ref INTERRUPT_TRG_PER_CHNL: Mutex<Vec<(String,Mutex<Vec<String>>)>> = Mutex::new(vec![]); + +} + +fn read_top_of_compare(channel: String) -> Option<(String, 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, 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, chatter: 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((chatter.clone(), message.clone())); + // dbg!("Push message to cmp stack ; result last cmp_pchnl - ",comp_perchnl.last()); + } + } + if !found { + comp_perchnl.push((channel, Mutex::new(vec![(chatter, 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()) + ) + // msgtext == format!("{} {} {}", + // get_start_pattern(channel.clone()), + // 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(), "".to_string())) + .1 + == get_start_pattern(channel.clone())) + { + return false; + } + + let mut pyramid_size = 0; + loop { + if !checking_started + && read_top_of_compare(channel.clone()) + .unwrap_or(("".to_string(), "".to_string())) + .1 + == get_start_pattern(channel.clone()) + { + checking_started = true; + } + + if temp_stack.last().is_none() + || read_top_of_compare(channel.clone()) + .unwrap_or(("".to_string(), "".to_string())) + .1 + .len() + > temp_stack.last().unwrap_or(&"".to_string()).len() + { + temp_stack.push( + pop_top_of_compare(channel.clone()) + .unwrap_or(("".to_string(), "".to_string())) + .1, + ); + pyramid_size += 1; + } else if temp_stack.last().is_some() + && read_top_of_compare(channel.clone()) + .unwrap_or(("".to_string(), "".to_string())) + .1 + .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(), "".to_string())) + .1 + { + temp_stack.pop(); + + continue; + } else { + set_pyramid_size(channel.clone(), 0); + temp_stack.clear(); + return false; + } + } else { + set_pyramid_size(channel.clone(), 0); + return false; + } + + if checking_started + && read_top_of_compare(channel.clone()).unwrap().1 == get_start_pattern(channel.clone()) + { + /* leave pyramid size set for exection */ + set_pyramid_size(channel.clone(), pyramid_size * 2 - 1); + temp_stack.clear(); + return true; + } + } +} + +fn set_pyramid_size(channel: String, size: i32) { + let mut size_perchnl = PYRAMID_SIZE_PER_CHNL.lock().unwrap(); + let mut found = false; + for rec in size_perchnl.iter() { + if rec.0 == channel { + found = true; + let mut rec_started = rec.1.lock().unwrap(); + *rec_started = size; + } + } + if !found { + size_perchnl.push((channel, Mutex::new(size))); + } +} + +fn get_pyramid_size(channel: String) -> i32 { + let size_perchnl = PYRAMID_SIZE_PER_CHNL.lock().unwrap(); + for rec in size_perchnl.iter() { + if rec.0 == channel { + let rec_started = rec.1.lock().unwrap(); + return *rec_started; + } + } + 0 +} + +/// #todo +/// +/// pyramid interruptor +/// +/// pick chatters that will be interrupted if they solo build +/// +/// takes in arguments as chatters +/// +/// chatters are then interrupted for a random duration under 15m +/// +/// if a duration is given, take that duration eg 15m , 25m +/// +/// +fn _create_interruptor_cmd() -> Command { + let mut cmd = Command::new( + vec!["no pyramid".to_string(), "no pyramids".to_string()], + "".to_string(), + ); + + /* 2. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, String::from("test success")) + .await; + return Result::Ok("Success".to_string()); + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(Box::new(move |a, b| Box::pin(execbody(a, b)))); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(false); + + /* 5. optionally, set min badge*/ + cmd.set_min_badge(Badge::Moderator); + + cmd +} + +/// #todo +fn _create_interruptor_module(channel: String) -> Module { + /* 1. Create a new module */ + let modname = format!("interruptor {}", channel); + let mut custom_mod = Module::new(vec![modname], "".to_string()); + + /* 2. Load the cmd into a new module */ + custom_mod.load_listener(_create_interruptor_listener()); + + custom_mod +} + +/// #todo +fn _create_interruptor_listener() -> Listener { + /* 2a. Create a new blank Listener */ + let mut listener = Listener::new(); + + /* 2b. Set a trigger condition function for listener */ + listener.set_trigger_cond_fn(|_: Arc<Bot>, _: ServerMessage| true); + + /* 2c. Define an async fn callback execution */ + async fn execbody(_: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + dbg!(message); /* outputs message to debug */ + Result::Ok("Success".to_string()) + } + + /* 2d. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); + + listener +} + +/// #todo +/// +/// Returns Some(chatter) if the pyramid in progress is being built by a solo +fn _solo_building(_channel: String) -> Option<String> { + None +} diff --git a/forcebot_core/src/lib.rs b/forcebot_core/src/lib.rs new file mode 100644 index 0000000..6d3f317 --- /dev/null +++ b/forcebot_core/src/lib.rs @@ -0,0 +1,234 @@ +//! `forcebot_core` library for `forcebot-rs-v2` Twitch chat bot +//! +//! Customize by adding additional bot objects +//! +//! # New Bot +//! +//! Uses Env defined variables to create and run the bot +//! +//! ```rust +//! use forcebot_core::Bot; +//! +//! #[tokio::main] +//! pub async fn main() { +//! +//! /* 1. Create the bot using env */ +//! let bot = Bot::new(); +//! +//! /* 2. Run the bot */ +//! bot.run().await; +//! +//! } +//! +//! ``` +//! +//! +//! # Customize with Modules +//! +//! A `Module` is a group of bot objects (eg `Command`) that elevated users can manage through built in `disable` and `enable` commands +//! +//! Custom `Modules` can be loaded into a new bot with minimum coding : just load the modules and run the bot +//! +//! ```rust +//! use forcebot_core::{custom_mods::{guest_badge, pyramid}, Bot}; +//! +//! #[tokio::main] +//! pub async fn main() { +//! +//! /* 1. Create the bot using env */ +//! let mut bot = Bot::new(); +//! +//! /* 2. Load Custom Modules */ +//! bot.load_module(guest_badge::create_module()).await; +//! bot.load_module(pyramid::create_module()).await; +//! +//! /* 3. Run the bot */ +//! bot.run().await; +//! +//! } +//! ``` +//! +//! +//! # Create your own Custom Modules +//! +//! Create a custom `Module` by : +//! +//! 1. Defining Functions that create the Custom Bot Objects (eg `Command`) +//! +//! 2. Define a function that creates a `Module` with the Custom Bot Objects loaded +//! +//! +//! ```rust +//! use forcebot_core::Bot; +//! +//! #[tokio::main] +//! pub async fn main() { +//! +//! /* Create the bot using env */ +//! let mut bot = Bot::new(); +//! +//! /* load the Module */ +//! bot.load_module(custom_mod::new()); +//! +//! /* Run the bot */ +//! bot.run().await; +//! +//! } +//! +//! pub mod custom_mod { +//! use std::sync::Arc; +//! +//! use forcebot_core::{execution_async, Badge, Bot, Command, Module}; +//! use twitch_irc::message::ServerMessage; +//! +//! +//! /// Module definition with a loaded command +//! pub fn new() -> Module { +//! /* 1. Create a new module */ +//! 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()); +//! +//! 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()); +//! +//! /* 2. Define exec callback */ +//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { +//! if let ServerMessage::Privmsg(msg) = message { +//! let _= bot.client.say_in_reply_to( +//! &msg, "test return".to_string()).await; +//! } +//! Result::Err("Not Valid message type".to_string()) +//! } +//! +//! /* 3. Set Command flags */ +//! cmd.set_exec_fn(execution_async(execbody)); +//! cmd.set_admin_only(false); +//! cmd.set_min_badge(Badge::Moderator); +//! +//! cmd +//! } +//! } +//! +//! +//! ``` +//! +//! # Simple Debug Listener +//! Bot with a simple listener that listens for all messages and prints in output +//! +//! ```rust +//! use std::sync::Arc; +//! +//! use forcebot_core::{execution_async, Bot, Listener}; +//! use twitch_irc::message::ServerMessage; +//! +//! #[tokio::main] +//! pub async fn main() { +//! +//! /* 1. Create the bot using env */ +//! let mut bot = Bot::new(); +//! +//! /* 2a. Create a new blank Listener */ +//! let mut listener = Listener::new(); +//! +//! /* 2b. Set a trigger condition function for listener */ +//! listener.set_trigger_cond_fn( +//! |_:Arc<Bot>,_:ServerMessage| true +//! ); +//! +//! /* 2c. Define an async fn callback execution */ +//! async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { +//! dbg!(message); /* outputs message to debug */ +//! Result::Ok("Success".to_string()) +//! } +//! +//! /* 2d. Set and Store the execution body using `execution_async()` */ +//! listener.set_exec_fn(execution_async(execbody)); +//! +//! /* 3. Load the listener into the bot */ +//! bot.load_listener(listener); +//! +//! /* 4. Run the bot */ +//! bot.run().await; +//! +//! } +//! +//! ``` +//! +//! # Moderator Reactor +//! +//! ``` +//! +//! use std::sync::Arc; +//! +//! use forcebot_core::Bot; +//! use forcebot_core::execution_async; +//! use forcebot_core::Listener; +//! use twitch_irc::message::ServerMessage; +//! +//! +//! #[tokio::main] +//! pub async fn main() { +//! +//! /* Create the bot using env */ +//! let mut bot = Bot::new(); +//! +//! /* 1. Create a new blank Listener */ +//! let mut listener = Listener::new(); +//! +//! /* 2. Set a trigger condition function for listener */ +//! +//! listener.set_trigger_cond_fn( +//! |_:Arc<Bot>,message:ServerMessage| +//! if let ServerMessage::Privmsg(msg) = message { +//! for badge in msg.badges { +//! if matches!(badge, x if x.name == "moderator") { +//! // dbg!("moderator found"); +//! return true; +//! } +//! } +//! false +//! } else { false } +//! ); +//! +//! /* 3. Define an async fn callback execution */ +//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { +//! if let ServerMessage::Privmsg(msg) = message { +//! let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".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 `execution_async()` */ +//! listener.set_exec_fn(execution_async(execbody)); +//! +//! /* 5. Load the listener into the bot */ +//! bot.load_listener(listener); +//! +//! /* Run the bot */ +//! bot.run().await; +//! +//! } + +pub mod botcore; +pub mod custom_mods; +pub use botcore::bot::Bot; +pub use botcore::bot_objects::command_condition_async; +pub use botcore::bot_objects::execution_async; +pub use botcore::bot_objects::listener::Listener; +pub use botcore::bot_objects::listener_condition_async; +// pub use crate::botcore::bot_objects::command::Command; +pub use botcore::bot_objects::command::Command; +pub use botcore::bot_objects::Badge; +pub use botcore::modules; +pub use botcore::modules::Module; diff --git a/moderator_reactor/Cargo.toml b/moderator_reactor/Cargo.toml new file mode 100644 index 0000000..f3431e6 --- /dev/null +++ b/moderator_reactor/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "moderator_reactor" +version = "0.1.0" +edition = "2021" + +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/moderator_reactor/src/main.rs b/moderator_reactor/src/main.rs new file mode 100644 index 0000000..284dd0a --- /dev/null +++ b/moderator_reactor/src/main.rs @@ -0,0 +1,67 @@ +//! Simple bot with a custom listeners that listens for moderators and respond to the moderator +//! +//! Be sure the followig is defined in `.env` +//! - login_name +//! - access_token +//! - bot_channels +//! - prefix +//! - bot_admins +//! +//! Bot access tokens be generated here - +//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> +//! - More Info - <https://dev.twitch.tv/docs/authentication> + +use std::sync::Arc; + +use forcebot_core::execution_async; +use forcebot_core::Bot; +use forcebot_core::Listener; +use twitch_irc::message::ServerMessage; + +#[tokio::main] +pub async fn main() { + /* Create the bot using env */ + let bot = Bot::new().await; + + /* 1. Create a new blank Listener */ + let mut listener = Listener::new(); + + /* 2. Set a trigger condition function for listener */ + + listener.set_trigger_cond_fn(|_: Arc<Bot>, message: ServerMessage| { + if let ServerMessage::Privmsg(msg) = message { + for badge in msg.badges { + if matches!(badge, x if x.name == "moderator") { + // dbg!("moderator found"); + return true; + } + } + false + } else { + false + } + }); + + /* 3. Define an async fn callback execution */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, "pepeKneel".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 `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); + + /* 5. Load the listener into the bot */ + bot.load_listener(listener).await; + + /* Run the bot */ + bot.run().await; +} diff --git a/new_empty_bot/Cargo.toml b/new_empty_bot/Cargo.toml new file mode 100644 index 0000000..96d5d3f --- /dev/null +++ b/new_empty_bot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "new_empty_bot" +version = "0.1.0" +edition = "2021" + +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/src/main.rs b/new_empty_bot/src/main.rs similarity index 71% rename from src/main.rs rename to new_empty_bot/src/main.rs index ebbe171..856f6b1 100644 --- a/src/main.rs +++ b/new_empty_bot/src/main.rs @@ -1,25 +1,22 @@ //! Example simple Binary crate that creates & runs bot based on `.env` -//! Be sure the followig is defined in `.env` +//! Be sure the followig is defined in `.env` //! - login_name //! - access_token //! - bot_channels //! - prefix //! - bot_admins -//! -//! Bot access tokens be generated here - +//! +//! Bot access tokens be generated here - //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> -use forcebot_rs_v2::botcore::bot::Bot; - +use forcebot_core::Bot; #[tokio::main] pub async fn main() { - /* 1. Create the bot using env */ - let bot = Bot::new(); + let bot = Bot::new().await; /* 2. Run the bot */ bot.run().await; - } diff --git a/readme.md b/readme.md index d192a1b..aa9355c 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -Twitch chat bot written in rust +Customizable Twitch chat bot written in rust # Quick Start @@ -22,28 +22,66 @@ bot_admins=ADMIN 3. Build & run ``` -cargo run +cargo run -p forcebot_core ``` +# Features + +## Built In Chat Commands + +- `quiet on` / `quiet off` - Moderators & Broadcasters can quiet the bot + +- `enable $module$` / `disable $module$` - Moderators & Broadcasters can enable or disable `Modules` of bot functionality through chat `Commands` + + +## Custom Modules can be coded to load additional functionality + +Developers an create Modules that add more bot functionality + +The main `forcebot_core` Binary crate includes the following Custom `Modules` + +- `debug` - outputs to console messages from the channel where it was enabled. Toggle debug with the Commands `debug on` or `debug off` +- `guest_badge` - Temporary badges can be issued to chatters +- `besty` - Tomfoolery +- `pyramid` - for detecting & handling pyramids + + +## `forcebot_core` Bot Library + +- `forcebot_core` library API provides Custom package developers a way to add functionality by adding `Modules` that contain Bot Objects like `Commands` and `Listeners` +- `Listeners` and `Commands` listen for a defined callback trigger condition and run an defined execution callback +- `Commands` are similar to `Listeners` with refined trigger conditions including using bot `prefix` with the `Command` , triggers based on `Badge` , and more +- Workspace for package developers to independently code their own `Modules` + +## Workspaces + + +Workspace comes with binary crates with working or example bots that use `forcebot_core` library +- `moderator_reactor` - bot kneels to all moderator messages +- `simple_module_example` - bot has a `test` `Module` with a `test` `Command` . Moderators & Broadcasters can manage the `Module` in chat with `enable` / `disable` `Commands` +- `new_empty_bot` - while empty, has `disable` and `enable` chat `Commands` . This is an example of the bot without any loaded modules +- `simple_command_bot` - bot responds to a `test` `Command`. As the command was not loaded through a `Module`, `disable` & `enable` commands don't work on the `test` command. This could be a Global `Command` +- `simple_debug_listener` - bot outputs all twitch `ServerMessages` received to terminal + + + # Example Bots -Use the following commands to build and run built-in bots. No coding required! +Use the following to build and run built-in bots. No coding required! -## New Bot +## New Empty Bot Run an empty simple bot that logs into chat and has minimum built in functions ``` -cargo run --bin new_bot +cargo run -p new_empty_bot ``` -## WIP Customized Fun Bot +## Full Featured Forcebot Run a forcebot with fun catered customizations -*ongoing work in progress* - ``` -cargo run --bin fun_bot +cargo run -p forcebot_core ``` @@ -51,28 +89,69 @@ cargo run --bin fun_bot Run a bot that listens to all messages and output to console ``` -cargo run --bin simple_debug_listener +cargo run -p simple_debug_listener ``` ## Simple Command Bot Run a bot that uses the `test` chat `Command` . `Commands` are prefixed and must be ran by a chatter with a `vip` badge or above ``` -cargo run --bin simple_command_bot +cargo run -p simple_command_bot ``` ## Moderator Reactor Run a bot that listens for messages with the `moderator` badge, and replies to that mod with an emote ``` -cargo run --bin moderator_reactor +cargo run -p moderator_reactor ``` ## Module loaded Bot Run a bot that has a `test` chat `Command`. As the command was loaded through a module, moderators or broadcastors can `enable` or `disable` the module through chat commands ``` -cargo run --bin simple_module +cargo run -p simple_module_example +``` + +# Workspace packages + +Source is a workspace of packages . In particular, `forcebot_core` is the main library crate to use + +*TIP* : if you want to start customizing you own bot, create a binary package in the workspace for your bot's binary crate + +More info about workspaces - https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html + + +## Creating a new package + +To create a new package + +1. Create a new package + +For example, to create a new binary crate in the workspace + +``` +cargo new my_new_bot +``` + +2. In the newly created directory for your package, adjust the `Cargo.toml` to the following + +``` +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" +``` + +3. Copy `main.rs` from the `new_empty_bot` package into your package + +4. Optionally, customize your `main()` to load modules before starting the bot + +5. Build and run your package +``` +cargo run -p my_new_bot ``` @@ -83,25 +162,55 @@ cargo run --bin simple_module Uses Env defined variables to create and run the bot ```rust -use forcebot_rs_v2::Bot; +use forcebot_core::Bot; #[tokio::main] pub async fn main() { /* 1. Create the bot using env */ - let bot = Bot::new(); + let bot = Bot::new().await; /* 2. Run the bot */ bot.run().await; } - ``` -## Customize with Modules +## Customize by Loading Custom Modules A `Module` is a group of bot objects (eg `Command`) that elevated users can manage through built in `disable` and `enable` commands +Custom `Modules` can be loaded into a new bot with minimum coding : just load the modules and run the bot + +```rust +use forcebot_core::{custom_mods::{debug, guest_badge, pyramid}, Bot}; + + + +#[tokio::main] +pub async fn main() { + + /* Create the bot using env */ + let bot = Bot::new().await; + + /* 1. Load the module into the bot */ + bot.load_module(funbot_objs::create_module()).await; + + /* 2. Load Custom Modules */ + bot.load_module(guest_badge::create_module()).await; + bot.load_module(pyramid::create_module()).await; + bot.load_module(debug::create_module()).await; + + + /* 3. Run the bot */ + bot.run().await; + +} +``` + + +## Create your own Custom Modules + Create a custom `Module` by : 1. Defining Functions that create the Custom Bot Objects (eg `Command`) @@ -110,16 +219,17 @@ Create a custom `Module` by : ```rust -use forcebot_rs_v2::Bot; + +use forcebot_core::Bot; #[tokio::main] pub async fn main() { /* Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* load the Module */ - bot.load_module(custom_mod::new()); + bot.load_module(custom_mod::new()).await; /* Run the bot */ bot.run().await; @@ -130,14 +240,16 @@ pub async fn main() { pub mod custom_mod { use std::sync::Arc; - use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module}; + use forcebot_core::{execution_async, 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,44 +258,45 @@ 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> { if let ServerMessage::Privmsg(msg) = message { - let _= bot.client.say_in_reply_to( + let _= bot.chat.lock().await.say_in_reply_to( &msg, "test return".to_string()).await; } Result::Err("Not Valid message type".to_string()) } /* 3. Set Command flags */ - cmd.set_exec_fn(asyncfn_box(execbody)); + cmd.set_exec_fn(execution_async(execbody)); cmd.set_admin_only(false); - cmd.set_min_badge(Badge::Moderator); + cmd.set_min_badge(Badge::Vip); cmd - } + } } - ``` ## Simple Debug Listener Bot with a simple listener that listens for all messages and prints in output ```rust + use std::sync::Arc; -use forcebot_rs_v2::{asyncfn_box, Bot, Listener}; +use forcebot_core::{execution_async, Bot, Listener}; use twitch_irc::message::ServerMessage; #[tokio::main] pub async fn main() { /* 1. Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* 2a. Create a new blank Listener */ let mut listener = Listener::new(); @@ -195,15 +308,15 @@ pub async fn main() { /* 2c. Define an async fn callback execution */ async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - dbg!(message); + dbg!(message); /* outputs message to debug */ Result::Ok("Success".to_string()) } - /* 2d. Set and Store the execution body using `async_box()` */ - listener.set_exec_fn(asyncfn_box(execbody)); + /* 2d. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); /* 3. Load the listener into the bot */ - bot.load_listener(listener); + bot.load_listener(listener).await; /* 4. Run the bot */ bot.run().await; @@ -217,11 +330,12 @@ pub async fn main() { Example listener listens for a moderator badge and reply in chat ```rust + use std::sync::Arc; -use forcebot_rs_v2::Bot; -use forcebot_rs_v2::asyncfn_box; -use forcebot_rs_v2::Listener; +use forcebot_core::Bot; +use forcebot_core::execution_async; +use forcebot_core::Listener; use twitch_irc::message::ServerMessage; @@ -229,16 +343,16 @@ use twitch_irc::message::ServerMessage; pub async fn main() { /* Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* 1. Create a new blank Listener */ let mut listener = Listener::new(); /* 2. Set a trigger condition function for listener */ + listener.set_trigger_cond_fn( |_:Arc<Bot>,message:ServerMessage| if let ServerMessage::Privmsg(msg) = message { - for badge in msg.badges { if matches!(badge, x if x.name == "moderator") { // dbg!("moderator found"); @@ -252,24 +366,23 @@ pub async fn main() { /* 3. Define an async fn callback execution */ async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { if let ServerMessage::Privmsg(msg) = message { - let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".to_string()).await ; + let _ = bot.chat.lock().await.say_in_reply_to(&msg, "pepeKneel".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)); + /* 4. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); /* 5. Load the listener into the bot */ - bot.load_listener(listener); + bot.load_listener(listener).await; /* Run the bot */ bot.run().await; } - ``` ## Simple Test Command @@ -277,9 +390,10 @@ pub async fn main() { ```rust use std::sync::Arc; -use forcebot_rs_v2::Bot; -use forcebot_rs_v2::asyncfn_box; -use forcebot_rs_v2::Command; +use forcebot_core::Badge; +use forcebot_core::Bot; +use forcebot_core::execution_async; +use forcebot_core::Command; use twitch_irc::message::ServerMessage; @@ -287,31 +401,31 @@ use twitch_irc::message::ServerMessage; pub async fn main() { /* Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* 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> { if let ServerMessage::Privmsg(msg) = message { - let _ = bot.client.say_in_reply_to(&msg, String::from("test success")).await; + let _ = bot.chat.lock().await.say_in_reply_to(&msg, String::from("test success")).await; return Result::Ok("Success".to_string()) ; } Result::Err("Not Valid message type".to_string()) } - /* 3. Set and Store the execution body using `async_box()` */ - cmd.set_exec_fn(asyncfn_box(execbody)); + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(execution_async(execbody)); /* 4. optionally, remove admin only default flag */ 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); + bot.load_command(cmd).await; /* Run the bot */ bot.run().await; @@ -320,16 +434,13 @@ pub async fn main() { ``` -# Crate Rust Documentation +# Crate Rust API Documentation -Create Crate documentation +Create `forcebot_rs_v2` Rust Crate documentation -Clean Build Documentation +Documentation - Clean Build & Open in Default Browser ``` -cargo clean && cargo doc +cargo clean && cargo doc --open ``` -Open Crate Doc -``` -cargo doc --open -``` + diff --git a/simple_command_bot/Cargo.toml b/simple_command_bot/Cargo.toml new file mode 100644 index 0000000..25f2f0f --- /dev/null +++ b/simple_command_bot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simple_command_bot" +version = "0.1.0" +edition = "2021" + +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/src/bin/simple_command_bot.rs b/simple_command_bot/src/main.rs similarity index 51% rename from src/bin/simple_command_bot.rs rename to simple_command_bot/src/main.rs index a6ec70e..c49e55c 100644 --- a/src/bin/simple_command_bot.rs +++ b/simple_command_bot/src/main.rs @@ -1,47 +1,50 @@ //! Bot with custom example commands that responds to caller if allowed -//! +//! //! Commands that are passed a blank prefix will use the bot prefix -//! -//! Be sure the followig is defined in `.env` +//! +//! Be sure the followig is defined in `.env` //! - login_name //! - access_token //! - bot_channels //! - prefix //! - bot_admins -//! -//! Bot access tokens be generated here - +//! +//! Bot access tokens be generated here - //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> use std::sync::Arc; -use forcebot_rs_v2::Badge; -use forcebot_rs_v2::Bot; -use forcebot_rs_v2::asyncfn_box; -use forcebot_rs_v2::Command; +use forcebot_core::execution_async; +use forcebot_core::Badge; +use forcebot_core::Bot; +use forcebot_core::Command; use twitch_irc::message::ServerMessage; - #[tokio::main] pub async fn main() { - /* Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* 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> { + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { if let ServerMessage::Privmsg(msg) = message { - let _ = bot.client.say_in_reply_to(&msg, String::from("test success")).await; - return Result::Ok("Success".to_string()) ; + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, String::from("test success")) + .await; + return Result::Ok("Success".to_string()); } - Result::Err("Not Valid message type".to_string()) + Result::Err("Not Valid message type".to_string()) } - /* 3. Set and Store the execution body using `async_box()` */ - cmd.set_exec_fn(asyncfn_box(execbody)); + /* 3. Set and Store the execution body using `execution_async()` */ + cmd.set_exec_fn(execution_async(execbody)); /* 4. optionally, remove admin only default flag */ cmd.set_admin_only(false); @@ -50,9 +53,8 @@ pub async fn main() { cmd.set_min_badge(Badge::Moderator); /* 6. Load the cmd into the bot */ - bot.load_command(cmd); + bot.load_command(cmd).await; /* Run the bot */ bot.run().await; - } diff --git a/simple_debug_listener/Cargo.toml b/simple_debug_listener/Cargo.toml new file mode 100644 index 0000000..fceb717 --- /dev/null +++ b/simple_debug_listener/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simple_debug_listener" +version = "0.1.0" +edition = "2021" + +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/src/bin/simple_debug_listener.rs b/simple_debug_listener/src/main.rs similarity index 54% rename from src/bin/simple_debug_listener.rs rename to simple_debug_listener/src/main.rs index ee129bd..b6dc626 100644 --- a/src/bin/simple_debug_listener.rs +++ b/simple_debug_listener/src/main.rs @@ -1,47 +1,43 @@ //! Example simple Binary crate that creates & runs bot based on `.env` -//! Be sure the followig is defined in `.env` +//! Be sure the followig is defined in `.env` //! - login_name //! - access_token //! - bot_channels //! - prefix //! - bot_admins -//! -//! Bot access tokens be generated here - +//! +//! Bot access tokens be generated here - //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> use std::sync::Arc; -use forcebot_rs_v2::{asyncfn_box, Bot, Listener}; +use forcebot_core::{execution_async, Bot, Listener}; use twitch_irc::message::ServerMessage; #[tokio::main] pub async fn main() { - /* 1. Create the bot using env */ - let mut bot = Bot::new(); + let bot = Bot::new().await; /* 2a. Create a new blank Listener */ let mut listener = Listener::new(); /* 2b. Set a trigger condition function for listener */ - listener.set_trigger_cond_fn( - |_:Arc<Bot>,_:ServerMessage| true - ); + listener.set_trigger_cond_fn(|_: Arc<Bot>, _: ServerMessage| true); /* 2c. Define an async fn callback execution */ - async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - dbg!(message); - Result::Ok("Success".to_string()) + async fn execbody(_: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + dbg!(message); /* outputs message to debug */ + Result::Ok("Success".to_string()) } - /* 2d. Set and Store the execution body using `async_box()` */ - listener.set_exec_fn(asyncfn_box(execbody)); + /* 2d. Set and Store the execution body using `execution_async()` */ + listener.set_exec_fn(execution_async(execbody)); /* 3. Load the listener into the bot */ - bot.load_listener(listener); + bot.load_listener(listener).await; /* 4. Run the bot */ bot.run().await; - } diff --git a/simple_module_example/Cargo.toml b/simple_module_example/Cargo.toml new file mode 100644 index 0000000..b00ba0b --- /dev/null +++ b/simple_module_example/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simple_module_example" +version = "0.1.0" +edition = "2021" + +[dependencies] +forcebot_core = {path = "../forcebot_core"} +dotenv = "0.15.0" +lazy_static = "1.5.0" +tokio = { version = "1.33.0", features = ["full"] } +twitch-irc = "5.0.1" diff --git a/simple_module_example/src/main.rs b/simple_module_example/src/main.rs new file mode 100644 index 0000000..0c07084 --- /dev/null +++ b/simple_module_example/src/main.rs @@ -0,0 +1,76 @@ +//! Simple Module with a Command +//! +//! Adding objects through packages provides controls , +//! such as moderators, and brodcasters can disable or enable mods +//! +//! Here, moderators or above can enable or disable the `test` +//! module with the command `<prefix> disable test` +//! +//! Be sure the followig is defined in `.env` +//! - login_name +//! - access_token +//! - bot_channels +//! - prefix +//! - bot_admins +//! +//! Bot access tokens be generated here - +//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> +//! - More Info - <https://dev.twitch.tv/docs/authentication> + +use forcebot_core::Bot; + +#[tokio::main] +pub async fn main() { + /* Create the bot using env */ + let bot = Bot::new().await; + + /* load the Module */ + bot.load_module(custom_mod::new()).await; + + /* Run the bot */ + bot.run().await; +} + +pub mod custom_mod { + use std::sync::Arc; + + use forcebot_core::{execution_async, Badge, Bot, Command, Module}; + use twitch_irc::message::ServerMessage; + + /// Module definition with a loaded command + pub fn new() -> Module { + /* 1. Create a new module */ + 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()); + + 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()); + + /* 2. Define exec callback */ + async fn execbody(bot: Arc<Bot>, message: ServerMessage) -> Result<String, String> { + if let ServerMessage::Privmsg(msg) = message { + let _ = bot + .chat + .lock() + .await + .say_in_reply_to(&msg, "test return".to_string()) + .await; + } + Result::Err("Not Valid message type".to_string()) + } + + /* 3. Set Command flags */ + cmd.set_exec_fn(execution_async(execbody)); + cmd.set_admin_only(false); + cmd.set_min_badge(Badge::Vip); + + cmd + } +} diff --git a/src/bin/fun_bot.rs b/src/bin/fun_bot.rs deleted file mode 100644 index de67fda..0000000 --- a/src/bin/fun_bot.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! WIP Fun forcebot with catered customizations #todo -//! -//! Custom modules that can be managed in chat through `disable` and `enable` commands -//! - funbot -//! -//! Be sure the followig is defined in `.env` -//! - login_name -//! - access_token -//! - bot_channels -//! - prefix -//! - bot_admins -//! -//! Bot access tokens be generated here - -//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> -//! - More Info - <https://dev.twitch.tv/docs/authentication> - -use forcebot_rs_v2::Bot; - -#[tokio::main] -pub async fn main() { - - /* Create the bot using env */ - let mut bot = Bot::new(); - - /* 1. Load the module into the bot */ - bot.load_module(funbot_objs::create_module()); - - /* 2. Run the bot */ - bot.run().await; - -} - - -pub mod funbot_objs { - use std::sync::Arc; - - use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module}; - use twitch_irc::message::ServerMessage; - - /// Create a Module with a loaded Command object - pub fn create_module() -> Module { - let mut custom_mod = Module::new("funbot".to_string(), "".to_string()); - - custom_mod.load_command(create_cmd_test()); - - custom_mod - } - - /// Create a Command Object - fn create_cmd_test() -> Command { - - let mut cmd = Command::new("remind besty".to_string(),"annytfYandere ".to_string()); - - async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - if let ServerMessage::Privmsg(msg) = message { - let _= bot.client.say_in_reply_to( - &msg, "annytfYandere he's mine".to_string()).await; - return Result::Ok("Success".to_string()); - } - Result::Err("Not Valid message type".to_string()) - } - - cmd.set_exec_fn(asyncfn_box(execbody)); - - cmd.set_admin_only(false); - cmd.set_min_badge(Badge::Vip); - cmd - - } -} \ No newline at end of file diff --git a/src/bin/moderator_reactor.rs b/src/bin/moderator_reactor.rs deleted file mode 100644 index a7bb62c..0000000 --- a/src/bin/moderator_reactor.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Simple bot with a custom listeners that listens for moderators and respond to the moderator -//! -//! Be sure the followig is defined in `.env` -//! - login_name -//! - access_token -//! - bot_channels -//! - prefix -//! - bot_admins -//! -//! Bot access tokens be generated here - -//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> -//! - More Info - <https://dev.twitch.tv/docs/authentication> - -use std::sync::Arc; - -use forcebot_rs_v2::Bot; -use forcebot_rs_v2::asyncfn_box; -use forcebot_rs_v2::Listener; -use twitch_irc::message::ServerMessage; - - -#[tokio::main] -pub async fn main() { - - /* Create the bot using env */ - let mut bot = Bot::new(); - - /* 1. Create a new blank Listener */ - let mut listener = Listener::new(); - - /* 2. Set a trigger condition function for listener */ - - listener.set_trigger_cond_fn( - |_:Arc<Bot>,message:ServerMessage| - if let ServerMessage::Privmsg(msg) = message { - // dbg!(msg.clone()); - for badge in msg.badges { - if matches!(badge, x if x.name == "moderator") { - // dbg!("moderator found"); - return true; - } - } - false - } else { false } - ); - - /* 3. Define an async fn callback execution */ - async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - if let ServerMessage::Privmsg(msg) = message { - let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".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)); - - /* 5. Load the listener into the bot */ - bot.load_listener(listener); - - /* Run the bot */ - bot.run().await; - -} diff --git a/src/botcore.rs b/src/botcore.rs deleted file mode 100644 index 678d9a9..0000000 --- a/src/botcore.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod bot; -pub mod bot_objects; -pub mod modules; \ No newline at end of file diff --git a/src/botcore/bot.rs b/src/botcore/bot.rs deleted file mode 100644 index 89598af..0000000 --- a/src/botcore/bot.rs +++ /dev/null @@ -1,238 +0,0 @@ - - -use tokio::sync::{mpsc::UnboundedReceiver, Mutex}; -use twitch_irc::{login::StaticLoginCredentials, message::ServerMessage, SecureTCPTransport, TwitchIRCClient}; -use dotenv::dotenv; -use std::{env, sync::Arc}; - -use crate::{Command, Listener, Module}; - -use super::bot_objects::built_in_objects; - - -/// Twitch chat bot -pub struct Bot -{ - /// Prefix for commands - prefix: String, - /// inbound chat msg stream - incoming_msgs: Mutex<UnboundedReceiver<ServerMessage>>, - /// outbound chat client msg stream - pub client: TwitchIRCClient<SecureTCPTransport,StaticLoginCredentials>, - /// joined channels - botchannels: Vec<String>, - /// admin chatters - admins : Vec<String>, - /// listeners - listeners: Vec<Listener>, - /// commands - commands: Vec<Command>, - /// modules - modules: Vec<Module>, - /// channel module status - channel_module_status: Mutex<Vec<(String,String,String)>> -} - - -impl Bot -{ - /// Creates a new `Bot` using env variables - /// - /// Be sure the following is defined in an `.env` file - /// - login_name - /// - access_token - /// - bot_channels - /// - prefix - /// - bot_admins - pub fn new() -> Bot { - - dotenv().ok(); - let bot_login_name = env::var("login_name").unwrap().to_owned(); - let oauth_token = env::var("access_token").unwrap().to_owned(); - let prefix = env::var("prefix") - .unwrap() - .to_owned(); - - let mut botchannels = Vec::new(); - - for chnl in env::var("bot_channels").unwrap().split(',') { - botchannels.push(chnl.to_owned()); - } - - Bot::new_from(bot_login_name, oauth_token, prefix, botchannels) - - - } - - /// Creates a new `Bot` using bot information - /// - /// Bot joined channels will include channels from `.env` and `botchannels` argument - pub fn new_from(bot_login_name:String,oauth_token:String,prefix:String,botchannels:Vec<String>) -> Bot { - - dotenv().ok(); - let bot_login_name = bot_login_name; - - let config = twitch_irc::ClientConfig::new_simple(StaticLoginCredentials::new( - bot_login_name.to_owned(), - Some(oauth_token.to_owned()), - )); - - let (incoming_messages, client) = - TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config); - - let mut botchannels_all = Vec::new(); - botchannels_all.extend(botchannels); - - for chnl in env::var("bot_channels").unwrap().split(',') { - botchannels_all.push(chnl.to_owned()); - } - - - let mut admins = Vec::new(); - - if let Ok(value) = env::var("bot_admins") { - for admin in value.split(',') { - admins.push(String::from(admin)) - } - } - - - let mut bot = Bot { - prefix, - incoming_msgs : Mutex::new(incoming_messages), - client, - botchannels : botchannels_all, - listeners : vec![], - commands : vec![], - admins, - modules: vec![], - channel_module_status: Mutex::new(vec![]), - }; - - for cmd in built_in_objects::create_commands() { - bot.load_command(cmd); - } - - bot - } - - /// Runs the bot - pub async fn run(self) { - - for chnl in &self.botchannels { - self.client.join(chnl.to_owned()).unwrap(); - } - - let bot = Arc::new(self); - - let join_handle = tokio::spawn(async move { - - let a = bot.clone(); - let mut in_msgs_lock = a.incoming_msgs.lock().await; - - while let Some(message) = in_msgs_lock.recv().await { - for listener in &(*bot).listeners { - - let a = listener.clone(); - if a.cond_triggered(bot.clone(),message.clone()) { - - let _ = listener.execute_fn(bot.clone(),message.clone()).await; - } - } - - if let ServerMessage::Privmsg(msg) = message.clone() { - - for cmd in &(*bot).commands { - - let a = cmd.clone(); - if a.command_triggered(bot.clone(),msg.clone()) { - - let _ = cmd.execute_fn(bot.clone(),message.clone()).await; - } - } - - - for module in &(*bot).modules { - - let cms_lock = bot.channel_module_status.lock().await; - if cms_lock.contains(&(msg.channel_login.clone(),module.get_name(),"disabled".to_string())) - { continue; } - - for listener in module.get_listeners() { - - let a = listener.clone(); - if a.cond_triggered(bot.clone(),message.clone()) { - - let _ = listener.execute_fn(bot.clone(),message.clone()).await; - } - } - for cmd in module.get_commands() { - - let a = cmd.clone(); - if a.command_triggered(bot.clone(),msg.clone()) { - - let _ = cmd.execute_fn(bot.clone(),message.clone()).await; - } - } - - } - - } else {} ; - - - } - drop(in_msgs_lock); - }); - - join_handle.await.unwrap(); - } - - /// Loads a `Listener` into the bot - pub fn load_listener(&mut self,l : Listener) { - self.listeners.push(l); - } - - /// Loads a `Command` into the bot - pub fn load_command(&mut self,c : Command) { - self.commands.push(c); - } - - - - pub fn get_prefix(&self) -> String { - self.prefix.clone() - } - - pub fn get_admins(&self) -> Vec<String> { - self.admins.clone() - } - - /// loads a `Module` and its bot objects - pub fn load_module(&mut self,m: Module) { - self.modules.push(m) - } - - pub async fn disable_module(&self,channel:String,module:String){ - let mut lock = self.channel_module_status.lock().await; - if !lock.contains(&(channel.clone(),module.clone(),"disabled".to_string())) { - lock.push((channel,module,"disabled".to_string())); - } - } - - pub async fn enable_module(&self,channel:String,module:String){ - let mut lock = self.channel_module_status.lock().await; - if lock.contains(&(channel.clone(),module.clone(),"disabled".to_string())) { - - let index = lock - .iter() - .position(|x| *x == - (channel.clone(),module.clone(),"disabled".to_string())) - .unwrap(); - lock.remove(index); - } - } - - -} - - diff --git a/src/botcore/bot_objects.rs b/src/botcore/bot_objects.rs deleted file mode 100644 index 9fb6985..0000000 --- a/src/botcore/bot_objects.rs +++ /dev/null @@ -1,119 +0,0 @@ -pub mod listener; -pub mod command; - - -use std::boxed::Box; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use twitch_irc::message::ServerMessage; - -use super::bot::Bot; - - -/// chat badge -#[derive(Clone)] -pub enum Badge { - Moderator, - Broadcaster, - Vip -} - - -pub type ExecBody = Box< - dyn Fn(Arc<Bot>,ServerMessage) -> Pin<Box<dyn Future<Output = Result<String,String>> + Send>> + Send + Sync, ->; - -pub fn asyncfn_box<T>(f: fn(Arc<Bot>,ServerMessage) -> T) -> ExecBody -where - T: Future<Output = Result<String,String>> + Send + 'static, -{ - Box::new(move |a,b| Box::pin(f(a,b))) -} - - - -/// collection of functions to create built in objects -pub mod built_in_objects { - - - use std::sync::Arc; - - use twitch_irc::message::ServerMessage; - - use crate::{asyncfn_box, Badge, Bot, Command}; - - - /// create a vector of command build in objects - pub fn create_commands() -> Vec<Command> - { - let mut cmds = vec![]; - - cmds.push(create_disable_cmd()); - cmds.push(create_enable_cmd()); - - cmds - - } - - fn create_disable_cmd() -> Command { - /* 1. Create a new blank cmd */ - let mut cmd = Command::new("disable".to_string(),"".to_string()); - - /* 2. Define an async fn callback execution */ - async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - if let ServerMessage::Privmsg(msg) = message { - for (i,arg) in msg.message_text.split(" ").enumerate() { - if i > 1 { - bot.disable_module(msg.channel_login.clone(), arg.to_string()).await; - } - } - let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ; - } - Result::Err("Not Valid message type".to_string()) - } - - /* 3. Set and Store the execution body using `async_box()` */ - cmd.set_exec_fn(asyncfn_box(execbody)); - - /* 4. optionally, remove admin only default flag */ - cmd.set_admin_only(false); - - /* 5. optionally, set min badge*/ - cmd.set_min_badge(Badge::Moderator); - cmd - - } - - fn create_enable_cmd() -> Command { - /* 1. Create a new blank cmd */ - let mut cmd = Command::new("enable".to_string(),"".to_string()); - - /* 2. Define an async fn callback execution */ - async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { - if let ServerMessage::Privmsg(msg) = message { - for (i,arg) in msg.message_text.split(" ").enumerate() { - if i > 1 { - bot.enable_module(msg.channel_login.clone(), arg.to_string()).await; - } - } - - let _ = bot.client.say_in_reply_to(&msg, String::from("Enabled!")).await ; - } - Result::Err("Not Valid message type".to_string()) - } - - /* 3. Set and Store the execution body using `async_box()` */ - cmd.set_exec_fn(asyncfn_box(execbody)); - - /* 4. optionally, remove admin only default flag */ - cmd.set_admin_only(false); - - /* 5. optionally, set min badge*/ - cmd.set_min_badge(Badge::Moderator); - cmd - } - - -} \ No newline at end of file diff --git a/src/botcore/bot_objects/command.rs b/src/botcore/bot_objects/command.rs deleted file mode 100644 index 574a22e..0000000 --- a/src/botcore/bot_objects/command.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::sync::Arc; - -use twitch_irc::message::{PrivmsgMessage, ServerMessage}; - -use crate::{asyncfn_box, botcore::bot::Bot, Badge}; - -use super::ExecBody; - -/// Bot `Command` that stores trigger condition callback and a execution functon -/// -/// A prefix character or phrase can be defined for the bot to evaluate a trigger condition -/// -/// A command or command phrase defines the phrase after the prefix phrase -/// -/// If no min badge role is provided, Broadcaster is defaulted. All commands require at least a vip role -/// -/// AdminOnly commands can only be ran by admin -/// -/// Use `asyncfn_box()` on custom async execution bodies -#[derive(Clone)] -pub struct Command -{ - command : String, - exec_fn : Arc<ExecBody>, - min_badge : Badge, - admin_only : bool, - prefix : String, - custom_cond_fn : fn(Arc<Bot>,PrivmsgMessage) -> bool, -} - -impl Command -{ - - /// Creates a new empty `Command` using command `String` and prefix `String` - /// Pass an empty string prefix if the bot should use the bot default - /// - /// Call `set_trigger_cond_fn()` and `set_exec_fn()` to trigger & execution function callbacks - /// if a blank prefix is given, the bot will look for the bot prefix instead - /// - /// By default, the new command is admin_only - pub fn new(command:String,prefix:String) -> Command { - - async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> - { Result::Ok("success".to_string()) } - - Command { - command , - prefix , - exec_fn : Arc::new(asyncfn_box(execbody)), - min_badge : Badge::Vip, - admin_only : true, - custom_cond_fn : |_:Arc<Bot>,_:PrivmsgMessage| true, - } - } - - /// set a trigger conditin callback that returns true if the listener shoud trigger - pub fn set_custom_cond_fn(&mut self,cond_fn: fn(Arc<Bot>,PrivmsgMessage) -> bool) { - self.custom_cond_fn = cond_fn; - } - - /// sets the execution body of the listener for when it triggers - /// - /// Use`asyncfn_box()` on the async fn when storing - /// - /// - pub fn set_exec_fn(&mut self,exec_fn:ExecBody ) { - self.exec_fn = Arc::new(exec_fn); - } - - /// checks if the trigger condition is met - /// specifically if the message is a valid command and min badge roles provided - /// - pub fn command_triggered(&self,bot:Arc<Bot>,msg:PrivmsgMessage) -> bool { - - fn cmd_called(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool { - let mut prefixed_cmd = "".to_string(); - if cmd.prefix == "" { - prefixed_cmd.push_str(&bot.get_prefix()); - } else { - prefixed_cmd.push_str(&cmd.prefix); - } - prefixed_cmd.push_str(&cmd.command); - return message.message_text.starts_with(prefixed_cmd.as_str()) - } - - - fn caller_badge_ok(cmd:&Command,_bot:Arc<Bot>,message:PrivmsgMessage) -> bool { - for badge in message.badges { - - match cmd.min_badge { - Badge::Broadcaster => { - if badge.name == "broadcaster" { return true } - else { return false } - }, - Badge::Moderator => { - match badge.name.as_str() { - "moderator" | "broadcaster" => return true, - _ => (), - } - }, - Badge::Vip => { - match badge.name.as_str() { - "vip" | "moderator" | "broadcaster" => return true, - _ => (), - } - }, - } - } - - return false; - } - - - /// determines if the command caller can run the command - /// based on admin_only flag - /// - /// callers who are admins can run admin_only commands - /// callers can run non-admin_only commands - fn admin_only_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool { - if (cmd.admin_only && bot.get_admins().contains(&message.sender.login)) || !cmd.admin_only { - return true; - } else { - return false; - } - } - - fn custom_cond_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool { - (cmd.custom_cond_fn)(bot,message) - } - - // dbg!(msg.clone()); - - // dbg!(caller_badge_ok(self, bot.clone(), msg.clone())); - // dbg!(cmd_called(self, bot.clone(), msg.clone()) && - // caller_badge_ok(self, bot.clone(), msg.clone()) && - // admin_only_ok(self, bot.clone(), msg.clone()) && - // custom_cond_ok(self, bot.clone(), msg.clone())); - - cmd_called(self, bot.clone(), msg.clone()) && - caller_badge_ok(self, bot.clone(), msg.clone()) && - admin_only_ok(self, bot.clone(), msg.clone()) && - custom_cond_ok(self, bot, msg) - - } - - /// executes the listeners executon body - pub async fn execute_fn(&self,bot:Arc<Bot>,msg:ServerMessage) -> Result<String, String> { - (self.exec_fn)(bot,msg).await - } - - - /// sets min_badge to run the cmd - // pub fn set_min_badge(&mut self,min_badge:String) { - pub fn set_min_badge(&mut self,min_badge:Badge) { - self.min_badge = min_badge - } - - /// sets admin_only - pub fn set_admin_only(&mut self,admin_only:bool) { - self.admin_only = admin_only - } -} diff --git a/src/botcore/bot_objects/listener.rs b/src/botcore/bot_objects/listener.rs deleted file mode 100644 index 2bebb8e..0000000 --- a/src/botcore/bot_objects/listener.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::Arc; - -use twitch_irc::message::ServerMessage; - -use crate::{asyncfn_box, Bot}; - -use super::ExecBody; - -/// Bot `Listener`` that stores trigger condition callback and a execution functon -/// -/// Use `asyncfn_box()` on custom async execution bodies -#[derive(Clone)] -pub struct Listener -{ - /// trigger condition - trigger_cond_fn : fn(Arc<Bot>,ServerMessage) -> bool, - /// execution body - exec_fn : Arc<ExecBody>, -} - -impl Listener -{ - - /// Creates a new empty `Listener` . - /// Call `set_trigger_cond_fn()` and `set_exec_fn()` to trigger & execution function callbacks - pub fn new() -> Listener { - - async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> {Result::Ok("success".to_string()) } - - Listener { - trigger_cond_fn : |_:Arc<Bot>,_:ServerMessage| false, - exec_fn : Arc::new(asyncfn_box(execbody)) - } - } - - /// set a trigger conditin callback that returns true if the listener shoud trigger - pub fn set_trigger_cond_fn(&mut self,cond_fn: fn(Arc<Bot>,ServerMessage) -> bool) { - self.trigger_cond_fn = cond_fn; - } - - /// sets the execution body of the listener for when it triggers - /// - /// Use`asyncfn_box()` on the async fn when storing - /// - /// Example - - /// ```rust - /// /* 1. Create a new blank Listener */ - /// let mut listener = Listener::new(); - /// - /// /* 2. define an async function */ - /// async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> {Result::Ok("success".to_string()) } - /// - /// /* 3. Set and Store the execution body using `async_box()` */ - /// listener.set_exec_fn(asyncfn_box(execbody)); - /// ``` - /// - pub fn set_exec_fn(&mut self,exec_fn:ExecBody ) { - self.exec_fn = Arc::new(exec_fn); - } - - /// checks if the trigger condition is met - pub fn cond_triggered(&self,bot:Arc<Bot>,msg:ServerMessage) -> bool { - (self.trigger_cond_fn)(bot,msg) - } - - /// executes the listeners executon body - pub async fn execute_fn(&self,bot:Arc<Bot>,msg:ServerMessage) -> Result<String, String> { - // (self.exec_fn)(bot,msg) - (self.exec_fn)(bot,msg).await - } -} - diff --git a/src/botcore/modules.rs b/src/botcore/modules.rs deleted file mode 100644 index e57ecc2..0000000 --- a/src/botcore/modules.rs +++ /dev/null @@ -1,51 +0,0 @@ - - -use crate::{Command, Listener}; - - -/// Bot `Module` that groups a set of `bot_objects` -/// -/// Elevated chatters can disable modules by their name or chat alias -pub struct Module -{ - name: String, - _alias: String, - listeners: Vec<Listener>, - commands: Vec<Command>, -} - -impl Module -{ - /// create a new module - pub fn new(name:String,alias:String) -> Module { - Module { - name, - _alias: alias, - listeners: vec![], - commands: vec![] - } - } - - /// Loads a `Listener` into the module - pub fn load_listener(&mut self,l : Listener) { - self.listeners.push(l); - } - - /// Loads a `Command` into the module - pub fn load_command(&mut self,c : Command) { - self.commands.push(c); - } - - pub fn get_listeners(&self) -> Vec<Listener> { - self.listeners.clone() - } - - pub fn get_commands(&self) -> Vec<Command> { - self.commands.clone() - } - - pub fn get_name(&self) -> String { - self.name.clone() - } - -} \ No newline at end of file diff --git a/src/custom_mods.rs b/src/custom_mods.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9ecb36f..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,206 +0,0 @@ -//! `forcebot-rs-v2` : Twitch chat bot written in rust. -//! -//! Customize by adding additional bot objects -//! -//! ## New Bot -//! -//! Uses Env defined variables to create and run the bot -//! -//! ```rust -//! use forcebot_rs_v2::Bot; -//! -//! #[tokio::main] -//! pub async fn main() { -//! -//! /* 1. Create the bot using env */ -//! let bot = Bot::new(); -//! -//! /* 2. Run the bot */ -//! bot.run().await; -//! -//! } -//! -//! ``` -//! -//! -//! ## Customize with Modules -//! -//! A `Module` is a group of bot objects (eg `Command`) that elevated users can manage through built in `disable` and `enable` commands -//! -//! Create a custom `Module` by : -//! -//! 1. Defining Functions that create the Custom Bot Objects (eg `Command`) -//! -//! 2. Define a function that creates a `Module` with the Custom Bot Objects loaded -//! -//! -//! ```rust -//! use forcebot_rs_v2::Bot; -//! -//! #[tokio::main] -//! pub async fn main() { -//! -//! /* Create the bot using env */ -//! let mut bot = Bot::new(); -//! -//! /* load the Module */ -//! bot.load_module(custom_mod::new()); -//! -//! /* Run the bot */ -//! bot.run().await; -//! -//! } -//! -//! -//! 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 -//! pub fn new() -> Module { -//! /* 1. Create a new module */ -//! let mut custom_mod = Module::new("test".to_string(), "".to_string()); -//! -//! /* 2. Load the cmd into a new module */ -//! custom_mod.load_command(cmd_test()); -//! -//! custom_mod -//! -//! } -//! -//! pub fn cmd_test() -> Command { -//! /* 1. Create a new cmd */ -//! let mut cmd = Command::new("test".to_string(),"".to_string()); -//! -//! /* 2. Define exec callback */ -//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { -//! if let ServerMessage::Privmsg(msg) = message { -//! let _= bot.client.say_in_reply_to( -//! &msg, "test return".to_string()).await; -//! } -//! Result::Err("Not Valid message type".to_string()) -//! } -//! -//! /* 3. Set Command flags */ -//! cmd.set_exec_fn(asyncfn_box(execbody)); -//! cmd.set_admin_only(false); -//! cmd.set_min_badge(Badge::Moderator); -//! -//! cmd -//! } -//! } -//! -//! ``` -//! -//! ## Simple Debug Listener -//! Bot with a simple listener that listens for all messages and prints in output -//! -//! ```rust -//! use std::sync::Arc; -//! -//! use forcebot_rs_v2::{asyncfn_box, Bot, Listener}; -//! use twitch_irc::message::ServerMessage; -//! -//! #[tokio::main] -//! pub async fn main() { -//! -//! /* 1. Create the bot using env */ -//! let mut bot = Bot::new(); -//! -//! /* 2a. Create a new blank Listener */ -//! let mut listener = Listener::new(); -//! -//! /* 2b. Set a trigger condition function for listener */ -//! listener.set_trigger_cond_fn( -//! |_:Arc<Bot>,_:ServerMessage| true -//! ); -//! -//! /* 2c. Define an async fn callback execution */ -//! async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { -//! dbg!(message); -//! Result::Ok("Success".to_string()) -//! } -//! -//! /* 2d. Set and Store the execution body using `async_box()` */ -//! listener.set_exec_fn(asyncfn_box(execbody)); -//! -//! /* 3. Load the listener into the bot */ -//! bot.load_listener(listener); -//! -//! /* 4. Run the bot */ -//! bot.run().await; -//! -//! } -//! -//! ``` -//! -//! ## Modertor Reactor -//! -//! ``` -//! -//! use std::sync::Arc; -//! -//! use forcebot_rs_v2::Bot; -//! use forcebot_rs_v2::asyncfn_box; -//! use forcebot_rs_v2::Listener; -//! use twitch_irc::message::ServerMessage; -//! -//! -//! #[tokio::main] -//! pub async fn main() { -//! -//! /* Create the bot using env */ -//! let mut bot = Bot::new(); -//! -//! /* 1. Create a new blank Listener */ -//! let mut listener = Listener::new(); -//! -//! /* 2. Set a trigger condition function for listener */ -//! -//! listener.set_trigger_cond_fn( -//! |_:Arc<Bot>,message:ServerMessage| -//! if let ServerMessage::Privmsg(msg) = message { -//! // dbg!(msg.clone()); -//! for badge in msg.badges { -//! if matches!(badge, x if x.name == "moderator") { -//! // dbg!("moderator found"); -//! return true; -//! } -//! } -//! false -//! } else { false } -//! ); -//! -//! /* 3. Define an async fn callback execution */ -//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> { -//! if let ServerMessage::Privmsg(msg) = message { -//! let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".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)); -//! -//! /* 5. Load the listener into the bot */ -//! bot.load_listener(listener); -//! -//! /* Run the bot */ -//! bot.run().await; -//! -//! } - - - -pub mod botcore; -pub use crate::botcore::bot::Bot; -pub use crate::botcore::bot_objects::asyncfn_box; -pub use crate::botcore::bot_objects::listener::Listener; -pub use crate::botcore::bot_objects::command::Command; -pub use crate::botcore::modules::Module; -pub use crate::botcore::bot_objects::Badge; -