From ef344402abbc58cfd65ae4dd959be01571c92be1 Mon Sep 17 00:00:00 2001 From: modulatingforce <modulatingforce@gmail.com> Date: Sun, 26 Jan 2025 18:17:36 -0500 Subject: [PATCH] listener obj --- Cargo.toml | 15 ++++- readme.md | 88 +++++++++++++++++++++++++++-- src/bin/simple_bot.rs | 24 ++++++++ src/bin/simple_bot_listener.rs | 66 ++++++++++++++++++++++ src/botcore.rs | 3 +- src/botcore/bot.rs | 42 +++++++++++--- src/botcore/bot_objects.rs | 1 + src/botcore/bot_objects/listener.rs | 83 +++++++++++++++++++++++++++ src/lib.rs | 82 +++++++++++++++++++++++++++ src/main.rs | 3 +- 10 files changed, 390 insertions(+), 17 deletions(-) create mode 100644 src/bin/simple_bot.rs create mode 100644 src/bin/simple_bot_listener.rs create mode 100644 src/botcore/bot_objects.rs create mode 100644 src/botcore/bot_objects/listener.rs create mode 100644 src/lib.rs 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 e77154e..4af0b0c 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ Twitch chat bot written in rust # Quick Start -Runs the bot's binary crate +Run a Simple bot with Built in functionality 1. Generate a twitch access token @@ -25,16 +25,32 @@ bot_admins=ADMIN cargo run ``` +# Binary Crates + +## Simple Bot +Run a simple bot that logs into chat based on env + +``` +cargo run --bin simple_bot +``` + +## Simple Bot with Example Custom Listener +Run a bot with some custom listeners + +``` +cargo run --bin simple_bot_listener +``` + + + # Example Code -**Quick Start Main** +## Simple Bot Uses Env defined variables to create and run the bot ```rust -use botcore::bot::Bot; - -mod botcore; +use forcebot_rs_v2::botcore::bot::Bot; #[tokio::main] pub async fn main() { @@ -46,10 +62,72 @@ pub async fn main() { bot.run().await; } + +``` + +## Custom Bot with listener +Bot with a simple listener + +Example listener listens for a moderator badge and reply in chat + +```rust +use std::sync::Arc; + +use forcebot_rs_v2::botcore::{bot::Bot, bot_objects::listener::asyncfn_box}; +use forcebot_rs_v2::botcore::bot_objects::listener::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 callback */ + 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") { + 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 { + match bot.client.say_in_reply_to(&msg, String::from("test")).await { + Ok(_) => return Result::Ok("Success".to_string()) , + Err(_) => return Result::Err("Not Valid message type".to_string()) + } + } + Result::Err("Not Valid message type".to_string()) + } + + /* 4. Set 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; + +} + ``` # Crate Rust Documentation +Create Crate documentation + Clean Build Documentation ``` cargo clean && cargo doc diff --git a/src/bin/simple_bot.rs b/src/bin/simple_bot.rs new file mode 100644 index 0000000..674a5d9 --- /dev/null +++ b/src/bin/simple_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::botcore::bot::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_bot_listener.rs b/src/bin/simple_bot_listener.rs new file mode 100644 index 0000000..b2a9141 --- /dev/null +++ b/src/bin/simple_bot_listener.rs @@ -0,0 +1,66 @@ +//! 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::botcore::{bot::Bot, bot_objects::listener::asyncfn_box}; +use forcebot_rs_v2::botcore::bot_objects::listener::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 { + match bot.client.say_in_reply_to(&msg, String::from("test")).await { + Ok(_) => return Result::Ok("Success".to_string()) , + Err(_) => return Result::Err("Not Valid message type".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 index 96f1e66..269a97a 100644 --- a/src/botcore.rs +++ b/src/botcore.rs @@ -1 +1,2 @@ -pub mod bot; \ No newline at end of file +pub mod bot; +pub mod bot_objects; \ No newline at end of file diff --git a/src/botcore/bot.rs b/src/botcore/bot.rs index f01a489..e21de83 100644 --- a/src/botcore/bot.rs +++ b/src/botcore/bot.rs @@ -3,23 +3,30 @@ use tokio::sync::{mpsc::UnboundedReceiver, Mutex}; use twitch_irc::{login::StaticLoginCredentials, message::ServerMessage, SecureTCPTransport, TwitchIRCClient}; use dotenv::dotenv; -use std::env; +use std::{env, sync::Arc}; + +use super::bot_objects::listener::Listener; + + /// Twitch chat bot -pub struct Bot { +pub struct Bot +{ /// Prefix for commands _prefix: char, /// inbound chat msg stream incoming_msgs: Mutex<UnboundedReceiver<ServerMessage>>, /// outbound chat client msg stream - client: TwitchIRCClient<SecureTCPTransport,StaticLoginCredentials>, + pub client: TwitchIRCClient<SecureTCPTransport,StaticLoginCredentials>, /// joined channels botchannels: Vec<String>, + /// listeners + listeners: Vec<Listener>, } -impl Bot { - +impl Bot +{ /// Creates a new `Bot` using env variables /// /// Be sure the following is defined in an `.env` file @@ -79,6 +86,7 @@ impl Bot { incoming_msgs : Mutex::new(incoming_messages), client, botchannels : botchannels_all, + listeners : vec![], } } @@ -88,17 +96,35 @@ impl Bot { 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 mut in_msgs_lock = self.incoming_msgs.lock().await; + + let mut in_msgs_lock = bot.incoming_msgs.lock().await; + while let Some(message) = in_msgs_lock.recv().await { - //sprintln!("Received message: {:?}", message); - dbg!("Received message: {:?}", message); + // dbg!("Received message: {:?}", message.clone()); + + 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; + } + } } 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); + } } \ No newline at end of file diff --git a/src/botcore/bot_objects.rs b/src/botcore/bot_objects.rs new file mode 100644 index 0000000..baefb0b --- /dev/null +++ b/src/botcore/bot_objects.rs @@ -0,0 +1 @@ +pub mod listener; \ No newline at end of file diff --git a/src/botcore/bot_objects/listener.rs b/src/botcore/bot_objects/listener.rs new file mode 100644 index 0000000..0f953c4 --- /dev/null +++ b/src/botcore/bot_objects/listener.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; + +use twitch_irc::message::ServerMessage; + +use crate::botcore::bot::Bot; + +/// 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_cond_fn : fn(Arc<Bot>,ServerMessage) -> bool, + 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 + } +} + + +use std::boxed::Box; +use std::future::Future; +use std::pin::Pin; + +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))) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ce1adca --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,82 @@ +//! `forcebot-rs-v2` : Twitch chat bot written in rust +//! Customize by adding additional bot objects +//! +//! # Example Simple Bot +//! ``` +//! use forcebot_rs_v2::botcore::bot::Bot; +//! +//! #[tokio::main] +//!pub async fn main() { +//! +//! /* 1. Create the bot using env */ +//! let bot = Bot::new(); +//! +//! /* 2. Run the bot */ +//! bot.run().await; +//! +//!} +//! +//! ``` +//! +//! # Example Code Add Listener +//! +//! Bot with a simple listener +//! +//! Example listener listens for a moderator badge and reply in chat +//! +//! ``` +//! use std::sync::Arc; +//! +//! use forcebot_rs_v2::botcore::{bot::Bot, bot_objects::listener::asyncfn_box}; +//! use forcebot_rs_v2::botcore::bot_objects::listener::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 callback */ +//! 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") { +//! 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 { +//! match bot.client.say_in_reply_to(&msg, String::from("test")).await { +//! Ok(_) => return Result::Ok("Success".to_string()) , +//! Err(_) => return Result::Err("Not Valid message type".to_string()) +//! } +//! } +//! Result::Err("Not Valid message type".to_string()) +//! } +//! +//! /* 4. Set 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; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 93f840e..ebbe171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,9 +10,8 @@ //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - More Info - <https://dev.twitch.tv/docs/authentication> -pub use botcore::bot::Bot; +use forcebot_rs_v2::botcore::bot::Bot; -mod botcore; #[tokio::main] pub async fn main() {