Include cid in item responses and update docs

This commit is contained in:
oxalica 2024-09-06 02:26:14 -04:00
parent a7f260027d
commit 51e2c8418b
3 changed files with 53 additions and 49 deletions

View file

@ -164,7 +164,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: WithSig<ChatPayload> $ref: WithItemId<WithSig<ChatPayload>>
example: example:
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09 sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
signee: signee:
@ -200,7 +200,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: WithSig<ChatPayload> $ref: WithSig<AdminPayload>
example: example:
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09 sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
signee: signee:
@ -260,7 +260,7 @@ components:
attrs: attrs:
type: int64 type: int64
last_chat: last_chat:
$ref: 'WithSig<ChatPayload>' $ref: 'WithItemId<WithSig<ChatPayload>>'
RoomMetadata: RoomMetadata:
type: object type: object
@ -280,6 +280,6 @@ components:
items: items:
type: array type: array
items: items:
$ref: 'WithSig<ChatPayload>' $ref: 'WithItemId<WithSig<ChatPayload>>'
skip_token: skip_token:
type: string type: string

View file

@ -13,7 +13,7 @@ use axum::{Json, Router};
use axum_extra::extract::WithRejection as R; use axum_extra::extract::WithRejection as R;
use blah::types::{ use blah::types::{
ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp, RoomAdminPayload, ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp, RoomAdminPayload,
RoomAttrs, ServerPermission, Signee, UserKey, WithSig, RoomAttrs, ServerPermission, Signee, UserKey, WithItemId, WithSig,
}; };
use config::Config; use config::Config;
use database::Database; use database::Database;
@ -205,7 +205,7 @@ struct ListRoomParams {
filter: ListRoomFilter, filter: ListRoomFilter,
// Workaround: serde(flatten) breaks deserialization // Workaround: serde(flatten) breaks deserialization
// See: https://github.com/nox/serde_urlencoded/issues/33 // See: https://github.com/nox/serde_urlencoded/issues/33
skip_token: Option<u64>, skip_token: Option<Id>,
top: Option<NonZeroUsize>, top: Option<NonZeroUsize>,
} }
@ -226,10 +226,9 @@ async fn room_list(
top: params.top, top: params.top,
}; };
let page_len = pagination.effective_page_len(&st); let page_len = pagination.effective_page_len(&st);
let start_rid = pagination.skip_token.unwrap_or(0); let start_rid = pagination.skip_token.unwrap_or(Id(0));
let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> { let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> {
let mut last_rid = None;
let rooms = st let rooms = st
.db .db
.get() .get()
@ -237,21 +236,23 @@ async fn room_list(
.query_map(params, |row| { .query_map(params, |row| {
// TODO: Extract this into a function. // TODO: Extract this into a function.
let rid = row.get("rid")?; let rid = row.get("rid")?;
last_rid = Some(rid);
let title = row.get("title")?; let title = row.get("title")?;
let attrs = row.get("attrs")?; let attrs = row.get("attrs")?;
let last_chat = row let last_chat = row
.get::<_, Option<UserKey>>("userkey")? .get::<_, Option<Id>>("cid")?
.map(|user| { .map(|cid| {
Ok::<_, rusqlite::Error>(ChatItem { Ok::<_, rusqlite::Error>(WithItemId {
sig: row.get("sig")?, cid,
signee: Signee { item: ChatItem {
nonce: row.get("nonce")?, sig: row.get("sig")?,
timestamp: row.get("timestamp")?, signee: Signee {
user, nonce: row.get("nonce")?,
payload: ChatPayload { timestamp: row.get("timestamp")?,
rich_text: row.get("rich_text")?, user: row.get("userkey")?,
room: rid, payload: ChatPayload {
rich_text: row.get("rich_text")?,
room: rid,
},
}, },
}, },
}) })
@ -265,7 +266,8 @@ async fn room_list(
}) })
})? })?
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let skip_token = (rooms.len() == page_len).then_some(()).and(last_rid); let skip_token =
(rooms.len() == page_len).then(|| rooms.last().expect("page must not be empty").rid);
Ok(RoomList { rooms, skip_token }) Ok(RoomList { rooms, skip_token })
}; };
@ -273,7 +275,7 @@ async fn room_list(
ListRoomFilter::Public => query( ListRoomFilter::Public => query(
r" r"
SELECT `rid`, `title`, `attrs`, SELECT `rid`, `title`, `attrs`,
`last_author`.`userkey` AS `userkey`, `timestamp`, `nonce`, `sig`, `rich_text` `cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
FROM `room` FROM `room`
LEFT JOIN `room_item` USING (`rid`) LEFT JOIN `room_item` USING (`rid`)
LEFT JOIN `user` AS `last_author` USING (`uid`) LEFT JOIN `user` AS `last_author` USING (`uid`)
@ -295,7 +297,7 @@ async fn room_list(
r" r"
SELECT SELECT
`rid`, `title`, `attrs`, `rid`, `title`, `attrs`,
`last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text` `cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
FROM `user` FROM `user`
JOIN `room_member` USING (`uid`) JOIN `room_member` USING (`uid`)
JOIN `room` USING (`rid`) JOIN `room` USING (`rid`)
@ -408,7 +410,7 @@ async fn room_create(
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Pagination { struct Pagination {
/// A opaque token from previous response to fetch the next page. /// A opaque token from previous response to fetch the next page.
skip_token: Option<u64>, skip_token: Option<Id>,
/// Maximum page size. /// Maximum page size.
top: Option<NonZeroUsize>, top: Option<NonZeroUsize>,
} }
@ -424,9 +426,9 @@ impl Pagination {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct RoomItems { struct RoomItems {
items: Vec<ChatItem>, items: Vec<WithItemId<ChatItem>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
skip_token: Option<String>, skip_token: Option<Id>,
} }
async fn room_get_item( async fn room_get_item(
@ -440,11 +442,7 @@ async fn room_get_item(
get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |_row| Ok(()))?; get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |_row| Ok(()))?;
query_room_items(&st, &conn, rid, pagination)? query_room_items(&st, &conn, rid, pagination)?
}; };
let items = items.into_iter().map(|(_, item)| item).collect(); Ok(Json(RoomItems { items, skip_token }))
Ok(Json(RoomItems {
items,
skip_token: skip_token.map(|x| x.to_string()),
}))
} }
async fn room_get_metadata( async fn room_get_metadata(
@ -482,7 +480,7 @@ async fn room_get_feed(
let items = items let items = items
.into_iter() .into_iter()
.map(|(cid, item)| { .map(|WithItemId { cid, item }| {
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(item.signee.timestamp); let time = SystemTime::UNIX_EPOCH + Duration::from_secs(item.signee.timestamp);
let author = FeedAuthor { let author = FeedAuthor {
name: item.signee.user.to_string(), name: item.signee.user.to_string(),
@ -578,7 +576,7 @@ pub struct RoomMetadata {
/// Optional extra information. Only included by the global room list response. /// Optional extra information. Only included by the global room list response.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub last_chat: Option<ChatItem>, pub last_chat: Option<WithItemId<ChatItem>>,
} }
fn get_room_if_readable<T>( fn get_room_if_readable<T>(
@ -610,8 +608,6 @@ fn get_room_if_readable<T>(
.ok_or_else(|| error_response!(StatusCode::NOT_FOUND, "not_found", "room not found")) .ok_or_else(|| error_response!(StatusCode::NOT_FOUND, "not_found", "room not found"))
} }
type ChatItemWithId = (u64, ChatItem);
/// Get room items with pagination parameters, /// Get room items with pagination parameters,
/// return a page of items and the next skip_token if this is not the last page. /// return a page of items and the next skip_token if this is not the last page.
fn query_room_items( fn query_room_items(
@ -619,7 +615,7 @@ fn query_room_items(
conn: &Connection, conn: &Connection,
rid: Id, rid: Id,
pagination: Pagination, pagination: Pagination,
) -> Result<(Vec<ChatItemWithId>, Option<u64>), ApiError> { ) -> Result<(Vec<WithItemId<ChatItem>>, Option<Id>), ApiError> {
let page_len = pagination.effective_page_len(st); let page_len = pagination.effective_page_len(st);
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
r" r"
@ -640,25 +636,26 @@ fn query_room_items(
":limit": page_len, ":limit": page_len,
}, },
|row| { |row| {
let cid = row.get::<_, u64>("cid")?; Ok(WithItemId {
let item = ChatItem { cid: row.get("cid")?,
sig: row.get("sig")?, item: ChatItem {
signee: Signee { sig: row.get("sig")?,
nonce: row.get("nonce")?, signee: Signee {
timestamp: row.get("timestamp")?, nonce: row.get("nonce")?,
user: row.get("userkey")?, timestamp: row.get("timestamp")?,
payload: ChatPayload { user: row.get("userkey")?,
room: rid, payload: ChatPayload {
rich_text: row.get("rich_text")?, room: rid,
rich_text: row.get("rich_text")?,
},
}, },
}, },
}; })
Ok((cid, item))
}, },
)? )?
.collect::<rusqlite::Result<Vec<_>>>()?; .collect::<rusqlite::Result<Vec<_>>>()?;
let skip_token = let skip_token =
(items.len() == page_len).then(|| items.last().expect("page must not be empty").0); (items.len() == page_len).then(|| items.last().expect("page must not be empty").cid);
Ok((items, skip_token)) Ok((items, skip_token))
} }

View file

@ -24,6 +24,13 @@ impl fmt::Display for Id {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct WithItemId<T> {
pub cid: Id,
#[serde(flatten)]
pub item: T,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);