mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
Maintain room member's last seen item and fix docs
This commit is contained in:
parent
e98c9f8b3c
commit
e74da2812b
6 changed files with 100 additions and 14 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -77,6 +77,8 @@
|
|||
<div>
|
||||
<label for="chat">chat:</label>
|
||||
<input type="text" id="chat" placeholder="message" />
|
||||
|
||||
<button id="mark-seen">mark history seen</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -10,6 +10,7 @@ let curRoom = null;
|
|||
let ws = null;
|
||||
let keypair = null;
|
||||
let defaultConfig = {};
|
||||
let lastCid = null;
|
||||
|
||||
function bufToHex(buf) {
|
||||
return [...new Uint8Array(buf)]
|
||||
|
@ -192,6 +193,7 @@ async function enterRoom(rid) {
|
|||
const { items } = json
|
||||
items.reverse();
|
||||
for (const chat of items) {
|
||||
lastCid = chat.cid;
|
||||
await showChatMsg(chat);
|
||||
}
|
||||
log('---history---');
|
||||
|
@ -267,10 +269,13 @@ async function loadRoomList(autoJoin) {
|
|||
const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader())
|
||||
const json = await resp.json()
|
||||
if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`);
|
||||
for (const { rid, title, attrs } of json.rooms) {
|
||||
for (const { rid, title, attrs, last_chat, last_seen_cid } of json.rooms) {
|
||||
const el = document.createElement('option');
|
||||
el.value = rid;
|
||||
el.innerText = `${title} (rid=${rid}, attrs=${attrs})`;
|
||||
if (last_chat !== undefined && last_chat.cid !== last_seen_cid) {
|
||||
el.innerText += ' (unread)';
|
||||
}
|
||||
targetEl.appendChild(el);
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -391,6 +396,19 @@ async function postChat(text) {
|
|||
}
|
||||
}
|
||||
|
||||
async function markSeen() {
|
||||
try {
|
||||
const resp = await fetch(`${serverUrl}/room/${curRoom}/item/${lastCid}/seen`, {
|
||||
method: 'POST',
|
||||
headers: (await genAuthHeader()).headers,
|
||||
})
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}: ${(await resp.json()).error.message}`);
|
||||
log('seen')
|
||||
} catch (err) {
|
||||
log(`failed to mark seen: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = async (_) => {
|
||||
try {
|
||||
const resp = await fetch('./default.json');
|
||||
|
@ -427,6 +445,7 @@ function onButtonClick(selector, handler) {
|
|||
onButtonClick('#leave-room', leaveRoom);
|
||||
onButtonClick('#regen-key', generateKeypair);
|
||||
onButtonClick('#refresh-rooms', async () => await loadRoomList(true));
|
||||
onButtonClick('#mark-seen', markSeen);
|
||||
|
||||
serverUrlInput.onchange = async (e) => {
|
||||
await connectServer(e.target.value);
|
||||
|
|
Loading…
Add table
Reference in a new issue