mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
feat(webapi): expose server metadata
This commit is contained in:
parent
8551540798
commit
fa14844d0d
4 changed files with 88 additions and 4 deletions
|
@ -1,6 +1,7 @@
|
||||||
//! Data types and constants for Chat Server interaction.
|
//! Data types and constants for Chat Server interaction.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId};
|
use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId};
|
||||||
use crate::PubKey;
|
use crate::PubKey;
|
||||||
|
@ -8,6 +9,34 @@ use crate::PubKey;
|
||||||
pub const X_BLAH_NONCE: &str = "x-blah-nonce";
|
pub const X_BLAH_NONCE: &str = "x-blah-nonce";
|
||||||
pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty";
|
pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty";
|
||||||
|
|
||||||
|
/// Metadata about the version and capabilities of a Chat Server.
|
||||||
|
///
|
||||||
|
/// It should be relatively stable and do not change very often.
|
||||||
|
/// 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)]
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
pub src_url: Option<Url>,
|
||||||
|
|
||||||
|
/// The server capabilities set.
|
||||||
|
pub capabilities: ServerCapabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ServerCapabilities {
|
||||||
|
/// Whether registration is open to public.
|
||||||
|
pub allow_public_register: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RoomMetadata {
|
pub struct RoomMetadata {
|
||||||
/// Room id.
|
/// Room id.
|
||||||
|
|
|
@ -3,10 +3,11 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use axum::body::Bytes;
|
||||||
use axum::extract::{ws, OriginalUri};
|
use axum::extract::{ws, OriginalUri};
|
||||||
use axum::extract::{Path, Query, State, WebSocketUpgrade};
|
use axum::extract::{Path, Query, State, WebSocketUpgrade};
|
||||||
use axum::http::{header, HeaderMap, HeaderName, StatusCode};
|
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||||
use axum::response::Response;
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use axum_extra::extract::WithRejection as R;
|
use axum_extra::extract::WithRejection as R;
|
||||||
|
@ -15,7 +16,9 @@ use blah_types::msg::{
|
||||||
MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
||||||
SignedChatMsgWithId, UserRegisterPayload,
|
SignedChatMsgWithId, UserRegisterPayload,
|
||||||
};
|
};
|
||||||
use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE};
|
use blah_types::server::{
|
||||||
|
RoomMetadata, ServerCapabilities, ServerMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||||
|
};
|
||||||
use blah_types::{get_timestamp, Id, Signed, UserKey};
|
use blah_types::{get_timestamp, Id, Signed, UserKey};
|
||||||
use database::{Transaction, TransactionOps};
|
use database::{Transaction, TransactionOps};
|
||||||
use feed::FeedData;
|
use feed::FeedData;
|
||||||
|
@ -41,6 +44,10 @@ mod utils;
|
||||||
pub use database::{Config as DatabaseConfig, Database};
|
pub use database::{Config as DatabaseConfig, Database};
|
||||||
pub use middleware::ApiError;
|
pub use middleware::ApiError;
|
||||||
|
|
||||||
|
/// The server name and version, for metadata report and user agent.
|
||||||
|
pub(crate) const SERVER_AND_VERSION: &str = concat!("blahd/", env!("CARGO_PKG_VERSION"));
|
||||||
|
const SERVER_SRC_URL: Option<&str> = option_env!("CFG_SRC_URL");
|
||||||
|
|
||||||
#[serde_inline_default]
|
#[serde_inline_default]
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
@ -85,11 +92,27 @@ pub struct AppState {
|
||||||
event: event::State,
|
event: event::State,
|
||||||
register: register::State,
|
register: register::State,
|
||||||
|
|
||||||
|
server_metadata: Bytes,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(db: Database, config: ServerConfig) -> Self {
|
pub fn new(db: Database, config: ServerConfig) -> Self {
|
||||||
|
let meta = ServerMetadata {
|
||||||
|
server: SERVER_AND_VERSION.into(),
|
||||||
|
// TODO: Validate this at compile time?
|
||||||
|
src_url: SERVER_SRC_URL.map(|url| {
|
||||||
|
url.parse()
|
||||||
|
.expect("BLAHD_SRC_URL from compile time should be valid")
|
||||||
|
}),
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
allow_public_register: config.register.enable_public,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let server_metadata = serde_json::to_string(&meta)
|
||||||
|
.expect("serialization cannot fail")
|
||||||
|
.into();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
db,
|
db,
|
||||||
used_nonces: Mutex::new(ExpiringSet::new(Duration::from_secs(
|
used_nonces: Mutex::new(ExpiringSet::new(Duration::from_secs(
|
||||||
|
@ -98,6 +121,7 @@ impl AppState {
|
||||||
event: event::State::default(),
|
event: event::State::default(),
|
||||||
register: register::State::new(config.register.clone()),
|
register: register::State::new(config.register.clone()),
|
||||||
|
|
||||||
|
server_metadata,
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +145,7 @@ type ArcState = State<Arc<AppState>>;
|
||||||
|
|
||||||
pub fn router(st: Arc<AppState>) -> Router {
|
pub fn router(st: Arc<AppState>) -> Router {
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
|
.route("/server", get(handle_server_metadata))
|
||||||
.route("/ws", get(handle_ws))
|
.route("/ws", get(handle_ws))
|
||||||
.route("/user/me", get(user_get).post(user_register))
|
.route("/user/me", get(user_get).post(user_register))
|
||||||
.route("/room", get(room_list))
|
.route("/room", get(room_list))
|
||||||
|
@ -150,6 +175,18 @@ pub fn router(st: Arc<AppState>) -> Router {
|
||||||
|
|
||||||
type RE<T> = R<T, ApiError>;
|
type RE<T> = R<T, ApiError>;
|
||||||
|
|
||||||
|
async fn handle_server_metadata(State(st): ArcState) -> Response {
|
||||||
|
// TODO: If-None-Match.
|
||||||
|
(
|
||||||
|
[(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
const { HeaderValue::from_static("application/json") },
|
||||||
|
)],
|
||||||
|
st.server_metadata.clone(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response {
|
async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response {
|
||||||
ws.on_upgrade(move |mut socket| async move {
|
ws.on_upgrade(move |mut socket| async move {
|
||||||
match event::handle_ws(st, &mut socket).await {
|
match event::handle_ws(st, &mut socket).await {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use blah_types::msg::{
|
||||||
MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
||||||
SignedChatMsg, SignedChatMsgWithId, UserRegisterPayload, WithMsgId,
|
SignedChatMsg, SignedChatMsgWithId, UserRegisterPayload, WithMsgId,
|
||||||
};
|
};
|
||||||
use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE};
|
use blah_types::server::{RoomMetadata, ServerMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE};
|
||||||
use blah_types::{Id, SignExt, Signed, UserKey};
|
use blah_types::{Id, SignExt, Signed, UserKey};
|
||||||
use blahd::{AppState, Database, RoomList, RoomMsgs};
|
use blahd::{AppState, Database, RoomList, RoomMsgs};
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
@ -245,6 +245,10 @@ impl Server {
|
||||||
msg.sign_msg(&user.pubkeys.id_key, &user.act_priv).unwrap()
|
msg.sign_msg(&user.pubkeys.id_key, &user.act_priv).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_metadata(&self) -> Result<ServerMetadata> {
|
||||||
|
self.get::<ServerMetadata>("/server", None).await
|
||||||
|
}
|
||||||
|
|
||||||
fn create_room(
|
fn create_room(
|
||||||
&self,
|
&self,
|
||||||
user: &User,
|
user: &User,
|
||||||
|
@ -450,6 +454,15 @@ async fn smoke(server: Server) {
|
||||||
assert_eq!(got, exp);
|
assert_eq!(got, exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
#[expect(clippy::print_stdout, reason = "allowed in tests for debugging")]
|
||||||
|
async fn server_metadata(server: Server) {
|
||||||
|
let meta = server.get_metadata().await.unwrap();
|
||||||
|
println!("{meta:#?}");
|
||||||
|
assert!(meta.server.starts_with("blahd/"));
|
||||||
|
}
|
||||||
|
|
||||||
fn auth(user: &User) -> String {
|
fn auth(user: &User) -> String {
|
||||||
let msg = AuthPayload {}
|
let msg = AuthPayload {}
|
||||||
.sign_msg(&user.pubkeys.id_key, &user.act_priv)
|
.sign_msg(&user.pubkeys.id_key, &user.act_priv)
|
||||||
|
@ -1262,6 +1275,10 @@ unsafe_allow_id_url_single_label = {allow_single_label}
|
||||||
};
|
};
|
||||||
let server = server_with(Database::open(&db_config).unwrap(), &config);
|
let server = server_with(Database::open(&db_config).unwrap(), &config);
|
||||||
|
|
||||||
|
// Report in capabilities.
|
||||||
|
let meta = server.get_metadata().await.unwrap();
|
||||||
|
assert_eq!(meta.capabilities.allow_public_register, enabled);
|
||||||
|
|
||||||
// Returns challenge headers only if registration is enabled.
|
// Returns challenge headers only if registration is enabled.
|
||||||
let hdrs = server.get_me(Some(&CAROL)).await.unwrap_err();
|
let hdrs = server.get_me(Some(&CAROL)).await.unwrap_err();
|
||||||
if enabled {
|
if enabled {
|
||||||
|
|
|
@ -85,6 +85,7 @@ rec {
|
||||||
blahd = (pkgs.callPackage mkPkg { }).overrideAttrs {
|
blahd = (pkgs.callPackage mkPkg { }).overrideAttrs {
|
||||||
# Only set this for the main derivation, not for deps.
|
# Only set this for the main derivation, not for deps.
|
||||||
CFG_RELEASE = "git-${rev}";
|
CFG_RELEASE = "git-${rev}";
|
||||||
|
CFG_SRC_URL = "https://github.com/Blah-IM/blahrs/archive/${rev}.tar.gz";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue