Compare commits

..

No commits in common. "5bb6114aa77e629fcc0c12177b401ac7ab287db2" and "d12c45ed637b0ba1e42b73fe46520e65b0d0dfd9" have entirely different histories.

12 changed files with 70 additions and 1571 deletions

1241
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,6 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
thiserror = "2.0.2"
regex = "1.11.1"
rust-i18n = "3.1.2"
reqwest = { version = "0.12.9", default-features = false, features = [
"charset",
@ -31,4 +30,3 @@ tokio = { version = "1.41.1", default-features = false, features = [
"signal",
] }
url = { version = "2.5.3", default-features = false, features = ["serde"] }
teloxide = { version = "0.13.0", default-features = false, features = ["macros", "ctrlc_handler", "rustls"] }

View file

@ -1,22 +0,0 @@
[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 = "تجاهل ⚠️"

View file

@ -1,22 +0,0 @@
[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

@ -1,7 +1,7 @@
[toolchain]
# We use nightly in development only, the project will always be compliant with
# the latest stable release and the MSRV as defined in `Cargo.toml` file.
channel = "nightly-2024-11-01"
channel = "nightly-2024-11-12"
components = [
"rustc",
"cargo",

View file

@ -21,11 +21,8 @@ pub(crate) const DEFAULT_CONFIG_PATH: &str = "/app/forgejo-guardian.toml";
use regex::Regex;
use serde::{de, Deserialize};
use teloxide::types::ChatId;
use url::Url;
use crate::telegram_bot::Lang;
/// Deserialize a string into a `url::Url`
///
/// This will check if the url is `http` or `https` and if it is a valid url
@ -67,17 +64,6 @@ pub struct Forgejo {
pub instance: Url,
}
/// The telegram bot configuration
#[derive(Deserialize)]
pub struct Telegram {
/// Telegram bot token
pub token: String,
/// Chat to send the alert in
pub chat: ChatId,
/// Bot language
pub lang: Lang,
}
/// The expression
#[derive(Deserialize, Debug, Default)]
pub struct Expr {
@ -140,8 +126,6 @@ pub struct Exprs {
pub struct Config {
/// Configuration for the forgejo guard itself
pub forgejo: Forgejo,
/// Configuration of the telegram bot
pub telegram: Telegram,
/// The expressions, which are used to determine the actions
#[serde(default)]
pub expressions: Exprs,

View file

@ -14,9 +14,6 @@
// 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};
@ -25,36 +22,27 @@ 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(),
));
tokio::spawn(telegram_bot::start_bot(
Arc::clone(&config),
cancellation_token.clone(),
sus_receiver,
));
// TODO: Sus worker, who will receive sus users
tokio::select! {
_ = ctrl_c() => {

View file

@ -1,92 +0,0 @@
// 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

@ -1,52 +0,0 @@
// 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(())
}

View file

@ -1,80 +0,0 @@
// 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

@ -1,93 +0,0 @@
// 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;
}
}
}
}

View file

@ -32,8 +32,7 @@ use crate::{
traits::ExprChecker,
};
/// Get the new instance users, the vector may be empty if there are no new
/// users
/// Get the new instance users, the vector may be empty if there are no new users
///
/// Forgejo use intger ids for the users, so we can use the last user id to get
/// the new users.