Compare commits

...

7 Commits

10 changed files with 202 additions and 306 deletions

8
Cargo.lock generated
View File

@ -1377,9 +1377,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
[[package]]
name = "rustls-webpki"
@ -2339,6 +2339,6 @@ dependencies = [
[[package]]
name = "zeroize"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
checksum = "63381fa6624bf92130a6b87c0d07380116f80b565c42cf0d754136f0238359ef"

View File

@ -1,15 +1,21 @@
use crate::{message::BotExt, utils::parse_bool};
use crate::{
fixer::FixerState,
message::BotExt,
utils::{parse_bool, AsyncError},
FIXER_STATE,
};
use once_cell::sync::Lazy;
use std::{env, error::Error, marker::Send};
use std::env;
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{Message, UserId},
types::{ChatAction, Message, UserId},
utils::command::BotCommands,
Bot,
};
pub(crate) type FilterState = String;
static BOT_OWNER: Lazy<UserId> = Lazy::new(|| {
let value = env::var("BOT_OWNER_ID").expect("BOT_OWNER_ID must be defined");
let id = value
@ -43,11 +49,31 @@ pub(crate) enum Command {
YouTube { filter_state: FilterState },
}
async fn check_authorized(bot: &Bot, message: &Message) -> Result<bool, AsyncError> {
let admins = bot.get_chat_administrators(message.chat.id).await?;
let admins = admins.iter().map(|c| c.user.clone()).collect::<Vec<_>>();
let from = message.from().ok_or("No user found")?;
Ok(from.id == *BOT_OWNER || admins.contains(from))
}
fn update_fixer_state<F>(message: &Message, update_state: F)
where
F: FnOnce(&mut FixerState) -> FixerState,
{
if let Ok(ref mut map) = FIXER_STATE.try_lock() {
let result = match map.get_mut(&message.chat.id) {
Some(state) => update_state(state),
None => update_state(&mut FixerState::default()),
};
map.insert(message.chat.id, result);
}
}
pub(crate) async fn handler(
bot: Bot,
message: Message,
command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> {
) -> Result<(), AsyncError> {
match command {
Command::Help | Command::Start => {
bot.send_chat_message(message, Command::descriptions().to_string())
@ -58,43 +84,43 @@ pub(crate) async fn handler(
}
#[cfg(feature = "ddinstagram")]
Command::Instagram { filter_state } => {
if let Some(from) = message.from()
&& from.id != *BOT_OWNER
if let Ok(authorized) = check_authorized(&bot, &message).await
&& authorized
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) {
Ok(filter_state) => {
crate::instagram::set_filter_state(bot, message, filter_state).await?;
update_fixer_state(&message, |x| *x.instagram(filter_state));
}
Err(error_message) => {
bot.send_chat_message(message, error_message).await?;
}
}
} else {
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(message.chat.id, "You are not authorized for this action")
.reply_to_message_id(message.id)
.await?;
}
}
Command::Medium { filter_state } => {
if let Some(from) = message.from()
&& from.id != *BOT_OWNER
if let Ok(authorized) = check_authorized(&bot, &message).await
&& authorized
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) {
Ok(filter_state) => {
crate::medium::set_filter_state(bot, message, filter_state).await?;
update_fixer_state(&message, |x| *x.medium(filter_state));
}
Err(error_message) => {
bot.send_chat_message(message, error_message).await?;
}
}
} else {
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(message.chat.id, "You are not authorized for this action")
.reply_to_message_id(message.id)
.await?;
}
}
Command::Ttv { names } => {
@ -104,43 +130,43 @@ pub(crate) async fn handler(
.await?;
}
Command::Twitter { filter_state } => {
if let Some(from) = message.from()
&& from.id != *BOT_OWNER
if let Ok(authorized) = check_authorized(&bot, &message).await
&& authorized
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) {
Ok(filter_state) => {
crate::twitter::set_filter_state(bot, message, filter_state).await?;
update_fixer_state(&message, |x| *x.twitter(filter_state));
}
Err(error_message) => {
bot.send_chat_message(message, error_message).await?;
}
}
} else {
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(message.chat.id, "You are not authorized for this action")
.reply_to_message_id(message.id)
.await?;
}
}
Command::YouTube { filter_state } => {
if let Some(from) = message.from()
&& from.id != *BOT_OWNER
if let Ok(authorized) = check_authorized(&bot, &message).await
&& authorized
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) {
Ok(filter_state) => {
crate::youtube::set_filter_state(bot, message, filter_state).await?;
update_fixer_state(&message, |x| *x.youtube(filter_state));
}
Err(error_message) => {
bot.send_chat_message(message, error_message).await?;
}
}
} else {
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(message.chat.id, "You are not authorized for this action")
.reply_to_message_id(message.id)
.await?;
}
}
};

View File

