From 164b99888e3d571f5ec454f25488d643f4e311fd Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 27 Oct 2025 12:56:20 +0200 Subject: [PATCH] refactor(handler): use a single Handler type --- src/download.rs | 12 +++--- src/handler.rs | 86 +++++++++++++++++++++++++++++++++++++++ src/handlers/instagram.rs | 46 --------------------- src/handlers/mod.rs | 53 ------------------------ src/handlers/tiktok.rs | 49 ---------------------- src/handlers/twitter.rs | 49 ---------------------- src/handlers/youtube.rs | 46 --------------------- src/lib.rs | 2 +- src/main.rs | 16 ++------ 9 files changed, 96 insertions(+), 263 deletions(-) create mode 100644 src/handler.rs delete mode 100644 src/handlers/instagram.rs delete mode 100644 src/handlers/mod.rs delete mode 100644 src/handlers/tiktok.rs delete mode 100644 src/handlers/twitter.rs delete mode 100644 src/handlers/youtube.rs diff --git a/src/download.rs b/src/download.rs index 73b85cf..e9f0ddc 100644 --- a/src/download.rs +++ b/src/download.rs @@ -109,7 +109,7 @@ async fn run_command_in_tempdir(cmd: &str, args: &[&str]) -> Result Result { +pub async fn download_instagram(url: impl Into) -> Result { let base_args = ["--extractor-args", "instagram:"]; let mut args = base_args .iter() @@ -133,7 +133,7 @@ pub async fn download_instagram(url: &str) -> Result { /// /// - Propagates `run_command_in_tempdir` errors. #[cfg(feature = "tiktok")] -pub async fn download_tiktok(url: &str) -> Result { +pub async fn download_tiktok(url: impl Into) -> Result { let base_args = ["--extractor-args", "tiktok:"]; let mut args = base_args .iter() @@ -157,8 +157,8 @@ pub async fn download_tiktok(url: &str) -> Result { /// /// - Propagates `run_command_in_tempdir` errors. #[cfg(feature = "twitter")] -pub async fn download_twitter(url: &str) -> Result { - let args = ["--extractor-args", "twitter:", url]; +pub async fn download_twitter(url: impl Into) -> Result { + let args = ["--extractor-args", "twitter:", &url.into()]; run_command_in_tempdir("yt-dlp", &args).await } @@ -168,7 +168,7 @@ pub async fn download_twitter(url: &str) -> Result { /// /// - Propagates `run_command_in_tempdir` errors. #[cfg(feature = "youtube")] -pub async fn download_ytdlp(url: &str) -> Result { +pub async fn download_youtube(url: impl Into) -> Result { let args = [ "--no-playlist", "-t", @@ -181,7 +181,7 @@ pub async fn download_ytdlp(url: &str) -> Result { "-f", "--postprocessor-args", "ffmpeg:-vf setsar=1 -c:v libx264 -crf 28 -preset ultrafast -maxrate 800k -bufsize 1600k -vf scale=854:480 -c:a aac -b:a 64k -movflags +faststart", - url, + &url.into(), ]; run_command_in_tempdir("yt-dlp", &args).await diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..615f292 --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,86 @@ +use crate::{ + download::{DownloadResult, process_download_result}, + error::Result, +}; +use regex::{Error as RegexError, Regex}; +use std::{pin::Pin, sync::Arc}; +use teloxide::{Bot, types::ChatId}; +use tracing::info; + +#[derive(Debug, Clone)] +pub struct Handler { + name: &'static str, + regex: Regex, + handler_fn: fn(&str) -> Pin> + Send>>, +} + +impl Handler { + #[must_use] + pub fn new( + name: &'static str, + regex_pattern: &'static str, + handler_fn: fn(&str) -> Pin> + Send>>, + ) -> std::result::Result { + let regex = Regex::new(regex_pattern)?; + Ok(Self { + name, + regex, + handler_fn, + }) + } + + #[inline] + #[must_use] + pub const fn name(&self) -> &'static str { + self.name + } + + #[must_use] + pub fn try_extract(&self, text: &str) -> Option { + self.regex + .captures(text) + .and_then(|c| c.get(0).map(|m| m.as_str().to_owned())) + } + + pub async fn handle(&self, bot: &Bot, chat_id: ChatId, url: String) -> Result<()> { + info!(handler = %self.name(), url = %url, "handling url"); + let dr = (self.handler_fn)(&url).await?; + process_download_result(bot, chat_id, dr).await + } +} + +macro_rules! handler { + ($feature:expr, $regex:expr, $download_fn:path) => { + #[cfg(feature = $feature)] + Handler::new($feature, $regex, |url| { + Box::pin($download_fn(url.to_string())) + }) + .expect(concat!("failed to create ", $feature, " handler")) + }; +} + +pub fn create_handlers() -> Arc<[Handler]> { + [ + handler!( + "instagram", + r"https?://(?:www\.)?(?:instagram\.com|instagr\.am)/(?:p|reel|tv)/([A-Za-z0-9_-]+)", + crate::download::download_instagram + ), + handler!( + "youtube", + r"https?:\/\/(?:www\.)?youtube\.com\/shorts\/[A-Za-z0-9_-]+(?:\?[^\s]*)?", + crate::download::download_youtube + ), + handler!( + "twitter", + r"https?://(?:www\.)?twitter\.com/([A-Za-z0-9_]+(?:/[A-Za-z0-9_]+)?)/status/(\d{1,20})", + crate::download::download_twitter + ), + handler!( + "tiktok", + r"https?://(?:www\.)?(?:vm|vt|tt|tik)\.tiktok\.com/([A-Za-z0-9_-]+)[/?#]?", + crate::download::download_tiktok + ), + ] + .into() +} diff --git a/src/handlers/instagram.rs b/src/handlers/instagram.rs deleted file mode 100644 index 0b60962..0000000 --- a/src/handlers/instagram.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::download::{download_instagram, process_download_result}; -use crate::error::Result; -use crate::handlers::SocialHandler; -use crate::lazy_regex; -use teloxide::{Bot, types::ChatId}; -use tracing::info; - -lazy_regex!( - URL_RE, - r#"https?://(?:www\.)?(?:instagram\.com|instagr\.am)/(?:p|reel|tv)/([A-Za-z0-9_-]+)"# -); - -/// Handler for Instagram posts / reels / tv -#[derive(Clone, Default)] -pub struct InstagramHandler; - -impl InstagramHandler { - #[inline] - #[must_use] - pub const fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl SocialHandler for InstagramHandler { - fn name(&self) -> &'static str { - "instagram" - } - - fn try_extract(&self, text: &str) -> Option { - regex() - .captures(text) - .and_then(|c| c.get(0).map(|m| m.as_str().to_owned())) - } - - async fn handle(&self, bot: &Bot, chat_id: ChatId, url: String) -> Result<()> { - info!(handler = %self.name(), url = %url, "handling instagram url"); - let dr = download_instagram(&url).await?; - process_download_result(bot, chat_id, dr).await - } - - fn box_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index 302e660..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -#[cfg(feature = "instagram")] -mod instagram; -#[cfg(feature = "tiktok")] -mod tiktok; -#[cfg(feature = "twitter")] -mod twitter; -#[cfg(feature = "youtube")] -mod youtube; - -use crate::error::Result; -use teloxide::{Bot, types::ChatId}; - -#[cfg(feature = "instagram")] -pub use instagram::InstagramHandler; -#[cfg(feature = "tiktok")] -pub use tiktok::TiktokHandler; -#[cfg(feature = "twitter")] -pub use twitter::TwitterHandler; -#[cfg(feature = "youtube")] -pub use youtube::YouTubeShortsHandler; - -#[macro_export] -macro_rules! lazy_regex { - ($name:ident, $pattern:expr) => { - static $name: std::sync::OnceLock = std::sync::OnceLock::new(); - - fn regex() -> &'static regex::Regex { - $name.get_or_init(|| regex::Regex::new($pattern).expect("failed to compile regex")) - } - }; -} - -#[async_trait::async_trait] -pub trait SocialHandler: Send + Sync { - /// Short name used for logging etc. - fn name(&self) -> &'static str; - - /// Try to extract a platform-specific identifier (shortcode, id, url) - /// from arbitrary text. Return `Some` if the handler should handle this message. - fn try_extract(&self, text: &str) -> Option; - - /// Do the heavy-lifting: fetch media and send to `chat_id`. - async fn handle(&self, bot: &Bot, chat_id: ChatId, id: String) -> Result<()>; - - /// Clone a boxed handler. - fn box_clone(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.box_clone() - } -} diff --git a/src/handlers/tiktok.rs b/src/handlers/tiktok.rs deleted file mode 100644 index 747e830..0000000 --- a/src/handlers/tiktok.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{ - download::{download_tiktok, process_download_result}, - error::Result, - lazy_regex, -}; -use teloxide::{Bot, types::ChatId}; -use tracing::info; - -use crate::handlers::SocialHandler; - -lazy_regex!( - URL_RE, - r#"https?://(?:www\.)?(?:vm|vt|tt|tik)\.tiktok\.com/([A-Za-z0-9_-]+)[/?#]?"# -); - -/// Handler for Tiktok -#[derive(Clone, Default)] -pub struct TiktokHandler; - -impl TiktokHandler { - #[inline] - #[must_use] - pub const fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl SocialHandler for TiktokHandler { - fn name(&self) -> &'static str { - "tiktok" - } - - fn try_extract(&self, text: &str) -> Option { - regex() - .captures(text) - .and_then(|c| c.get(0).map(|m| m.as_str().to_owned())) - } - - async fn handle(&self, bot: &Bot, chat_id: ChatId, url: String) -> Result<()> { - info!(handler = %self.name(), url = %url, "handling tiktok url"); - let dr = download_tiktok(&url).await?; - process_download_result(bot, chat_id, dr).await - } - - fn box_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/handlers/twitter.rs b/src/handlers/twitter.rs deleted file mode 100644 index a02790b..0000000 --- a/src/handlers/twitter.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{ - download::{download_twitter, process_download_result}, - error::Result, - lazy_regex, -}; -use teloxide::{Bot, types::ChatId}; -use tracing::info; - -use crate::handlers::SocialHandler; - -lazy_regex!( - URL_RE, - r#"https?://(?:www\.)?twitter\.com/([A-Za-z0-9_]+(?:/[A-Za-z0-9_]+)?)/status/(\d{1,20})"# -); - -/// Handler for Tiktok -#[derive(Clone, Default)] -pub struct TwitterHandler; - -impl TwitterHandler { - #[inline] - #[must_use] - pub const fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl SocialHandler for TwitterHandler { - fn name(&self) -> &'static str { - "twitter" - } - - fn try_extract(&self, text: &str) -> Option { - regex() - .captures(text) - .and_then(|c| c.get(0).map(|m| m.as_str().to_owned())) - } - - async fn handle(&self, bot: &Bot, chat_id: ChatId, url: String) -> Result<()> { - info!(handler = %self.name(), url = %url, "handling twitter url"); - let dr = download_twitter(&url).await?; - process_download_result(bot, chat_id, dr).await - } - - fn box_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/handlers/youtube.rs b/src/handlers/youtube.rs deleted file mode 100644 index d48b26f..0000000 --- a/src/handlers/youtube.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::handlers::SocialHandler; -use crate::lazy_regex; -use crate::{ - download::{download_ytdlp, process_download_result}, - error::Result, -}; -use teloxide::{Bot, types::ChatId}; -use tracing::info; - -lazy_regex!( - URL_RE, - r#"https?:\/\/(?:www\.)?youtube\.com\/shorts\/[A-Za-z0-9_-]+(?:\?[^\s]*)?"# -); - -/// Handler for `YouTube Shorts` (and short youtu.be links) -#[derive(Clone, Default)] -pub struct YouTubeShortsHandler; - -impl YouTubeShortsHandler { - #[inline] - #[must_use] - pub const fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl SocialHandler for YouTubeShortsHandler { - fn name(&self) -> &'static str { - "youtube" - } - - fn try_extract(&self, text: &str) -> Option { - regex().find(text).map(|m| m.as_str().to_owned()) - } - - async fn handle(&self, bot: &Bot, chat_id: ChatId, url: String) -> Result<()> { - info!(handler = %self.name(), url = %url, "handling youtube url"); - let dr = download_ytdlp(&url).await?; - process_download_result(bot, chat_id, dr).await - } - - fn box_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 2e0ac25..5395148 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,6 @@ pub mod commands; pub mod comments; pub mod download; pub mod error; -pub mod handlers; +pub mod handler; pub mod telemetry; pub mod utils; diff --git a/src/main.rs b/src/main.rs index 0104a6d..2712f9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ use dotenv::dotenv; -use std::sync::Arc; use teloxide::{Bot, prelude::Requester, repls::CommandReplExt, respond, types::Message}; use tg_relay_rs::{ commands::{Command, answer}, comments::{Comments, init_global_comments}, - handlers::SocialHandler, + handler::create_handlers, telemetry::setup_logger, }; use tracing::{error, info, warn}; @@ -28,16 +27,7 @@ async fn main() -> color_eyre::Result<()> { let bot = Bot::from_env(); info!("bot starting"); - let handlers: Vec> = vec![ - #[cfg(feature = "instagram")] - Arc::new(tg_relay_rs::handlers::InstagramHandler), - #[cfg(feature = "youtube")] - Arc::new(tg_relay_rs::handlers::YouTubeShortsHandler), - #[cfg(feature = "tiktok")] - Arc::new(tg_relay_rs::handlers::TiktokHandler), - #[cfg(feature = "twitter")] - Arc::new(tg_relay_rs::handlers::TwitterHandler), - ]; + let handlers = create_handlers(); Command::repl(bot.clone(), answer).await; @@ -46,7 +36,7 @@ async fn main() -> color_eyre::Result<()> { let handlers = handlers.clone(); async move { if let Some(text) = msg.text() { - for handler in handlers { + for handler in handlers.iter() { if let Some(id) = handler.try_extract(text) { let handler = handler.clone(); let bot_for_task = bot.clone();