Compare commits

...

14 Commits

Author SHA1 Message Date
Harsh Shandilya 4cbc8c0030 fix: prevent moving `Message` in `BotExt` 2024-04-29 06:32:42 +05:30
Harsh Shandilya b689a6e698 fix: restore confirmation for fixer state toggle 2024-04-29 06:28:17 +05:30
Harsh Shandilya d928d6947c fix: special case PMs 2024-04-29 06:15:01 +05:30
Harsh Shandilya b38af61ec9 fix: use entry API for `update_fixer_state` 2024-04-29 06:03:05 +05:30
Harsh Shandilya e324a82e34 fix: insert default values 2024-04-29 06:03:05 +05:30
Harsh Shandilya a297ef7074 refactor: commonize fixer state update 2024-04-29 06:03:05 +05:30
Harsh Shandilya 3e5e0550f3 refactor: commonize authorization check 2024-04-29 06:03:05 +05:30
Harsh Shandilya 2e8c824686 refactor: add a typealias for async errors 2024-04-29 05:26:14 +05:30
Harsh Shandilya e03ef7a607 refactor: simplify fixer state handling 2024-04-29 05:26:14 +05:30
Harsh Shandilya 775d8378f3 refactor: track fixer state per-chat rather than globally 2024-04-29 05:01:27 +05:30
GitHub Actions 669bce3f52 flake.lock: Update
Flake lock file updates:

• Updated input 'advisory-db':
    'github:rustsec/advisory-db/6ab370c779c140c9cb2e7ff1367dd1b66c415409?narHash=sha256-JXiXi2Egq7gHfIvigBXFSdzNsxIjk1s9fcq1ibfoD/U%3D' (2024-04-20)
  → 'github:rustsec/advisory-db/35e7459a331d3e0c585e56dabd03006b9b354088?narHash=sha256-1BVft7ggSN2XXFeXQjazU3jN9wVECd9qp2mZx/8GDMk%3D' (2024-04-27)
• Updated input 'crane':
    'github:ipetkov/crane/45ea0059fb325132fdc3c39faffb0941d25d08d3?narHash=sha256-LjQ11ASxnv/FXfb8QnrIyMkyqSqcBPX%2BlFK8gu0jSQE%3D' (2024-04-18)
  → 'github:ipetkov/crane/a5eca68a2cf11adb32787fc141cddd29ac8eb79c?narHash=sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH%2BDcP4%3D' (2024-04-24)
• Updated input 'fenix':
    'github:nix-community/fenix/3247290e1bba55878a2c62d43894d0309d29c918?narHash=sha256-lYWehi0cqBdsL1W4xeUnUcXw4U4aBKKCmmQrR01yqE0%3D' (2024-04-20)
  → 'github:nix-community/fenix/055f6db376eaf544d84aa55bd5a7c70634af41ba?narHash=sha256-QO3Yv3UfJRfhZE1wsHOartg%2Bk8/Kf1BiDyfl8eEpqcE%3D' (2024-04-27)
• Updated input 'fenix/rust-analyzer-src':
    'github:rust-lang/rust-analyzer/c83d8cf5844fff3d6e243ab408669222059af1c6?narHash=sha256-HsVa%2BQM2vMra80OjnjH7JhdvLeJuMdR4sxBNHJveMe4%3D' (2024-04-19)
  → 'github:rust-lang/rust-analyzer/1ed7e2de05ee76f6ae83cc9c72eb0b33ad6903f2?narHash=sha256-S8AsTyJvT6Q3pRFeo8QepWF/husnJh61cOhRt18Xmyc%3D' (2024-04-26)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/fd16bb6d3bcca96039b11aa52038fafeb6e4f4be?narHash=sha256-LJbHQQ5aX1LVth2ST%2BKkse/DRzgxlVhTL1rxthvyhZc%3D' (2024-04-20)
  → 'github:NixOS/nixpkgs/d6f6eb2a984f2ba9a366c31e4d36d65465683450?narHash=sha256-Yg5D5LhyAZvd3DZrQQfJAVK8K3TkUYKooFtH1ulM0mw%3D' (2024-04-27)
