diff --git a/Cargo.lock b/Cargo.lock index 76c85278..5c23a3d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2793,6 +2793,37 @@ dependencies = [ "wither", ] +[[package]] +name = "rauth" +version = "1.0.0" +source = "git+https://github.com/insertish/rauth?rev=611b11baa9e199bcefd0ca5bd3302f9d6904a2c6#611b11baa9e199bcefd0ca5bd3302f9d6904a2c6" +dependencies = [ + "async-std", + "base32", + "chrono", + "futures", + "handlebars", + "json", + "lazy_static", + "lettre", + "mongodb 1.2.3", + "nanoid", + "okapi", + "rand 0.8.4", + "regex", + "reqwest", + "rocket", + "rocket_okapi", + "rust-argon2", + "schemars", + "serde", + "serde_json", + "totp-lite", + "ulid 0.4.1", + "validator", + "wither", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -2989,7 +3020,7 @@ dependencies = [ "num_enum", "once_cell", "phf", - "rauth", + "rauth 1.0.0 (git+https://github.com/insertish/rauth?rev=611b11baa9e199bcefd0ca5bd3302f9d6904a2c6)", "redis 0.21.2", "regex", "reqwest", @@ -3029,7 +3060,7 @@ dependencies = [ "ops", "optional_struct", "pretty_env_logger", - "rauth", + "rauth 1.0.0 (git+https://github.com/insertish/rauth?rev=501d445e7b42a1c229bfa88c01fa6415f4cb7992)", "redis-kiss", "rocket", "rocket_okapi", diff --git a/Cargo.toml b/Cargo.toml index f1da5c54..41b224c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] web-push = "0.7.2" many-to-many = "0.1.2" lettre = "0.10.0-alpha.4" -rauth = { git = "https://github.com/insertish/rauth", rev = "501d445e7b42a1c229bfa88c01fa6415f4cb7992" } +rauth = { git = "https://github.com/insertish/rauth", rev = "611b11baa9e199bcefd0ca5bd3302f9d6904a2c6" } hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "b0f3db9d33990530d7640d4bbb309c8d9eb5c0cf", features = ["redis-backend"] } # redis diff --git a/src/routes/bots/edit.rs b/src/routes/bots/edit.rs index ee4fc504..d1f7cb95 100644 --- a/src/routes/bots/edit.rs +++ b/src/routes/bots/edit.rs @@ -22,6 +22,8 @@ pub struct DataEditBot { /// Whether the bot can be added by anyone public: Option, /// Whether analytics should be gathered for this bot + /// + /// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg). analytics: Option, /// Interactions URL #[validate(length(min = 1, max = 2048))] @@ -36,7 +38,12 @@ pub struct DataEditBot { /// 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); } diff --git a/src/routes/channels/message_send.rs b/src/routes/channels/message_send.rs index 44e8ebae..6905a821 100644 --- a/src/routes/channels/message_send.rs +++ b/src/routes/channels/message_send.rs @@ -20,7 +20,7 @@ use crate::util::idempotency::IdempotencyKey; pub struct DataMessageSend { /// Unique token to prevent duplicate message sending /// - /// **This is deprecated and replaced by Idempotency-Token!** + /// **This is deprecated and replaced by `Idempotency-Key`!** nonce: Option, /// Message content to send diff --git a/src/routes/invites/invite_delete.rs b/src/routes/invites/invite_delete.rs index e2b12b3a..c2b15c10 100644 --- a/src/routes/invites/invite_delete.rs +++ b/src/routes/invites/invite_delete.rs @@ -3,6 +3,10 @@ use revolt_quark::{ perms, Db, EmptyResponse, Permission, Ref, Result, }; +/// # Delete Invite +/// +/// Delete an invite by its id. +#[openapi(tag = "Invites")] #[delete("/")] pub async fn req(db: &Db, user: User, target: Ref) -> Result { let invite = target.as_invite(db).await?; diff --git a/src/routes/invites/invite_fetch.rs b/src/routes/invites/invite_fetch.rs index d24b5e0a..ac1db8da 100644 --- a/src/routes/invites/invite_fetch.rs +++ b/src/routes/invites/invite_fetch.rs @@ -4,37 +4,59 @@ use revolt_quark::{Db, Ref, Result}; use rocket::serde::json::Json; use serde::Serialize; +/// # Invite #[allow(clippy::large_enum_variant)] -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone, JsonSchema)] #[serde(tag = "type")] pub enum InviteResponse { + /// Server channel invite Server { + /// Id of the server server_id: String, + /// Name of the server server_name: String, + /// Attachment for server icon #[serde(skip_serializing_if = "Option::is_none")] server_icon: Option, + /// Attachment for server banner #[serde(skip_serializing_if = "Option::is_none")] server_banner: Option, + /// Id of server channel channel_id: String, + /// Name of server channel channel_name: String, + /// Description of server channel #[serde(skip_serializing_if = "Option::is_none")] channel_description: Option, + /// Name of user who created the invite user_name: String, + /// Avatar of the user who created the invite #[serde(skip_serializing_if = "Option::is_none")] user_avatar: Option, + /// Number of members in this server member_count: i64, }, + /// Group channel invite Group { + /// Id of group channel channel_id: String, + /// Name of group channel channel_name: String, + /// Description of group channel #[serde(skip_serializing_if = "Option::is_none")] channel_description: Option, + /// Name of user who created the invite user_name: String, + /// Avatar of the user who created the invite #[serde(skip_serializing_if = "Option::is_none")] user_avatar: Option, }, } +/// # Fetch Invite +/// +/// Fetch an invite by its id. +#[openapi(tag = "Invites")] #[get("/")] pub async fn req(db: &Db, target: Ref) -> Result> { Ok(Json(match target.as_invite(db).await? { diff --git a/src/routes/invites/invite_join.rs b/src/routes/invites/invite_join.rs index 0ccefdf5..781386bf 100644 --- a/src/routes/invites/invite_join.rs +++ b/src/routes/invites/invite_join.rs @@ -7,6 +7,10 @@ use rocket::serde::json::Value; use crate::util::variables::MAX_SERVER_COUNT; +/// # Join Invite +/// +/// Join an invite by its ID. +#[openapi(tag = "Invites")] #[post("/")] pub async fn req(db: &Db, user: User, target: Ref) -> Result { if user.bot.is_some() { diff --git a/src/routes/invites/mod.rs b/src/routes/invites/mod.rs index 69843215..059b6d94 100644 --- a/src/routes/invites/mod.rs +++ b/src/routes/invites/mod.rs @@ -1,9 +1,10 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod invite_delete; mod invite_fetch; mod invite_join; -pub fn routes() -> Vec { - routes![invite_fetch::req, invite_join::req, invite_delete::req] +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req] } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 50ea8738..528ff951 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -20,19 +20,19 @@ pub fn mount(mut rocket: Rocket) -> Rocket { rocket, "/".to_owned(), settings, "/" => (vec![], custom_openapi_spec()), "" => openapi_get_routes_spec![root::root, root::ping], + "/users" => users::routes(), "/bots" => bots::routes(), "/channels" => channels::routes(), + "/servers" => servers::routes(), + "/invites" => invites::routes(), + "/auth/account" => rauth::web::account::routes(), + "/auth/session" => rauth::web::session::routes(), + "/onboard" => onboard::routes(), + "/push" => push::routes(), + "/sync" => sync::routes(), }; rocket - .mount("/auth/account", rauth::web::account::routes()) - .mount("/auth/session", rauth::web::session::routes()) - .mount("/onboard", onboard::routes()) - .mount("/users", users::routes()) - .mount("/servers", servers::routes()) - .mount("/invites", invites::routes()) - .mount("/push", push::routes()) - .mount("/sync", sync::routes()) } fn custom_openapi_spec() -> OpenApi { @@ -51,7 +51,9 @@ fn custom_openapi_spec() -> OpenApi { openapi: OpenApi::default_version(), info: Info { title: "Revolt API".to_owned(), - description: Some("User-first privacy focused chat platform.".to_owned()), + description: Some( + "User-first privacy focused chat platform.\n\n".to_owned(), + ), terms_of_service: Some("https://revolt.chat/terms".to_owned()), contact: Some(Contact { name: Some("Revolt Support".to_owned()), @@ -71,8 +73,24 @@ fn custom_openapi_spec() -> OpenApi { url: "https://api.revolt.chat".to_owned(), description: Some("Revolt API".to_owned()), ..Default::default() + }, Server { + url: "http://local.revolt.chat:8000".to_owned(), + description: Some("Local Revolt Environment".to_owned()), + ..Default::default() }], + external_docs: Some(ExternalDocs { + url: "https://developers.revolt.chat".to_owned(), + description: Some("Revolt Developer Documentation".to_owned()), + ..Default::default() + }), extensions, + /*tags: vec![ + Tag { + name: "aaa".to_owned(), + description: Some("aaa".to_owned()), + ..Default::default() + } + ],*/ ..Default::default() } } diff --git a/src/routes/onboard/complete.rs b/src/routes/onboard/complete.rs index 9fefca3b..97d31a28 100644 --- a/src/routes/onboard/complete.rs +++ b/src/routes/onboard/complete.rs @@ -6,18 +6,24 @@ use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # New User Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataOnboard { + /// New username which will be used to identify the user on the platform #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] username: String, } +/// # Complete Onboarding +/// +/// This sets a new username, completes onboarding and allows a user to start using Revolt. +#[openapi(tag = "Onboarding")] #[post("/complete", data = "")] pub async fn req( db: &State, session: Session, user: Option, - data: Json, + data: Json, ) -> Result { if user.is_some() { return Err(Error::AlreadyOnboarded); diff --git a/src/routes/onboard/hello.rs b/src/routes/onboard/hello.rs index eba3235d..d1651338 100644 --- a/src/routes/onboard/hello.rs +++ b/src/routes/onboard/hello.rs @@ -1,10 +1,22 @@ use rauth::entities::Session; use revolt_quark::models::User; -use rocket::serde::json::Value; +use rocket::serde::json::Json; +use serde::Serialize; +/// # Onboarding Status +#[derive(Serialize, JsonSchema)] +pub struct DataHello { + /// Whether onboarding is required + onboarding: bool, +} + +/// # Check Onboarding Status +/// +/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session. +#[openapi(tag = "Onboarding")] #[get("/hello")] -pub async fn req(_session: Session, user: Option) -> Value { - json!({ - "onboarding": user.is_none() +pub async fn req(_session: Session, user: Option) -> Json { + Json(DataHello { + onboarding: user.is_none(), }) } diff --git a/src/routes/onboard/mod.rs b/src/routes/onboard/mod.rs index 727886dd..16ec2708 100644 --- a/src/routes/onboard/mod.rs +++ b/src/routes/onboard/mod.rs @@ -1,8 +1,9 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod complete; mod hello; -pub fn routes() -> Vec { - routes![hello::req, complete::req] +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![hello::req, complete::req] } diff --git a/src/routes/push/mod.rs b/src/routes/push/mod.rs index ff0a14cd..e5d5d151 100644 --- a/src/routes/push/mod.rs +++ b/src/routes/push/mod.rs @@ -1,8 +1,9 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod subscribe; mod unsubscribe; -pub fn routes() -> Vec { - routes![subscribe::req, unsubscribe::req] +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![subscribe::req, unsubscribe::req] } diff --git a/src/routes/push/subscribe.rs b/src/routes/push/subscribe.rs index a18fed49..f5125f82 100644 --- a/src/routes/push/subscribe.rs +++ b/src/routes/push/subscribe.rs @@ -6,6 +6,12 @@ use rauth::{ }; use rocket::{serde::json::Json, State}; +/// # Push Subscribe +/// +/// Create a new Web Push subscription. +/// +/// If an existing subscription exists on this session, it will be removed. +#[openapi(tag = "Web Push")] #[post("/subscribe", data = "")] pub async fn req( auth: &State, diff --git a/src/routes/push/unsubscribe.rs b/src/routes/push/unsubscribe.rs index c4df3695..141e72a7 100644 --- a/src/routes/push/unsubscribe.rs +++ b/src/routes/push/unsubscribe.rs @@ -6,6 +6,10 @@ use rauth::{ }; use rocket::State; +/// # Unsubscribe +/// +/// Remove the Web Push subscription associated with the current session. +#[openapi(tag = "Web Push")] #[post("/unsubscribe")] pub async fn req(auth: &State, mut session: Session) -> Result { session.subscription = None; diff --git a/src/routes/servers/ban_create.rs b/src/routes/servers/ban_create.rs index 426c8327..bee3617b 100644 --- a/src/routes/servers/ban_create.rs +++ b/src/routes/servers/ban_create.rs @@ -7,19 +7,25 @@ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Ban Information +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataBanCreate { + /// Ban reason #[validate(length(min = 1, max = 1024))] reason: Option, } +/// # Ban User +/// +/// Ban a user by their id. +#[openapi(tag = "Server Members")] #[put("//bans/", data = "")] pub async fn req( db: &Db, user: User, server: Ref, target: Ref, - data: Json, + data: Json, ) -> Result> { let data = data.into_inner(); data.validate() diff --git a/src/routes/servers/ban_list.rs b/src/routes/servers/ban_list.rs index cafbfcc7..217f0b81 100644 --- a/src/routes/servers/ban_list.rs +++ b/src/routes/servers/ban_list.rs @@ -4,20 +4,33 @@ use revolt_quark::{perms, Db, Permission, Ref, Result}; use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +/// # Banned User +/// +/// Just enoguh user information to list bans. +#[derive(Serialize, Deserialize, JsonSchema)] struct BannedUser { + /// Id of the banned user #[serde(rename = "_id")] pub id: String, + /// Username of the banned user pub username: String, + /// Avatar of the banned user pub avatar: Option, } -#[derive(Serialize, Deserialize)] +/// # Ban List Result +#[derive(Serialize, Deserialize, JsonSchema)] pub struct BanListResult { + /// Users objects users: Vec, + /// Ban objects bans: Vec, } +/// # Fetch Bans +/// +/// Fetch all bans on a server. +#[openapi(tag = "Server Members")] #[get("//bans")] pub async fn req(db: &Db, user: User, target: Ref) -> Result> { let server = target.as_server(db).await?; diff --git a/src/routes/servers/ban_remove.rs b/src/routes/servers/ban_remove.rs index b317f2cb..3486ce45 100644 --- a/src/routes/servers/ban_remove.rs +++ b/src/routes/servers/ban_remove.rs @@ -1,5 +1,9 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result}; +/// # Unban user +/// +/// Remove a user's ban. +#[openapi(tag = "Server Members")] #[delete("//bans/")] pub async fn req(db: &Db, user: User, server: Ref, target: Ref) -> Result { let server = server.as_server(db).await?; diff --git a/src/routes/servers/channel_create.rs b/src/routes/servers/channel_create.rs index 3d861153..aff4f964 100644 --- a/src/routes/servers/channel_create.rs +++ b/src/routes/servers/channel_create.rs @@ -10,9 +10,12 @@ use serde::{Deserialize, Serialize}; use ulid::Ulid; use validator::Validate; -#[derive(Serialize, Deserialize)] +/// # Channel Type +#[derive(Serialize, Deserialize, JsonSchema)] enum ChannelType { + /// Text Channel Text, + /// Voice Channel Voice, } @@ -22,20 +25,34 @@ impl Default for ChannelType { } } -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Channel Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataCreateChannel { + /// Channel type #[serde(rename = "type", default = "ChannelType::default")] channel_type: ChannelType, + /// Channel name #[validate(length(min = 1, max = 32))] name: String, + /// Channel description #[validate(length(min = 0, max = 1024))] description: Option, + /// Whether this channel is age restricted #[serde(skip_serializing_if = "Option::is_none")] nsfw: Option, } +/// # Create Channel +/// +/// Create a new Text or Voice channel. +#[openapi(tag = "Server Information")] #[post("//channels", data = "")] -pub async fn req(db: &Db, user: User, target: Ref, info: Json) -> Result> { +pub async fn req( + db: &Db, + user: User, + target: Ref, + info: Json, +) -> Result> { let info = info.into_inner(); info.validate() .map_err(|error| Error::FailedValidation { error })?; @@ -50,7 +67,7 @@ pub async fn req(db: &Db, user: User, target: Ref, info: Json) -> Result/invites")] pub async fn req(db: &Db, user: User, target: Ref) -> Result>> { let server = target.as_server(db).await?; diff --git a/src/routes/servers/member_edit.rs b/src/routes/servers/member_edit.rs index 65c29a48..93a75603 100644 --- a/src/routes/servers/member_edit.rs +++ b/src/routes/servers/member_edit.rs @@ -10,23 +10,32 @@ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Member Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataMemberEdit { + /// Member nickname #[validate(length(min = 1, max = 32))] nickname: Option, + /// Attachment Id to set for avatar avatar: Option, + /// Array of role ids roles: Option>, + /// Fields to remove from channel object #[validate(length(min = 1))] remove: Option>, } +/// # Edit Member +/// +/// Edit a member by their id. +#[openapi(tag = "Server Members")] #[patch("//members/", data = "")] pub async fn req( db: &Db, user: User, server: Ref, target: Ref, - data: Json, + data: Json, ) -> Result> { let data = data.into_inner(); data.validate() @@ -37,7 +46,7 @@ pub async fn req( let mut member = target.as_member(db, &server.id).await?; - let Data { + let DataMemberEdit { nickname, avatar, roles, diff --git a/src/routes/servers/member_fetch.rs b/src/routes/servers/member_fetch.rs index fe07a62e..47b254f0 100644 --- a/src/routes/servers/member_fetch.rs +++ b/src/routes/servers/member_fetch.rs @@ -4,6 +4,10 @@ use revolt_quark::{ }; use rocket::serde::json::Json; +/// # Fetch Member +/// +/// Retrieve a member. +#[openapi(tag = "Server Members")] #[get("//members/")] pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result> { let server = target.as_server(db).await?; diff --git a/src/routes/servers/member_fetch_all.rs b/src/routes/servers/member_fetch_all.rs index 0b7a720c..f0faf534 100644 --- a/src/routes/servers/member_fetch_all.rs +++ b/src/routes/servers/member_fetch_all.rs @@ -2,12 +2,16 @@ use revolt_quark::{models::User, perms, presence::presence_filter_online, Db, Re use rocket::serde::json::Value; +/// # Fetch Members +/// +/// Fetch all server members. +#[openapi(tag = "Server Members")] #[get("//members")] pub async fn req(db: &Db, user: User, target: Ref) -> Result { let server = target.as_server(db).await?; perms(&user).server(&server).calc(db).await?; - let members = db.fetch_all_members(&server.id).await?; + let mut members = db.fetch_all_members(&server.id).await?; let mut user_ids = vec![]; for member in &members { @@ -15,7 +19,7 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result { } let online_ids = presence_filter_online(&user_ids).await; - let users = db + let mut users = db .fetch_users(&user_ids) .await? .into_iter() @@ -25,6 +29,10 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result { }) .collect::>(); + // Ensure the lists match up exactly. + members.sort_by(|a, b| a.id.user.cmp(&b.id.user)); + users.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(json!({ "members": members, "users": users diff --git a/src/routes/servers/member_remove.rs b/src/routes/servers/member_remove.rs index c0181db2..07dffa27 100644 --- a/src/routes/servers/member_remove.rs +++ b/src/routes/servers/member_remove.rs @@ -1,5 +1,9 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; +/// # Kick Member +/// +/// Removes a member from the server. +#[openapi(tag = "Server Members")] #[delete("//members/")] pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result { let server = target.as_server(db).await?; diff --git a/src/routes/servers/mod.rs b/src/routes/servers/mod.rs index 56a16a57..91c24752 100644 --- a/src/routes/servers/mod.rs +++ b/src/routes/servers/mod.rs @@ -1,4 +1,5 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod server_ack; mod server_create; @@ -25,8 +26,8 @@ mod roles_create; mod roles_delete; mod roles_edit; -pub fn routes() -> Vec { - routes![ +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![ server_create::req, server_delete::req, server_fetch::req, diff --git a/src/routes/servers/permissions_set.rs b/src/routes/servers/permissions_set.rs index b9941559..3b6128e4 100644 --- a/src/routes/servers/permissions_set.rs +++ b/src/routes/servers/permissions_set.rs @@ -6,18 +6,24 @@ use revolt_quark::{ perms, Db, Error, Override, Permission, Ref, Result, }; -#[derive(Deserialize)] -pub struct Data { +/// # Permission Value +#[derive(Deserialize, JsonSchema)] +pub struct DataSetServerRolePermission { + /// Allow / deny values for the role in this server. permissions: Override, } +/// # Set Role Permission +/// +/// Sets permissions for the specified role in the server. +#[openapi(tag = "Server Permissions")] #[put("//permissions/", data = "", rank = 2)] pub async fn req( db: &Db, user: User, target: Ref, role_id: String, - data: Json, + data: Json, ) -> Result> { let data = data.into_inner(); diff --git a/src/routes/servers/permissions_set_default.rs b/src/routes/servers/permissions_set_default.rs index ce25533a..cd651387 100644 --- a/src/routes/servers/permissions_set_default.rs +++ b/src/routes/servers/permissions_set_default.rs @@ -6,13 +6,24 @@ use revolt_quark::{ perms, Db, Permission, Ref, Result, }; -#[derive(Deserialize)] -pub struct Data { +/// # Permission Value +#[derive(Deserialize, JsonSchema)] +pub struct DataSetServerDefaultPermission { + /// Default member permission value permissions: u64, } +/// # Set Default Permission +/// +/// Sets permissions for the default role in this server. +#[openapi(tag = "Server Permissions")] #[put("//permissions/default", data = "", rank = 1)] -pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result> { +pub async fn req( + db: &Db, + user: User, + target: Ref, + data: Json, +) -> Result> { let data = data.into_inner(); let mut server = target.as_server(db).await?; diff --git a/src/routes/servers/roles_create.rs b/src/routes/servers/roles_create.rs index c95cd620..d7b4a560 100644 --- a/src/routes/servers/roles_create.rs +++ b/src/routes/servers/roles_create.rs @@ -7,15 +7,24 @@ use rocket::serde::json::{Json, Value}; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Role Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataCreateRole { + /// Role name #[validate(length(min = 1, max = 32))] name: String, + /// Ranking position + /// + /// Smaller values take priority. rank: Option, } +/// # Create Role +/// +/// Creates a new server role. +#[openapi(tag = "Server Permissions")] #[post("//roles", data = "")] -pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result { +pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result { let data = data.into_inner(); data.validate() .map_err(|error| Error::FailedValidation { error })?; diff --git a/src/routes/servers/roles_delete.rs b/src/routes/servers/roles_delete.rs index 2e9121cb..1e3bec70 100644 --- a/src/routes/servers/roles_delete.rs +++ b/src/routes/servers/roles_delete.rs @@ -1,5 +1,9 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; +/// # Delete Role +/// +/// Delete a server role by its id. +#[openapi(tag = "Server Permissions")] #[delete("//roles/")] pub async fn req(db: &Db, user: User, target: Ref, role_id: String) -> Result { let mut server = target.as_server(db).await?; diff --git a/src/routes/servers/roles_edit.rs b/src/routes/servers/roles_edit.rs index 6a9cae3c..39422e86 100644 --- a/src/routes/servers/roles_edit.rs +++ b/src/routes/servers/roles_edit.rs @@ -10,25 +10,37 @@ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Role Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataEditRole { + /// Role name #[validate(length(min = 1, max = 32))] name: Option, + /// Role colour #[validate(length(min = 1, max = 32))] colour: Option, + /// Whether this role should be displayed separately hoist: Option, + /// Ranking position + /// + /// Smaller values take priority. rank: Option, + /// Fields to remove from role object #[validate(length(min = 1))] remove: Option>, } +/// # Edit Role +/// +/// Edit a role by its id. +#[openapi(tag = "Server Permissions")] #[patch("//roles/", data = "")] pub async fn req( db: &Db, user: User, target: Ref, role_id: String, - data: Json, + data: Json, ) -> Result> { let data = data.into_inner(); data.validate() @@ -44,7 +56,7 @@ pub async fn req( let member_rank = permissions.get_member_rank().unwrap_or(i64::MIN); if let Some(mut role) = server.roles.remove(&role_id) { - let Data { + let DataEditRole { name, colour, hoist, diff --git a/src/routes/servers/server_ack.rs b/src/routes/servers/server_ack.rs index 837467ba..dc30134a 100644 --- a/src/routes/servers/server_ack.rs +++ b/src/routes/servers/server_ack.rs @@ -1,5 +1,9 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Ref, Result}; +/// # Mark Server As Read +/// +/// Mark all channels in a server as read. +#[openapi(tag = "Server Information")] #[put("//ack")] pub async fn req(db: &Db, user: User, target: Ref) -> Result { let server = target.as_server(db).await?; diff --git a/src/routes/servers/server_create.rs b/src/routes/servers/server_create.rs index 13e36752..d5dab694 100644 --- a/src/routes/servers/server_create.rs +++ b/src/routes/servers/server_create.rs @@ -12,18 +12,26 @@ use validator::Validate; use crate::util::variables::MAX_SERVER_COUNT; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Server Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataCreateServer { + /// Server name #[validate(length(min = 1, max = 32))] name: String, + /// Server description #[validate(length(min = 0, max = 1024))] description: Option, + /// Whether this server is age-restricted #[serde(skip_serializing_if = "Option::is_none")] nsfw: Option, } +/// # Create Server +/// +/// Create a new server. +#[openapi(tag = "Server Information")] #[post("/create", data = "")] -pub async fn req(db: &Db, user: User, info: Json) -> Result> { +pub async fn req(db: &Db, user: User, info: Json) -> Result> { let info = info.into_inner(); info.validate() .map_err(|error| Error::FailedValidation { error })?; @@ -34,7 +42,7 @@ pub async fn req(db: &Db, user: User, info: Json) -> Result> }); } - let Data { + let DataCreateServer { name, description, nsfw, diff --git a/src/routes/servers/server_delete.rs b/src/routes/servers/server_delete.rs index 5c8d3208..d060b204 100644 --- a/src/routes/servers/server_delete.rs +++ b/src/routes/servers/server_delete.rs @@ -1,5 +1,9 @@ use revolt_quark::{models::User, Db, EmptyResponse, Ref, Result}; +/// # Delete / Leave Server +/// +/// Deletes a server if owner otherwise leaves. +#[openapi(tag = "Server Information")] #[delete("/")] pub async fn req(db: &Db, user: User, target: Ref) -> Result { let server = target.as_server(db).await?; diff --git a/src/routes/servers/server_edit.rs b/src/routes/servers/server_edit.rs index ee840a47..25fd9d0f 100644 --- a/src/routes/servers/server_edit.rs +++ b/src/routes/servers/server_edit.rs @@ -10,29 +10,50 @@ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Server Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataEditServer { + /// Server name #[validate(length(min = 1, max = 32))] name: Option, + /// Server description #[validate(length(min = 0, max = 1024))] description: Option, + /// Attachment Id for icon icon: Option, + /// Attachment Id for banner banner: Option, + /// Category structure for server #[validate] categories: Option>, + /// System message configuration system_messages: Option, - nsfw: Option, + // Whether this server is age-restricted + // nsfw: Option, + /// Whether analytics should be collected for this server + /// + /// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg). analytics: Option, + /// Fields to remove from server object #[validate(length(min = 1))] remove: Option>, } +/// # Edit Server +/// +/// Edit a server by its id. +#[openapi(tag = "Server Information")] #[patch("/", data = "")] -pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result> { +pub async fn req( + db: &Db, + user: User, + target: Ref, + data: Json, +) -> Result> { let data = data.into_inner(); data.validate() .map_err(|error| Error::FailedValidation { error })?; @@ -48,7 +69,7 @@ pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result) -> Result) -> Result) -> Result")] pub async fn req(db: &Db, user: User, target: Ref) -> Result> { let server = target.as_server(db).await?; diff --git a/src/routes/sync/get_settings.rs b/src/routes/sync/get_settings.rs index c85802c1..3c40d3be 100644 --- a/src/routes/sync/get_settings.rs +++ b/src/routes/sync/get_settings.rs @@ -6,13 +6,25 @@ use revolt_quark::{ use rocket::serde::json::Json; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] -pub struct Options { +/// # Fetch Options +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct OptionsFetchSettings { + /// Keys to fetch keys: Vec, } +/// # Fetch Settings +/// +/// Fetch settings from server filtered by keys. +/// +/// This will return an object with the requested keys, each value is a tuple of `(timestamp, value)`, the value is the previously uploaded data. +#[openapi(tag = "Sync")] #[post("/settings/fetch", data = "")] -pub async fn req(db: &Db, user: User, options: Json) -> Result> { +pub async fn req( + db: &Db, + user: User, + options: Json, +) -> Result> { db.fetch_user_settings(&user.id, &options.into_inner().keys) .await .map(Json) diff --git a/src/routes/sync/get_unreads.rs b/src/routes/sync/get_unreads.rs index 4a728a0f..94faefe7 100644 --- a/src/routes/sync/get_unreads.rs +++ b/src/routes/sync/get_unreads.rs @@ -5,6 +5,10 @@ use revolt_quark::{ use rocket::serde::json::Json; +/// # Fetch Unreads +/// +/// Fetch information about unread state on channels. +#[openapi(tag = "Sync")] #[get("/unreads")] pub async fn req(db: &Db, user: User) -> Result>> { db.fetch_unreads(&user.id).await.map(Json) diff --git a/src/routes/sync/mod.rs b/src/routes/sync/mod.rs index d9efc4ef..4083bd44 100644 --- a/src/routes/sync/mod.rs +++ b/src/routes/sync/mod.rs @@ -1,9 +1,10 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod get_settings; mod get_unreads; mod set_settings; -pub fn routes() -> Vec { - routes![get_settings::req, set_settings::req, get_unreads::req] +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![get_settings::req, set_settings::req, get_unreads::req] } diff --git a/src/routes/sync/set_settings.rs b/src/routes/sync/set_settings.rs index d06185d0..d4795c9b 100644 --- a/src/routes/sync/set_settings.rs +++ b/src/routes/sync/set_settings.rs @@ -9,13 +9,26 @@ use std::collections::HashMap; type Data = HashMap; -#[derive(FromForm, Serialize, Deserialize)] -pub struct Options { +/// # Set Options +#[derive(FromForm, Serialize, Deserialize, JsonSchema)] +pub struct OptionsSetSettings { + /// Timestamp of settings change. + /// + /// Used to avoid feedback loops. timestamp: Option, } +/// # Set Settings +/// +/// Upload data to save to settings. +#[openapi(tag = "Sync")] #[post("/settings/set?", data = "")] -pub async fn req(db: &Db, user: User, data: Json, options: Options) -> Result { +pub async fn req( + db: &Db, + user: User, + data: Json, + options: OptionsSetSettings, +) -> Result { let data = data.into_inner(); let current_time = Utc::now().timestamp_millis(); let timestamp = if let Some(timestamp) = options.timestamp { diff --git a/src/routes/users/add_friend.rs b/src/routes/users/add_friend.rs index 39879587..2b66eca1 100644 --- a/src/routes/users/add_friend.rs +++ b/src/routes/users/add_friend.rs @@ -4,6 +4,10 @@ use revolt_quark::{Database, Result}; use rocket::serde::json::Json; use rocket::State; +/// # Send Friend Request / Accept Request +/// +/// Send a friend request to another user or accept another user's friend request. +#[openapi(tag = "Relationships")] #[put("//friend")] pub async fn req(db: &State, user: User, username: String) -> Result> { let mut target = db.fetch_user_by_username(&username).await?; diff --git a/src/routes/users/block_user.rs b/src/routes/users/block_user.rs index 4572529f..dd171fd7 100644 --- a/src/routes/users/block_user.rs +++ b/src/routes/users/block_user.rs @@ -4,6 +4,10 @@ use revolt_quark::{Database, Result}; use rocket::serde::json::Json; use rocket::State; +/// # Block User +/// +/// Block another user by their id. +#[openapi(tag = "Relationships")] #[put("//block")] pub async fn req(db: &State, user: User, target: String) -> Result> { let mut target = db.fetch_user(&target).await?; diff --git a/src/routes/users/change_username.rs b/src/routes/users/change_username.rs index e7a94802..9e534110 100644 --- a/src/routes/users/change_username.rs +++ b/src/routes/users/change_username.rs @@ -5,20 +5,27 @@ use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # Username Information +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataChangeUsername { + /// New username #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] username: String, + /// Current account password #[validate(length(min = 8, max = 1024))] password: String, } +/// # Change Username +/// +/// Change your username. +#[openapi(tag = "User Information")] #[patch("/@me/username", data = "")] pub async fn req( db: &State, account: Account, mut user: User, - data: Json, + data: Json, ) -> Result> { let data = data.into_inner(); account diff --git a/src/routes/users/edit_user.rs b/src/routes/users/edit_user.rs index 9ab79ee6..40788c96 100644 --- a/src/routes/users/edit_user.rs +++ b/src/routes/users/edit_user.rs @@ -1,5 +1,3 @@ -//! Edit currently authenticated user. - use revolt_quark::models::user::{FieldsUser, PartialUser, User}; use revolt_quark::models::File; use revolt_quark::{Database, Error, Result}; @@ -10,30 +8,48 @@ use rocket::State; use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Validate, Serialize, Deserialize, Debug)] +/// # Profile Data +#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)] pub struct UserProfileData { + /// Text to set as user profile description #[validate(length(min = 0, max = 2000))] #[serde(skip_serializing_if = "Option::is_none")] content: Option, + /// Attachment Id for background #[serde(skip_serializing_if = "Option::is_none")] #[validate(length(min = 1, max = 128))] background: Option, } -#[derive(Validate, Serialize, Deserialize)] -pub struct Data { +/// # User Data +#[derive(Validate, Serialize, Deserialize, JsonSchema)] +pub struct DataEditUser { + /// New user status #[validate] status: Option, + /// New user profile data + /// + /// This is applied as a partial. #[validate] profile: Option, + /// Attachment Id for avatar #[validate(length(min = 1, max = 128))] avatar: Option, + /// Fields to remove from user object #[validate(length(min = 1))] remove: Option>, } +/// # Edit User +/// +/// Edit currently authenticated user. +#[openapi(tag = "User Information")] #[patch("/@me", data = "")] -pub async fn req(db: &State, mut user: User, data: Json) -> Result> { +pub async fn req( + db: &State, + mut user: User, + data: Json, +) -> Result> { let data = data.into_inner(); data.validate() .map_err(|error| Error::FailedValidation { error })?; diff --git a/src/routes/users/fetch_dms.rs b/src/routes/users/fetch_dms.rs index e611eb60..2f11de44 100644 --- a/src/routes/users/fetch_dms.rs +++ b/src/routes/users/fetch_dms.rs @@ -1,7 +1,3 @@ -//! Fetch direct messages that the current user is involved in -//! -//! This includes group DMs and "active" 1:1 DMs. - use revolt_quark::{ models::{Channel, User}, Database, Result, @@ -9,6 +5,10 @@ use revolt_quark::{ use rocket::{serde::json::Json, State}; +/// # Fetch Direct Message Channels +/// +/// This fetches your direct messages, including any DM and group DM conversations. +#[openapi(tag = "Direct Messaging")] #[get("/dms")] pub async fn req(db: &State, user: User) -> Result>> { db.find_direct_messages(&user.id).await.map(Json) diff --git a/src/routes/users/fetch_profile.rs b/src/routes/users/fetch_profile.rs index 818294c2..cdd44ffa 100644 --- a/src/routes/users/fetch_profile.rs +++ b/src/routes/users/fetch_profile.rs @@ -1,8 +1,3 @@ -//! Fetch another user's profile -//! -//! Will fail if the authenticated user does not -//! have permission to access the other user's profile. - use revolt_quark::{ models::{user::UserProfile, User}, perms, Database, Error, Ref, Result, @@ -10,6 +5,12 @@ use revolt_quark::{ use rocket::{serde::json::Json, State}; +/// # Fetch User Profile +/// +/// Retrieve a user's profile data. +/// +/// Will fail if you do not have permission to access the other user's profile. +#[openapi(tag = "User Information")] #[get("//profile")] pub async fn req(db: &State, user: User, target: Ref) -> Result> { let target = target.as_user(db).await?; diff --git a/src/routes/users/fetch_self.rs b/src/routes/users/fetch_self.rs index daecd9b1..818b3243 100755 --- a/src/routes/users/fetch_self.rs +++ b/src/routes/users/fetch_self.rs @@ -1,10 +1,12 @@ -//! Fetch the currently authenticated session's user account - use revolt_quark::models::User; use revolt_quark::Result; use rocket::serde::json::Json; +/// # Fetch Self +/// +/// Retrieve your user information. +#[openapi(tag = "User Information")] #[get("/@me")] pub async fn req(user: User) -> Result> { Ok(Json(user.foreign())) diff --git a/src/routes/users/fetch_user.rs b/src/routes/users/fetch_user.rs index 411c4745..f1671d75 100644 --- a/src/routes/users/fetch_user.rs +++ b/src/routes/users/fetch_user.rs @@ -1,12 +1,11 @@ -//! Fetch another user -//! -//! Will fail if the authenticated user does not -//! have permission to access the other user. - use revolt_quark::{models::User, perms, Database, Error, Ref, Result}; use rocket::{serde::json::Json, State}; +/// # Fetch User +/// +/// Retrieve a user's information. +#[openapi(tag = "User Information")] #[get("/")] pub async fn req(db: &State, user: User, target: Ref) -> Result> { let target = target.as_user(db).await?; diff --git a/src/routes/users/find_mutual.rs b/src/routes/users/find_mutual.rs index 59750ef6..2e4627ee 100644 --- a/src/routes/users/find_mutual.rs +++ b/src/routes/users/find_mutual.rs @@ -1,12 +1,13 @@ -//! Fetch IDs of servers and friends that are mutually shared between the -//! authenticated user and the target user - use revolt_quark::models::User; use revolt_quark::{perms, Database, Error, Ref, Result}; use rocket::serde::json::Value; use rocket::State; +/// # Fetch Mutual Friends And Servers +/// +/// Retrieve a list of mutual friends and servers with another user. +#[openapi(tag = "Relationships")] #[get("//mutual")] pub async fn req(db: &State, user: User, target: Ref) -> Result { let target = target.as_user(db).await?; diff --git a/src/routes/users/get_default_avatar.rs b/src/routes/users/get_default_avatar.rs index d3b07eb3..8ec688bf 100644 --- a/src/routes/users/get_default_avatar.rs +++ b/src/routes/users/get_default_avatar.rs @@ -1,6 +1,8 @@ use rocket::http::ContentType; use rocket::response::{self, Responder}; use rocket::{Request, Response}; +use rocket_okapi::okapi::openapi3::{self, MediaType, RefOr}; +use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; pub struct CachedFile((ContentType, Vec)); @@ -14,13 +16,51 @@ impl<'r> Responder<'r, 'static> for CachedFile { } } -// Charset: 0123456789ABCDEFGHJKMNPQRSTVWXYZ +impl rocket_okapi::response::OpenApiResponderInner for CachedFile { + fn responses( + _gen: &mut rocket_okapi::gen::OpenApiGenerator, + ) -> std::result::Result { + let mut responses = schemars::Map::new(); + let mut content = schemars::Map::new(); + content.insert( + "image/png".to_owned(), + MediaType { + schema: Some(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + format: Some("binary".to_owned()), + ..Default::default() + }), + ..Default::default() + }, + ); + + responses.insert( + "200".to_string(), + RefOr::Object(openapi3::Response { + description: "Default Avatar Picture".to_string(), + content, + ..Default::default() + }), + ); + + Ok(openapi3::Responses { + responses, + ..Default::default() + }) + } +} + +/// # Fetch Default Avatar +/// +/// This returns a default avatar based on the given id. +#[openapi(tag = "User Information")] #[get("//default_avatar")] pub async fn req(target: String) -> CachedFile { CachedFile(( ContentType::PNG, match target.chars().last().unwrap() { + // 0123456789ABCDEFGHJKMNPQRSTVWXYZ '0' | '1' | '2' | '3' | 'S' | 'Z' => { include_bytes!("../../../assets/user/2.png").to_vec() } diff --git a/src/routes/users/mod.rs b/src/routes/users/mod.rs index 1c34fe2c..60b92e94 100644 --- a/src/routes/users/mod.rs +++ b/src/routes/users/mod.rs @@ -1,4 +1,5 @@ use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; mod add_friend; mod block_user; @@ -14,8 +15,8 @@ mod open_dm; mod remove_friend; mod unblock_user; -pub fn routes() -> Vec { - routes![ +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![ // User Information fetch_self::req, fetch_user::req, diff --git a/src/routes/users/open_dm.rs b/src/routes/users/open_dm.rs index dae9a7a9..f962a002 100644 --- a/src/routes/users/open_dm.rs +++ b/src/routes/users/open_dm.rs @@ -1,5 +1,3 @@ -//! Open a direct message with another user - use revolt_quark::{ models::{Channel, User}, perms, Database, Error, Ref, Result, UserPermission, @@ -8,6 +6,10 @@ use revolt_quark::{ use rocket::{serde::json::Json, State}; use ulid::Ulid; +/// # Open Direct Message +/// +/// Open a DM with another user. +#[openapi(tag = "Direct Messaging")] #[get("//dm")] pub async fn req(db: &State, user: User, target: Ref) -> Result> { let target = target.as_user(db).await?; diff --git a/src/routes/users/remove_friend.rs b/src/routes/users/remove_friend.rs index 314de23f..5d6c3734 100644 --- a/src/routes/users/remove_friend.rs +++ b/src/routes/users/remove_friend.rs @@ -4,6 +4,10 @@ use revolt_quark::{Database, Result}; use rocket::serde::json::Json; use rocket::State; +/// # Deny Friend Request / Remove Friend +/// +/// Denies another user's friend request or removes an existing friend. +#[openapi(tag = "Relationships")] #[delete("//friend")] pub async fn req(db: &State, user: User, target: String) -> Result> { let mut target = db.fetch_user(&target).await?; diff --git a/src/routes/users/unblock_user.rs b/src/routes/users/unblock_user.rs index b907669f..e7733191 100644 --- a/src/routes/users/unblock_user.rs +++ b/src/routes/users/unblock_user.rs @@ -4,6 +4,10 @@ use revolt_quark::{Database, Result}; use rocket::serde::json::Json; use rocket::State; +/// # Unblock User +/// +/// Unblock another user by their id. +#[openapi(tag = "Relationships")] #[delete("//block")] pub async fn req(db: &State, user: User, target: String) -> Result> { let mut target = db.fetch_user(&target).await?; diff --git a/src/util/idempotency.rs b/src/util/idempotency.rs index a35b3a74..74dfeb40 100644 --- a/src/util/idempotency.rs +++ b/src/util/idempotency.rs @@ -55,7 +55,7 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey { required: false, deprecated: false, extensions: schemars::Map::new(), - location: "sus".to_string(), + location: "header".to_string(), value: ParameterValue::Schema { allow_reserved: false, example: None,