Maintain room member's last seen item and fix docs

This commit is contained in:
oxalica 2024-09-06 02:52:31 -04:00
parent e98c9f8b3c
commit e74da2812b
6 changed files with 100 additions and 14 deletions

View file

@ -28,12 +28,12 @@ paths:
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:
top:
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:
skipToken:
in: query
description:
The page token returned from a previous list response to fetch the
@ -41,7 +41,7 @@ paths:
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.
description: Proof of membership for private rooms. Required if `filter=joined`.
required: false
schema:
$ret: WithSig<AuthPayload>
@ -49,7 +49,7 @@ paths:
200:
content:
application/json:
$ref: '#/components/schema/ListRoom'
$ref: '#/components/schema/RoomList'
401:
description: Missing or invalid Authorization header.
content:
@ -193,6 +193,29 @@ paths:
application/json:
$ref: '#/components/schemas/ApiError'
/room/{rid}/item/{cid}/seen:
post:
summary: Mark item {cid} in room {rid} seen by the current user.
description:
Server will enforce that last seen item does not go backward. Marking
an older item seen or sending the same request multiple times will be a
no-op.
headers:
Authorization:
description: Proof of membership for private rooms.
schema:
$ret: WithSig<AuthPayload>
responses:
204:
description: Operation completed.
404:
description: |
Room does not exist or the user is not in the room.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/room/{rid}/admin:
post:
summary: Room management
@ -261,6 +284,8 @@ components:
type: int64
last_chat:
$ref: 'WithItemId<WithSig<ChatPayload>>'
last_seen_cid:
type: string
RoomMetadata:
type: object

View file

@ -17,10 +17,13 @@ CREATE TABLE IF NOT EXISTS `room_member` (
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
`permission` INTEGER NOT NULL,
`last_seen_cid` INTEGER NOT NULL REFERENCES `room_item` (`cid`) ON DELETE NO ACTION
DEFAULT 0,
PRIMARY KEY (`rid`, `uid`)
) STRICT;
CREATE INDEX IF NOT EXISTS `member_room` ON `room_member` (`uid` ASC, `rid` ASC);
CREATE INDEX IF NOT EXISTS `ix_member_room` ON `room_member`
(`uid` ASC, `rid` ASC, `permission`, `last_seen_cid`);
CREATE TABLE IF NOT EXISTS `room_item` (
`cid` INTEGER NOT NULL PRIMARY KEY,

View file

@ -10,7 +10,7 @@ static INIT_SQL: &str = include_str!("../schema.sql");
// Simple and stupid version check for now.
// `echo -n 'blahd-database-0' | sha256sum | head -c5` || version
const APPLICATION_ID: i32 = 0xd9e_8402;
const APPLICATION_ID: i32 = 0xd9e_8403;
#[derive(Debug)]
pub struct Database {

View file

@ -19,7 +19,7 @@ use config::Config;
use database::Database;
use ed25519_dalek::SIGNATURE_LENGTH;
use id::IdExt;
use middleware::{ApiError, MaybeAuth, ResultExt as _, SignedJson};
use middleware::{ApiError, Auth, MaybeAuth, ResultExt as _, SignedJson};
use parking_lot::Mutex;
use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql};
use serde::{Deserialize, Serialize};
@ -147,7 +147,8 @@ async fn main_async(st: AppState) -> Result<()> {
.route("/room/:rid", get(room_get_metadata))
// NB. Sync with `feed_url` and `next_url` generation.
.route("/room/:rid/feed.json", get(room_get_feed))
.route("/room/:rid/item", get(room_get_item).post(room_post_item))
.route("/room/:rid/item", get(room_item_list).post(room_item_post))
.route("/room/:rid/item/:cid/seen", post(room_item_mark_seen))
.route("/room/:rid/admin", post(room_admin))
.with_state(st.clone())
.layer(tower_http::limit::RequestBodyLimitLayer::new(
@ -258,11 +259,14 @@ async fn room_list(
})
})
.transpose()?;
let last_seen_cid =
Some(row.get::<_, Id>("last_seen_cid")?).filter(|cid| cid.0 != 0);
Ok(RoomMetadata {
rid,
title,
attrs,
last_chat,
last_seen_cid,
})
})?
.collect::<Result<Vec<_>, _>>()?;
@ -274,7 +278,7 @@ async fn room_list(
match params.filter {
ListRoomFilter::Public => query(
r"
SELECT `rid`, `title`, `attrs`,
SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`,
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
FROM `room`
LEFT JOIN `room_item` USING (`rid`)
@ -296,7 +300,7 @@ async fn room_list(
query(
r"
SELECT
`rid`, `title`, `attrs`,
`rid`, `title`, `attrs`, `last_seen_cid`,
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
FROM `user`
JOIN `room_member` USING (`uid`)
@ -431,7 +435,7 @@ struct RoomItems {
skip_token: Option<Id>,
}
async fn room_get_item(
async fn room_item_list(
st: ArcState,
R(Path(rid), _): RE<Path<Id>>,
R(Query(pagination), _): RE<Query<Pagination>>,
@ -463,6 +467,7 @@ async fn room_get_metadata(
title,
attrs,
last_chat: None,
last_seen_cid: None,
}))
}
@ -574,9 +579,11 @@ pub struct RoomMetadata {
pub title: String,
pub attrs: RoomAttrs,
/// Optional extra information. Only included by the global room list response.
/// Optional extra information. Only included by the room list response.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_chat: Option<WithItemId<ChatItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_seen_cid: Option<Id>,
}
fn get_room_if_readable<T>(
@ -660,7 +667,7 @@ fn query_room_items(
Ok((items, skip_token))
}
async fn room_post_item(
async fn room_item_post(
st: ArcState,
R(Path(rid), _): RE<Path<Id>>,
SignedJson(chat): SignedJson<ChatPayload>,
@ -898,3 +905,33 @@ async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiErro
txn.commit()?;
Ok(())
}
async fn room_item_mark_seen(
st: ArcState,
R(Path((rid, cid)), _): RE<Path<(Id, u64)>>,
Auth(user): Auth,
) -> Result<StatusCode, ApiError> {
let changed = st.db.get().execute(
r"
UPDATE `room_member`
SET `last_seen_cid` = MAX(`last_seen_cid`, :cid)
WHERE
`rid` = :rid AND
`uid` = (SELECT `uid` FROM `user` WHERE `userkey` = :userkey)
",
named_params! {
":cid": cid,
":rid": rid,
":userkey": user,
},
)?;
if changed != 1 {
return Err(error_response!(
StatusCode::NOT_FOUND,
"not_found",
"room does not exists or user is not a room member",
));
}
Ok(StatusCode::NO_CONTENT)
}