diff --git a/Cargo.toml b/Cargo.toml index d4ad532..f6155b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "red-alert" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] diff --git a/README.md b/README.md index f174042..28fbe23 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ Red Alert Main Configuration `config.yaml`: ```yaml discord_token: "DISCORD_TOKEN" lang_id: "ru_RU" -listening_text: "красную тревогу..." vosk_model_path: "vosk-model-small-ru-0.22" vosk_log_level: -1 ``` diff --git a/ru_RU.ftl b/ru_RU.ftl index 620665f..2674a87 100644 --- a/ru_RU.ftl +++ b/ru_RU.ftl @@ -1,3 +1,4 @@ +listening-text = красную тревогу... help-command-prefix-anchor = кринж киллер помощь help-command-full-header = > **`{$header} {$suffix}`** help-command-short-header = > **`{$header}`** @@ -51,7 +52,7 @@ actions-history-red-alert-command-voice-record-reason-format = __{$reason}__ actions-history-red-alert-command-voice-self-record = КРИНЖОВИК {$target-name} {$status} ФРАЗОЙ "{$reason-text}" ГДЕ ЕСТЬ СОВПАДЕНИЕ С "{$restricted-word}" НА {$similarity-percent}%. actions-history-red-alert-command-voice-target-record = КРИНЖОВИК {$target-name} {$status} ГОЛОСОМ МИРОТВОРЦA {$author-name} ПРИ ПОМОЩИ ФРАЗЫ "{$reason-text}" ГДЕ ЕСТЬ СОВПАДЕНИЕ С "{$restricted-word}" НА {$similarity-percent}%. actions-history-red-alert-command-text-self-record = КРИНЖОВИК {$target-name} {$status} КОМАНДОЙ -actions-history-red-alert-command-text-target-record = КРИНЖОВИК {$target-name} {status} КОМАНДОЙ МИРОТВОРЦA {$author-name} +actions-history-red-alert-command-text-target-record = КРИНЖОВИК {$target-name} {$status} КОМАНДОЙ МИРОТВОРЦA {$author-name} actions-history-red-alert-command-record = {$record-number}. [ВРЕМЯ: {$time}] {$record}. actions-history-red-alert-command-empty-list = ПОКА ЕЩЕ НИКОГО НЕ УШАТАЛ НА ЭТОМ СЕРВЕР)!1!)) guilds-voice-config-red-alert-command-prefix-anchor = код красный настройка голоса diff --git a/src/components/discord_chat/commands_handler.rs b/src/components/discord_chat/commands_handler.rs index 307f748..0d5cea0 100644 --- a/src/components/discord_chat/commands_handler.rs +++ b/src/components/discord_chat/commands_handler.rs @@ -70,8 +70,8 @@ impl EventHandler for Handler { .iter() .collect::>>(); commands.push(&help_command); - let mut args_commands: Vec<(Vec, &Box)> = - vec![]; + let mut args_commands = + Vec::<(Vec, &Box)>::new(); for command in commands { args_commands.push((args(command.prefix_anchor()), command)) } diff --git a/src/main.rs b/src/main.rs index 0d0bc00..8feda6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,8 +42,6 @@ async fn main() { let l10n = components::L10n::load(&lang_id_string); - let listening_text = settings.get_string("listening_text").ok(); - let vosk_model_path = settings .get_string("vosk_model_path") .expect("Expected a VOSK model path in the config!"); @@ -59,7 +57,6 @@ async fn main() { red_alert::RedAlertCommandsHandlerConstructor { recognition_model: VoskModel::new(vosk_model_path.as_str()) .expect("Incorrect recognition model!"), - listening_text, red_alert_handler: Arc::new(red_alert::RedAlertHandler), l10n, } diff --git a/src/red_alert/commands_handler/guilds_voice_config_command.rs b/src/red_alert/commands_handler/guilds_voice_config_command.rs index 358256e..439fd66 100644 --- a/src/red_alert/commands_handler/guilds_voice_config_command.rs +++ b/src/red_alert/commands_handler/guilds_voice_config_command.rs @@ -282,14 +282,7 @@ impl Command for GuildsVoiceConfigRedAlertCommand { return; }; let mut guilds_voice_config = self.guilds_voice_config.write().await; - let mut guild_voice_config = { - if let Some(specific) = guilds_voice_config.specific.remove(&guild_id.0) { - specific - } else { - let base = guilds_voice_config.base.clone(); - base - } - }; + let mut guild_voice_config = guilds_voice_config.remove(&guild_id); let mut args = params.args.to_vec(); let answer_msg = { let access_granted = guild_voice_config @@ -359,16 +352,14 @@ impl Command for GuildsVoiceConfigRedAlertCommand { fluent_args![], ) == action_string { - if guilds_voice_config.auto_track_ids.contains(&guild_id.0) { - guilds_voice_config.auto_track_ids.remove(&guild_id.0); + if guilds_voice_config.switch_auto_track(guild_id) { self.l10n.string( - "guilds-voice-config-red-alert-command-auto-track-remove", + "guilds-voice-config-red-alert-command-auto-track-add", fluent_args![], ) } else { - guilds_voice_config.auto_track_ids.insert(guild_id.0); self.l10n.string( - "guilds-voice-config-red-alert-command-auto-track-add", + "guilds-voice-config-red-alert-command-auto-track-remove", fluent_args![], ) } @@ -380,9 +371,7 @@ impl Command for GuildsVoiceConfigRedAlertCommand { } } }; - guilds_voice_config - .specific - .insert(guild_id.0, guild_voice_config); + guilds_voice_config.insert(guild_id, guild_voice_config); guilds_voice_config.write(); drop(guilds_voice_config); let _ = params.channel_id.say(&ctx, answer_msg).await; diff --git a/src/red_alert/commands_handler/mod.rs b/src/red_alert/commands_handler/mod.rs index 381f43a..6111f17 100644 --- a/src/red_alert/commands_handler/mod.rs +++ b/src/red_alert/commands_handler/mod.rs @@ -24,7 +24,6 @@ use voskrust::api::Model as VoskModel; pub struct RedAlertCommandsHandlerConstructor { pub recognition_model: VoskModel, - pub listening_text: Option, pub red_alert_handler: Arc, pub l10n: L10n, } @@ -40,14 +39,20 @@ impl RedAlertCommandsHandlerConstructor { l10n: self.l10n.clone(), }), on_ready: Box::new(RedAlertOnReady { - guilds_voices_receivers: guilds_voices_receivers.clone(), - actions_history: actions_history.clone(), - guilds_voice_config: guilds_voice_config.clone(), - recognition_model: self.recognition_model, - listening_text: self.listening_text, - red_alert_handler: self.red_alert_handler.clone(), + monitoring_performer: RedAlertMonitoringPerformer { + guilds_voices_receivers: guilds_voices_receivers.clone(), + guilds_voice_config: guilds_voice_config.clone(), + }, + recognizer_performer: RedAlertRecognizerPerformer { + guilds_voices_receivers: guilds_voices_receivers.clone(), + actions_history: actions_history.clone(), + guilds_voice_config: guilds_voice_config.clone(), + recognition_model: self.recognition_model, + red_alert_handler: self.red_alert_handler.clone(), + }, cancel_recognizer_sender: Arc::new(Mutex::new(None)), cancel_monitoring_sender: Arc::new(Mutex::new(None)), + l10n: self.l10n.clone(), }), commands: vec![ Box::new(TextRedAlertCommand { diff --git a/src/red_alert/commands_handler/on_ready.rs b/src/red_alert/commands_handler/on_ready.rs index f5c31ba..16148a4 100644 --- a/src/red_alert/commands_handler/on_ready.rs +++ b/src/red_alert/commands_handler/on_ready.rs @@ -1,233 +1,39 @@ use super::*; use serenity::model::gateway::Activity; -use serenity::model::id::GuildId; -use serenity::model::prelude::{ChannelId, OnlineStatus, Ready, UserId}; +use serenity::model::prelude::{OnlineStatus, Ready}; use serenity::prelude::Context; -use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use std::time::Duration; -use tokio::sync::oneshot::{channel, Sender}; -use tokio::sync::{Mutex, RwLock}; -use voskrust::api::Model as VoskModel; +use tokio::sync::oneshot::Sender; +use tokio::sync::Mutex; pub(super) struct RedAlertOnReady { - pub(super) guilds_voices_receivers: Arc>>, - pub(super) actions_history: Arc>, - pub(super) guilds_voice_config: Arc>, - pub(super) recognition_model: VoskModel, - pub(super) listening_text: Option, - pub(super) red_alert_handler: Arc, + pub(super) monitoring_performer: RedAlertMonitoringPerformer, + pub(super) recognizer_performer: RedAlertRecognizerPerformer, pub(super) cancel_recognizer_sender: Arc>>>, pub(super) cancel_monitoring_sender: Arc>>>, -} - -impl RedAlertOnReady { - async fn start_recognizer(&self, ctx: &Context) { - let (tx, mut rx) = channel::<()>(); - let mut cancel_sender = self.cancel_recognizer_sender.lock().await; - *cancel_sender = Some(tx); - drop(cancel_sender); - let guilds_voices_receivers = self.guilds_voices_receivers.clone(); - let actions_history = self.actions_history.clone(); - let recognition_model = self.recognition_model.clone(); - let guilds_voice_config = self.guilds_voice_config.clone(); - let red_alert_handler = self.red_alert_handler.clone(); - let ctx = ctx.clone(); - tokio::spawn(async move { - let mut recognizer_signal = Recognizer { - model: recognition_model, - voices_queue: GuildsVoicesReceivers(guilds_voices_receivers), - } - .start(); - let mut authors_processed_kicks: HashMap> = HashMap::new(); - loop { - let Some(recognizer_state) = tokio::select! { - recognizer_state = recognizer_signal.recv() => recognizer_state, - _ = &mut rx => None, - } else { - break; - }; - let log_prefix = match recognizer_state { - RecognizerState::RecognitionStart(info) - | RecognizerState::RecognitionResult(info, _) - | RecognizerState::RecognitionEnd(info) => { - let mut prefix_parts: Vec = vec![]; - let guild_id = info.guild_id; - if let Some(guild) = ctx.cache.guild(guild_id) { - prefix_parts.push(format!("[G:{}]", guild.name)); - } else { - prefix_parts.push(format!("[GID:{}]", guild_id)); - } - let user_id = info.user_id; - if let Some(user) = ctx.cache.user(user_id) { - prefix_parts.push(format!( - "[U:{}#{:04}]", - user.name, - user.discriminator.min(9999).max(1) - )); - } else { - prefix_parts.push(format!("[UID:{}]", user_id)); - } - prefix_parts.join("") - } - }; - match recognizer_state { - RecognizerState::RecognitionResult(info, result) => { - info!( - "{} Recognition RESULT: type: {:?}, text: \"{}\".", - log_prefix, result.result_type, result.text - ); - let guilds_voice_config = guilds_voice_config.read().await; - let users_ids_kicks_reasons = guilds_voice_config - .get(&info.guild_id) - .should_kick(&info.user_id.0, &result.text) - .into_iter() - .map(|v| (UserId(*v.0), v.1)) - .collect::>(); - drop(guilds_voice_config); - let mut users_ids_kicks = users_ids_kicks_reasons - .keys() - .cloned() - .collect::>(); - if let Some(mut author_processed_kicks) = - authors_processed_kicks.remove(&info.user_id) - { - users_ids_kicks = &users_ids_kicks - &author_processed_kicks; - author_processed_kicks.extend(users_ids_kicks.clone()); - authors_processed_kicks.insert(info.user_id, author_processed_kicks); - } else { - authors_processed_kicks.insert(info.user_id, users_ids_kicks.clone()); - } - if users_ids_kicks.is_empty() { - continue; - }; - for (kick_user_id, kick_reason) in users_ids_kicks_reasons { - if !users_ids_kicks.contains(&kick_user_id) { - continue; - } - info!( - "{} Recognition RESULT will be used for kick. Have restriction \"{}\"({}) =~ \"{}\".", - log_prefix, - kick_reason.real_word, - kick_reason.total_similarity, - kick_reason.word - ); - let actions_history = actions_history.clone(); - let red_alert_handler = red_alert_handler.clone(); - let ctx = ctx.clone(); - let log_prefix = log_prefix.clone(); - let result_text = result.text.clone(); - tokio::spawn(async move { - let guild_id = info.guild_id; - let deportation_result = red_alert_handler - .single(&ctx, &guild_id, &kick_user_id) - .await; - info!( - "{} Recognition RESULT used for kick, status is {:?}.", - log_prefix, deportation_result - ); - actions_history.lock().await.log_history( - guild_id, - RedAlertActionType::Voice { - author_id: info.user_id, - target_id: kick_user_id, - full_text: result_text, - reason: kick_reason, - is_success: deportation_result.is_deported(), - }, - ); - }); - } - } - RecognizerState::RecognitionStart(info) => { - info!("{} Recognition STARTED.", log_prefix); - authors_processed_kicks.remove(&info.user_id); - } - RecognizerState::RecognitionEnd(info) => { - info!("{} Recognition ENDED.", log_prefix); - authors_processed_kicks.remove(&info.user_id); - } - } - } - }); - } - async fn start_monitoring(&self, ctx: &Context) { - let (tx, mut rx) = channel::<()>(); - let mut cancel_sender = self.cancel_monitoring_sender.lock().await; - *cancel_sender = Some(tx); - drop(cancel_sender); - let guilds_voices_receivers = self.guilds_voices_receivers.clone(); - let guilds_voice_config = self.guilds_voice_config.clone(); - let ctx = ctx.clone(); - tokio::spawn(async move { - let mut guilds_active_channels: HashMap = HashMap::new(); - loop { - let Some(_) = tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(1)) => Some(()), - _ = &mut rx => None, - } else { - break; - }; - let bot_user_id = ctx.cache.current_user_id(); - let guilds_voice_config = guilds_voice_config.read().await; - for guild_id in &guilds_voice_config.auto_track_ids { - let Some(guild) = ctx.cache.guild(*guild_id) else { - continue; - }; - let mut channels_users_counts: HashMap = HashMap::new(); - for (user_id, voice_state) in guild.voice_states { - if bot_user_id == user_id { - continue; - } - let Some(channel_id) = voice_state.channel_id else { - continue; - }; - if let Some(users_count) = channels_users_counts.remove(&channel_id) { - channels_users_counts.insert(channel_id, users_count + 1); - } else { - channels_users_counts.insert(channel_id, 1); - } - } - if let Some(channel_id) = { - let mut channels_users_counts = channels_users_counts - .into_iter() - .collect::>(); - channels_users_counts.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); - channels_users_counts.first().map(|c| c.0) - } { - let is_prev_channel = guilds_active_channels - .get(&guild.id) - .map_or_else(|| false, |i| i == &channel_id); - if is_prev_channel { - continue; - } - guilds_active_channels.insert(guild.id, channel_id); - _ = start_listen( - guilds_voices_receivers.clone(), - &ctx, - guild.id, - channel_id, - ) - .await; - } else { - if !guilds_active_channels.remove(&guild.id).is_some() { - continue; - } - _ = stop_listen(guilds_voices_receivers.clone(), &ctx, guild.id).await; - } - } - } - }); - } + pub(super) l10n: L10n, } #[async_trait] impl OnReady for RedAlertOnReady { async fn process(&self, ctx: Context, ready: Ready) { info!("{} is connected!", ready.user.name); - let activity = self.listening_text.as_ref().map(|t| Activity::listening(t)); - ctx.set_presence(activity, OnlineStatus::Online).await; - self.start_recognizer(&ctx).await; - self.start_monitoring(&ctx).await; + ctx.set_presence( + Some(Activity::listening( + self.l10n.string("listening-text", fluent_args![]), + )), + OnlineStatus::Online, + ) + .await; + + let new_cancel_monitoring_sender = self.monitoring_performer.perform(&ctx); + let mut cancel_monitoring_sender = self.cancel_monitoring_sender.lock().await; + *cancel_monitoring_sender = Some(new_cancel_monitoring_sender); + drop(cancel_monitoring_sender); + + let new_cancel_recognizer_sender = self.recognizer_performer.perform(&ctx); + let mut cancel_recognizer_sender = self.cancel_recognizer_sender.lock().await; + *cancel_recognizer_sender = Some(new_cancel_recognizer_sender); + drop(cancel_recognizer_sender); } } diff --git a/src/red_alert/commands_handler/start_listen_command.rs b/src/red_alert/commands_handler/start_listen_command.rs index 7713572..7d93c2b 100644 --- a/src/red_alert/commands_handler/start_listen_command.rs +++ b/src/red_alert/commands_handler/start_listen_command.rs @@ -4,7 +4,6 @@ use serenity::model::prelude::ChannelId; use serenity::model::prelude::Mention; use serenity::prelude::{Context, Mentionable}; use std::collections::HashMap; -use std::ops::DerefMut; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; @@ -14,32 +13,6 @@ pub(super) struct StartListenRedAlertCommand { pub(super) l10n: L10n, } -pub enum StartListenError { - SongbirdMissing, - ConnectingError, -} - -pub async fn start_listen( - guilds_voices_receivers: Arc>>, - ctx: &Context, - guild_id: GuildId, - channel_id: ChannelId, -) -> Result<(), StartListenError> { - let Some(manager) = songbird::get(ctx).await else { - return Err(StartListenError::SongbirdMissing); - }; - let (handler_lock, connection_result) = manager.join(guild_id, channel_id).await; - if !connection_result.is_ok() { - return Err(StartListenError::ConnectingError); - } - let mut handler = handler_lock.lock().await; - let voice_receiver = VoiceReceiver::with_configuration(Default::default()); - voice_receiver.subscribe(handler.deref_mut()); - let mut guilds_voices_receivers = guilds_voices_receivers.write().await; - guilds_voices_receivers.insert(guild_id, voice_receiver); - Ok(()) -} - #[async_trait] impl Command for StartListenRedAlertCommand { fn prefix_anchor(&self) -> String { diff --git a/src/red_alert/commands_handler/stop_listen_command.rs b/src/red_alert/commands_handler/stop_listen_command.rs index c42085d..66ba88f 100644 --- a/src/red_alert/commands_handler/stop_listen_command.rs +++ b/src/red_alert/commands_handler/stop_listen_command.rs @@ -10,31 +10,6 @@ pub(super) struct StopListenRedAlertCommand { pub(super) l10n: L10n, } -pub enum StopListenError { - SongbirdMissing, - DisconnectingError, - NoListeners, -} - -pub async fn stop_listen( - guilds_voices_receivers: Arc>>, - ctx: &Context, - guild_id: GuildId, -) -> Result<(), StopListenError> { - let Some(manager) = songbird::get(ctx).await else { - return Err(StopListenError::SongbirdMissing); - }; - if !manager.get(guild_id).is_some() { - return Err(StopListenError::NoListeners); - } - if manager.remove(guild_id).await.is_err() { - return Err(StopListenError::DisconnectingError); - } - let mut guilds_voices_receivers = guilds_voices_receivers.write().await; - guilds_voices_receivers.remove(&guild_id); - Ok(()) -} - #[async_trait] impl Command for StopListenRedAlertCommand { fn prefix_anchor(&self) -> String { diff --git a/src/red_alert/guilds_voice_config.rs b/src/red_alert/guilds_voice_config.rs index 159fee2..9b0b712 100644 --- a/src/red_alert/guilds_voice_config.rs +++ b/src/red_alert/guilds_voice_config.rs @@ -5,9 +5,9 @@ use std::collections::{HashMap, HashSet}; #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct RedAlertGuildsVoiceConfig { - pub auto_track_ids: HashSet, - pub base: RedAlertVoiceConfig, - pub specific: HashMap>, + auto_track_ids: HashSet, + base: RedAlertVoiceConfig, + specific: HashMap>, } impl RedAlertGuildsVoiceConfig { @@ -27,4 +27,23 @@ impl RedAlertGuildsVoiceConfig { pub fn get(&self, guild_id: &GuildId) -> &RedAlertVoiceConfig { self.specific.get(&guild_id.0).unwrap_or(&self.base) } + pub fn remove(&mut self, guild_id: &GuildId) -> RedAlertVoiceConfig { + self.specific + .remove(&guild_id.0) + .unwrap_or(self.base.clone()) + } + pub fn insert(&mut self, guild_id: GuildId, guild_voice_config: RedAlertVoiceConfig) { + self.specific.insert(guild_id.0, guild_voice_config); + } + pub fn switch_auto_track(&mut self, guild_id: GuildId) -> bool { + if self.auto_track_ids.remove(&guild_id.0) { + false + } else { + self.auto_track_ids.insert(guild_id.0); + true + } + } + pub fn auto_track_ids(&self) -> &HashSet { + &self.auto_track_ids + } } diff --git a/src/red_alert/listen_actions.rs b/src/red_alert/listen_actions.rs new file mode 100644 index 0000000..948d038 --- /dev/null +++ b/src/red_alert/listen_actions.rs @@ -0,0 +1,60 @@ +use super::super::components::*; +use serenity::model::id::GuildId; +use serenity::model::prelude::ChannelId; +use serenity::prelude::Context; +use std::collections::HashMap; +use std::ops::DerefMut; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub enum StartListenError { + SongbirdMissing, + ConnectingError, +} + +pub async fn start_listen( + guilds_voices_receivers: Arc>>, + ctx: &Context, + guild_id: GuildId, + channel_id: ChannelId, +) -> Result<(), StartListenError> { + let Some(manager) = songbird::get(ctx).await else { + return Err(StartListenError::SongbirdMissing); + }; + let (handler_lock, connection_result) = manager.join(guild_id, channel_id).await; + if !connection_result.is_ok() { + return Err(StartListenError::ConnectingError); + } + let mut handler = handler_lock.lock().await; + _ = handler.mute(true).await; + let voice_receiver = VoiceReceiver::with_configuration(Default::default()); + voice_receiver.subscribe(handler.deref_mut()); + let mut guilds_voices_receivers = guilds_voices_receivers.write().await; + guilds_voices_receivers.insert(guild_id, voice_receiver); + Ok(()) +} + +pub enum StopListenError { + SongbirdMissing, + DisconnectingError, + NoListeners, +} + +pub async fn stop_listen( + guilds_voices_receivers: Arc>>, + ctx: &Context, + guild_id: GuildId, +) -> Result<(), StopListenError> { + let Some(manager) = songbird::get(ctx).await else { + return Err(StopListenError::SongbirdMissing); + }; + if !manager.get(guild_id).is_some() { + return Err(StopListenError::NoListeners); + } + if manager.remove(guild_id).await.is_err() { + return Err(StopListenError::DisconnectingError); + } + let mut guilds_voices_receivers = guilds_voices_receivers.write().await; + guilds_voices_receivers.remove(&guild_id); + Ok(()) +} diff --git a/src/red_alert/mod.rs b/src/red_alert/mod.rs index daba626..cef23e7 100644 --- a/src/red_alert/mod.rs +++ b/src/red_alert/mod.rs @@ -2,13 +2,19 @@ mod actions_history; mod commands_handler; mod guilds_voice_config; mod handler; +mod listen_actions; +mod monitoring_performer; +mod recognizer_performer; mod voice_config; -pub use actions_history::*; +use actions_history::*; pub use commands_handler::*; -pub use guilds_voice_config::*; +use guilds_voice_config::*; pub use handler::*; -pub use voice_config::*; +use listen_actions::*; +use monitoring_performer::*; +use recognizer_performer::*; +use voice_config::*; pub(super) const NEW_LINE: &'static str = "\n"; pub(super) const SPACE: &'static str = " "; diff --git a/src/red_alert/monitoring_performer.rs b/src/red_alert/monitoring_performer.rs new file mode 100644 index 0000000..1e06682 --- /dev/null +++ b/src/red_alert/monitoring_performer.rs @@ -0,0 +1,92 @@ +use super::super::components::*; +use super::*; +use serenity::model::id::GuildId; +use serenity::model::prelude::ChannelId; +use serenity::prelude::Context; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::oneshot::{channel, Sender}; +use tokio::sync::RwLock; + +pub struct RedAlertMonitoringPerformer { + pub guilds_voices_receivers: Arc>>, + pub guilds_voice_config: Arc>, +} + +impl RedAlertMonitoringPerformer { + pub fn perform(&self, ctx: &Context) -> Sender<()> { + let (tx, mut rx) = channel::<()>(); + let guilds_voices_receivers = self.guilds_voices_receivers.clone(); + let guilds_voice_config = self.guilds_voice_config.clone(); + let ctx = ctx.clone(); + tokio::spawn(async move { + loop { + let Some(_) = tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(1)) => Some(()), + _ = &mut rx => None, + } else { + break; + }; + let bot_user_id = ctx.cache.current_user_id(); + let guilds_voice_config = guilds_voice_config.read().await; + for guild_id in guilds_voice_config.auto_track_ids() { + let Some(guild) = ctx.cache.guild(*guild_id) else { + continue; + }; + let mut bot_channel_id = Option::::None; + let mut channels_users_count = HashMap::::new(); + for (user_id, voice_state) in guild.voice_states { + let Some(channel_id) = voice_state.channel_id else { + continue; + }; + if bot_user_id == user_id { + bot_channel_id = Some(channel_id); + continue; + } + if voice_state.self_mute || voice_state.mute { + continue; + } + if let Some(users_count) = channels_users_count.remove(&channel_id) { + channels_users_count.insert(channel_id, users_count + 1); + } else { + channels_users_count.insert(channel_id, 1); + } + } + if let Some(channel_id) = { + let mut channels_users_count = channels_users_count + .into_iter() + .collect::>(); + channels_users_count.sort_by(|a, b| { + let ordering = b.1.partial_cmp(&a.1).unwrap(); + if ordering == Ordering::Equal { + b.0.partial_cmp(&a.0).unwrap() + } else { + ordering + } + }); + channels_users_count.first().map(|c| c.0) + } { + if bot_channel_id.map_or_else(|| false, |i| i == channel_id) { + continue; + } + _ = start_listen( + guilds_voices_receivers.clone(), + &ctx, + guild.id, + channel_id, + ) + .await; + } else { + if bot_channel_id.is_none() { + continue; + } + _ = stop_listen(guilds_voices_receivers.clone(), &ctx, guild.id).await; + } + } + } + }); + tx + } +} diff --git a/src/red_alert/recognizer_performer.rs b/src/red_alert/recognizer_performer.rs new file mode 100644 index 0000000..eff88e2 --- /dev/null +++ b/src/red_alert/recognizer_performer.rs @@ -0,0 +1,148 @@ +use super::super::components::*; +use super::*; +use serenity::model::id::GuildId; +use serenity::model::prelude::UserId; +use serenity::prelude::Context; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::oneshot::{channel, Sender}; +use tokio::sync::{Mutex, RwLock}; +use voskrust::api::Model as VoskModel; + +pub struct RedAlertRecognizerPerformer { + pub guilds_voices_receivers: Arc>>, + pub actions_history: Arc>, + pub recognition_model: VoskModel, + pub guilds_voice_config: Arc>, + pub red_alert_handler: Arc, +} + +impl RedAlertRecognizerPerformer { + pub fn perform(&self, ctx: &Context) -> Sender<()> { + let (tx, mut rx) = channel::<()>(); + let guilds_voices_receivers = self.guilds_voices_receivers.clone(); + let actions_history = self.actions_history.clone(); + let recognition_model = self.recognition_model.clone(); + let guilds_voice_config = self.guilds_voice_config.clone(); + let red_alert_handler = self.red_alert_handler.clone(); + let ctx = ctx.clone(); + tokio::spawn(async move { + let mut recognizer_signal = Recognizer { + model: recognition_model, + voices_queue: GuildsVoicesReceivers(guilds_voices_receivers), + } + .start(); + let mut authors_processed_kicks: HashMap> = HashMap::new(); + loop { + let Some(recognizer_state) = tokio::select! { + recognizer_state = recognizer_signal.recv() => recognizer_state, + _ = &mut rx => None, + } else { + break; + }; + let log_prefix = match recognizer_state { + RecognizerState::RecognitionStart(info) + | RecognizerState::RecognitionResult(info, _) + | RecognizerState::RecognitionEnd(info) => { + let mut prefix_parts: Vec = vec![]; + let guild_id = info.guild_id; + if let Some(guild) = ctx.cache.guild(guild_id) { + prefix_parts.push(format!("[G:{}]", guild.name)); + } else { + prefix_parts.push(format!("[GID:{}]", guild_id)); + } + let user_id = info.user_id; + if let Some(user) = ctx.cache.user(user_id) { + prefix_parts.push(format!( + "[U:{}#{:04}]", + user.name, + user.discriminator.min(9999).max(1) + )); + } else { + prefix_parts.push(format!("[UID:{}]", user_id)); + } + prefix_parts.join("") + } + }; + match recognizer_state { + RecognizerState::RecognitionResult(info, result) => { + info!( + "{} Recognition RESULT: type: {:?}, text: \"{}\".", + log_prefix, result.result_type, result.text + ); + let guilds_voice_config = guilds_voice_config.read().await; + let users_ids_kicks_reasons = guilds_voice_config + .get(&info.guild_id) + .should_kick(&info.user_id.0, &result.text) + .into_iter() + .map(|v| (UserId(*v.0), v.1)) + .collect::>(); + drop(guilds_voice_config); + let mut users_ids_kicks = users_ids_kicks_reasons + .keys() + .cloned() + .collect::>(); + if let Some(mut author_processed_kicks) = + authors_processed_kicks.remove(&info.user_id) + { + users_ids_kicks = &users_ids_kicks - &author_processed_kicks; + author_processed_kicks.extend(users_ids_kicks.clone()); + authors_processed_kicks.insert(info.user_id, author_processed_kicks); + } else { + authors_processed_kicks.insert(info.user_id, users_ids_kicks.clone()); + } + if users_ids_kicks.is_empty() { + continue; + }; + for (kick_user_id, kick_reason) in users_ids_kicks_reasons { + if !users_ids_kicks.contains(&kick_user_id) { + continue; + } + info!( + "{} Recognition RESULT will be used for kick. Have restriction \"{}\"({}) =~ \"{}\".", + log_prefix, + kick_reason.real_word, + kick_reason.total_similarity, + kick_reason.word + ); + let actions_history = actions_history.clone(); + let red_alert_handler = red_alert_handler.clone(); + let ctx = ctx.clone(); + let log_prefix = log_prefix.clone(); + let result_text = result.text.clone(); + tokio::spawn(async move { + let guild_id = info.guild_id; + let deportation_result = red_alert_handler + .single(&ctx, &guild_id, &kick_user_id) + .await; + info!( + "{} Recognition RESULT used for kick, status is {:?}.", + log_prefix, deportation_result + ); + actions_history.lock().await.log_history( + guild_id, + RedAlertActionType::Voice { + author_id: info.user_id, + target_id: kick_user_id, + full_text: result_text, + reason: kick_reason, + is_success: deportation_result.is_deported(), + }, + ); + }); + } + } + RecognizerState::RecognitionStart(info) => { + info!("{} Recognition STARTED.", log_prefix); + authors_processed_kicks.remove(&info.user_id); + } + RecognizerState::RecognitionEnd(info) => { + info!("{} Recognition ENDED.", log_prefix); + authors_processed_kicks.remove(&info.user_id); + } + } + } + }); + tx + } +}