feat: Send sus alert via telegram

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb 2024-11-14 12:28:09 +00:00
parent 598e5a53b1
commit 5bb6114aa7
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
7 changed files with 375 additions and 2 deletions

22
locales/ar-sa.toml Normal file
View file

@ -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 = "تجاهل ⚠️"

22
locales/en-us.toml Normal file
View file

@ -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 ⚠️"

View file

@ -14,6 +14,9 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl.txt>. // along with this program. If not, see <https://gnu.org/licenses/agpl.txt>.
#[macro_use]
extern crate rust_i18n;
use std::{process::ExitCode, sync::Arc, time::Duration}; use std::{process::ExitCode, sync::Arc, time::Duration};
use tokio::{signal::ctrl_c, sync}; use tokio::{signal::ctrl_c, sync};
@ -22,27 +25,36 @@ use tokio_util::sync::CancellationToken;
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod forgejo_api; pub mod forgejo_api;
pub mod telegram_bot;
pub mod traits; pub mod traits;
pub mod users_fetcher; pub mod users_fetcher;
pub mod utils; pub mod utils;
i18n!("locales", fallback = "en-us");
async fn try_main() -> error::GuardResult<()> { async fn try_main() -> error::GuardResult<()> {
let config = Arc::new(utils::get_config()?); let config = Arc::new(utils::get_config()?);
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
// 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);
tracing::info!("The instance: {}", config.forgejo.instance); tracing::info!("The instance: {}", config.forgejo.instance);
tracing::debug!("The config exprs: {:#?}", config.expressions); tracing::debug!("The config exprs: {:#?}", config.expressions);
rust_i18n::set_locale(config.telegram.lang.as_str());
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.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! { tokio::select! {
_ = ctrl_c() => { _ = ctrl_c() => {

View file

@ -0,0 +1,92 @@
// Simple Forgejo instance guardian, banning users and alerting admins based on
// certain regular expressions. Copyright (C) 2024 Awiteb <a@4rs.nl>
//
// 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 <https://gnu.org/licenses/agpl.txt>.
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<Config>,
) -> 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(())
}

View file

@ -0,0 +1,52 @@
// Simple Forgejo instance guardian, banning users and alerting admins based on
// certain regular expressions. Copyright (C) 2024 Awiteb <a@4rs.nl>
//
// 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 <https://gnu.org/licenses/agpl.txt>.
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(())
}

80
src/telegram_bot/mod.rs Normal file
View file

@ -0,0 +1,80 @@
// Simple Forgejo instance guardian, banning users and alerting admins based on
// certain regular expressions. Copyright (C) 2024 Awiteb <a@4rs.nl>
//
// 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 <https://gnu.org/licenses/agpl.txt>.
//! 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<Config>,
cancellation_token: CancellationToken,
sus_receiver: Receiver<ForgejoUser>,
) {
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;
}

View file

@ -0,0 +1,93 @@
// Simple Forgejo instance guardian, banning users and alerting admins based on
// certain regular expressions. Copyright (C) 2024 Awiteb <a@4rs.nl>
//
// 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 <https://gnu.org/licenses/agpl.txt>.
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<Config>,
cancellation_token: CancellationToken,
mut sus_receiver: Receiver<ForgejoUser>,
) {
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;
}
}
}
}