guest_badge module

This commit is contained in:
modulatingforce 2025-01-29 20:00:20 -05:00
parent 2a884f9571
commit a9da9f4192
9 changed files with 413 additions and 46 deletions

View file

@ -1,7 +1,9 @@
//! WIP Fun forcebot with catered customizations #todo
//!
//! Custom modules that can be managed in chat through `disable` and `enable` commands
//! - funbot
//! - funbot
//! - guests
//!
//!
//! Be sure the followig is defined in `.env`
//! - login_name
@ -14,7 +16,7 @@
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
//! - More Info - <https://dev.twitch.tv/docs/authentication>
use forcebot_rs_v2::Bot;
use forcebot_rs_v2::{custom_mods::guest_badge, Bot};
#[tokio::main]
pub async fn main() {
@ -24,8 +26,11 @@ pub async fn main() {
/* 1. Load the module into the bot */
bot.load_module(funbot_objs::create_module());
/* 2. Load Custom Modules */
bot.load_module(guest_badge::create_module());
/* 2. Run the bot */
/* 3. Run the bot */
bot.run().await;
}
@ -39,7 +44,9 @@ pub mod funbot_objs {
/// Create a Module with a loaded Command object
pub fn create_module() -> Module {
let mut custom_mod = Module::new("funbot".to_string(), "".to_string());
let mut custom_mod = Module::new(
vec!["funbot".to_string()],
"".to_string());
custom_mod.load_command(create_cmd_test());

View file

@ -44,7 +44,9 @@ pub mod custom_mod {
/// 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());
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());

View file

@ -3,11 +3,11 @@
use tokio::sync::{mpsc::UnboundedReceiver, Mutex};
use twitch_irc::{login::StaticLoginCredentials, message::ServerMessage, SecureTCPTransport, TwitchIRCClient};
use dotenv::dotenv;
use std::{env, sync::Arc};
use std::{env, sync::Arc, time::{Duration, Instant}};
use crate::{Command, Listener, Module};
use crate::{Badge, Command, Listener, Module};
use super::bot_objects::built_in_objects;
use super::{bot_objects::built_in_objects, modules::{self, Status}};
/// Twitch chat bot
@ -30,7 +30,9 @@ pub struct Bot
/// modules
modules: Vec<Module>,
/// channel module status
channel_module_status: Mutex<Vec<(String,String,String)>>
channel_module_status: Mutex<Vec<(String,String,modules::Status)>>,
/// chatter guest badges - chatter,channel,Badge,start_time,duration
chatter_guest_badges: Mutex<Vec<(String,String,Badge,Instant,Duration)>>
}
@ -46,6 +48,8 @@ impl Bot
/// - 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();
@ -107,6 +111,7 @@ impl Bot
admins,
modules: vec![],
channel_module_status: Mutex::new(vec![]),
chatter_guest_badges: Mutex::new(vec![]),
};
for cmd in built_in_objects::create_commands() {
@ -145,18 +150,28 @@ impl Bot
for cmd in &(*bot).commands {
let a = cmd.clone();
if a.command_triggered(bot.clone(),msg.clone()) {
if a.command_triggered(bot.clone(),msg.clone()).await {
let _ = cmd.execute_fn(bot.clone(),message.clone()).await;
}
}
for module in &(*bot).modules {
'module_loop: for module in &(*bot).modules {
// skip modules that are disable
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 channel_flags in cms_lock.iter() {
if channel_flags.0 == msg.channel_login.clone() {
if module.get_names().contains(&channel_flags.1) && channel_flags.2 == Status::Disabled {
continue 'module_loop;
}
}
}
for listener in module.get_listeners() {
@ -169,7 +184,7 @@ impl Bot
for cmd in module.get_commands() {
let a = cmd.clone();
if a.command_triggered(bot.clone(),msg.clone()) {
if a.command_triggered(bot.clone(),msg.clone()).await {
let _ = cmd.execute_fn(bot.clone(),message.clone()).await;
}
@ -196,6 +211,16 @@ impl Bot
pub fn load_command(&mut self,c : Command) {
self.commands.push(c);
}
pub fn get_module(&self,module:String) -> Option<Module> {
for modl in self.modules.clone() {
if modl.get_names().contains(&module) {
return Some(modl);
}
}
None
}
@ -212,26 +237,93 @@ impl Bot
self.modules.push(m)
}
pub async fn get_channel_module_status(&self,channel:String,module:String) -> Status {
let found_disabled:bool = {
let cms_lock = self.channel_module_status.lock().await;
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
};
if found_disabled { return Status::Disabled;}
else { return Status::Enabled; };
}
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()));
}
let found_disabled:bool = {
let cms_lock = self.channel_module_status.lock().await;
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
};
if !found_disabled {
let mut cms_lock = self.channel_module_status.lock().await;
cms_lock.push((channel,module,Status::Disabled));
}
}
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())) {
if lock.contains(&(channel.clone(),module.clone(),Status::Disabled)) {
let index = lock
.iter()
.position(|x| *x ==
(channel.clone(),module.clone(),"disabled".to_string()))
(channel.clone(),module.clone(),Status::Disabled))
.unwrap();
lock.remove(index);
}
}
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));
}
}

View file

