chore(doc): implement remainder of documentation

This commit is contained in:
Paul Makles
2022-03-19 18:19:24 +00:00
parent 43c475eabb
commit 8dd9acdb59
54 changed files with 499 additions and 123 deletions

35
Cargo.lock generated
View File

@@ -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",

View File

@@ -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

View File

@@ -22,6 +22,8 @@ pub struct DataEditBot {
/// Whether the bot can be added by anyone
public: Option<bool>,
/// Whether analytics should be gathered for this bot
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
analytics: Option<bool>,
/// 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("/<target>", data = "<data>")]
pub async fn edit_bot(db: &Db, user: User, target: Ref, data: Json<DataEditBot>) -> Result<Json<Bot>> {
pub async fn edit_bot(
db: &Db,
user: User,
target: Ref,
data: Json<DataEditBot>,
) -> Result<Json<Bot>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}

View File

@@ -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<String>,
/// Message content to send

View File

@@ -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("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;

View File

@@ -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<File>,
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// 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<String>,
/// 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<File>,
/// 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<String>,
/// 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<File>,
},
}
/// # Fetch Invite
///
/// Fetch an invite by its id.
#[openapi(tag = "Invites")]
#[get("/<target>")]
pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
Ok(Json(match target.as_invite(db).await? {

View File

@@ -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("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {

View File

@@ -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<Route> {
routes![invite_fetch::req, invite_join::req, invite_delete::req]
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req]
}

View File

@@ -20,19 +20,19 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
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<!-- ReDoc-Inject: <security-definitions> -->".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()
}
}

View File

@@ -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 = "<data>")]
pub async fn req(
db: &State<Database>,
session: Session,
user: Option<User>,
data: Json<Data>,
data: Json<DataOnboard>,
) -> Result<EmptyResponse> {
if user.is_some() {
return Err(Error::AlreadyOnboarded);

View File

@@ -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<User>) -> Value {
json!({
"onboarding": user.is_none()
pub async fn req(_session: Session, user: Option<User>) -> Json<DataHello> {
Json(DataHello {
onboarding: user.is_none(),
})
}

View File

@@ -1,8 +1,9 @@
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod complete;
mod hello;
pub fn routes() -> Vec<Route> {
routes![hello::req, complete::req]
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![hello::req, complete::req]
}

View File

@@ -1,8 +1,9 @@
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![subscribe::req, unsubscribe::req]
}

View File

@@ -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 = "<data>")]
pub async fn req(
auth: &State<Auth>,

View File

@@ -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<Auth>, mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;

View File

@@ -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<String>,
}
/// # Ban User
///
/// Ban a user by their id.
#[openapi(tag = "Server Members")]
#[put("/<server>/bans/<target>", data = "<data>")]
pub async fn req(
db: &Db,
user: User,
server: Ref,
target: Ref,
data: Json<Data>,
data: Json<DataBanCreate>,
) -> Result<Json<ServerBan>> {
let data = data.into_inner();
data.validate()

View File

@@ -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<File>,
}
#[derive(Serialize, Deserialize)]
/// # Ban List Result
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct BanListResult {
/// Users objects
users: Vec<BannedUser>,
/// Ban objects
bans: Vec<ServerBan>,
}
/// # Fetch Bans
///
/// Fetch all bans on a server.
#[openapi(tag = "Server Members")]
#[get("/<target>/bans")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<BanListResult>> {
let server = target.as_server(db).await?;

View File

@@ -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("/<server>/bans/<target>")]
pub async fn req(db: &Db, user: User, server: Ref, target: Ref) -> Result<EmptyResponse> {
let server = server.as_server(db).await?;

View File

@@ -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<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Channel
///
/// Create a new Text or Voice channel.
#[openapi(tag = "Server Information")]
#[post("/<target>/channels", data = "<info>")]
pub async fn req(db: &Db, user: User, target: Ref, info: Json<Data>) -> Result<Json<Channel>> {
pub async fn req(
db: &Db,
user: User,
target: Ref,
info: Json<DataCreateChannel>,
) -> Result<Json<Channel>> {
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<Data>) -> Result<J
let mut channels = server.channels.clone();
channels.push(id.clone());
let Data {
let DataCreateChannel {
name,
description,
nsfw,

View File

@@ -4,16 +4,11 @@ use revolt_quark::{
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServerInvite {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
}
/// # Fetch Invites
///
/// Fetch all server invites.
#[openapi(tag = "Server Members")]
#[get("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Invite>>> {
let server = target.as_server(db).await?;

View File

@@ -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<String>,
/// Attachment Id to set for avatar
avatar: Option<String>,
/// Array of role ids
roles: Option<Vec<String>>,
/// Fields to remove from channel object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsMember>>,
}
/// # Edit Member
///
/// Edit a member by their id.
#[openapi(tag = "Server Members")]
#[patch("/<server>/members/<target>", data = "<data>")]
pub async fn req(
db: &Db,
user: User,
server: Ref,
target: Ref,
data: Json<Data>,
data: Json<DataMemberEdit>,
) -> Result<Json<Member>> {
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,

View File

@@ -4,6 +4,10 @@ use revolt_quark::{
};
use rocket::serde::json::Json;
/// # Fetch Member
///
/// Retrieve a member.
#[openapi(tag = "Server Members")]
#[get("/<target>/members/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<Json<Member>> {
let server = target.as_server(db).await?;

View File

@@ -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("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Value> {
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<Value> {
}
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<Value> {
})
.collect::<Vec<User>>();
// 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

View File

@@ -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("/<target>/members/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
let server = target.as_server(db).await?;

View File

@@ -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<Route> {
routes![
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
server_create::req,
server_delete::req,
server_fetch::req,

View File

@@ -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("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(
db: &Db,
user: User,
target: Ref,
role_id: String,
data: Json<Data>,
data: Json<DataSetServerRolePermission>,
) -> Result<Json<Server>> {
let data = data.into_inner();

View File

@@ -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("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<Json<Server>> {
pub async fn req(
db: &Db,
user: User,
target: Ref,
data: Json<DataSetServerDefaultPermission>,
) -> Result<Json<Server>> {
let data = data.into_inner();
let mut server = target.as_server(db).await?;

View File

@@ -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<i64>,
}
/// # Create Role
///
/// Creates a new server role.
#[openapi(tag = "Server Permissions")]
#[post("/<target>/roles", data = "<data>")]
pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<Value> {
pub async fn req(db: &Db, user: User, target: Ref, data: Json<DataCreateRole>) -> Result<Value> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;

View File

@@ -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("/<target>/roles/<role_id>")]
pub async fn req(db: &Db, user: User, target: Ref, role_id: String) -> Result<EmptyResponse> {
let mut server = target.as_server(db).await?;

View File

@@ -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<String>,
/// Role colour
#[validate(length(min = 1, max = 32))]
colour: Option<String>,
/// Whether this role should be displayed separately
hoist: Option<bool>,
/// Ranking position
///
/// Smaller values take priority.
rank: Option<i64>,
/// Fields to remove from role object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsRole>>,
}
/// # Edit Role
///
/// Edit a role by its id.
#[openapi(tag = "Server Permissions")]
#[patch("/<target>/roles/<role_id>", data = "<data>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
role_id: String,
data: Json<Data>,
data: Json<DataEditRole>,
) -> Result<Json<Role>> {
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,

View File

@@ -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("/<target>/ack")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
let server = target.as_server(db).await?;

View File

@@ -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<String>,
/// Whether this server is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Server
///
/// Create a new server.
#[openapi(tag = "Server Information")]
#[post("/create", data = "<info>")]
pub async fn req(db: &Db, user: User, info: Json<Data>) -> Result<Json<Server>> {
pub async fn req(db: &Db, user: User, info: Json<DataCreateServer>) -> Result<Json<Server>> {
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<Data>) -> Result<Json<Server>>
});
}
let Data {
let DataCreateServer {
name,
description,
nsfw,

View File

@@ -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("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
let server = target.as_server(db).await?;

View File

@@ -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<String>,
/// Server description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Attachment Id for icon
icon: Option<String>,
/// Attachment Id for banner
banner: Option<String>,
/// Category structure for server
#[validate]
categories: Option<Vec<Category>>,
/// System message configuration
system_messages: Option<SystemMessageChannels>,
nsfw: Option<bool>,
// Whether this server is age-restricted
// nsfw: Option<bool>,
/// Whether analytics should be collected for this server
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
analytics: Option<bool>,
/// Fields to remove from server object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsServer>>,
}
/// # Edit Server
///
/// Edit a server by its id.
#[openapi(tag = "Server Information")]
#[patch("/<target>", data = "<data>")]
pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<Json<Server>> {
pub async fn req(
db: &Db,
user: User,
target: Ref,
data: Json<DataEditServer>,
) -> Result<Json<Server>> {
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<Data>) -> Result<J
&& data.banner.is_none()
&& data.system_messages.is_none()
&& data.categories.is_none()
&& data.nsfw.is_none()
// && data.nsfw.is_none()
&& data.analytics.is_none()
&& data.remove.is_none()
{
@@ -58,7 +79,7 @@ pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<J
|| data.icon.is_some()
|| data.banner.is_some()
|| data.system_messages.is_some()
|| data.nsfw.is_some()
// || data.nsfw.is_some()
|| data.analytics.is_some()
|| data.remove.is_some()
{
@@ -73,14 +94,14 @@ pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<J
.await?;
}
let Data {
let DataEditServer {
name,
description,
icon,
banner,
categories,
system_messages,
nsfw,
// nsfw,
analytics,
remove,
} = data;
@@ -90,7 +111,7 @@ pub async fn req(db: &Db, user: User, target: Ref, data: Json<Data>) -> Result<J
description,
categories,
system_messages,
nsfw,
// nsfw,
analytics,
..Default::default()
};

View File

@@ -4,6 +4,10 @@ use revolt_quark::{
};
use rocket::serde::json::Json;
/// # Fetch Server
///
/// Fetch a server by its id.
#[openapi(tag = "Server Information")]
#[get("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Server>> {
let server = target.as_server(db).await?;

View File

@@ -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<String>,
}
/// # 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 = "<options>")]
pub async fn req(db: &Db, user: User, options: Json<Options>) -> Result<Json<UserSettings>> {
pub async fn req(
db: &Db,
user: User,
options: Json<OptionsFetchSettings>,
) -> Result<Json<UserSettings>> {
db.fetch_user_settings(&user.id, &options.into_inner().keys)
.await
.map(Json)

View File

@@ -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<Json<Vec<ChannelUnread>>> {
db.fetch_unreads(&user.id).await.map(Json)

View File

@@ -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<Route> {
routes![get_settings::req, set_settings::req, get_unreads::req]
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![get_settings::req, set_settings::req, get_unreads::req]
}

View File

@@ -9,13 +9,26 @@ use std::collections::HashMap;
type Data = HashMap<String, String>;
#[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<i64>,
}
/// # Set Settings
///
/// Upload data to save to settings.
#[openapi(tag = "Sync")]
#[post("/settings/set?<options..>", data = "<data>")]
pub async fn req(db: &Db, user: User, data: Json<Data>, options: Options) -> Result<EmptyResponse> {
pub async fn req(
db: &Db,
user: User,
data: Json<Data>,
options: OptionsSetSettings,
) -> Result<EmptyResponse> {
let data = data.into_inner();
let current_time = Utc::now().timestamp_millis();
let timestamp = if let Some(timestamp) = options.timestamp {

View File

@@ -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("/<username>/friend")]
pub async fn req(db: &State<Database>, user: User, username: String) -> Result<Json<User>> {
let mut target = db.fetch_user_by_username(&username).await?;

View File

@@ -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("/<target>/block")]
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
let mut target = db.fetch_user(&target).await?;

View File

@@ -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 = "<data>")]
pub async fn req(
db: &State<Database>,
account: Account,
mut user: User,
data: Json<Data>,
data: Json<DataChangeUsername>,
) -> Result<Json<User>> {
let data = data.into_inner();
account

View File

@@ -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<String>,
/// Attachment Id for background
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
background: Option<String>,
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
/// # User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditUser {
/// New user status
#[validate]
status: Option<UserStatus>,
/// New user profile data
///
/// This is applied as a partial.
#[validate]
profile: Option<UserProfileData>,
/// Attachment Id for avatar
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
/// Fields to remove from user object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsUser>>,
}
/// # Edit User
///
/// Edit currently authenticated user.
#[openapi(tag = "User Information")]
#[patch("/@me", data = "<data>")]
pub async fn req(db: &State<Database>, mut user: User, data: Json<Data>) -> Result<Json<User>> {
pub async fn req(
db: &State<Database>,
mut user: User,
data: Json<DataEditUser>,
) -> Result<Json<User>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;

View File

@@ -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<Database>, user: User) -> Result<Json<Vec<Channel>>> {
db.find_direct_messages(&user.id).await.map(Json)

View File

@@ -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("/<target>/profile")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<UserProfile>> {
let target = target.as_user(db).await?;

View File

@@ -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<Json<User>> {
Ok(Json(user.foreign()))

View File

@@ -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("/<target>")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<User>> {
let target = target.as_user(db).await?;

View File

@@ -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("/<target>/mutual")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Value> {
let target = target.as_user(db).await?;

View File

@@ -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<u8>));
@@ -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<openapi3::Responses, rocket_okapi::OpenApiError> {
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("/<target>/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()
}

View File

@@ -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<Route> {
routes![
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
// User Information
fetch_self::req,
fetch_user::req,

View File

@@ -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("/<target>/dm")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<Channel>> {
let target = target.as_user(db).await?;

View File

@@ -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("/<target>/friend")]
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
let mut target = db.fetch_user(&target).await?;

View File

@@ -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("/<target>/block")]
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
let mut target = db.fetch_user(&target).await?;

View File

@@ -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,