mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 08:41:09 +00:00
Impl global room listing at /room
This commit is contained in:
parent
a9d5eb9631
commit
2b6fbe8794
4 changed files with 180 additions and 8 deletions
|
@ -16,6 +16,46 @@ paths:
|
||||||
interested in (eg. chat from joined rooms).
|
interested in (eg. chat from joined rooms).
|
||||||
The message has type `Outgoing` in `blahd/src/ws.rs`.
|
The message has type `Outgoing` in `blahd/src/ws.rs`.
|
||||||
|
|
||||||
|
/room:
|
||||||
|
get:
|
||||||
|
summary: List rooms on the server
|
||||||
|
parameters:
|
||||||
|
filter:
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: |
|
||||||
|
Either "public" or "joined".
|
||||||
|
For "public", it returns all public rooms on the server.
|
||||||
|
For "joined", `Authorization` must be provided and it will return
|
||||||
|
rooms user have joined.
|
||||||
|
page_len:
|
||||||
|
in: query
|
||||||
|
description:
|
||||||
|
The maximum number of items returned in each page. This is only an
|
||||||
|
advice and server can clamp it to a smaller value.
|
||||||
|
page_token:
|
||||||
|
in: query
|
||||||
|
description:
|
||||||
|
The page token returned from a previous list response to fetch the
|
||||||
|
next page. NB. Other parameters (eg. `joined` and `page_len`)
|
||||||
|
should be included (as the same value) for each page fetch.
|
||||||
|
headers:
|
||||||
|
Authorization:
|
||||||
|
description: Proof of membership for private rooms. Required if `joined` is true.
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
$ret: WithSig<AuthPayload>
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
$ref: '#/components/schema/ListRoom'
|
||||||
|
401:
|
||||||
|
description: Missing or invalid Authorization header.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
/room/create:
|
/room/create:
|
||||||
post:
|
post:
|
||||||
summary: Create a new room
|
summary: Create a new room
|
||||||
|
@ -196,9 +236,24 @@ components:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
RoomList:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- rooms
|
||||||
|
properties:
|
||||||
|
rooms:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RoomMetadata'
|
||||||
|
next_token:
|
||||||
|
type: string
|
||||||
|
description: An opaque token to fetch the next page.
|
||||||
|
|
||||||
RoomMetadata:
|
RoomMetadata:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
ruuid:
|
||||||
|
type: string
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
attrs:
|
attrs:
|
||||||
|
|
|
@ -21,6 +21,8 @@ CREATE TABLE IF NOT EXISTS `room_member` (
|
||||||
PRIMARY KEY (`rid`, `uid`)
|
PRIMARY KEY (`rid`, `uid`)
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS `member_room` ON `room_member` (`uid` ASC, `rid` ASC);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `room_item` (
|
CREATE TABLE IF NOT EXISTS `room_item` (
|
||||||
`cid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
`cid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -28,8 +29,8 @@ pub struct ServerConfig {
|
||||||
pub listen: String,
|
pub listen: String,
|
||||||
pub base_url: Url,
|
pub base_url: Url,
|
||||||
|
|
||||||
#[serde_inline_default(1024)]
|
#[serde_inline_default(1024.try_into().unwrap())]
|
||||||
pub max_page_len: usize,
|
pub max_page_len: NonZeroUsize,
|
||||||
#[serde_inline_default(4096)] // 4KiB
|
#[serde_inline_default(4096)] // 4KiB
|
||||||
pub max_request_len: usize,
|
pub max_request_len: usize,
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ use blah::types::{
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||||
use middleware::{ApiError, OptionalAuth, SignedJson};
|
use middleware::{ApiError, OptionalAuth, SignedJson};
|
||||||
use rusqlite::{named_params, params, Connection, OptionalExtension, Row};
|
use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use utils::ExpiringSet;
|
use utils::ExpiringSet;
|
||||||
|
@ -149,6 +149,7 @@ async fn main_async(st: AppState) -> Result<()> {
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/ws", get(handle_ws))
|
.route("/ws", get(handle_ws))
|
||||||
|
.route("/room", get(room_list))
|
||||||
.route("/room/create", post(room_create))
|
.route("/room/create", post(room_create))
|
||||||
.route("/room/:ruuid", get(room_get_metadata))
|
.route("/room/:ruuid", get(room_get_metadata))
|
||||||
// NB. Sync with `feed_url` and `next_url` generation.
|
// NB. Sync with `feed_url` and `next_url` generation.
|
||||||
|
@ -196,6 +197,108 @@ async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RoomList {
|
||||||
|
rooms: Vec<RoomMetadata>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
skip_token: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
struct ListRoomParams {
|
||||||
|
filter: ListRoomFilter,
|
||||||
|
// Workaround: serde(flatten) breaks deserialization
|
||||||
|
// See: https://github.com/nox/serde_urlencoded/issues/33
|
||||||
|
skip_token: Option<u64>,
|
||||||
|
top: Option<NonZeroUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum ListRoomFilter {
|
||||||
|
Public,
|
||||||
|
Joined,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn room_list(
|
||||||
|
st: ArcState,
|
||||||
|
WithRejection(params, _): WithRejection<Query<ListRoomParams>, ApiError>,
|
||||||
|
OptionalAuth(user): OptionalAuth,
|
||||||
|
) -> Result<Json<RoomList>, ApiError> {
|
||||||
|
let pagination = Pagination {
|
||||||
|
skip_token: params.skip_token,
|
||||||
|
top: params.top,
|
||||||
|
};
|
||||||
|
let page_len = pagination.effective_page_len(&st);
|
||||||
|
let start_rid = pagination.skip_token.unwrap_or(0);
|
||||||
|
|
||||||
|
let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> {
|
||||||
|
let mut last_rid = None;
|
||||||
|
let rooms = st
|
||||||
|
.conn
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.prepare(sql)?
|
||||||
|
.query_map(params, |row| {
|
||||||
|
last_rid = Some(row.get::<_, u64>("rid")?);
|
||||||
|
Ok(RoomMetadata {
|
||||||
|
ruuid: row.get("ruuid")?,
|
||||||
|
title: row.get("title")?,
|
||||||
|
attrs: row.get("attrs")?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let skip_token = (rooms.len() == page_len).then_some(()).and(last_rid);
|
||||||
|
Ok(RoomList { rooms, skip_token })
|
||||||
|
};
|
||||||
|
|
||||||
|
match params.filter {
|
||||||
|
ListRoomFilter::Public => query(
|
||||||
|
r"
|
||||||
|
SELECT `rid`, `ruuid`, `title`, `attrs`
|
||||||
|
FROM `room`
|
||||||
|
WHERE `rid` > :start_rid AND
|
||||||
|
(`attrs` & :perm) = :perm
|
||||||
|
ORDER BY `rid` ASC
|
||||||
|
LIMIT :page_len
|
||||||
|
",
|
||||||
|
named_params! {
|
||||||
|
":start_rid": start_rid,
|
||||||
|
":page_len": page_len,
|
||||||
|
":perm": RoomAttrs::PUBLIC_READABLE,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListRoomFilter::Joined => {
|
||||||
|
let Some(user) = user else {
|
||||||
|
return Err(error_response!(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"unauthorized",
|
||||||
|
"missing Authorization header for listing joined rooms",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
query(
|
||||||
|
r"
|
||||||
|
SELECT `rid`, `ruuid`, `title`, `attrs`
|
||||||
|
FROM `user`
|
||||||
|
JOIN `room_member` USING (`uid`)
|
||||||
|
JOIN `room` USING (`rid`)
|
||||||
|
WHERE `userkey` = :userkey AND
|
||||||
|
`rid` > :start_rid
|
||||||
|
ORDER BY `rid` ASC
|
||||||
|
LIMIT :page_len
|
||||||
|
",
|
||||||
|
named_params! {
|
||||||
|
":start_rid": start_rid,
|
||||||
|
":page_len": page_len,
|
||||||
|
":userkey": user,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(Json)
|
||||||
|
}
|
||||||
|
|
||||||
async fn room_create(
|
async fn room_create(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
SignedJson(params): SignedJson<CreateRoomPayload>,
|
SignedJson(params): SignedJson<CreateRoomPayload>,
|
||||||
|
@ -293,6 +396,15 @@ struct Pagination {
|
||||||
top: Option<NonZeroUsize>,
|
top: Option<NonZeroUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Pagination {
|
||||||
|
fn effective_page_len(&self, st: &AppState) -> usize {
|
||||||
|
self.top
|
||||||
|
.unwrap_or(usize::MAX.try_into().unwrap())
|
||||||
|
.min(st.config.server.max_page_len)
|
||||||
|
.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct RoomItems {
|
struct RoomItems {
|
||||||
items: Vec<ChatItem>,
|
items: Vec<ChatItem>,
|
||||||
|
@ -331,7 +443,11 @@ async fn room_get_metadata(
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Json(RoomMetadata { title, attrs }))
|
Ok(Json(RoomMetadata {
|
||||||
|
ruuid,
|
||||||
|
title,
|
||||||
|
attrs,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_get_feed(
|
async fn room_get_feed(
|
||||||
|
@ -436,6 +552,7 @@ struct FeedItemExtra {
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct RoomMetadata {
|
pub struct RoomMetadata {
|
||||||
|
pub ruuid: Uuid,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub attrs: RoomAttrs,
|
pub attrs: RoomAttrs,
|
||||||
}
|
}
|
||||||
|
@ -479,10 +596,7 @@ fn query_room_items(
|
||||||
ruuid: Uuid,
|
ruuid: Uuid,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<(Vec<ChatItemWithId>, Option<u64>), ApiError> {
|
) -> Result<(Vec<ChatItemWithId>, Option<u64>), ApiError> {
|
||||||
let page_len = pagination
|
let page_len = pagination.effective_page_len(st);
|
||||||
.top
|
|
||||||
.map_or(usize::MAX, |n| n.get())
|
|
||||||
.min(st.config.server.max_page_len);
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r"
|
r"
|
||||||
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
||||||
|
|
Loading…
Add table
Reference in a new issue