diff --git a/.gitignore b/.gitignore index b0e641b..9621492 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.profraw result result-* +/docs/types.json # Test configurations. config.toml diff --git a/Cargo.lock b/Cargo.lock index 71e97a3..223cb6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,7 @@ dependencies = [ "sha2", "thiserror", "url", + "utoipa", ] [[package]] @@ -2692,6 +2693,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845494cea2113cf53dbb60904638afc8cb6c36fe39be7bcbb0eca1cfa49c3c1a" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1223ed4a64c622737615a02d062c20813b978d9d39ceced627337449b195771" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "url", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2939da7..f681a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ members = [ ] default-members = ["blahd"] +[workspace.dependencies] +utoipa = "5" + [workspace.lints.clippy] allow_attributes_without_reason = "warn" dbg_macro = "warn" diff --git a/blah-types/Cargo.toml b/blah-types/Cargo.toml index cff810a..51c21a9 100644 --- a/blah-types/Cargo.toml +++ b/blah-types/Cargo.toml @@ -6,6 +6,11 @@ edition = "2021" [features] default = [] unsafe_use_mock_instant_for_testing = ["dep:mock_instant"] +utoipa = ["dep:utoipa"] + +[[example]] +name = "openapi" +required-features = ["utoipa"] [[bench]] name = "crypto_ops" @@ -26,6 +31,7 @@ serde_json = "1" serde_with = "3" thiserror = "1" url = { version = "2", features = ["serde"] } +utoipa = { workspace = true, optional = true, features = ["url"] } # Generics support. [dev-dependencies] criterion = "0.5" diff --git a/blah-types/examples/openapi.rs b/blah-types/examples/openapi.rs new file mode 100644 index 0000000..284ce88 --- /dev/null +++ b/blah-types/examples/openapi.rs @@ -0,0 +1,8 @@ +#![expect(clippy::print_stdout, reason = "allowed to dump OAPI")] + +fn main() { + let json = blah_types::openapi() + .to_pretty_json() + .expect("serialization cannot fail"); + println!("{json}"); +} diff --git a/blah-types/src/crypto.rs b/blah-types/src/crypto.rs index 2e97ce4..cba392e 100644 --- a/blah-types/src/crypto.rs +++ b/blah-types/src/crypto.rs @@ -11,6 +11,7 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct UserKey { pub id_key: PubKey, pub act_key: PubKey, @@ -18,6 +19,7 @@ pub struct UserKey { #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = String))] pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); impl FromStr for PubKey { @@ -55,14 +57,17 @@ impl From<&VerifyingKey> for PubKey { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(deny_unknown_fields)] pub struct Signed { #[serde(with = "hex::serde")] + #[cfg_attr(feature = "utoipa", schema(value_type = String))] pub sig: [u8; SIGNATURE_LENGTH], pub signee: Signee, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(deny_unknown_fields)] pub struct Signee { pub nonce: u32, diff --git a/blah-types/src/identity.rs b/blah-types/src/identity.rs index 5012c70..2b56086 100644 --- a/blah-types/src/identity.rs +++ b/blah-types/src/identity.rs @@ -12,6 +12,7 @@ use crate::{PubKey, Signed}; /// User identity description structure. // TODO: Revise and shrink duplicates (pubkey fields). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct UserIdentityDesc { /// User primary identity key, only for signing action keys. pub id_key: PubKey, @@ -90,6 +91,7 @@ impl UserIdentityDesc { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "user_act_key")] pub struct UserActKeyDesc { pub act_key: PubKey, @@ -98,6 +100,7 @@ pub struct UserActKeyDesc { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "user_profile")] pub struct UserProfile { pub preferred_chat_server_urls: Vec, @@ -105,6 +108,7 @@ pub struct UserProfile { } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = Url))] #[serde(try_from = "Url")] pub struct IdUrl(Url); diff --git a/blah-types/src/lib.rs b/blah-types/src/lib.rs index 699f95b..d8098f5 100644 --- a/blah-types/src/lib.rs +++ b/blah-types/src/lib.rs @@ -10,3 +10,46 @@ pub mod crypto; pub mod identity; pub mod msg; pub mod server; + +#[cfg(feature = "utoipa")] +pub fn openapi() -> utoipa::openapi::OpenApi { + use utoipa::OpenApi; + + #[derive(OpenApi)] + #[openapi(components(schemas( + crypto::Signed::, + crypto::Signed::, + crypto::Signed::, + crypto::Signed::, + crypto::Signed::, + crypto::Signed::, + identity::UserIdentityDesc, + identity::UserProfile, + msg::AuthPayload, + msg::ChatPayload, + msg::DeleteRoomPayload, + msg::RichText, + msg::RoomAdminPayload, + msg::UserRegisterPayload, + server::ClientEvent, + server::ErrorResponse, + server::ErrorResponseWithChallenge, + server::RoomList, + server::RoomMetadata, + server::RoomMsgs, + server::ServerCapabilities, + server::ServerEvent, + server::ServerMetadata, + )))] + struct ApiDoc; + + ApiDoc::openapi() +} + +#[cfg(feature = "utoipa")] +#[test] +#[expect(clippy::print_stdout, reason = "allowed in tests")] +fn test_openapi() { + let json = crate::openapi().to_pretty_json().unwrap(); + println!("{json}"); +} diff --git a/blah-types/src/msg.rs b/blah-types/src/msg.rs index 7d94a3c..e4f75e6 100644 --- a/blah-types/src/msg.rs +++ b/blah-types/src/msg.rs @@ -15,6 +15,7 @@ use crate::{PubKey, Signed}; /// It's currently serialized as a string for JavaScript's convenience. #[serde_as] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = String))] #[serde(transparent)] pub struct Id(#[serde_as(as = "DisplayFromStr")] pub i64); @@ -39,6 +40,7 @@ impl Id { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct WithMsgId { pub cid: Id, #[serde(flatten)] @@ -53,17 +55,20 @@ impl WithMsgId { /// Register a user on a chat server. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "user_register")] pub struct UserRegisterPayload { pub server_url: Url, pub id_url: IdUrl, pub id_key: PubKey, #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub challenge: Option, } /// The server-specific challenge data for registration. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum UserRegisterChallengeResponse { /// Proof of work challenge containing the same nonce from server challenge request. @@ -73,6 +78,7 @@ pub enum UserRegisterChallengeResponse { // FIXME: `deny_unknown_fields` breaks this. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "chat")] pub struct ChatPayload { pub rich_text: RichText, @@ -81,6 +87,7 @@ pub struct ChatPayload { /// Ref: #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = Vec))] #[serde(transparent)] pub struct RichText(pub Vec); @@ -103,8 +110,9 @@ impl Serialize for RichTextPiece { } } -/// The protocol representation of `RichTextPiece`. +/// The representation on wire of `RichTextPiece`. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(untagged)] enum RichTextPieceRaw { Text(String), @@ -148,6 +156,7 @@ impl<'de> Deserialize<'de> for RichText { // TODO: This protocol format is quite large. Could use bitflags for database. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct TextAttrs { #[serde(default, rename = "b", skip_serializing_if = "is_default")] pub bold: bool, @@ -159,6 +168,7 @@ pub struct TextAttrs { pub italic: bool, // TODO: Should we validate and/or filter the URL. #[serde(default, skip_serializing_if = "is_default")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub link: Option, #[serde(default, rename = "s", skip_serializing_if = "is_default")] pub strike: bool, @@ -271,6 +281,7 @@ pub type SignedChatMsg = Signed; pub type SignedChatMsgWithId = WithMsgId; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ")] pub enum CreateRoomPayload { #[serde(rename = "create_room")] @@ -281,6 +292,7 @@ pub enum CreateRoomPayload { /// Multi-user room. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct CreateGroup { pub attrs: RoomAttrs, pub title: String, @@ -288,11 +300,13 @@ pub struct CreateGroup { /// Peer-to-peer chat room with exactly two symmetric users. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct CreatePeerChat { pub peer: PubKey, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "delete_room")] pub struct DeleteRoomPayload { pub room: Id, @@ -302,6 +316,7 @@ pub struct DeleteRoomPayload { /// 1. Sorted by userkeys. /// 2. No duplicated users. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(try_from = "Vec")] pub struct RoomMemberList(pub Vec); @@ -327,6 +342,7 @@ impl TryFrom> for RoomMemberList { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomMember { pub permission: MemberPermission, pub user: PubKey, @@ -336,11 +352,13 @@ pub struct RoomMember { /// /// TODO: Should we use JWT here instead? #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename = "auth")] pub struct AuthPayload {} // FIXME: Remove this. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] // `typ` is provided by `RoomAdminOp`. pub struct RoomAdminPayload { #[serde(flatten)] @@ -348,6 +366,7 @@ pub struct RoomAdminPayload { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename_all = "snake_case", rename = "remove_member")] pub struct RemoveMemberPayload { pub room: Id, @@ -357,6 +376,7 @@ pub struct RemoveMemberPayload { // TODO: Maybe disallow adding other user without consent? #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename_all = "snake_case", rename = "add_member")] pub struct AddMemberPayload { pub room: Id, @@ -365,6 +385,7 @@ pub struct AddMemberPayload { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(tag = "typ", rename_all = "snake_case", rename = "update_member")] pub struct UpdateMemberPayload { pub room: Id, @@ -373,6 +394,7 @@ pub struct UpdateMemberPayload { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(untagged)] pub enum RoomAdminOp { AddMember(AddMemberPayload), @@ -382,6 +404,7 @@ pub enum RoomAdminOp { bitflags::bitflags! { /// TODO: Is this a really all about permission, or is a generic `UserFlags`? #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = i32))] pub struct ServerPermission: i32 { const CREATE_ROOM = 1 << 0; @@ -391,6 +414,7 @@ bitflags::bitflags! { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = i32))] pub struct MemberPermission: i32 { const POST_CHAT = 1 << 0; const ADD_MEMBER = 1 << 1; @@ -410,6 +434,7 @@ bitflags::bitflags! { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = i32))] pub struct RoomAttrs: i32 { // NB. Used by schema. const PUBLIC_READABLE = 1 << 0; diff --git a/blah-types/src/server.rs b/blah-types/src/server.rs index 3696cb1..2f1abc6 100644 --- a/blah-types/src/server.rs +++ b/blah-types/src/server.rs @@ -10,6 +10,7 @@ use crate::PubKey; /// The response object returned as body on HTTP error status. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ErrorResponse { /// The error object. pub error: ErrorObject, @@ -18,21 +19,27 @@ pub struct ErrorResponse { /// The response object of `/_blah/user/me` endpoint on HTTP error status. /// It contains additional registration information. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ErrorResponseWithChallenge { /// The error object. pub error: ErrorObject, /// The challenge metadata returned by the `/_blah/user/me` endpoint for registration. #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub register_challenge: Option, } /// The error object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ErrorObject { /// A machine-readable error code string. + #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "user_not_found"))] pub code: S, + /// A human-readable error message. + #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "the user does not exist"))] pub message: S, } @@ -50,16 +57,20 @@ impl std::error::Error for ErrorObject {} /// It may contains extra fields and clients should ignore them for future compatibility. /// Chat Servers can also include any custom fields here as long they have a `_` prefix. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ServerMetadata { /// A server-defined version string indicating its implementation name and the version. /// /// It is expected to be in form `/` but not mandatory. + #[cfg_attr(feature = "utoipa", schema(example = "blahd/0.0.1"))] pub server: String, /// The URL to the source code of the Chat Server. /// /// It is expected to be a public accessible maybe-compressed tarball link without /// access control. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub src_url: Option, /// The server capabilities set. @@ -67,6 +78,7 @@ pub struct ServerMetadata { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ServerCapabilities { /// Whether registration is open to public. pub allow_public_register: bool, @@ -74,6 +86,7 @@ pub struct ServerCapabilities { /// Registration challenge information. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum UserRegisterChallenge { /// Proof-of-work (PoW) challenge. @@ -86,67 +99,82 @@ pub enum UserRegisterChallenge { /// Response to list rooms. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomList { /// Result list of rooms. pub rooms: Vec, /// The skip-token to fetch the next page. #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub skip_token: Option, } /// The metadata of a room. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomMetadata { /// Room id. pub rid: Id, /// Plain text room title. None for peer chat. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub title: Option, /// Room attributes. pub attrs: RoomAttrs, // Extra information is only available for some APIs. /// The last message in the room. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub last_msg: Option, /// The current user's last seen message's `cid`. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub last_seen_cid: Option, /// The number of unseen messages, ie. the number of messages from `last_seen_cid` to /// `last_msg.cid`. /// This may or may not be a precise number. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub unseen_cnt: Option, /// The member permission of current user in the room, or `None` if it is not a member. /// Only available with authentication. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub member_permission: Option, /// The peer user, if this is a peer chat room. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub peer_user: Option, } /// Response to list room msgs. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomMsgs { /// Result list of msgs. pub msgs: Vec, /// The skip-token to fetch the next page. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub skip_token: Option, } /// Response to list room members. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomMemberList { /// Result list of members. pub members: Vec, /// The skip-token to fetch the next page. #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub skip_token: Option, } /// The description of a room member. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RoomMember { /// The identity key of the member user. pub id_key: PubKey, @@ -154,11 +182,13 @@ pub struct RoomMember { pub permission: MemberPermission, /// The user's last seen message `cid` in the room. #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "utoipa", schema(nullable = false))] pub last_seen_cid: Option, } /// A server-to-client event. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum ServerEvent { /// A message from a joined room. @@ -170,5 +200,6 @@ pub enum ServerEvent { /// A client-to-server event. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum ClientEvent {} diff --git a/docs/webapi.yaml b/docs/webapi.yaml index 38430c1..8419841 100644 --- a/docs/webapi.yaml +++ b/docs/webapi.yaml @@ -12,19 +12,7 @@ paths: content: application/json: schema: - type: object - properties: - server: - type: string - example: 'blah/0.0.0' - src_url: - type: string - example: 'https://github.com/Blah-IM/blahrs' - capabilities: - type: object - properties: - allow_public_register: - type: boolean + $ref: 'types.json#/components/schemas/ServerMetadata' # OAPI does not support WebSocket interface definitions. # See: https://github.com/OAI/OpenAPI-Specification/issues/55#issuecomment-929382279 @@ -35,7 +23,7 @@ paths: This endpoint is for server-side-event dispatching. Once connected, client must send a JSON text message of type - `Signed-Auth` for authentication. + `Signed_Auth` for authentication. If server does not close it immediately, it means success. Since OAPI does not support WebSocket interface, we use request and @@ -54,7 +42,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/WSClientToServer' + $ref: 'types.json#/components/schemas/ClientEvent' responses: 101: @@ -66,7 +54,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/WSServerToClient' + $ref: 'types.json#/components/schemas/ServerEvent' /_blah/user/me: get: @@ -76,7 +64,7 @@ paths: in: header description: Optional user authentication token. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' responses: 204: @@ -88,7 +76,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiErrorWithRegisterChallenge' + $ref: 'types.json#/components/schemas/ErrorResponseWithChallenge' post: summary: Register or update user identity @@ -116,7 +104,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-UserRegister' + $ref: 'types.json#/components/schemas/Signed_UserRegisterPayload' responses: 204: @@ -127,7 +115,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 401: description: | @@ -136,7 +124,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 403: description: | @@ -145,7 +133,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 409: description: | @@ -153,7 +141,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 422: description: | @@ -163,7 +151,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room: get: @@ -207,7 +195,7 @@ paths: in: header description: Optional proof of membership for private rooms. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' responses: 200: @@ -215,14 +203,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RoomList' + $ref: 'types.json#/components/schemas/RoomList' 401: description: Missing or invalid Authorization header. content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' post: summary: Create a room @@ -237,7 +225,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-CreateRoom' + $ref: 'types.json#/components/schemas/Signed_CreateRoomPayload' responses: 200: @@ -253,7 +241,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 404: description: | @@ -262,14 +250,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 409: description: There is already a peer chat room between the user pair. content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/create: post: @@ -286,7 +274,7 @@ paths: in: header description: Optional proof of membership for private rooms. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' responses: 200: @@ -294,7 +282,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RoomMetadata' + $ref: 'types.json#/components/schemas/RoomMetadata' 404: description: | @@ -302,7 +290,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' delete: summary: Delete a room @@ -310,7 +298,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-DeleteRoom' + $ref: 'types.json#/components/schemas/Signed_DeleteRoomPayload' responses: 204: @@ -321,7 +309,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 404: description: | @@ -329,7 +317,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/admin: post: @@ -343,7 +331,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-RoomAdmin' + $ref: 'types.json#/components/schemas/Signed_RoomAdminPayload' responses: 204: @@ -356,7 +344,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 409: description: @@ -364,7 +352,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/feed.json: get: @@ -385,7 +373,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/feed.atom: get: @@ -408,8 +396,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' - + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/msg: get: @@ -426,7 +413,7 @@ paths: in: header description: Optional proof of membership for private rooms. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' - name: top in: query @@ -450,7 +437,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RoomMsgs' + $ref: 'types.json#/components/schemas/RoomMsgs' 404: description: | @@ -458,7 +445,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' post: summary: Post a `Msg` into a room @@ -466,14 +453,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-Chat' + $ref: 'types.json#/components/schemas/Signed_ChatPayload' responses: 200: content: application/json: schema: - type: string + $ref: 'types.json#/components/schemas/Id' description: Newly created message id `cid`. 403: @@ -481,14 +468,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 404: description: The room does not exist or the user is not a room member. content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/msg/{cid}/seen: post: @@ -507,7 +494,7 @@ paths: required: true description: Proof of membership for private rooms. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' responses: 204: @@ -519,7 +506,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/member: get: @@ -530,7 +517,7 @@ paths: required: true description: Proof of membership. schema: - $ref: '#/components/schemas/Signed-Auth' + $ref: 'types.json#/components/schemas/Signed_AuthPayload' - name: top in: query @@ -553,7 +540,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RoomMemberList' + $ref: 'types.json#/components/schemas/RoomMemberList' 403: description: | @@ -561,7 +548,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 404: description: | @@ -569,7 +556,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' post: summary: Join a room @@ -577,7 +564,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-AddMember' + $ref: 'types.json#/components/schemas/Signed_AddMemberPayload' responses: 204: @@ -590,7 +577,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' 409: description: @@ -598,7 +585,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' /_blah/room/{rid}/member/{member_id_key}: get: @@ -616,7 +603,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RoomMember' + $ref: 'types.json#/components/schemas/ErrorResponse' 404: description: | @@ -625,7 +612,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ErrorResponse' patch: summary: Update permission of a room member @@ -634,7 +621,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-UpdateMember' + $ref: 'types.json#/components/schemas/Signed_UpdateMember' responses: 204: @@ -647,7 +634,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ServerMetadata' 404: description: | @@ -656,7 +643,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ServerMetadata' delete: summary: Remove a room member. @@ -665,7 +652,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Signed-RemoveMember' + $ref: 'types.json#/components/schemas/Signed_RemoveMember' responses: 204: @@ -677,7 +664,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' + $ref: 'types.json#/components/schemas/ServerMetadata' 404: description: | @@ -686,540 +673,4 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiError' - - -# Ideally we should generate these from src, but we need to -# WAIT: https://github.com/juhaku/utoipa/pull/1034 -components: - schemas: - WSClientToServer: - anyOf: - - $ref: '#/components/schemas/Signed-Auth' - - WSServerToClient: - anyOf: - - type: object - properties: - chat: - $ref: '#/components/schemas/WithMsgId-Signed-Chat' - - - type: object - properties: - lagged: - type: object - const: {} - - ApiError: - type: object - properties: - error: - type: object - properties: - code: - type: string - description: A machine-readable error code string. - example: invalid_signature - message: - type: string - description: A human-readable error message. - example: signature verification failed - - ApiErrorWithRegisterChallenge: - allOf: - - $ref: '#/components/schemas/ApiError' - - type: object - properties: - register_challenge: - type: object - properties: - pow: - type: object - properties: - nonce: - type: integer - format: uint32 - difficulty: - type: integer - format: uint32 - - RoomList: - type: object - required: - - rooms - properties: - rooms: - type: array - items: - $ref: '#/components/schemas/RoomMetadataForList' - next_token: - type: string - description: An opaque token to fetch the next page. - - RoomMetadataForList: - type: object - required: ['rid', 'title', 'attrs'] - properties: - rid: - type: string - title: - type: string - attrs: - description: Room attributes bitset, see `RoomAttrs`. - type: integer - format: int32 - last_msg: - $ref: '#/components/schemas/WithMsgId-Signed-Chat' - last_seen_cid: - description: The `cid` of the last chat being marked as seen. - type: string - unseen_cnt: - description: | - The number of unseen messages. Only available for - GET `/room?filter=unseen`. - type: integer - format: uint32 - member_permission: - type: integer - format: int32 - peer_user: - type: string - description: | - For peer chat room, this gives the identity of the peer user. - - RoomMetadata: - type: object - required: ['rid', 'title', 'attrs'] - properties: - rid: - type: string - title: - type: string - attrs: - type: integer - format: int32 - - RoomMsgs: - type: object - required: - - msgs - properties: - msgs: - description: Room messages in reversed server-received time order. - type: array - items: - $ref: '#/components/schemas/WithMsgId-Signed-Chat' - skip_token: - description: The token for fetching the next page. - type: string - - RoomMemberList: - type: object - required: - - members - properties: - members: - description: Room members in server-specified order. - type: array - items: - $ref: '#/components/schemas/RoomMember' - skip_token: - description: The token for fetching the next page. - type: string - - RoomMember: - type: object - required: - - id_key - - permission - properties: - id_key: - type: string - permission: - type: integer - format: int32 - last_seen_cid: - type: string - - RichText: - type: array - items: - anyOf: - - type: string - description: Unstyled text piece. - - type: array - items: false - prefixItems: - - type: string - description: The text piece to apply styles on. - - type: object - properties: - b: - type: boolean - description: Bold. - m: - type: boolean - description: Monospace. - i: - type: boolean - description: Italic. - s: - type: boolean - description: Strikethrough. - u: - type: boolean - description: Underline. - hashtag: - type: boolean - description: Hashtag. - link: - type: string - description: Link target. - - - Signed-Auth: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'auth' - - Signed-RoomAdmin: - oneOf: - - $ref: '#/components/schemas/Signed-AddMember' - - $ref: '#/components/schemas/Signed-RemoveMember' - - Signed-AddMember: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'add_member' - room: - type: string - permission: - type: integer - format: int32 - user: - type: string - - Signed-UpdateMember: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'update_member' - room: - type: string - permission: - type: integer - format: int32 - user: - type: string - - Signed-RemoveMember: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'remove_member' - room: - type: string - user: - type: string - - Signed-Chat: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'chat' - room: - type: string - rich_text: - $ref: '$/components/schemas/RichText' - - WithMsgId-Signed-Chat: - allOf: - - $ref: '#/components/schemas/Signed-Chat' - - type: object - properties: - cid: - type: string - description: An opaque server-specific identifier. - - Signed-CreateRoom: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - oneOf: - - type: object - properties: - typ: - type: string - const: 'create_room' - title: - type: string - - type: object - properties: - typ: - type: string - const: 'create_peer_chat' - peer: - type: string - - Signed-DeleteRoom: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'delete_room' - room: - type: integer - format: in64 - - Signed-UserRegister: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'user_register' - server_url: - type: string - description: | - The server URL to register on. Must matches chat server's base_url. - It's path segment must be normalized, eg. always contains a `/` path for top-level. - id_url: - type: string - description: | - The identity server URL. Must be in form `https:///`. - It's path segment must be normalized, eg. always contains a `/` path for top-level. - id_key: - type: string - description: Hex encoded user primary key `id_key`. - challenge: - type: object - properties: - pow: - type: object - properties: - nonce: - type: integer - format: uint32 - description: The challenge nonce retrieved from a recent GET response of `/user/me`. - - UserIdentityDescription: - type: object - properties: - id_key: - type: string - - act_keys: - type: array - items: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'user_act_key' - act_key: - type: string - expire_time: - type: integer - format: uint64 - comment: - type: string - - profile: - type: object - properties: - sig: - type: string - signee: - type: object - properties: - nonce: - type: integer - format: uint32 - timestamp: - type: integer - format: uint64 - id_key: - type: string - act_key: - type: string - payload: - type: object - properties: - typ: - type: string - const: 'user_profile' - preferred_chat_server_urls: - type: array - items: - type: string - format: url - id_urls: - type: array - items: - type: string - format: url + $ref: 'types.json#/components/schemas/ServerMetadata'