From 0bd2ea1b19a1d1ab980ba2bc3858964a9c466a2a Mon Sep 17 00:00:00 2001 From: Igor Bubelov Date: Mon, 26 Aug 2024 18:23:02 +0700 Subject: [PATCH] Add experimental JSON RPC API --- Cargo.lock | 195 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- src/area/admin.rs | 2 +- src/area/mod.rs | 1 + src/area/model.rs | 6 +- src/area/rpc.rs | 175 +++++++++++++++++++++++++++++++++++++++ src/area/service.rs | 58 ++++++++++--- src/server/mod.rs | 15 +++- 8 files changed, 439 insertions(+), 18 deletions(-) create mode 100644 src/area/rpc.rs diff --git a/Cargo.lock b/Cargo.lock index 31ea531..4705bdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,9 @@ dependencies = [ "bytestring", "derive_more", "encoding_rs", + "flate2", "futures-core", + "h2", "http 0.2.9", "httparse", "httpdate", @@ -65,6 +67,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "zstd", ] [[package]] @@ -86,6 +89,7 @@ dependencies = [ "bytestring", "cfg-if", "http 0.2.9", + "regex", "regex-lite", "serde", "tracing", @@ -159,6 +163,7 @@ dependencies = [ "bytes", "bytestring", "cfg-if", + "cookie", "derive_more", "encoding_rs", "futures-core", @@ -170,6 +175,7 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", + "regex", "regex-lite", "serde", "serde_json", @@ -207,6 +213,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.3" @@ -219,6 +231,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -249,6 +270,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -265,7 +297,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.1", "object", "rustc-demangle", ] @@ -331,6 +363,7 @@ dependencies = [ "geojson", "http 1.1.0", "include_dir", + "jsonrpc-v2", "reqwest", "rusqlite", "serde", @@ -373,6 +406,9 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -386,6 +422,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -395,6 +442,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -526,6 +582,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + +[[package]] +name = "extensions" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258f70bd2b060d448403a66d420e81dcac3e5247a4928a887404a5e03715e2e0" +dependencies = [ + "fxhash", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -538,6 +618,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "flate2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "float_next_after" version = "1.0.0" @@ -654,6 +744,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -748,6 +847,25 @@ dependencies = [ "smallvec", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.9", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -956,6 +1074,16 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + [[package]] name = "ipnet" version = "2.5.0" @@ -968,6 +1096,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.57" @@ -977,6 +1114,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc-v2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759affe8550a30591c68f5e85d1784f24dc65217d2cca765949857f844fcecb0" +dependencies = [ + "actix-service", + "actix-web", + "async-trait", + "bytes", + "erased-serde", + "extensions", + "futures", + "serde", + "serde_json", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1088,6 +1242,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.10" @@ -1403,6 +1566,8 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax 0.7.1", ] @@ -2360,3 +2525,31 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index fa812a8..d6ab808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,4 +61,7 @@ http = { version = "1.1.0", default-features = false } deadpool-sqlite = { version = "0.8.1", default-features = false, features = ["rt_tokio_1"] } # https://github.com/AaronErhardt/actix-governor/releases -actix-governor = { version = "0.5.0", default-features = false } \ No newline at end of file +actix-governor = { version = "0.5.0", default-features = false } + +# https://crates.io/crates/jsonrpc-v2 +jsonrpc-v2 = { version = "0.13.0", default-features = false, features = ["actix-web-v4-integration", "easy-errors"] } \ No newline at end of file diff --git a/src/area/admin.rs b/src/area/admin.rs index 0a5ddd7..7a92d4b 100644 --- a/src/area/admin.rs +++ b/src/area/admin.rs @@ -81,7 +81,7 @@ pub async fn patch( let area = pool .get() .await? - .interact(move |conn| area::service::patch_tags(area.id, args.tags.clone(), conn)) + .interact(move |conn| area::service::patch_tags(&area.id.to_string(), args.tags.clone(), conn)) .await??; let log_message = format!( "{} updated area https://api.btcmap.org/v3/areas/{}", diff --git a/src/area/mod.rs b/src/area/mod.rs index c9fcd49..fe97e7f 100644 --- a/src/area/mod.rs +++ b/src/area/mod.rs @@ -1,6 +1,7 @@ pub mod model; pub use model::Area; pub mod admin; +pub mod rpc; pub mod service; pub mod v2; pub mod v3; diff --git a/src/area/model.rs b/src/area/model.rs index 28f9a41..a7e2b25 100644 --- a/src/area/model.rs +++ b/src/area/model.rs @@ -1,6 +1,7 @@ use crate::{Error, Result}; use geojson::{GeoJson, Geometry}; use rusqlite::{named_params, Connection, OptionalExtension, Row}; +use serde::Serialize; use serde_json::{Map, Value}; use std::{ thread::sleep, @@ -9,12 +10,15 @@ use std::{ use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use tracing::{debug, error, info}; -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Serialize)] pub struct Area { pub id: i64, pub tags: Map, + #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] pub updated_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339::option")] pub deleted_at: Option, } diff --git a/src/area/rpc.rs b/src/area/rpc.rs new file mode 100644 index 0000000..955a35b --- /dev/null +++ b/src/area/rpc.rs @@ -0,0 +1,175 @@ +use super::Area; +use crate::{area, auth::Token, discord, Error}; +use deadpool_sqlite::Pool; +use jsonrpc_v2::{Data, Params}; +use serde::Deserialize; +use serde_json::{Map, Value}; +use std::sync::Arc; +use tracing::info; + +#[derive(Deserialize)] +pub struct CreateAreaArgs { + pub token: String, + pub tags: Map, +} + +pub async fn create( + Params(args): Params, + pool: Data>, +) -> Result { + let token = pool + .get() + .await? + .interact(move |conn| Token::select_by_secret(&args.token, conn)) + .await?? + .unwrap(); + let area = pool + .get() + .await? + .interact(move |conn| area::service::insert(args.tags, conn)) + .await??; + let log_message = format!( + "{} created area {} https://api.btcmap.org/v3/areas/{}", + token.owner, + area.name(), + area.id, + ); + info!(log_message); + discord::send_message_to_channel(&log_message, discord::CHANNEL_API).await; + Ok(area) +} + +#[derive(Deserialize)] +pub struct GetAreaArgs { + pub token: String, + pub area_id_or_alias: String, +} + +pub async fn get(Params(args): Params, pool: Data>) -> Result { + pool.get() + .await? + .interact(move |conn| Token::select_by_secret(&args.token, conn)) + .await?? + .unwrap(); + let area = pool + .get() + .await? + .interact(move |conn| Area::select_by_id_or_alias(&args.area_id_or_alias, conn)) + .await?? + .unwrap(); + Ok(area) +} + +#[derive(Deserialize)] +pub struct SetAreaTagArgs { + pub token: String, + pub area_id_or_alias: String, + pub tag_name: String, + pub tag_value: Value, +} + +pub async fn set_tag( + Params(params): Params, + pool: Data>, +) -> Result { + let token = pool + .get() + .await? + .interact(move |conn| Token::select_by_secret(¶ms.token, conn)) + .await?? + .unwrap(); + let cloned_tag_name = params.tag_name.clone(); + let cloned_tag_value = params.tag_value.clone(); + let area = pool + .get() + .await? + .interact(move |conn| { + area::service::patch_tag( + ¶ms.area_id_or_alias, + &cloned_tag_name, + &cloned_tag_value, + conn, + ) + }) + .await??; + let log_message = format!( + "{} set tag {} = {} for area {} https://api.btcmap.org/v3/areas/{}", + token.owner, + params.tag_name, + serde_json::to_string(¶ms.tag_value)?, + area.name(), + area.id, + ); + info!(log_message); + discord::send_message_to_channel(&log_message, discord::CHANNEL_API).await; + Ok(area) +} + +#[derive(Deserialize)] +pub struct RemoveAreaTagArgs { + pub token: String, + pub area_id_or_alias: String, + pub tag_name: String, +} + +pub async fn remove_tag( + Params(params): Params, + pool: Data>, +) -> Result { + let token = pool + .get() + .await? + .interact(move |conn| Token::select_by_secret(¶ms.token, conn)) + .await?? + .unwrap(); + let cloned_tag_name = params.tag_name.clone(); + let area = pool + .get() + .await? + .interact(move |conn| { + area::service::remove_tag(¶ms.area_id_or_alias, &cloned_tag_name, conn) + }) + .await??; + let log_message = format!( + "{} removed tag {} from area {} https://api.btcmap.org/v3/areas/{}", + token.owner, + params.tag_name, + area.name(), + area.id, + ); + info!(log_message); + discord::send_message_to_channel(&log_message, discord::CHANNEL_API).await; + Ok(area) +} + +#[derive(Deserialize)] +pub struct RemoveAreaArgs { + pub token: String, + pub area_id: i64, +} + +pub async fn remove( + Params(params): Params, + pool: Data>, +) -> Result { + let token = pool + .get() + .await? + .interact(move |conn| Token::select_by_secret(¶ms.token, conn)) + .await?? + .unwrap(); + let area = pool + .get() + .await? + .interact(move |conn| area::service::soft_delete(params.area_id, conn)) + .await??; + let log_message = format!( + "{} removed area {} https://api.btcmap.org/v3/areas/{}", + token.owner, + area.name(), + area.id, + ); + info!(log_message); + discord::send_message_to_channel(&log_message, discord::CHANNEL_API).await; + Ok(area) +} diff --git a/src/area/service.rs b/src/area/service.rs index 057084b..89a2d8c 100644 --- a/src/area/service.rs +++ b/src/area/service.rs @@ -9,6 +9,9 @@ use serde_json::{Map, Value}; use time::OffsetDateTime; pub fn insert(tags: Map, conn: &mut Connection) -> Result { + if !tags.contains_key("geo_json") { + return Err(Error::HttpBadRequest("geo_json tag is missing".into())); + } let sp = conn.savepoint()?; let url_alias = tags .get("url_alias") @@ -44,16 +47,45 @@ pub fn insert(tags: Map, conn: &mut Connection) -> Result { Ok(area) } -pub fn patch_tags(id: i64, tags: Map, conn: &mut Connection) -> Result { - let sp = conn.savepoint()?; - let area = Area::select_by_id(id, &sp)?.unwrap(); - let area_elements = element::service::find_in_area(&area, &sp)?; - element::service::update_areas_tag(&area_elements, &sp)?; - let area = Area::patch_tags(id, tags, &sp)?; - let area_elements = element::service::find_in_area(&area, &sp)?; - element::service::update_areas_tag(&area_elements, &sp)?; - sp.commit()?; - Ok(area) +pub fn patch_tag( + id_or_alias: &str, + tag_name: &str, + tag_value: &Value, + conn: &mut Connection, +) -> Result { + let mut tags = Map::new(); + tags.insert(tag_name.to_string(), tag_value.clone()); + patch_tags(id_or_alias, tags, conn) +} + +pub fn patch_tags( + id_or_alias: &str, + tags: Map, + conn: &mut Connection, +) -> Result { + let area = Area::select_by_id_or_alias(id_or_alias, conn)?.unwrap(); + if tags.contains_key("geo_json") { + let sp = conn.savepoint()?; + let area_elements = element::service::find_in_area(&area, &sp)?; + element::service::update_areas_tag(&area_elements, &sp)?; + let area = Area::patch_tags(area.id, tags, &sp)?; + let area_elements = element::service::find_in_area(&area, &sp)?; + element::service::update_areas_tag(&area_elements, &sp)?; + sp.commit()?; + Ok(area) + } else { + Ok(Area::patch_tags(area.id, tags, conn)?) + } +} + +pub fn remove_tag(area_id_or_alias: &str, tag_name: &str, conn: &mut Connection) -> Result { + if tag_name == "geo_json" { + return Err(Error::HttpBadRequest( + "geo_json tag can't be removed".into(), + )); + } + let area = Area::select_by_id_or_alias(area_id_or_alias, conn)?.unwrap(); + Ok(Area::remove_tag(area.id, tag_name, conn)?) } pub fn soft_delete(id: i64, conn: &mut Connection) -> Result { @@ -121,7 +153,7 @@ mod test { patch_tags.insert(new_tag_name.into(), new_tag_value.clone()); let new_alias = json!("test1"); patch_tags.insert("url_alias".into(), new_alias.clone()); - let area = super::patch_tags(area.id, patch_tags, &mut conn)?; + let area = super::patch_tags(&area.id.to_string(), patch_tags, &mut conn)?; let db_area = Area::select_by_id(area.id, &conn)?.unwrap(); assert_eq!(area, db_area); assert_eq!(new_tag_value, db_area.tags[new_tag_name]); @@ -144,8 +176,8 @@ mod test { let url_alias = json!("test"); tags.insert("url_alias".into(), url_alias.clone()); tags.insert("geo_json".into(), phuket_geo_json()); - let area = Area::insert(tags, &mut conn)?; - let area = super::patch_tags(area.id, Map::new(), &mut conn)?; + let area = Area::insert(tags.clone(), &mut conn)?; + let area = super::patch_tags(&area.id.to_string(), tags, &mut conn)?; let db_area = Area::select_by_id(area.id, &conn)?.unwrap(); assert_eq!(area, db_area); let db_area_element = Element::select_by_id(area_element.id, &conn)?.unwrap(); diff --git a/src/server/mod.rs b/src/server/mod.rs index 4943dac..5b2467b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -5,8 +5,8 @@ use crate::{report, Result}; use actix_governor::{Governor, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError}; use actix_web::dev::{Service, ServiceRequest}; use actix_web::http::header::HeaderValue; -use actix_web::web::scope; use actix_web::web::QueryConfig; +use actix_web::web::{scope, service}; use actix_web::{ middleware::{Compress, NormalizePath}, web::Data, @@ -77,6 +77,19 @@ pub async fn run() -> Result<()> { .wrap(Compress::default()) .app_data(Data::new(pool.clone())) .app_data(QueryConfig::default().error_handler(error::query_error_handler)) + .service( + service("rpc").guard(actix_web::guard::Post()).finish( + jsonrpc_v2::Server::new() + .with_data(jsonrpc_v2::Data::new(pool.clone())) + .with_method("createarea", area::rpc::create) + .with_method("getarea", area::rpc::get) + .with_method("setareatag", area::rpc::set_tag) + .with_method("removeareatag", area::rpc::remove_tag) + .with_method("removearea", area::rpc::remove) + .finish() + .into_actix_web_service(), + ), + ) .service( scope("tiles") .wrap(Governor::new(&tile_rate_limit_conf))