added server-side user unread_count to rooms
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -619,7 +619,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "frangipane"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "frangipane"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
63
db/init.sql
63
db/init.sql
@@ -10,14 +10,14 @@ CREATE TABLE IF NOT EXISTS user_ (
|
||||
CREATE TABLE IF NOT EXISTS friendship_ (
|
||||
user_first INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
user_second INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_first, user_second)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS friend_request_ (
|
||||
sender INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
receiver INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (sender, receiver),
|
||||
CHECK (sender <> receiver)
|
||||
);
|
||||
@@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS room_ (
|
||||
CREATE TABLE IF NOT EXISTS membership_ (
|
||||
user_id INT REFERENCES user_(id),
|
||||
room INT REFERENCES room_(id),
|
||||
last_read_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, room)
|
||||
);
|
||||
|
||||
@@ -40,7 +41,7 @@ CREATE TABLE IF NOT EXISTS room_invite_ (
|
||||
sender INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
receiver INT NOT NULL REFERENCES user_(id) ON DELETE CASCADE,
|
||||
room INT NOT NULL,
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (sender, receiver),
|
||||
CHECK (sender <> receiver)
|
||||
);
|
||||
@@ -52,7 +53,7 @@ CREATE TABLE IF NOT EXISTS message_ (
|
||||
room INT REFERENCES room_(id) NOT NULL,
|
||||
message_type VARCHAR(32) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE ws_token_ (
|
||||
@@ -60,22 +61,40 @@ CREATE TABLE ws_token_ (
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- ==== INDICES ====
|
||||
CREATE INDEX idx_message_room_sent_at ON message_ (room, sent_at);
|
||||
CREATE UNIQUE INDEX idx_membership_user_room ON membership_ (user_id, room) INCLUDE (last_read_at);
|
||||
|
||||
-- Timestamp creation
|
||||
CREATE OR REPLACE FUNCTION create_notification_timestamp()
|
||||
RETURNS trigger
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.sent_at := CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE TRIGGER insert_message
|
||||
BEFORE INSERT ON message_
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_notification_timestamp();
|
||||
|
||||
CREATE OR REPLACE TRIGGER insert_room_invite
|
||||
BEFORE INSERT ON room_invite_
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_notification_timestamp();
|
||||
-- CREATE OR REPLACE FUNCTION create_notification_timestamp()
|
||||
-- RETURNS trigger
|
||||
-- AS $$
|
||||
-- BEGIN
|
||||
-- NEW.sent_at := CURRENT_TIMESTAMP;
|
||||
-- RETURN NEW;
|
||||
-- END;
|
||||
-- $$ LANGUAGE plpgsql;
|
||||
--
|
||||
-- CREATE OR REPLACE TRIGGER insert_message
|
||||
-- BEFORE INSERT ON message_
|
||||
-- FOR EACH ROW
|
||||
-- EXECUTE FUNCTION create_notification_timestamp();
|
||||
--
|
||||
-- CREATE OR REPLACE TRIGGER insert_room_invite
|
||||
-- BEFORE INSERT ON room_invite_
|
||||
-- FOR EACH ROW
|
||||
-- EXECUTE FUNCTION create_notification_timestamp();
|
||||
--
|
||||
-- CREATE OR REPLACE FUNCTION create_membership_timestamp()
|
||||
-- RETURNS trigger
|
||||
-- AS $$
|
||||
-- BEGIN
|
||||
-- NEW.last_read_at = CURRENT_TIMESTAMP;
|
||||
-- RETURN NEW;
|
||||
-- END;
|
||||
-- $$ LANGUAGE plpgsql;
|
||||
--
|
||||
-- CREATE OR REPLACE TRIGGER insert_membership
|
||||
-- BEFORE INSERT ON membership_
|
||||
-- FOR EACH ROW
|
||||
-- EXECUTE FUNCTION create_membership_timestamp();
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn routes() -> Router {
|
||||
.route("/messages/{room_uuid}", post(create_message))
|
||||
}
|
||||
|
||||
/// Also resets `last_read_at`
|
||||
async fn list_messages(
|
||||
Path(room_uuid): Path<Uuid>,
|
||||
Query(query): Query<MessageFetchQuery>,
|
||||
@@ -73,6 +74,11 @@ async fn list_messages(
|
||||
|
||||
let limit: i32 = query.limit.unwrap_or(30).abs().min(80);
|
||||
|
||||
let mut tx = db
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||
|
||||
let messages = sqlx::query_as::<_, MessageRow>(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -95,7 +101,7 @@ async fn list_messages(
|
||||
.bind(room_id)
|
||||
.bind(query.before)
|
||||
.bind(limit)
|
||||
.fetch_all(&db)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -119,6 +125,30 @@ async fn list_messages(
|
||||
|
||||
messages.reverse();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE membership_
|
||||
SET last_read_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1
|
||||
AND room = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(room_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error updating membership timestamp: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to "))
|
||||
})?;
|
||||
|
||||
tx.commit().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not list messages".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(messages))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct Room {
|
||||
pub global: bool,
|
||||
pub owner_name: String,
|
||||
pub owner_uuid: Uuid,
|
||||
pub unread_count: i64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -89,24 +90,32 @@ async fn list_rooms(
|
||||
|
||||
let rooms = sqlx::query_as::<_, Room>(
|
||||
r#"
|
||||
SELECT r.uuid,
|
||||
u.username AS owner_name,
|
||||
u.uuid AS owner_uuid,
|
||||
r.name,
|
||||
r.global
|
||||
SELECT
|
||||
r.uuid,
|
||||
u.username AS owner_name,
|
||||
u.uuid AS owner_uuid,
|
||||
r.name,
|
||||
r.global,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM message_ m
|
||||
WHERE m.room = r.id
|
||||
AND m.sent_at > mem.last_read_at
|
||||
AND m.sender != $1
|
||||
) AS unread_count
|
||||
FROM room_ r
|
||||
JOIN user_ u ON u.id = r.owner
|
||||
WHERE r.global OR EXISTS (
|
||||
SELECT 1
|
||||
FROM membership_ m
|
||||
WHERE m.user_id = $1 AND m.room = r.id
|
||||
)
|
||||
LEFT JOIN membership_ mem ON mem.room = r.id AND mem.user_id = $1
|
||||
WHERE r.global OR mem.user_id IS NOT NULL
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.unwrap_or(Vec::new());
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("Failed listing rooms: {e}");
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
Ok(Json(rooms))
|
||||
}
|
||||
@@ -158,6 +167,7 @@ async fn create_room(
|
||||
owner_uuid: claims.sub,
|
||||
name: payload.name,
|
||||
global: payload.global,
|
||||
unread_count: 0,
|
||||
}),
|
||||
))
|
||||
}
|
||||
@@ -170,42 +180,63 @@ async fn get_room(
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
let room_id = room_id_from_uuid(&db, room_uuid).await?;
|
||||
|
||||
if !is_member(user_id, room_id, &db).await {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
String::from("You are not a member of this room"),
|
||||
));
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RoomRow {
|
||||
uuid: Uuid,
|
||||
owner_name: String,
|
||||
owner_uuid: Uuid,
|
||||
name: String,
|
||||
global: bool,
|
||||
unread_count: Option<i64>,
|
||||
is_member: Option<bool>,
|
||||
}
|
||||
|
||||
let room: Room = sqlx::query_as(
|
||||
let row: RoomRow = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
r.uuid,
|
||||
u.username AS owner_name,
|
||||
u.uuid AS owner_uuid,
|
||||
r.name,
|
||||
r.global
|
||||
r.global,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM message_ m
|
||||
WHERE m.room = r.id
|
||||
AND m.sent_at > mem.last_read_at
|
||||
AND m.sender != $2
|
||||
) AS unread_count,
|
||||
(r.global OR mem.user_id IS NOT NULL) AS is_member
|
||||
FROM room_ r
|
||||
JOIN user_ u ON u.id = r.owner
|
||||
LEFT JOIN membership_ mem ON mem.room = r.id AND mem.user_id = $2
|
||||
WHERE r.uuid = $1
|
||||
"#,
|
||||
)
|
||||
.bind(room_uuid)
|
||||
.bind(user_id)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e}");
|
||||
tracing::error!("Failed getting room: {e}");
|
||||
(StatusCode::NOT_FOUND, "Room not found".to_string())
|
||||
})?;
|
||||
|
||||
if !row.is_member.unwrap_or(false) {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
"You are not a member of this room".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(Room {
|
||||
uuid: room_uuid,
|
||||
owner_name: room.owner_name,
|
||||
owner_uuid: room.owner_uuid,
|
||||
name: room.name,
|
||||
global: room.global,
|
||||
uuid: row.uuid,
|
||||
owner_name: row.owner_name,
|
||||
owner_uuid: row.owner_uuid,
|
||||
name: row.name,
|
||||
global: row.global,
|
||||
unread_count: row.unread_count.unwrap_or(0),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -362,7 +393,8 @@ async fn accept_request(
|
||||
u.username AS owner_name,
|
||||
u.uuid AS owner_uuid,
|
||||
r.name,
|
||||
r.global
|
||||
r.global,
|
||||
0::bigint AS unread_count
|
||||
FROM room_ r
|
||||
JOIN user_ u ON u.id = r.owner
|
||||
WHERE r.id = $1 AND r.owner = $2
|
||||
@@ -394,6 +426,7 @@ async fn accept_request(
|
||||
owner_uuid: room.owner_uuid,
|
||||
name: room.name,
|
||||
global: room.global,
|
||||
unread_count: room.unread_count,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user