feat: Reason for banned and suspicious
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
62ca71a140
commit
c96b859931
9 changed files with 162 additions and 51 deletions
|
@ -8,6 +8,7 @@ sus_alert = """تم اكتشاف مستخدم مشبوه! 🚨
|
||||||
• النبذة: %{bio}
|
• النبذة: %{bio}
|
||||||
• الموقع: %{website}
|
• الموقع: %{website}
|
||||||
• الملف التعريفي: %{profile}
|
• الملف التعريفي: %{profile}
|
||||||
|
• السبب: %{reason}
|
||||||
|
|
||||||
هل تريد حظر هذا المستخدم؟
|
هل تريد حظر هذا المستخدم؟
|
||||||
"""
|
"""
|
||||||
|
@ -19,6 +20,7 @@ ban_notify = """تم حظر مستخدم ⛔
|
||||||
• النبذة: %{bio}
|
• النبذة: %{bio}
|
||||||
• الموقع: %{website}
|
• الموقع: %{website}
|
||||||
• الملف التعريفي: %{profile}
|
• الملف التعريفي: %{profile}
|
||||||
|
• السبب: %{reason}
|
||||||
"""
|
"""
|
||||||
ban_success = "تم حظر المستخدم بنجاح ⛔"
|
ban_success = "تم حظر المستخدم بنجاح ⛔"
|
||||||
ban_failed = "فشل حظر المستخدم! ⚠️"
|
ban_failed = "فشل حظر المستخدم! ⚠️"
|
||||||
|
|
|
@ -8,6 +8,7 @@ sus_alert = """Suspicious user detected! 🚨
|
||||||
• Bio: %{bio}
|
• Bio: %{bio}
|
||||||
• Website: %{website}
|
• Website: %{website}
|
||||||
• Profile: %{profile}
|
• Profile: %{profile}
|
||||||
|
• Reason: %{reason}
|
||||||
|
|
||||||
Do you want to ban this user?
|
Do you want to ban this user?
|
||||||
"""
|
"""
|
||||||
|
@ -19,6 +20,7 @@ ban_notify = """User has been banned ⛔
|
||||||
• Bio: %{bio}
|
• Bio: %{bio}
|
||||||
• Website: %{website}
|
• Website: %{website}
|
||||||
• Profile: %{profile}
|
• Profile: %{profile}
|
||||||
|
• Reason: %{reason}
|
||||||
"""
|
"""
|
||||||
ban_success = "User has been banned successfully ⛔"
|
ban_success = "User has been banned successfully ⛔"
|
||||||
ban_failed = "Failed to ban the user! ⚠️"
|
ban_failed = "Failed to ban the user! ⚠️"
|
||||||
|
|
|
@ -8,6 +8,7 @@ sus_alert = """Обнаружен подозрительный пользова
|
||||||
• Био: %{bio}
|
• Био: %{bio}
|
||||||
• Вебсайт: %{website}
|
• Вебсайт: %{website}
|
||||||
• Профиль: %{profile}
|
• Профиль: %{profile}
|
||||||
|
• Причина: %{reason}
|
||||||
|
|
||||||
Хотите забанить этого пользователя?
|
Хотите забанить этого пользователя?
|
||||||
"""
|
"""
|
||||||
|
@ -19,6 +20,7 @@ ban_notify = """Пользователь заблокирован ⛔
|
||||||
• Био: %{bio}
|
• Био: %{bio}
|
||||||
• Вебсайт: %{website}
|
• Вебсайт: %{website}
|
||||||
• Профиль: %{profile}
|
• Профиль: %{profile}
|
||||||
|
• Причина: %{reason}
|
||||||
"""
|
"""
|
||||||
ban_success = "Пользователь успешно забанен ⛔"
|
ban_success = "Пользователь успешно забанен ⛔"
|
||||||
ban_failed = "Не удалось забанить пользователя! ⚠️"
|
ban_failed = "Не удалось забанить пользователя! ⚠️"
|
||||||
|
|
128
src/config.rs
128
src/config.rs
|
@ -19,13 +19,24 @@ pub(crate) const CONFIG_PATH_ENV: &str = "FORGEJO_GUARDIAN_CONFIG";
|
||||||
/// Defult config path location
|
/// Defult config path location
|
||||||
pub(crate) const DEFAULT_CONFIG_PATH: &str = "/app/forgejo-guardian.toml";
|
pub(crate) const DEFAULT_CONFIG_PATH: &str = "/app/forgejo-guardian.toml";
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{de, Deserialize};
|
use serde::{de, Deserialize};
|
||||||
use teloxide::types::ChatId;
|
use teloxide::types::ChatId;
|
||||||
|
use toml::Value;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::telegram_bot::Lang;
|
use crate::telegram_bot::Lang;
|
||||||
|
|
||||||
|
/// Function to create a custom error for deserialization from its string
|
||||||
|
fn custom_de_err<'de, D>(err: impl ToString) -> D::Error
|
||||||
|
where
|
||||||
|
D: de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
de::Error::custom(err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Deserialize a string into a `url::Url`
|
/// Deserialize a string into a `url::Url`
|
||||||
///
|
///
|
||||||
/// This will check if the url is `http` or `https` and if it is a valid url
|
/// This will check if the url is `http` or `https` and if it is a valid url
|
||||||
|
@ -33,8 +44,7 @@ fn deserialize_str_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
|
||||||
where
|
where
|
||||||
D: de::Deserializer<'de>,
|
D: de::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let url = Url::parse(&String::deserialize(deserializer)?)
|
let url = Url::parse(&String::deserialize(deserializer)?).map_err(custom_de_err::<'de, D>)?;
|
||||||
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
||||||
if url.scheme() != "http" && url.scheme() != "https" {
|
if url.scheme() != "http" && url.scheme() != "https" {
|
||||||
return Err(de::Error::custom("URL scheme must be http or https"));
|
return Err(de::Error::custom("URL scheme must be http or https"));
|
||||||
}
|
}
|
||||||
|
@ -42,15 +52,69 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize a vector of strings into a vector of `regex::Regex`
|
/// Deserialize a vector of strings into a vector of `regex::Regex`
|
||||||
fn deserialize_regex_vec<'de, D>(deserializer: D) -> Result<Vec<Regex>, D::Error>
|
fn deserialize_regex_reason<'de, D>(deserializer: D) -> Result<Vec<RegexReason>, D::Error>
|
||||||
where
|
where
|
||||||
D: de::Deserializer<'de>,
|
D: de::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
Vec::<String>::deserialize(deserializer)?
|
let Ok(json_value) = Vec::<Value>::deserialize(deserializer) else {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"expected an array of strings or tables with the keys `re` and optional `reason`",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
json_value
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| Regex::new(&s))
|
.map(|value| {
|
||||||
.collect::<Result<_, _>>()
|
if let Value::String(re) = value {
|
||||||
.map_err(|e| de::Error::custom(e.to_string()))
|
Ok(RegexReason::new(
|
||||||
|
re.parse().map_err(custom_de_err::<'de, D>)?,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
} else if let Value::Table(table) = value {
|
||||||
|
let re = table
|
||||||
|
.get("re")
|
||||||
|
.map(|re| {
|
||||||
|
re.as_str().map(String::from).ok_or_else(|| {
|
||||||
|
<D::Error as de::Error>::custom(format!(
|
||||||
|
"expected a string value for `re`, found `{re}`"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
<D::Error as de::Error>::custom(
|
||||||
|
"The table must contain a `re` key with a string value",
|
||||||
|
)
|
||||||
|
})??;
|
||||||
|
let reason = table
|
||||||
|
.get("reason")
|
||||||
|
.map(|reason| {
|
||||||
|
reason.as_str().map(String::from).ok_or_else(|| {
|
||||||
|
<D::Error as de::Error>::custom(format!(
|
||||||
|
"expected a string value for `reason`, found `{reason}`"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
// Warn for unused keys
|
||||||
|
for key in table.keys() {
|
||||||
|
if !["re", "reason"].contains(&key.as_str()) {
|
||||||
|
tracing::warn!("Unused key `{key}` in the configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RegexReason::new(
|
||||||
|
re.parse().map_err(custom_de_err::<'de, D>)?,
|
||||||
|
reason,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(de::Error::custom(format!(
|
||||||
|
"unexpected value in the regex list, expected a string or a table with `re` \
|
||||||
|
(string) and optional `reason` (string), found `{value}`"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The forgejo config of the guard
|
/// The forgejo config of the guard
|
||||||
|
@ -81,44 +145,53 @@ pub struct Telegram {
|
||||||
pub lang: Lang,
|
pub lang: Lang,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The regular expression with the reason
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RegexReason {
|
||||||
|
/// The regular expression
|
||||||
|
pub re: Regex,
|
||||||
|
/// Optional reason
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The expression
|
/// The expression
|
||||||
#[derive(Deserialize, Debug, Default)]
|
#[derive(Deserialize, Debug, Default)]
|
||||||
pub struct Expr {
|
pub struct Expr {
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the username
|
/// present in the username
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub usernames: Vec<Regex>,
|
pub usernames: Vec<RegexReason>,
|
||||||
|
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the user full_name
|
/// present in the user full_name
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub full_names: Vec<Regex>,
|
pub full_names: Vec<RegexReason>,
|
||||||
|
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the user biography
|
/// present in the user biography
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub biographies: Vec<Regex>,
|
pub biographies: Vec<RegexReason>,
|
||||||
|
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the user email
|
/// present in the user email
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub emails: Vec<Regex>,
|
pub emails: Vec<RegexReason>,
|
||||||
|
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the user website
|
/// present in the user website
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub websites: Vec<Regex>,
|
pub websites: Vec<RegexReason>,
|
||||||
|
|
||||||
/// The regular expressions that the action will be performed if they are
|
/// The regular expressions that the action will be performed if they are
|
||||||
/// present in the user location
|
/// present in the user location
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_regex_vec")]
|
#[serde(deserialize_with = "deserialize_regex_reason")]
|
||||||
pub locations: Vec<Regex>,
|
pub locations: Vec<RegexReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the expressions
|
/// the expressions
|
||||||
|
@ -155,3 +228,20 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub expressions: Exprs,
|
pub expressions: Exprs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RegexReason {
|
||||||
|
/// Create a new `RegexReason` instance
|
||||||
|
fn new(re: Regex, reason: Option<String>) -> Self {
|
||||||
|
Self { re, reason }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RegexReason {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.re).ok();
|
||||||
|
if let Some(ref reason) = self.reason {
|
||||||
|
write!(f, " ({reason})").ok();
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,10 +37,12 @@ async fn try_main() -> error::GuardResult<()> {
|
||||||
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, config::RegexReason)>(100);
|
||||||
// Banned users (already banned) are sent and received in this channel, this
|
// Banned users (already banned) are sent and received in this channel, this
|
||||||
// to alert the admins on Telegram if `ban_alert` is set to true
|
// to alert the admins on Telegram if `ban_alert` is set to true
|
||||||
let (ban_sender, ban_receiver) = sync::mpsc::channel::<forgejo_api::ForgejoUser>(100);
|
let (ban_sender, ban_receiver) =
|
||||||
|
sync::mpsc::channel::<(forgejo_api::ForgejoUser, config::RegexReason)>(100);
|
||||||
|
|
||||||
tracing::info!("The instance: {}", config.forgejo.instance);
|
tracing::info!("The instance: {}", config.forgejo.instance);
|
||||||
tracing::info!("Dry run: {}", config.dry_run);
|
tracing::info!("Dry run: {}", config.dry_run);
|
||||||
|
|
|
@ -28,7 +28,10 @@ use teloxide::{dispatching::UpdateFilterExt, prelude::*};
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{config::Config, forgejo_api::ForgejoUser};
|
use crate::{
|
||||||
|
config::{Config, RegexReason},
|
||||||
|
forgejo_api::ForgejoUser,
|
||||||
|
};
|
||||||
|
|
||||||
/// Language of the telegram bot
|
/// Language of the telegram bot
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -54,8 +57,8 @@ impl Lang {
|
||||||
pub async fn start_bot(
|
pub async fn start_bot(
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
cancellation_token: CancellationToken,
|
cancellation_token: CancellationToken,
|
||||||
sus_receiver: Receiver<ForgejoUser>,
|
sus_receiver: Receiver<(ForgejoUser, RegexReason)>,
|
||||||
ban_receiver: Receiver<ForgejoUser>,
|
ban_receiver: Receiver<(ForgejoUser, RegexReason)>,
|
||||||
) {
|
) {
|
||||||
tracing::info!("Starting the telegram bot");
|
tracing::info!("Starting the telegram bot");
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,10 @@ use teloxide::{
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{config::Config, forgejo_api::ForgejoUser};
|
use crate::{
|
||||||
|
config::{Config, RegexReason},
|
||||||
|
forgejo_api::ForgejoUser,
|
||||||
|
};
|
||||||
|
|
||||||
/// Create an inline keyboard of the suspicious user
|
/// Create an inline keyboard of the suspicious user
|
||||||
fn make_sus_inline_keyboard(sus_user: &ForgejoUser) -> InlineKeyboardMarkup {
|
fn make_sus_inline_keyboard(sus_user: &ForgejoUser) -> InlineKeyboardMarkup {
|
||||||
|
@ -49,7 +52,7 @@ fn not_found_if_empty(text: &str) -> Cow<'_, str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a user details message
|
/// Generate a user details message
|
||||||
fn user_details(msg: &str, user: &ForgejoUser) -> String {
|
fn user_details(msg: &str, user: &ForgejoUser, re: &RegexReason) -> String {
|
||||||
t!(
|
t!(
|
||||||
msg,
|
msg,
|
||||||
user_id = user.id,
|
user_id = user.id,
|
||||||
|
@ -59,6 +62,10 @@ fn user_details(msg: &str, user: &ForgejoUser) -> String {
|
||||||
bio = not_found_if_empty(&user.biography),
|
bio = not_found_if_empty(&user.biography),
|
||||||
website = not_found_if_empty(&user.website),
|
website = not_found_if_empty(&user.website),
|
||||||
profile = user.html_url,
|
profile = user.html_url,
|
||||||
|
reason = re
|
||||||
|
.reason
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| t!("words.not_found").to_string()),
|
||||||
)
|
)
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
@ -66,12 +73,13 @@ fn user_details(msg: &str, user: &ForgejoUser) -> String {
|
||||||
/// Send a suspicious user alert to the admins
|
/// Send a suspicious user alert to the admins
|
||||||
pub async fn send_sus_alert(
|
pub async fn send_sus_alert(
|
||||||
bot: &Bot,
|
bot: &Bot,
|
||||||
|
re: &RegexReason,
|
||||||
sus_user: ForgejoUser,
|
sus_user: ForgejoUser,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> ResponseResult<()> {
|
) -> ResponseResult<()> {
|
||||||
let keyboard = make_sus_inline_keyboard(&sus_user);
|
let keyboard = make_sus_inline_keyboard(&sus_user);
|
||||||
|
|
||||||
let caption = user_details("messages.sus_alert", &sus_user);
|
let caption = user_details("messages.sus_alert", &sus_user, re);
|
||||||
bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url))
|
bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url))
|
||||||
.caption(caption)
|
.caption(caption)
|
||||||
.reply_markup(keyboard)
|
.reply_markup(keyboard)
|
||||||
|
@ -83,10 +91,11 @@ pub async fn send_sus_alert(
|
||||||
/// Send a ban notification to the admins chat
|
/// Send a ban notification to the admins chat
|
||||||
pub async fn send_ban_notify(
|
pub async fn send_ban_notify(
|
||||||
bot: &Bot,
|
bot: &Bot,
|
||||||
|
re: &RegexReason,
|
||||||
sus_user: ForgejoUser,
|
sus_user: ForgejoUser,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> ResponseResult<()> {
|
) -> ResponseResult<()> {
|
||||||
let caption = user_details("messages.ban_notify", &sus_user);
|
let caption = user_details("messages.ban_notify", &sus_user, re);
|
||||||
bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url))
|
bot.send_photo(config.telegram.chat, InputFile::url(sus_user.avatar_url))
|
||||||
.caption(caption)
|
.caption(caption)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -99,16 +108,16 @@ pub async fn users_handler(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
cancellation_token: CancellationToken,
|
cancellation_token: CancellationToken,
|
||||||
mut sus_receiver: Receiver<ForgejoUser>,
|
mut sus_receiver: Receiver<(ForgejoUser, RegexReason)>,
|
||||||
mut ban_receiver: Receiver<ForgejoUser>,
|
mut ban_receiver: Receiver<(ForgejoUser, RegexReason)>,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
Some(sus_user) = sus_receiver.recv() => {
|
Some((sus_user, re)) = sus_receiver.recv() => {
|
||||||
send_sus_alert(&bot, sus_user, &config).await.ok();
|
send_sus_alert(&bot, &re, sus_user, &config).await.ok();
|
||||||
}
|
}
|
||||||
Some(banned_user) = ban_receiver.recv() => {
|
Some((banned_user, re)) = ban_receiver.recv() => {
|
||||||
send_ban_notify(&bot, banned_user, &config).await.ok();
|
send_ban_notify(&bot, &re, banned_user, &config).await.ok();
|
||||||
}
|
}
|
||||||
_ = cancellation_token.cancelled() => {
|
_ = cancellation_token.cancelled() => {
|
||||||
tracing::info!("sus users handler has been stopped successfully.");
|
tracing::info!("sus users handler has been stopped successfully.");
|
||||||
|
|
|
@ -14,22 +14,23 @@
|
||||||
// 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>.
|
||||||
|
|
||||||
use regex::Regex;
|
use crate::{
|
||||||
|
config::{Expr, RegexReason},
|
||||||
use crate::{config::Expr, forgejo_api::ForgejoUser};
|
forgejo_api::ForgejoUser,
|
||||||
|
};
|
||||||
|
|
||||||
/// Trait for checking if a user matches one of the expressions
|
/// Trait for checking if a user matches one of the expressions
|
||||||
pub trait ExprChecker {
|
pub trait ExprChecker {
|
||||||
/// Returns the first matching expression, if any
|
/// Returns the first matching expression, if any
|
||||||
fn is_match(&self, user: &ForgejoUser) -> Option<Regex>;
|
fn is_match(&self, user: &ForgejoUser) -> Option<RegexReason>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExprChecker for Expr {
|
impl ExprChecker for Expr {
|
||||||
fn is_match<'a>(&'a self, user: &ForgejoUser) -> Option<Regex> {
|
fn is_match<'a>(&'a self, user: &ForgejoUser) -> Option<RegexReason> {
|
||||||
let one_of = |hay: &str, exprs: &'a Vec<Regex>| {
|
let one_of = |hay: &str, exprs: &'a Vec<RegexReason>| {
|
||||||
exprs
|
exprs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|re| hay.split('\n').any(|line| re.is_match(line.trim())))
|
.find(|re| hay.split('\n').any(|line| re.re.is_match(line.trim())))
|
||||||
};
|
};
|
||||||
[
|
[
|
||||||
one_of(&user.username, &self.usernames),
|
one_of(&user.username, &self.usernames),
|
||||||
|
|
|
@ -26,7 +26,7 @@ use tokio::sync::mpsc::Sender;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::{Config, RegexReason},
|
||||||
error::GuardResult,
|
error::GuardResult,
|
||||||
forgejo_api::{self, get_users, ForgejoUser},
|
forgejo_api::{self, get_users, ForgejoUser},
|
||||||
traits::ExprChecker,
|
traits::ExprChecker,
|
||||||
|
@ -58,15 +58,15 @@ async fn check_new_user(
|
||||||
user: ForgejoUser,
|
user: ForgejoUser,
|
||||||
request_client: &reqwest::Client,
|
request_client: &reqwest::Client,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
sus_sender: &Sender<ForgejoUser>,
|
sus_sender: &Sender<(ForgejoUser, RegexReason)>,
|
||||||
ban_sender: &Sender<ForgejoUser>,
|
ban_sender: &Sender<(ForgejoUser, RegexReason)>,
|
||||||
) {
|
) {
|
||||||
if let Some(re) = config.expressions.ban.is_match(&user) {
|
if let Some(re) = config.expressions.ban.is_match(&user) {
|
||||||
tracing::info!("@{} has been banned because `{re}`", user.username);
|
tracing::info!("@{} has been banned because `{re}`", user.username);
|
||||||
if config.dry_run {
|
if config.dry_run {
|
||||||
// If it's a dry run, we don't need to ban the user
|
// If it's a dry run, we don't need to ban the user
|
||||||
if config.telegram.ban_alert {
|
if config.telegram.ban_alert {
|
||||||
ban_sender.send(user).await.ok();
|
ban_sender.send((user, re)).await.ok();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -81,11 +81,11 @@ async fn check_new_user(
|
||||||
{
|
{
|
||||||
tracing::error!("Error while banning a user: {err}");
|
tracing::error!("Error while banning a user: {err}");
|
||||||
} else if config.telegram.ban_alert {
|
} else if config.telegram.ban_alert {
|
||||||
ban_sender.send(user).await.ok();
|
ban_sender.send((user, re)).await.ok();
|
||||||
}
|
}
|
||||||
} else if let Some(re) = config.expressions.sus.is_match(&user) {
|
} else if let Some(re) = config.expressions.sus.is_match(&user) {
|
||||||
tracing::info!("@{} has been suspected because `{re}`", user.username);
|
tracing::info!("@{} has been suspected because `{re}`", user.username);
|
||||||
sus_sender.send(user).await.ok();
|
sus_sender.send((user, re)).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +95,8 @@ async fn check_new_users(
|
||||||
last_user_id: Arc<AtomicUsize>,
|
last_user_id: Arc<AtomicUsize>,
|
||||||
request_client: Arc<reqwest::Client>,
|
request_client: Arc<reqwest::Client>,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
sus_sender: Sender<ForgejoUser>,
|
sus_sender: Sender<(ForgejoUser, RegexReason)>,
|
||||||
ban_sender: Sender<ForgejoUser>,
|
ban_sender: Sender<(ForgejoUser, RegexReason)>,
|
||||||
) {
|
) {
|
||||||
let is_first_fetch = last_user_id.load(Ordering::Relaxed) == 0;
|
let is_first_fetch = last_user_id.load(Ordering::Relaxed) == 0;
|
||||||
match get_new_users(
|
match get_new_users(
|
||||||
|
@ -135,8 +135,8 @@ async fn check_new_users(
|
||||||
pub async fn users_fetcher(
|
pub async fn users_fetcher(
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
cancellation_token: CancellationToken,
|
cancellation_token: CancellationToken,
|
||||||
sus_sender: Sender<ForgejoUser>,
|
sus_sender: Sender<(ForgejoUser, RegexReason)>,
|
||||||
ban_sender: Sender<ForgejoUser>,
|
ban_sender: Sender<(ForgejoUser, RegexReason)>,
|
||||||
) {
|
) {
|
||||||
let last_user_id = Arc::new(AtomicUsize::new(0));
|
let last_user_id = Arc::new(AtomicUsize::new(0));
|
||||||
let request_client = Arc::new(reqwest::Client::new());
|
let request_client = Arc::new(reqwest::Client::new());
|
||||||
|
|
Loading…
Reference in a new issue