From 24bfeb4efc6c265cf69e919f1298b039da707d20 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 27 Oct 2025 10:59:49 +0200 Subject: [PATCH] feat(tiktok): add tiktok handler --- Cargo.toml | 3 ++- src/download.rs | 19 +++++++++++++++ src/handlers/mod.rs | 4 ++++ src/handlers/tiktok.rs | 54 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 ++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/handlers/tiktok.rs diff --git a/Cargo.toml b/Cargo.toml index ff3feec..9475516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,10 @@ tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } url = "2.5" [features] -default = ["instagram", "youtube"] +default = ["instagram", "youtube", "tiktok"] instagram = [] youtube = [] +tiktok = [] [lints.clippy] pedantic = "warn" diff --git a/src/download.rs b/src/download.rs index 82a8dba..71c8aff 100644 --- a/src/download.rs +++ b/src/download.rs @@ -128,6 +128,25 @@ pub async fn download_instagram(url: &str) -> Result { run_command_in_tempdir("yt-dlp", &args_ref).await } +#[cfg(feature = "tiktok")] +pub async fn download_tiktok(url: &str) -> Result { + let base_args = ["--extractor-args", "tiktok:"]; + let mut args = base_args + .iter() + .map(ToString::to_string) + .collect::>(); + + if let Ok(cookies_path) = env::var("IG_SESSION_COOKIE_PATH") { + args.extend(["--cookies".into(), cookies_path]); + } + + args.push(url.into()); + + let args_ref = args.iter().map(String::as_ref).collect::>(); + + run_command_in_tempdir("yt-dlp", &args_ref).await +} + /// Download a URL with yt-dlp. /// /// # Errors diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 75c0c93..8b482fd 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "instagram")] mod instagram; +#[cfg(feature = "tiktok")] +mod tiktok; #[cfg(feature = "youtube")] mod youtube; @@ -8,6 +10,8 @@ use teloxide::{Bot, types::ChatId}; #[cfg(feature = "instagram")] pub use instagram::InstagramHandler; +#[cfg(feature = "tiktok")] +pub use tiktok::TiktokHandler; #[cfg(feature = "youtube")] pub use youtube::YouTubeShortsHandler; diff --git a/src/handlers/tiktok.rs b/src/handlers/tiktok.rs new file mode 100644 index 0000000..784e482 --- /dev/null +++ b/src/handlers/tiktok.rs @@ -0,0 +1,54 @@ +use crate::{ + download::{download_tiktok, 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\.)?(?:vm|vt|tt|tik)\.tiktok\.com/([A-Za-z0-9_-]+)[/?#]?") + .expect("filed to compile regex") + }) +} + +/// 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 { + 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 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/main.rs b/src/main.rs index 18b2145..cc730f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,8 @@ async fn main() -> color_eyre::Result<()> { 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), ]; teloxide::repl(bot.clone(), move |bot: Bot, msg: Message| {