diff --git a/src/db/push_task.rs b/src/db/push_task.rs index 4c81889..4694a03 100644 --- a/src/db/push_task.rs +++ b/src/db/push_task.rs @@ -1,5 +1,7 @@ use crate::pixiv_app::PixivRestrictType; use crate::push::every_push::EveryPushTextType; +use crate::push::telegram::botapi_client::BotapiClientConfig; +use crate::push::telegram::tg_type::ChatId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -118,6 +120,10 @@ fn default_true() -> bool { true } +fn default_false() -> bool { + false +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EveryPushConfig { @@ -204,11 +210,69 @@ pub struct PushDeerConfig { pub allow_failed: bool, } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum TelegramBackend { + Botapi(BotapiClientConfig), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TelegramPushConfig { + /// Backend + pub backend: TelegramBackend, + /// Unique identifier for the target chat or username of the target channel + /// (in the format `@channelusername`) + pub chat_id: ChatId, + /// Unique identifier for the target message thread (topic) of the forum; + /// for forum supergroups only + pub message_thread_id: Option, + /// Sends the message silently. Users will receive a notification with no sound. + #[serde(default = "default_false")] + pub disable_notification: bool, + /// Protects the contents of the sent message from forwarding and saving + #[serde(default = "default_false")] + pub protect_content: bool, + /// Pass True, if the caption must be shown above the message media + #[serde(default = "default_false")] + pub show_caption_above_media: bool, + /// Enable spoiler if image is R18 image + #[serde(default = "default_true")] + pub enable_spoiler: bool, + #[serde(default = "defualt_author_locations")] + /// Author locations + pub author_locations: Vec, + #[serde(default = "default_true")] + /// Whether to filter author name + pub filter_author: bool, + #[serde(default = "default_true")] + /// Whether to add artwork link + pub add_link: bool, + #[serde(default = "default_true")] + /// Whether to add artwork link to title + pub add_link_to_title: bool, + #[serde(default = "default_true")] + /// Whether to add tags + pub add_tags: bool, + #[serde(default = "default_true")] + /// Whether to add AI tag + pub add_ai_tag: bool, + #[serde(default = "default_true")] + /// Whether to add translated tag + pub add_translated_tag: bool, + #[serde(default = "default_true")] + /// Whether to allow failed + pub allow_failed: bool, + /// Download media first and send media to telegram server directly. + pub download_media: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum PushConfig { EveryPush(EveryPushConfig), PushDeer(PushDeerConfig), + Telegram(TelegramPushConfig), } impl PushConfig { @@ -217,6 +281,7 @@ impl PushConfig { match self { Self::EveryPush(config) => config.allow_failed, Self::PushDeer(config) => config.allow_failed, + Self::Telegram(config) => config.allow_failed, } } } diff --git a/src/error.rs b/src/error.rs index 823bf4f..a9650aa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -32,6 +32,7 @@ pub enum PixivDownloaderError { SerdeJsonError(serde_json::Error), #[cfg(feature = "serde_urlencoded")] SerdeUrlencodedError(serde_urlencoded::ser::Error), + BotApiError(crate::push::telegram::botapi_client::BotapiClientError), } impl std::error::Error for PixivDownloaderError {} diff --git a/src/main.rs b/src/main.rs index 2b08e76..171ce39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,6 +51,8 @@ mod server; mod settings; mod settings_list; mod task_manager; +#[cfg(feature = "server")] +mod tmp_cache; #[cfg(feature = "ugoira")] mod ugoira; mod utils; diff --git a/src/opthelper.rs b/src/opthelper.rs index f29408c..89b6e16 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -28,6 +28,7 @@ use std::net::Ipv4Addr; #[cfg(feature = "server")] use std::net::SocketAddr; use std::ops::Deref; +use std::path::PathBuf; #[cfg(any(feature = "server", feature = "ugoira"))] use std::str::FromStr; use std::sync::Arc; @@ -636,6 +637,13 @@ impl OptHelper { pub fn disable_push_task(&self) -> bool { self.opt.get_ref().disable_push_task } + + #[cfg(feature = "server")] + pub fn temp_dir(&self) -> PathBuf { + #[cfg(feature = "docker")] + return PathBuf::from("/app/temp"); + std::env::temp_dir() + } } impl Default for OptHelper { diff --git a/src/parser/description.rs b/src/parser/description.rs index d2fbb54..ad5295a 100644 --- a/src/parser/description.rs +++ b/src/parser/description.rs @@ -137,14 +137,17 @@ pub struct DescriptionParserBuilder { md_mode: bool, /// Ensure link is ASCII _ensure_link_ascii: bool, + /// Telegram HTML Mode + tg_html_mode: bool, } #[allow(dead_code)] impl DescriptionParserBuilder { - pub fn new(md_mode: bool) -> Self { + pub fn new(md_mode: bool, tg_html_mode: bool) -> Self { Self { md_mode, _ensure_link_ascii: false, + tg_html_mode, } } @@ -170,11 +173,11 @@ pub struct DescriptionParser { } impl DescriptionParser { - pub fn new(md_mode: bool) -> Self { + pub fn new(md_mode: bool, tg_html_mode: bool) -> Self { Self { nodes: Vec::new(), data: String::from(""), - opts: DescriptionParserBuilder::new(md_mode), + opts: DescriptionParserBuilder::new(md_mode, tg_html_mode), } } @@ -218,7 +221,24 @@ impl DescriptionParser { } let node = self.nodes.pop().unwrap(); let mut is_paragraph = false; - let s = if node.is_link(self.opts.md_mode) { + let s = if self.opts.tg_html_mode { + if node.tag == "a" && node.is_link(true) { + format!( + "{}", + node.attrs.get("href").unwrap(), + node.data + ) + } else if node.tag.is_empty() + || node.tag == "a" + || node.tag == "html" + || node.tag == "body" + || node.tag == "head" + { + node.data + } else { + format!("<{}>{}", node.tag, node.data, node.tag) + } + } else if node.is_link(self.opts.md_mode) { node.to_link(self.opts._ensure_link_ascii) } else if self.opts.md_mode && node.is_headline() { node.to_headline() @@ -277,8 +297,8 @@ impl DescriptionParser { } #[allow(dead_code)] - pub fn builder(md_mode: bool) -> DescriptionParserBuilder { - DescriptionParserBuilder::new(md_mode) + pub fn builder(md_mode: bool, tg_html_mode: bool) -> DescriptionParserBuilder { + DescriptionParserBuilder::new(md_mode, tg_html_mode) } } @@ -293,7 +313,7 @@ impl From for DescriptionParser { } pub fn parse_description + ?Sized>(desc: &S) -> Option { - let mut p = DescriptionParser::new(false); + let mut p = DescriptionParser::new(false, false); match p.parse(desc) { Ok(_) => Some(p.data), Err(e) => { @@ -306,7 +326,15 @@ pub fn parse_description + ?Sized>(desc: &S) -> Option { pub fn convert_description_to_md + ?Sized>( desc: &S, ) -> Result { - let mut p = DescriptionParser::new(true); + let mut p = DescriptionParser::new(true, false); + p.parse(desc)?; + Ok(p.data) +} + +pub fn convert_description_to_tg_html + ?Sized>( + desc: &S, +) -> Result { + let mut p = DescriptionParser::new(false, true); p.parse(desc)?; Ok(p.data) } @@ -356,7 +384,9 @@ fn test_convert_description_to_md() { #[test] fn test_ensure_link_ascii() { - let mut p = DescriptionParser::builder(true).ensure_link_ascii().build(); + let mut p = DescriptionParser::builder(true, false) + .ensure_link_ascii() + .build(); p.parse("测试") .unwrap(); assert_eq!( @@ -364,3 +394,15 @@ fn test_ensure_link_ascii() { p.data ); } + +#[test] +fn test_convert_description_to_tg_html() { + assert_eq!( + String::from("ご依頼・お仕事について:https://lit.link/en/hamiyamiko\nVGen:https://vgen.co/hamiyamiko\nFanbox:https://hamiya.fanbox.cc/\nX(Twitter):twitter/hamiyamiko"), + convert_description_to_tg_html("ご依頼・お仕事について:https://lit.link/en/hamiyamiko
VGen:https://vgen.co/hamiyamiko
Fanbox:https://hamiya.fanbox.cc/
X(Twitter):twitter/hamiyamiko").unwrap(), + ); + assert_eq!( + String::from("ロリっくorロリっ娘! 様の音声作品にイラスト描かせていただきました!\nhttps://www.dlsite.com/maniax/work/=/product_id/RJ01233310.html"), + convert_description_to_tg_html("ロリっくorロリっ娘! 様の音声作品にイラスト描かせていただきました!
https://www.dlsite.com/maniax/work/=/product_id/RJ01233310.html").unwrap(), + ); +} diff --git a/src/push/telegram/botapi_client.rs b/src/push/telegram/botapi_client.rs index eaa9bf9..161c89b 100644 --- a/src/push/telegram/botapi_client.rs +++ b/src/push/telegram/botapi_client.rs @@ -47,6 +47,10 @@ impl BotapiClient { } } + pub fn is_custom(&self) -> bool { + self.cfg.base != "https://api.telegram.org" + } + pub async fn send_animation + ?Sized>( &self, chat_id: &ChatId, diff --git a/src/push/telegram/text.rs b/src/push/telegram/text.rs index 1cfc2c5..2ea59c8 100644 --- a/src/push/telegram/text.rs +++ b/src/push/telegram/text.rs @@ -5,7 +5,7 @@ use html5ever::{parse_document, ParseOpts, QualName}; use markup5ever_rcdom::{Node, NodeData, RcDom}; use std::collections::BTreeMap; -fn encode_data + ?Sized>(data: &S) -> String { +pub fn encode_data + ?Sized>(data: &S) -> String { data.as_ref() .replace("&", "&") .replace("<", "<") @@ -311,6 +311,10 @@ impl TextSpliter { false } + pub fn is_empty(&self) -> bool { + self.text.is_empty() + } + fn is_in_entities(&self, pos: usize) -> bool { for entity in &self.entities { if pos >= entity.offset && pos < entity.offset + entity.length { diff --git a/src/push/telegram/tg_type.rs b/src/push/telegram/tg_type.rs index 486c0bd..1f53e2b 100644 --- a/src/push/telegram/tg_type.rs +++ b/src/push/telegram/tg_type.rs @@ -119,6 +119,17 @@ where } => panic!("{} ({})", description, error_code), } } + + pub fn to_result(self) -> Result { + match self { + Self::Ok { result, .. } => Ok(result), + Self::Failed { + description, + error_code, + .. + } => Err(format!("{} ({})", description, error_code)), + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/server/push/task/pixiv_send_message.rs b/src/server/push/task/pixiv_send_message.rs index 30cf9c7..424b54d 100644 --- a/src/server/push/task/pixiv_send_message.rs +++ b/src/server/push/task/pixiv_send_message.rs @@ -1,11 +1,18 @@ use super::super::super::preclude::*; -use crate::db::push_task::{AuthorLocation, EveryPushConfig, PushConfig, PushDeerConfig}; +use crate::db::push_task::{ + AuthorLocation, EveryPushConfig, PushConfig, PushDeerConfig, TelegramBackend, + TelegramPushConfig, +}; use crate::error::PixivDownloaderError; use crate::opt::author_name_filter::AuthorFiler; +use crate::parser::description::convert_description_to_tg_html; use crate::parser::description::DescriptionParser; use crate::pixivapp::illust::PixivAppIllust; use crate::push::every_push::{EveryPushClient, EveryPushTextType}; use crate::push::pushdeer::PushdeerClient; +use crate::push::telegram::botapi_client::{BotapiClient, BotapiClientConfig}; +use crate::push::telegram::text::{encode_data, TextSpliter}; +use crate::push::telegram::tg_type::{InputFile, ParseMode, ReplyParametersBuilder}; use crate::utils::{get_file_name_from_url, parse_pixiv_id}; use crate::{get_helper, gettext}; use json::JsonValue; @@ -114,6 +121,7 @@ impl RunContext { match self.cfg.as_ref() { PushConfig::EveryPush(e) => e.filter_author, PushConfig::PushDeer(e) => e.filter_author, + PushConfig::Telegram(e) => e.filter_author, } } @@ -178,6 +186,25 @@ impl RunContext { } } + pub async fn get_input_file( + &self, + index: u64, + download_media: bool, + ) -> Result, PixivDownloaderError> { + if download_media { + match self._get_image_url(index) { + Some(u) => Ok(Some(InputFile::URL(u))), + None => Ok(None), + } + } else { + match self.get_image_url(index).await { + Ok(Some(u)) => Ok(Some(InputFile::URL(u))), + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } + } + pub fn add_author + ?Sized>(&self, text: &mut String, author: &S) { text.push_str(gettext("by ")); let author = author.as_ref(); @@ -247,6 +274,7 @@ impl RunContext { match self.cfg.as_ref() { PushConfig::EveryPush(e) => e.add_ai_tag, PushConfig::PushDeer(e) => e.add_ai_tag, + PushConfig::Telegram(e) => e.add_ai_tag, } } @@ -254,6 +282,7 @@ impl RunContext { match self.cfg.as_ref() { PushConfig::EveryPush(e) => e.add_translated_tag, PushConfig::PushDeer(e) => e.add_translated_tag, + PushConfig::Telegram(e) => e.add_translated_tag, } } @@ -342,7 +371,7 @@ impl RunContext { } } if let Some(desc) = self.desc() { - let mut p = DescriptionParser::new(false); + let mut p = DescriptionParser::new(false, false); p.parse(desc)?; text.push_str(&p.data); text.push_str("\n"); @@ -399,7 +428,7 @@ impl RunContext { } } if let Some(desc) = self.desc() { - let mut p = DescriptionParser::new(true); + let mut p = DescriptionParser::new(true, false); p.parse(desc)?; while !text.ends_with("\n\n") { text.push_str("\n"); @@ -506,7 +535,9 @@ impl RunContext { } } if let Some(desc) = self.desc() { - let mut p = DescriptionParser::builder(true).ensure_link_ascii().build(); + let mut p = DescriptionParser::builder(true, false) + .ensure_link_ascii() + .build(); p.parse(desc)?; while !text.ends_with("\n\n") { text.push_str("\n"); @@ -563,7 +594,9 @@ impl RunContext { } } if let Some(desc) = self.desc() { - let mut p = DescriptionParser::builder(true).ensure_link_ascii().build(); + let mut p = DescriptionParser::builder(true, false) + .ensure_link_ascii() + .build(); p.parse(desc)?; while !text.ends_with("\n\n") { text.push_str("\n"); @@ -593,10 +626,149 @@ impl RunContext { Ok(()) } + pub async fn send_telegram( + &self, + cfg: &TelegramPushConfig, + ) -> Result<(), PixivDownloaderError> { + match &cfg.backend { + TelegramBackend::Botapi(b) => self.send_telegram_botapi(cfg, b).await, + } + } + + pub async fn send_telegram_botapi( + &self, + cfg: &TelegramPushConfig, + b: &BotapiClientConfig, + ) -> Result<(), PixivDownloaderError> { + let c = BotapiClient::new(b); + let mut text = String::new(); + let mut title = self.title().unwrap_or(""); + if title.is_empty() { + title = "Unknown title"; + } + if cfg.add_link_to_title { + if let Some(id) = self.id() { + text += &format!( + "{}", + id, + encode_data(title) + ); + } else { + text += &encode_data(title); + } + } else { + text += &encode_data(title); + } + let author = self.author().map(|a| { + if let Some(uid) = self.user_id() { + format!( + "{}", + uid, + encode_data(&a) + ) + } else { + encode_data(&a) + } + }); + if cfg.author_locations.contains(&AuthorLocation::Title) { + if let Some(author) = &author { + text += " - "; + text += author; + } + } + text += "\n"; + if cfg.author_locations.contains(&AuthorLocation::Top) { + if let Some(a) = &author { + text += a; + text.push('\n'); + } + } + if cfg.add_link { + if let Some(id) = self.id() { + text += &format!("https://www.pixiv.net/artworks/{}\n", id); + } + } + text += &convert_description_to_tg_html(self.desc().unwrap_or(""))?; + text.push('\n'); + if cfg.author_locations.contains(&AuthorLocation::Bottom) { + if let Some(a) = &author { + text += a; + text.push('\n'); + } + } + let mut ts = TextSpliter::builder().max_length(1024).build(); + ts.parse(&text)?; + let len = self.len().unwrap_or(1); + let mut last_message_id: Option = None; + let download_media = cfg.download_media.unwrap_or(c.is_custom()); + if len == 1 { + let f = self + .get_input_file(0, download_media) + .await? + .ok_or("Failed to get image.")?; + let r = match last_message_id { + Some(m) => Some( + ReplyParametersBuilder::default() + .message_id(m) + .build() + .map_err(|_| "Failed to gen.")?, + ), + None => None, + }; + let text = ts.to_html(None); + let m = c + .send_photo( + &cfg.chat_id, + cfg.message_thread_id, + f, + Some(text.as_str()), + Some(ParseMode::HTML), + Some(cfg.show_caption_above_media), + None, + Some(cfg.disable_notification), + Some(cfg.protect_content), + None, + r.as_ref(), + ) + .await? + .to_result()?; + last_message_id = Some(m.message_id); + } + while !ts.is_empty() { + let r = match last_message_id { + Some(m) => Some( + ReplyParametersBuilder::default() + .message_id(m) + .build() + .map_err(|_| "Failed to gen.")?, + ), + None => None, + }; + let text = ts.to_html(Some(4096)); + let m = c + .send_message( + &cfg.chat_id, + cfg.message_thread_id, + &text, + Some(ParseMode::HTML), + None, + Some(cfg.disable_notification), + Some(cfg.protect_content), + None, + r.as_ref(), + ) + .await? + .to_result()?; + last_message_id = Some(m.message_id); + } + Ok(()) + } + pub async fn run(&self) -> Result<(), PixivDownloaderError> { match self.cfg.as_ref() { PushConfig::EveryPush(e) => self.send_every_push(e).await, PushConfig::PushDeer(e) => self.send_push_deer(e).await, + PushConfig::Telegram(e) => self.send_telegram(e).await, } } } diff --git a/src/tmp_cache/mod.rs b/src/tmp_cache/mod.rs new file mode 100644 index 0000000..be57a85 --- /dev/null +++ b/src/tmp_cache/mod.rs @@ -0,0 +1,3 @@ +use crate::get_helper; + +pub struct TmpCache {}