@ -1,9 +1,12 @@
mod model;
use crate::{message::BotExt, utils::get_urls_from_message};
use crate::{
message::BotExt,
utils::{get_urls_from_message, AsyncError},
};
use model::AMPResponse;
use reqwest::Url;
use std::{error::Error, str::FromStr};
use std::str::FromStr;
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
use tracing::debug;
@ -13,10 +16,7 @@ fn deserialize_amp_response(text: &str) -> Result<AMPResponse, serde_json::Error
serde_json::from_str(text)
}
pub async fn handler(
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
if let Some(text) = message.text()
&& let Some(user) = message.from()
{

30
src/fixer.rs Normal file
View File

@ -0,0 +1,30 @@
#[allow(clippy::struct_excessive_bools)] // Does not apply
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FixerState {
pub(crate) instagram: bool,
pub(crate) medium: bool,
pub(crate) twitter: bool,
pub(crate) youtube: bool,
}
impl FixerState {
pub(crate) fn instagram(&mut self, value: bool) -> &FixerState {
self.instagram = value;
self
}
pub(crate) fn medium(&mut self, value: bool) -> &FixerState {
self.medium = value;
self
}
pub(crate) fn twitter(&mut self, value: bool) -> &FixerState {
self.twitter = value;
self
}
pub(crate) fn youtube(&mut self, value: bool) -> &FixerState {
self.youtube = value;
self
}
}

View File

@ -1,17 +1,10 @@
use crate::{message::BotExt, utils::scrub_urls};
use crate::{
message::BotExt,
utils::{scrub_urls, AsyncError},
};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
const HOST_MATCH_GROUP: &str = "host";
@ -19,49 +12,7 @@ pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("https://(?:www.)?(?P<host>instagram.com)/(p|reel|tv)/[A-Za-z0-9]+.*/").unwrap()
});
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true);
pub async fn set_filter_state(
bot: Bot,
message: Message,
filter_state: Option<bool>,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
match filter_state {
None => {
let state = if FILTER_ENABLED.load(Ordering::Relaxed) {
"enabled"
} else {
"disabled"
};
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Instagram link replacement is {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
Some(state) => {
FILTER_ENABLED.store(state, Ordering::Relaxed);
let state = if state { "enabled" } else { "disabled" };
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Instagram link replacement has been {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
};
Ok(())
}
pub async fn handler(
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text)

View File

@ -1,6 +1,7 @@
#![feature(let_chains)]
mod commands;
mod deamp;
mod fixer;
#[cfg(feature = "ddinstagram")]
mod instagram;
mod logging;
@ -13,16 +14,23 @@ mod youtube;
use crate::commands::Command;
use crate::logging::TeloxideLogger;
use dotenvy::dotenv;
use std::sync::{atomic::Ordering, Arc};
use fixer::FixerState;
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use teloxide::{
dispatching::{HandlerExt, UpdateFilterExt},
dispatching::{dialogue::GetChatId, HandlerExt, UpdateFilterExt},
dptree,
prelude::Dispatcher,
types::{Message, Update},
types::{ChatId, Message, Update},
update_listeners::Polling,
Bot,
};
pub(crate) static FIXER_STATE: Lazy<Mutex<HashMap<ChatId, FixerState>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
const REPLACE_SKIP_TOKEN: &str = "#skip";
async fn run() {
@ -42,51 +50,77 @@ async fn run() {
)
.branch(
dptree::filter(|msg: Message| {
twitter::FILTER_ENABLED.load(Ordering::Relaxed)
&& msg
.text()
.map(|text| {
twitter::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default()
if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& let Some(chat_id) = msg.chat_id()
&& let Some(state) = map.get(&chat_id)
{
return state.twitter
&& msg
.text()
.map(|text| {
twitter::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
})
.endpoint(twitter::handler),
);
#[cfg(feature = "ddinstagram")]
let handler = handler.branch(
dptree::filter(|msg: Message| {
instagram::FILTER_ENABLED.load(Ordering::Relaxed)
&& msg
.text()
.map(|text| {
instagram::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default()
if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& let Some(chat_id) = msg.chat_id()
&& let Some(state) = map.get(&chat_id)
{
return state.instagram
&& msg
.text()
.map(|text| {
instagram::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
})
.endpoint(instagram::handler),
);
let handler = handler.branch(
dptree::filter(|msg: Message| {
youtube::FILTER_ENABLED.load(Ordering::Relaxed)
&& msg
.text()
.map(|text| {
youtube::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default()
if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& let Some(chat_id) = msg.chat_id()
&& let Some(state) = map.get(&chat_id)
{
return state.youtube
&& msg
.text()
.map(|text| {
youtube::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
})
.endpoint(youtube::handler),
);
let handler = handler.branch(
dptree::filter(|msg: Message| {
medium::FILTER_ENABLED.load(Ordering::Relaxed)
&& msg
.text()
.map(|text| {
medium::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default()
if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& let Some(chat_id) = msg.chat_id()
&& let Some(state) = map.get(&chat_id)
{
return state.medium
&& msg
.text()
.map(|text| {
medium::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
})
.endpoint(medium::handler),
);

View File

@ -1,66 +1,17 @@
use crate::{message::BotExt, utils::scrub_urls};
use crate::{
message::BotExt,
utils::{scrub_urls, AsyncError},
};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
const HOST_MATCH_GROUP: &str = "host";
pub static MATCH_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new("https://(?P<host>(?:.*)?medium.com)/.*").unwrap());
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true);
pub async fn set_filter_state(
bot: Bot,
message: Message,
filter_state: Option<bool>,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
match filter_state {
None => {
let state = if FILTER_ENABLED.load(Ordering::Relaxed) {
"enabled"
} else {
"disabled"
};
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Medium link replacement is {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
Some(state) => {
FILTER_ENABLED.store(state, Ordering::Relaxed);
let state = if state { "enabled" } else { "disabled" };
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Medium link replacement has been {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
};
Ok(())
}
pub async fn handler(
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text)

View File

@ -1,17 +1,10 @@
use crate::{message::BotExt, utils::scrub_urls};
use crate::{
message::BotExt,
utils::{scrub_urls, AsyncError},
};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
const HOST_MATCH_GROUP: &str = "host";
const ROOT_MATCH_GROUP: &str = "root";
@ -21,49 +14,7 @@ pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| {
.unwrap()
});
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true);
pub async fn set_filter_state(
bot: Bot,
message: Message,
filter_state: Option<bool>,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
match filter_state {
None => {
let state = if FILTER_ENABLED.load(Ordering::Relaxed) {
"enabled"
} else {
"disabled"
};
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Twitter link replacement is {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
Some(state) => {
FILTER_ENABLED.store(state, Ordering::Relaxed);
let state = if state { "enabled" } else { "disabled" };
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("Twitter link replacement has been {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
};
Ok(())
}
pub async fn handler(
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text)

View File

@ -1,8 +1,11 @@
use once_cell::sync::Lazy;
use reqwest::Url;
use std::error::Error;
use teloxide::types::{Message, MessageEntityKind};
use tracing::{error, info};
pub(crate) type AsyncError = Box<dyn Error + Send + Sync + 'static>;
pub(crate) fn get_urls_from_message(msg: &Message) -> Vec<String> {
if let Some(entities) = msg.entities()
&& !entities.is_empty()
@ -51,7 +54,7 @@ pub(crate) fn scrub_urls(msg: &Message) -> Option<String> {
}
}
pub(crate) fn parse_bool(input: &str) -> Result<Option<bool>, String> {
pub(crate) fn parse_bool(input: &str) -> Result<bool, String> {
const TRUE_VALUES: [&str; 4] = ["true", "on", "yes", "enable"];
const FALSE_VALUES: [&str; 4] = ["false", "off", "no", "disable"];
static EXPECTED_VALUES: Lazy<String> = Lazy::new(|| {
@ -72,9 +75,8 @@ pub(crate) fn parse_bool(input: &str) -> Result<Option<bool>, String> {
}
match input[0].to_lowercase().as_str() {
arg if TRUE_VALUES.contains(&arg) => Ok(Some(true)),
arg if FALSE_VALUES.contains(&arg) => Ok(Some(false)),
"" => Ok(None),
arg if TRUE_VALUES.contains(&arg) => Ok(true),
arg if FALSE_VALUES.contains(&arg) => Ok(false),
arg => {
let message = format!(
"Unexpected argument '{arg}'. Expected one of: {}.",

View File

@ -1,65 +1,16 @@
use crate::{message::BotExt, utils::scrub_urls};
use crate::{
message::BotExt,
utils::{scrub_urls, AsyncError},
};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("https://(?:www.)?youtube.com/(?P<shorts>shorts/)[A-Za-z0-9-_]{11}.*").unwrap()
});
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true);
pub async fn set_filter_state(
bot: Bot,
message: Message,
filter_state: Option<bool>,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
match filter_state {
None => {
let state = if FILTER_ENABLED.load(Ordering::Relaxed) {
"enabled"
} else {
"disabled"
};
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("YouTube link replacement is {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
Some(state) => {
FILTER_ENABLED.store(state, Ordering::Relaxed);
let state = if state { "enabled" } else { "disabled" };
bot.send_chat_action(message.chat.id, ChatAction::Typing)
.await?;
bot.send_message(
message.chat.id,
format!("YouTube link replacement has been {state}"),
)
.reply_to_message_id(message.id)
.await?;
}
};
Ok(())
}
pub async fn handler(
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text)