From 40ae0b73711641f5f364abd3a1b58c47a511f8db Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 27 Oct 2025 11:23:28 +0200 Subject: [PATCH] feat(twitter): add handler --- Cargo.toml | 5 ++-- src/download.rs | 18 ++++++++++++- src/handlers/mod.rs | 4 +++ src/handlers/twitter.rs | 56 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 ++ 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/handlers/twitter.rs diff --git a/Cargo.toml b/Cargo.toml index 9475516..69973a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,11 @@ tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } url = "2.5" [features] -default = ["instagram", "youtube", "tiktok"] +default = ["instagram", "tiktok", "twitter", "youtube"] instagram = [] -youtube = [] tiktok = [] +twitter = [] +youtube = [] [lints.clippy] pedantic = "warn" diff --git a/src/download.rs b/src/download.rs index 158858d..2ad8fb8 100644 --- a/src/download.rs +++ b/src/download.rs @@ -104,7 +104,7 @@ async fn run_command_in_tempdir(cmd: &str, args: &[&str]) -> Result Result { run_command_in_tempdir("yt-dlp", &args_ref).await } +/// Download a Tiktok URL with yt-dlp. +/// +/// # Errors +/// +/// - Propagates `run_command_in_tempdir` errors. #[cfg(feature = "tiktok")] pub async fn download_tiktok(url: &str) -> Result { let base_args = ["--extractor-args", "tiktok:"]; @@ -147,6 +152,17 @@ pub async fn download_tiktok(url: &str) -> Result { run_command_in_tempdir("yt-dlp", &args_ref).await } +/// Download a Twitter URL with yt-dlp. +/// +/// # Errors +/// +/// - Propagates `run_command_in_tempdir` errors. +#[cfg(feature = "twitter")] +pub async fn download_twitter(url: &str) -> Result { + let args = ["--extractor-args", "twitter:", url]; + run_command_in_tempdir("yt-dlp", &args).await +} + /// Download a URL with yt-dlp. /// /// # Errors diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 8b482fd..fc21774 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,6 +2,8 @@ mod instagram; #[cfg(feature = "tiktok")] mod tiktok; +#[cfg(feature = "twitter")] +mod twitter; #[cfg(feature = "youtube")] mod youtube; @@ -12,6 +14,8 @@ use teloxide::{Bot, types::ChatId}; 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; diff --git a/src/handlers/twitter.rs b/src/handlers/twitter.rs new file mode 100644 index 0000000..20dbb8a --- /dev/null +++ b/src/handlers/twitter.rs @@ -0,0 +1,56 @@ +use crate::{ + download::{download_twitter, process_download_result}, + error::Result, +}; +use regex::Regex; +use std::sync::OnceLock; +use teloxide::{Bot, types::ChatId}; +use tracing::info; + +use crate::handlers::SocialHandler; + +static SHORTCODE_RE: OnceLock = OnceLock::new(); + +fn shortcode_regex() -> &'static Regex { + SHORTCODE_RE.get_or_init(|| { + Regex::new( + r"https?://(?:www\.)?twitter\.com/([A-Za-z0-9_]+(?:/[A-Za-z0-9_]+)?)/status/(\d{1,20})", + ) + .expect("filed to compile regex") + }) +} + +/// 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 { + shortcode_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/main.rs b/src/main.rs index cc730f9..2e65e94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,8 @@ async fn main() -> color_eyre::Result<()> { 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), ]; teloxide::repl(bot.clone(), move |bot: Bot, msg: Message| {