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() => {