@ -13,7 +13,7 @@ use super::bot::Bot;
/// chat badge
#[derive(Clone)]
#[derive(Clone,PartialEq, Eq,Debug)]
pub enum Badge {
Moderator,
Broadcaster,
@ -36,13 +36,13 @@ where
/// collection of functions to create built in objects
pub mod built_in_objects {
const TEMP_BADGE_DUR_MIN:u64 = 30;
use std::sync::Arc;
use std::{sync::Arc, time::{Duration, Instant}};
use twitch_irc::message::ServerMessage;
use crate::{asyncfn_box, Badge, Bot, Command};
use crate::{asyncfn_box, modules::Status, Badge, Bot, Command};
/// create a vector of command build in objects
@ -52,6 +52,7 @@ pub mod built_in_objects {
cmds.push(create_disable_cmd());
cmds.push(create_enable_cmd());
cmds.push(create_iam_role_cmd());
cmds
@ -64,12 +65,19 @@ pub mod built_in_objects {
/* 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() {
let mut action_taken = false;
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;
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;
}
}
}
let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ;
if action_taken {
let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ;
}
}
Result::Err("Not Valid message type".to_string())
}
@ -93,13 +101,38 @@ pub mod built_in_objects {
/* 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() {
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 {
bot.enable_module(msg.channel_login.clone(), arg.to_string()).await;
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()) {
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.client.say_in_reply_to(&msg,
format!("Enabled! {}", bot_message)
).await ;
}
let _ = bot.client.say_in_reply_to(&msg, String::from("Enabled!")).await ;
}
Result::Err("Not Valid message type".to_string())
}
@ -113,7 +146,7 @@ pub mod built_in_objects {
/* 5. optionally, set min badge*/
cmd.set_min_badge(Badge::Moderator);
cmd
}
}
/// adminonly command that grants a temporary role
@ -121,31 +154,88 @@ pub mod built_in_objects {
/* 1. Create a new blank cmd */
let mut cmd = Command::new(vec![
"I am ".to_string(),
"I'm ".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.split(" ").enumerate() {
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.client.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.client.say_in_reply_to(&msg,
format!("Temp {:?} issued for {:?} minutes",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.client.say_in_reply_to(&msg,
format!("Temp {:?} issued for {:?} minutes",Badge::Broadcaster,TEMP_BADGE_DUR_MIN)
).await ;
}
}
}
}
let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ;
// let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ;
}
Result::Err("Not Valid message type".to_string())
}

View file

@ -24,6 +24,8 @@ pub struct Command
exec_fn : Arc<ExecBody>,
min_badge : Badge,
admin_only : bool,
/// admin role overrides channel badgen- default : false
admin_override : bool,
prefix : String,
custom_cond_fn : fn(Arc<Bot>,PrivmsgMessage) -> bool,
}
@ -49,6 +51,7 @@ impl Command
exec_fn : Arc::new(asyncfn_box(execbody)),
min_badge : Badge::Vip,
admin_only : true,
admin_override : false ,
custom_cond_fn : |_:Arc<Bot>,_:PrivmsgMessage| true,
}
}
@ -70,7 +73,7 @@ impl Command
/// 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 {
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();
@ -89,18 +92,21 @@ impl Command
}
fn caller_badge_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool {
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
// 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 {
@ -123,6 +129,15 @@ impl Command
}
}
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;
}
@ -153,7 +168,7 @@ impl Command
// custom_cond_ok(self, bot.clone(), msg.clone()));
cmd_called(self, bot.clone(), msg.clone()) &&
caller_badge_ok(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, msg)
@ -175,4 +190,10 @@ impl Command
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

@ -2,14 +2,21 @@
use crate::{Command, Listener};
#[derive(PartialEq, Eq,Debug)]
pub enum Status {
Disabled,
Enabled
}
/// 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: String,
_alias: String,
name: Vec<String>,
// _alias: String,
bot_read_description : String,
listeners: Vec<Listener>,
commands: Vec<Command>,
}
@ -17,10 +24,11 @@ pub struct Module
impl Module
{
/// create a new module
pub fn new(name:String,alias:String) -> Module {
pub fn new(name:Vec<String>,bot_read_description:String) -> Module {
Module {
name,
_alias: alias,
// _alias: alias,
bot_read_description,
listeners: vec![],
commands: vec![]
}
@ -44,8 +52,13 @@ impl Module
self.commands.clone()
}
pub fn get_name(&self) -> String {
pub fn get_names(&self) -> Vec<String> {
self.name.clone()
}
pub fn get_bot_read_description(&self) -> String {
self.bot_read_description.clone()
}
}

View file

@ -0,0 +1 @@
pub mod guest_badge;

View file

@ -0,0 +1,140 @@
use std::{sync::Arc, time::{Duration, Instant}};
use twitch_irc::message::ServerMessage;
use crate::{asyncfn_box, Badge, Bot, Command, Module};
/// guest_badge / guest module
///
/// Temporary badges can be issued to chatters. The bot then opens functionality
/// to that chatter baded 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;
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.client.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(asyncfn_box(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.client.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(asyncfn_box(execbody));
cmd.set_admin_only(false);
cmd.set_admin_override(true);
cmd.set_min_badge(Badge::Moderator);
cmd
}

View file

@ -137,7 +137,7 @@
//!
//! ```
//!
//! ## Modertor Reactor
//! ## Moderator Reactor
//!
//! ```
//!
@ -197,10 +197,11 @@
pub mod botcore;
pub mod custom_mods;
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;
pub use crate::botcore::modules;