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 <a@4rs.nl>
This commit is contained in:
Awiteb 2024-11-15 13:33:35 +03:00
parent 3e6c4dece8
commit 6070ca035c
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
5 changed files with 65 additions and 22 deletions

View file

@ -71,11 +71,14 @@ pub struct Forgejo {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Telegram { pub struct Telegram {
/// Telegram bot token /// Telegram bot token
pub token: String, pub token: String,
/// Chat to send the alert in /// 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 /// Bot language
pub lang: Lang, pub lang: Lang,
} }
/// The expression /// The expression

View file

@ -38,6 +38,9 @@ async fn try_main() -> error::GuardResult<()> {
// Suspicious users are sent and received in this channel, users who meet the // Suspicious users are sent and received in this channel, users who meet the
// `alert` expressions // `alert` expressions
let (sus_sender, sus_receiver) = sync::mpsc::channel::<forgejo_api::ForgejoUser>(100); let (sus_sender, sus_receiver) = sync::mpsc::channel::<forgejo_api::ForgejoUser>(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::<forgejo_api::ForgejoUser>(100);
tracing::info!("The instance: {}", config.forgejo.instance); tracing::info!("The instance: {}", config.forgejo.instance);
tracing::info!("Dry run: {}", config.dry_run); tracing::info!("Dry run: {}", config.dry_run);
@ -48,13 +51,15 @@ async fn try_main() -> error::GuardResult<()> {
tokio::spawn(users_fetcher::users_fetcher( tokio::spawn(users_fetcher::users_fetcher(
Arc::clone(&config), Arc::clone(&config),
cancellation_token.clone(), cancellation_token.clone(),
sus_sender.clone(), sus_sender,
ban_sender,
)); ));
tokio::spawn(telegram_bot::start_bot( tokio::spawn(telegram_bot::start_bot(
Arc::clone(&config), Arc::clone(&config),
cancellation_token.clone(), cancellation_token.clone(),
sus_receiver, sus_receiver,
ban_receiver,
)); ));
tokio::select! { tokio::select! {

View file

@ -18,7 +18,7 @@
mod callback_handler; mod callback_handler;
mod message_handler; mod message_handler;
mod sus_handler; mod users_handler;
use std::sync::Arc; use std::sync::Arc;
@ -55,6 +55,7 @@ pub async fn start_bot(
config: Arc<Config>, config: Arc<Config>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
sus_receiver: Receiver<ForgejoUser>, sus_receiver: Receiver<ForgejoUser>,
ban_receiver: Receiver<ForgejoUser>,
) { ) {
tracing::info!("Starting the telegram bot"); tracing::info!("Starting the telegram bot");
@ -66,11 +67,12 @@ pub async fn start_bot(
) )
.branch(Update::filter_callback_query().endpoint(callback_handler)); .branch(Update::filter_callback_query().endpoint(callback_handler));
tokio::spawn(sus_handler::sus_users_handler( tokio::spawn(users_handler::users_handler(
bot.clone(), bot.clone(),
Arc::clone(&config), Arc::clone(&config),
cancellation_token, cancellation_token,
sus_receiver, sus_receiver,
ban_receiver,
)); ));
Dispatcher::builder(bot, handler) Dispatcher::builder(bot, handler)

View file

@ -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 /// Send a suspicious user alert to the admins
pub async fn send_sus_alert( pub async fn send_sus_alert(
bot: &Bot, bot: &Bot,
@ -56,34 +70,45 @@ pub async fn send_sus_alert(
) -> ResponseResult<()> { ) -> ResponseResult<()> {
let keyboard = make_sus_inline_keyboard(&sus_user); 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)) bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url))
.caption(t!( .caption(caption)
"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,
))
.reply_markup(keyboard) .reply_markup(keyboard)
.await?; .await?;
Ok(()) Ok(())
} }
/// Handle the suspicious users /// Send a ban notification to the admins chat
pub async fn sus_users_handler( 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, bot: Bot,
config: Arc<Config>, config: Arc<Config>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
mut sus_receiver: Receiver<ForgejoUser>, mut sus_receiver: Receiver<ForgejoUser>,
mut ban_receiver: Receiver<ForgejoUser>,
) { ) {
loop { loop {
tokio::select! { tokio::select! {
Some(sus_user) = sus_receiver.recv() => { Some(sus_user) = sus_receiver.recv() => {
send_sus_alert(&bot, sus_user, &config).await.ok(); 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() => { _ = cancellation_token.cancelled() => {
tracing::info!("sus users handler has been stopped successfully."); tracing::info!("sus users handler has been stopped successfully.");
break; break;

View file

@ -59,10 +59,13 @@ async fn check_new_user(
request_client: &reqwest::Client, request_client: &reqwest::Client,
config: &Config, config: &Config,
sus_sender: &Sender<ForgejoUser>, sus_sender: &Sender<ForgejoUser>,
ban_sender: &Sender<ForgejoUser>,
) { ) {
if let Some(re) = config.expressions.ban.is_match(&user) { if let Some(re) = config.expressions.ban.is_match(&user) {
tracing::info!("@{} has been banned because `{re}`", user.username); tracing::info!("@{} has been banned because `{re}`", user.username);
if config.dry_run { 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; return;
} }
@ -75,10 +78,12 @@ async fn check_new_user(
.await .await
{ {
tracing::error!("Error while banning a user: {err}"); 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) { } else if let Some(re) = config.expressions.sus.is_match(&user) {
tracing::info!("@{} has been suspected because `{re}`", user.username); 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 /// banned users
async fn check_new_users( async fn check_new_users(
last_user_id: Arc<AtomicUsize>, last_user_id: Arc<AtomicUsize>,
sus_sender: Sender<ForgejoUser>,
request_client: Arc<reqwest::Client>, request_client: Arc<reqwest::Client>,
config: Arc<Config>, config: Arc<Config>,
sus_sender: Sender<ForgejoUser>,
ban_sender: Sender<ForgejoUser>,
) { ) {
match get_new_users( match get_new_users(
&request_client, &request_client,
@ -108,7 +114,7 @@ async fn check_new_users(
} }
for user in 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) => { Err(err) => {
@ -123,6 +129,7 @@ pub async fn users_fetcher(
config: Arc<Config>, config: Arc<Config>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
sus_sender: Sender<ForgejoUser>, sus_sender: Sender<ForgejoUser>,
ban_sender: Sender<ForgejoUser>,
) { ) {
let last_user_id = Arc::new(AtomicUsize::new(0)); let last_user_id = Arc::new(AtomicUsize::new(0));
let request_client = Arc::new(reqwest::Client::new()); let request_client = Arc::new(reqwest::Client::new());
@ -130,12 +137,13 @@ pub async fn users_fetcher(
tracing::info!("Starting users fetcher"); tracing::info!("Starting users fetcher");
loop { loop {
tokio::select! { tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(120)) => { _ = tokio::time::sleep(Duration::from_secs(20)) => {
tokio::spawn(check_new_users( tokio::spawn(check_new_users(
Arc::clone(&last_user_id), Arc::clone(&last_user_id),
sus_sender.clone(),
Arc::clone(&request_client), Arc::clone(&request_client),
Arc::clone(&config), Arc::clone(&config),
sus_sender.clone(),
ban_sender.clone(),
)); ));
} }
_ = cancellation_token.cancelled() => { _ = cancellation_token.cancelled() => {