From 6070ca035cb6f18b18a2e467240c06d6df3c6092 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Fri, 15 Nov 2024 13:33:35 +0300 Subject: [PATCH] feat: Notification when users are banned When `ban_alert` is set to true, the Telegram bot will send a notification after banning users Signed-off-by: Awiteb --- src/config.rs | 9 ++-- src/main.rs | 7 ++- src/telegram_bot/mod.rs | 6 ++- .../{sus_handler.rs => users_handler.rs} | 47 ++++++++++++++----- src/users_fetcher.rs | 18 +++++-- 5 files changed, 65 insertions(+), 22 deletions(-) rename src/telegram_bot/{sus_handler.rs => users_handler.rs} (70%) diff --git a/src/config.rs b/src/config.rs index f25dbef..3045ba3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -71,11 +71,14 @@ pub struct Forgejo { #[derive(Deserialize)] pub struct Telegram { /// Telegram bot token - pub token: String, + pub token: String, /// Chat to send the alert in - pub chat: ChatId, + pub chat: ChatId, + /// Send an alert when ban a user + #[serde(default)] + pub ban_alert: bool, /// Bot language - pub lang: Lang, + pub lang: Lang, } /// The expression diff --git a/src/main.rs b/src/main.rs index 28cf832..b72e9c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,9 @@ async fn try_main() -> error::GuardResult<()> { // Suspicious users are sent and received in this channel, users who meet the // `alert` expressions let (sus_sender, sus_receiver) = sync::mpsc::channel::(100); + // Banned users (already banned) are sent and received in this channel, this + // to alert the admins on Telegram if `ban_alert` is set to true + let (ban_sender, ban_receiver) = sync::mpsc::channel::(100); tracing::info!("The instance: {}", config.forgejo.instance); tracing::info!("Dry run: {}", config.dry_run); @@ -48,13 +51,15 @@ async fn try_main() -> error::GuardResult<()> { tokio::spawn(users_fetcher::users_fetcher( Arc::clone(&config), cancellation_token.clone(), - sus_sender.clone(), + sus_sender, + ban_sender, )); tokio::spawn(telegram_bot::start_bot( Arc::clone(&config), cancellation_token.clone(), sus_receiver, + ban_receiver, )); tokio::select! { diff --git a/src/telegram_bot/mod.rs b/src/telegram_bot/mod.rs index e022e93..b160c8d 100644 --- a/src/telegram_bot/mod.rs +++ b/src/telegram_bot/mod.rs @@ -18,7 +18,7 @@ mod callback_handler; mod message_handler; -mod sus_handler; +mod users_handler; use std::sync::Arc; @@ -55,6 +55,7 @@ pub async fn start_bot( config: Arc, cancellation_token: CancellationToken, sus_receiver: Receiver, + ban_receiver: Receiver, ) { tracing::info!("Starting the telegram bot"); @@ -66,11 +67,12 @@ pub async fn start_bot( ) .branch(Update::filter_callback_query().endpoint(callback_handler)); - tokio::spawn(sus_handler::sus_users_handler( + tokio::spawn(users_handler::users_handler( bot.clone(), Arc::clone(&config), cancellation_token, sus_receiver, + ban_receiver, )); Dispatcher::builder(bot, handler) diff --git a/src/telegram_bot/sus_handler.rs b/src/telegram_bot/users_handler.rs similarity index 70% rename from src/telegram_bot/sus_handler.rs rename to src/telegram_bot/users_handler.rs index a1d0532..a24503d 100644 --- a/src/telegram_bot/sus_handler.rs +++ b/src/telegram_bot/users_handler.rs @@ -48,6 +48,20 @@ fn not_found_if_empty(text: &str) -> Cow<'_, str> { } } +/// Generate a user details message +fn user_details(msg: &str, user: &ForgejoUser) -> String { + t!( + msg, + user_id = user.id, + username = user.username, + full_name = not_found_if_empty(&user.full_name), + bio = not_found_if_empty(&user.biography), + website = not_found_if_empty(&user.website), + profile = user.html_url, + ) + .to_string() +} + /// Send a suspicious user alert to the admins pub async fn send_sus_alert( bot: &Bot, @@ -56,34 +70,45 @@ pub async fn send_sus_alert( ) -> ResponseResult<()> { let keyboard = make_sus_inline_keyboard(&sus_user); + let caption = user_details("messages.sus_alert", &sus_user); bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url)) - .caption(t!( - "messages.sus_alert", - user_id = sus_user.id, - username = sus_user.username, - full_name = not_found_if_empty(&sus_user.full_name), - bio = not_found_if_empty(&sus_user.biography), - website = not_found_if_empty(&sus_user.website), - profile = sus_user.html_url, - )) + .caption(caption) .reply_markup(keyboard) .await?; Ok(()) } -/// Handle the suspicious users -pub async fn sus_users_handler( +/// Send a ban notification to the admins chat +pub async fn send_ban_notify( + bot: &Bot, + sus_user: ForgejoUser, + config: &Config, +) -> ResponseResult<()> { + let caption = user_details("messages.ban_notify", &sus_user); + bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url)) + .caption(caption) + .await?; + + Ok(()) +} + +/// Handle the suspicious and banned users +pub async fn users_handler( bot: Bot, config: Arc, cancellation_token: CancellationToken, mut sus_receiver: Receiver, + mut ban_receiver: Receiver, ) { loop { tokio::select! { Some(sus_user) = sus_receiver.recv() => { send_sus_alert(&bot, sus_user, &config).await.ok(); } + Some(banned_user) = ban_receiver.recv() => { + send_ban_notify(&bot, banned_user, &config).await.ok(); + } _ = cancellation_token.cancelled() => { tracing::info!("sus users handler has been stopped successfully."); break; diff --git a/src/users_fetcher.rs b/src/users_fetcher.rs index 2809e52..a2d6d0b 100644 --- a/src/users_fetcher.rs +++ b/src/users_fetcher.rs @@ -59,10 +59,13 @@ async fn check_new_user( request_client: &reqwest::Client, config: &Config, sus_sender: &Sender, + ban_sender: &Sender, ) { if let Some(re) = config.expressions.ban.is_match(&user) { tracing::info!("@{} has been banned because `{re}`", user.username); if config.dry_run { + // If it's a dry run, we don't need to ban the user + ban_sender.send(user).await.ok(); return; } @@ -75,10 +78,12 @@ async fn check_new_user( .await { tracing::error!("Error while banning a user: {err}"); + } else { + ban_sender.send(user).await.ok(); } } else if let Some(re) = config.expressions.sus.is_match(&user) { tracing::info!("@{} has been suspected because `{re}`", user.username); - let _ = sus_sender.send(user).await; + sus_sender.send(user).await.ok(); } } @@ -86,9 +91,10 @@ async fn check_new_user( /// banned users async fn check_new_users( last_user_id: Arc, - sus_sender: Sender, request_client: Arc, config: Arc, + sus_sender: Sender, + ban_sender: Sender, ) { match get_new_users( &request_client, @@ -108,7 +114,7 @@ async fn check_new_users( } for user in new_users { - check_new_user(user, &request_client, &config, &sus_sender).await; + check_new_user(user, &request_client, &config, &sus_sender, &ban_sender).await; } } Err(err) => { @@ -123,6 +129,7 @@ pub async fn users_fetcher( config: Arc, cancellation_token: CancellationToken, sus_sender: Sender, + ban_sender: Sender, ) { let last_user_id = Arc::new(AtomicUsize::new(0)); let request_client = Arc::new(reqwest::Client::new()); @@ -130,12 +137,13 @@ pub async fn users_fetcher( tracing::info!("Starting users fetcher"); loop { tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(120)) => { + _ = tokio::time::sleep(Duration::from_secs(20)) => { tokio::spawn(check_new_users( Arc::clone(&last_user_id), - sus_sender.clone(), Arc::clone(&request_client), Arc::clone(&config), + sus_sender.clone(), + ban_sender.clone(), )); } _ = cancellation_token.cancelled() => {