diff --git a/Cargo.toml b/Cargo.toml index 436b75c..53faa8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,21 @@ 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" \ No newline at end of file +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 diff --git a/readme.md b/readme.md index 75ccfba..d192a1b 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,335 @@ -Twitch bot written in rust +Twitch chat bot written in rust + +# Quick Start + +Run a Simple bot with Built in functionality + +1. Generate a twitch access token + + - Get a Bot Chat Token here - https://twitchtokengenerator.com + - More Info - https://dev.twitch.tv/docs/authentication + +2. Define an `.env` file with the following + +``` +login_name=BOTNAME +access_token=ACCESS_TOKEN +bot_channels=BOTNAME +prefix=` +bot_admins=ADMIN +``` + +3. Build & run -# Compile & Run ``` cargo run -``` \ No newline at end of file +``` + +# Example Bots + +Use the following commands to build and run built-in bots. No coding required! + +## New Bot +Run an empty simple bot that logs into chat and has minimum built in functions + +``` +cargo run --bin new_bot +``` + +## WIP Customized Fun Bot + +Run a forcebot with fun catered customizations + +*ongoing work in progress* + +``` +cargo run --bin fun_bot +``` + + +## Simple Debug Listener +Run a bot that listens to all messages and output to console + +``` +cargo run --bin 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 +``` + +## 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 +``` + +## 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 +``` + + +# Example Code + +## 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; + +} + +``` + +## Moderator Reactor + +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 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 `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; + +} + + +``` + +## Simple Test Command + +```rust +use std::sync::Arc; + +use forcebot_rs_v2::Bot; +use forcebot_rs_v2::asyncfn_box; +use forcebot_rs_v2::Command; +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 cmd */ + let mut cmd = Command::new("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; + 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)); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(false); + + /* 5. optionally, set min badge*/ + cmd.set_min_badge("broadcaster".to_string()); +// + /* 6. Load the cmd into the bot */ + bot.load_command(cmd); + + /* Run the bot */ + bot.run().await; + +} + +``` + +# Crate Rust Documentation + +Create Crate documentation + +Clean Build Documentation +``` +cargo clean && cargo doc +``` + +Open Crate Doc +``` +cargo doc --open +``` diff --git a/src/bin/fun_bot.rs b/src/bin/fun_bot.rs new file mode 100644 index 0000000..de67fda --- /dev/null +++ b/src/bin/fun_bot.rs @@ -0,0 +1,70 @@ +//! 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 new file mode 100644 index 0000000..a7bb62c --- /dev/null +++ b/src/bin/moderator_reactor.rs @@ -0,0 +1,65 @@ +//! 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/bin/new_bot.rs b/src/bin/new_bot.rs new file mode 100644 index 0000000..cbe682b --- /dev/null +++ b/src/bin/new_bot.rs @@ -0,0 +1,24 @@ +//! Example simple Binary crate that creates & runs bot based on `.env` +//! 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() { + + /* 1. Create the bot using env */ + let bot = Bot::new(); + + /* 2. Run the bot */ + bot.run().await; + +} diff --git a/src/bin/simple_command_bot.rs b/src/bin/simple_command_bot.rs new file mode 100644 index 0000000..a6ec70e --- /dev/null +++ b/src/bin/simple_command_bot.rs @@ -0,0 +1,58 @@ +//! 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` +//! - 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::Badge; +use forcebot_rs_v2::Bot; +use forcebot_rs_v2::asyncfn_box; +use forcebot_rs_v2::Command; +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 cmd */ + let mut cmd = Command::new("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; + 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)); + + /* 4. optionally, remove admin only default flag */ + cmd.set_admin_only(false); + + /* 5. optionally, set min badge*/ + cmd.set_min_badge(Badge::Moderator); + + /* 6. Load the cmd into the bot */ + bot.load_command(cmd); + + /* Run the bot */ + bot.run().await; + +} diff --git a/src/bin/simple_debug_listener.rs b/src/bin/simple_debug_listener.rs new file mode 100644 index 0000000..ee129bd --- /dev/null +++ b/src/bin/simple_debug_listener.rs @@ -0,0 +1,47 @@ +//! Example simple Binary crate that creates & runs bot based on `.env` +//! 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::{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; + +} diff --git a/src/bin/simple_module.rs b/src/bin/simple_module.rs new file mode 100644 index 0000000..61d66e5 --- /dev/null +++ b/src/bin/simple_module.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_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 + } +} \ No newline at end of file diff --git a/src/botcore.rs b/src/botcore.rs index e69de29..678d9a9 100644 --- a/src/botcore.rs +++ b/src/botcore.rs @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..89598af --- /dev/null +++ b/src/botcore/bot.rs @@ -0,0 +1,238 @@ + + +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 new file mode 100644 index 0000000..9fb6985 --- /dev/null +++ b/src/botcore/bot_objects.rs @@ -0,0 +1,119 @@ +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 new file mode 100644 index 0000000..574a22e --- /dev/null +++ b/src/botcore/bot_objects/command.rs @@ -0,0 +1,162 @@ +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 new file mode 100644 index 0000000..2bebb8e --- /dev/null +++ b/src/botcore/bot_objects/listener.rs @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000..e57ecc2 --- /dev/null +++ b/src/botcore/modules.rs @@ -0,0 +1,51 @@ + + +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/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9ecb36f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,206 @@ +//! `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; + diff --git a/src/main.rs b/src/main.rs index 6448eeb..ebbe171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,25 @@ -use dotenv::dotenv; -use twitch_irc::{login::StaticLoginCredentials, ClientConfig, SecureTCPTransport, TwitchIRCClient}; -use std::env; +//! Example simple Binary crate that creates & runs bot based on `.env` +//! 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::botcore::bot::Bot; -mod botcore; #[tokio::main] pub async fn main() { - - dotenv().ok(); - let login_name = env::var("login_name").unwrap().to_owned(); - let oauth_token = env::var("access_token").unwrap().to_owned(); - let mut botchannels = Vec::new(); + /* 1. Create the bot using env */ + let bot = Bot::new(); - for chnl in env::var("bot_channels").unwrap().split(',') { - botchannels.push(chnl.to_owned()); - } - - let config = ClientConfig::new_simple(StaticLoginCredentials::new( - login_name.to_owned(), - Some(oauth_token.to_owned()), - )); - - let (mut incoming_messages, client) = - TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config); - - for chnl in botchannels { - client.join(chnl.to_owned()).unwrap(); - } - - let join_handle = tokio::spawn(async move { - while let Some(message) = incoming_messages.recv().await { - println!("Received message: {:?}", message); - } - }); - - join_handle.await.unwrap(); + /* 2. Run the bot */ + bot.run().await; }