WIP: Enhancements through Funbot #4

Draft
modulatingforce wants to merge 14 commits from enh-with-fun-bot into master
42 changed files with 3267 additions and 1174 deletions

3
.gitignore vendored
View file

@ -3,3 +3,6 @@
# env # env
.env .env
# temp
/tmp

127
Cargo.lock generated
View file

@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.85" version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -63,9 +63,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.10" version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -142,10 +142,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "forcebot-rs-v2" name = "forcebot_core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dotenv", "dotenv",
"lazy_static",
"tokio", "tokio",
"twitch-irc", "twitch-irc",
] ]
@ -199,13 +200,14 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.13.3+wasi-0.2.2",
"windows-targets",
] ]
[[package]] [[package]]
@ -214,6 +216,12 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.169"
@ -264,15 +272,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "moderator_reactor"
version = "0.1.0"
dependencies = [
"dotenv",
"forcebot_core",
"lazy_static",
"tokio",
"twitch-irc",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@ -285,6 +304,17 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "new_empty_bot"
version = "0.1.0"
dependencies = [
"dotenv",
"forcebot_core",
"lazy_static",
"tokio",
"twitch-irc",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -311,9 +341,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.68" version = "0.10.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -337,9 +367,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
@ -429,9 +459,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.43" version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -493,6 +523,39 @@ dependencies = [
"libc", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -520,9 +583,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.96" version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -531,9 +594,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.15.0" version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@ -680,9 +743,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
@ -696,6 +759,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -777,3 +849,12 @@ name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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",
]

View file

@ -1,22 +1,7 @@
[package] [workspace]
name = "forcebot-rs-v2" members = [
version = "0.1.0" "forcebot_core",
edition = "2021" "simple_module_example",
default-run = "forcebot-rs-v2" "new_empty_bot",
"simple_debug_listener",
[dependencies] "moderator_reactor", "simple_command_bot"]
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"

12
forcebot_core/Cargo.toml Normal file
View file

@ -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"

View file

@ -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
}
}

View file

@ -1,24 +1,22 @@
//! Example simple Binary crate that creates & runs bot based on `.env` //! 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 //! - login_name
//! - access_token //! - access_token
//! - bot_channels //! - bot_channels
//! - prefix //! - prefix
//! - bot_admins //! - bot_admins
//! //!
//! Bot access tokens be generated here - //! Bot access tokens be generated here -
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication> //! - More Info - <https://dev.twitch.tv/docs/authentication>
use forcebot_rs_v2::Bot; use forcebot_core::Bot;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* 1. Create the bot using env */ /* 1. Create the bot using env */
let bot = Bot::new(); let bot = Bot::new().await;
/* 2. Run the bot */ /* 2. Run the bot */
bot.run().await; bot.run().await;
} }

View file

