From d8c0cac94c825921155a41b539f98728298a84f5 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 24 Oct 2023 08:14:15 +0000 Subject: [PATCH] Update --- Cargo.lock | 2 + Cargo.toml | 5 +- src/db/mod.rs | 42 ++++++-- src/db/push_task.rs | 76 ++++++++++++++ src/db/sqlite/db.rs | 195 +++++++++++++++++++++++++++++++++- src/db/traits.rs | 67 ++++++++++-- src/error.rs | 2 + src/ext/json.rs | 2 +- src/push/every_push.rs | 12 ++- src/server/context.rs | 32 +++++- src/server/mod.rs | 2 + src/server/preclude.rs | 2 +- src/server/push/index.rs | 221 +++++++++++++++++++++++++++++++++++++++ src/server/push/mod.rs | 3 + src/server/result.rs | 35 +++++++ src/server/route.rs | 2 + 16 files changed, 675 insertions(+), 25 deletions(-) create mode 100644 src/db/push_task.rs create mode 100644 src/server/push/index.rs create mode 100644 src/server/push/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5186ad0..9cc8f73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1472,6 +1473,7 @@ dependencies = [ "reqwest", "rusqlite", "serde", + "serde_json", "tokio", "url", "urlparse", diff --git a/Cargo.toml b/Cargo.toml index 049d01e..9652ce3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ base64 = { version = "0.21", optional = true } bytes = { version = "1.4", optional = true } c_fixed_string = { version = "0.2", optional = true } cfg-if = "1" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } dateparser = "0.2.0" derive_more = "0.99" fancy-regex = "0.11" @@ -42,6 +42,7 @@ reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "socks", rusqlite = { version = "0.29", features = ["bundled", "chrono"], optional = true } RustyXML = "0.3" serde = "1" +serde_json = { version = "1", optional = true } tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] } url = "2.3" urlparse = "0.7" @@ -59,7 +60,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"] +server = ["async-trait", "base64", "db", "hex", "hyper", "multipart", "openssl", "serde_json"] ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"] [patch.crates-io] diff --git a/src/db/mod.rs b/src/db/mod.rs index 175fe14..723df9e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,7 @@ pub mod config; pub mod pixiv_artworks; +#[cfg(feature = "server")] +pub mod push_task; #[cfg(feature = "db_sqlite")] pub mod sqlite; #[cfg(feature = "server")] @@ -13,6 +15,8 @@ pub use config::PixivDownloaderDbConfig; #[cfg(feature = "db_sqlite")] pub use config::PixivDownloaderSqliteConfig; pub use pixiv_artworks::{PixivArtwork, PixivArtworkLock}; +#[cfg(feature = "server")] +pub use push_task::{PushConfig, PushTask, PushTaskConfig}; #[cfg(feature = "db_sqlite")] pub use sqlite::{PixivDownloaderSqlite, SqliteError}; #[cfg(feature = "server")] @@ -22,15 +26,15 @@ pub use traits::PixivDownloaderDb; pub use user::User; #[derive(Debug, derive_more::Display)] -pub struct PixivDownloaderDbError { - e: anyhow::Error, +pub enum PixivDownloaderDbError { + AnyHow(anyhow::Error), + #[cfg(feature = "db_sqlite")] + Sqlite(SqliteError), } impl PixivDownloaderDbError { pub fn msg(msg: S) -> Self { - Self { - e: anyhow::Error::msg(msg), - } + Self::AnyHow(anyhow::Error::msg(msg)) } } @@ -39,7 +43,7 @@ where T: Into, { fn from(e: T) -> Self { - Self { e: e.into() } + Self::AnyHow(e.into()) } } @@ -48,7 +52,31 @@ use crate::gettext; #[cfg(feature = "db_sqlite")] impl From for PixivDownloaderDbError { fn from(e: SqliteError) -> Self { - PixivDownloaderDbError::msg(e) + PixivDownloaderDbError::Sqlite(e) + } +} + +#[cfg(all(feature = "db_sqlite", feature = "server"))] +pub trait Optional2Extension { + fn optional2(self) -> Result, PixivDownloaderDbError>; +} + +#[cfg(all(feature = "db_sqlite", feature = "server"))] +impl Optional2Extension for Result { + fn optional2(self) -> Result, PixivDownloaderDbError> { + match self { + Ok(v) => Ok(Some(v)), + Err(e) => match e { + PixivDownloaderDbError::Sqlite(e) => match e { + SqliteError::DbError(e) => match e { + rusqlite::Error::QueryReturnedNoRows => Ok(None), + _ => Err(PixivDownloaderDbError::Sqlite(SqliteError::DbError(e))), + }, + _ => Err(PixivDownloaderDbError::Sqlite(e)), + }, + _ => Err(e), + }, + } } } diff --git a/src/db/push_task.rs b/src/db/push_task.rs new file mode 100644 index 0000000..4923f07 --- /dev/null +++ b/src/db/push_task.rs @@ -0,0 +1,76 @@ +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, + }, + Bookmarks { + restrict: PushTaskPixivRestrictType, + uid: u64, + }, + Illusts { + uid: u64, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PushTaskPixivConfig { + pub act: PushTaskPixivAction, + /// Tag translation language + pub lang: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum PushTaskConfig { + Pixiv(PushTaskPixivConfig), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EveryPushConfig { + /// Push server + 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, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum PushConfig { + EveryPush(EveryPushConfig), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PushTask { + /// The task ID + pub id: u64, + /// Configurations of the task + pub config: PushTaskConfig, + /// Push configurations + pub push_configs: Vec, + #[serde(with = "chrono::serde::ts_seconds")] + /// Last updated time + pub last_updated: DateTime, + /// Update interval + pub ttl: u64, +} diff --git a/src/db/sqlite/db.rs b/src/db/sqlite/db.rs index 21c5a80..967f5d7 100644 --- a/src/db/sqlite/db.rs +++ b/src/db/sqlite/db.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "server")] +use super::super::Optional2Extension; use super::super::{PixivArtwork, PixivArtworkLock}; use super::super::{ PixivDownloaderDb, PixivDownloaderDbConfig, PixivDownloaderDbError, PixivDownloaderSqliteConfig, }; #[cfg(feature = "server")] +use super::super::{PushConfig, PushTask, PushTaskConfig}; +#[cfg(feature = "server")] use super::super::{Token, User}; use super::SqliteError; use bytes::BytesMut; @@ -51,6 +55,13 @@ id INT, file_id INT, page INT );"; +const PUSH_TASK_TABLE: &'static str = "CREATE TABLE push_task ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +config TEXT, +push_configs TEXT, +last_updated DATETIME, +ttl INT +);"; const TAGS_TABLE: &'static str = "CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT @@ -82,7 +93,7 @@ v3 INT, v4 INT, PRIMARY KEY (id) );"; -const VERSION: [u8; 4] = [1, 0, 0, 6]; +const VERSION: [u8; 4] = [1, 0, 0, 7]; pub struct PixivDownloaderSqlite { db: Mutex, @@ -116,6 +127,29 @@ impl PixivDownloaderSqlite { Ok(()) } + #[cfg(feature = "server")] + fn _add_push_task( + ts: &Transaction, + config: &PushTaskConfig, + push_configs: &[PushConfig], + ttl: u64, + ) -> Result { + ts.execute( + "INSERT INTO push_task (config, push_configs, last_updated, ttl) VALUES (?, ?, ?, ?);", + ( + serde_json::to_string(config)?, + serde_json::to_string(push_configs)?, + DateTime::UNIX_EPOCH, + ttl, + ), + )?; + Ok(ts.query_row( + "SELECT seq FROM sqlite_sequence WHERE name = 'push_task';", + [], + |row| Ok(row.get(0)?), + )?) + } + #[cfg(feature = "server")] async fn _add_root_user( &self, @@ -215,6 +249,9 @@ impl PixivDownloaderSqlite { if db_version < [1, 0, 0, 6] { tx.execute(CONFIG_TABLE, [])?; } + if db_version < [1, 0, 0, 7] { + tx.execute(PUSH_TASK_TABLE, [])?; + } self._write_version(&tx)?; tx.commit()?; } @@ -247,6 +284,9 @@ impl PixivDownloaderSqlite { if !tables.contains_key("pixiv_files") { t.execute(PIXIV_FILES_TABLE, [])?; } + if !tables.contains_key("push_task") { + t.execute(PUSH_TASK_TABLE, [])?; + } if !tables.contains_key("tags") { t.execute(TAGS_TABLE, [])?; } @@ -342,6 +382,29 @@ impl PixivDownloaderSqlite { .optional()?) } + #[cfg(feature = "server")] + async fn get_push_task(&self, id: u64) -> Result, PixivDownloaderDbError> { + let con = self.db.lock().await; + con.query_row_and_then::( + "SELECT * FROM push_task WHERE id = ?;", + [id], + |row| { + let config: String = row.get(1)?; + let config: PushTaskConfig = serde_json::from_str(&config)?; + let push_configs: String = row.get(2)?; + let push_configs: Vec = serde_json::from_str(&push_configs)?; + Ok(PushTask { + id: row.get(0)?, + config, + push_configs, + last_updated: row.get(3)?, + ttl: row.get(4)?, + }) + }, + ) + .optional2() + } + #[cfg(feature = "server")] async fn get_token(&self, id: u64) -> Result, SqliteError> { let con = self.db.lock().await; @@ -554,6 +617,56 @@ impl PixivDownloaderSqlite { } } + #[cfg(feature = "server")] + fn _update_push_task( + tx: &Transaction, + id: u64, + config: Option<&PushTaskConfig>, + push_configs: Option<&[PushConfig]>, + ttl: Option, + ) -> Result<(), PixivDownloaderDbError> { + match config { + Some(config) => { + let config = serde_json::to_string(config)?; + tx.execute( + "UPDATE push_task SET config = ? WHERE id = ?;", + (config, id), + )?; + } + None => {} + } + match push_configs { + Some(push_configs) => { + let push_configs = serde_json::to_string(push_configs)?; + tx.execute( + "UPDATE push_task SET push_configs = ? WHERE id = ?;", + (push_configs, id), + )?; + } + None => {} + } + match ttl { + Some(ttl) => { + tx.execute("UPDATE push_task SET ttl = ? WHERE id = ?;", (ttl, id))?; + } + None => {} + } + Ok(()) + } + + #[cfg(feature = "server")] + fn _update_push_task_last_updated( + tx: &Transaction, + id: u64, + last_updated: &DateTime, + ) -> Result<(), PixivDownloaderDbError> { + tx.execute( + "UPDATE push_task SET last_updated = ? WHERE id = ?;", + (last_updated, id), + )?; + Ok(()) + } + #[cfg(feature = "server")] async fn _update_user( &self, @@ -677,6 +790,23 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { Ok(self.get_pixiv_artwork(id).await?.expect("User not found:")) } + #[cfg(feature = "server")] + async fn add_push_task( + &self, + config: &PushTaskConfig, + push_configs: &[PushConfig], + ttl: u64, + ) -> Result { + let task = { + let mut db = self.db.lock().await; + let tx = db.transaction()?; + let task = Self::_add_push_task(&tx, config, push_configs, ttl)?; + tx.commit()?; + task + }; + Ok(self.get_push_task(task).await?.expect("Task not found:")) + } + #[cfg(feature = "server")] async fn add_root_user( &self, @@ -770,6 +900,32 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { Ok(self.get_config(key).await?) } + async fn get_config_or_set_default( + &self, + key: &str, + default: fn() -> Result, + ) -> Result { + let mut db = self.db.lock().await; + let tx = db.transaction()?; + let value = tx + .query_row("SELECT value FROM config WHERE key = ?;", [key], |row| { + row.get(0) + }) + .optional()?; + match value { + Some(v) => { + tx.commit()?; + Ok(v) + } + None => { + let v = default()?; + Self::_set_config(&tx, key, &v)?; + tx.commit()?; + Ok(v) + } + } + } + async fn get_pixiv_artwork( &self, id: u64, @@ -777,6 +933,11 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { Ok(self.get_pixiv_artwork(id).await?) } + #[cfg(feature = "server")] + async fn get_push_task(&self, id: u64) -> Result, PixivDownloaderDbError> { + Ok(self.get_push_task(id).await?) + } + #[cfg(feature = "server")] async fn get_token(&self, id: u64) -> Result, PixivDownloaderDbError> { Ok(self.get_token(id).await?) @@ -851,6 +1012,38 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { .await?) } + #[cfg(feature = "server")] + async fn update_push_task( + &self, + id: u64, + config: Option<&PushTaskConfig>, + push_configs: Option<&[PushConfig]>, + ttl: Option, + ) -> Result { + { + let mut db = self.db.lock().await; + let tx = db.transaction()?; + Self::_update_push_task(&tx, id, config, push_configs, ttl)?; + tx.commit()?; + } + Ok(self.get_push_task(id).await?.expect("Task not found:")) + } + + #[cfg(feature = "server")] + async fn update_push_task_last_updated( + &self, + id: u64, + last_updated: &DateTime, + ) -> Result<(), PixivDownloaderDbError> { + { + let mut db = self.db.lock().await; + let mut tx = db.transaction()?; + Self::_update_push_task_last_updated(&mut tx, id, last_updated)?; + tx.commit()?; + } + Ok(()) + } + #[cfg(feature = "server")] async fn update_user( &self, diff --git a/src/db/traits.rs b/src/db/traits.rs index 1b4acf8..88a08d5 100644 --- a/src/db/traits.rs +++ b/src/db/traits.rs @@ -2,6 +2,8 @@ use super::PixivDownloaderDbConfig; use super::PixivDownloaderDbError; use super::{PixivArtwork, PixivArtworkLock}; #[cfg(feature = "server")] +use super::{PushConfig, PushTask, PushTaskConfig}; +#[cfg(feature = "server")] use super::{Token, User}; use chrono::{DateTime, Utc}; use flagset::FlagSet; @@ -36,6 +38,17 @@ pub trait PixivDownloaderDb { lock: &FlagSet, ) -> Result; #[cfg(feature = "server")] + /// Add a push task + /// * `config` - The task's config + /// * `push_configs` - The task's push configurations + /// * `ttl` - The task's update interval + async fn add_push_task( + &self, + config: &PushTaskConfig, + push_configs: &[PushConfig], + ttl: u64, + ) -> Result; + #[cfg(feature = "server")] /// Add root user to database. /// * `name` - User name /// * `username` - Unique user name @@ -98,6 +111,14 @@ pub trait PixivDownloaderDb { /// Get a config from database /// * `key` - The config key async fn get_config(&self, key: &str) -> Result, PixivDownloaderDbError>; + /// Get a config from database or set a default value to database and return + /// * `key` - The config key + /// * `default` - The function to return default value + async fn get_config_or_set_default( + &self, + key: &str, + default: fn() -> Result, + ) -> Result; /// Get an artwork from database /// * `id` - The artwork ID async fn get_pixiv_artwork( @@ -107,19 +128,21 @@ pub trait PixivDownloaderDb { #[cfg(feature = "server")] /// Get proxy pixiv secrets async fn get_proxy_pixiv_secrets(&self) -> Result { - let key = "proxy_pixiv_secrets"; - match self.get_config(key).await? { - Some(v) => Ok(v), - None => { - let mut buf = [0; 32]; - openssl::rand::rand_bytes(&mut buf)?; - let v = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &buf); - self.set_config(key, &v).await?; - Ok(v) - } - } + self.get_config_or_set_default("proxy_pixiv_secrets", || { + let mut buf = [0; 32]; + openssl::rand::rand_bytes(&mut buf)?; + Ok(base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &buf, + )) + }) + .await } #[cfg(feature = "server")] + /// Get a push task by ID + /// * `id` - The task's ID + async fn get_push_task(&self, id: u64) -> Result, PixivDownloaderDbError>; + #[cfg(feature = "server")] /// Get token by ID /// * `id` - The token ID async fn get_token(&self, id: u64) -> Result, PixivDownloaderDbError>; @@ -181,6 +204,28 @@ pub trait PixivDownloaderDb { is_admin: bool, ) -> Result; #[cfg(feature = "server")] + /// Update a push task + /// * `id`: The task's ID + /// * `config`: The task's config + /// * `push_configs`: The task's push configurations + /// * `ttl`: The task's update interval + async fn update_push_task( + &self, + id: u64, + config: Option<&PushTaskConfig>, + push_configs: Option<&[PushConfig]>, + ttl: Option, + ) -> Result; + #[cfg(feature = "server")] + /// Update a push task's last updated time + /// * `id`: The task's ID + /// * `last_updated`: The task's last updated time + async fn update_push_task_last_updated( + &self, + id: u64, + last_updated: &DateTime, + ) -> Result<(), PixivDownloaderDbError>; + #[cfg(feature = "server")] /// Update a user's information /// * `id`: The user's ID /// * `name`: The user's name diff --git a/src/error.rs b/src/error.rs index acf76d2..2156a37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,6 +29,8 @@ pub enum PixivDownloaderError { ParseIntError(std::num::ParseIntError), ReqwestError(reqwest::Error), PixivAppError(crate::pixivapp::error::PixivAppError), + #[cfg(feature = "serde_json")] + SerdeJsonError(serde_json::Error), } impl std::error::Error for PixivDownloaderError {} diff --git a/src/ext/json.rs b/src/ext/json.rs index 725fc4b..ddd9e60 100644 --- a/src/ext/json.rs +++ b/src/ext/json.rs @@ -53,7 +53,7 @@ impl ToJson for &T { } } -impl ToJson2 for &T { +impl ToJson2 for &T { fn to_json2(&self) -> JsonValue { (*self).to_json2() } diff --git a/src/push/every_push.rs b/src/push/every_push.rs index 9bdae02..5a94c82 100644 --- a/src/push/every_push.rs +++ b/src/push/every_push.rs @@ -1,7 +1,9 @@ use crate::webclient::WebClient; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] /// Text type pub enum EveryPushTextType { Text, @@ -42,6 +44,7 @@ impl EveryPushClient { /// * `push_token` - push token /// * `text` - text /// * `title` - title + /// * `topic_id` - topic id /// * `typ` - text type /// /// For more information, see [API document](https://github.com/PeanutMelonSeedBigAlmond/EveryPush.Server/blob/main/api.md#推送消息) @@ -49,11 +52,13 @@ impl EveryPushClient { P: AsRef + ?Sized, T: AsRef + ?Sized, I: AsRef + ?Sized, + O: AsRef + ?Sized, >( &self, push_token: &P, text: &T, title: Option<&I>, + topic_id: Option<&O>, typ: Option, ) -> Result<(), String> { let mut params = HashMap::new(); @@ -63,6 +68,10 @@ impl EveryPushClient { Some(t) => params.insert("title", t.as_ref()), None => None, }; + match topic_id { + Some(t) => params.insert("topicId", t.as_ref()), + None => None, + }; match &typ { Some(t) => params.insert("type", t.as_ref()), None => None, @@ -105,6 +114,7 @@ async fn test_every_push_push() { &token, "Push Test", Some("Push"), + Some("test"), Some(EveryPushTextType::Text), ) .await diff --git a/src/server/context.rs b/src/server/context.rs index a2e6ff0..2efc5c5 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -1,7 +1,9 @@ use super::auth::RSAKey; +use super::body::hyper::HyperBody; use super::cors::CorsContext; use super::params::RequestParams; -use super::result::JSONResult; +use super::preclude::HttpBodyType; +use super::result::{JSONResult, SerdeJSONResult, SerdeJSONResult2}; use crate::db::{open_and_init_database, PixivDownloaderDb, Token, User}; use crate::error::PixivDownloaderError; use crate::ext::json::ToJson2; @@ -11,6 +13,7 @@ use futures_util::lock::Mutex; use hyper::{http::response::Builder, Body, Request, Response}; use json::JsonValue; use std::collections::BTreeMap; +use std::pin::Pin; use std::sync::Arc; pub struct ServerContext { @@ -53,6 +56,33 @@ impl ServerContext { Ok(builder.body(re.to_json2())?) } + pub fn response_serde_json_result( + &self, + builder: Builder, + re: SerdeJSONResult, + ) -> Result>>, PixivDownloaderError> { + let builder = match &re { + Ok(_) => builder, + Err(err) => { + if err.code <= -400 && err.code >= -600 { + builder.status((-err.code) as u16) + } else if err.code < 0 { + builder.status(500) + } else if err.code > 0 { + builder.status(400) + } else { + builder + } + } + }; + let s = SerdeJSONResult2::new(re); + Ok( + builder.body::>>(Box::pin(HyperBody::from( + serde_json::to_string(&s)?, + )))?, + ) + } + pub async fn verify_token2( &self, req: &Request, diff --git a/src/server/mod.rs b/src/server/mod.rs index fa2ae09..f2f68b7 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -11,6 +11,8 @@ pub mod params; pub mod preclude; /// Routes about proxy pub mod proxy; +/// Push tasks management +pub mod push; /// Base result type for JSON response pub mod result; /// Routes diff --git a/src/server/preclude.rs b/src/server/preclude.rs index c44837c..7ef367a 100644 --- a/src/server/preclude.rs +++ b/src/server/preclude.rs @@ -2,7 +2,7 @@ pub use super::body::hyper::HyperBody; pub use super::body::response::ResponseBody; pub use super::context::ServerContext; pub use super::params::RequestParams; -pub use super::result::JSONResult; +pub use super::result::{JSONResult, SerdeJSONResult}; pub use super::route::ResponseForType; pub use super::traits::{GetRequestParams, MatchRoute, ResponseFor, ResponseJsonFor}; pub use crate::error::PixivDownloaderError; diff --git a/src/server/push/index.rs b/src/server/push/index.rs new file mode 100644 index 0000000..5436441 --- /dev/null +++ b/src/server/push/index.rs @@ -0,0 +1,221 @@ +use super::super::preclude::*; +use crate::db::{PushConfig, PushTaskConfig}; +use crate::ext::try_err::TryErr3; + +/// Push task manage action +pub enum PushAction { + /// Add a new push task + Add, + /// Change a exist push task + Change, + /// Get a exist push task + Get, +} + +pub struct PushContext { + ctx: Arc, + action: Option, + is_restful: bool, +} + +impl PushContext { + pub fn new(ctx: Arc, action: Option, is_restful: bool) -> Self { + Self { + ctx, + action, + is_restful, + } + } + + async fn handle(&self, mut req: Request) -> SerdeJSONResult { + let params = req + .get_params() + .await + .try_err3(400, "Failed to get parameters:")?; + let t = self + .ctx + .verify(&req, ¶ms) + .await + .try_err3(401, "Unauthorized")?; + if t.is_some_and(|t| !t.is_admin) { + return Err((403, "Permission denied.").into()); + } + match &self.action { + Some(a) => match a { + PushAction::Add => { + 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 ttl = params + .get_u64("ttl") + .try_err3(400, "Bad ttl.")? + .unwrap_or(300); + 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 re = self + .ctx + .db + .add_push_task(&config, &push_configs, ttl) + .await + .try_err3(500, "Failed to add push task:")?; + Ok(serde_json::to_value(re).try_err3(500, "Failed to serialize result:")?) + } + PushAction::Change => { + let id = params + .get_u64("id") + .try_err3(400, "Bad id.")? + .try_err3(400, "Missing id.")?; + let config = match params.get("config") { + Some(v) => Some( + serde_json::from_str::(v) + .try_err3(400, "Failed to parse config:")?, + ), + None => None, + }; + let ttl = params.get_u64("ttl").try_err3(400, "Bad ttl")?; + let push_configs = match params.get("push_configs") { + Some(v) => Some( + serde_json::from_str::>(v) + .try_err3(400, "Failed to parse push_configs:")?, + ), + None => None, + }; + let push_configs = match &push_configs { + Some(v) => Some(v.as_ref()), + None => None, + }; + let re = self + .ctx + .db + .update_push_task(id, config.as_ref(), push_configs, ttl) + .await + .try_err3(500, "Failed to change push task:")?; + Ok(serde_json::to_value(re).try_err3(500, "Failed to serialize result:")?) + } + PushAction::Get => { + let id = params + .get_u64("id") + .try_err3(400, "Bad id.")? + .try_err3(400, "Missing id.")?; + let re = self + .ctx + .db + .get_push_task(id) + .await + .try_err3(500, "Failed to get push task:")?; + Ok(serde_json::to_value(re).try_err3(500, "Failed to serialize result:")?) + } + }, + None => { + panic!("PushContext::handle: action is None") + } + } + } +} + +#[async_trait] +impl ResponseFor>> for PushContext { + async fn response( + &self, + req: Request, + ) -> Result>>, PixivDownloaderError> { + let builder = if self.is_restful { + filter_http_methods!( + req, + Box::pin(HyperBody::empty()), + true, + self.ctx, + allow_headers = [CONTENT_TYPE, X_SIGN, X_TOKEN_ID], + typ_def = Pin>, + GET, + OPTIONS, + PATCH, + PUT, + ); + builder + } else { + filter_http_methods!( + req, + Box::pin(HyperBody::empty()), + true, + self.ctx, + allow_headers = [CONTENT_TYPE, X_SIGN, X_TOKEN_ID], + typ_def = Pin>, + GET, + OPTIONS, + POST, + ); + builder + }; + let re = self.handle(req).await; + self.ctx.response_serde_json_result(builder, re) + } +} + +pub struct PushRoute { + regex: Regex, +} + +impl PushRoute { + pub fn new() -> Self { + Self { + regex: Regex::new(r"^(/+api)?/+push(/+(add|change|get))?$").unwrap(), + } + } +} + +impl MatchRoute>> for PushRoute { + fn match_route( + &self, + ctx: &Arc, + req: &http::Request, + ) -> Option> { + let path = req.uri().path(); + let pat = self.regex.captures(path); + match pat { + Some(cap) => { + if req.method() == Method::OPTIONS { + return Some(Box::new(PushContext::new( + Arc::clone(ctx), + None, + cap.get(2).is_none(), + ))); + } + let cap2 = cap.get(2); + let is_restful = cap2.is_none(); + let action = match cap2 { + Some(m) => { + let m = m.as_str().trim_start_matches("/"); + match m { + "add" => Some(PushAction::Add), + "change" => Some(PushAction::Change), + "get" => Some(PushAction::Get), + _ => None, + } + } + None => { + let m = req.method(); + if m == Method::PUT { + Some(PushAction::Add) + } else if m == Method::GET { + Some(PushAction::Get) + } else if m == Method::PATCH { + Some(PushAction::Change) + } else { + None + } + } + }; + Some(Box::new(PushContext::new( + Arc::clone(ctx), + action, + is_restful, + ))) + } + None => None, + } + } +} diff --git a/src/server/push/mod.rs b/src/server/push/mod.rs new file mode 100644 index 0000000..1e681a6 --- /dev/null +++ b/src/server/push/mod.rs @@ -0,0 +1,3 @@ +pub mod index; + +pub use index::{PushContext, PushRoute}; diff --git a/src/server/result.rs b/src/server/result.rs index c9ffab6..6239409 100644 --- a/src/server/result.rs +++ b/src/server/result.rs @@ -2,6 +2,8 @@ use crate::ext::json::ToJson2; #[cfg(test)] use crate::ext::json::{FromJson, ToJson}; use json::JsonValue; +use serde::ser::SerializeMap; +use serde::Serialize; #[derive(Clone, Debug)] /// Error information of a request @@ -85,6 +87,7 @@ impl From<(i32, String, String)> for JSONError { } pub type JSONResult = Result; +pub type SerdeJSONResult = Result; impl ToJson2 for JSONResult { fn to_json2(&self) -> JsonValue { @@ -104,6 +107,38 @@ impl ToJson2 for JSONResult { } } +pub struct SerdeJSONResult2 { + r: SerdeJSONResult, +} + +impl SerdeJSONResult2 { + pub fn new(r: SerdeJSONResult) -> Self { + Self { r } + } +} + +impl Serialize for SerdeJSONResult2 { + fn serialize(&self, serializer: S) -> Result { + match &self.r { + Ok(v) => { + let mut s = serializer.serialize_map(Some(3))?; + s.serialize_entry("ok", &true)?; + s.serialize_entry("code", &0)?; + s.serialize_entry("result", v)?; + s.end() + } + Err(e) => { + let mut s = serializer.serialize_map(Some(4))?; + s.serialize_entry("ok", &false)?; + s.serialize_entry("code", &e.code)?; + s.serialize_entry("msg", &e.msg)?; + s.serialize_entry::<_, Option>("debug_msg", &None)?; + s.end() + } + } + } +} + #[cfg(test)] impl FromJson for JSONResult { type Err = crate::error::PixivDownloaderError; diff --git a/src/server/route.rs b/src/server/route.rs index 5ae2bbb..4567c84 100644 --- a/src/server/route.rs +++ b/src/server/route.rs @@ -2,6 +2,7 @@ use super::auth::*; use super::context::ServerContext; use super::preclude::HttpBodyType; use super::proxy::*; +use super::push::*; use super::traits::MatchRoute; use super::traits::ResponseFor; use super::version::VersionRoute; @@ -26,6 +27,7 @@ impl ServerRoutes { routes.push(Box::new(AuthPubkeyRoute::new())); routes.push(Box::new(AuthTokenRoute::new())); routes.push(Box::new(ProxyPixivRoute::new())); + routes.push(Box::new(PushRoute::new())); Self { routes } }