save: add utoipa for OAPI generation

This commit is contained in:
oxalica 2024-10-16 06:43:54 -04:00
parent 71c5f038fa
commit acaf0f955a
11 changed files with 212 additions and 610 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
*.profraw
result
result-*
/docs/types.json
# Test configurations.
config.toml

25
Cargo.lock generated
View file

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

View file

@ -7,6 +7,9 @@ members = [
]
default-members = ["blahd"]
[workspace.dependencies]
utoipa = "5"
[workspace.lints.clippy]
allow_attributes_without_reason = "warn"
dbg_macro = "warn"

View file

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

View file

@ -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}");
}

View file

@ -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<T> {
#[serde(with = "hex::serde")]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub sig: [u8; SIGNATURE_LENGTH],
pub signee: Signee<T>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Signee<T> {
pub nonce: u32,

View file

@ -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<Url>,
@ -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);

View file

@ -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::<msg::AuthPayload>,
crypto::Signed::<msg::ChatPayload>,
crypto::Signed::<msg::CreateRoomPayload>,
crypto::Signed::<msg::DeleteRoomPayload>,
crypto::Signed::<msg::RoomAdminPayload>,
crypto::Signed::<msg::UserRegisterPayload>,
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}");
}

View file

@ -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<T> {
pub cid: Id,
#[serde(flatten)]
@ -53,17 +55,20 @@ impl<T> WithMsgId<T> {
/// 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<UserRegisterChallengeResponse>,
}
/// 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: <https://github.com/Blah-IM/Weblah/blob/a3fa0f265af54c846f8d65f42aa4409c8dba9dd9/src/lib/richText.ts>
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(value_type = Vec<RichTextPieceRaw>))]
#[serde(transparent)]
pub struct RichText(pub Vec<RichTextPiece>);
@ -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<String>,
#[serde(default, rename = "s", skip_serializing_if = "is_default")]
pub strike: bool,
@ -271,6 +281,7 @@ pub type SignedChatMsg = Signed<ChatPayload>;
pub type SignedChatMsgWithId = WithMsgId<SignedChatMsg>;
#[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<RoomMember>")]
pub struct RoomMemberList(pub Vec<RoomMember>);
@ -327,6 +342,7 @@ impl TryFrom<Vec<RoomMember>> 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;

View file

@ -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<S = String> {
/// The error object.
pub error: ErrorObject<S>,
@ -18,21 +19,27 @@ pub struct ErrorResponse<S = String> {
/// 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<S = String> {
/// The error object.
pub error: ErrorObject<S>,
/// 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<UserRegisterChallenge>,
}
/// The error object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ErrorObject<S = String> {
/// 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<S: fmt::Display + fmt::Debug> std::error::Error for ErrorObject<S> {}
/// 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 `<server-name>/<server-version>` 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<Url>,
/// 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<RoomMetadata>,
/// 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<Id>,
}
/// 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<String>,
/// 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<SignedChatMsgWithId>,
/// 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<Id>,
/// 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<u32>,
/// 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<MemberPermission>,
/// 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<PubKey>,
}
/// 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<SignedChatMsgWithId>,
/// 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<Id>,
}
/// 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<RoomMember>,
/// 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<Id>,
}
/// 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<Id>,
}
/// 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 {}

View file

@ -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://<domain>/`.
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'