@ -1,76 +1,76 @@
//! Simple Module with a Command //! 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 //! 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` //! 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 //! - login_name
//! - access_token //! - access_token
//! - bot_channels //! - bot_channels
//! - prefix //! - prefix
//! - bot_admins //! - bot_admins
//! //!
//! Bot access tokens be generated here - //! Bot access tokens be generated here -
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication> //! - More Info - <https://dev.twitch.tv/docs/authentication>
use forcebot_rs_v2::Bot; use forcebot_core::Bot;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* Create the bot using env */ /* Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* load the Module */ /* load the Module */
bot.load_module(custom_mod::new()); bot.load_module(custom_mod::new()).await;
/* Run the bot */ /* Run the bot */
bot.run().await; bot.run().await;
} }
pub mod custom_mod { pub mod custom_mod {
use std::sync::Arc; 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; use twitch_irc::message::ServerMessage;
/// Module definition with a loaded command
/// Module with a loaded command
pub fn new() -> Module { pub fn new() -> Module {
/* 1. Create a 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 */ /* 2. Load the cmd into a new module */
custom_mod.load_command(cmd_test()); custom_mod.load_command(cmd_test());
custom_mod custom_mod
} }
/// Command definition
pub fn cmd_test() -> Command { pub fn cmd_test() -> Command {
/* 1. Create a new cmd */ /* 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 */ /* 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 { if let ServerMessage::Privmsg(msg) = message {
let _= bot.client.say_in_reply_to( let _ = bot
&msg, "test return".to_string()).await; .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 */ /* 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_admin_only(false);
cmd.set_min_badge(Badge::Moderator); cmd.set_min_badge(Badge::Moderator);
cmd cmd
} }
} }

View file

@ -0,0 +1,5 @@
pub mod bot;
pub mod bot_objects;
pub mod built_in_mods;
pub mod chat;
pub mod modules;

View file

@ -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);
// }
// }
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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));
}
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -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(());
}
}
}

View file

@ -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()
}
}

View file

@ -0,0 +1,4 @@
pub mod debug;
pub mod guest_badge;
pub mod pyramid;
// pub mod quiet;

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

234
forcebot_core/src/lib.rs Normal file
View file

@ -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;

View file

@ -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"

View file

@ -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;
}

11
new_empty_bot/Cargo.toml Normal file
View file

@ -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"

View file

@ -1,25 +1,22 @@
//! Example simple Binary crate that creates & runs bot based on `.env` //! 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 //! - login_name
//! - access_token //! - access_token
//! - bot_channels //! - bot_channels
//! - prefix //! - prefix
//! - bot_admins //! - bot_admins
//! //!
//! Bot access tokens be generated here - //! Bot access tokens be generated here -
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication> //! - More Info - <https://dev.twitch.tv/docs/authentication>
use forcebot_rs_v2::botcore::bot::Bot; use forcebot_core::Bot;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* 1. Create the bot using env */ /* 1. Create the bot using env */
let bot = Bot::new(); let bot = Bot::new().await;
/* 2. Run the bot */ /* 2. Run the bot */
bot.run().await; bot.run().await;
} }

239
readme.md
View file

@ -1,4 +1,4 @@
Twitch chat bot written in rust Customizable Twitch chat bot written in rust
# Quick Start # Quick Start
@ -22,28 +22,66 @@ bot_admins=ADMIN
3. Build & run 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 # 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 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 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 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 ## 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 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 ## Moderator Reactor
Run a bot that listens for messages with the `moderator` badge, and replies to that mod with an emote 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 ## 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 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 Uses Env defined variables to create and run the bot
```rust ```rust
use forcebot_rs_v2::Bot; use forcebot_core::Bot;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* 1. Create the bot using env */ /* 1. Create the bot using env */
let bot = Bot::new(); let bot = Bot::new().await;
/* 2. Run the bot */ /* 2. Run the bot */
bot.run().await; 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 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 : Create a custom `Module` by :
1. Defining Functions that create the Custom Bot Objects (eg `Command`) 1. Defining Functions that create the Custom Bot Objects (eg `Command`)
@ -110,16 +219,17 @@ Create a custom `Module` by :
```rust ```rust
use forcebot_rs_v2::Bot;
use forcebot_core::Bot;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* Create the bot using env */ /* Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* load the Module */ /* load the Module */
bot.load_module(custom_mod::new()); bot.load_module(custom_mod::new()).await;
/* Run the bot */ /* Run the bot */
bot.run().await; bot.run().await;
@ -130,14 +240,16 @@ pub async fn main() {
pub mod custom_mod { pub mod custom_mod {
use std::sync::Arc; 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; use twitch_irc::message::ServerMessage;
/// Module with a loaded command /// Module definition with a loaded command
pub fn new() -> Module { pub fn new() -> Module {
/* 1. Create a 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 */ /* 2. Load the cmd into a new module */
custom_mod.load_command(cmd_test()); custom_mod.load_command(cmd_test());
@ -146,44 +258,45 @@ pub mod custom_mod {
} }
/// Command definition
pub fn cmd_test() -> Command { pub fn cmd_test() -> Command {
/* 1. Create a new cmd */ /* 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 */ /* 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 { 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; &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 */ /* 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_admin_only(false);
cmd.set_min_badge(Badge::Moderator); cmd.set_min_badge(Badge::Vip);
cmd cmd
} }
} }
``` ```
## Simple Debug Listener ## Simple Debug Listener
Bot with a simple listener that listens for all messages and prints in output Bot with a simple listener that listens for all messages and prints in output
```rust ```rust
use std::sync::Arc; use std::sync::Arc;
use forcebot_rs_v2::{asyncfn_box, Bot, Listener}; use forcebot_core::{execution_async, Bot, Listener};
use twitch_irc::message::ServerMessage; use twitch_irc::message::ServerMessage;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* 1. Create the bot using env */ /* 1. Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* 2a. Create a new blank Listener */ /* 2a. Create a new blank Listener */
let mut listener = Listener::new(); let mut listener = Listener::new();
@ -195,15 +308,15 @@ pub async fn main() {
/* 2c. Define an async fn callback execution */ /* 2c. Define an async fn callback execution */
async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
dbg!(message); dbg!(message); /* outputs message to debug */
Result::Ok("Success".to_string()) Result::Ok("Success".to_string())
} }
/* 2d. Set and Store the execution body using `async_box()` */ /* 2d. Set and Store the execution body using `execution_async()` */
listener.set_exec_fn(asyncfn_box(execbody)); listener.set_exec_fn(execution_async(execbody));
/* 3. Load the listener into the bot */ /* 3. Load the listener into the bot */
bot.load_listener(listener); bot.load_listener(listener).await;
/* 4. Run the bot */ /* 4. Run the bot */
bot.run().await; bot.run().await;
@ -217,11 +330,12 @@ pub async fn main() {
Example listener listens for a moderator badge and reply in chat Example listener listens for a moderator badge and reply in chat
```rust ```rust
use std::sync::Arc; use std::sync::Arc;
use forcebot_rs_v2::Bot; use forcebot_core::Bot;
use forcebot_rs_v2::asyncfn_box; use forcebot_core::execution_async;
use forcebot_rs_v2::Listener; use forcebot_core::Listener;
use twitch_irc::message::ServerMessage; use twitch_irc::message::ServerMessage;
@ -229,16 +343,16 @@ use twitch_irc::message::ServerMessage;
pub async fn main() { pub async fn main() {
/* Create the bot using env */ /* Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* 1. Create a new blank Listener */ /* 1. Create a new blank Listener */
let mut listener = Listener::new(); let mut listener = Listener::new();
/* 2. Set a trigger condition function for listener */ /* 2. Set a trigger condition function for listener */
listener.set_trigger_cond_fn( listener.set_trigger_cond_fn(
|_:Arc<Bot>,message:ServerMessage| |_:Arc<Bot>,message:ServerMessage|
if let ServerMessage::Privmsg(msg) = message { if let ServerMessage::Privmsg(msg) = message {
for badge in msg.badges { for badge in msg.badges {
if matches!(badge, x if x.name == "moderator") { if matches!(badge, x if x.name == "moderator") {
// dbg!("moderator found"); // dbg!("moderator found");
@ -252,24 +366,23 @@ pub async fn main() {
/* 3. Define an async fn callback execution */ /* 3. 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 { 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()) ; return Result::Ok("Success".to_string()) ;
} }
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()` */ /* 4. Set and Store the execution body using `execution_async()` */
listener.set_exec_fn(asyncfn_box(execbody)); listener.set_exec_fn(execution_async(execbody));
/* 5. Load the listener into the bot */ /* 5. Load the listener into the bot */
bot.load_listener(listener); bot.load_listener(listener).await;
/* Run the bot */ /* Run the bot */
bot.run().await; bot.run().await;
} }
``` ```
## Simple Test Command ## Simple Test Command
@ -277,9 +390,10 @@ pub async fn main() {
```rust ```rust
use std::sync::Arc; use std::sync::Arc;
use forcebot_rs_v2::Bot; use forcebot_core::Badge;
use forcebot_rs_v2::asyncfn_box; use forcebot_core::Bot;
use forcebot_rs_v2::Command; use forcebot_core::execution_async;
use forcebot_core::Command;
use twitch_irc::message::ServerMessage; use twitch_irc::message::ServerMessage;
@ -287,31 +401,31 @@ use twitch_irc::message::ServerMessage;
pub async fn main() { pub async fn main() {
/* Create the bot using env */ /* Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* 1. Create a new blank cmd */ /* 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 */ /* 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 { 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()) ; 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()` */ /* 3. Set and Store the execution body using `execution_async()` */
cmd.set_exec_fn(asyncfn_box(execbody)); cmd.set_exec_fn(execution_async(execbody));
/* 4. optionally, remove admin only default flag */ /* 4. optionally, remove admin only default flag */
cmd.set_admin_only(false); cmd.set_admin_only(false);
/* 5. optionally, set min badge*/ /* 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 */ /* 6. Load the cmd into the bot */
bot.load_command(cmd); bot.load_command(cmd).await;
/* Run the bot */ /* Run the bot */
bot.run().await; 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
```

View file

@ -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"

View file

@ -1,47 +1,50 @@
//! Bot with custom example commands that responds to caller if allowed //! Bot with custom example commands that responds to caller if allowed
//! //!
//! Commands that are passed a blank prefix will use the bot prefix //! 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 //! - login_name
//! - access_token //! - access_token
//! - bot_channels //! - bot_channels
//! - prefix //! - prefix
//! - bot_admins //! - bot_admins
//! //!
//! Bot access tokens be generated here - //! Bot access tokens be generated here -
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication> //! - More Info - <https://dev.twitch.tv/docs/authentication>
use std::sync::Arc; use std::sync::Arc;
use forcebot_rs_v2::Badge; use forcebot_core::execution_async;
use forcebot_rs_v2::Bot; use forcebot_core::Badge;
use forcebot_rs_v2::asyncfn_box; use forcebot_core::Bot;
use forcebot_rs_v2::Command; use forcebot_core::Command;
use twitch_irc::message::ServerMessage; use twitch_irc::message::ServerMessage;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* Create the bot using env */ /* Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* 1. Create a new blank cmd */ /* 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 */ /* 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 { if let ServerMessage::Privmsg(msg) = message {
let _ = bot.client.say_in_reply_to(&msg, String::from("test success")).await; let _ = bot
return Result::Ok("Success".to_string()) ; .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()` */ /* 3. Set and Store the execution body using `execution_async()` */
cmd.set_exec_fn(asyncfn_box(execbody)); cmd.set_exec_fn(execution_async(execbody));
/* 4. optionally, remove admin only default flag */ /* 4. optionally, remove admin only default flag */
cmd.set_admin_only(false); cmd.set_admin_only(false);
@ -50,9 +53,8 @@ pub async fn main() {
cmd.set_min_badge(Badge::Moderator); cmd.set_min_badge(Badge::Moderator);
/* 6. Load the cmd into the bot */ /* 6. Load the cmd into the bot */
bot.load_command(cmd); bot.load_command(cmd).await;
/* Run the bot */ /* Run the bot */
bot.run().await; bot.run().await;
} }

View file

@ -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"

View file

@ -1,47 +1,43 @@
//! Example simple Binary crate that creates & runs bot based on `.env` //! 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 //! - login_name
//! - access_token //! - access_token
//! - bot_channels //! - bot_channels
//! - prefix //! - prefix
//! - bot_admins //! - bot_admins
//! //!
//! Bot access tokens be generated here - //! Bot access tokens be generated here -
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com> //! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication> //! - More Info - <https://dev.twitch.tv/docs/authentication>
use std::sync::Arc; use std::sync::Arc;
use forcebot_rs_v2::{asyncfn_box, Bot, Listener}; use forcebot_core::{execution_async, Bot, Listener};
use twitch_irc::message::ServerMessage; use twitch_irc::message::ServerMessage;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* 1. Create the bot using env */ /* 1. Create the bot using env */
let mut bot = Bot::new(); let bot = Bot::new().await;
/* 2a. Create a new blank Listener */ /* 2a. Create a new blank Listener */
let mut listener = Listener::new(); let mut listener = Listener::new();
/* 2b. Set a trigger condition function for listener */ /* 2b. Set a trigger condition function for listener */
listener.set_trigger_cond_fn( listener.set_trigger_cond_fn(|_: Arc<Bot>, _: ServerMessage| true);
|_:Arc<Bot>,_:ServerMessage| true
);
/* 2c. Define an async fn callback execution */ /* 2c. Define an async fn callback execution */
async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> { async fn execbody(_: Arc<Bot>, message: ServerMessage) -> Result<String, String> {
dbg!(message); dbg!(message); /* outputs message to debug */
Result::Ok("Success".to_string()) Result::Ok("Success".to_string())
} }
/* 2d. Set and Store the execution body using `async_box()` */ /* 2d. Set and Store the execution body using `execution_async()` */
listener.set_exec_fn(asyncfn_box(execbody)); listener.set_exec_fn(execution_async(execbody));
/* 3. Load the listener into the bot */ /* 3. Load the listener into the bot */
bot.load_listener(listener); bot.load_listener(listener).await;
/* 4. Run the bot */ /* 4. Run the bot */
bot.run().await; bot.run().await;
} }

View file

@ -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"

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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;
}

View file

@ -1,3 +0,0 @@
pub mod bot;
pub mod bot_objects;
pub mod modules;

View file

@ -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);
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

View file

@ -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;