2024-04-28 00:20:47 +00:00
renovate[bot] 46e19af1f5
chore(deps): lock file maintenance 2024-04-27 01:19:28 +00:00
renovate[bot] 67ec88a3e8
chore(deps): lock file maintenance 2024-04-26 01:21:15 +00:00
renovate[bot] 81ab49659b
chore(deps): update actions/checkout digest to 0ad4b8f 2024-04-25 17:36:01 +00:00
13 changed files with 275 additions and 346 deletions

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
- name: Set up flyctl - name: Set up flyctl
uses: superfly/flyctl-actions/setup-flyctl@master uses: superfly/flyctl-actions/setup-flyctl@master

18
Cargo.lock generated
View File

@ -394,9 +394,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.28" version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@ -705,7 +705,7 @@ dependencies = [
"futures-util", "futures-util",
"http 0.2.12", "http 0.2.12",
"hyper 0.14.28", "hyper 0.14.28",
"rustls 0.21.11", "rustls 0.21.12",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
] ]
@ -1239,7 +1239,7 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.21.11", "rustls 0.21.12",
"rustls-pemfile 1.0.4", "rustls-pemfile 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
@ -1332,9 +1332,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.21.11" version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [ dependencies = [
"log", "log",
"ring", "ring",
@ -1770,7 +1770,7 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [ dependencies = [
"rustls 0.21.11", "rustls 0.21.12",
"tokio", "tokio",
] ]
@ -2339,6 +2339,6 @@ dependencies = [
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.8.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63381fa6624bf92130a6b87c0d07380116f80b565c42cf0d754136f0238359ef" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"

View File

@ -3,11 +3,11 @@
"advisory-db": { "advisory-db": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1713579674, "lastModified": 1714183630,
"narHash": "sha256-JXiXi2Egq7gHfIvigBXFSdzNsxIjk1s9fcq1ibfoD/U=", "narHash": "sha256-1BVft7ggSN2XXFeXQjazU3jN9wVECd9qp2mZx/8GDMk=",
"owner": "rustsec", "owner": "rustsec",
"repo": "advisory-db", "repo": "advisory-db",
"rev": "6ab370c779c140c9cb2e7ff1367dd1b66c415409", "rev": "35e7459a331d3e0c585e56dabd03006b9b354088",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1713459701, "lastModified": 1713979152,
"narHash": "sha256-LjQ11ASxnv/FXfb8QnrIyMkyqSqcBPX+lFK8gu0jSQE=", "narHash": "sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH+DcP4=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "45ea0059fb325132fdc3c39faffb0941d25d08d3", "rev": "a5eca68a2cf11adb32787fc141cddd29ac8eb79c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -67,11 +67,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1713594079, "lastModified": 1714199028,
"narHash": "sha256-lYWehi0cqBdsL1W4xeUnUcXw4U4aBKKCmmQrR01yqE0=", "narHash": "sha256-QO3Yv3UfJRfhZE1wsHOartg+k8/Kf1BiDyfl8eEpqcE=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "3247290e1bba55878a2c62d43894d0309d29c918", "rev": "055f6db376eaf544d84aa55bd5a7c70634af41ba",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -118,11 +118,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1713596654, "lastModified": 1714213793,
"narHash": "sha256-LJbHQQ5aX1LVth2ST+Kkse/DRzgxlVhTL1rxthvyhZc=", "narHash": "sha256-Yg5D5LhyAZvd3DZrQQfJAVK8K3TkUYKooFtH1ulM0mw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fd16bb6d3bcca96039b11aa52038fafeb6e4f4be", "rev": "d6f6eb2a984f2ba9a366c31e4d36d65465683450",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -147,11 +147,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1713559870, "lastModified": 1714150666,
"narHash": "sha256-HsVa+QM2vMra80OjnjH7JhdvLeJuMdR4sxBNHJveMe4=", "narHash": "sha256-S8AsTyJvT6Q3pRFeo8QepWF/husnJh61cOhRt18Xmyc=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "c83d8cf5844fff3d6e243ab408669222059af1c6", "rev": "1ed7e2de05ee76f6ae83cc9c72eb0b33ad6903f2",
"type": "github" "type": "github"
}, },
"original": { "original": {

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 once_cell::sync::Lazy;
use std::{env, error::Error, marker::Send}; use std::env;
use teloxide::{ use teloxide::{
payloads::SendMessageSetters, payloads::SendMessageSetters,
prelude::Requester, prelude::Requester,
types::{Message, UserId}, types::{ChatAction, Message, UserId},
utils::command::BotCommands, utils::command::BotCommands,
Bot, Bot,
}; };
pub(crate) type FilterState = String; pub(crate) type FilterState = String;
static BOT_OWNER: Lazy<UserId> = Lazy::new(|| { static BOT_OWNER: Lazy<UserId> = Lazy::new(|| {
let value = env::var("BOT_OWNER_ID").expect("BOT_OWNER_ID must be defined"); let value = env::var("BOT_OWNER_ID").expect("BOT_OWNER_ID must be defined");
let id = value let id = value
@ -43,58 +49,96 @@ pub(crate) enum Command {
YouTube { filter_state: FilterState }, 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 = match admins {
Ok(admins) => admins,
Err(e) => {
return Ok(e
.to_string()
.contains("there are no administrators in the private chat"));
}
};
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) -> () + Copy,
{
if let Ok(ref mut map) = FIXER_STATE.try_lock() {
map.entry(message.chat.id)
.and_modify(|e| update_state(e))
.or_insert_with(|| {
let mut state = FixerState::default();
update_state(&mut state);
state
});
}
}
pub(crate) async fn handler( pub(crate) async fn handler(
bot: Bot, bot: Bot,
message: Message, message: Message,
command: Command, command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), AsyncError> {
match command { match command {
Command::Help | Command::Start => { Command::Help | Command::Start => {
bot.send_chat_message(message, Command::descriptions().to_string()) bot.send_chat_message(&message, Command::descriptions().to_string())
.await?; .await?;
} }
Command::Ping => { Command::Ping => {
bot.send_chat_message(message, "Pong".to_string()).await?; bot.send_chat_message(&message, "Pong".to_string()).await?;
} }
#[cfg(feature = "ddinstagram")] #[cfg(feature = "ddinstagram")]
Command::Instagram { filter_state } => { Command::Instagram { filter_state } => {
if let Some(from) = message.from() if check_authorized(&bot, &message).await? {
&& from.id != *BOT_OWNER
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) { match parse_bool(&filter_state) {
Ok(filter_state) => { Ok(filter_state) => {
crate::instagram::set_filter_state(bot, message, filter_state).await?; update_fixer_state(&message, |x| x.instagram(filter_state));
let state = if filter_state { "enabled" } else { "disabled" };
bot.send_chat_message(
&message,
format!("Instagram link replacement is now {}", state),
)
.await?;
} }
Err(error_message) => { Err(error_message) => {
bot.send_chat_message(message, error_message).await?; 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 } => { Command::Medium { filter_state } => {
if let Some(from) = message.from() if check_authorized(&bot, &message).await? {
&& from.id != *BOT_OWNER
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) { match parse_bool(&filter_state) {
Ok(filter_state) => { Ok(filter_state) => {
crate::medium::set_filter_state(bot, message, filter_state).await?; update_fixer_state(&message, |x| x.medium(filter_state));
let state = if filter_state { "enabled" } else { "disabled" };
bot.send_chat_message(
&message,
format!("Medium link replacement is now {}", state),
)
.await?;
} }
Err(error_message) => { Err(error_message) => {
bot.send_chat_message(message, error_message).await?; 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 } => { Command::Ttv { names } => {
@ -104,43 +148,51 @@ pub(crate) async fn handler(
.await?; .await?;
} }
Command::Twitter { filter_state } => { Command::Twitter { filter_state } => {
if let Some(from) = message.from() if check_authorized(&bot, &message).await? {
&& from.id != *BOT_OWNER
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) { match parse_bool(&filter_state) {
Ok(filter_state) => { Ok(filter_state) => {
crate::twitter::set_filter_state(bot, message, filter_state).await?; update_fixer_state(&message, |x| x.twitter(filter_state));
let state = if filter_state { "enabled" } else { "disabled" };
bot.send_chat_message(
&message,
format!("Twitter link replacement is now {}", state),
)
.await?;
} }
Err(error_message) => { Err(error_message) => {
bot.send_chat_message(message, error_message).await?; 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 } => { Command::YouTube { filter_state } => {
if let Some(from) = message.from() if check_authorized(&bot, &message).await? {
&& from.id != *BOT_OWNER
{
bot.send_chat_message(
message,
"You are not authorized for this action".to_string(),
)
.await?;
} else {
match parse_bool(&filter_state) { match parse_bool(&filter_state) {
Ok(filter_state) => { Ok(filter_state) => {
crate::youtube::set_filter_state(bot, message, filter_state).await?; update_fixer_state(&message, |x| x.youtube(filter_state));
let state = if filter_state { "enabled" } else { "disabled" };
bot.send_chat_message(
&message,
format!("YouTube link replacement is now {}", state),
)
.await?;
} }
Err(error_message) => { Err(error_message) => {
bot.send_chat_message(message, error_message).await?; 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; 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 model::AMPResponse;
use reqwest::Url; use reqwest::Url;
use std::{error::Error, str::FromStr}; use std::str::FromStr;
use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot}; use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
use tracing::debug; use tracing::debug;
@ -13,10 +16,7 @@ fn deserialize_amp_response(text: &str) -> Result<AMPResponse, serde_json::Error
serde_json::from_str(text) serde_json::from_str(text)
} }
pub async fn handler( pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
bot: Bot,
message: Message,
) -> Result<(), Box<dyn Error + Sync + Send + 'static>> {
if let Some(text) = message.text() if let Some(text) = message.text()
&& let Some(user) = message.from() && let Some(user) = message.from()
{ {
@ -38,7 +38,7 @@ pub async fn handler(
} }
let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text); let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text);
let _del = bot.delete_message(message.chat.id, message.id).await; let _del = bot.delete_message(message.chat.id, message.id).await;
bot.try_reply(message, text).await?; bot.try_reply(&message, text).await?;
} }
Ok(()) Ok(())
} }

37
src/fixer.rs Normal file
View File

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

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 once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::{ use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
const HOST_MATCH_GROUP: &str = "host"; 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() 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 handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
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>> {
if let Some(text) = scrub_urls(&message) if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from() && let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text) && let Some(caps) = MATCH_REGEX.captures(&text)
@ -69,7 +20,7 @@ pub async fn handler(
let text = text.replace(&caps[HOST_MATCH_GROUP], "ddinstagram.com"); let text = text.replace(&caps[HOST_MATCH_GROUP], "ddinstagram.com");
let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text); let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text);
let _del = bot.delete_message(message.chat.id, message.id).await; let _del = bot.delete_message(message.chat.id, message.id).await;
bot.try_reply(message, text).await?; bot.try_reply(&message, text).await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,6 +1,7 @@
#![feature(let_chains)] #![feature(let_chains)]
mod commands; mod commands;
mod deamp; mod deamp;
mod fixer;
#[cfg(feature = "ddinstagram")] #[cfg(feature = "ddinstagram")]
mod instagram; mod instagram;
mod logging; mod logging;
@ -13,16 +14,23 @@ mod youtube;
use crate::commands::Command; use crate::commands::Command;
use crate::logging::TeloxideLogger; use crate::logging::TeloxideLogger;
use dotenvy::dotenv; 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::{ use teloxide::{
dispatching::{HandlerExt, UpdateFilterExt}, dispatching::{dialogue::GetChatId, HandlerExt, UpdateFilterExt},
dptree, dptree,
prelude::Dispatcher, prelude::Dispatcher,
types::{Message, Update}, types::{ChatId, Message, Update},
update_listeners::Polling, update_listeners::Polling,
Bot, Bot,
}; };
pub(crate) static FIXER_STATE: Lazy<Mutex<HashMap<ChatId, FixerState>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
const REPLACE_SKIP_TOKEN: &str = "#skip"; const REPLACE_SKIP_TOKEN: &str = "#skip";
async fn run() { async fn run() {
@ -42,51 +50,77 @@ async fn run() {
) )
.branch( .branch(
dptree::filter(|msg: Message| { dptree::filter(|msg: Message| {
twitter::FILTER_ENABLED.load(Ordering::Relaxed) if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& msg && let Some(chat_id) = msg.chat_id()
.text() {
.map(|text| { let state = map.entry(chat_id).or_insert(FixerState::default());
twitter::MATCH_REGEX.is_match(text) return state.twitter
&& !text.contains(REPLACE_SKIP_TOKEN) && msg
}) .text()
.unwrap_or_default() .map(|text| {
twitter::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
}) })
.endpoint(twitter::handler), .endpoint(twitter::handler),
); );
#[cfg(feature = "ddinstagram")] #[cfg(feature = "ddinstagram")]
let handler = handler.branch( let handler = handler.branch(
dptree::filter(|msg: Message| { dptree::filter(|msg: Message| {
instagram::FILTER_ENABLED.load(Ordering::Relaxed) if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& msg && let Some(chat_id) = msg.chat_id()
.text() {
.map(|text| { let state = map.entry(chat_id).or_insert(FixerState::default());
instagram::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN) return state.instagram
}) && msg
.unwrap_or_default() .text()
.map(|text| {
instagram::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
}) })
.endpoint(instagram::handler), .endpoint(instagram::handler),
); );
let handler = handler.branch( let handler = handler.branch(
dptree::filter(|msg: Message| { dptree::filter(|msg: Message| {
youtube::FILTER_ENABLED.load(Ordering::Relaxed) if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& msg && let Some(chat_id) = msg.chat_id()
.text() {
.map(|text| { let state = map.entry(chat_id).or_insert(FixerState::default());
youtube::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN) return state.youtube
}) && msg
.unwrap_or_default() .text()
.map(|text| {
youtube::MATCH_REGEX.is_match(text)
&& !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
}) })
.endpoint(youtube::handler), .endpoint(youtube::handler),
); );
let handler = handler.branch( let handler = handler.branch(
dptree::filter(|msg: Message| { dptree::filter(|msg: Message| {
medium::FILTER_ENABLED.load(Ordering::Relaxed) if let Ok(ref mut map) = FIXER_STATE.try_lock()
&& msg && let Some(chat_id) = msg.chat_id()
.text() {
.map(|text| { let state = map.entry(chat_id).or_insert(FixerState::default());
medium::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN) return state.medium
}) && msg
.unwrap_or_default() .text()
.map(|text| {
medium::MATCH_REGEX.is_match(text) && !text.contains(REPLACE_SKIP_TOKEN)
})
.unwrap_or_default();
}
false
}) })
.endpoint(medium::handler), .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 once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::{ use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
const HOST_MATCH_GROUP: &str = "host"; const HOST_MATCH_GROUP: &str = "host";
pub static MATCH_REGEX: Lazy<Regex> = pub static MATCH_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new("https://(?P<host>(?:.*)?medium.com)/.*").unwrap()); Lazy::new(|| Regex::new("https://(?P<host>(?:.*)?medium.com)/.*").unwrap());
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true); pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
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>> {
if let Some(text) = scrub_urls(&message) if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from() && let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text) && let Some(caps) = MATCH_REGEX.captures(&text)
@ -68,7 +19,7 @@ pub async fn handler(
let text = text.replace(&caps[HOST_MATCH_GROUP], "medium.rip"); let text = text.replace(&caps[HOST_MATCH_GROUP], "medium.rip");
let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text); let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text);
let _del = bot.delete_message(message.chat.id, message.id).await; let _del = bot.delete_message(message.chat.id, message.id).await;
bot.try_reply(message, text).await?; bot.try_reply(&message, text).await?;
} }
Ok(()) Ok(())
} }

View File

@ -6,16 +6,16 @@ use teloxide::{
}; };
pub(crate) trait BotExt { pub(crate) trait BotExt {
async fn try_reply(&self, message: Message, text: String) -> Result<Message, RequestError>; async fn try_reply(&self, message: &Message, text: String) -> Result<Message, RequestError>;
async fn send_chat_message( async fn send_chat_message(
&self, &self,
message: Message, message: &Message,
text: String, text: String,
) -> Result<Message, RequestError>; ) -> Result<Message, RequestError>;
} }
impl BotExt for Bot { impl BotExt for Bot {
async fn try_reply(&self, message: Message, text: String) -> Result<Message, RequestError> { async fn try_reply(&self, message: &Message, text: String) -> Result<Message, RequestError> {
if let Some(reply) = message.reply_to_message() { if let Some(reply) = message.reply_to_message() {
self.send_message(message.chat.id, text) self.send_message(message.chat.id, text)
.reply_to_message_id(reply.id) .reply_to_message_id(reply.id)
@ -30,7 +30,7 @@ impl BotExt for Bot {
async fn send_chat_message( async fn send_chat_message(
&self, &self,
message: Message, message: &Message,
text: String, text: String,
) -> Result<Message, RequestError> { ) -> Result<Message, RequestError> {
self.send_chat_action(message.chat.id, ChatAction::Typing) self.send_chat_action(message.chat.id, ChatAction::Typing)

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 once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::{ use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
const HOST_MATCH_GROUP: &str = "host"; const HOST_MATCH_GROUP: &str = "host";
const ROOT_MATCH_GROUP: &str = "root"; const ROOT_MATCH_GROUP: &str = "root";
@ -21,49 +14,7 @@ pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| {
.unwrap() .unwrap()
}); });
pub static FILTER_ENABLED: AtomicBool = AtomicBool::new(true); pub async fn handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
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>> {
if let Some(text) = scrub_urls(&message) if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from() && let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text) && let Some(caps) = MATCH_REGEX.captures(&text)
@ -78,7 +29,7 @@ pub async fn handler(
}; };
let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text); let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text);
let _del = bot.delete_message(message.chat.id, message.id).await; let _del = bot.delete_message(message.chat.id, message.id).await;
bot.try_reply(message, text).await?; bot.try_reply(&message, text).await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,8 +1,11 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reqwest::Url; use reqwest::Url;
use std::error::Error;
use teloxide::types::{Message, MessageEntityKind}; use teloxide::types::{Message, MessageEntityKind};
use tracing::{error, info}; 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> { pub(crate) fn get_urls_from_message(msg: &Message) -> Vec<String> {
if let Some(entities) = msg.entities() if let Some(entities) = msg.entities()
&& !entities.is_empty() && !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 TRUE_VALUES: [&str; 4] = ["true", "on", "yes", "enable"];
const FALSE_VALUES: [&str; 4] = ["false", "off", "no", "disable"]; const FALSE_VALUES: [&str; 4] = ["false", "off", "no", "disable"];
static EXPECTED_VALUES: Lazy<String> = Lazy::new(|| { 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() { match input[0].to_lowercase().as_str() {
arg if TRUE_VALUES.contains(&arg) => Ok(Some(true)), arg if TRUE_VALUES.contains(&arg) => Ok(true),
arg if FALSE_VALUES.contains(&arg) => Ok(Some(false)), arg if FALSE_VALUES.contains(&arg) => Ok(false),
"" => Ok(None),
arg => { arg => {
let message = format!( let message = format!(
"Unexpected argument '{arg}'. Expected one of: {}.", "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 once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::{ use teloxide::{prelude::Requester, types::Message, utils::html::link, Bot};
error::Error,
sync::atomic::{AtomicBool, Ordering},
};
use teloxide::{
payloads::SendMessageSetters,
prelude::Requester,
types::{ChatAction, Message},
utils::html::link,
Bot,
};
pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| { pub static MATCH_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("https://(?:www.)?youtube.com/(?P<shorts>shorts/)[A-Za-z0-9-_]{11}.*").unwrap() 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 handler(bot: Bot, message: Message) -> Result<(), AsyncError> {
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>> {
if let Some(text) = scrub_urls(&message) if let Some(text) = scrub_urls(&message)
&& let Some(user) = message.from() && let Some(user) = message.from()
&& let Some(caps) = MATCH_REGEX.captures(&text) && let Some(caps) = MATCH_REGEX.captures(&text)
@ -67,7 +18,7 @@ pub async fn handler(
let text = text.replace(&caps["shorts"], "watch?v="); let text = text.replace(&caps["shorts"], "watch?v=");
let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text); let text = format!("{}: {}", link(user.url().as_str(), &user.full_name()), text);
let _del = bot.delete_message(message.chat.id, message.id).await; let _del = bot.delete_message(message.chat.id, message.id).await;
bot.try_reply(message, text).await?; bot.try_reply(&message, text).await?;
} }
Ok(()) Ok(())
} }