diff --git a/Cargo.lock b/Cargo.lock index 8a21edb..72cd045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "capitalize" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5271031022835ee8c7582fe67403bd6cb3d962095787af7921027234bab5bf" + [[package]] name = "cc" version = "1.2.38" @@ -1726,6 +1732,7 @@ name = "tg-relay-rs" version = "0.1.0" dependencies = [ "async-trait", + "capitalize", "color-eyre", "dotenv", "futures", diff --git a/Cargo.toml b/Cargo.toml index 54056a9..896d5f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" [dependencies] async-trait = "0.1" +capitalize = "0.3.4" color-eyre = "0.6" dotenv = "0.15" futures = "0.3" diff --git a/src/comments.rs b/src/comments.rs index 77aaef2..44e5605 100644 --- a/src/comments.rs +++ b/src/comments.rs @@ -107,7 +107,7 @@ pub fn init_global_comments(comments: Comments) -> Result<()> { .map_err(|_| Error::other("comments already initialized")) } -/// Get global comments (if initialized). Returns Option<&'static Comments>. +/// Get global comments (if initialized). #[inline] #[must_use] pub fn global_comments() -> Option<&'static Comments> { diff --git a/src/lib.rs b/src/lib.rs index c4d3cdf..e7f3b95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,3 @@ pub mod error; pub mod handlers; pub mod telemetry; pub mod utils; -pub mod validate; diff --git a/src/telemetry.rs b/src/telemetry.rs index 5946f5a..36be3d7 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -8,5 +8,5 @@ pub fn setup_logger() { tracing_subscriber::registry() .with(env_filter) .with(formatter) - .init() + .init(); } diff --git a/src/utils.rs b/src/utils.rs index b0915c0..0eecd7a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,18 +2,15 @@ use crate::{ comments::{Comments, global_comments}, error::{Error, Result}, }; +use capitalize::Capitalize; use std::{ ffi::OsStr, + fmt::Display, path::{Path, PathBuf}, }; -use teloxide::{ - Bot, - payloads::{SendPhotoSetters, SendVideoSetters}, - prelude::Requester, - types::{ChatId, InputFile}, -}; +use teloxide::{prelude::*, types::InputFile}; use tokio::{fs::File, io::AsyncReadExt}; -use tracing::warn; +use tracing::{error, info, warn}; pub const VIDEO_EXTSTENSIONS: &[&str] = &["mp4", "webm", "mov", "mkv", "avi", "m4v", "3gp"]; pub const IMAGE_EXTSTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "gif", "bmp"]; @@ -26,6 +23,18 @@ pub enum MediaKind { Unknown, } +impl MediaKind { + #[must_use] + #[inline] + pub const fn to_str(&self) -> &str { + match self { + Self::Video => "video", + Self::Image => "image", + Self::Unknown => "unknown", + } + } +} + /// Detect media kind first by extension, then by content/magic (sync). pub fn detect_media_kind(path: &Path) -> MediaKind { if let Some(ext) = path.extension().and_then(OsStr::to_str) { @@ -93,7 +102,7 @@ pub async fn detect_media_kind_async(path: &Path) -> MediaKind { /// /// # Errors /// -/// Returns an error if sending fails or the media kind is unknown. +/// Returns an `Error::UnknownMediaKind` if sending fails or the media kind is unknown. pub async fn send_media_from_path( bot: &Bot, chat_id: ChatId, @@ -104,32 +113,46 @@ pub async fn send_media_from_path( .map(Comments::build_caption) .filter(|caption| !caption.is_empty()); + let input = InputFile::file(path); + + macro_rules! send_msg { + ($request_expr:expr) => {{ + let mut request = $request_expr; + if let Some(cap) = caption_opt { + request = request.caption(cap); + } + if let Ok(message) = request.await { + info!(message_id = message.id.to_string(), "{} sent", kind); + } + }}; + } + match kind { - MediaKind::Video => { - let video = InputFile::file(path); - let mut req = bot.send_video(chat_id, video); - if let Some(c) = caption_opt { - req = req.caption(c); - } - req.await?; - } - MediaKind::Image => { - let photo = InputFile::file(path); - let mut req = bot.send_photo(chat_id, photo); - if let Some(c) = caption_opt { - req = req.caption(c); - } - req.await?; - } + MediaKind::Video => send_msg!(bot.send_video(chat_id, input)), + MediaKind::Image => send_msg!(bot.send_photo(chat_id, input)), MediaKind::Unknown => { bot.send_message(chat_id, "No supported media found") .await?; + error!("No supported media found"); return Err(Error::UnknownMediaKind); } } + Ok(()) } +impl AsRef for MediaKind { + fn as_ref(&self) -> &str { + self.to_str() + } +} + +impl Display for MediaKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.capitalize()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/validate.rs b/src/validate.rs deleted file mode 100644 index e789d01..0000000 --- a/src/validate.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::error::Result; -use regex::Regex; -use std::sync::OnceLock; - -/// Trait for validating platform-specific identifiers (e.g., shortcodes, URLs) -/// extracted from user input. -/// -/// Implementors should: -/// - Check format (e.g., length, characters). -/// - Canonicalize if needed (e.g., trim query params from a URL). -/// - Return `Ok(canonical_id)` on success or `Err(Error::Other(...))` on failure. -pub trait Validate { - /// Validate the input and return a canonicalized String (e.g., cleaned shortcode or URL). - fn validate(&self, input: &str) -> Result; -} - -/// Helper function to create a lazy static Regex (reused across impls). -/// -/// # Panics -/// -/// If no pattern found -pub fn lazy_regex(pattern: &str) -> &'static Regex { - static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| Regex::new(pattern).expect("failed to compile validation regex")) -}