feat: Send sus alert via telegram
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
598e5a53b1
commit
5bb6114aa7
7 changed files with 375 additions and 2 deletions
22
locales/ar-sa.toml
Normal file
22
locales/ar-sa.toml
Normal 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
22
locales/en-us.toml
Normal 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 ⚠️"
|
16
src/main.rs
16
src/main.rs
|
@ -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() => {
|
||||||
|
|
92
src/telegram_bot/callback_handler.rs
Normal file
92
src/telegram_bot/callback_handler.rs
Normal 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(())
|
||||||
|
}
|
52
src/telegram_bot/message_handler.rs
Normal file
52
src/telegram_bot/message_handler.rs
Normal 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
80
src/telegram_bot/mod.rs
Normal 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;
|
||||||
|
}
|
93
src/telegram_bot/sus_handler.rs
Normal file
93
src/telegram_bot/sus_handler.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue