From 14e17b10c091b4768b4251be4d61b9a7b6d710eb Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Fri, 18 Mar 2022 21:22:31 +0000 Subject: [PATCH] feat: implement OpenAPI spec for bots and root --- Cargo.lock | 88 ++++++++++++++++++- Cargo.toml | 9 +- src/main.rs | 11 ++- src/routes/bots/create.rs | 18 ++-- src/routes/bots/delete.rs | 7 +- src/routes/bots/edit.rs | 19 +++- src/routes/bots/fetch.rs | 4 + src/routes/bots/fetch_owned.rs | 4 + src/routes/bots/fetch_public.rs | 11 ++- src/routes/bots/invite.rs | 27 ++++-- src/routes/bots/mod.rs | 5 +- src/routes/channels/message_edit.rs | 4 +- src/routes/mod.rs | 15 +++- src/routes/root.rs | 131 +++++++++++++++++++++------- 14 files changed, 290 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0b572d1..76c85278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,6 +850,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + [[package]] name = "either" version = "1.6.1" @@ -2086,6 +2092,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "okapi" +version = "0.7.0-rc.1" +source = "git+https://github.com/insertish/okapi?rev=dcf0df115596ee07a587a7a543cddf3d7944645b#dcf0df115596ee07a587a7a543cddf3d7944645b" +dependencies = [ + "log", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "once_cell" version = "1.8.0" @@ -2163,7 +2180,7 @@ dependencies = [ [[package]] name = "optional_struct" version = "0.2.0" -source = "git+https://github.com/insertish/OptionalStruct?rev=b57e8e8e77db6e1a7f68c715d0d1760cb64feb0c#b57e8e8e77db6e1a7f68c715d0d1760cb64feb0c" +source = "git+https://github.com/insertish/OptionalStruct?rev=e275d2726595474632485934aa0887fa52281f70#e275d2726595474632485934aa0887fa52281f70" dependencies = [ "quote 0.3.15", "syn 0.11.11", @@ -2980,6 +2997,8 @@ dependencies = [ "rmp-serde 0.15.5", "rocket", "rocket_cors", + "rocket_okapi", + "schemars", "serde", "serde_json", "ulid 0.4.1", @@ -2991,7 +3010,6 @@ dependencies = [ [[package]] name = "revolt-quark" version = "0.1.0" -source = "git+https://github.com/revoltchat/quark?rev=7585b7944f1a654a28f9902f2cf0f6778f7abd5a#7585b7944f1a654a28f9902f2cf0f6778f7abd5a" dependencies = [ "async-recursion", "async-std", @@ -3007,12 +3025,15 @@ dependencies = [ "mongodb 2.1.0", "nanoid", "num_enum", + "okapi", "ops", "optional_struct", "pretty_env_logger", "rauth", "redis-kiss", "rocket", + "rocket_okapi", + "schemars", "serde", "serde_json", "ulid 0.5.0", @@ -3172,6 +3193,33 @@ dependencies = [ "uncased", ] +[[package]] +name = "rocket_okapi" +version = "0.8.0-rc.1" +source = "git+https://github.com/insertish/okapi?rev=dcf0df115596ee07a587a7a543cddf3d7944645b#dcf0df115596ee07a587a7a543cddf3d7944645b" +dependencies = [ + "either", + "log", + "okapi", + "rocket", + "rocket_okapi_codegen", + "schemars", + "serde", + "serde_json", +] + +[[package]] +name = "rocket_okapi_codegen" +version = "0.8.0-rc.1" +source = "git+https://github.com/insertish/okapi?rev=dcf0df115596ee07a587a7a543cddf3d7944645b#dcf0df115596ee07a587a7a543cddf3d7944645b" +dependencies = [ + "darling 0.13.0", + "proc-macro2", + "quote 1.0.9", + "rocket_http", + "syn 1.0.76", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -3271,6 +3319,31 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "schemars" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" +dependencies = [ + "dyn-clone", + "indexmap", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "serde_derive_internals", + "syn 1.0.76", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -3360,6 +3433,17 @@ dependencies = [ "syn 1.0.76", ] +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.76", +] + [[package]] name = "serde_json" version = "1.0.78" diff --git a/Cargo.toml b/Cargo.toml index 95353326..f1da5c54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,11 @@ rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] mongodb = { version = "1.2.2", features = ["async-std-runtime"], default-features = false } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" } +# spec generation +schemars = "0.8.8" +# rocket_okapi = "0.8.0-rc.1" +rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "dcf0df115596ee07a587a7a543cddf3d7944645b", features = [ "swagger" ] } + # quark -revolt-quark = { git = "https://github.com/revoltchat/quark", rev = "7585b7944f1a654a28f9902f2cf0f6778f7abd5a" } -# revolt-quark = { path = "/run/media/mink/oldssd/revolt/quark/" } +# revolt-quark = { git = "https://github.com/revoltchat/quark", rev = "7585b7944f1a654a28f9902f2cf0f6778f7abd5a" } +revolt-quark = { path = "/run/media/mink/oldssd/revolt/quark/" } diff --git a/src/main.rs b/src/main.rs index a0a8ecd7..2e68acea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #[macro_use] extern crate rocket; #[macro_use] +extern crate rocket_okapi; +#[macro_use] extern crate serde_json; #[macro_use] extern crate lazy_static; @@ -134,8 +136,13 @@ async fn launch_web() { let rocket = rocket::build(); routes::mount(rocket) .mount("/", rocket_cors::catch_all_options_routes()) - .mount("/auth/account", rauth::web::account::routes()) - .mount("/auth/session", rauth::web::session::routes()) + .mount( + "/swagger/", + rocket_okapi::swagger_ui::make_swagger_ui(&rocket_okapi::swagger_ui::SwaggerUIConfig { + url: "../openapi.json".to_owned(), + ..Default::default() + }), + ) .manage(auth) .manage(db) .manage(cors.clone()) diff --git a/src/routes/bots/create.rs b/src/routes/bots/create.rs index 58ef2490..b20fc766 100644 --- a/src/routes/bots/create.rs +++ b/src/routes/bots/create.rs @@ -2,23 +2,29 @@ use crate::util::{regex::RE_USERNAME, variables::MAX_BOT_COUNT}; use nanoid::nanoid; use revolt_quark::{ - models::{Bot, User, user::BotInformation}, + models::{user::BotInformation, Bot, User}, Db, Error, Result, }; use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use ulid::Ulid; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Bot Details +#[derive(Validate, Deserialize, JsonSchema)] +pub struct DataCreateBot { + /// Bot username #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] name: String, } +/// # Create Bot +/// +/// Create a new Revolt bot. +#[openapi(tag = "Bots")] #[post("/create", data = "")] -pub async fn create_bot(db: &Db, user: User, info: Json) -> Result> { +pub async fn create_bot(db: &Db, user: User, info: Json) -> Result> { if user.bot.is_some() { return Err(Error::IsBot); } @@ -40,7 +46,7 @@ pub async fn create_bot(db: &Db, user: User, info: Json) -> Result")] pub async fn delete_bot(db: &Db, user: User, target: Ref) -> Result { if user.bot.is_some() { diff --git a/src/routes/bots/edit.rs b/src/routes/bots/edit.rs index 9c87c1d4..ee4fc504 100644 --- a/src/routes/bots/edit.rs +++ b/src/routes/bots/edit.rs @@ -12,20 +12,31 @@ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Bot Details +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataEditBot { + /// Bot username #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] #[serde(skip_serializing_if = "Option::is_none")] name: Option, + /// Whether the bot can be added by anyone public: Option, + /// Whether analytics should be gathered for this bot analytics: Option, + /// Interactions URL + #[validate(length(min = 1, max = 2048))] interactions_url: Option, + /// Fields to remove from bot object #[validate(length(min = 1))] remove: Option>, } +/// # Edit Bot +/// +/// Edit bot details by its id. +#[openapi(tag = "Bots")] #[patch("/", data = "")] -pub async fn edit_bot(db: &Db, user: User, target: Ref, data: Json) -> Result> { +pub async fn edit_bot(db: &Db, user: User, target: Ref, data: Json) -> Result> { if user.bot.is_some() { return Err(Error::IsBot); } @@ -56,7 +67,7 @@ pub async fn edit_bot(db: &Db, user: User, target: Ref, data: Json) -> Res return Ok(Json(bot)); } - let Data { + let DataEditBot { public, analytics, interactions_url, diff --git a/src/routes/bots/fetch.rs b/src/routes/bots/fetch.rs index c9920e5c..c1759b3e 100644 --- a/src/routes/bots/fetch.rs +++ b/src/routes/bots/fetch.rs @@ -4,6 +4,10 @@ use revolt_quark::{ }; use rocket::serde::json::Json; +/// # Fetch Bot +/// +/// Fetch details of a bot you own by its id. +#[openapi(tag = "Bots")] #[get("/")] pub async fn fetch_bot(db: &Db, user: User, target: Ref) -> Result> { if user.bot.is_some() { diff --git a/src/routes/bots/fetch_owned.rs b/src/routes/bots/fetch_owned.rs index e0e0d6bb..06a9e183 100644 --- a/src/routes/bots/fetch_owned.rs +++ b/src/routes/bots/fetch_owned.rs @@ -4,6 +4,10 @@ use revolt_quark::{ }; use rocket::serde::json::Json; +/// # Fetch Owned Bots +/// +/// Fetch all of the bots that you have control over. +#[openapi(tag = "Bots")] #[get("/@me")] pub async fn fetch_owned_bots(db: &Db, user: User) -> Result>> { if user.bot.is_some() { diff --git a/src/routes/bots/fetch_public.rs b/src/routes/bots/fetch_public.rs index 73fa5fe6..20d3f6a4 100644 --- a/src/routes/bots/fetch_public.rs +++ b/src/routes/bots/fetch_public.rs @@ -3,17 +3,26 @@ use revolt_quark::{models::File, Db, Error, Ref, Result}; use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +/// # Public Bot +#[derive(Serialize, Deserialize, JsonSchema)] pub struct PublicBot { + /// Bot Id #[serde(rename = "_id")] id: String, + /// Bot Username username: String, + /// Profile Avatar #[serde(skip_serializing_if = "Option::is_none")] avatar: Option, + /// Profile Description #[serde(skip_serializing_if = "Option::is_none")] description: Option, } +/// # Fetch Public Bot +/// +/// Fetch details of a public (or owned) bot by its id. +#[openapi(tag = "Bots")] #[get("//invite")] pub async fn fetch_public_bot(db: &Db, target: Ref) -> Result> { let bot = target.as_bot(db).await?; diff --git a/src/routes/bots/invite.rs b/src/routes/bots/invite.rs index 105250bb..5a6fa66e 100644 --- a/src/routes/bots/invite.rs +++ b/src/routes/bots/invite.rs @@ -6,19 +6,32 @@ use revolt_quark::{ use rocket::serde::json::Json; use serde::Deserialize; -#[derive(Deserialize)] +/// # Invite Destination +#[derive(Deserialize, JsonSchema)] #[serde(untagged)] -pub enum Destination { - Server { server: String }, - Group { group: String }, +pub enum InviteBotDestination { + /// Invite to a server + Server { + /// Server Id + server: String, + }, + /// Invite to a group + Group { + /// Group Id + group: String, + }, } +/// # Invite Bot +/// +/// Invite a bot to a server or group by its id.` +#[openapi(tag = "Bots")] #[post("//invite", data = "")] pub async fn invite_bot( db: &Db, user: User, target: Ref, - dest: Json, + dest: Json, ) -> Result { if user.bot.is_some() { return Err(Error::IsBot); @@ -30,7 +43,7 @@ pub async fn invite_bot( } match dest.into_inner() { - Destination::Server { server } => { + InviteBotDestination::Server { server } => { let server = db.fetch_server(&server).await?; perms(&user) @@ -48,7 +61,7 @@ pub async fn invite_bot( db.insert_member(&member).await.map(|_| EmptyResponse) } - Destination::Group { group } => { + InviteBotDestination::Group { group } => { let channel = db.fetch_channel(&group).await?; perms(&user) diff --git a/src/routes/bots/mod.rs b/src/routes/bots/mod.rs index e2affbc1..c29b90e5 100644 --- a/src/routes/bots/mod.rs +++ b/src/routes/bots/mod.rs @@ -1,4 +1,5 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod create; mod delete; @@ -8,8 +9,8 @@ mod fetch_owned; mod fetch_public; mod invite; -pub fn routes() -> Vec { - routes![ +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![ create::create_bot, invite::invite_bot, fetch_public::fetch_public_bot, diff --git a/src/routes/channels/message_edit.rs b/src/routes/channels/message_edit.rs index 9384817a..cae04db3 100644 --- a/src/routes/channels/message_edit.rs +++ b/src/routes/channels/message_edit.rs @@ -3,7 +3,7 @@ use revolt_quark::{ models::message::{PartialMessage, SendableEmbed}, models::{message::Content, Message, User}, types::january::Embed, - Db, Error, Ref, Result, + DateTimeContainer, Db, Error, Ref, Result, }; use chrono::Utc; @@ -40,7 +40,7 @@ pub async fn req( return Err(Error::CannotEditMessage); } - message.edited = Some(DateTime::from_chrono(Utc::now())); + message.edited = Some(DateTimeContainer(DateTime::from_chrono(Utc::now()))); let mut partial = PartialMessage { edited: message.edited, ..Default::default() diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 03a2902b..75b22556 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,6 +1,7 @@ pub use rocket::http::Status; pub use rocket::response::Redirect; use rocket::{Build, Rocket}; +use rocket_okapi::settings::OpenApiSettings; mod bots; mod channels; @@ -12,14 +13,22 @@ mod servers; mod sync; mod users; -pub fn mount(rocket: Rocket) -> Rocket { +pub fn mount(mut rocket: Rocket) -> Rocket { + let settings = OpenApiSettings::default(); + + mount_endpoints_and_merged_docs! { + rocket, "/".to_owned(), settings, + "" => openapi_get_routes_spec![root::root, root::ping], + "/bots" => bots::routes(), + }; + rocket - .mount("/", routes![root::root, root::ping]) + .mount("/auth/account", rauth::web::account::routes()) + .mount("/auth/session", rauth::web::session::routes()) .mount("/onboard", onboard::routes()) .mount("/users", users::routes()) .mount("/channels", channels::routes()) .mount("/servers", servers::routes()) - .mount("/bots", bots::routes()) .mount("/invites", invites::routes()) .mount("/push", push::routes()) .mount("/sync", sync::routes()) diff --git a/src/routes/root.rs b/src/routes/root.rs index 859eeb3e..c634b40f 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -3,39 +3,110 @@ use crate::util::variables::{ USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST, }; -use rocket::{http::Status, serde::json::Value}; +use rocket::http::Status; +use rocket::serde::json::Json; +use serde::Serialize; -#[get("/")] -pub async fn root() -> Value { - json!({ - "revolt": crate::version::VERSION, - "features": { - "captcha": { - "enabled": *USE_HCAPTCHA, - "key": HCAPTCHA_SITEKEY.to_string() - }, - "email": *USE_EMAIL, - "invite_only": *INVITE_ONLY, - "autumn": { - "enabled": *USE_AUTUMN, - "url": *AUTUMN_URL - }, - "january": { - "enabled": *USE_JANUARY, - "url": *JANUARY_URL - }, - "voso": { - "enabled": *USE_VOSO, - "url": *VOSO_URL, - "ws": *VOSO_WS_HOST - } - }, - "ws": *EXTERNAL_WS_URL, - "app": *APP_URL, - "vapid": *VAPID_PUBLIC_KEY - }) +use revolt_quark::Result; + +/// # hCaptcha Configuration +#[derive(Serialize, JsonSchema, Debug)] +pub struct CaptchaFeature { + /// Whether captcha is enabled + pub enabled: bool, + /// Client key used for solving captcha + pub key: String, } +/// # Generic Service Configuration +#[derive(Serialize, JsonSchema, Debug)] +pub struct Feature { + /// Whether the service is enabled + pub enabled: bool, + /// URL pointing to the service + pub url: String, +} + +/// # Voice Server Configuration +#[derive(Serialize, JsonSchema, Debug)] +pub struct VoiceFeature { + /// Whether voice is enabled + pub enabled: bool, + /// URL pointing to the voice API + pub url: String, + /// URL pointing to the voice WebSocket server + pub ws: String, +} + +/// # Feature Configuration +#[derive(Serialize, JsonSchema, Debug)] +pub struct RevoltFeatures { + /// hCaptcha configuration + pub captcha: CaptchaFeature, + /// Whether email verification is enabled + pub email: bool, + /// Whether this server is invite only + pub invite_only: bool, + /// File server service configuration + pub autumn: Feature, + /// Proxy service configuration + pub january: Feature, + /// Voice server configuration + pub voso: VoiceFeature, +} + +/// # Server Configuration +#[derive(Serialize, JsonSchema, Debug)] +pub struct RevoltConfig { + /// Revolt API Version + pub revolt: String, + /// Features enabled on this Revolt node + pub features: RevoltFeatures, + /// WebSocket URL + pub ws: String, + /// URL pointing to the client serving this node + pub app: String, + /// Web Push VAPID public key + pub vapid: String, +} + +/// # Query Node +/// +/// Fetch the server configuration for this Revolt instance. +#[openapi(tag = "Core")] +#[get("/")] +pub async fn root() -> Result> { + Ok(Json(RevoltConfig { + revolt: crate::version::VERSION.to_string(), + features: RevoltFeatures { + captcha: CaptchaFeature { + enabled: *USE_HCAPTCHA, + key: HCAPTCHA_SITEKEY.to_string(), + }, + email: *USE_EMAIL, + invite_only: *INVITE_ONLY, + autumn: Feature { + enabled: *USE_AUTUMN, + url: AUTUMN_URL.to_string(), + }, + january: Feature { + enabled: *USE_JANUARY, + url: JANUARY_URL.to_string(), + }, + voso: VoiceFeature { + enabled: *USE_VOSO, + url: VOSO_URL.to_string(), + ws: VOSO_WS_HOST.to_string(), + }, + }, + ws: EXTERNAL_WS_URL.to_string(), + app: APP_URL.to_string(), + vapid: VAPID_PUBLIC_KEY.to_string(), + })) +} + +/// Example endpoint. +#[openapi(skip)] #[get("/ping")] pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status { Status::Ok