From 87562d3b5d9225bbca1385e70afe677d541bb5d7 Mon Sep 17 00:00:00 2001 From: trisua Date: Tue, 9 Sep 2025 23:12:49 -0400 Subject: [PATCH] add: chat invites table --- app/public/style.css | 4 +- src/database/chat_invites.rs | 105 +++++++++++++++++++++++ src/database/mod.rs | 2 + src/database/sql/create_chat_invites.sql | 7 ++ src/database/sql/mod.rs | 1 + src/model.rs | 23 ++++- 6 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/database/chat_invites.rs create mode 100644 src/database/sql/create_chat_invites.sql diff --git a/app/public/style.css b/app/public/style.css index 190377e..0e595c6 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -108,7 +108,8 @@ article { } .tab { - flex: 1 0 auto; + /* flex: 1 0 auto; */ + flex: 0 0 auto; overflow: auto; } @@ -118,6 +119,7 @@ article { .tabs { overflow: auto; + width: 100%; } .fadein { diff --git a/src/database/chat_invites.rs b/src/database/chat_invites.rs new file mode 100644 index 0000000..43da5e3 --- /dev/null +++ b/src/database/chat_invites.rs @@ -0,0 +1,105 @@ +use super::DataManager; +use crate::model::ChatInvite; +use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_rows}; +use tetratto_core::{ + auto_method, + model::{Error, Result, auth::User}, +}; + +impl DataManager { + /// Get a [`ChatInvite`] from an SQL row. + pub(crate) fn get_invite_from_row(x: &PostgresRow) -> ChatInvite { + ChatInvite { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + chat: get!(x->3(i64)) as usize, + code: get!(x->4(String)), + } + } + + auto_method!(get_invite_by_id(usize as i64)@get_invite_from_row -> "SELECT * FROM t_chat_invites WHERE id = $1" --name="invite" --returns=ChatInvite --cache-key-tmpl="twny.invt:{}"); + + /// Get invites by their chat ID. + pub async fn get_chat_invites_by_chat( + &self, + id: usize, + batch: usize, + page: usize, + ) -> Result> { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM t_chat_invites WHERE chat = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", + params![&(id as i64), &(batch as i64), &((page * batch) as i64)], + |x| { Self::get_invite_from_row(x) } + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new invite in the database. + /// + /// # Arguments + /// * `data` - a mock [`ChatInvite`] object to insert + pub async fn create_invite(&self, data: ChatInvite) -> Result { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO t_chat_invites VALUES ($1, $2, $3, $4, $5)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.owner as i64), + &(data.chat as i64), + &data.code, + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } + + /// Delete an existing invite. + pub async fn delete_invite(&self, id: usize, user: &User) -> Result<()> { + let invite = self.get_invite_by_id(id).await?; + + if invite.owner != user.id { + return Err(Error::NotAllowed); + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + // delete invite + let res = execute!( + &conn, + "DELETE FROM t_chat_invites WHERE id = $1", + params![&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(()) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index b991201..1ecc9c0 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,4 @@ +mod chat_invites; mod chats; mod messages; mod notifications; @@ -52,6 +53,7 @@ impl DataManager { execute!(&conn, sql::CREATE_TABLE_CHATS).unwrap(); execute!(&conn, sql::CREATE_TABLE_MESSAGES).unwrap(); + execute!(&conn, sql::CREATE_TABLE_CHAT_INVITES).unwrap(); for x in sql::VERSION_MIGRATIONS.split(";") { execute!(&conn, x).unwrap(); diff --git a/src/database/sql/create_chat_invites.sql b/src/database/sql/create_chat_invites.sql new file mode 100644 index 0000000..a9df271 --- /dev/null +++ b/src/database/sql/create_chat_invites.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS t_chat_invites ( + id BIGINT NOT NULL, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + chat BIGINT NOT NULL, + code TEXT NOT NULL +); diff --git a/src/database/sql/mod.rs b/src/database/sql/mod.rs index 28bf53b..e852200 100644 --- a/src/database/sql/mod.rs +++ b/src/database/sql/mod.rs @@ -1,3 +1,4 @@ pub const CREATE_TABLE_CHATS: &str = include_str!("./create_chats.sql"); pub const CREATE_TABLE_MESSAGES: &str = include_str!("./create_messages.sql"); +pub const CREATE_TABLE_CHAT_INVITES: &str = include_str!("./create_chat_invites.sql"); pub const VERSION_MIGRATIONS: &str = include_str!("./version_migrations.sql"); diff --git a/src/model.rs b/src/model.rs index 1e1ccc4..19d13c1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; +use tetratto_shared::{hash::salt, snow::Snowflake, unix_epoch_timestamp}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct GroupChatInfo { @@ -118,3 +118,24 @@ impl SocketMessage { serde_json::to_string(&self).unwrap() } } + +#[derive(Clone, Serialize, Deserialize)] +pub struct ChatInvite { + pub id: usize, + pub created: usize, + pub owner: usize, + pub chat: usize, + pub code: String, +} + +impl ChatInvite { + pub fn new(owner: usize, chat: usize) -> Self { + Self { + id: Snowflake::new().to_string().parse::().unwrap(), + created: unix_epoch_timestamp(), + owner, + chat, + code: salt(), + } + } +}