mirror of
https://github.com/kristoferssolo/tg-relay-rs.git
synced 2025-12-20 11:04:41 +00:00
refactor(handler): use a single Handler type
This commit is contained in:
parent
90805674c6
commit
164b99888e
@ -109,7 +109,7 @@ async fn run_command_in_tempdir(cmd: &str, args: &[&str]) -> Result<DownloadResu
|
|||||||
///
|
///
|
||||||
/// - Propagates `run_command_in_tempdir` errors.
|
/// - Propagates `run_command_in_tempdir` errors.
|
||||||
#[cfg(feature = "instagram")]
|
#[cfg(feature = "instagram")]
|
||||||
pub async fn download_instagram(url: &str) -> Result<DownloadResult> {
|
pub async fn download_instagram(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let base_args = ["--extractor-args", "instagram:"];
|
let base_args = ["--extractor-args", "instagram:"];
|
||||||
let mut args = base_args
|
let mut args = base_args
|
||||||
.iter()
|
.iter()
|
||||||
@ -133,7 +133,7 @@ pub async fn download_instagram(url: &str) -> Result<DownloadResult> {
|
|||||||
///
|
///
|
||||||
/// - Propagates `run_command_in_tempdir` errors.
|
/// - Propagates `run_command_in_tempdir` errors.
|
||||||
#[cfg(feature = "tiktok")]
|
#[cfg(feature = "tiktok")]
|
||||||
pub async fn download_tiktok(url: &str) -> Result<DownloadResult> {
|
pub async fn download_tiktok(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let base_args = ["--extractor-args", "tiktok:"];
|
let base_args = ["--extractor-args", "tiktok:"];
|
||||||
let mut args = base_args
|
let mut args = base_args
|
||||||
.iter()
|
.iter()
|
||||||
@ -157,8 +157,8 @@ pub async fn download_tiktok(url: &str) -> Result<DownloadResult> {
|
|||||||
///
|
///
|
||||||
/// - Propagates `run_command_in_tempdir` errors.
|
/// - Propagates `run_command_in_tempdir` errors.
|
||||||
#[cfg(feature = "twitter")]
|
#[cfg(feature = "twitter")]
|
||||||
pub async fn download_twitter(url: &str) -> Result<DownloadResult> {
|
pub async fn download_twitter(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let args = ["--extractor-args", "twitter:", url];
|
let args = ["--extractor-args", "twitter:", &url.into()];
|
||||||
run_command_in_tempdir("yt-dlp", &args).await
|
run_command_in_tempdir("yt-dlp", &args).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ pub async fn download_twitter(url: &str) -> Result<DownloadResult> {
|
|||||||
///
|
///
|
||||||
/// - Propagates `run_command_in_tempdir` errors.
|
/// - Propagates `run_command_in_tempdir` errors.
|
||||||
#[cfg(feature = "youtube")]
|
#[cfg(feature = "youtube")]
|
||||||
pub async fn download_ytdlp(url: &str) -> Result<DownloadResult> {
|
pub async fn download_youtube(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let args = [
|
let args = [
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
"-t",
|
"-t",
|
||||||
@ -181,7 +181,7 @@ pub async fn download_ytdlp(url: &str) -> Result<DownloadResult> {
|
|||||||
"-f",
|
"-f",
|
||||||
"--postprocessor-args",
|
"--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",
|
"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
|
run_command_in_tempdir("yt-dlp", &args).await
|
||||||
|
|||||||
86
src/handler.rs
Normal file
86
src/handler.rs
Normal file
@ -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<Box<dyn Future<Output = Result<DownloadResult>> + Send>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
name: &'static str,
|
||||||
|
regex_pattern: &'static str,
|
||||||
|
handler_fn: fn(&str) -> Pin<Box<dyn Future<Output = Result<DownloadResult>> + Send>>,
|
||||||
|
) -> std::result::Result<Self, RegexError> {
|
||||||
|
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<String> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
@ -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<String> {
|
|
||||||
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<dyn SocialHandler> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<regex::Regex> = 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<String>;
|
|
||||||
|
|
||||||
/// 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<dyn SocialHandler>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for Box<dyn SocialHandler> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.box_clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> {
|
|
||||||
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<dyn SocialHandler> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> {
|
|
||||||
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<dyn SocialHandler> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> {
|
|
||||||
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<dyn SocialHandler> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,6 +2,6 @@ pub mod commands;
|
|||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod handlers;
|
pub mod handler;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@ -1,10 +1,9 @@
|
|||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use std::sync::Arc;
|
|
||||||
use teloxide::{Bot, prelude::Requester, repls::CommandReplExt, respond, types::Message};
|
use teloxide::{Bot, prelude::Requester, repls::CommandReplExt, respond, types::Message};
|
||||||
use tg_relay_rs::{
|
use tg_relay_rs::{
|
||||||
commands::{Command, answer},
|
commands::{Command, answer},
|
||||||
comments::{Comments, init_global_comments},
|
comments::{Comments, init_global_comments},
|
||||||
handlers::SocialHandler,
|
handler::create_handlers,
|
||||||
telemetry::setup_logger,
|
telemetry::setup_logger,
|
||||||
};
|
};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
@ -28,16 +27,7 @@ async fn main() -> color_eyre::Result<()> {
|
|||||||
let bot = Bot::from_env();
|
let bot = Bot::from_env();
|
||||||
info!("bot starting");
|
info!("bot starting");
|
||||||
|
|
||||||
let handlers: Vec<Arc<dyn SocialHandler>> = vec![
|
let handlers = create_handlers();
|
||||||
#[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),
|
|
||||||
];
|
|
||||||
|
|
||||||
Command::repl(bot.clone(), answer).await;
|
Command::repl(bot.clone(), answer).await;
|
||||||
|
|
||||||
@ -46,7 +36,7 @@ async fn main() -> color_eyre::Result<()> {
|
|||||||
let handlers = handlers.clone();
|
let handlers = handlers.clone();
|
||||||
async move {
|
async move {
|
||||||
if let Some(text) = msg.text() {
|
if let Some(text) = msg.text() {
|
||||||
for handler in handlers {
|
for handler in handlers.iter() {
|
||||||
if let Some(id) = handler.try_extract(text) {
|
if let Some(id) = handler.try_extract(text) {
|
||||||
let handler = handler.clone();
|
let handler = handler.clone();
|
||||||
let bot_for_task = bot.clone();
|
let bot_for_task = bot.clone();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user