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
|
||||
// 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 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::<forgejo_api::ForgejoUser>(100);
|
||||
let (sus_sender, sus_receiver) = sync::mpsc::channel::<forgejo_api::ForgejoUser>(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() => {
|
||||
|
|
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