diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..46ba45d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ + +[env] +# Based on https://doc.rust-lang.org/cargo/reference/config.html +OtherBots = "Supibot,buttsbot,PotatBotat,StreamElements,yuumeibot" diff --git a/.gitignore b/.gitignore index 5c5e6b4..e602fce 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,10 @@ target/ *.log # debug -.vscode/ \ No newline at end of file +.vscode/ + +# nix +result/ + +# pre-commit +/.pre-commit-config.yaml diff --git a/Cargo.lock b/Cargo.lock index 18195c8..da35376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-recursion" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -122,9 +133,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -194,8 +205,10 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" name = "forcebot_rs" version = "0.1.0" dependencies = [ + "async-recursion", "async-trait", "casual_logger", + "chrono", "dotenv", "futures", "rand", diff --git a/Cargo.toml b/Cargo.toml index 5b20c49..f3f7724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,13 @@ twitch-irc = "5.0.1" rand = { version = "0.8.5", features = [] } futures = "0.3" async-trait = "0.1.77" +async-recursion = "1.1.0" casual_logger = "0.6.5" +chrono = "0.4.35" [lib] name = "bot_lib" -path = "src/lib.rs" \ No newline at end of file +path = "src/lib.rs" + + diff --git a/README.md b/README.md index 33fdb59..dafeb69 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,11 @@ access_token = bot_channels = , prefix = bot_admins = +``` + + +2. If required, adjust the following additional `.cargo\config.toml` configurations : +``` +[env] +OtherBots = # Other Bots for special handling (e.g., bot commands cant be ran by bots) ``` \ No newline at end of file diff --git a/flake.lock b/flake.lock index 67734d7..7da4bc6 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1706768574, - "narHash": "sha256-4o6TMpzBHO659EiJTzd/EGQGUDdbgwKwhqf3u6b23U8=", + "lastModified": 1711952616, + "narHash": "sha256-WJvDdOph001fA1Ap3AyaQtz/afJAe7meSG5uJAdSE+A=", "owner": "nix-community", "repo": "fenix", - "rev": "668102037129923cd0fc239d864fce71eabdc6a3", + "rev": "209048d7c545905c470f6f8c05c5061f391031a8", "type": "github" }, "original": { @@ -20,13 +20,68 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1706550542, - "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "lastModified": 1711703276, + "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", "type": "github" }, "original": { @@ -36,36 +91,92 @@ "type": "github" } }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1710695816, + "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_2": { "locked": { - "lastModified": 1708296515, - "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=", - "owner": "nixos", + "lastModified": 1712573573, + "narHash": "sha256-xxon7WwNm4/EadMKg1eF40/5s0O78nXUy2ILZt6vT7E=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "rev": "0d28066770464d19d637f6e8e42e8688420b6ac6", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1710765496, + "narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_3", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1712579741, + "narHash": "sha256-igpsH+pa6yFwYOdah3cFciCk8gw+ytniG9quf5f/q84=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "70f504012f0a132ac33e56988e1028d88a48855c", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { "fenix": "fenix", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_2", + "pre-commit-hooks": "pre-commit-hooks", + "systems": "systems_2" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1706735270, - "narHash": "sha256-IJk+UitcJsxzMQWm9pa1ZbJBriQ4ginXOlPyVq+Cu40=", + "lastModified": 1711885694, + "narHash": "sha256-dyezzeSbWMpflma+E9USmvSxuLgGcNGcGw3cOnX36ko=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "42cb1a2bd79af321b0cc503d2960b73f34e2f92b", + "rev": "e4a405f877efd820bef9c0e77a02494e47c17512", "type": "github" }, "original": { @@ -74,6 +185,36 @@ "repo": "rust-analyzer", "type": "github" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index c6be631..e645270 100644 --- a/flake.nix +++ b/flake.nix @@ -1,39 +1,80 @@ { - description = "forcebot_rs flake"; + description = "A basic flake"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default-linux"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; fenix.url = "github:nix-community/fenix/monthly"; + pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix"; }; + outputs = { + self, + systems, nixpkgs, fenix, - ... - } @ inputs: let - system = "x86_64-linux"; - overlays = [fenix.overlays.default]; - pkgs = import nixpkgs { - inherit system overlays; - }; + pre-commit-hooks, + }: let + eachSystem = nixpkgs.lib.genAttrs (import systems); + pkgsFor = eachSystem (system: + import nixpkgs { + localSystem.system = system; + overlays = [fenix.overlays.default]; + }); in { - devShells.${system}.default = pkgs.mkShell { - name = "forcebot_rs-devenv"; - nativeBuildInputs = [pkgs.pkg-config]; - buildInputs = with pkgs; [openssl libiconv]; - packages = with pkgs; [ - nil - alejandra - rust-analyzer-nightly - (fenix.packages.${system}.complete.withComponents [ - "cargo" - "clippy" - "rust-src" - "rustc" - "rustfmt" - ]) - ]; - RUST_BACKTRACE = 1; - RUST_SRC_PATH = "${fenix.packages.${system}.complete.rust-src}/lib/rustlib/src/rust/library"; - }; + packages = eachSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + inherit ((builtins.fromTOML (builtins.readFile ./Cargo.toml)).package) version; + in { + default = pkgsFor.${system}.rustPlatform.buildRustPackage { + pname = "forcebot_rs"; + version = "${version}"; + + src = self; + + cargoLock = { + lockFile = ./Cargo.lock; + }; + + nativeBuildInputs = with pkgs; [pkg-config]; + buildInputs = with pkgs; [openssl]; + + doCheck = false; + }; + }); + checks = eachSystem (system: { + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + # rust + rustfmt.enable = true; + clippy.enable = true; + # nix + statix.enable = true; + alejandra.enable = true; + deadnix.enable = true; + }; + }; + }); + devShells = eachSystem (system: { + default = pkgsFor.${system}.mkShell { + inherit (self.checks.${system}.pre-commit-check) shellHook; + packages = with pkgsFor.${system}; [ + nil + alejandra + rust-analyzer-nightly + (fenix.packages.${system}.complete.withComponents [ + "cargo" + "clippy" + "rust-src" + "rustc" + "rustfmt" + ]) + ]; + RUST_BACKTRACE = 1; + RUST_SRC_PATH = "${fenix.packages.${system}.complete.rust-src}/lib/rustlib/src/rust/library"; + }; + }); + nixosModules.default = import ./nix/module.nix {inherit self;}; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..cdc3706 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,30 @@ +{self}: { + pkgs, + config, + lib, + ... +}: let + inherit (lib) types; + inherit (lib.modules) mkIf; + inherit (lib.options) mkOption mkEnableOption; + inherit (pkgs.stdenv.hostPlatform) system; + cfg = config.services.forcebot_rs; +in { + options.services.forcebot_rs = { + enable = mkEnableOption '' + Enable forcebot + ''; + + package = mkOption { + type = types.package; + inherit (self.packages.${system}) default; + }; + }; + + config = mkIf cfg.enable { + systemd.services.forcebot_rs = { + wantedBy = ["multi-user.target"]; + serviceConfig.ExecStart = "${cfg.package}/bin/forcebot_rs"; + }; + }; +} diff --git a/src/core/bot_actions.rs b/src/core/bot_actions.rs index 2e6b456..7ab34a0 100644 --- a/src/core/bot_actions.rs +++ b/src/core/bot_actions.rs @@ -1,27 +1,141 @@ +use twitch_irc::message::{PrivmsgMessage, TwitchUserBasics}; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::core::botinstance::BotInstance; + +use super::{botmodules::{BotAction, BotModule}, identity::ChatBadge}; + + +pub type BotAR = Arc>; +pub type ActAR = Arc>; + +#[derive(Clone)] +pub struct ExecBodyParams { + pub bot : BotAR, + pub msg : PrivmsgMessage, + pub parent_act : ActAR , +} + + +impl ExecBodyParams { + + pub async fn get_parent_module(&self) -> Option { + + let parent_act = Arc::clone(&self.parent_act); + let parent_act_lock = parent_act.read().await; + let act = &(*parent_act_lock); + match act { + BotAction::C(c) => { + let temp = c.module.clone(); + Some(temp) + }, + BotAction::L(l) => { + let temp = l.module.clone(); + Some(temp) + }, + _ => None + } + } + + pub fn get_sender(&self) -> String { + self.msg.sender.name.clone() + } + + pub fn get_sender_chatbadge(&self) -> Option { + + let mut requestor_badge_mut: Option = None; + + for b in &self.msg.badges { + if b.name == "moderator" { + requestor_badge_mut = Some(ChatBadge::Mod); + } else if b.name == "broadcaster" { + requestor_badge_mut = Some(ChatBadge::Broadcaster); + } + } + requestor_badge_mut + } + + /// Returns some information about the message that was replied to by the `PrivmsgMessage` contained + /// in the `msg` field of this struct. + /// + /// If that message replied to message return that information in form of `Some`. + /// Otherwise, return `None`. + pub fn get_parent_reply(&self) -> Option { + let map = &self.msg.source.tags.0; + let tags = [ + "reply-parent-user-id", + "reply-parent-user-login", + "reply-parent-display-name", + "reply-parent-msg-id", + "reply-parent-msg-body" + ]; + + // filter out all tags that do not have content. + let tag_contents: Vec = tags.iter().filter_map(|tag| { + // if let Some(&Some(ref t)) = map.get(*tag) { + if let Some(Some(t)) = map.get(*tag) { + Some(t.clone()) + } else { + None + } + }).collect(); + + // if no tags got filtered out return the struct. + // else return `None`. + if tag_contents.len() == 5 { + Some(ReplyParent { + sender: TwitchUserBasics { + id: tag_contents[0].clone(), + login: tag_contents[1].clone(), + name: tag_contents[2].clone(), + }, + message_id: tag_contents[3].clone(), + message_text: tag_contents[4].clone(), + channel_login: self.msg.channel_login.clone(), + channel_id: self.msg.channel_id.clone(), + }) + } else { + None + } + } +} + +/// Represents the message a `PrivmsgMessage` replies to. +/// Similar to a less detailed `PrivmsgMessage`. +/// +/// This should not be constructed manually but only from calling `get_parent_reply()` on +/// `ExecBodyParams`. +/// +/// Fields that will be the same as the `PrivmsgMessage` this was generated from: +/// - `channel_login` +/// - `channel_id` +#[derive(Debug, Clone, PartialEq)] +pub struct ReplyParent { + pub sender: TwitchUserBasics, + pub message_id: String, + pub message_text: String, + pub channel_login: String, + pub channel_id: String, +} + + pub mod actions_util { + use super::*; + use std::boxed::Box; use std::future::Future; use std::pin::Pin; - use std::sync::Arc; - - use tokio::sync::{Mutex, RwLock}; - - use twitch_irc::message::PrivmsgMessage; - - use crate::core::botinstance::BotInstance; - - pub type BotAM = Arc>; - pub type BotAR = Arc>; pub type ExecBody = Box< - dyn Fn(BotAR, PrivmsgMessage) -> Pin + Send>> + Send + Sync, + dyn Fn(ExecBodyParams) -> Pin + Send>> + Send + Sync, >; - pub fn asyncbox(f: fn(BotAR, PrivmsgMessage) -> T) -> ExecBody + pub fn asyncbox(f: fn(ExecBodyParams) -> T) -> ExecBody where T: Future + Send + 'static, { - Box::new(move |a, b| Box::pin(f(a, b))) + Box::new(move |a| Box::pin(f(a))) } } diff --git a/src/core/botinstance.rs b/src/core/botinstance.rs index ea8e03c..efd0f9b 100644 --- a/src/core/botinstance.rs +++ b/src/core/botinstance.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{Mutex, RwLock}; +use tokio::time::{sleep, Duration}; use twitch_irc::login::StaticLoginCredentials; use twitch_irc::message::{PrivmsgMessage, ServerMessage}; @@ -16,19 +17,28 @@ use casual_logger::Log; use crate::core::ratelimiter::RateLimiter; -use crate::core::bot_actions::actions_util::BotAR; +use crate::core::bot_actions::BotAR; use crate::core::botmodules::ModulesManager; -use crate::core::identity::{ChangeResult, IdentityManager, Permissible}; +use crate::core::identity::{IdentityManager, Permissible}; use crate::core::botlog; use crate::core::chat::Chat; -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum ChType { - Channel(String), + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ChangeResult { + Success(String), + Failed(String), + NoChange(String), } -pub use ChType::Channel; + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] + +pub struct Channel(pub String); + +use super::bot_actions::ExecBodyParams; +use super::botmodules::StatusType; #[derive(Clone)] pub struct BotManagers { @@ -38,7 +48,7 @@ pub struct BotManagers { impl BotManagers { pub fn init( - ratelimiters: HashMap, + ratelimiters: HashMap, client: TwitchIRCClient, StaticLoginCredentials>, ) -> BotManagers { BotManagers { @@ -62,11 +72,11 @@ impl ArcBox { pub struct BotInstance { pub prefix: char, - pub bot_channel: ChType, + pub bot_channel: Channel, pub incoming_messages: Arc>>, pub botmodules: Arc, pub twitch_oauth: String, - pub bot_channels: Vec, + pub bot_channels: Vec, pub botmgrs: BotManagers, //modesmgr : ModesManager, // [FUTURE] Silent/Quiet , uwu , frisky/horny } @@ -109,6 +119,19 @@ impl BotInstance { ratelimiters.insert(Channel(String::from(chnl)), n); } + + tokio::spawn(async { + loop { + let routine_mins = 60 * 24 ; // Every 1 Day + // let routine_mins = 1; // Every 1 Minute + Log::remove_old_logs(); + Log::info(&format!("Internal Purge Routine Triggered - running every {} mins",routine_mins)); + Log::flush(); + sleep(Duration::from_secs(60 * routine_mins)).await + } + }); + + BotInstance { prefix, bot_channel: Channel(login_name), @@ -130,6 +153,19 @@ impl BotInstance { let mut msglock = botlock.incoming_messages.write().await; while let Some(message) = msglock.recv().await { + + + botlog::trace( + format!( + "[TRACE][ServerMessage] > {:?}", + message + ) + .as_str(), + Some("BotInstance > runner()".to_string()), + None, + ); + + match message { ServerMessage::Notice(msg) => { botlog::notice( @@ -138,8 +174,10 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Privmsg(msg) => { + botlog::debug( format!( "[Twitch Chat > {}] > {}: {}", @@ -150,6 +188,18 @@ impl BotInstance { Some(&msg), ); + + botlog::trace( + format!( + "[TRACE][Twitch Chat > {}] > {}: {:?}", + msg.channel_login, msg.sender.name, msg + ) + .as_str(), + Some("BotInstance > runner()".to_string()), + Some(&msg), + ); + Log::flush(); + BotInstance::listener_main_prvmsg(Arc::clone(&bot), &msg).await; } ServerMessage::Whisper(msg) => { @@ -158,6 +208,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Join(msg) => { botlog::notice( @@ -165,6 +216,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } ServerMessage::Part(msg) => { botlog::notice( @@ -172,6 +224,7 @@ impl BotInstance { Some("BotInstance > runner()".to_string()), None, ); + Log::flush(); } _ => {} }; @@ -202,6 +255,42 @@ impl BotInstance { // // [ ] #todo Need to run through all Listener Bodies for Enabled Modules for the context of the message (e.g., ModStatus is Enabled in the context for the channel) + + /* + [ ] What we should do instead is : + 1. Check if the message is related to a Reply (so we know how many arguments we should skip) + 2. If a reply, skip the first argument + */ + + let mut msgiter= msg + .message_text + .split(' '); + + let arg1 = msgiter.next(); + let arg2 = msgiter.next(); + + let reply = if let Some(Some(replyid)) = msg.source.tags.0.get("reply-thread-parent-msg-id") { + Some(replyid) + } else { None } + ; + + + let inpt = match reply { + None => { // Regular message, use the first arg as the command + match arg1 { + None => return, // return if no argument found + Some(a) => a, + } + }, + Some(_) => { + match arg2 { // A reply message, use the 2nd arg as the command + None => return, // return if no argument found + Some(a) => a, + } + }, + }; + + let botlock = bot.read().await; let actsdb = Arc::clone(&botlock.botmodules.botactions); let actsdblock = actsdb.read().await; @@ -212,14 +301,20 @@ impl BotInstance { Some(msg), ); + for acts in (*actsdblock).values() { + + for a in acts { - match a { + + let act_clone = Arc::clone(a); + + match &(*act_clone.read().await) { crate::core::botmodules::BotAction::C(c) => { /* BotCommand handling - - [x] Checks if the input message is a prefix with command name or alias - - [ ] Validate User can run based on identityModule(From_Bot)::can_user_run( + - [x] Validate User can run based on identityModule(From_Bot)::can_user_run( _usr:String, _channelname:ChType, _chat_badge:ChatBadge, @@ -232,11 +327,7 @@ impl BotInstance { Some(msg), ); - let inpt = msg - .message_text - .split(' ') - .next() - .expect("ERROR during BotCommand"); + // [x] Check if a bot command based on ... // [x] prefix + command @@ -268,6 +359,72 @@ impl BotInstance { let botlock = bot.read().await; let id = botlock.get_identity(); + // [x] Check first if the Module for that Given Command is Enabled or Disabled on the given Channel + let modmgr = Arc::clone(&botlock.botmodules); + let modstatus = modmgr.modstatus( + c.module.clone(), + Channel(msg.channel_login.to_string())).await; + + + if let StatusType::Disabled(a) = modstatus { + + // [x] Should only respond if a BotAdmin , Mod , SupMod , BroadCaster + // - Specifically it should respond only to those who may be able to enable the module + + botlog::trace( + &format!("Identified cmd is associated with Disabled Module : StatusLvl = {:?}", a), + Some("BotInstance > listener_main_prvmsg()".to_string()), + Some(msg), + ); + + + let botclone = Arc::clone(&bot); + let botlock = botclone.read().await; + let id = botlock.get_identity(); + let id = Arc::clone(&id); + let idlock = id.read().await; // <-- [ ] 03.24 - seems to work + let user_roles = idlock.getspecialuserroles( + msg.sender.name.clone(), + Some(Channel(msg.channel_login.clone())) + ).await; + + + botlog::trace( + &format!("For Disabled Command Evaluating User Roles {:?}", user_roles), + Some("BotInstance > listener_main_prvmsg()".to_string()), + Some(msg), + ); + + // Only respond to those with th ebelow User Roles + + let outstr = + format!("sadg Module is disabled : {:?}",a); + + + let params = ExecBodyParams { + bot : Arc::clone(&bot), + msg : (*msg).clone(), + parent_act : Arc::clone(&act_clone), + }; + + // When sending a BotMsgTypeNotif, send_botmsg does Roles related validation as required + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outstr + ), + params, + ).await; + + return; + }; + + botlog::trace( + "ACQUIRING WRITE LOCK : ID", + Some("BotInstance > listener_main_prvmsg()".to_string()), + Some(msg), + ); + + let eval = { let mut idlock = id.write().await; let (permissability, chngrslt) = idlock @@ -299,25 +456,72 @@ impl BotInstance { let botlock = bot.read().await; let outstr = "o7 a Mod. I kneel to serve! pepeKneel ".to_string(); - botlock.botmgrs.chat.say_in_reply_to(msg, outstr).await; + + + let params = ExecBodyParams { + bot : Arc::clone(&bot), + msg : (*msg).clone(), + parent_act : Arc::clone(&act_clone), + + }; + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outstr.to_string() + ), + params.clone(), + ).await; + + } + + if innerstr + .to_lowercase() + .contains(&"Auto Promoted VIP".to_lowercase()) + { + botlog::notice( + "Assigning VIP UserRole to VIP", + Some("botinstance > listener_main_prvmsg()".to_string()), + Some(msg), + ); + + let botlock = bot.read().await; + let outstr = + "❤️ a VIP - love ya!".to_string(); + + + let params = ExecBodyParams { + bot : Arc::clone(&bot), + msg : (*msg).clone(), + parent_act : Arc::clone(&act_clone), + + }; + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outstr.to_string() + ), + params.clone(), + ).await; + } } match eval { Permissible::Allow => { botlog::debug( - "Executed as permissible", + "Executing as permissible", Some("BotInstance > listener_main_prvmsg()".to_string()), Some(msg), ); let a = Arc::clone(&bot); - c.execute(a, msg.clone()).await; + c.execute(ExecBodyParams { + bot : a, + msg : msg.clone() , + parent_act : Arc::clone(&act_clone), + }).await; botlog::trace( "exit out of execution", Some("BotInstance > listener_main_prvmsg()".to_string()), - // Some(&msg), Some(msg), ); } @@ -334,8 +538,36 @@ impl BotInstance { } crate::core::botmodules::BotAction::L(l) => { - let a = Arc::clone(&bot); - l.execute(a, msg.clone()).await; + + let botlock = bot.read().await; + + // [x] Check first if the Module for that Given Command is Enabled or Disabled on the given Channel + let modmgr = Arc::clone(&botlock.botmodules); + let modstatus = modmgr.modstatus( + l.module.clone(), + Channel(msg.channel_login.to_string())).await; + + + if let StatusType::Disabled(a) = modstatus { + + // [x] Should only respond if a BotAdmin , Mod , SupMod , BroadCaster + // - Specifically it should respond only to those who may be able to enable the module + + botlog::trace( + &format!("Identified listener is associated with Disabled Module : StatusLvl = {:?}", a), + Some("BotInstance > listener_main_prvmsg()".to_string()), + Some(msg), + ); + + } else { + let a = Arc::clone(&bot); + l.execute(ExecBodyParams { + bot : a, + msg : msg.clone() , + parent_act : Arc::clone(&act_clone), + } ).await; + } + } _ => (), diff --git a/src/core/botlog.rs b/src/core/botlog.rs index 27272d2..48ae783 100644 --- a/src/core/botlog.rs +++ b/src/core/botlog.rs @@ -24,11 +24,10 @@ debug = "Checking bot actions", pub fn trace(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -45,11 +44,10 @@ pub fn trace(in_msg: &str, in_module: Option, in_prvmsg: Option<&Privmsg pub fn debug(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -66,11 +64,10 @@ pub fn debug(in_msg: &str, in_module: Option, in_prvmsg: Option<&Privmsg pub fn info(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -87,11 +84,10 @@ pub fn info(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgM pub fn notice(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -108,11 +104,10 @@ pub fn notice(in_msg: &str, in_module: Option, in_prvmsg: Option<&Privms pub fn warn(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -129,11 +124,10 @@ pub fn warn(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgM pub fn error(in_msg: &str, in_module: Option, in_prvmsg: Option<&PrivmsgMessage>) { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; @@ -154,11 +148,10 @@ pub fn fatal<'a>( ) -> &'a str { let (chnl, chatter) = match in_prvmsg { Some(prvmsg) => { - //Log::trace(&format!("(#{}) {}: {}", prvmsg.channel_login, prvmsg.sender.name, prvmsg.message_text)); ( Some(prvmsg.channel_login.clone()), Some(prvmsg.sender.name.clone()), - ) // <-- Clone fine atm while we're just working with Strings + ) } None => (None, None), }; diff --git a/src/core/botmodules.rs b/src/core/botmodules.rs index 0bfe4fa..408b2d5 100644 --- a/src/core/botmodules.rs +++ b/src/core/botmodules.rs @@ -3,12 +3,12 @@ ModulesManager is used to manage Modules and BotActions associated with those modules pub struct ModulesManager { - statusdb: HashMap>, - botactions: HashMap>, + statusdb: HashMap>, + botactions: HashMap>, } -- statusdb: HashMap> - Defines Modules and their ModStatusType (e.g., Enabled at an Instance level, Disabled at a Channel Level) -- botactions: HashMap> - Defines Modules and their BotActions (e.g., BotCommand , Listener, Routine) +- statusdb: HashMap> - Defines Modules and their ModStatusType (e.g., Enabled at an Instance level, Disabled at a Channel Level) +- botactions: HashMap> - Defines Modules and their BotActions (e.g., BotCommand , Listener, Routine) Example { @@ -19,40 +19,441 @@ Example */ + +const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + use core::panic; use std::collections::HashMap; -use std::error::Error; use std::sync::Arc; -use twitch_irc::message::PrivmsgMessage; +use casual_logger::Log; use tokio::sync::RwLock; use async_trait::async_trait; -use self::bot_actions::actions_util::BotAR; -use crate::core::botinstance::{BotInstance, ChType}; +use crate::core::bot_actions::actions_util; +use crate::core::bot_actions::ExecBodyParams; +use crate::core::botinstance::{BotInstance, Channel,ChangeResult}; use crate::core::botlog; -use crate::core::identity; +use crate::core::identity::{self, Permissible,IdentityManager}; use crate::core::bot_actions; -pub use ChType::Channel; -pub use ModType::BotModule; + +use std::hash::{Hash, Hasher}; + +use super::identity::ChatBadge; + + +pub async fn init(mgr: Arc) { + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("core")), + command: String::from("enable"), // command call name + alias: vec![ + String::from("e"), + String::from("en")], // String of alternative names + exec_body: actions_util::asyncbox(cmd_enable), + help: String::from("Test Command tester"), + required_roles: vec![ + identity::UserRole::BotAdmin, + identity::UserRole::Mod(OF_CMD_CHANNEL), + identity::UserRole::SupMod(OF_CMD_CHANNEL), + identity::UserRole::Broadcaster, + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_core_to_modmgr(Arc::clone(&mgr)).await; + + // async fn cmd_enable(bot: BotAR, msg: PrivmsgMessage) { + async fn cmd_enable(params : ExecBodyParams) { + /* + There should be additional validation checks + - BotAdmins can only run instance level (-i) enables + - If BotAdmins need to enable/disable at Channel level, they must Promote themselves to be a Mod at least + - Other Special Roles (Mod,SupMod,Broadcaster) can run without issues to enable the module at Channel Level + */ + + /* + enable -i // enables at Instance + enable // enables at Channel + */ + + /* + + 1. Parse out Message Arguments + + exec_enable() + + 2. Get Special Roles of CmdSender + 3. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 3a. , and is not -i (to instance) , return a Failure recommending BotAdmin promote themselves first + 3b. , and is -i (to instance) , return a Success + 4. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 4a. , and is not -i (to instance) , return a Success + 4b. , and is -i (to instance) , return a Failure they are not allowed + 5. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 5a. , and is not -i (to instance) , return a Success + 5b. , and is -i (to instance) , return a Success + + */ + + + // [x] Unwraps arguments from message + + let (arg1, arg2) = { + + let mut argv = params.msg.message_text.split(' '); + + argv.next(); // Skip the command name + + let arg1 = argv.next(); + + let arg2 = argv.next(); + + (arg1, arg2) + }; + + + /* -- Related function to call later + exec_enable( + &self, + requestor: String, + requestor_badge: Option, + trg_module: BotModule, + // channel: Option, + trg_level: StatusLvl, + bot: BotAR, + ) -> ChangeResult + */ + + + // [x] requestor: String, + let requestor = params.msg.clone().sender.name; + + + // [x] requestor_badge: Option, + + let mut requestor_badge_mut: Option = None; + + for b in ¶ms.msg.badges { + if b.name == "moderator" { + requestor_badge_mut = Some(ChatBadge::Mod); + } else if b.name == "broadcaster" { + requestor_badge_mut = Some(ChatBadge::Broadcaster); + } else if b.name == "vip" { + requestor_badge_mut = Some(ChatBadge::VIP); + } + } + + let requestor_badge = requestor_badge_mut; + + + // [x] trg_module: BotModule, + // - [x] Need to validate an actual BotModule - otherwise, fail or exit the cmd + + let trg_module = if (arg1 == Some("-i")) || (arg1 == Some("-f")) { arg2 } else { arg1 }; + + // if no trg_module was passed + // if let None = trg_module { + if trg_module.is_none() { + + // let botlock = params.bot.read().await; + + let outmsg = "uuh You need to pass a module"; + + botlog::debug( + outmsg, + Some("botmodules.rs > cmd_enable()".to_string()), + Some(¶ms.msg), + ); + + // We should call a notification around here + + let bot = params.clone().bot; + + let botclone = Arc::clone(&bot); + let botlock = botclone.read().await; + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + return; + + } + + + // [x] trg_level: StatusLvl, + + let currchnl = params.msg.channel_login.to_lowercase(); + + let trg_level = + if arg1 == Some("-i") || arg1 == Some("-f") { StatusLvl::Instance } + else { StatusLvl::Ch(Channel(currchnl)) } + ; + + + + let botlock = params.bot.read().await; + let modmgr = Arc::clone(&botlock.botmodules); + let id = botlock.get_identity(); + + let rslt = modmgr.exec_enable( + requestor, + requestor_badge, + BotModule(trg_module.unwrap().to_string()), + trg_level, + id).await; + + + // We should call a notification around here + + + let outmsg = match rslt.clone() { + ChangeResult::Failed(a) => format!("Stare Failed : {}",a), + ChangeResult::NoChange(a) => format!("Hmm No Change : {}",a), + ChangeResult::Success(a) => format!("YAAY Success : {}",a), + }; + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + + + } + + + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("core")), + command: String::from("disable"), // command call name + alias: vec![ + String::from("d")], // String of alternative names + exec_body: actions_util::asyncbox(cmd_disable), + help: String::from("Test Command tester"), + required_roles: vec![ + identity::UserRole::BotAdmin, + identity::UserRole::Mod(OF_CMD_CHANNEL), + identity::UserRole::SupMod(OF_CMD_CHANNEL), + identity::UserRole::Broadcaster, + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_core_to_modmgr(Arc::clone(&mgr)).await; + + async fn cmd_disable(params : ExecBodyParams) { + /* + There should be additional validation checks + - BotAdmins can only run instance level (-i) disables and (-f) force disable + - If BotAdmins need to enable/disable at Channel level, they must Promote themselves to be a Mod at least + - Other Special Roles (Mod,SupMod,Broadcaster) can run without issues to disable the module at Channel Level + */ + + /* + disable -i // disables at Instance + disable // disables at Channel + disable -f // force disables (instance and enabled are removed) + */ + + /* + + 1. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 1. can_user_run for cmdreqRoles including BotAdmin & not can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 1a. , and has no special flags (-i / -f) , return a Failure recommending BotAdmin promote themselves first + 1b. , and is -i (to instance) , return a Success + 1c. , and is -f (forced) , return a Success + + 2. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 2. not can_user_run for cmdreqRoles including BotAdmin & can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 2a. , and has no special flags (-i / -f) , return a Success + 2b. , and is -i (to instance) , return a Failure they are not allowed + 2c. , and is -f (forced) , return a Failure they are not allowed + + 3. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 3. can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) & can_user_run for cmdreqRoles including BotAdmin + 3a. , and has no special flags (-i / -f) , return a Success + 3b. , and is -i (to instance) , return a Success + 3c. , and is -f (forced) , return a Success + */ + + + // [x] Unwraps arguments from message + + let (arg1, arg2) = { + + let mut argv = params.msg.message_text.split(' '); + + argv.next(); // Skip the command name + + let arg1 = argv.next(); + + let arg2 = argv.next(); + + (arg1, arg2) + }; + + + /* -- Related function to call later + exec_disable( + &self, + requestor: String, + requestor_badge: Option, + trg_module: BotModule, + // channel: Option, + trg_level: StatusLvl, + force: bool, + // bot: BotAR, + id: Arc>, + ) -> ChangeResult + */ + + + // [x] requestor: String, + let requestor = params.msg.clone().sender.name; + + + // [x] requestor_badge: Option, + + let mut requestor_badge_mut: Option = None; + + for b in ¶ms.msg.badges { + if b.name == "moderator" { + requestor_badge_mut = Some(ChatBadge::Mod); + } else if b.name == "broadcaster" { + requestor_badge_mut = Some(ChatBadge::Broadcaster); + } else if b.name == "vip" { + requestor_badge_mut = Some(ChatBadge::VIP); + } + } + + let requestor_badge = requestor_badge_mut; + + // [x] trg_module: BotModule, + // - [x] Need to validate an actual BotModule - otherwise, fail or exit the cmd + + let trg_module = if (arg1 == Some("-i")) || (arg1 == Some("-f")) { arg2 } else { arg1 }; + + // if no trg_module was passed + if trg_module.is_none() { + + let botlock = params.bot.read().await; + + let outmsg = "uuh You need to pass a module"; + + botlog::debug( + outmsg, + Some("botmodules.rs > cmd_disable()".to_string()), + Some(¶ms.msg), + ); + + // We should call a notification around here + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + return; + + } + + + + // [x] trg_level: StatusLvl, + + let currchnl = params.msg.channel_login.to_lowercase(); + + let trg_level = + if arg1 == Some("-i") || arg1 == Some("-f") { StatusLvl::Instance } + // else if arg1 == Some("-f") { StatusLvl::Instance } + else { StatusLvl::Ch(Channel(currchnl)) } + ; + + + + let botlock = params.bot.read().await; + let modmgr = Arc::clone(&botlock.botmodules); + let id = botlock.get_identity(); + + let force = arg1 == Some("-f"); + + let rslt = modmgr.exec_disable( + requestor, + requestor_badge, + BotModule(trg_module.unwrap().to_string()), + trg_level, + force, + id).await; + + + let outmsg = match rslt.clone() { + ChangeResult::Failed(a) => format!("Stare Failed : {}",a), + ChangeResult::NoChange(a) => format!("Hmm No Change : {}",a), + ChangeResult::Success(a) => format!("YAAY Success : {}",a), + }; + + // We should call a notification around here + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + } + + +} + + + +#[derive(Debug, Clone)] +pub struct BotModule(pub String); + +impl PartialEq for BotModule { + fn eq(&self, other: &Self) -> bool { + let BotModule(name1) = self.clone(); + let BotModule(name2) = other.clone(); + name1.to_lowercase() == name2.to_lowercase() + } +} +impl Eq for BotModule {} + +impl Hash for BotModule{ + fn hash(&self, state: &mut H) { + let BotModule(name) = self.clone(); + name.to_lowercase().hash(state); + } +} + #[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum ModType { - BotModule(String), +pub enum ModGroup { + Core, + Custom, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum StatusLvl { Instance, - _Ch(ChType), + Ch(Channel), } -#[derive(Debug)] -pub enum ModStatusType { +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum StatusType { Enabled(StatusLvl), Disabled(StatusLvl), } @@ -64,10 +465,10 @@ pub enum BotAction { } impl BotAction { - pub async fn execute(&self, m: BotAR, n: PrivmsgMessage) { + pub async fn execute(&self, params : ExecBodyParams) { match self { - BotAction::L(a) => a.execute(m, n).await, - BotAction::C(a) => a.execute(m, n).await, + BotAction::L(a) => a.execute(params).await, + BotAction::C(a) => a.execute(params).await, _ => (), } } @@ -77,10 +478,12 @@ impl BotAction { pub trait BotActionTrait { async fn add_to_bot(self, bot: BotInstance); async fn add_to_modmgr(self, modmgr: Arc); + async fn add_core_to_bot(self, bot: BotInstance); + async fn add_core_to_modmgr(self, modmgr: Arc); } pub struct BotCommand { - pub module: ModType, + pub module: BotModule, pub command: String, // command call name pub alias: Vec, // String of alternative names pub exec_body: bot_actions::actions_util::ExecBody, @@ -89,8 +492,8 @@ pub struct BotCommand { } impl BotCommand { - pub async fn execute(&self, m: BotAR, n: PrivmsgMessage) { - (*self.exec_body)(m, n).await; + pub async fn execute(&self, params : ExecBodyParams) { + (*self.exec_body)(params).await; } } @@ -105,18 +508,28 @@ impl BotActionTrait for BotCommand { .add_botaction(self.module.clone(), BotAction::C(self)) .await } + + async fn add_core_to_bot(self, bot: BotInstance) { + self.add_core_to_modmgr(bot.botmodules).await; + } + + async fn add_core_to_modmgr(self, modmgr: Arc) { + modmgr + .add_core_act(self.module.clone(), BotAction::C(self)) + .await + } } pub struct Listener { - pub module: ModType, + pub module: BotModule, pub name: String, pub exec_body: bot_actions::actions_util::ExecBody, pub help: String, } impl Listener { - pub async fn execute(&self, m: BotAR, n: PrivmsgMessage) { - (self.exec_body)(m, n).await; + pub async fn execute(&self, params : ExecBodyParams) { + (self.exec_body)(params).await; } } @@ -142,26 +555,39 @@ impl BotActionTrait for Listener { .add_botaction(self.module.clone(), BotAction::L(self)) .await; } + + async fn add_core_to_bot(self, bot: BotInstance) { + self.add_core_to_modmgr(bot.botmodules).await; + } + + async fn add_core_to_modmgr(self, modmgr: Arc) { + modmgr + .add_core_act(self.module.clone(), BotAction::L(self)) + .await + } } #[derive(Debug)] pub struct Routine {} +type StatusdbEntry = (ModGroup, Vec); +type ModuleActions = Vec>>; + pub struct ModulesManager { - statusdb: Arc>>>, - pub botactions: Arc>>>, + statusdb: Arc>>, + pub botactions: Arc>>, } /* statusdb <-- shows Enabled/Disabled per Status level botactions HashMap< - ModType, <-- e.g., BotModule(String::from("experiments001")) + BotModule, <-- e.g., BotModule(String::from("experiments001")) Vec> BotCommand, Listener */ @@ -184,6 +610,7 @@ impl ModulesManager { // 1. load core modules crate::core::identity::init(Arc::clone(&mgrarc)).await; + crate::core::botmodules::init(Arc::clone(&mgrarc)).await; // 2. load custom modules crate::custom::init(Arc::clone(&mgrarc)).await; @@ -197,7 +624,25 @@ impl ModulesManager { mgrarc } - pub fn modstatus(&self, _: ModType, _: ChType) -> ModStatusType { + + pub async fn moduleslist(&self) -> HashMap + { + + // let db = Arc::clone(&self.statusdb); + let db = self.statusdb.clone(); + let dblock = db.read().await; + + let mut outmap = HashMap::new(); + + for (k,v) in &(*dblock) { + let (mgrp,_) = v; + let mtype = k; + outmap.insert((*mtype).clone(), (*mgrp).clone()); + } + outmap + } + + pub async fn modstatus(&self, in_module: BotModule, in_chnl: Channel) -> StatusType { // Example usage : botmanager.modstatus( // BotModule("GambaCore"), // Channel("modulatingforce") @@ -205,21 +650,668 @@ impl ModulesManager { // - The ModStatusType checks in the context of the given channel , // but also validates based on wheher the module is disabled at a bot instance // level as well - ModStatusType::Enabled(StatusLvl::Instance) + + let dbt = self.statusdb.read().await; + + let (mgrp,statusvector) = dbt.get(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + StatusType::Enabled(StatusLvl::Instance) // This forces core to be validated as Enabled, even if undesired scenario of missing StatusLvl::Instance or empty vectors + }, + ModGroup::Custom => { + + /* + + [x] 1. If Disabled at Instance Level , + [x] a. And Enabled at a Channel Level > return Enabled(Channel) + [x] b. And Disabled at a Channel Level > return Disabled(Channel) + [x] c. And Not Defined at Channel Level > return Disabled(Instance) + [x] 2. If Enabled at Instance Level , + [x] a. And Enabled at a Channel Level > return Enabled(Channel) + [x] b. And Disabled at a Channel Level > return Disabled(Channel) + [x] c. And Not Defined at Channel Level > return Enabled(Instance) + */ + + + + if statusvector.contains(&StatusType::Disabled(StatusLvl::Instance)) { + // [x] 1. If Disabled at Instance Level , + + + if statusvector.contains(&StatusType::Enabled(StatusLvl::Ch(in_chnl.clone()))) { + // [x] a. And Enabled at a Channel Level > return Enabled(Channel) + StatusType::Enabled(StatusLvl::Ch(in_chnl.clone())) + } else if statusvector.contains(&StatusType::Disabled(StatusLvl::Ch(in_chnl.clone()))) { + // [x] b. And Disabled at a Channel Level > return Disabled(Channel) + StatusType::Disabled(StatusLvl::Ch(in_chnl.clone())) + } else { + // [x] c. And Not Defined at Channel Level > return Disabled(Instance) + StatusType::Disabled(StatusLvl::Instance) + } + + } else if statusvector.contains(&StatusType::Enabled(StatusLvl::Instance)) { + // [x] 2. If Enabled at Instance Level , + + if statusvector.contains(&StatusType::Enabled(StatusLvl::Ch(in_chnl.clone()))) { + // [x] a. And Enabled at a Channel Level > return Enabled(Channel) + StatusType::Enabled(StatusLvl::Ch(in_chnl.clone())) + } else if statusvector.contains(&StatusType::Disabled(StatusLvl::Ch(in_chnl.clone()))) { + // [x] b. And Disabled at a Channel Level > return Disabled(Channel) + StatusType::Disabled(StatusLvl::Ch(in_chnl.clone())) + } else { + // [x] c. And Not Defined at Channel Level > return Enabled(Instance) + StatusType::Enabled(StatusLvl::Instance) + } + + } else { + // ? In some unexpected scenario (e.g., not define at instance level), assume Disabled at Instance level and set as this way + self.set_instance_disabled(in_module).await; + StatusType::Disabled(StatusLvl::Instance) + } + }, + } + + + //StatusType::Enabled(StatusLvl::Instance) } - pub fn togglestatus(&self, _: ModType, _: ChType) -> ModStatusType { - // enables or disables based on current status - ModStatusType::Enabled(StatusLvl::Instance) + pub async fn exec_enable( + &self, + requestor: String, + requestor_badge: Option, + trg_module: BotModule, + trg_level: StatusLvl, + id: Arc>, + ) -> ChangeResult + { + + /* + + 1. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 1. can_user_run for cmdreqRoles including BotAdmin & not can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 1a. , and is not -i (to instance) , return a Failure recommending BotAdmin promote themselves first + 1b. , and is -i (to instance) , return a Success + + 2. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 2. not can_user_run for cmdreqRoles including BotAdmin & can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 2a. , and is not -i (to instance) , return a Success + 2b. , and is -i (to instance) , return a Failure they are not allowed + + 3. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 3. can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) & can_user_run for cmdreqRoles including BotAdmin + 3a. , and is not -i (to instance) , return a Success + 3b. , and is -i (to instance) , return a Success + */ + + + /* + [x] 1. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 1. can_user_run for cmdreqRoles including BotAdmin & not can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 1a. , and is -i (to instance) , return a Success + 1b. , and is not -i (to instance) , return a Failure recommending BotAdmin promote themselves first + + + */ + + + // [x] Validate in trg_module first + + let modlist = self.moduleslist().await; + let rslt = modlist.get(&trg_module); + + if rslt.is_none() { + return ChangeResult::Failed("Module doesn't exist".to_string()); + } + + botlog::trace( + "ACQUIRING WRITE LOCK : ID", + Some("ModulesManager > Exec_enable".to_string()), + None, + ); + + + let mut idlock = id.write().await; + + // if trg_level = StatusLvl::Instance , the temp_chnl = the broadcaster's or the chatter's + + let arb_chnl = match trg_level.clone() { + StatusLvl::Instance => Channel(requestor.to_lowercase()), + StatusLvl::Ch(a) => a, + }; + + const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + let (admin_level_access,_) = idlock.can_user_run(requestor.clone(), arb_chnl.clone(), requestor_badge.clone(), + vec![ + identity::UserRole::BotAdmin, + ]).await; + + + let (chnl_elevated_access,_) = idlock.can_user_run(requestor, arb_chnl, requestor_badge.clone(), + vec![ + identity::UserRole::Mod(OF_CMD_CHANNEL), + identity::UserRole::SupMod(OF_CMD_CHANNEL), + identity::UserRole::Broadcaster, + ]).await; + + + if let Permissible::Allow = admin_level_access { + if let Permissible::Block = chnl_elevated_access { + + botlog::debug( + &format!("?? REACHED INNER TIER : + admin_level_access : {:?} ; chnl_elevated_access : {:?}", + admin_level_access , chnl_elevated_access), + Some("botmodules.rs > exec_enable()".to_string()), + None, + ); + match trg_level { + StatusLvl::Instance => { + self.set_instance_enabled(trg_module.clone()).await; + return ChangeResult::Success("Enabled at Instance Level".to_string()); + }, + StatusLvl::Ch(_) => { + return ChangeResult::Failed("Promote yourself Temporarily First".to_string()); + }, + }; + + } + } + + + /* + [x] 2. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 2. not can_user_run for cmdreqRoles including BotAdmin & can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 2a. , and is -i (to instance) , return a Failure they are not allowed + 2b. , and is not -i (to instance) , return a Success + + */ + + if let Permissible::Block = admin_level_access { + if let Permissible::Allow = chnl_elevated_access { + match trg_level.clone() { + StatusLvl::Instance => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + StatusLvl::Ch(in_chnl) => { + self.set_ch_enabled(trg_module.clone(), in_chnl).await; + return ChangeResult::Success("Enabled at Channel Level".to_string()); + }, + }; + } + } + + + /* + + [x] 3. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 3. can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) & can_user_run for cmdreqRoles including BotAdmin + 3a. , and is not -i (to instance) , return a Success + 3b. , and is -i (to instance) , return a Success + */ + + + if let Permissible::Allow = admin_level_access { + if let Permissible::Allow = chnl_elevated_access { + match trg_level { + StatusLvl::Instance => { + self.set_instance_enabled(trg_module.clone()).await; + return ChangeResult::Success("Enabled at Instance Level".to_string()); + }, + StatusLvl::Ch(in_chnl) => { + self.set_ch_enabled(trg_module.clone(), in_chnl).await; + return ChangeResult::Success("Enabled at Channel Level".to_string()); + }, + }; + } + } + + + // Respond in case of General Chatter + // The below should NOT be required , as current internal logic would prevent + // a BotCommand to be ran by a Chatter if it requires any special roles and + // that chatter does not have htose roles + // However, below is added to satisfy unit tests + + + if let Permissible::Block = admin_level_access { + if let Permissible::Block = chnl_elevated_access { + match trg_level { + StatusLvl::Instance => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + StatusLvl::Ch(_) => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + }; + } + } + + + + // ======================= + // ======================= + // ======================= + + + + botlog::debug( + &format!("FAILURE involves : + admin_level_access : {:?} ; chnl_elevated_access : {:?}", + admin_level_access , chnl_elevated_access), + Some("botmodules.rs > exec_enable()".to_string()), + None, + ); + + + Log::flush(); + + ChangeResult::Failed("ERROR : Not implemented yet".to_string()) } - pub fn setstatus(&self, _: ModType, _: ModStatusType) -> Result<&str, Box> { - // sets the status based given ModSatusType - // e.g., b.setstatus(BodModule("GambaCore"), Enabled(Channel("modulatingforce"))).expect("ERROR") - Ok("") + + pub async fn exec_disable( + &self, + requestor: String, + requestor_badge: Option, + trg_module: BotModule, + trg_level: StatusLvl, + force: bool, + id: Arc>, + ) -> ChangeResult + { + + /* + + 1. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 1. can_user_run for cmdreqRoles including BotAdmin & not can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 1a. , and has no special flags (-i / -f) , return a Failure recommending BotAdmin promote themselves first + 1b. , and is -i (to instance) , return a Success + 1c. , and is -f (forced) , return a Success + + 2. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 2. not can_user_run for cmdreqRoles including BotAdmin & can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 2a. , and has no special flags (-i / -f) , return a Success + 2b. , and is -i (to instance) , return a Failure they are not allowed + 2c. , and is -f (forced) , return a Failure they are not allowed + + 3. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 3. can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) & can_user_run for cmdreqRoles including BotAdmin + 3a. , and has no special flags (-i / -f) , return a Success + 3b. , and is -i (to instance) , return a Success + 3c. , and is -f (forced) , return a Success + */ + + // [x] Validate in trg_module first + + let modlist = self.moduleslist().await; + let rslt = modlist.get(&trg_module); + + if rslt.is_none() { + return ChangeResult::Failed("Module doesn't exist".to_string()); + } + + botlog::trace( + "ACQUIRING WRITE LOCK : ID", + Some("ModulesManager > Exec_disable".to_string()), + None, + ); + + + + let mut idlock = id.write().await; + + // if trg_level = StatusLvl::Instance , the temp_chnl = the broadcaster's or the chatter's + + let arb_chnl = match trg_level.clone() { + StatusLvl::Instance => Channel(requestor.to_lowercase()), + StatusLvl::Ch(a) => a, + }; + + const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + let (admin_level_access,_) = idlock.can_user_run(requestor.clone(), arb_chnl.clone(), requestor_badge.clone(), + vec![ + identity::UserRole::BotAdmin, + ]).await; + + + let (chnl_elevated_access,_) = idlock.can_user_run(requestor, arb_chnl, requestor_badge.clone(), + vec![ + identity::UserRole::Mod(OF_CMD_CHANNEL), + identity::UserRole::SupMod(OF_CMD_CHANNEL), + identity::UserRole::Broadcaster, + ]).await; + + + /* + + [x] 1. If CmdSender is BotAdmin but not (Mod,SupMod,Broadcaster) + 1. can_user_run for cmdreqRoles including BotAdmin & not can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 1a. , and is -f (forced) , return a Success + 1b. , and is -i (to instance) , return a Success + 1c. , and has no special flags (-i / -f) , return a Failure recommending BotAdmin promote themselves first + + */ + + if let Permissible::Allow = admin_level_access { + if let Permissible::Block = chnl_elevated_access { + if force { + self.force_disable(trg_module.clone()).await; + return ChangeResult::Success("Forced Disable".to_string()); + } else { + match trg_level { + StatusLvl::Instance => { + self.set_instance_disabled(trg_module.clone()).await; + return ChangeResult::Success("Disabled at Instance Level".to_string()); + }, + StatusLvl::Ch(_) => { + return ChangeResult::Failed("Promote yourself Temporarily First".to_string()); + }, + }; + } + } + } + + + /* + [x] 2. If CmdSender not a BotAdmin but is (Mod,SupMod,Broadcaster) + 2. not can_user_run for cmdreqRoles including BotAdmin & can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) + 2a. , and is -f (forced) , return a Failure they are not allowed + 2b. , and is -i (to instance) , return a Failure they are not allowed + 2c. , and has no special flags (-i / -f) , return a Success + + */ + + if let Permissible::Block = admin_level_access { + if let Permissible::Allow = chnl_elevated_access { + if force { + return ChangeResult::Failed("You're not allowed".to_string()); + } else { + match trg_level.clone() { + StatusLvl::Instance => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + StatusLvl::Ch(in_chnl) => { + self.set_ch_disabled(trg_module.clone(), in_chnl).await; + return ChangeResult::Success("Disabled at Channel Level".to_string()); + }, + }; + } + } + } + + + /* + [x] 3. If CmdSender is (Mod,SupMod,Broadcaster) and a BotAdmin + 3. can_user_run for cmdreqRoles (Mod,SupMod,Broadcaster) & can_user_run for cmdreqRoles including BotAdmin + 3a. , and is -f (forced) , return a Success + 3b. , and is -i (to instance) , return a Success + 3c. , and has no special flags (-i / -f) , return a Success + + */ + + if let Permissible::Allow = admin_level_access { + if let Permissible::Allow = chnl_elevated_access { + if force { + self.force_disable(trg_module.clone()).await; + return ChangeResult::Success("Forced Disable".to_string()); + } else { + match trg_level { + StatusLvl::Instance => { + self.set_instance_disabled(trg_module.clone()).await; + return ChangeResult::Success("Disabled at Instance Level".to_string()); + }, + StatusLvl::Ch(in_chnl) => { + self.set_ch_disabled(trg_module.clone(), in_chnl).await; + return ChangeResult::Success("Disabled at Channel Level".to_string()); + }, + }; + } + } + } + + + + // Respond in case of General Chatter + // The below should NOT be required , as current internal logic would prevent + // a BotCommand to be ran by a Chatter if it requires any special roles and + // that chatter does not have htose roles + // However, below is added to satisfy unit tests + + + if let Permissible::Block = admin_level_access { + if let Permissible::Block = chnl_elevated_access { + match trg_level { + StatusLvl::Instance => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + StatusLvl::Ch(_) => { + return ChangeResult::Failed("You're not allowed".to_string()); + }, + }; + } + } + + + + ChangeResult::Failed("ERROR : Not implemented yet".to_string()) } - pub async fn add_botaction(&self, in_module: ModType, in_action: BotAction) { + pub async fn set_instance_disabled(&self, in_module: BotModule) -> (StatusType,ChangeResult) { + // at Instance level + // - If core module, do nothing + + let mut dbt = self.statusdb.write().await; + + let (mgrp,statusvector) = dbt.get_mut(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::Failed("Core Modules cannot be disabled".to_string()) + ) + }, + ModGroup::Custom => { + // remove all instance level pattern for the module + while let Some(index) = statusvector + .iter() + .position(|x| (*x == StatusType::Enabled(StatusLvl::Instance)) || (*x == StatusType::Disabled(StatusLvl::Instance))) { + + statusvector.remove(index); + } + statusvector.push(StatusType::Disabled(StatusLvl::Instance)); + + ( + StatusType::Disabled(StatusLvl::Instance), + ChangeResult::Success("Set Disabled at Instance".to_string()) + ) + }, + } + + } + + pub async fn force_disable(&self, in_module: BotModule) -> (StatusType,ChangeResult) { + // Disables the module at Instance level, and removes all Enabled at Channel level + // - Bot Moderators MUST Re-enable if they were enabled before + // - If core module, do nothing + + let mut dbt = self.statusdb.write().await; + + let (mgrp,statusvector) = dbt.get_mut(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::Failed("Core Modules cannot be disabled".to_string()) + ) + }, + ModGroup::Custom => { + // remove all instance level pattern & Enabled Channel patterns for the module + // Disabled at Channel level might be fine? That way if it gets Enabled at instance level, channel level disables are uninterrupted + while let Some(index) = statusvector + .iter() + .position(|x| + if (*x == StatusType::Enabled(StatusLvl::Instance)) + || (*x == StatusType::Disabled(StatusLvl::Instance)) { + true + } else { + matches!((*x).clone(), StatusType::Enabled(StatusLvl::Ch(_))) + } + ) + { + statusvector.remove(index); + } + statusvector.push(StatusType::Disabled(StatusLvl::Instance)); + + ( + StatusType::Disabled(StatusLvl::Instance), + ChangeResult::Success("Forced Disabled".to_string()) + ) + }, + } + + } + + pub async fn set_instance_enabled(&self, in_module: BotModule) -> (StatusType,ChangeResult) { + // at Instance level + // - If core module, do nothing + + let mut dbt = self.statusdb.write().await; + + let (mgrp,statusvector) = dbt.get_mut(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::NoChange("Core Modules are always Enabled".to_string()) + ) + }, + ModGroup::Custom => { + // remove all instance level pattern for the module + while let Some(index) = statusvector + .iter() + .position(|x| (*x == StatusType::Enabled(StatusLvl::Instance)) || (*x == StatusType::Disabled(StatusLvl::Instance))) { + + statusvector.remove(index); + } + statusvector.push(StatusType::Enabled(StatusLvl::Instance)); + + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::Success("Set Enabled at Instance".to_string()) + ) + }, + } + + } + + pub async fn set_ch_disabled(&self, in_module: BotModule , in_chnl: Channel) -> (StatusType,ChangeResult) { + // at Instance level + // - If core module, do nothing + + + let mut dbt = self.statusdb.write().await; + + let (mgrp,statusvector) = dbt.get_mut(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::Failed("Core Modules cannot be disabled".to_string()) + ) + }, + ModGroup::Custom => { + // remove all channel level pattern for the module + while let Some(index) = statusvector + .iter() + .position(|x| + (*x == StatusType::Enabled(StatusLvl::Ch(in_chnl.clone()))) || (*x == StatusType::Disabled(StatusLvl::Ch(in_chnl.clone())))) + { + + statusvector.remove(index); + } + statusvector.push(StatusType::Disabled(StatusLvl::Ch(in_chnl.clone()))); + + ( + StatusType::Disabled(StatusLvl::Ch(in_chnl.clone())), + ChangeResult::Success("Set Disabled at Channel Level".to_string()) + ) + }, + } + + } + + pub async fn set_ch_enabled(&self, in_module: BotModule , in_chnl: Channel) -> (StatusType,ChangeResult) { + // at Instance level + // - If core module, do nothing + + let mut dbt = self.statusdb.write().await; + + let (mgrp,statusvector) = dbt.get_mut(&in_module).unwrap(); + + match mgrp { + ModGroup::Core => { + ( + StatusType::Enabled(StatusLvl::Instance), + ChangeResult::NoChange("Core Modules are always Enabled".to_string()) + ) + }, + ModGroup::Custom => { + // remove all channel level pattern for the module + while let Some(index) = statusvector + .iter() + .position(|x| + (*x == StatusType::Enabled(StatusLvl::Ch(in_chnl.clone()))) || (*x == StatusType::Disabled(StatusLvl::Ch(in_chnl.clone())))) + { + + statusvector.remove(index); + } + statusvector.push(StatusType::Enabled(StatusLvl::Ch(in_chnl.clone()))); + + ( + StatusType::Enabled(StatusLvl::Ch(in_chnl.clone())), + ChangeResult::Success("Set Enabled at Channel Level".to_string()) + ) + }, + } + + + } + + + + pub async fn add_botaction(&self, in_module: BotModule, in_action: BotAction) { + self.int_add_botaction(in_module,ModGroup::Custom,in_action).await; + } + + pub async fn add_core_act(&self, in_module: BotModule, in_action: BotAction) { + self.int_add_botaction(in_module,ModGroup::Core,in_action).await; + } + + + pub async fn affirm_in_statusdb(&self,in_module:BotModule,in_modgroup: ModGroup) { + + let mut dbt = self.statusdb.write().await; + + let (_,statusvector) = dbt.entry(in_module.clone()).or_insert((in_modgroup.clone(),Vec::new())); + + if !statusvector.contains(&StatusType::Enabled(StatusLvl::Instance)) && !statusvector.contains(&StatusType::Disabled(StatusLvl::Instance)) + { + match in_modgroup { + ModGroup::Core => statusvector.push(StatusType::Enabled(StatusLvl::Instance)) , // Pushes the Module as Enabled at Instance Level + ModGroup::Custom => statusvector.push(StatusType::Disabled(StatusLvl::Instance)), + } + } + + } + + async fn int_add_botaction(&self, in_module: BotModule, in_modgroup: ModGroup, in_action: BotAction) { botlog::trace( "Add botaction called", Some("ModulesManager > init()".to_string()), @@ -242,13 +1334,21 @@ impl ModulesManager { // - If BotAction to Add is a BotCommand , In Module Manager DB (botactions), // Check All Other BotAction Command Names & Aliases to ensure they don't conflict - async fn find_conflict_module(mgr: &ModulesManager, act: &BotAction) -> Option { + async fn find_conflict_module(mgr: &ModulesManager, act: &BotAction) -> Option { + if let BotAction::C(incmd) = act { let actdb = mgr.botactions.read().await; for (module, moduleactions) in &(*actdb) { - for modact in moduleactions.iter() { - if let BotAction::C(dbcmd) = &modact { + + + // for modact in moduleactions.iter() { + for modact_prelock in moduleactions.iter() { + + let modact = modact_prelock.read().await; + + // if let BotAction::C(dbcmd) = &modact { + if let BotAction::C(dbcmd) = &(*modact) { // At this point, there is an command incmd and looked up dbcmd // [x] check if given botcommand c.command:String conflicts with any in botactions @@ -299,15 +1399,12 @@ impl ModulesManager { ) } - let mut dbt = self.statusdb.write().await; - let statusvector = dbt.entry(in_module.clone()).or_insert(Vec::new()); - - statusvector.push(ModStatusType::Enabled(StatusLvl::Instance)); // Pushes the Module as Enabled at Instance Level + self.affirm_in_statusdb(in_module.clone(),in_modgroup).await; let mut a = self.botactions.write().await; let modactions = a.entry(in_module.clone()).or_insert(Vec::new()); - modactions.push(in_action); + modactions.push(Arc::new(RwLock::new(in_action))); botlog::trace( format!( @@ -320,12 +1417,807 @@ impl ModulesManager { ); } - fn _statuscleanup(&self, _: Option) { + fn _statuscleanup(&self, _: Option) { // internal cleans up statusdb . For example : // - remove redudancies . If we see several Enabled("m"), only keep 1x // - Clarify Conflict. If we see Enabled("m") and Disabled("m") , we remove Enabled("m") and keep Disabled("m") // the IDEAL is that this is ran before every read/update operation to ensure quality - // Option can pass Some(Channel("m")) (as an example) so statuscleanup only works on the given channel + // Option can pass Some(Channel("m")) (as an example) so statuscleanup only works on the given channel // Passing None to chnl may be a heavy operation, as this will review and look at the whole table } } + + +// ===================== +// ===================== +// ===================== +// ===================== +// ===================== + +#[cfg(test)] +mod core_modulesmanager { + + + use casual_logger::Log; + use casual_logger::Extension; + + use super::*; + + #[test] + fn case_insensitive_test() { + Log::set_file_ext(Extension::Log); + assert_eq!( + BotModule("TEST".to_string()), + BotModule("test".to_string()) + ); + } + + + /* + Possible Tests + + [x] Test 1 - Custom ModGroup Workflow + 1. affirm_in_statusdb(Experiments01,Custom) + 2. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 3. set_instance_enabled(Experiments01) + 4. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 5. set_ch_disabled(Experiments01,TestChannel01) + 6. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 7. set_ch_enabled(Experiments01,TestChannel01) & set_ch_disabled(Experiments01,TestChannel02) + 8. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 9. set_instance_disabled(Experiments01) + 10. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 11. force_disable(Experiments01) + 12. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + + + [x] Test 2 - Core ModGroup Workflow + 1. affirm_in_statusdb(CoreModule01,Core) + 2. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + 3. set_instance_enabled(CoreModule01) + 4. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + 5. set_ch_disabled(CoreModule01,TestChannel01) + 6. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + 7. set_ch_enabled(CoreModule01,TestChannel01) & set_ch_disabled(CoreModule01,TestChannel02) + 8. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + 9. set_instance_disabled(CoreModule01) + 10. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + 11. force_disable(CoreModule01) + 12. modstatus(CoreModule01,TestChannel01) & modstatus(CoreModule01,TestChannel02) + + + */ + + async fn complex_workflow( + in_module: BotModule , + in_modgroup : ModGroup , + in_chnl1 : Channel, + in_chnl2 : Channel) + { + + + let mgr = ModulesManager::init().await; + + /* + 1. affirm_in_statusdb(Experiments01,Custom) + 2. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + + mgr.affirm_in_statusdb(in_module.clone(), in_modgroup.clone()).await; + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Disabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Disabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + + + /* + 3. set_instance_enabled(Experiments01) + 4. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + mgr.set_instance_enabled(in_module.clone()).await; + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + + /* + 5. set_ch_disabled(Experiments01,TestChannel01) + 6. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + + mgr.set_ch_disabled(in_module.clone(),in_chnl1.clone()).await; + + //StatusType::Disabled(StatusLvl::Ch(in_chnl1.clone())) + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Disabled(StatusLvl::Ch(in_chnl1.clone()))); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + + + /* + 7. set_ch_enabled(Experiments01,TestChannel01) & set_ch_disabled(Experiments01,TestChannel02) + 8. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + + mgr.set_ch_enabled(in_module.clone(),in_chnl1.clone()).await; + + //StatusType::Disabled(StatusLvl::Ch(in_chnl1.clone())) + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Ch(in_chnl1.clone()))); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + + /* + 9. set_instance_disabled(Experiments01) + 10. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + + mgr.set_instance_disabled(in_module.clone()).await; + + // StatusType::Disabled(StatusLvl::Ch(in_chnl1.clone())) + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Ch(in_chnl1.clone()))); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Disabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + /* + 11. force_disable(Experiments01) + 12. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + */ + + mgr.force_disable(in_module.clone()).await; + + match in_modgroup { + ModGroup::Custom => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Disabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Disabled(StatusLvl::Instance)); + }, + ModGroup::Core => { + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl1.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + assert_eq!(mgr.modstatus(in_module.clone(), in_chnl2.clone()).await, + StatusType::Enabled(StatusLvl::Instance)); + }, + } + + } + + + #[tokio::test] + async fn custom_modgroup_workflow() { + Log::set_file_ext(Extension::Log); + + /* + + + [x] Test 1 - Custom ModGroup Workflow + 1. affirm_in_statusdb(Experiments01,Custom) + 2. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 3. set_instance_enabled(Experiments01) + 4. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 5. set_ch_disabled(Experiments01,TestChannel01) + 6. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 7. set_ch_enabled(Experiments01,TestChannel01) & set_ch_disabled(Experiments01,TestChannel02) + 8. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 9. set_instance_disabled(Experiments01) + 10. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + 11. force_disable(Experiments01) + 12. modstatus(Experiments01,TestChannel01) & modstatus(Experiments01,TestChannel02) + + */ + + let in_module = BotModule("Experiments01".to_string()); + let in_modgroup = ModGroup::Custom; + let (in_chnl1,in_chnl2) = + (Channel("TestChannel01".to_string()),Channel("TestChannel02".to_string())); + + complex_workflow(in_module, in_modgroup, in_chnl1, in_chnl2).await; + + + } + + + #[tokio::test] + async fn core_modgroup_workflow() { + Log::set_file_ext(Extension::Log); + + + let in_module = BotModule("CoreModule01".to_string()); + let in_modgroup = ModGroup::Core; + let (in_chnl1,in_chnl2) = + (Channel("TestChannel01".to_string()),Channel("TestChannel02".to_string())); + + complex_workflow(in_module, in_modgroup, in_chnl1, in_chnl2).await; + + + } + + + + + /* + 1. Create new ModulesManager & Identity Manager + 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + 3. affirm when BotAdmin attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + 4. affirm when BotAdmin attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + d. force disable + + + 1. Create new ModulesManager & Identity Manager + 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + 3. affirm when Mod attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + 4. affirm when Mod attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + d. force disable + + */ + + async fn inner_enable_disable_complex( + requestor:String, + channel:Channel, + idmgr:IdentityManager, + modsmgr:Arc) + { + + /* + Parent Tests would involve : + - Testing with a BotAdmin User + - Testing with a Mod User + - Testing with a Regular Chatter + */ + + enum TestScenarios { + BotadminUser, + ModUser, + RegularChatter, + // ModuleDoesNotExist, // preferring instead to handle in it's own smaller test + } + + let mut idlock = idmgr.clone(); + + let requestor_badge = None; // If they are a Mod on the Given Channel already, that can be evaluated without the current badge + + const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + let (admin_level_access,_) = idlock.can_user_run(requestor.clone(), channel.clone(), requestor_badge.clone(), + vec![ + identity::UserRole::BotAdmin, + ]).await; + + + let (chnl_elevated_access,_) = idlock.can_user_run(requestor.clone(), channel.clone(), requestor_badge.clone(), + vec![ + identity::UserRole::Mod(OF_CMD_CHANNEL), + identity::UserRole::SupMod(OF_CMD_CHANNEL), + identity::UserRole::Broadcaster, + ]).await; + + + let current_test_scenario = + match admin_level_access { + Permissible::Allow => { + match chnl_elevated_access { + Permissible::Allow => { TestScenarios::BotadminUser }, + Permissible::Block => { TestScenarios::BotadminUser } + } + }, + Permissible::Block => { + match chnl_elevated_access { + Permissible::Allow => { TestScenarios::ModUser }, + Permissible::Block => { TestScenarios::RegularChatter } + } + } + }; + + + + // [x] 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + let in_module = BotModule("Experiments01".to_string()); + let in_modgroup = ModGroup::Custom; + + modsmgr.affirm_in_statusdb(in_module.clone(), in_modgroup.clone()).await; + + /* + [x] 3. affirm when BotAdmin attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + */ + + + // [-] requestor_badge: Option, + + // [x] trg_module: BotModule, + let trg_module = in_module; + + // [x] trg_level: StatusLvl, + + let trg_level = StatusLvl::Ch(channel.clone()); // setting to Channel Level + + + // [x] id: Arc>, + let id = Arc::new(RwLock::new(idmgr.clone())); + + + let rslt = modsmgr.exec_enable(requestor.clone(), + None, + trg_module.clone(), + trg_level.clone(), + id.clone()).await; + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Failed("Promote yourself Temporarily First".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Success("Enabled at Channel Level".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + + /* + [x] 3. affirm when BotAdmin attempts to exec_enable on the following + b. Channel Level , when they are a Mod + */ + + // [x] requestor_badge: Option, + + let requestor_badge = match current_test_scenario { + TestScenarios::BotadminUser => + Some(ChatBadge::Mod), // setting badge to Mod -- for the Problem Scenario . They are both BotAdmin & Mod + TestScenarios::ModUser => + Some(ChatBadge::Mod), // setting badge to Mod + TestScenarios::RegularChatter => + None, // setting badge to None + } ; + + + let rslt = modsmgr.exec_enable(requestor.clone(), + requestor_badge, + trg_module.clone(), + trg_level.clone(), + id.clone()).await; + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Enabled at Channel Level".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Success("Enabled at Channel Level".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + /* + [x] 3. affirm when BotAdmin attempts to exec_enable on the following + c. Instance Level + */ + + let trg_level = StatusLvl::Instance; // setting to Instance level + + let requestor_badge = match current_test_scenario { + TestScenarios::BotadminUser => + None, + TestScenarios::ModUser => + Some(ChatBadge::Mod), + TestScenarios::RegularChatter => + None, // setting badge to None + }; + + let rslt = modsmgr.exec_enable(requestor.clone(), + requestor_badge, // passing based on scenario + trg_module.clone(), + trg_level.clone(), + id.clone()).await; + + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Enabled at Instance Level".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + /* + [x] 4. affirm when BotAdmin attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + */ + + let trg_level = StatusLvl::Ch(channel.clone()); // setting to Channel Level + + let rslt: ChangeResult = modsmgr.exec_disable(requestor.clone(), + None, // Does not have a ChatBadge like Mod + trg_module.clone(), + trg_level.clone(), + false, + id.clone()).await; + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Disabled at Channel Level".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Success("Disabled at Channel Level".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + + /* + [x] 4. affirm when BotAdmin attempts to exec_disable on the following + b. Channel Level , when they are a Mod + */ + + + let trg_level = StatusLvl::Ch(channel.clone()); // setting to Channel Level + + let requestor_badge = match current_test_scenario { + TestScenarios::BotadminUser => + None, + TestScenarios::ModUser => + Some(ChatBadge::Mod), + TestScenarios::RegularChatter => + None, // setting badge to None + }; + + let rslt: ChangeResult = modsmgr.exec_disable(requestor.clone(), + requestor_badge, + trg_module.clone(), + trg_level.clone(), + false, + id.clone()).await; + + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Disabled at Channel Level".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Success("Disabled at Channel Level".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + + /* + [x] 4. affirm when BotAdmin attempts to exec_disable on the following + c. Instance Level + */ + + let trg_level = StatusLvl::Instance; // setting to Instance level + + + let rslt: ChangeResult = modsmgr.exec_disable(requestor.clone(), + None, // Does not have a ChatBadge like Mod + trg_module.clone(), + trg_level.clone(), + false, + id.clone()).await; + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Disabled at Instance Level".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + /* + [ ] 4. affirm when BotAdmin attempts to exec_disable on the following + d. force disable + */ + + let trg_level = StatusLvl::Instance; // setting to Instance level + + let rslt: ChangeResult = modsmgr.exec_disable(requestor.clone(), + None, // Does not have a ChatBadge like Mod + trg_module.clone(), + trg_level.clone(), + true, // force flag - true + id.clone()).await; + + match current_test_scenario { + TestScenarios::BotadminUser => + assert_eq!(rslt,ChangeResult::Success("Forced Disable".to_string())), + TestScenarios::ModUser => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + TestScenarios::RegularChatter => + assert_eq!(rslt,ChangeResult::Failed("You're not allowed".to_string())), + } + + + } + + + #[tokio::test] + async fn enable_disable_bot_admin_workflow() { + Log::set_file_ext(Extension::Log); + /* + + 1. Create new ModulesManager & Identity Manager + 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + 3. affirm when BotAdmin attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + 4. affirm when BotAdmin attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + d. force disable + + */ + + + // [x] 1. Create new ModulesManager & Identity Manager + let idmgr = IdentityManager::init(); + let modsmgr = ModulesManager::init().await; + + /* + [x] 3. affirm when BotAdmin attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + */ + + // [x] Create BotAdmin first + + let requestor = "botadministrator".to_string(); + + idmgr.affirm_chatter_in_db(requestor.clone()).await; + idmgr + .add_role(requestor.clone(), identity::UserRole::BotAdmin) + .await; + + let rslt = idmgr + .getspecialuserroles(requestor.clone(), None) + .await; + + assert!(rslt.contains(&identity::UserRole::BotAdmin)); + + let channel = Channel("somechannel".to_string()); + + + inner_enable_disable_complex(requestor, channel, idmgr, modsmgr).await; + + + + } + + + + #[tokio::test] + async fn enable_disable_mod_workflow() { + Log::set_file_ext(Extension::Log); + + /* + 1. Create new ModulesManager & Identity Manager + 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + 3. affirm when Mod attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + 4. affirm when Mod attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + d. force disable + */ + + + // [x] 1. Create new ModulesManager & Identity Manager + let idmgr = IdentityManager::init(); + let modsmgr = ModulesManager::init().await; + + + let requestor = "mod_user".to_string(); + // let botadmin_badge = &None; + let channel = Channel("somechannel".to_string()); + + + idmgr.affirm_chatter_in_db(requestor.clone()).await; + idmgr + .add_role(requestor.clone(), identity::UserRole::Mod(channel.clone())) + .await; + + let rslt = idmgr + .getspecialuserroles( + requestor.clone(), + Some(channel.clone()) // None if BotAdmin ; Otherwise, pass Some(Channel) + ) + .await; + + assert!(rslt.contains(&identity::UserRole::Mod(channel.clone()))); + + + inner_enable_disable_complex(requestor, channel, idmgr, modsmgr).await; + + + + } + + #[tokio::test] + async fn enable_disable_chatter_workflow() { + Log::set_file_ext(Extension::Log); + + + /* + 1. Create new ModulesManager & Identity Manager + 2. modmgr.affirm_in_statusdb(Experiments01,Custom) + + 3. affirm when Mod attempts to exec_enable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + 4. affirm when Mod attempts to exec_disable on the following + a. Channel Level , where they are not a Mod + b. Channel Level , when they are a Mod + c. Instance Level + d. force disable + */ + + + // [x] 1. Create new ModulesManager & Identity Manager + let idmgr = IdentityManager::init(); + let modsmgr = ModulesManager::init().await; + + + let requestor = "regular_user".to_string(); + let channel = Channel("somechannel".to_string()); + + + idmgr.affirm_chatter_in_db(requestor.clone()).await; + + let rslt = idmgr + .getspecialuserroles( + requestor.clone(), + Some(channel.clone()) // None if BotAdmin ; Otherwise, pass Some(Channel) + ) + .await; + + assert!(!rslt.contains(&identity::UserRole::Mod(channel.clone())) || + !rslt.contains(&identity::UserRole::BotAdmin)); + + + inner_enable_disable_complex(requestor, channel, idmgr, modsmgr).await; + + } + + + #[tokio::test] + async fn enable_disable_modulenotexist_workflow() { + Log::set_file_ext(Extension::Log); + + + // [x] 1. Create new ModulesManager & Identity Manager + let idmgr = IdentityManager::init(); + let modsmgr = ModulesManager::init().await; + + + let requestor = "regular_user".to_string(); + + let channel = Channel("somechannel".to_string()); + + + idmgr.affirm_chatter_in_db(requestor.clone()).await; + + + let rslt = idmgr + .getspecialuserroles( + requestor.clone(), + Some(channel.clone()) // None if BotAdmin ; Otherwise, pass Some(Channel) + ) + .await; + + assert!(!rslt.contains(&identity::UserRole::Mod(channel.clone())) || + !rslt.contains(&identity::UserRole::BotAdmin)); + + // After above, regular chatter is created + + // [x] 2. modmgr.affirm_in_statusdb(Existing_Module,Custom) + + let in_module = BotModule("Existing_Module".to_string()); + let in_modgroup = ModGroup::Custom; + + modsmgr.affirm_in_statusdb(in_module.clone(), in_modgroup.clone()).await; + + + let trg_level = StatusLvl::Ch(channel.clone()); // setting to Channel Level + + + // [x] Test with Non Existing module > exec + + let trg_module = BotModule("Non_Existent_Module".to_string()); + + let rslt = modsmgr.exec_enable(requestor.clone(), + None, + trg_module.clone(), + trg_level.clone(), + Arc::new(RwLock::new(idmgr.clone()))).await; + + assert_eq!(rslt,ChangeResult::Failed("Module doesn't exist".to_string())); + + // [x] Test with Non Existing module > disable + + let trg_module = BotModule("Non_Existent_Module".to_string()); + + let rslt = modsmgr.exec_disable(requestor.clone(), + None, + trg_module.clone(), + trg_level.clone(), + false, + Arc::new(RwLock::new(idmgr))).await; + + assert_eq!(rslt,ChangeResult::Failed("Module doesn't exist".to_string())); + + } + +} diff --git a/src/core/chat.rs b/src/core/chat.rs index 4720af7..43984bc 100644 --- a/src/core/chat.rs +++ b/src/core/chat.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use twitch_irc::login::StaticLoginCredentials; -use twitch_irc::message::PrivmsgMessage; +use twitch_irc::message::ReplyToMessage; use twitch_irc::transport::tcp::{TCPTransport, TLS}; use twitch_irc::TwitchIRCClient; @@ -15,19 +15,36 @@ use rand::Rng; use crate::core::ratelimiter; use crate::core::ratelimiter::RateLimiter; -use crate::core::botinstance::ChType; +use crate::core::botinstance::Channel; use crate::core::botlog; -pub use ChType::Channel; + + +use tokio::time::{sleep, Duration}; + +use super::bot_actions::ExecBodyParams; +use super::identity; + + +use async_recursion::async_recursion; #[derive(Clone)] pub struct Chat { - pub ratelimiters: Arc>>, // used to limit messages sent per channel + pub ratelimiters: Arc>>, // used to limit messages sent per channel pub client: TwitchIRCClient, StaticLoginCredentials>, } + +#[derive(Clone,Debug)] +pub enum BotMsgType { + SayInReplyTo(Channel,String,String), // ( Destination Channel , Message ID to reply to , OutMessage ) // https://docs.rs/twitch-irc/latest/twitch_irc/client/struct.TwitchIRCClient.html#method.say_in_reply_to + Say(String,String), + Notif(String), // For Bot Sent Notifications +} + + impl Chat { pub fn init( - ratelimiters: HashMap, + ratelimiters: HashMap, client: TwitchIRCClient, StaticLoginCredentials>, ) -> Chat { Chat { @@ -36,12 +53,16 @@ impl Chat { } } - pub async fn init_channel(&mut self, chnl: ChType) { + pub async fn init_channel(&mut self, chnl: Channel) { let n = RateLimiter::new(); self.ratelimiters.lock().await.insert(chnl, n); } - pub async fn say_in_reply_to(&self, msg: &PrivmsgMessage, mut outmsg: String) { + #[async_recursion] + pub async fn send_botmsg(&self, msginput: BotMsgType, params : ExecBodyParams) { + + + /* formats message before sending to TwitchIRC @@ -51,14 +72,265 @@ impl Chat { */ + botlog::trace( + format!("send_bot_msg params : {:?}",msginput).as_str(), + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + Log::flush(); + + + let (channel_login,mut outmsg) = match msginput.clone() { + BotMsgType::SayInReplyTo(chnl, _, outmsg) => { + (chnl.0.to_lowercase(), // Desintation Channel + outmsg) + }, + BotMsgType::Say(a,b ) => { + (a.clone(),b.clone()) + }, + BotMsgType::Notif(outmsg) => { + (params.msg.channel_login.clone(),outmsg) + } + }; + + + + + botlog::trace( + "BEFORE parent_module call", + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + + let parent_module = params.get_parent_module().await; + + let params_clone = params.clone(); + let botclone = Arc::clone(¶ms_clone.bot); + let botlock = botclone.read().await; + let modmgr = Arc::clone(&botlock.botmodules); + let modstatus = (*modmgr).modstatus( + parent_module.clone().expect("ERROR - Expected a module"), + Channel(channel_login.clone()) + ).await; + + if !params.bot.read().await.bot_channels.contains(&Channel(channel_login.clone())) { + + dbg!("ISSUE : NONJOINED CHANNEL",¶ms.bot.read().await.bot_channels,Channel(channel_login.clone())); + botlog::warn( + &format!("A message attempted to send for a Non-Joined Channel : {}",channel_login.clone()), + Some("Chat > send_botmsg".to_string()), + None, + ); + + if let BotMsgType::SayInReplyTo(_chnl,_msgid, _outmsg) = msginput { + + self.send_botmsg(BotMsgType::Notif( + "uuh Bot can't send to a channel it isn't joined".to_string(), + ), + params).await; + + } + + return ; + } + + /* + [x] !! => 03.24 - Somewhere around here, we should be validating module for target channel + */ + + + /* + - Use ModulesManager.modstatus + + modstatus(&self, in_module: BotModule, in_chnl: Channel) -> StatusType + + */ + + botlog::trace( + format!("BEFORE modstatus check : modstatus = {:?}",modstatus).as_str(), + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + + + + if let super::botmodules::StatusType::Disabled(lvl) = modstatus { + // Note : At this point, chat was called in a channel where the parent module IS enabled + // - this type of validation is done outside of Chat() + // This though takes into account scenarios where we are targetting a different channel + + + botlog::trace( + "BEFORE msginput check", + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + + Log::flush(); + + match msginput { + BotMsgType::Notif(_) => (), // Do nothing with Notif > We'll validate the user later to handle + BotMsgType::SayInReplyTo(_, _, _) | BotMsgType::Say(_,_) => { + + botlog::trace( + "BEFORE potential Async recursion", + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.clone().msg), + ); + + Log::flush(); + + + self.send_botmsg(BotMsgType::Notif( + format!("uuh {:?} is disabled on {} : {:?}", + parent_module.clone().unwrap(), + channel_login.clone(), + lvl + ), + ), params.clone() + ).await; + + + botlog::trace( + "AFTER potential Async recursion", + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + + + Log::flush(); + + return + }, + + } + + } + + + /* + + [x] !! => 03.24 - Would be nice if around here , validate the user has at least some special roles in target channel + - NOTE : If these need to be refined, they can be by the custom module developer at the parent calling function of say() + - This just prevents Chat from being triggered in a channel where the sending chatter does not have any special roles + + */ + + + + /* + + Use + pub async fn getspecialuserroles( + &self, + chattername: String, + channel: Option, + ) -> Vec { + + */ + + // let params_clone = params.clone(); + + let botclone = Arc::clone(¶ms.bot); + let botlock = botclone.read().await; + let id = botlock.get_identity(); + let id = Arc::clone(&id); + let idlock = id.read().await; + let user_roles = idlock.getspecialuserroles( + params.get_sender(), + Some(Channel(channel_login.clone())) + ).await; + + botlog::trace( + format!("BEFORE user roles check check : userroles = {:?}",user_roles).as_str(), + Some("chat.rs > send_botmsg ".to_string()), + Some(¶ms.msg), + ); + + Log::flush(); + + // [x] If user has any of the following target roles, they will be allowed - otherwise, they will not be allowed to send + // - Otherwise if not (checked here) , this will not run + // - NOTE : For now, I've removed BotAdmin just for curiosity - BotAdmins can always elevate themselves if they want + // - Will be adding VIP to this as this should include Channel_Level Roles + + if !(user_roles.contains(&identity::UserRole::Mod(Channel(channel_login.clone()))) + || user_roles.contains(&identity::UserRole::SupMod(Channel(channel_login.clone()))) + || user_roles.contains(&identity::UserRole::Broadcaster) + || user_roles.contains(&identity::UserRole::VIP(Channel(channel_login.clone()))) + ) + { + + + match msginput { + BotMsgType::Notif(_) => { + // If Sender is Not a BotAdmin, don't do anything about the notification and return + if !user_roles.contains(&identity::UserRole::BotAdmin) { + return; + } + }, + BotMsgType::SayInReplyTo(_,_,_ ) | BotMsgType::Say(_,_) => { + // If the BotMsg a Say/SayInReplyTo (from Developer or Chatter) , and the Sender does not have Specific Roles in the Source Channel Sent + + self.send_botmsg(BotMsgType::Notif( + format!("uuh You do not have the right roles to send to {}", + channel_login.clone(), + ), + ), params.clone() + ).await; + + return; + + }, + }; + + + + } + + + /* + At this stage from the above Validations : + msginput would be : + a. BotMsgType::SayInReplyTo | BotMsgType::Say that is + - Towards a Destination Channel that the Sender has Elevated User Roles to Send to + b. BotMsgType::Notif that is + - Going to be sent to the Source Channel (rather than the original say/sayinreplyto was towards) + - A Sender that has Elevated User Roles in Source Channel will see a message ; otherwise, they will not + */ + + /* + + Use the following + + pub async fn can_user_run( + &mut self, + usr: String, + channelname: Channel, + chat_badge: Option, + cmdreqroles: Vec, // ) -> Result> { + ) -> (Permissible, ChangeResult) { + + */ + let rl = Arc::clone(&self.ratelimiters); let mut rllock = rl.lock().await; + botlog::debug( + &format!("Ratelimiter being checked for channel : {}",channel_login.clone()), + Some("Chat > send_botmsg".to_string()), + None, + ); + let contextratelimiter = rllock - // .get_mut() - .get_mut(&Channel(String::from(&msg.channel_login))) + .get_mut(&Channel(channel_login.to_lowercase().clone())) .expect("ERROR: Issue with Rate limiters"); + // Continue to check the limiter and sleep if required if the minimum is not reached + while let ratelimiter::LimiterResp::Sleep(sleeptime) = contextratelimiter.check_limiter() { + sleep(Duration::from_secs_f64(sleeptime)).await; + } + match contextratelimiter.check_limiter() { ratelimiter::LimiterResp::Allow => { let maxblanks = rand::thread_rng().gen_range(1..=20); @@ -68,34 +340,106 @@ impl Chat { outmsg.push_str(blankspace); } - self.client.say_in_reply_to(msg, outmsg).await.unwrap(); + match msginput.clone() { + BotMsgType::SayInReplyTo(chnl,msgid, _) => { + dbg!(chnl.clone(),msgid.clone(),outmsg.clone()); + + self.client.say_in_reply_to(&( + chnl.0, + msgid), + outmsg).await.unwrap(); + }, + BotMsgType::Say(a, _) => { + self.client.say(a, outmsg).await.unwrap(); + } + BotMsgType::Notif(outmsg) => { + + dbg!(params.msg.channel_login(),params.msg.message_id()); + self.client.say_in_reply_to(¶ms.msg, outmsg).await.unwrap(); + } + } + contextratelimiter.increment_counter(); let logstr = format!( - "(#{}) > {} ; Ratelimiers : {:?}", - msg.channel_login, "rate limit counter increase", self.ratelimiters + "(#{}) > {} ; contextratelimiter : {:?}", + channel_login.clone(), "rate limit counter increase", contextratelimiter ); - botlog::trace( - logstr.as_str(), - Some("Chat > say_in_reply_to".to_string()), - Some(msg), - ); + if let BotMsgType::SayInReplyTo(_,_,_ ) = msginput { + botlog::trace( + logstr.as_str(), + Some("Chat > send_botmsg".to_string()), + None, + ); + } else { + botlog::trace( + logstr.as_str(), + Some("Chat > send_botmsg".to_string()), + None, + ); + } + + + } ratelimiter::LimiterResp::Skip => { // (); // do nothing otherwise } + ratelimiter::LimiterResp::Sleep(_) => { + panic!("ISSUE : sleep was already awaited - Should not happen?"); + } } - Log::flush(); + + + Log::flush(); } - async fn _say(&self, _: String, _: String) { + + pub async fn say_in_reply( + &self, + destination_channel : Channel , + outmsg: String , + params : ExecBodyParams) + { + + self.send_botmsg(BotMsgType::SayInReplyTo( + destination_channel, + params.msg.message_id().to_string(), + outmsg) , params).await; + + + } + + pub async fn say_in_reply_to( + &self, + destination_channel : Channel , + reply_message_id : String , + outmsg: String , + params : ExecBodyParams) + { + + self.send_botmsg(BotMsgType::SayInReplyTo( + destination_channel, + reply_message_id, + outmsg) , params).await; + + } + + + + + pub async fn say(&self, channel_login: String, message: String , params : ExecBodyParams) { // more info https://docs.rs/twitch-irc/latest/twitch_irc/client/struct.TwitchIRCClient.html#method.say - // self.client.say(msg,outmsg).await.unwrap(); + self.send_botmsg(BotMsgType::Say(channel_login.to_lowercase(), message), params).await; } + + + + async fn _me(&self, _: String, _: String) { // more info https://docs.rs/twitch-irc/latest/twitch_irc/client/struct.TwitchIRCClient.html#method.say diff --git a/src/core/identity.rs b/src/core/identity.rs index effb317..0bfd934 100644 --- a/src/core/identity.rs +++ b/src/core/identity.rs @@ -1,3 +1,8 @@ + + +const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + use std::collections::HashMap; use std::sync::Arc; @@ -7,13 +12,17 @@ use twitch_irc::message::PrivmsgMessage; use casual_logger::Log; -use crate::core::bot_actions::actions_util::{self, BotAR}; -use crate::core::botinstance::ChType; +use crate::core::bot_actions::actions_util; +use crate::core::bot_actions::ExecBodyParams; +use crate::core::botinstance::{Channel,ChangeResult}; use crate::core::botlog; use crate::core::botmodules::{BotActionTrait, BotCommand, BotModule, ModulesManager}; use dotenv::dotenv; use std::env; +use dotenv::dotenv; +use std::env; + fn adminvector() -> Vec { dotenv().ok(); let mut admins = Vec::new(); @@ -27,6 +36,30 @@ fn adminvector() -> Vec { admins } + +pub fn otherbots_vector() -> Vec { + + dotenv().ok(); + let mut other_bots = Vec::new(); + + if let Ok(value) = env::var("OtherBots") { + for bot in value.split(',') { + other_bots.push(String::from(bot).to_lowercase()) + } + } + + botlog::debug( + &format!( + "OtherBots : {:?}",other_bots, + ), + Some("identity.rs > otherbots_vector()".to_string()), + None, + ); + + other_bots +} + + pub async fn init(mgr: Arc) { botlog::trace( "Went into Identity Module init", @@ -41,170 +74,15 @@ pub async fn init(mgr: Arc) { exec_body: actions_util::asyncbox(cmd_promote), help: String::from("promote"), required_roles: vec![ - UserRole::Mod(ChType::Channel(String::new())), - UserRole::SupMod(ChType::Channel(String::new())), + UserRole::Mod(OF_CMD_CHANNEL), + UserRole::SupMod(OF_CMD_CHANNEL), UserRole::Broadcaster, UserRole::BotAdmin, ], }; - tempb.add_to_modmgr(Arc::clone(&mgr)).await; - - async fn cmd_promote(bot: BotAR, msg: PrivmsgMessage) { - botlog::trace( - "Called cmd promote", - Some("identity.rs > cmd_prommote()".to_string()), - Some(&msg), - ); - - // -- If the BotCommand.command was called (e.g., promote) & required roles were validated OUTSIDE of this call - // , this is the current function body to execute - - /* - - `promote` / `demote` - - [ ] `SupMod` & `Broadcaster` & `BotAdmin` can run - - [ ] `UserRole`s that can run, can - - [ ] run `promote` on a regular `Chatter` to make them a `Mod` - - [ ] run `demote` on a `Mod` to make them a `Chatter` - - [ ] Only `BotAdmin` can : - - [ ] target themselves to `promote` / `demote` , in the case that they want to make themselves either a `Mod` or `SupMod` for the channel temporarily - - [ ] `promote admin ` to assign them `BotAdmin` role - - `[ ] Broadcaster` & `BotAdmin` can `demote` a `SupMod` to make them a `Mod` or `promote` the other way - */ - - /* - Usage : - - promote - - demote - - promote -admin - - */ - - // println!("{}",msg.message_text); - botlog::trace( - format!("Twich Message > {}", msg.message_text).as_str(), - Some("identity.rs > cmd_promote()".to_string()), - None, - ); - - let sendername = msg.clone().sender.name; - - let mut argv = msg.message_text.split(' '); - - argv.next(); // Skip the command name - - let arg1 = argv.next(); - - let arg2 = argv.next(); - - let mut sender_badge: Option = None; - - for b in &msg.badges { - if b.name == "moderator" { - sender_badge = Some(ChatBadge::Mod); - } else if b.name == "broadcaster" { - sender_badge = Some(ChatBadge::Broadcaster); - } - } - - let targetchnl = msg.channel_login.to_lowercase(); - - /* - - [x] 1. Get trgusr (regardless of -admin flag) - [x] 2. promote trguser - [x] 3. Output resulting change - - */ - - // [x] 1. Get trgusr (regardless of -admin flag) - - let targetusr = if arg1 == Some("-admin") { arg2 } else { arg1 }; - - // [x] 2. promote trguser - - // [x] Get a required lock first - - let botlock = bot.read().await; - let id = botlock.get_identity(); - let idlock = id.read().await; - - let rslt = match targetusr { - Some(targetusr) => { - botlog::debug( - "running promote()", - Some("identity.rs > cmd_promote()".to_string()), - None, - ); - Log::flush(); - - let target_bot_admin_role = if arg1 == Some("-admin") { - Some(UserRole::BotAdmin) - } else { - None - }; - - idlock - .promote( - sendername.clone(), - &sender_badge, - targetusr.to_string(), - Some(ChType::Channel(targetchnl.clone())), - target_bot_admin_role, - ) - .await - } - - None => { - botlog::debug( - // &format!("No Targer User argument"), - "No Targer User argument", - Some("identity.rs > cmd_demote()".to_string()), - None, - ); - Log::flush(); - - ChangeResult::NoChange("No Targer User".to_string()) - } - }; - - // [x] 3. Output resulting change - - let outmsg = match rslt { - ChangeResult::Success(a) => { - format!("o7 Successfully promoted : {a}") - } - ChangeResult::Failed(a) => { - format!("PoroSad failed to promote : {a}") - } - ChangeResult::NoChange(a) => { - format!("uuh No Promotion Change : {a}") - } - }; - - botlog::debug( - outmsg.as_str(), - Some("identity.rs > cmd_prommote()".to_string()), - Some(&msg), - ); - - botlock - .botmgrs - .chat - .say_in_reply_to(&msg, outmsg.to_string()) - .await; - - botlog::trace( - // &format!("End of cmd_promote()"), - "End of cmd_promote()", - Some("identity.rs > cmd_prommote()".to_string()), - None, - ); - } - + tempb.add_core_to_modmgr(Arc::clone(&mgr)).await; + let tempb = BotCommand { module: BotModule(String::from("identity")), command: String::from("demote"), // command call name @@ -212,189 +90,15 @@ pub async fn init(mgr: Arc) { exec_body: actions_util::asyncbox(cmd_demote), help: String::from("demote"), required_roles: vec![ - UserRole::Mod(ChType::Channel(String::new())), - UserRole::SupMod(ChType::Channel(String::new())), + UserRole::Mod(OF_CMD_CHANNEL), + UserRole::SupMod(OF_CMD_CHANNEL), UserRole::Broadcaster, UserRole::BotAdmin, ], }; - tempb.add_to_modmgr(Arc::clone(&mgr)).await; - - async fn cmd_demote(bot: BotAR, msg: PrivmsgMessage) { - botlog::debug( - "Called cmd demote", - Some("identity.rs > cmd_demote()".to_string()), - Some(&msg), - ); - Log::flush(); - - // -- If the BotCommand.command was called (e.g., demote) & required roles were validated OUTSIDE of this call - // , this is the current function body to execute - - /* - - `promote` / `demote` - - [ ] `SupMod` & `Broadcaster` & `BotAdmin` can run - - [ ] `UserRole`s that can run, can - - [ ] run `promote` on a regular `Chatter` to make them a `Mod` - - [ ] run `demote` on a `Mod` to make them a `Chatter` - - [ ] Only `BotAdmin` can : - - [ ] target themselves to `promote` / `demote` , in the case that they want to make themselves either a `Mod` or `SupMod` for the channel temporarily - - [ ] `promote admin ` to assign them `BotAdmin` role - - `[ ] Broadcaster` & `BotAdmin` can `demote` a `SupMod` to make them a `Mod` or `promote` the other way - */ - - /* - Usage : - - promote - - demote - - promote -admin - - */ - - // [x] Unwraps arguments from message - - let (arg1, _arg2) = { - let mut argv = msg.message_text.split(' '); - - argv.next(); // Skip the command name - - let arg1 = argv.next(); - - let arg2 = argv.next(); - - (arg1, arg2) - }; - - // --- - - /* - - [x] 1. Parse out the following - - Sender (e.g., Name & Badge) - - Target User (arg1) - - Target Channel (current channel) - - Msg or Msg.Message_Text (for later) - - - [x] 2. Run Demote() - - within demote(), getspecialuserroles() is called on both the sender and the target - - getspecialuserroles() only sends current db , while canuserrun() may change db depending on the most current state of the sender - - getspecialuserroles also borrows the sender's badge to evaluate - - - [x] 3. Take ChangeResult and output response - - */ - - /* - - - [x] 1. Parse out the following - - Sender (e.g., Name & Badge) - - Target User (arg1) - - Target Channel (current channel) - - (no need) Msg or Msg.Message_Text (for later) - - */ - - let sendername = msg.clone().sender.name; - - let mut sender_badge_mut: Option = None; - - for b in &msg.badges { - if b.name == "moderator" { - sender_badge_mut = Some(ChatBadge::Mod); - } else if b.name == "broadcaster" { - sender_badge_mut = Some(ChatBadge::Broadcaster); - } - } - - let sender_badge = sender_badge_mut; - - let targetusr = arg1; - - let targetchnl = msg.channel_login.to_lowercase(); - - /* - - - [x] 2. Run Demote() - - within demote(), getspecialuserroles() is called on both the sender and the target - - getspecialuserroles() only sends current db , while canuserrun() may change db depending on the most current state of the sender - - getspecialuserroles also borrows the sender's badge to evaluate - - - */ - - // [x] Get a required lock first - - let botlock = bot.read().await; - let id = botlock.get_identity(); - let idlock = id.read().await; - - let rslt = match targetusr { - Some(targetusr) => { - botlog::debug( - // &format!("running demote()"), - "running demote()", - Some("identity.rs > cmd_demote()".to_string()), - None, - ); - Log::flush(); - - idlock - .demote( - sendername.clone(), - &sender_badge, - targetusr.to_string(), - Some(ChType::Channel(targetchnl.clone())), - ) - .await - } - - None => { - botlog::debug( - // &format!("No Targer User argument"), - "No Targer User argument", - Some("identity.rs > cmd_demote()".to_string()), - None, - ); - Log::flush(); - - ChangeResult::NoChange("No Targer User".to_string()) - } - }; - - /* - - - [x] 3. Take ChangeResult and output response - - */ - - let outmsg = match rslt { - ChangeResult::Success(a) => { - format!("o7 Successfully demoted : {a}") - } - ChangeResult::Failed(a) => { - format!("PoroSad failed to demote : {a}") - } - ChangeResult::NoChange(a) => { - format!("uuh No Demotion Change : {a}") - } - }; - - botlog::debug( - outmsg.as_str(), - Some("identity.rs > cmd_demote()".to_string()), - Some(&msg), - ); - - botlock - .botmgrs - .chat - .say_in_reply_to(&msg, outmsg.to_string()) - .await; - } - + tempb.add_core_to_modmgr(Arc::clone(&mgr)).await; + let tempcomm = BotCommand { module: BotModule(String::from("identity")), command: String::from("getroles"), // command call name @@ -402,146 +106,15 @@ pub async fn init(mgr: Arc) { exec_body: actions_util::asyncbox(getroles), help: String::from("getroles"), required_roles: vec![ - UserRole::Mod(ChType::Channel(String::new())), - UserRole::SupMod(ChType::Channel(String::new())), + UserRole::Mod(OF_CMD_CHANNEL), + UserRole::SupMod(OF_CMD_CHANNEL), UserRole::Broadcaster, UserRole::BotAdmin, ], }; - tempcomm.add_to_modmgr(Arc::clone(&mgr)).await; - - async fn getroles(bot: BotAR, msg: PrivmsgMessage) { - botlog::debug( - "Called cmd getroles", - Some("identity.rs > cmd_getroles()".to_string()), - Some(&msg), - ); - - /* - Usage - - getroles - - If channel is provided, provide roles for that channel specifically - - */ - - let mut argv = msg.message_text.split(' '); - - argv.next(); // Skip the command name - - let arg1 = argv.next(); - - let targetuser = match arg1 { - None => return, // exit if no arguments - Some(arg) => arg, - }; - - let arg2 = argv.next(); - - let targetchnl = arg2; - - let botlock = bot.read().await; - - let id = botlock.get_identity(); - - let idlock = id.read().await; - - let sproles = match targetchnl { - None => { - // [ ] If targetchnl is not provided, default to pulling the current channel - idlock - .getspecialuserroles( - String::from(targetuser), - Some(ChType::Channel(msg.channel_login.to_lowercase())), - ) - .await - } - Some(targetchnl) => { - // [x] gets special roles for caller - let callersproles = idlock - .getspecialuserroles( - msg.sender.name.to_lowercase(), - Some(ChType::Channel(targetchnl.to_lowercase().to_string())), - ) - .await; - - if callersproles.contains(&UserRole::Mod(ChType::Channel( - targetchnl.to_lowercase().to_string(), - ))) || callersproles.contains(&UserRole::SupMod(ChType::Channel( - targetchnl.to_lowercase().to_string(), - ))) || callersproles.contains(&UserRole::Broadcaster) - { - idlock - .getspecialuserroles( - String::from(targetuser), - Some(ChType::Channel(targetchnl.to_lowercase())), - ) - .await - } else { - // Otherwise, don't get the target channel, return the current channel instead - idlock - .getspecialuserroles( - String::from(targetuser), - Some(ChType::Channel(msg.channel_login.to_lowercase())), - ) - .await - } - } - }; - - botlog::debug( - &format!("User roles of Target Chatter >> {:?}", sproles), - Some("identity.rs > init > getroles()".to_string()), - Some(&msg), - ); - - botlog::trace( - // &format!("Evaluating special roles"), - "Evaluating special roles", - Some("identity.rs > init > getroles()".to_string()), - Some(&msg), - ); - - let outmsg = if ((targetuser.to_lowercase() == msg.channel_login.to_lowercase()) - && arg2.is_none()) - || (arg2.is_some() && arg2.unwrap() == targetuser.to_lowercase()) - { - // First evaluates if they're broadcaster - - let mut outmsg = "FeelsWowMan they're the broadcaster. ".to_string(); - - if sproles.contains(&UserRole::Mod(ChType::Channel( - msg.channel_login.to_lowercase(), - ))) || sproles.contains(&UserRole::SupMod(ChType::Channel( - msg.channel_login.to_lowercase(), - ))) || sproles.contains(&UserRole::BotAdmin) - { - outmsg += format!("Target chatter's user roles are : {:?}", sproles).as_str(); - } - outmsg - } else if sproles.contains(&UserRole::Mod(ChType::Channel( - msg.channel_login.to_lowercase(), - ))) || sproles.contains(&UserRole::SupMod(ChType::Channel( - msg.channel_login.to_lowercase(), - ))) || sproles.contains(&UserRole::BotAdmin) - { - format!("Target chatter's user roles are : {:?}", sproles) - } else { - "Target chatter has no special roles LULE ".to_string() - }; - - botlog::debug( - format!("Chat Say Reply message : {}", outmsg).as_str(), - Some("identity.rs > init > getroles()".to_string()), - Some(&msg), - ); - - botlock.botmgrs.chat.say_in_reply_to(&msg, outmsg).await; - - // [ ] NOTE : After the above, I should receive only the roles in the context of the current channel I received this ideally and maybe BotAdmin ; not outside - } - + tempcomm.add_core_to_modmgr(Arc::clone(&mgr)).await; + botlog::trace( "End of Init MOdule add", Some("identity.rs > init ".to_string()), @@ -551,15 +124,572 @@ pub async fn init(mgr: Arc) { Log::flush(); } + + +async fn cmd_promote(params : ExecBodyParams) { + botlog::trace( + "Called cmd promote", + Some("identity.rs > cmd_prommote()".to_string()), + Some(¶ms.msg), + ); + + // -- If the BotCommand.command was called (e.g., promote) & required roles were validated OUTSIDE of this call + // , this is the current function body to execute + + /* + - `promote` / `demote` + - [ ] `SupMod` & `Broadcaster` & `BotAdmin` can run + - [ ] `UserRole`s that can run, can + - [ ] run `promote` on a regular `Chatter` to make them a `Mod` + - [ ] run `demote` on a `Mod` to make them a `Chatter` + - [ ] Only `BotAdmin` can : + - [ ] target themselves to `promote` / `demote` , in the case that they want to make themselves either a `Mod` or `SupMod` for the channel temporarily + - [ ] `promote admin ` to assign them `BotAdmin` role + - `[ ] Broadcaster` & `BotAdmin` can `demote` a `SupMod` to make them a `Mod` or `promote` the other way + */ + + /* + Usage : + + promote + promote -m + promote -mod + + demote + + promote -v + promote -vip + + promote -admin + + */ + + // println!("{}",params.msg.message_text); + botlog::trace( + format!("Twich Message > {}", params.msg.message_text).as_str(), + Some("identity.rs > cmd_promote()".to_string()), + None, + ); + + let sendername = params.msg.clone().sender.name; + + let mut argv = params.msg.message_text.split(' '); + + argv.next(); // Skip the command name + + let arg1 = argv.next(); + + let arg2 = argv.next(); + + let mut sender_badge: Option = None; + + for b in ¶ms.msg.badges { + if b.name == "moderator" { + sender_badge = Some(ChatBadge::Mod); + } else if b.name == "broadcaster" { + sender_badge = Some(ChatBadge::Broadcaster); + } else if b.name == "vip" { + sender_badge = Some(ChatBadge::VIP); + } + }; + + let targetchnl = params.msg.channel_login.to_lowercase(); + + /* + + [x] 1. Get trgusr (regardless of -admin flag) + [x] 2. promote trguser + [x] 3. Output resulting change + + */ + + + + let botlock = params.bot.read().await; + let id = botlock.get_identity(); + let idlock = id.read().await; + + + // [x] 1. Get trgusr (regardless of -admin flag) + + // let targetusr = if arg1 == Some("-admin") { arg2 } else { arg1 }; + let targetusr = if + arg1 == Some("-admin") + || arg1 == Some("-v") + || arg1 == Some("-vip") + || arg1 == Some("-m") + || arg1 == Some("-mod") + { arg2 } + else if let Some(a) = arg1 { + if a.starts_with('-') { + botlock.botmgrs.chat.send_botmsg( + super::chat::BotMsgType::Notif( + "Invalid Argument Flag".to_string() + ), + params.clone(), + ).await; + return + } else { arg1 } + } + else { arg1 }; + + // [x] 2. promote trguser + + // [x] Get a required lock first + let rslt = match targetusr { + Some(targetusr) => { + botlog::debug( + "running promote()", + Some("identity.rs > cmd_promote()".to_string()), + None, + ); + + Log::flush(); + + + // // Believe this is just using this as a Constaint depending on input + let target_role = + if arg1 == Some("-admin") { + Some(UserRole::BotAdmin) + } else if arg1 == Some("-vip") || arg1 == Some("-v") { + Some(UserRole::VIP(Channel(targetchnl.clone()))) + } else { + None // [x] Internal promote() logic automatically considers trg_role targetting -mod or -m + }; + + idlock + .promote( + sendername.clone(), + &sender_badge, + targetusr.to_string(), + Some(Channel(targetchnl.clone())), + target_role, + ) + .await + } + + None => { + botlog::debug( + "No Targer User argument", + Some("identity.rs > cmd_demote()".to_string()), + None, + ); + Log::flush(); + + ChangeResult::NoChange("No Targer User".to_string()) + } + }; + + // [x] 3. Output resulting change + + let outmsg = match rslt { + ChangeResult::Success(a) => { + format!("o7 Successfully promoted : {a}") + } + ChangeResult::Failed(a) => { + format!("PoroSad failed to promote : {a}") + } + ChangeResult::NoChange(a) => { + format!("uuh No Promotion Change : {a}") + } + }; + + botlog::debug( + outmsg.as_str(), + Some("identity.rs > cmd_prommote()".to_string()), + Some(¶ms.msg), + ); + + // We should call a notification around here + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + + botlog::trace( + "End of cmd_promote()", + Some("identity.rs > cmd_prommote()".to_string()), + None, + ); +} + + + +async fn cmd_demote(params : ExecBodyParams) { + botlog::debug( + "Called cmd demote", + Some("identity.rs > cmd_demote()".to_string()), + Some(¶ms.msg), + ); + Log::flush(); + + // -- If the BotCommand.command was called (e.g., demote) & required roles were validated OUTSIDE of this call + // , this is the current function body to execute + + /* + - `promote` / `demote` + - [ ] `SupMod` & `Broadcaster` & `BotAdmin` can run + - [ ] `UserRole`s that can run, can + - [ ] run `promote` on a regular `Chatter` to make them a `Mod` + - [ ] run `demote` on a `Mod` to make them a `Chatter` + - [ ] Only `BotAdmin` can : + - [ ] target themselves to `promote` / `demote` , in the case that they want to make themselves either a `Mod` or `SupMod` for the channel temporarily + - [ ] `promote admin ` to assign them `BotAdmin` role + - `[ ] Broadcaster` & `BotAdmin` can `demote` a `SupMod` to make them a `Mod` or `promote` the other way + */ + + /* + Usage : + + promote + + demote + + demote -m + demote -mod + + demote -v + demote -vip + + // promote -admin + + + + */ + + // [x] Unwraps arguments from message + + let (arg1, arg2) = { + let mut argv = params.msg.message_text.split(' '); + + argv.next(); // Skip the command name + + let arg1 = argv.next(); + + let arg2 = argv.next(); + + (arg1, arg2) + }; + + // --- + + /* + - [x] 1. Parse out the following + - Sender (e.g., Name & Badge) + - Target User (arg1) + - Target Channel (current channel) + - Msg or Msg.Message_Text (for later) + + - [x] 2. Run Demote() + - within demote(), getspecialuserroles() is called on both the sender and the target + - getspecialuserroles() only sends current db , while canuserrun() may change db depending on the most current state of the sender + - getspecialuserroles also borrows the sender's badge to evaluate + + - [x] 3. Take ChangeResult and output response + + */ + + /* + + - [x] 1. Parse out the following + - Sender (e.g., Name & Badge) + - Target User (arg1) + - Target Channel (current channel) + - (no need) Msg or Msg.Message_Text (for later) + + */ + + + + // [x] Get a required lock first + + let botlock = params.bot.read().await; + let id = botlock.get_identity(); + let idlock = id.read().await; + + + let sendername = params.msg.clone().sender.name; + + let mut sender_badge_mut: Option = None; + + for b in ¶ms.msg.badges { + if b.name == "moderator" { + sender_badge_mut = Some(ChatBadge::Mod); + } else if b.name == "broadcaster" { + sender_badge_mut = Some(ChatBadge::Broadcaster); + } else if b.name == "vip" { + sender_badge_mut = Some(ChatBadge::VIP); + } + }; + + let sender_badge = sender_badge_mut; + + + let targetchnl = params.msg.channel_login.to_lowercase(); + + + // let targetusr = arg1; + let targetusr = if + arg1 == Some("-v") + || arg1 == Some("-vip") + || arg1 == Some("-m") + || arg1 == Some("-mod") + { arg2 } + else if let Some(a) = arg1 { + if a.starts_with('-') { + botlock.botmgrs.chat.send_botmsg( + super::chat::BotMsgType::Notif( + "Invalid Argument Flag".to_string() + ), + params.clone(), + ).await; + return + } else { arg1 } + } + else { arg1 }; + + // Note : At the moment, no handling of -admin + let target_role = + if arg1 == Some("-vip") || arg1 == Some("-v") { + Some(UserRole::VIP(Channel(targetchnl.clone()))) + } else { + None // [x] Internal promote() logic automatically considers trg_role targetting -mod or -m + }; + + + + /* + + - [x] 2. Run Demote() + - within demote(), getspecialuserroles() is called on both the sender and the target + - getspecialuserroles() only sends current db , while canuserrun() may change db depending on the most current state of the sender + - getspecialuserroles also borrows the sender's badge to evaluate + + + */ + + let rslt = match targetusr { + Some(targetusr) => { + botlog::debug( + "running demote()", + Some("identity.rs > cmd_demote()".to_string()), + None, + ); + Log::flush(); + + idlock + .demote( + sendername.clone(), + &sender_badge, + targetusr.to_string(), + Some(Channel(targetchnl.clone())), + target_role, + ) + .await + } + + None => { + botlog::debug( + "No Targer User argument", + Some("identity.rs > cmd_demote()".to_string()), + None, + ); + Log::flush(); + + ChangeResult::NoChange("No Targer User".to_string()) + } + }; + + /* + + - [x] 3. Take ChangeResult and output response + + */ + + let outmsg = match rslt { + ChangeResult::Success(a) => { + format!("o7 Successfully demoted : {a}") + } + ChangeResult::Failed(a) => { + format!("PoroSad failed to demote : {a}") + } + ChangeResult::NoChange(a) => { + format!("uuh No Demotion Change : {a}") + } + }; + + botlog::debug( + outmsg.as_str(), + Some("identity.rs > cmd_demote()".to_string()), + Some(¶ms.msg), + ); + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + +} + + + +async fn getroles(params : ExecBodyParams) { + botlog::debug( + "Called cmd getroles", + Some("identity.rs > cmd_getroles()".to_string()), + Some(¶ms.msg), + ); + + /* + Usage + + getroles + - If channel is provided, provide roles for that channel specifically + + */ + + + let mut argv = params.msg.message_text.split(' '); + + argv.next(); // Skip the command name + + let arg1 = argv.next(); + + let targetuser = match arg1 { + None => return, // exit if no arguments + Some(arg) => arg, + }; + + let arg2 = argv.next(); + + let targetchnl = arg2; + + let botlock = params.bot.read().await; + + let id = botlock.get_identity(); + + let idlock = id.read().await; + + let sproles = match targetchnl { + None => { + // [ ] If targetchnl is not provided, default to pulling the current channel + idlock + .getspecialuserroles( + String::from(targetuser), + Some(Channel(params.msg.channel_login.to_lowercase())), + ) + .await + } + Some(targetchnl) => { + // [x] gets special roles for caller + let callersproles = idlock + .getspecialuserroles( + params.msg.sender.name.to_lowercase(), + Some(Channel(targetchnl.to_lowercase().to_string())), + ) + .await; + + // Below appears to be validating if getroles() should run based on caller's specific roles + // - No Need to add VIP here + if callersproles.contains(&UserRole::Mod(Channel( + targetchnl.to_lowercase().to_string(), + ))) || callersproles.contains(&UserRole::SupMod(Channel( + targetchnl.to_lowercase().to_string(), + ))) || callersproles.contains(&UserRole::Broadcaster) + { + idlock + .getspecialuserroles( + String::from(targetuser), + Some(Channel(targetchnl.to_lowercase())), + ) + .await + } else { + // Otherwise, don't get the target channel, return the current channel instead + idlock + .getspecialuserroles( + String::from(targetuser), + Some(Channel(params.msg.channel_login.to_lowercase())), + ) + .await + } + } + }; + + botlog::debug( + &format!("User roles of Target Chatter >> {:?}", sproles), + Some("identity.rs > init > getroles()".to_string()), + Some(¶ms.msg), + ); + + botlog::trace( + "Evaluating special roles", + Some("identity.rs > init > getroles()".to_string()), + Some(¶ms.msg), + ); + + let outmsg = if ((targetuser.to_lowercase() == params.msg.channel_login.to_lowercase()) + && arg2.is_none()) + || (arg2.is_some() && arg2.unwrap() == targetuser.to_lowercase()) + { + // First evaluates if they're broadcaster + + let mut outmsg = "FeelsWowMan they're the broadcaster. ".to_string(); + + // Below appears to be validating if getroles() should run based on caller's specific roles + // - No Need to add VIP here + if sproles.contains(&UserRole::Mod(Channel( + params.msg.channel_login.to_lowercase(), + ))) || sproles.contains(&UserRole::SupMod(Channel( + params.msg.channel_login.to_lowercase(), + ))) || sproles.contains(&UserRole::BotAdmin) + { + outmsg += format!("Target chatter's user roles are : {:?}", sproles).as_str(); + } + outmsg + } else if sproles.contains(&UserRole::Mod(Channel( + params.msg.channel_login.to_lowercase(), + ))) || sproles.contains(&UserRole::SupMod(Channel( + params.msg.channel_login.to_lowercase(), + ))) || sproles.contains(&UserRole::BotAdmin) + { + format!("Target chatter's user roles are : {:?}", sproles) + } else { + "Target chatter has no special roles LULE ".to_string() + }; + + botlog::debug( + format!("Chat Say Reply message : {}", outmsg).as_str(), + Some("identity.rs > init > getroles()".to_string()), + Some(¶ms.msg), + ); + + botlock.botmgrs.chat.send_botmsg(super::chat::BotMsgType::Notif( + outmsg.to_string() + ), + params.clone(), + ).await; + + + // [ ] NOTE : After the above, I should receive only the roles in the context of the current channel I received this ideally and maybe BotAdmin ; not outside +} + + + #[derive(Debug, PartialEq, Eq, Clone)] pub enum UserRole { Chatter, - Mod(ChType), // String specifies Channel - SupMod(ChType), // String specifies Channel + Mod(Channel), + SupMod(Channel), + VIP(Channel), Broadcaster, BotAdmin, } - +#[derive(Debug, PartialEq, Eq)] pub enum Permissible { Allow, Block, @@ -578,17 +708,11 @@ pub struct IdentityManager { > */ -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ChatBadge { Broadcaster, Mod, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ChangeResult { - Success(String), - Failed(String), - NoChange(String), + VIP, } impl IdentityManager { @@ -606,7 +730,8 @@ impl IdentityManager { } } - async fn add_role(&self, trgchatter: String, trg_role: UserRole) { + // => 03.22 - Force - Made public because botmodules unit tests + pub async fn add_role(&self, trgchatter: String, trg_role: UserRole) { let mut srulock = self.special_roles_users.write().await; let mut usrrolelock = srulock .get_mut(&trgchatter) @@ -628,7 +753,8 @@ impl IdentityManager { } } - async fn affirm_chatter_in_db(&self, trgchatter: String) { + // => 03.22 - Force - Made public because botmodules unit tests + pub async fn affirm_chatter_in_db(&self, trgchatter: String) { let mut srulock = self.special_roles_users.write().await; srulock .entry(trgchatter.clone()) @@ -656,7 +782,6 @@ impl IdentityManager { botlog::trace( "Checking within PRVMSG", Some("identity.rs > can_user_run_PRVMSG()".to_string()), - // Some(&msg), Some(msg), ); @@ -667,79 +792,28 @@ impl IdentityManager { sender_badge = Some(ChatBadge::Mod); } else if b.name == "broadcaster" { sender_badge = Some(ChatBadge::Broadcaster); + } else if b.name == "vip" { + sender_badge = Some(ChatBadge::VIP); } - } - - // if &msg.badges.contains(Badge{}) { - - // } - - // if let Some(sender_badge) = sender_badge { - // match sender_badge { - // Some(sender_badge) => { - // return &self.can_user_run(msg.sender.name.to_owned(), - // ChType::Channel(msg.channel_login.to_owned()), - // sender_badge, - // cmdreqroles - // return self.can_user_run(msg.sender.name.to_owned(), - // let a = Arc::new(Mutex::new(self)); - // let mut a = a.lock().await; - // let a = **a; - // let a = a.can_user_run(msg.sender.name.to_owned(), - // ChType::Channel(msg.channel_login.to_owned()), - // sender_badge, - // cmdreqroles - // ) ; - // let a = *self; - // let a = Arc::new(Mutex::new(a)); - // let a = a.lock().await.can_user_run(msg.sender.name.to_owned(), - // ChType::Channel(msg.channel_login.to_owned()), - // sender_badge, - // cmdreqroles - // ) ; - // return a; - // return self.can_user_run(msg.sender.name.to_owned(), - // ChType::Channel(msg.channel_login.to_owned()), - // sender_badge, - // cmdreqroles - // ).await - - // * NOTE : We're preferring to pass the ChangeResult up , where we have access to Chat via BotInstance - // that have more strained chatting rules - // let evalpermissible = self.can_user_run(msg.sender.name.to_owned(), - // ChType::Channel(msg.channel_login.to_owned()), - // sender_badge, - // cmdreqroles - // ).await ; - // evalpermissible - // // } - // None => { - - // } - // here , sender_badge is likely None - // This could be a regular chatter, BotAdmin,SupserMod - - // [ ] Call can_user_run() - // (self,Permissible::Block) - // (Permissible::Block,ChangeResult::NoChange("".to_string())) + }; self.can_user_run( msg.sender.name.to_owned(), - ChType::Channel(msg.channel_login.to_owned()), + Channel(msg.channel_login.to_owned()), sender_badge, cmdreqroles, ) .await + } pub async fn can_user_run( &mut self, usr: String, - channelname: ChType, + channelname: Channel, chat_badge: Option, cmdreqroles: Vec, // ) -> Result> { ) -> (Permissible, ChangeResult) { - // println!{"Checking within can_user_run()"}; botlog::debug( &format!( "Checking within can_user_run() : @@ -786,29 +860,45 @@ impl IdentityManager { // [x] If cmdreqroles is empty vector , automatically assume Ok(Permissible::Allow) - // let idar = Arc::new(RwLock::new(self)); - let usr = usr.to_lowercase(); - // if cmdreqroles.len() == 0 { + + let bot_vector = otherbots_vector() ; // result of pulling from Cargo.toml + + botlog::trace( + &format!( + "Checking user is part of known bots: bot_vector.contains(&usr) : {:?}",bot_vector.contains(&usr) + ), + Some("identity.rs > can_user_run()".to_string()), + None, + ); + + if bot_vector.contains(&usr) { + return ( + Permissible::Block, + ChangeResult::NoChange("Other Bots Cannot Run Commands".to_string()), + ); + } + if cmdreqroles.is_empty() { - // return Ok(Permissible::Allow) return ( Permissible::Allow, ChangeResult::NoChange("Command has no required cmdreqroles".to_string()), ); } - let mut modrolechange = ChangeResult::NoChange("".to_string()); + let mut rolechange = ChangeResult::NoChange("".to_string()); match chat_badge { - // [x] If chatBadge::Broadcaster ... - // [x] and cmdreqroles includes UserRole::Broadcaster , Ok(Permissible::Allow) - // [x] and cmdreqroles includes UserRole::Mod("") OR UserRole::SupMod("") , Ok(Permissible::Allow) + + // If ChatBadge::Broadcaster is observed, + // Check if cmdreqroles contains Channel Level Roles . Broadcaster should have Permissible::Allow for any of these + Some(ChatBadge::Broadcaster) => { if cmdreqroles.contains(&UserRole::Broadcaster) - || cmdreqroles.contains(&UserRole::Mod(ChType::Channel(String::new()))) - || cmdreqroles.contains(&UserRole::SupMod(ChType::Channel(String::new()))) + || cmdreqroles.contains(&UserRole::Mod(OF_CMD_CHANNEL)) + || cmdreqroles.contains(&UserRole::SupMod(OF_CMD_CHANNEL)) + || cmdreqroles.contains(&UserRole::VIP(OF_CMD_CHANNEL)) { // return Ok(Permissible::Allow) return ( @@ -862,7 +952,48 @@ impl IdentityManager { usrroles_lock.push(UserRole::Mod(channelname.clone())); - modrolechange = ChangeResult::Success("Auto Promoted Mod".to_string()); + rolechange = ChangeResult::Success("Auto Promoted Mod".to_string()); + } + } + } + Some(ChatBadge::VIP) => { + botlog::info( + "VIP Chatbadge detected", + Some("identity.rs > can_user_run()".to_string()), + None, + ); + + let rolesdb = Arc::clone(&self.special_roles_users); + + self.affirm_chatter_in_db(usr.clone()).await; + + let rolesdb_lock = rolesdb.write().await; + + match (*rolesdb_lock).get(&usr.to_lowercase()) { + Some(usrroles) + if usrroles + .read() + .await + .contains(&UserRole::VIP(channelname.clone())) => + { + // Do nothing when theh have a VIP badge and have VIP badge for the channel + botlog::trace( + "Already a VIP in roles", + Some("identity.rs > can_user_run()".to_string()), + None, + ); + } + + _ => { + // In the event they have a VIP badge , are running a bot command, but don't have a channel mod role yet... + + let mut rolesdb_lock_mut = rolesdb_lock; + let usrroles = rolesdb_lock_mut.get_mut(&usr.to_lowercase()).unwrap(); + let mut usrroles_lock = usrroles.write().await; + + usrroles_lock.push(UserRole::VIP(channelname.clone())); + + rolechange = ChangeResult::Success("Auto Promoted VIP".to_string()); } } } @@ -877,7 +1008,7 @@ impl IdentityManager { None, ); - if cmdreqroles.contains(&UserRole::Mod(ChType::Channel(String::new()))) { + if cmdreqroles.contains(&UserRole::Mod(OF_CMD_CHANNEL)) { botlog::trace( "Command requires Mod Role", Some("identity.rs > can_user_run()".to_string()), @@ -906,14 +1037,14 @@ impl IdentityManager { Some("identity.rs > can_user_run()".to_string()), None, ); - return (Permissible::Allow, modrolechange); + return (Permissible::Allow, rolechange); } } } // [x] If cmdreqroles includes UserRole::SupMod("") , checks if chatter has UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow) - if cmdreqroles.contains(&UserRole::SupMod(ChType::Channel(String::new()))) { + if cmdreqroles.contains(&UserRole::SupMod(OF_CMD_CHANNEL)) { if let Some(a) = self .special_roles_users .read() @@ -924,7 +1055,7 @@ impl IdentityManager { .await .contains(&UserRole::SupMod(channelname.clone())) { - return (Permissible::Allow, modrolechange); + return (Permissible::Allow, rolechange); } } } @@ -971,11 +1102,47 @@ impl IdentityManager { ); if a.read().await.contains(&UserRole::BotAdmin) { - return (Permissible::Allow, modrolechange); + return (Permissible::Allow, rolechange); } } } + // [x] If cmdreqroles includes UserRole::VIP and chatter has UserRole::VIP , Ok(Permissible::Allow) + + if cmdreqroles.contains(&UserRole::VIP(OF_CMD_CHANNEL)) { + + botlog::trace( + "Command requires VIP Role", + Some("identity.rs > can_user_run()".to_string()), + None, + ); + + if let Some(a) = self + .special_roles_users + .read() + .await + .get(&usr.to_lowercase()) + { + botlog::trace( + "Special roles found for user", + Some("identity.rs > can_user_run()".to_string()), + None, + ); + + if a.read().await.contains(&UserRole::VIP(channelname.clone())) + { + botlog::trace( + "> Special Role Identified : VIP ", + Some("identity.rs > can_user_run()".to_string()), + None, + ); + return (Permissible::Allow, rolechange); + } + } + + } + + ( Permissible::Block, ChangeResult::NoChange("Not any permissiable condition".to_string()), @@ -987,7 +1154,7 @@ impl IdentityManager { authorizer: String, authorizer_badge: &Option, trgchatter: String, - channel: Option, + channel: Option, trg_role: Option, ) -> ChangeResult { botlog::trace( @@ -1000,20 +1167,27 @@ impl IdentityManager { Log::flush(); /* + + + // [x] => 03.25 - Q. Would there need to be extra handling here for VIP? + + [x] 1. Check if Authorizer Mod Badge then Auto Promote to Mod if not Mod [x] 2. Get Authorizer & Target Chatter Roles with a Given Channel [x] 3. If the authorizer & Target Chatters are the same, and the Authorizer is not a Admin, return no change [x] 4a. If Authorizer is BotAdmin & trg_role is Some(BotAdmin) , set Target as BotAdmin and return - [x] 4b. If target is Broadcaster, return NoChange - [ ] 4c. If Authorizer is a SupMod,Broadcaster,BotAdmin , can Promote Target Chatter > Mod + [x] 4b. If Authorizer is a Mod,SupMod,Broadcaster & trg_role is Some(VIP(channel)), can Promote a Target Chatter > VIP + [x] 4c. If target is Broadcaster, return NoChange + [x] 4d. If Authorizer is a SupMod,Broadcaster,BotAdmin , can Promote Target Chatter > Mod - NOTE : We do not validate trg_role here - app logic requires you to promote 1 to Mod and 1 more to SupMod - [ ] 4d. If Authorizer is a Broadcaster,BotAdmin , can Promote a Target Mod > SupMod + [x] 4e. If Authorizer is a Broadcaster,BotAdmin , can Promote a Target Mod > SupMod - NOTE : We do not validate trg_role here - app logic requires you to promote 1 to Mod and 1 more to SupMod + */ - // [x] 1. Check if Authorizer Mod Badge then Auto Promote to Mod if not Mod + // [x] 1. Check if Authorizer Mod or VIP Badge then Auto Promote to matching UserRole if not already assigned let trgchatter = trgchatter.to_lowercase(); @@ -1035,10 +1209,23 @@ impl IdentityManager { .await; } + + // [x] - May want to Auto VIP Authorizer here + Some(ChatBadge::VIP) + if (!authusrroles.contains(&UserRole::VIP(channel.clone()))) => + { + authusrroles.push(UserRole::VIP(channel.clone())); + + self.affirm_chatter_in_db(authorizer.clone()).await; + self.add_role(authorizer.clone(), UserRole::VIP(channel.clone())) + .await; + } + _ => (), } } + // [x] 2. Get Authorizer & Target Chatter Roles let trgusrroles = self @@ -1056,7 +1243,6 @@ impl IdentityManager { (authusrroles, trgusrroles) }; - // [x] 3. If the authorizer & Target Chatters are the same, and the Authorizer is not a Admin, return no change if trgchatter == authorizer && !authusrroles.contains(&UserRole::BotAdmin) { return ChangeResult::NoChange("Can't target yourself".to_string()); @@ -1075,20 +1261,59 @@ impl IdentityManager { } } - // [x] 4b. If target is Broadcaster, return NoChange + botlog::debug( + format!("VIP Evaluation : Channel = {:?} ; trg_role = {:?} ", + channel.clone(),trg_role + ).as_str(), + Some("identity.rs > promote()".to_string()), + None, + ); + + // [x] 4b. If Authorizer is a Mod,SupMod,Broadcaster & trg_role is Some(VIP(channel)), can Promote a Target Chatter > VIP + if let Some(trg_chnl) = channel.clone() { + if trg_role == Some(UserRole::VIP(trg_chnl.clone())) + && ( authusrroles.contains(&UserRole::Mod(trg_chnl.clone())) + || authusrroles.contains(&UserRole::SupMod(trg_chnl.clone())) + || authusrroles.contains(&UserRole::Broadcaster) + ) + { + if trgusrroles.contains(&UserRole::VIP(trg_chnl.clone())) { + return ChangeResult::NoChange("Already has the role".to_string()); + } + else { + self.affirm_chatter_in_db(trgchatter.clone()).await; + + self.add_role(trgchatter.clone(), UserRole::VIP(trg_chnl.clone())).await; + + return ChangeResult::Success("Promotion Successful".to_string()); + } + } else if trg_role == Some(UserRole::VIP(trg_chnl.clone())) + { + return ChangeResult::Failed(String::from("You're not permitted to do that")); + } + } + + + + + + // [x] 4c. If target is Broadcaster, return NoChange if trgusrroles.contains(&UserRole::Broadcaster) { return ChangeResult::NoChange("Can't target broadcaster".to_string()); } + /* - [ ] 4c. If Authorizer is a SupMod,Broadcaster,BotAdmin , can Promote Target Chatter > Mod + [x] 4d. If Authorizer is a SupMod,Broadcaster,BotAdmin , can Promote Target Chatter > Mod - NOTE : We do not validate trg_role here - app logic requires you to promote 1 to Mod and 1 more to SupMod - [ ] 4d. If Authorizer is a Broadcaster,BotAdmin , can Promote a Target Mod > SupMod + [x] 4e. If Authorizer is a Broadcaster,BotAdmin , can Promote a Target Mod > SupMod - NOTE : We do not validate trg_role here - app logic requires you to promote 1 to Mod and 1 more to SupMod */ if let Some(trg_chnl) = channel.clone() { + + // 1. Checks first if Target User's Roles do not Include Broadcaster,Mod,SupMod for the Channel if !trgusrroles.contains(&UserRole::Broadcaster) && !trgusrroles.contains(&UserRole::Mod(trg_chnl.clone())) && !trgusrroles.contains(&UserRole::SupMod(trg_chnl.clone())) @@ -1097,6 +1322,8 @@ impl IdentityManager { // target's Next Role would be Mod // Authorizer must be SupMod,Broadcaster,BotAdmin // > Promote target to Mod + + // 2. If Authorizer has Elevated Admin Roles for the Channel (SupMod,Broadcaster,BotAdmin) > set target to MOD if authusrroles.contains(&UserRole::SupMod(trg_chnl.clone())) || authusrroles.contains(&UserRole::Broadcaster) || authusrroles.contains(&UserRole::BotAdmin) @@ -1158,6 +1385,8 @@ impl IdentityManager { } }; + + botlog::warn( "Runtime reached undeveloped code", Some("identity.rs > promote()".to_string()), @@ -1171,7 +1400,8 @@ impl IdentityManager { authorizer: String, authorizer_badge: &Option, trgchatter: String, - channel: Option, + channel: Option, + trg_role: Option, ) -> ChangeResult { botlog::trace(&format!("IN VARS for demote() : Authorizer : {:?} ; Target Chatter : {} ; Target Channel : {:?}", authorizer,trgchatter,channel), Some("identity.rs > demote()".to_string()), None); @@ -1185,7 +1415,7 @@ impl IdentityManager { Use the roles of the above to determine whether the authorizer can demote the target user or not */ - // [x] 1. If Authorizer's Badge is Mod, ensuring Sender is in DB as Mod(Channel) + // [x] 1. If Authorizer's Badge is Mod/VIP, ensuring Sender is in DB as Mod(Channel) let trgchatter = trgchatter.to_lowercase(); @@ -1208,6 +1438,17 @@ impl IdentityManager { self.add_role(authorizer.clone(), UserRole::Mod(channel.clone())) .await; } + // [x] - May want to Auto VIP Authorizer here + Some(ChatBadge::VIP) + if (!authusrroles.contains(&UserRole::VIP(channel.clone()))) => + { + authusrroles.push(UserRole::VIP(channel.clone())); + + self.affirm_chatter_in_db(authorizer.clone()).await; + self.add_role(authorizer.clone(), UserRole::VIP(channel.clone())) + .await; + } + _ => (), } } @@ -1223,7 +1464,29 @@ impl IdentityManager { return ChangeResult::NoChange("Can't target yourself".to_string()); } - // [x] 4a. Authorizers who are BotAdmin, Broadcaster or Supermod can demote a Mod + // [x] 4. If Authorizer is a Mod,SupMod,Broadcaster & trg_role is Some(VIP(channel)), can Promote a Target Chatter > VIP + + if ( authusrroles.contains(&UserRole::Mod(channel.clone())) + || authusrroles.contains(&UserRole::SupMod(channel.clone())) + || authusrroles.contains(&UserRole::Broadcaster) + ) + && trg_role == Some(UserRole::VIP(channel.clone())) { + if !trgusrroles.contains(&UserRole::VIP(channel.clone())) { + return ChangeResult::NoChange("Already does not have VIP role".to_string()); + } + else { + + self.remove_role(trgchatter.clone(), UserRole::VIP(channel.clone())).await; + + return ChangeResult::Success("Demotion Successful".to_string()); + } + } + + + // [x] 5. - Mod/SupMod Logic + + + // [x] 5a. Authorizers who are BotAdmin, Broadcaster or Supermod can demote a Mod if (authusrroles.contains(&UserRole::BotAdmin) || authusrroles.contains(&UserRole::Broadcaster) @@ -1234,7 +1497,7 @@ impl IdentityManager { .await; return ChangeResult::Success("Demoted successfully".to_string()); } - // [x] 4b. Authorizers who are BotAdmin, Broadcaster can demote a SupMod + // [x] 5b. Authorizers who are BotAdmin, Broadcaster can demote a SupMod else if (authusrroles.contains(&UserRole::BotAdmin) || authusrroles.contains(&UserRole::Broadcaster)) && trgusrroles.contains(&UserRole::SupMod(channel.clone())) @@ -1245,7 +1508,7 @@ impl IdentityManager { .await; return ChangeResult::Success("Demoted successfully".to_string()); } - // [x] 4c. When Target chatter isnt a Mod or SupMod to demote + // [x] 5c. When Target chatter isnt a Mod or SupMod to demote else if !trgusrroles.contains(&UserRole::Mod(channel.clone())) && !trgusrroles.contains(&UserRole::SupMod(channel.clone())) { @@ -1253,7 +1516,7 @@ impl IdentityManager { "Target chatter does not have a role that can be demoted".to_string(), ); } - // [x] 4d. When they're only a Mod + // [x] 5d. When they're only a Mod else if authusrroles.contains(&UserRole::Mod(channel.clone())) { return ChangeResult::Failed("You're not permitted to do that".to_string()); } @@ -1268,8 +1531,13 @@ impl IdentityManager { pub async fn getspecialuserroles( &self, chattername: String, - channel: Option, + channel: Option, ) -> Vec { + /* + NOTE : Any NEW or CHANGES to UserRole type should have additional handling here + Specifically for Channel Elevated Roles + */ + /* Note : Ideally this be called for a given chatter name ? */ @@ -1291,22 +1559,20 @@ impl IdentityManager { // Checks if broadcaster let channel_out = match channel { - Some(channel_tmp) => { - match channel_tmp { - ChType::Channel(channel_tmp) => { - // In this block, Some input channel is given - // We're comparing the channel name with chattername to determine if they're a broadcaster - if chattername == channel_tmp.to_lowercase() { - evalsproles.push(UserRole::Broadcaster); - } - - Some(ChType::Channel(channel_tmp)) - } // _ => () + Some(chnl) => { + // In this block, Some input channel is given + // We're comparing the channel name with chattername to determine if they're a broadcaster + // if chattername == chnl.0 + if chattername == chnl.0.to_lowercase() + { + evalsproles.push(UserRole::Broadcaster); } - } + Some(chnl) + }, None => None, }; + let rolesdb = Arc::clone(&self.special_roles_users); let rolesdb_lock = rolesdb.read().await; @@ -1342,6 +1608,9 @@ impl IdentityManager { if a.read().await.contains(&UserRole::SupMod(channel.clone())) { evalsproles.push(UserRole::SupMod(channel.clone())); } + if a.read().await.contains(&UserRole::VIP(channel.clone())) { + evalsproles.push(UserRole::VIP(channel.clone())); + } // else {}; } None => { @@ -1387,11 +1656,40 @@ mod core_identity { fn user_role_identity() { Log::set_file_ext(Extension::Log); assert_eq!( - UserRole::SupMod(ChType::Channel("strong".to_string())), - UserRole::SupMod(ChType::Channel("Strong".to_lowercase())) + UserRole::SupMod(Channel("strong".to_string())), + UserRole::SupMod(Channel("Strong".to_lowercase())) ); } + + #[tokio::test] + async fn otherbots_checks() { + Log::set_file_ext(Extension::Log); + + let mut test_id_mgr = IdentityManager::init(); + + for bot in otherbots_vector() { + + let (usr, channelname, chat_badge, cmdreqroles) = ( + bot, + // Channel::construct("twitchchanneltest".to_string()), + Channel("twitchchanneltest".to_string()), + None, + vec![] + ); + + let rslt = test_id_mgr.can_user_run(usr, channelname, chat_badge, cmdreqroles).await; + + assert_eq!( + (Permissible::Block, + ChangeResult::NoChange("Other Bots Cannot Run Commands".to_string())), + rslt + ); + + } + + } + #[tokio::test] async fn promote_workflow_01() { Log::set_file_ext(Extension::Log); @@ -1400,7 +1698,8 @@ mod core_identity { let test_id_mgr = IdentityManager::init(); // [x] Mod Attempts to Promote User - let channel = Some(ChType::Channel("twitchchanneltest".to_string())); + // let channel = Some(Channel::construct("twitchchanneltest".to_string())); + let channel = Some(Channel("twitchchanneltest".to_string())); let trgchatter = "regularChatter".to_string(); let authorizer_badge = &Some(ChatBadge::Mod); let authorizer = "chatMod".to_string(); @@ -1430,7 +1729,7 @@ mod core_identity { let test_id_mgr = IdentityManager::init(); // [x] Broadcaster Promotes Chatter to SupMod - let channel = Some(ChType::Channel("broadcasterer".to_string())); + let channel = Some(Channel("broadcasterer".to_string())); let trgchatter = "regularChatter".to_string(); let authorizer_badge = &Some(ChatBadge::Broadcaster); let authorizer = "broadcasterer".to_string(); @@ -1455,7 +1754,7 @@ mod core_identity { .getspecialuserroles(trgchatter.clone(), channel.clone()) .await; - assert!(rslt.contains(&UserRole::Mod(ChType::Channel("broadcasterer".to_string())))); + assert!(rslt.contains(&UserRole::Mod(Channel("broadcasterer".to_string())))); let rslt = test_id_mgr .promote( @@ -1476,7 +1775,7 @@ mod core_identity { .getspecialuserroles(trgchatter.clone(), channel.clone()) .await; - assert!(rslt.contains(&UserRole::SupMod(ChType::Channel( + assert!(rslt.contains(&UserRole::SupMod(Channel( "broadcasterer".to_string() )))); @@ -1509,8 +1808,7 @@ mod core_identity { let broadcaster = "broadcasterer".to_string(); let broadcaster_badge = &Some(ChatBadge::Broadcaster); - // let channel = Some(ChType::Channel(broadcaster.clone())); - let channel = ChType::Channel(broadcaster.clone()); + let channel = Channel(broadcaster.clone()); let supchatter = "superModerator".to_string(); let trg_role = None; @@ -1552,10 +1850,9 @@ mod core_identity { // [x] SupMod Attempts to Promote Chatter to SupMod - // let broadcaster = "broadcasterer".to_string(); let authorizer = supchatter; let authorizer_badge = &Some(ChatBadge::Broadcaster); - let channel = Some(ChType::Channel(broadcaster.clone())); + let channel = Some(Channel(broadcaster.clone())); let trgchatter = "regularChatter".to_string(); let trg_role = None; @@ -1623,10 +1920,9 @@ mod core_identity { // [x] SupMod Attempts to Promote Chatter to SupMod - // let broadcaster = "broadcasterer".to_string(); let authorizer = botadmin; let authorizer_badge = botadmin_badge; - let channel = Some(ChType::Channel("somechannel".to_string())); + let channel = Some(Channel("somechannel".to_string())); let trgchatter = "regularChatter".to_string(); let trg_role = None; @@ -1701,7 +1997,7 @@ mod core_identity { let supmod = "supmoder".to_string(); - let channel = Some(ChType::Channel("somechannel".to_string())); + let channel = Some(Channel("somechannel".to_string())); test_id_mgr.affirm_chatter_in_db(supmod.clone()).await; test_id_mgr @@ -1734,6 +2030,7 @@ mod core_identity { let authorizer = regmod.clone(); let authorizer_badge = &None; let trgchatter = supmod.clone(); + let trg_role = None; let rslt = test_id_mgr .demote( @@ -1741,6 +2038,7 @@ mod core_identity { authorizer_badge, trgchatter.clone(), channel.clone(), + trg_role.clone(), ) .await; @@ -1761,6 +2059,7 @@ mod core_identity { authorizer_badge, trgchatter.clone(), channel.clone(), + trg_role.clone(), ) .await; @@ -1775,6 +2074,7 @@ mod core_identity { authorizer_badge, trgchatter.clone(), channel.clone(), + trg_role.clone(), ) .await; @@ -1785,4 +2085,213 @@ mod core_identity { ) ); } + + + + #[tokio::test] + async fn vip_workflow_01() { + Log::set_file_ext(Extension::Log); + //Log::set_level(Level::Trace); + + // Channel Elevated User Promotes/Demotes VIP + + let test_id_mgr = IdentityManager::init(); + + + + // [x] 1. Requester has a Mod Role + + let channel = Some(Channel("somechannel".to_string())); + let authorizer_badge = &Some(ChatBadge::Mod); + let authorizer = "chatMod".to_string(); + let trgchatter = "regularChatter".to_string(); + let trg_role = Some(UserRole::VIP(channel.clone().unwrap())); + + let authorizer = authorizer.to_lowercase(); + let trgchatter = trgchatter.to_lowercase(); + + test_id_mgr.affirm_chatter_in_db(authorizer.clone()).await; + test_id_mgr.affirm_chatter_in_db(trgchatter.clone()).await; + + test_id_mgr + .add_role(authorizer.clone(), UserRole::Mod(channel.clone().unwrap())) + .await; + + let rslt = test_id_mgr + .getspecialuserroles(authorizer.clone(), channel.clone()) + .await; + + assert_eq!(rslt,vec![UserRole::Mod(channel.clone().unwrap())]); + + // [x] 2. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(trgchatter.clone(), channel.clone()) + .await; + + assert_eq!(rslt,vec![]); + + // [x] 3. Requester Promotes a Target Chatter to VIP + + let rslt = test_id_mgr + .promote( + authorizer.clone(), + authorizer_badge, + trgchatter.clone(), + channel.clone(), + trg_role.clone(), + ) + .await; + + assert_eq!( + rslt, + ChangeResult::Success("Promotion Successful".to_string()) + ); + + + // [x] 4. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(trgchatter.clone(), channel.clone()) + .await; + + assert!(rslt.contains(&UserRole::VIP(channel.clone().unwrap()))); + + // [x] 5. Requester Promotes a Target Chatter to VIP + + let rslt = test_id_mgr + .promote( + authorizer.clone(), + authorizer_badge, + trgchatter.clone(), + channel.clone(), + trg_role.clone(), + ) + .await; + + assert_eq!( + rslt, + ChangeResult::NoChange("Already has the role".to_string()) + ); + + + // [x] 6. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(trgchatter.clone(), channel.clone()) + .await; + + assert!(rslt.contains(&UserRole::VIP(channel.clone().unwrap()))); + + + // [x] 7. Requester Demotes a Target Chatter from VIP + + let rslt = test_id_mgr + .demote( + authorizer.clone(), + authorizer_badge, + trgchatter.clone(), + channel.clone(), + trg_role.clone(), + ) + .await; + + assert_eq!( + rslt, + ChangeResult::Success("Demotion Successful".to_string()) + ); + + // [x] 8. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(trgchatter.clone(), channel.clone()) + .await; + + // assert!(rslt.contains(&UserRole::VIP(channel.clone().unwrap()))); + assert_eq!(rslt,vec![]); + + + + // [x] 9. Requester Demotes a Target Chatter from VIP + + let rslt = test_id_mgr + .demote( + authorizer.clone(), + authorizer_badge, + trgchatter.clone(), + channel.clone(), + trg_role.clone(), + ) + .await; + + assert_eq!( + rslt, + ChangeResult::NoChange("Already does not have VIP role".to_string()) + ); + + // [x] 10. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(trgchatter.clone(), channel.clone()) + .await; + + assert_eq!(rslt,vec![]); + + + + + + } + + + #[tokio::test] + async fn auto_vip_workflow() { + Log::set_file_ext(Extension::Log); + + let mut test_id_mgr = IdentityManager::init(); + + + + // let channel = Some(Channel("somechannel".to_string())); + let channel = Channel("somechannel".to_string()); + let authorizer_badge = Some(ChatBadge::VIP); + let authorizer = "chatMod".to_string(); + + let authorizer = authorizer.to_lowercase(); + + + // [x] 1. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(authorizer.clone(), Some(channel.clone())) + .await; + + assert_eq!(rslt,vec![]); + + // [x] 2. Run canuserrun() for the Requester . (This is ran after BotCommands are ran) + + let rslt = test_id_mgr + .can_user_run( + authorizer.clone(), + channel.clone(), + authorizer_badge, + vec![ + UserRole::VIP(OF_CMD_CHANNEL), + ] + ).await; + + assert_eq!(rslt, + (Permissible::Allow,ChangeResult::Success("Auto Promoted VIP".to_string()))); + + // [x] 3. assert getspecialuserroles for Target Chatter + + let rslt = test_id_mgr + .getspecialuserroles(authorizer.clone(), Some(channel.clone())) + .await; + + assert_eq!(rslt,vec![UserRole::VIP(channel)]); + + + + } } diff --git a/src/core/ratelimiter.rs b/src/core/ratelimiter.rs index b43abba..4de77bc 100644 --- a/src/core/ratelimiter.rs +++ b/src/core/ratelimiter.rs @@ -1,18 +1,23 @@ const TIME_THRESHOLD_S: u64 = 30; +const TIME_MIN_S_F64: f64 = 1.0; const MSG_THRESHOLD: u32 = 20; use std::time::Instant; +use crate::core::botlog; #[derive(Debug, Clone)] pub struct RateLimiter { timer: Instant, msgcounter: u32, + lastmsgtimer : Instant, } +#[derive(Debug)] pub enum LimiterResp { Allow, // when it's evaluated to be within limits Skip, // as outside of rate limits // Enqueue, // [FUTURE] + Sleep(f64), // Sleep for x seconds } impl Default for RateLimiter { @@ -26,23 +31,50 @@ impl RateLimiter { Self { timer: Instant::now(), msgcounter: 0, + lastmsgtimer: Instant::now(), } } pub fn check_limiter(&mut self) -> LimiterResp { - if self.timer.elapsed().as_secs() >= TIME_THRESHOLD_S { + + + let logstr = format!( + ">> RateLimiter > {:?}",self + ); + + botlog::trace( + logstr.as_str(), + Some("Rate Limiter Inner".to_string()), + None, + ); + + + let rsp = if self.timer.elapsed().as_secs() >= TIME_THRESHOLD_S { self.timer = Instant::now(); self.msgcounter = 0; LimiterResp::Allow - } else if self.msgcounter < MSG_THRESHOLD { + } else if self.msgcounter < MSG_THRESHOLD && + self.lastmsgtimer.elapsed().as_secs_f64() >= TIME_MIN_S_F64 { LimiterResp::Allow } else { // when elapsed() < TIME_THRESHOLD_S && msgcounter >= MSG_THRESHOLD - LimiterResp::Skip - } + // LimiterResp::Skip + LimiterResp::Sleep(TIME_MIN_S_F64 - self.lastmsgtimer.elapsed().as_secs_f64() + 0.1) + }; + + botlog::trace( + &format!("Limiter Response : {:?} ; Elapsed (as_sec_f64) : {}", + rsp, self.lastmsgtimer.elapsed().as_secs_f64()), + Some("Rate Limiter Inner".to_string()), + None, + ); + + rsp + } pub fn increment_counter(&mut self) { self.msgcounter += 1; + self.lastmsgtimer = Instant::now(); } } diff --git a/src/custom.rs b/src/custom.rs index fc802e6..6107797 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -1,5 +1,5 @@ /* - `modules` will : + `custom` will : - be a starting refrence point for the bot instance to pull module definitions for */ @@ -11,7 +11,8 @@ pub use crate::core::botmodules::ModulesManager; // [ ] Load submodules -mod experiments; +// mod experiments; +mod experimental; // [ ] init() function that accepts bot instance - this is passed to init() on submodules @@ -19,5 +20,6 @@ pub async fn init(mgr: Arc) { // Modules initializer loads modules into the bot // this is achieved by calling submodules that also have fn init() defined - experiments::init(mgr).await + // experiments::init(mgr).await + experimental::init(mgr).await; } diff --git a/src/custom/experimental.rs b/src/custom/experimental.rs new file mode 100644 index 0000000..409abd1 --- /dev/null +++ b/src/custom/experimental.rs @@ -0,0 +1,24 @@ +/* + `experimental` will : + - be for mostly experimental +*/ + +use std::sync::Arc; + +// pub use crate::core::botinstance::BotInstance; +pub use crate::core::botmodules::ModulesManager; + +// [ ] Load submodules + +mod experiment001; +mod experiment002; + +// [ ] init() function that accepts bot instance - this is passed to init() on submodules + +pub async fn init(mgr: Arc) { + // Modules initializer loads modules into the bot + // this is achieved by calling submodules that also have fn init() defined + + experiment001::init(Arc::clone(&mgr)).await; + experiment002::init(Arc::clone(&mgr)).await; +} diff --git a/src/custom/experimental/experiment001.rs b/src/custom/experimental/experiment001.rs new file mode 100644 index 0000000..18d7aaa --- /dev/null +++ b/src/custom/experimental/experiment001.rs @@ -0,0 +1,288 @@ +/* + Custom Modules - + + Usage : + [ ] within the file's init(), define BotActions & Load them into the ModulesManager + [ ] Define Execution Bodies for these BotActions + [ ] Afterwards, add the following to parent modules.rs file + - mod ; + - within init(), ::init(mgr).await + +*/ + + +const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + +use rand::Rng; +use twitch_irc::message::ReplyToMessage; +use std::sync::Arc; + +use crate::core::bot_actions::ExecBodyParams; +use crate::core::botinstance::Channel; +use crate::core::botlog; + +use crate::core::bot_actions::actions_util; +use crate::core::botmodules::{BotActionTrait, BotCommand, BotModule, Listener, ModulesManager}; + +use crate::core::identity::UserRole::*; + +use tokio::time::{sleep, Duration}; + +pub async fn init(mgr: Arc) { + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("experiments001")), + command: String::from("test1"), // command call name + alias: vec![ + String::from("tester1"), + String::from("testy1")], // String of alternative names + exec_body: actions_util::asyncbox(testy), + help: String::from("Test Command tester"), + required_roles: vec![ + BotAdmin, + Mod(OF_CMD_CHANNEL), + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_to_modmgr(Arc::clone(&mgr)).await; + + // 1. Define the BotAction + let list1 = Listener { + module: BotModule(String::from("experiments001")), + name: String::from("GoodGirl Listener"), + exec_body: actions_util::asyncbox(good_girl), + help: String::from(""), + }; + + // 2. Add the BotAction to ModulesManager + list1.add_to_modmgr(Arc::clone(&mgr)).await; + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("experiments001")), + command: String::from("babygirl"), // command call name + alias: vec![], // String of alternative names + exec_body: actions_util::asyncbox(babygirl), + help: String::from("Babygirl"), + required_roles: vec![ + BotAdmin, + // Mod(OF_CMD_CHANNEL), + Broadcaster, + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_to_modmgr(Arc::clone(&mgr)).await; + + + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("experiments001")), + command: String::from("rtest"), // command call name + alias: vec![], // String of alternative names + exec_body: actions_util::asyncbox(routinelike), + help: String::from("routinelike"), + required_roles: vec![ + BotAdmin, + // Mod(OF_CMD_CHANNEL), + //Broadcaster, + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_to_modmgr(Arc::clone(&mgr)).await; + + let bc1 = BotCommand { + module: BotModule(String::from("experiments001")), + command: String::from("rp1"), // command call name + alias: vec![ + String::from("rp2"), + String::from("rp3")], // String of alternative names + exec_body: actions_util::asyncbox(rp), + help: String::from("Test Command tester"), + required_roles: vec![ + BotAdmin, + Mod(OF_CMD_CHANNEL), + ], + }; + bc1.add_core_to_modmgr(Arc::clone(&mgr)).await; +} + +async fn rp(params : ExecBodyParams) +{ + //triggers if the message is a reply + if params.get_parent_reply().is_some(){ + + //getting the channel id where the message was sent + let channel_id = params.get_parent_reply().unwrap().channel_login; + + //getting the first message id that was sent + let message_id = params.get_parent_reply().unwrap().message_id; + + //just for testing purposes + //print!("{} , {}",channel_id, message_id); + + //creating a tuple with the channel id and message id + let answear = + ( + channel_id.clone(), + message_id.clone() + ); + + let bot = Arc::clone(¶ms.bot); + let botlock = bot.read().await; + // uses chat.say_in_reply_to() for the bot controls for messages + botlock + .botmgrs + .chat + .say_in_reply_to( + //using the tuple as param to the message being replied + Channel(answear.0), + answear.1, + String::from("hey there"), + params.clone() + ).await; + } + else + { + println!("no reply") + } +} + + + +async fn good_girl(params : ExecBodyParams) { + + // [ ] Uses gen_ratio() to output bool based on a ratio probability . + // - For example gen_ratio(2,3) is 2 out of 3 or 0.67% (numerator,denomitator) + // - More Info : https://rust-random.github.io/rand/rand/trait.Rng.html#method.gen_ratio + + if params.msg.sender.name.to_lowercase() == "ModulatingForce".to_lowercase() + || params.msg.sender.name.to_lowercase() == "mzNToRi".to_lowercase() + { + botlog::debug( + "Good Girl Detected > Pausechamp", + Some("experiments > goodgirl()".to_string()), + Some(¶ms.msg), + ); + + let rollwin = rand::thread_rng().gen_ratio(1, 10); + + if rollwin { + botlog::debug( + "Oh that's a good girl!", + Some("experiments > goodgirl()".to_string()), + Some(¶ms.msg), + ); + + let bot = Arc::clone(¶ms.bot); + + + let botlock = bot.read().await; + + // uses chat.say_in_reply_to() for the bot controls for messages + botlock + .botmgrs + .chat + .say_in_reply_to( + Channel(params.clone().msg.channel_login().to_string()), + params.clone().msg.message_id().to_string(), + String::from("GoodGirl xdd "), + params.clone() + ).await; + + + } + } +} + +async fn testy(params : ExecBodyParams) { + println!("testy triggered!"); // NOTE : This test function intends to print (e.g., to stdout) at fn call + botlog::debug( + "testy triggered!", + Some("experiments > testy()".to_string()), + Some(¶ms.msg), + ); +} + + +async fn babygirl(params : ExecBodyParams) { + + + println!("babygirl triggered!"); // NOTE : This test function intends to print (e.g., to stdout) at fn call + botlog::debug( + "babygirl triggered!", + Some("experiments > babygirl()".to_string()), + Some(¶ms.msg), + ); + + + let bot = Arc::clone(¶ms.bot); + + let botlock = bot.read().await; + + + botlock + .botmgrs + .chat + .say_in_reply( + Channel(params.clone().msg.channel_login().to_string()), + String::from("16:13 notohh: cafdk"), + params.clone() + ).await; + + + sleep(Duration::from_secs_f64(0.5)).await; + + botlock + .botmgrs + .chat + .say_in_reply( + Channel(params.clone().msg.channel_login().to_string()), + String::from("16:13 notohh: have fun eating princess"), + params.clone() + ).await; + + + sleep(Duration::from_secs_f64(2.0)).await; + + botlock + .botmgrs + .chat + .say_in_reply( + Channel(params.clone().msg.channel_login().to_string()), + String::from("16:13 notohh: baby girl"), + params.clone() + ).await; + + + +} + + +async fn routinelike(params : ExecBodyParams) { + println!("routinelike triggered!"); // NOTE : This test function intends to print (e.g., to stdout) at fn call + botlog::debug( + "routinelike triggered!", + Some("experiments > routinelike()".to_string()), + Some(¶ms.msg), + ); + + // spawn an async block that runs independently from others + + tokio::spawn( async { + for _ in 0..5 { + println!(">> Innterroutine triggered!"); + sleep(Duration::from_secs_f64(5.0)).await; + } + } + ); + + // lines are executed after in conjunction to the spawn + +} + diff --git a/src/custom/experimental/experiment002.rs b/src/custom/experimental/experiment002.rs new file mode 100644 index 0000000..a7d1610 --- /dev/null +++ b/src/custom/experimental/experiment002.rs @@ -0,0 +1,196 @@ +/* + Custom Modules - + + Usage : + [ ] within the file's init(), define BotActions & Load them into the ModulesManager + [ ] Define Execution Bodies for these BotActions + [ ] Afterwards, add the following to parent modules.rs file + - mod ; + - within init(), ::init(mgr).await + +*/ + + +const OF_CMD_CHANNEL:Channel = Channel(String::new()); + + +use std::sync::Arc; + +use chrono::{TimeZone,Local}; +use twitch_irc::message::ReplyToMessage; + + +use crate::core::bot_actions::ExecBodyParams; +use crate::core::botinstance::Channel; +use crate::core::botlog; + +use casual_logger::Log; + +use crate::core::bot_actions::actions_util; +use crate::core::botmodules::{BotActionTrait, BotCommand, BotModule, ModulesManager}; + +use crate::core::identity::UserRole::*; + +pub async fn init(mgr: Arc) { + + + // 1. Define the BotAction + let botc1 = BotCommand { + module: BotModule(String::from("experiments002")), + command: String::from("say"), // command call name + alias: vec![ + "s".to_string(), + ], // String of alternative names + exec_body: actions_util::asyncbox(sayout), + help: String::from("Test Command tester"), + required_roles: vec![ + BotAdmin, + // Mod(OF_CMD_CHANNEL), + VIP(OF_CMD_CHANNEL), + ], + }; + + // 2. Add the BotAction to ModulesManager + botc1.add_to_modmgr(Arc::clone(&mgr)).await; + + // If enabling by defauling at instance level , uncomment the following + // mgr.set_instance_enabled(BotModule(String::from("experiments002"))).await; + + +} + + +async fn sayout(params : ExecBodyParams) { + + + /* + usage : + + */ + + + + let reply_parent = if let Some(Some(reply)) = params.msg.source.tags.0.get("reply-parent-msg-body") { + Some(reply) + } else { None } + ; + + + let reply_parent_ts = if let Some(Some(replyts)) = params.msg.source.tags.0.get("tmi-sent-ts") { + + let a: i64 = replyts.parse().unwrap(); + let b = Local.timestamp_millis_opt(a).unwrap(); + Some(b.format("%m-%d %H:%M")) + } else { None } + ; + + // [x] Unwraps arguments from message + + + let argrslt = + if let Some((_,str1)) = params.msg.message_text.split_once(' ') { + if reply_parent.is_none() { + if let Some((channelstr,msgstr)) = str1.split_once(' ') { + Some((channelstr,msgstr)) + } + else { None } + } else if let Some((_,str2)) = str1.split_once(' ') { + if let Some((channelstr,msgstr)) = str2.split_once(' ') { + Some((channelstr,msgstr)) + } + else { None } + } else { None } + } + else { None }; + + + + + match argrslt { + Some((trgchnl,outmsg)) => { + + let bot = Arc::clone(¶ms.bot); + + let botlock = bot.read().await; + + // [x] Validate first if trgchnl exists + + botlog::trace( + &format!("[TRACE] Evaluated status of {} : {:?}", + trgchnl.to_string().clone(),botlock.botmgrs.chat.client.get_channel_status(trgchnl.to_string().clone()).await), + Some("Chat > send_botmsg".to_string()), + None, + ); + + /* + 1. If a Reply , + [ ] Get Parent Content message - reply_parent + [ ] Get Parent Chatter - reply_parent_usr + [ ] Get Parent Channel - msg.channel_login + -> Share this first then + [ ] Get Reply Message (that triggered bot command) - msgstr + [ ] Get Reply Sender - msg.sender.name + [ ] Get Target Channel - trgchnl + + 2. If not a reply + [ ] Get Reply Message (that triggered bot command) - msgstr + [ ] Get Reply Sender - msg.sender.name + [ ] Get Target Channel - trgchnl + */ + + // reply_parent_ts + + let newoutmsg = if let Some(srcmsg) = reply_parent { + + format!("{} {} @ {} : {}", + reply_parent_ts.unwrap(), + params.msg.sender.name, + params.msg.channel_login, + srcmsg) + } else { + format!("in {} - {} : {}", + params.msg.channel_login, + params.msg.sender.name, + outmsg) + }; + + botlock + .botmgrs + .chat + .say( + trgchnl.to_string(), + newoutmsg.to_string(), + params.clone(), + ).await; + + + }, + None => { + botlog::debug( + "sayout had issues trying to parse arguments", + Some("experiment002 > sayout".to_string()), + Some(¶ms.msg), + ); + + let bot = Arc::clone(¶ms.bot); + + let botlock = bot.read().await; + + // uses chat.say_in_reply_to() for the bot controls for messages + botlock + .botmgrs + .chat + .say_in_reply_to( + Channel(params.clone().msg.channel_login().to_string()), + params.clone().msg.message_id().to_string(), + String::from("Invalid arguments"), + params.clone() + ).await; + + + }, + + } + + Log::flush(); +} \ No newline at end of file diff --git a/src/custom/experiments.rs b/src/custom/experiments.rs deleted file mode 100644 index 8b1bece..0000000 --- a/src/custom/experiments.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* - Custom Modules - - - Usage : - [ ] within the file's init(), define BotActions & Load them into the ModulesManager - [ ] Define Execution Bodies for these BotActions - [ ] Afterwards, add the following to parent modules.rs file - - mod ; - - within init(), ::init(mgr).await - -*/ - -use rand::Rng; -use std::sync::Arc; - -use twitch_irc::message::PrivmsgMessage; - -// use crate::core::botinstance::ChType::Channel; -use crate::core::botinstance::ChType; -use ChType::Channel; -use crate::core::botlog; - -use crate::core::bot_actions::actions_util::{self, BotAR}; -use crate::core::botmodules::{BotActionTrait, BotCommand, BotModule, Listener, ModulesManager}; - -use crate::core::identity::UserRole::*; - -pub async fn init(mgr: Arc) { - - const OF_CMD_CHANNEL:ChType = Channel(String::new()); - - // 1. Define the BotAction - let botc1 = BotCommand { - module: BotModule(String::from("experiments001")), - command: String::from("test1"), // command call name - alias: vec![ - String::from("tester1"), - String::from("testy1")], // String of alternative names - exec_body: actions_util::asyncbox(testy), - help: String::from("Test Command tester"), - required_roles: vec![ - BotAdmin, - Mod(OF_CMD_CHANNEL), - ], - }; - - // 2. Add the BotAction to ModulesManager - botc1.add_to_modmgr(Arc::clone(&mgr)).await; - - // 1. Define the BotAction - let list1 = Listener { - module: BotModule(String::from("experiments001")), - name: String::from("GoodGirl Listener"), - exec_body: actions_util::asyncbox(good_girl), - help: String::from(""), - }; - - // 2. Add the BotAction to ModulesManager - list1.add_to_modmgr(Arc::clone(&mgr)).await; -} - -async fn good_girl(bot: BotAR, msg: PrivmsgMessage) { - // [ ] Uses gen_ratio() to output bool based on a ratio probability . - // - For example gen_ratio(2,3) is 2 out of 3 or 0.67% (numerator,denomitator) - // - More Info : https://rust-random.github.io/rand/rand/trait.Rng.html#method.gen_ratio - - if msg.sender.name.to_lowercase() == "ModulatingForce".to_lowercase() - || msg.sender.name.to_lowercase() == "mzNToRi".to_lowercase() - { - botlog::debug( - "Good Girl Detected > Pausechamp", - Some("experiments > goodgirl()".to_string()), - Some(&msg), - ); - - let rollwin = rand::thread_rng().gen_ratio(1, 8); - - if rollwin { - botlog::debug( - "Oh that's a good girl!", - Some("experiments > goodgirl()".to_string()), - Some(&msg), - ); - - let bot = Arc::clone(&bot); - - let botlock = bot.read().await; - - // uses chat.say_in_reply_to() for the bot controls for messages - botlock - .botmgrs - .chat - .say_in_reply_to(&msg, String::from("GoodGirl xdd ")) - .await; - } - } -} - -async fn testy(mut _chat: BotAR, msg: PrivmsgMessage) { - println!("testy triggered!"); // NOTE : This test function intends to print (e.g., to stdout) at fn call - botlog::debug( - "testy triggered!", - Some("experiments > testy()".to_string()), - Some(&msg), - ); -} diff --git a/src/main.rs b/src/main.rs index b45ab98..6bc6c0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,19 @@ use bot_lib::core::botmodules; pub type BotAR = Arc>; // God I love anime girls +// fr fr #[tokio::main] pub async fn main() { Log::set_file_ext(Extension::Log); Log::set_level(Level::Trace); + Log::set_retention_days(2); // Log::set_level(Level::Notice); + + + + let bot = BotInstance::init().await; { @@ -27,7 +33,11 @@ pub async fn main() { for acts in (*actsdb_lock).values() { for act in acts { - let outstr = match act { + + let act_prelock = act; + let act = act_prelock.read().await; + + let outstr = match &(*act) { botmodules::BotAction::C(b) => { format!("bot actions > Command : {}", b.command) } @@ -52,3 +62,6 @@ pub async fn main() { let pstr = botlog::fatal("ERROR : EXIT Game loop", Some("main()".to_string()), None); panic!("{}", pstr); } + + + diff --git a/statix.toml b/statix.toml new file mode 100644 index 0000000..fbe25a9 --- /dev/null +++ b/statix.toml @@ -0,0 +1,3 @@ +disabled = [] +nix_version = '2.4' +ignore = ['.direnv']