mirror of
https://github.com/kristoferssolo/tg-relay-rs.git
synced 2025-12-20 11:04:41 +00:00
feat: add config struct
This commit is contained in:
parent
fee8178ad2
commit
9a6e8bbefc
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1739,7 +1739,6 @@ dependencies = [
|
|||||||
"infer",
|
"infer",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
|
||||||
"teloxide",
|
"teloxide",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.16",
|
||||||
|
|||||||
@ -14,7 +14,6 @@ futures = "0.3"
|
|||||||
infer = "0.19"
|
infer = "0.19"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
teloxide = { version = "0.17", features = ["macros"] }
|
teloxide = { version = "0.17", features = ["macros"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
use crate::comments::global_comments;
|
||||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||||
|
|
||||||
use crate::comments::{Comments, global_comments};
|
|
||||||
|
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(rename_rule = "lowercase")]
|
#[command(rename_rule = "lowercase")]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
@ -25,9 +24,8 @@ pub async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()>
|
|||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
Command::Curse => {
|
Command::Curse => {
|
||||||
let comment = global_comments().map(Comments::build_caption);
|
let comment = global_comments().build_caption();
|
||||||
bot.send_message(msg.chat.id, comment.unwrap_or_else(|| "To comment".into()))
|
bot.send_message(msg.chat.id, comment).await?
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tokio::fs::read_to_string;
|
use tokio::fs::read_to_string;
|
||||||
|
|
||||||
|
static GLOBAL_COMMENTS: OnceLock<Comments> = OnceLock::new();
|
||||||
|
|
||||||
const DISCLAIMER: &str = "(Roleplay — fictional messages for entertainment.)";
|
const DISCLAIMER: &str = "(Roleplay — fictional messages for entertainment.)";
|
||||||
pub const TELEGRAM_CAPTION_LIMIT: usize = 4096;
|
pub const TELEGRAM_CAPTION_LIMIT: usize = 4096;
|
||||||
const FALLBACK_COMMENTS: &[&str] = &[
|
const FALLBACK_COMMENTS: &[&str] = &[
|
||||||
@ -92,26 +94,28 @@ impl Comments {
|
|||||||
pub fn lines(&self) -> &[String] {
|
pub fn lines(&self) -> &[String] {
|
||||||
&self.lines
|
&self.lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize the global comments (call once at startup).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `Error::Other` when the global is already initialized.
|
||||||
|
pub fn init(self) -> Result<()> {
|
||||||
|
GLOBAL_COMMENTS
|
||||||
|
.set(self)
|
||||||
|
.map_err(|_| Error::other("comments already initialized"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static GLOBAL_COMMENTS: OnceLock<Comments> = OnceLock::new();
|
/// Get global comments (initialized by `Comments::init(self)`).
|
||||||
|
|
||||||
/// Initialize the global comments (call once at startup).
|
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// - Returns `Error::Other` when the global is already initialized.
|
/// Panics if comments have not been initialized.
|
||||||
pub fn init_global_comments(comments: Comments) -> Result<()> {
|
|
||||||
GLOBAL_COMMENTS
|
|
||||||
.set(comments)
|
|
||||||
.map_err(|_| Error::other("comments already initialized"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get global comments (if initialized).
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn global_comments() -> Option<&'static Comments> {
|
pub fn global_comments() -> &'static Comments {
|
||||||
GLOBAL_COMMENTS.get()
|
GLOBAL_COMMENTS.get().expect("comments not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Comments {
|
impl Display for Comments {
|
||||||
|
|||||||
111
src/config.rs
Normal file
111
src/config.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use crate::error::{Error, Result};
|
||||||
|
use std::{env, fmt::Debug, path::PathBuf, sync::OnceLock};
|
||||||
|
|
||||||
|
static GLOBAL_CONFIG: OnceLock<Config> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub youtube: YoutubeConfig,
|
||||||
|
pub instagram: InstagramConfig,
|
||||||
|
pub tiktok: TiktokConfig,
|
||||||
|
pub twitter: TwitterConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct YoutubeConfig {
|
||||||
|
pub cookies_path: Option<PathBuf>,
|
||||||
|
pub postprocessor_args: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct InstagramConfig {
|
||||||
|
pub cookies_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TiktokConfig {
|
||||||
|
pub cookies_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TwitterConfig {
|
||||||
|
pub cookies_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Load configuration from environment variables.
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
Self {
|
||||||
|
youtube: YoutubeConfig::from_env(),
|
||||||
|
instagram: InstagramConfig::from_env(),
|
||||||
|
tiktok: TiktokConfig::from_env(),
|
||||||
|
twitter: TwitterConfig::from_env(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the global config (call once at startup).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns error if config is already initialized.
|
||||||
|
pub fn init(self) -> Result<()> {
|
||||||
|
GLOBAL_CONFIG
|
||||||
|
.set(self)
|
||||||
|
.map_err(|_| Error::other("config already initialized"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Get global config (initialized by `Config::init(self)`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn global_config() -> Config {
|
||||||
|
GLOBAL_CONFIG.get().cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YoutubeConfig {
|
||||||
|
const DEFAULT_POSTPROCESSOR_ARGS: &'static str = "ffmpeg:-vf setsar=1 -c:v libx264 -crf 20 -preset ultrafast -c:a aac -b:a 128k -movflags +faststart";
|
||||||
|
|
||||||
|
fn from_env() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies_path: get_path_from_env("YOUTUBE_SESSION_COOKIE_PATH"),
|
||||||
|
postprocessor_args: env::var("YOUTUBE_POSTPROCESSOR_ARGS")
|
||||||
|
.unwrap_or_else(|_| Self::DEFAULT_POSTPROCESSOR_ARGS.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstagramConfig {
|
||||||
|
fn from_env() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies_path: get_path_from_env("IG_SESSION_COOKIE_PATH"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiktokConfig {
|
||||||
|
fn from_env() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies_path: get_path_from_env("TIKTOK_SESSION_COOKIE_PATH"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TwitterConfig {
|
||||||
|
fn from_env() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies_path: get_path_from_env("TWITTER_SESSION_COOKIE_PATH"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_path_from_env(key: &str) -> Option<PathBuf> {
|
||||||
|
env::var(key).ok().map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for YoutubeConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies_path: None,
|
||||||
|
postprocessor_args: Self::DEFAULT_POSTPROCESSOR_ARGS.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::config::global_config;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
utils::{
|
utils::{
|
||||||
@ -8,7 +9,6 @@ use crate::{
|
|||||||
use futures::{StreamExt, stream};
|
use futures::{StreamExt, stream};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::min,
|
cmp::min,
|
||||||
env,
|
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fs::{self, metadata},
|
fs::{self, metadata},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -105,21 +105,12 @@ 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: impl Into<String>) -> Result<DownloadResult> {
|
pub async fn download_instagram(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let base_args = ["-t", "mp4", "--extractor-args", "instagram:"];
|
let config = global_config();
|
||||||
let mut args = base_args
|
let args = ["-t", "mp4", "--extractor-args", "instagram:"]
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
|
run_yt_dlp(args, config.instagram.cookies_path.as_ref(), &url.into()).await
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
run_command_in_tempdir("yt-dlp", &args_ref).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a Tiktok URL with yt-dlp.
|
/// Download a Tiktok URL with yt-dlp.
|
||||||
@ -129,21 +120,12 @@ pub async fn download_instagram(url: impl Into<String>) -> 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: impl Into<String>) -> Result<DownloadResult> {
|
pub async fn download_tiktok(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let base_args = ["-t", "mp4", "--extractor-args", "tiktok:"];
|
let config = global_config();
|
||||||
let mut args = base_args
|
let args = ["-t", "mp4", "--extractor-args", "tiktok:"]
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
|
run_yt_dlp(args, config.tiktok.cookies_path.as_ref(), &url.into()).await
|
||||||
if let Ok(cookies_path) = env::var("TIKTOK_SESSION_COOKIE_PATH") {
|
|
||||||
args.extend(["--cookies".into(), cookies_path]);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(url.into());
|
|
||||||
|
|
||||||
let args_ref = args.iter().map(String::as_ref).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
run_command_in_tempdir("yt-dlp", &args_ref).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a Twitter URL with yt-dlp.
|
/// Download a Twitter URL with yt-dlp.
|
||||||
@ -153,8 +135,12 @@ pub async fn download_tiktok(url: impl Into<String>) -> 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: impl Into<String>) -> Result<DownloadResult> {
|
pub async fn download_twitter(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let args = ["-t", "mp4", "--extractor-args", "twitter:", &url.into()];
|
let config = global_config();
|
||||||
run_command_in_tempdir("yt-dlp", &args).await
|
let args = ["-t", "mp4", "--extractor-args", "twitter:"]
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect();
|
||||||
|
run_yt_dlp(args, config.twitter.cookies_path.as_ref(), &url.into()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a URL with yt-dlp.
|
/// Download a URL with yt-dlp.
|
||||||
@ -164,7 +150,8 @@ pub async fn download_twitter(url: impl Into<String>) -> Result<DownloadResult>
|
|||||||
/// - Propagates `run_command_in_tempdir` errors.
|
/// - Propagates `run_command_in_tempdir` errors.
|
||||||
#[cfg(feature = "youtube")]
|
#[cfg(feature = "youtube")]
|
||||||
pub async fn download_youtube(url: impl Into<String>) -> Result<DownloadResult> {
|
pub async fn download_youtube(url: impl Into<String>) -> Result<DownloadResult> {
|
||||||
let base_args = [
|
let config = global_config();
|
||||||
|
let args = [
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
"-t",
|
"-t",
|
||||||
"mp4",
|
"mp4",
|
||||||
@ -172,21 +159,12 @@ pub async fn download_youtube(url: impl Into<String>) -> Result<DownloadResult>
|
|||||||
"-o",
|
"-o",
|
||||||
"%(title)s.%(ext)s",
|
"%(title)s.%(ext)s",
|
||||||
"--postprocessor-args",
|
"--postprocessor-args",
|
||||||
"ffmpeg:-vf setsar=1 -c:v libx264 -crf 20 -preset veryfast -c:a aac -b:a 128k -movflags +faststart",
|
&config.youtube.postprocessor_args,
|
||||||
];
|
]
|
||||||
let mut args = base_args
|
.iter()
|
||||||
.iter()
|
.map(ToString::to_string)
|
||||||
.map(ToString::to_string)
|
.collect();
|
||||||
.collect::<Vec<_>>();
|
run_yt_dlp(args, config.youtube.cookies_path.as_ref(), &url.into()).await
|
||||||
|
|
||||||
if let Ok(cookies_path) = env::var("YOUTUBE_SESSION_COOKIE_PATH") {
|
|
||||||
args.extend(["--cookies".into(), cookies_path]);
|
|
||||||
}
|
|
||||||
args.push(url.into());
|
|
||||||
|
|
||||||
let args_ref = args.iter().map(String::as_ref).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
run_command_in_tempdir("yt-dlp", &args_ref).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post-process a `DownloadResult`.
|
/// Post-process a `DownloadResult`.
|
||||||
@ -278,6 +256,20 @@ fn is_potential_media_file(path: &Path) -> bool {
|
|||||||
.any(|allowed| allowed.eq_ignore_ascii_case(&ext))
|
.any(|allowed| allowed.eq_ignore_ascii_case(&ext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_yt_dlp(
|
||||||
|
mut args: Vec<String>,
|
||||||
|
cookies_path: Option<&PathBuf>,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<DownloadResult> {
|
||||||
|
if let Some(path) = cookies_path {
|
||||||
|
args.extend(["--cookies".to_string(), path.to_string_lossy().to_string()]);
|
||||||
|
}
|
||||||
|
args.push(url.to_string());
|
||||||
|
|
||||||
|
let args_ref = args.iter().map(String::as_ref).collect::<Vec<_>>();
|
||||||
|
run_command_in_tempdir("yt-dlp", &args_ref).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
|
pub mod config;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@ -1,7 +1,7 @@
|
|||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use teloxide::{prelude::*, respond};
|
use teloxide::{prelude::*, respond};
|
||||||
use tg_relay_rs::{
|
use tg_relay_rs::{
|
||||||
comments::{Comments, init_global_comments},
|
comments::Comments,
|
||||||
handler::{Handler, create_handlers},
|
handler::{Handler, create_handlers},
|
||||||
telemetry::setup_logger,
|
telemetry::setup_logger,
|
||||||
};
|
};
|
||||||
@ -13,15 +13,15 @@ async fn main() -> color_eyre::Result<()> {
|
|||||||
color_eyre::install().expect("color-eyre install");
|
color_eyre::install().expect("color-eyre install");
|
||||||
setup_logger();
|
setup_logger();
|
||||||
|
|
||||||
let comments = Comments::load_from_file("comments.txt")
|
Comments::load_from_file("comments.txt")
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!("failed to laod comments.txt: {}; using dummy comments", e);
|
warn!("failed to laod comments.txt: {}; using dummy comments", e);
|
||||||
e
|
e
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| Comments::dummy());
|
.unwrap_or_else(|_| Comments::dummy())
|
||||||
|
.init()
|
||||||
init_global_comments(comments).expect("failed to initialize global comments");
|
.expect("failed to initialize comments");
|
||||||
|
|
||||||
let bot = Bot::from_env();
|
let bot = Bot::from_env();
|
||||||
info!("bot starting");
|
info!("bot starting");
|
||||||
|
|||||||
11
src/utils.rs
11
src/utils.rs
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comments::{Comments, global_comments},
|
comments::global_comments,
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
};
|
};
|
||||||
use capitalize::Capitalize;
|
use capitalize::Capitalize;
|
||||||
@ -109,18 +109,13 @@ pub async fn send_media_from_path(
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
kind: MediaKind,
|
kind: MediaKind,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let caption_opt = global_comments()
|
let caption = global_comments().build_caption();
|
||||||
.map(Comments::build_caption)
|
|
||||||
.filter(|caption| !caption.is_empty());
|
|
||||||
|
|
||||||
let input = InputFile::file(path);
|
let input = InputFile::file(path);
|
||||||
|
|
||||||
macro_rules! send_msg {
|
macro_rules! send_msg {
|
||||||
($request_expr:expr) => {{
|
($request_expr:expr) => {{
|
||||||
let mut request = $request_expr;
|
let mut request = $request_expr;
|
||||||
if let Some(cap) = caption_opt {
|
request = request.caption(caption);
|
||||||
request = request.caption(cap);
|
|
||||||
}
|
|
||||||
match request.await {
|
match request.await {
|
||||||
Ok(message) => info!(message_id = message.id.to_string(), "{} sent", kind),
|
Ok(message) => info!(message_id = message.id.to_string(), "{} sent", kind),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user