Compare commits
No commits in common. "5bb6114aa77e629fcc0c12177b401ac7ab287db2" and "d12c45ed637b0ba1e42b73fe46520e65b0d0dfd9" have entirely different histories.
5bb6114aa7
...
d12c45ed63
12 changed files with 70 additions and 1571 deletions
1241
Cargo.lock
generated
1241
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"] }
|
||||
|
|
|
@ -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 = "تجاهل ⚠️"
|
|
@ -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 ⚠️"
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -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() => {
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue