diff --git a/Cargo.lock b/Cargo.lock index ed4d60a..0453a0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,11 +1469,13 @@ dependencies = [ "openssl", "parse-size", "proc_macros", + "rand", "regex", "reqwest", "rusqlite", "serde", "serde_json", + "serde_urlencoded", "tokio", "url", "urlparse", diff --git a/Cargo.toml b/Cargo.toml index 9652ce3..ac15d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,12 +37,14 @@ multipart = { features = ["server"], git = 'https://github.com/lifegpc/multipart openssl = { version = "0.10", optional = true } parse-size = "1" proc_macros = { path = "proc_macros" } +rand = { version = "0", optional = true } regex = "1" reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "socks", "stream"] } rusqlite = { version = "0.29", features = ["bundled", "chrono"], optional = true } RustyXML = "0.3" serde = "1" serde_json = { version = "1", optional = true } +serde_urlencoded = { version = "*", optional = true } tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] } url = "2.3" urlparse = "0.7" @@ -60,7 +62,7 @@ db_all = ["db", "db_sqlite"] db_sqlite = ["rusqlite"] docker = [] exif = ["bindgen", "c_fixed_string", "cmake", "link-cplusplus", "utf16string"] -server = ["async-trait", "base64", "db", "hex", "hyper", "multipart", "openssl", "serde_json"] +server = ["async-trait", "base64", "db", "hex", "hyper", "multipart", "openssl", "serde_json", "rand", "serde_urlencoded"] ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"] [patch.crates-io] diff --git a/src/db/push_task.rs b/src/db/push_task.rs index 4923f07..2273781 100644 --- a/src/db/push_task.rs +++ b/src/db/push_task.rs @@ -1,23 +1,16 @@ +use crate::pixiv_app::PixivRestrictType; use crate::push::every_push::EveryPushTextType; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum PushTaskPixivRestrictType { - Public, - Private, - All, -} - #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum PushTaskPixivAction { Follow { - restrict: PushTaskPixivRestrictType, + restrict: PixivRestrictType, }, Bookmarks { - restrict: PushTaskPixivRestrictType, + restrict: PixivRestrictType, uid: u64, }, Illusts { @@ -31,6 +24,12 @@ pub struct PushTaskPixivConfig { pub act: PushTaskPixivAction, /// Tag translation language pub lang: Option, + /// Whether to use description from Web API when description from APP API is empty. + pub use_web_description: Option, + /// Whether to use Pixiv APP API first. + pub use_app_api: Option, + /// Use data from webpage first. + pub use_webpage: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -39,6 +38,26 @@ pub enum PushTaskConfig { Pixiv(PushTaskPixivConfig), } +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +/// Author location +pub enum AuthorLocation { + /// add author name to title + Title, + /// add author name to the top of description + Top, + /// add author name to the bottom of description + Bottom, +} + +fn defualt_author_locations() -> Vec { + vec![AuthorLocation::Top] +} + +fn default_true() -> bool { + true +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EveryPushConfig { @@ -46,11 +65,17 @@ pub struct EveryPushConfig { pub push_server: String, /// Push token pub push_token: String, - #[serde(rename = "type")] /// Push type pub typ: EveryPushTextType, /// Push topic ID pub topic_id: Option, + #[serde(default = "defualt_author_locations")] + /// Author locations + /// If type is `Image`, this field only support [AuthorLocation::Title]. + pub author_locations: Vec, + #[serde(default = "default_true")] + /// Whether to filter author name + pub filter_author: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -74,3 +99,15 @@ pub struct PushTask { /// Update interval pub ttl: u64, } + +impl PushTask { + pub fn new(config: PushTaskConfig, push_configs: Vec) -> Self { + Self { + id: 0, + config, + push_configs, + last_updated: DateTime::UNIX_EPOCH, + ttl: 0, + } + } +} diff --git a/src/error.rs b/src/error.rs index 2156a37..8f854ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,6 +31,8 @@ pub enum PixivDownloaderError { PixivAppError(crate::pixivapp::error::PixivAppError), #[cfg(feature = "serde_json")] SerdeJsonError(serde_json::Error), + #[cfg(feature = "serde_urlencoded")] + SerdeUrlencodedError(serde_urlencoded::ser::Error), } impl std::error::Error for PixivDownloaderError {} diff --git a/src/opthelper.rs b/src/opthelper.rs index a998dd4..b90af3c 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -573,6 +573,12 @@ impl OptHelper { } false } + + #[cfg(feature = "server")] + /// The server's host name. Used in some proxy. + pub fn server_base(&self) -> Option { + self.settings.get_ref().get_str("server-base") + } } impl Default for OptHelper { diff --git a/src/pixiv_app.rs b/src/pixiv_app.rs index b5724c2..fa60ed5 100644 --- a/src/pixiv_app.rs +++ b/src/pixiv_app.rs @@ -7,17 +7,37 @@ use crate::ext::rw_lock::GetRwLock; use crate::opthelper::OptHelper; use crate::pixivapp::error::handle_error; use crate::pixivapp::illust::PixivAppIllust; +use crate::pixivapp::illusts::PixivAppIllusts; use crate::webclient::{ReqMiddleware, WebClient}; use crate::{get_helper, gettext}; use chrono::{DateTime, Local, SecondsFormat, Utc}; use json::JsonValue; -use reqwest::{Client, Request, RequestBuilder}; +use reqwest::{Client, IntoUrl, Request, RequestBuilder}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ops::Deref; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::RwLock; +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PixivRestrictType { + Public, + Private, + All, +} + +impl ToString for PixivRestrictType { + fn to_string(&self) -> String { + match self { + PixivRestrictType::Public => String::from("public"), + PixivRestrictType::Private => String::from("private"), + PixivRestrictType::All => String::from("all"), + } + } +} + pub struct PixivAppMiddleware { internal: Arc, helper: Arc, @@ -239,6 +259,31 @@ impl PixivAppClientInternal { Ok(()) } + pub async fn get_follow( + &self, + restrict: &PixivRestrictType, + ) -> Result { + self.auto_handle().await?; + let re = self + .client + .get_with_param( + "https://app-api.pixiv.net/v2/illust/follow", + json::object! {"restrict": restrict.to_string()}, + None, + ) + .await + .ok_or(gettext("Failed to get follow."))?; + let obj = handle_error(re).await?; + if get_helper().verbose() { + println!( + "{}{}", + gettext("Follower's new illusts: "), + obj.pretty(2).as_str() + ); + } + Ok(obj) + } + pub async fn get_illust_details(&self, id: u64) -> Result { self.auto_handle().await?; let re = self @@ -275,6 +320,24 @@ impl PixivAppClientInternal { handle_error(re).await?; Ok(()) } + + pub async fn get_url( + &self, + url: U, + err: E, + info: &I, + ) -> Result + where + PixivDownloaderError: From, + { + self.auto_handle().await?; + let re = self.client.get(url, None).await.ok_or(err)?; + let obj = handle_error(re).await?; + if get_helper().verbose() { + println!("{}{}", info, obj.pretty(2).as_str()); + } + Ok(obj) + } } #[derive(Clone)] @@ -314,6 +377,14 @@ impl PixivAppClient { let obj = self.internal.get_illust_details(id).await?; Ok(PixivAppIllust::new(obj["illust"].clone())) } + + pub async fn get_follow( + &self, + restrict: &PixivRestrictType, + ) -> Result { + let obj = self.internal.get_follow(restrict).await?; + PixivAppIllusts::new(self.internal.clone(), obj) + } } impl AsRef for PixivAppClient { diff --git a/src/pixivapp/illusts.rs b/src/pixivapp/illusts.rs new file mode 100644 index 0000000..8144700 --- /dev/null +++ b/src/pixivapp/illusts.rs @@ -0,0 +1,62 @@ +use super::illust::PixivAppIllust; +use crate::error::PixivDownloaderError; +use crate::gettext; +use crate::pixiv_app::PixivAppClientInternal; +use json::JsonValue; +use std::sync::Arc; + +pub struct PixivAppIllusts { + pub illusts: Vec, + next_url: Option, + client: Arc, +} + +impl PixivAppIllusts { + #[allow(dead_code)] + /// Get next page. + /// # Note + /// If no next page presented, will return a error. + pub async fn get_next_page(&self) -> Result { + match &self.next_url { + Some(url) => Self::new( + self.client.clone(), + self.client + .get_url( + url, + gettext("Failed to get next page of the illusts."), + gettext("Illusts data:"), + ) + .await?, + ), + None => Err("No next url, can not get next page.".into()), + } + } + #[allow(dead_code)] + /// Returns true if next page is presented. + pub fn has_next_page(&self) -> bool { + self.next_url.is_some() + } + + pub fn new( + client: Arc, + value: JsonValue, + ) -> Result { + let oillusts = &value["illusts"]; + if !oillusts.is_array() { + return Err(gettext("Failed to parse illust list.").into()); + } + let mut illusts = Vec::new(); + for item in oillusts.members() { + illusts.push(PixivAppIllust::new(item.clone())); + } + let next_url = match value["nextUrl"].as_str() { + Some(next_url) => Some(next_url.to_owned()), + None => None, + }; + Ok(Self { + illusts, + next_url, + client, + }) + } +} diff --git a/src/pixivapp/mod.rs b/src/pixivapp/mod.rs index 7ed4e30..80c4d3b 100644 --- a/src/pixivapp/mod.rs +++ b/src/pixivapp/mod.rs @@ -2,5 +2,6 @@ pub mod check; /// Error handling for pixiv app api pub mod error; pub mod illust; +pub mod illusts; pub mod image_urls; pub mod tag; diff --git a/src/push/every_push.rs b/src/push/every_push.rs index 5a94c82..31fd5fb 100644 --- a/src/push/every_push.rs +++ b/src/push/every_push.rs @@ -33,10 +33,10 @@ pub struct EveryPushClient { } impl EveryPushClient { - pub fn new(server: String) -> Self { + pub fn new + ?Sized>(server: &S) -> Self { Self { client: WebClient::default(), - server, + server: server.as_ref().to_owned(), } } @@ -108,7 +108,7 @@ async fn test_every_push_push() { match std::env::var("EVERY_PUSH_SERVER") { Ok(server) => match std::env::var("EVERY_PUSH_TOKEN") { Ok(token) => { - let client = EveryPushClient::new(server); + let client = EveryPushClient::new(&server); match client .push_message( &token, diff --git a/src/server/context.rs b/src/server/context.rs index 2efc5c5..3799dbb 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -9,10 +9,13 @@ use crate::error::PixivDownloaderError; use crate::ext::json::ToJson2; use crate::get_helper; use crate::gettext; +use crate::pixiv_app::PixivAppClient; +use crate::pixiv_web::PixivWebClient; use futures_util::lock::Mutex; use hyper::{http::response::Builder, Body, Request, Response}; use json::JsonValue; -use std::collections::BTreeMap; +use reqwest::IntoUrl; +use std::collections::{BTreeMap, HashMap}; use std::pin::Pin; use std::sync::Arc; @@ -20,6 +23,8 @@ pub struct ServerContext { pub cors: CorsContext, pub db: Arc>, pub rsa_key: Mutex>, + pub _pixiv_app_client: Mutex>, + pub _pixiv_web_client: Mutex>>, } impl ServerContext { @@ -31,9 +36,53 @@ impl ServerContext { Err(e) => panic!("{} {}", gettext("Failed to open database:"), e), }, rsa_key: Mutex::new(None), + _pixiv_app_client: Mutex::new(None), + _pixiv_web_client: Mutex::new(None), } } + pub async fn generate_pixiv_proxy_url( + &self, + u: U, + ) -> Result { + let u = u.into_url()?; + let host = u.host_str().ok_or("Host not found.")?; + if !host.ends_with(".pximg.net") { + return Err("Host not match.".into()); + } + let helper = get_helper(); + let base = helper + .server_base() + .unwrap_or(format!("http://{}", helper.server())); + let mut map = HashMap::new(); + map.insert("url", u.as_str()); + let secret = self.db.get_proxy_pixiv_secrets().await?; + let mut sha512 = openssl::sha::Sha512::new(); + sha512.update(secret.as_bytes()); + sha512.update("url".as_bytes()); + sha512.update(u.as_str().as_bytes()); + let sign = hex::encode(sha512.finish()); + map.insert("sign", &sign); + let url = format!("{}/proxy/pixiv?{}", base, serde_urlencoded::to_string(map)?); + Ok(url) + } + + pub async fn pixiv_app_client(&self) -> PixivAppClient { + let mut pixiv_app_client = self._pixiv_app_client.lock().await; + if pixiv_app_client.is_none() { + pixiv_app_client.replace(PixivAppClient::with_db(Some(self.db.clone()))); + } + pixiv_app_client.as_ref().unwrap().clone() + } + + pub async fn pixiv_web_client(&self) -> Arc { + let mut pixiv_web_client = self._pixiv_web_client.lock().await; + if pixiv_web_client.is_none() { + pixiv_web_client.replace(Arc::new(PixivWebClient::new())); + } + pixiv_web_client.as_ref().unwrap().clone() + } + pub fn response_json_result( &self, builder: Builder, diff --git a/src/server/push/index.rs b/src/server/push/index.rs index d731e55..397d4be 100644 --- a/src/server/push/index.rs +++ b/src/server/push/index.rs @@ -1,5 +1,6 @@ use super::super::preclude::*; -use crate::db::{PushConfig, PushTaskConfig}; +use super::task::{run_push_task, TestSendMode}; +use crate::db::{PushConfig, PushTask, PushTaskConfig}; use crate::ext::try_err::TryErr3; /// Push task manage action @@ -10,6 +11,8 @@ pub enum PushAction { Change, /// Get a exist push task Get, + /// Test a push task + Test, } pub struct PushContext { @@ -109,6 +112,26 @@ impl PushContext { .try_err3(404, "Push task not found.")?; Ok(serde_json::to_value(re).try_err3(500, "Failed to serialize result:")?) } + PushAction::Test => { + let config = params.get("config").ok_or((400, "Missing config."))?; + let config: PushTaskConfig = + serde_json::from_str(config).try_err3(400, "Failed to parse config:")?; + let push_configs = params + .get("push_configs") + .ok_or((400, "Missing push_configs."))?; + let push_configs: Vec = serde_json::from_str(push_configs) + .try_err3(400, "Failed to parse push_configs:")?; + let task = PushTask::new(config, push_configs); + let test_send_mode = params + .get("test_send_mode") + .ok_or((400, "Missing test_send_mode."))?; + let test_send_mode: TestSendMode = serde_json::from_str(test_send_mode) + .try_err3(400, "Failed to parse test_send_mode:")?; + run_push_task(self.ctx.clone(), &task, Some(&test_send_mode)) + .await + .try_err3(1, "Failed to test push task:")?; + Ok(serde_json::to_value(true).try_err3(500, "Failed to serialize result:")?) + } }, None => { panic!("PushContext::handle: action is None") @@ -163,7 +186,7 @@ pub struct PushRoute { impl PushRoute { pub fn new() -> Self { Self { - regex: Regex::new(r"^(/+api)?/+push(/+(add|change|get))?$").unwrap(), + regex: Regex::new(r"^(/+api)?/+push(/+(add|change|get|test))?$").unwrap(), } } } @@ -194,6 +217,7 @@ impl MatchRoute>> for PushRoute { "add" => Some(PushAction::Add), "change" => Some(PushAction::Change), "get" => Some(PushAction::Get), + "test" => Some(PushAction::Test), _ => None, } } diff --git a/src/server/push/mod.rs b/src/server/push/mod.rs index 1e681a6..1d74b52 100644 --- a/src/server/push/mod.rs +++ b/src/server/push/mod.rs @@ -1,3 +1,4 @@ pub mod index; +pub mod task; pub use index::{PushContext, PushRoute}; diff --git a/src/server/push/task/mod.rs b/src/server/push/task/mod.rs new file mode 100644 index 0000000..ef03e56 --- /dev/null +++ b/src/server/push/task/mod.rs @@ -0,0 +1,66 @@ +pub mod pixiv_follow; +pub mod pixiv_send_message; + +use super::super::preclude::*; +use crate::db::push_task::PushTaskPixivAction; +use crate::db::{PushTask, PushTaskConfig}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TestSendMode { + /// Send first message + First, + /// Send last message + Last, + /// Send random message + Random, + /// Send all messages + All, + #[serde(untagged)] + /// Send specified message + Fixed(u64), +} + +impl TestSendMode { + pub fn is_all(&self) -> bool { + matches!(self, TestSendMode::All) + } + + pub fn to_index(&self, len: usize) -> Option { + match self { + TestSendMode::First => Some(0), + TestSendMode::Last => Some(len - 1), + TestSendMode::Random => { + if len == 0 { + None + } else { + Some(rand::random::() % len) + } + } + TestSendMode::All => None, + TestSendMode::Fixed(index) => { + if *index >= len as u64 { + None + } else { + Some(*index as usize) + } + } + } + } +} + +pub async fn run_push_task( + ctx: Arc, + task: &PushTask, + send_mode: Option<&TestSendMode>, +) -> Result<(), PixivDownloaderError> { + match &task.config { + PushTaskConfig::Pixiv(config) => match &config.act { + PushTaskPixivAction::Follow { restrict } => { + pixiv_follow::run_push_task(ctx, task, config, restrict, send_mode).await + } + _ => Ok(()), + }, + } +} diff --git a/src/server/push/task/pixiv_follow.rs b/src/server/push/task/pixiv_follow.rs new file mode 100644 index 0000000..0e01f7d --- /dev/null +++ b/src/server/push/task/pixiv_follow.rs @@ -0,0 +1,135 @@ +use super::super::super::preclude::*; +use super::pixiv_send_message::send_message; +use super::TestSendMode; +use crate::db::push_task::PushTaskPixivConfig; +use crate::db::PushTask; +use crate::ext::rw_lock::GetRwLock; +use crate::get_helper; +use crate::pixiv_app::PixivRestrictType; +use crate::pixivapp::illust::PixivAppIllust; +use json::JsonValue; +use std::collections::HashMap; +use std::sync::RwLock; + +struct PixivFollowData { + web_list: RwLock>, + web_data: RwLock>, +} + +impl PixivFollowData { + pub fn new() -> Self { + Self { + web_list: RwLock::new(None), + web_data: RwLock::new(HashMap::new()), + } + } + + pub fn get_web_data(&self, id: u64) -> Option { + self.web_data.get_ref().get(&id).cloned() + } + + pub fn set_web_data(&self, id: u64, data: JsonValue) { + self.web_data.get_mut().insert(id, data); + } +} + +struct RunContext<'a> { + ctx: Arc, + task: &'a PushTask, + config: &'a PushTaskPixivConfig, + restrict: &'a PixivRestrictType, + send_mode: Option<&'a TestSendMode>, + data: Arc, + use_app_api: bool, + use_web_description: bool, + use_webpage: bool, +} + +impl<'a> RunContext<'a> { + pub fn new( + ctx: Arc, + task: &'a PushTask, + config: &'a PushTaskPixivConfig, + restrict: &'a PixivRestrictType, + send_mode: Option<&'a TestSendMode>, + ) -> Self { + let helper = get_helper(); + Self { + ctx, + task, + config, + restrict, + send_mode, + data: Arc::new(PixivFollowData::new()), + use_app_api: config.use_app_api.unwrap_or(helper.use_app_api()), + use_web_description: config + .use_web_description + .unwrap_or(helper.use_web_description()), + use_webpage: config.use_webpage.unwrap_or(helper.use_webpage()), + } + } + + pub async fn run(&self) -> Result<(), PixivDownloaderError> { + if self.use_app_api { + self.app_run().await?; + } + Ok(()) + } + + pub async fn app_run(&self) -> Result<(), PixivDownloaderError> { + let app = self.ctx.pixiv_app_client().await; + let app_data = app.get_follow(self.restrict).await?; + match self.send_mode { + Some(m) => { + if m.is_all() { + for i in app_data.illusts.iter() { + self.app_illust(i).await?; + } + } else { + let index = m.to_index(app_data.illusts.len()); + if let Some(index) = index { + self.app_illust(&app_data.illusts[index]).await?; + } + } + } + None => {} + } + Ok(()) + } + + pub async fn app_illust(&self, illust: &PixivAppIllust) -> Result<(), PixivDownloaderError> { + let id = illust.id().ok_or("illust id is none")?; + let data = match self.data.get_web_data(id) { + Some(d) => Some(d), + None => { + if self.use_web_description && illust.caption_is_empty() { + let pw = self.ctx.pixiv_web_client().await; + match pw.get_artwork_ajax(id).await { + Some(data) => { + self.data.set_web_data(id, data.clone()); + Some(data) + } + None => None, + } + } else { + None + } + } + }; + for i in self.task.push_configs.iter() { + send_message(self.ctx.clone(), Some(&illust), data.as_ref(), i).await?; + } + Ok(()) + } +} + +pub async fn run_push_task( + ctx: Arc, + task: &PushTask, + config: &PushTaskPixivConfig, + restrict: &PixivRestrictType, + send_mode: Option<&TestSendMode>, +) -> Result<(), PixivDownloaderError> { + let ctx = RunContext::new(ctx, task, config, restrict, send_mode); + ctx.run().await +} diff --git a/src/server/push/task/pixiv_send_message.rs b/src/server/push/task/pixiv_send_message.rs new file mode 100644 index 0000000..6e410b4 --- /dev/null +++ b/src/server/push/task/pixiv_send_message.rs @@ -0,0 +1,126 @@ +use super::super::super::preclude::*; +use crate::db::push_task::{AuthorLocation, EveryPushConfig, PushConfig}; +use crate::error::PixivDownloaderError; +use crate::get_helper; +use crate::opt::author_name_filter::AuthorFiler; +use crate::pixivapp::illust::PixivAppIllust; +use crate::push::every_push::{EveryPushClient, EveryPushTextType}; +use json::JsonValue; + +struct RunContext<'a> { + ctx: Arc, + illust: Option<&'a PixivAppIllust>, + data: Option<&'a JsonValue>, + cfg: &'a PushConfig, +} + +impl<'a> RunContext<'a> { + pub fn title(&self) -> Option<&str> { + match self.illust { + Some(i) => match i.title() { + Some(t) => return Some(t), + None => {} + }, + None => {} + } + match self.data { + Some(d) => d["illustTitle"].as_str(), + None => None, + } + } + + pub fn _author(&self) -> Option<&str> { + match self.illust { + Some(i) => match i.user_name() { + Some(u) => return Some(u), + None => {} + }, + None => {} + } + match self.data { + Some(d) => d["userName"].as_str(), + None => None, + } + } + + pub fn author(&self) -> Option { + match self._author() { + Some(a) => { + if self.filter_author() { + match get_helper().author_name_filters() { + Some(l) => return Some(l.filter(a)), + None => {} + } + } + Some(a.to_owned()) + } + None => None, + } + } + + /// Whether to filter author name + pub fn filter_author(&self) -> bool { + match self.cfg { + PushConfig::EveryPush(e) => e.filter_author, + } + } + + pub async fn send_every_push(&self, cfg: &EveryPushConfig) -> Result<(), PixivDownloaderError> { + let client = EveryPushClient::new(&cfg.push_server); + match cfg.typ { + EveryPushTextType::Text => {} + EveryPushTextType::Markdown => {} + EveryPushTextType::Image => { + let mut title = self.title().map(|s| s.to_owned()); + if cfg.author_locations.contains(&AuthorLocation::Title) { + if let Some(t) = &title { + if let Some(a) = self.author() { + title = Some(format!("{} - {}", t, a)); + } + } + } + let url = match self.illust { + Some(i) => i.original_image_url().map(|s| s.to_owned()).or_else(|| { + match i.meta_pages().get(0) { + Some(p) => p.original().map(|s| s.to_owned()), + None => None, + } + }), + None => None, + }; + let url = url.ok_or("image url not found.")?; + let url = self.ctx.generate_pixiv_proxy_url(url).await?; + client + .push_message( + &cfg.push_token, + &url, + title.as_ref(), + cfg.topic_id.as_ref(), + Some(EveryPushTextType::Image), + ) + .await?; + } + } + Ok(()) + } + pub async fn run(&self) -> Result<(), PixivDownloaderError> { + match self.cfg { + PushConfig::EveryPush(e) => self.send_every_push(e).await, + } + } +} + +pub async fn send_message( + ctx: Arc, + illust: Option<&PixivAppIllust>, + data: Option<&JsonValue>, + cfg: &PushConfig, +) -> Result<(), PixivDownloaderError> { + let ctx = RunContext { + ctx, + illust, + data, + cfg, + }; + ctx.run().await +} diff --git a/src/server/unittest/mod.rs b/src/server/unittest/mod.rs index 8740158..856f515 100644 --- a/src/server/unittest/mod.rs +++ b/src/server/unittest/mod.rs @@ -40,6 +40,8 @@ impl UnitTestContext { .unwrap(), ), rsa_key: Mutex::new(None), + _pixiv_app_client: Mutex::new(None), + _pixiv_web_client: Mutex::new(None), }), routes: ServerRoutes::new(), } diff --git a/src/settings_list.rs b/src/settings_list.rs index 3fe46f1..26b222d 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -67,6 +67,8 @@ pub fn get_settings_list() -> Vec { SettingDes::new("use-app-api", gettext("Whether to use Pixiv APP API first."), JsonValueType::Boolean, None).unwrap(), SettingDes::new("use-web-description", gettext("Whether to use description from Web API when description from APP API is empty."), JsonValueType::Boolean, None).unwrap(), SettingDes::new("add-history", gettext("Whether to add artworks to pixiv's history. Only works for APP API."), JsonValueType::Boolean, None).unwrap(), + #[cfg(feature = "server")] + SettingDes::new("server-base", gettext("The server's host name. Used in some proxy."), JsonValueType::Str, None).unwrap(), ] }