From 5bb6114aa77e629fcc0c12177b401ac7ab287db2 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Thu, 14 Nov 2024 12:28:09 +0000
Subject: [PATCH] feat: Send sus alert via telegram
Signed-off-by: Awiteb
---
locales/ar-sa.toml | 22 +++++++
locales/en-us.toml | 22 +++++++
src/main.rs | 16 ++++-
src/telegram_bot/callback_handler.rs | 92 +++++++++++++++++++++++++++
src/telegram_bot/message_handler.rs | 52 ++++++++++++++++
src/telegram_bot/mod.rs | 80 ++++++++++++++++++++++++
src/telegram_bot/sus_handler.rs | 93 ++++++++++++++++++++++++++++
7 files changed, 375 insertions(+), 2 deletions(-)
create mode 100644 locales/ar-sa.toml
create mode 100644 locales/en-us.toml
create mode 100644 src/telegram_bot/callback_handler.rs
create mode 100644 src/telegram_bot/message_handler.rs
create mode 100644 src/telegram_bot/mod.rs
create mode 100644 src/telegram_bot/sus_handler.rs
diff --git a/locales/ar-sa.toml b/locales/ar-sa.toml
new file mode 100644
index 0000000..d5231ed
--- /dev/null
+++ b/locales/ar-sa.toml
@@ -0,0 +1,22 @@
+[messages]
+help_start = "مرحبًا، أنا حارس فورجيو، سأرسل لك إشعارًا حول المستخدمين المشبوهين، وسأنتظر موافقتك أو رفضك لحظرهم.\n\nالشيفرة المصدرية الخاصة بي: https://git.4rs.nl/awiteb/forgejo-guardian"
+sus_alert = """تم اكتشاف مستخدم مشبوه! 🚨
+• معرف المستخدم: %{user_id}
+• اسم المستخدم: %{username}
+• الاسم الكامل: %{full_name}
+• النبذة: %{bio}
+• الموقع: %{website}
+• الملف التعريفي: %{profile}
+
+هل تريد حظر هذا المستخدم؟
+"""
+ban_success = "تم حظر المستخدم بنجاح ⛔"
+ban_failed = "فشل حظر المستخدم! ⚠️"
+ban_denied = "تم تجاهل المستخدم ⚠️"
+
+[words]
+not_found = "غير موجود"
+
+[buttons]
+ban = "حظر ⛔"
+ignore = "تجاهل ⚠️"
diff --git a/locales/en-us.toml b/locales/en-us.toml
new file mode 100644
index 0000000..5b6f042
--- /dev/null
+++ b/locales/en-us.toml
@@ -0,0 +1,22 @@
+[messages]
+help_start = "Hi, I'm forgejo-guardian, I'll send you a notification about suspicious users, and I'll wait for your approval or rejection to ban them.\n\nMy source code: https://git.4rs.nl/awiteb/forgejo-guardian"
+sus_alert = """Suspicious user detected! 🚨
+• User ID: %{user_id}
+• Username: %{username}
+• Full name: %{full_name}
+• Bio: %{bio}
+• Website: %{website}
+• Profile: %{profile}
+
+Do you want to ban this user?
+"""
+ban_success = "User has been banned successfully ⛔"
+ban_failed = "Failed to ban the user! ⚠️"
+ban_denied = "User has been ignored ⚠️"
+
+[words]
+not_found = "Not found"
+
+[buttons]
+ban = "Ban ⛔"
+ignore = "Ignore ⚠️"
diff --git a/src/main.rs b/src/main.rs
index cb2faa7..94c6c14 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+#[macro_use]
+extern crate rust_i18n;
+
use std::{process::ExitCode, sync::Arc, time::Duration};
use tokio::{signal::ctrl_c, sync};
@@ -22,27 +25,36 @@ use tokio_util::sync::CancellationToken;
pub mod config;
pub mod error;
pub mod forgejo_api;
+pub mod telegram_bot;
pub mod traits;
pub mod users_fetcher;
pub mod utils;
+i18n!("locales", fallback = "en-us");
+
async fn try_main() -> error::GuardResult<()> {
let config = Arc::new(utils::get_config()?);
let cancellation_token = CancellationToken::new();
// Suspicious users are sent and received in this channel, users who meet the
// `alert` expressions
- let (sus_sender, _sus_receiver) = sync::mpsc::channel::(100);
+ let (sus_sender, sus_receiver) = sync::mpsc::channel::(100);
tracing::info!("The instance: {}", config.forgejo.instance);
tracing::debug!("The config exprs: {:#?}", config.expressions);
+ rust_i18n::set_locale(config.telegram.lang.as_str());
+
tokio::spawn(users_fetcher::users_fetcher(
Arc::clone(&config),
cancellation_token.clone(),
sus_sender.clone(),
));
- // TODO: Sus worker, who will receive sus users
+ tokio::spawn(telegram_bot::start_bot(
+ Arc::clone(&config),
+ cancellation_token.clone(),
+ sus_receiver,
+ ));
tokio::select! {
_ = ctrl_c() => {
diff --git a/src/telegram_bot/callback_handler.rs b/src/telegram_bot/callback_handler.rs
new file mode 100644
index 0000000..69a8adf
--- /dev/null
+++ b/src/telegram_bot/callback_handler.rs
@@ -0,0 +1,92 @@
+// Simple Forgejo instance guardian, banning users and alerting admins based on
+// certain regular expressions. Copyright (C) 2024 Awiteb
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::sync::Arc;
+
+use reqwest::Client;
+use teloxide::{
+ prelude::*,
+ types::{
+ CallbackQuery,
+ InlineKeyboardButton,
+ InlineKeyboardButtonKind,
+ InlineKeyboardMarkup,
+ MaybeInaccessibleMessage,
+ },
+};
+
+use crate::{config::Config, forgejo_api};
+
+/// Inline keyboard with a single button that links to the Forgejo Guardian repository.
+fn source_inline_keyboard(text: &str) -> InlineKeyboardMarkup {
+ InlineKeyboardMarkup::new([[InlineKeyboardButton::new(
+ text,
+ InlineKeyboardButtonKind::Url(
+ url::Url::parse("https://git.4rs.nl/awiteb/forgejo-guardian").expect("Valid url"),
+ ),
+ )]])
+}
+
+/// Handle callback queries from the inline keyboard.
+pub async fn callback_handler(
+ bot: Bot,
+ callback_query: CallbackQuery,
+ config: Arc,
+) -> ResponseResult<()> {
+ let Some(callback_data) = callback_query.data else {
+ return Ok(());
+ };
+
+ let Some((command, data)) = callback_data.split_once(' ') else {
+ // Invalid callback data
+ return Ok(());
+ };
+
+ match command {
+ "b" => {
+ // ban the user
+ let button_text = if forgejo_api::ban_user(
+ &Client::new(),
+ &config.forgejo.instance,
+ &config.forgejo.token,
+ data,
+ )
+ .await
+ .is_ok()
+ {
+ t!("messages.ban_success")
+ } else {
+ t!("messages.ban_failed")
+ };
+
+ if let Some(MaybeInaccessibleMessage::Regular(msg)) = callback_query.message {
+ bot.edit_message_reply_markup(msg.chat.id, msg.id)
+ .reply_markup(source_inline_keyboard(&button_text))
+ .await?;
+ }
+ }
+ "ignore" => {
+ if let Some(MaybeInaccessibleMessage::Regular(msg)) = callback_query.message {
+ bot.edit_message_reply_markup(msg.chat.id, msg.id)
+ .reply_markup(source_inline_keyboard(&t!("messages.ban_denied")))
+ .await?;
+ }
+ }
+ _ => {}
+ };
+
+ Ok(())
+}
diff --git a/src/telegram_bot/message_handler.rs b/src/telegram_bot/message_handler.rs
new file mode 100644
index 0000000..e63b9c0
--- /dev/null
+++ b/src/telegram_bot/message_handler.rs
@@ -0,0 +1,52 @@
+// Simple Forgejo instance guardian, banning users and alerting admins based on
+// certain regular expressions. Copyright (C) 2024 Awiteb
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use teloxide::{
+ prelude::*,
+ types::{Me, ReplyParameters},
+ utils::command::BotCommands,
+};
+
+#[derive(BotCommands, Clone, Debug, PartialEq)]
+#[command(rename_rule = "lowercase")]
+enum Command {
+ Start,
+ #[command(aliases = ["h", "?"])]
+ Help,
+}
+
+/// Help and start commands handler
+pub async fn help_start_handler(bot: &Bot, msg: &Message) -> ResponseResult<()> {
+ bot.send_message(msg.chat.id, t!("messages.help_start"))
+ .reply_parameters(ReplyParameters::new(msg.id))
+ .await?;
+
+ Ok(())
+}
+
+/// Handle text messages
+pub async fn text_handler(bot: Bot, me: Me, msg: Message) -> ResponseResult<()> {
+ let text = msg.text().expect("Is a text handler");
+ let Ok(command) = Command::parse(text, me.username()) else {
+ return Ok(());
+ };
+
+ match command {
+ Command::Help | Command::Start => help_start_handler(&bot, &msg).await?,
+ };
+
+ Ok(())
+}
diff --git a/src/telegram_bot/mod.rs b/src/telegram_bot/mod.rs
new file mode 100644
index 0000000..86dc1aa
--- /dev/null
+++ b/src/telegram_bot/mod.rs
@@ -0,0 +1,80 @@
+// Simple Forgejo instance guardian, banning users and alerting admins based on
+// certain regular expressions. Copyright (C) 2024 Awiteb
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Telegram bot module, to alert the admins of the bot of any suspicious users.
+
+mod callback_handler;
+mod message_handler;
+mod sus_handler;
+
+use std::sync::Arc;
+
+use callback_handler::callback_handler;
+use serde::Deserialize;
+use teloxide::{dispatching::UpdateFilterExt, prelude::*};
+use tokio::sync::mpsc::Receiver;
+use tokio_util::sync::CancellationToken;
+
+use crate::{config::Config, forgejo_api::ForgejoUser};
+
+/// Language of the telegram bot
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum Lang {
+ EnUs,
+ ArSa,
+}
+
+impl Lang {
+ /// Get the language as a string
+ pub fn as_str(&self) -> &str {
+ match self {
+ Lang::EnUs => "en-us",
+ Lang::ArSa => "ar-sa",
+ }
+ }
+}
+
+/// Start the telegram bot
+pub async fn start_bot(
+ config: Arc,
+ cancellation_token: CancellationToken,
+ sus_receiver: Receiver,
+) {
+ tracing::info!("Starting the telegram bot");
+
+ let bot = Bot::new(&config.telegram.token);
+ let handler = dptree::entry()
+ .branch(
+ Update::filter_message()
+ .branch(Message::filter_text().endpoint(message_handler::text_handler)),
+ )
+ .branch(Update::filter_callback_query().endpoint(callback_handler));
+
+ tokio::spawn(sus_handler::sus_users_handler(
+ bot.clone(),
+ Arc::clone(&config),
+ cancellation_token,
+ sus_receiver,
+ ));
+
+ Dispatcher::builder(bot, handler)
+ .dependencies(dptree::deps![config])
+ .enable_ctrlc_handler()
+ .build()
+ .dispatch()
+ .await;
+}
diff --git a/src/telegram_bot/sus_handler.rs b/src/telegram_bot/sus_handler.rs
new file mode 100644
index 0000000..a1d0532
--- /dev/null
+++ b/src/telegram_bot/sus_handler.rs
@@ -0,0 +1,93 @@
+// Simple Forgejo instance guardian, banning users and alerting admins based on
+// certain regular expressions. Copyright (C) 2024 Awiteb
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{borrow::Cow, sync::Arc};
+
+use teloxide::{
+ prelude::*,
+ types::{InlineKeyboardButton, InlineKeyboardButtonKind, InlineKeyboardMarkup, InputFile},
+};
+use tokio::sync::mpsc::Receiver;
+use tokio_util::sync::CancellationToken;
+
+use crate::{config::Config, forgejo_api::ForgejoUser};
+
+/// Create an inline keyboard of the suspicious user
+fn make_sus_inline_keyboard(sus_user: &ForgejoUser) -> InlineKeyboardMarkup {
+ let button = |text: &str, callback: String| {
+ InlineKeyboardButton::new(text, InlineKeyboardButtonKind::CallbackData(callback))
+ };
+
+ InlineKeyboardMarkup::new([[
+ button(
+ t!("buttons.ban").as_ref(),
+ format!("b {}", sus_user.username),
+ ),
+ button(t!("buttons.ignore").as_ref(), "ignore -".to_owned()),
+ ]])
+}
+
+fn not_found_if_empty(text: &str) -> Cow<'_, str> {
+ if text.is_empty() {
+ t!("words.not_found")
+ } else {
+ Cow::Borrowed(text)
+ }
+}
+
+/// Send a suspicious user alert to the admins
+pub async fn send_sus_alert(
+ bot: &Bot,
+ sus_user: ForgejoUser,
+ config: &Config,
+) -> ResponseResult<()> {
+ let keyboard = make_sus_inline_keyboard(&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,
+ ))
+ .reply_markup(keyboard)
+ .await?;
+
+ Ok(())
+}
+
+/// Handle the suspicious users
+pub async fn sus_users_handler(
+ bot: Bot,
+ config: Arc,
+ cancellation_token: CancellationToken,
+ mut sus_receiver: Receiver,
+) {
+ loop {
+ tokio::select! {
+ Some(sus_user) = sus_receiver.recv() => {
+ send_sus_alert(&bot, sus_user, &config).await.ok();
+ }
+ _ = cancellation_token.cancelled() => {
+ tracing::info!("sus users handler has been stopped successfully.");
+ break;
+ }
+ }
+ }
+}