From 8883ff6400ffca79112066b8178b0d6e9e2fcf21 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:16:14 +0200 Subject: [PATCH 1/4] feat: modify existing tables and add more tables for servers/chatting --- src/main.rs | 75 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index a591c1f..20cf85c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,17 +45,29 @@ async fn main() -> Result<(), Error> { /* TODO: Figure out if a table should be used here and if not then what. Also figure out if these should be different types from what they currently are and if we should add more "constraints" + + TODO: References to time should be removed in favor of using the timestamp built in to UUIDv7 (apart from deleted_at in users) */ sqlx::raw_sql( r#" CREATE TABLE IF NOT EXISTS users ( - uuid uuid PRIMARY KEY UNIQUE NOT NULL, - username varchar(32) UNIQUE NOT NULL, + uuid uuid PRIMARY KEY NOT NULL, + username varchar(32) NOT NULL, display_name varchar(64) DEFAULT NULL, password varchar(512) NOT NULL, - email varchar(100) UNIQUE NOT NULL, - email_verified boolean NOT NULL DEFAULT FALSE + email varchar(100) NOT NULL, + email_verified boolean NOT NULL DEFAULT FALSE, + is_deleted boolean NOT NULL DEFAULT FALSE, + deleted_at int8 DEFAULT NULL, + CONSTRAINT unique_username_active UNIQUE NULLS NOT DISTINCT (username, is_deleted), + CONSTRAINT unique_email_active UNIQUE NULLS NOT DISTINCT (email, is_deleted) ); + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_username_active + ON users(username) + WHERE is_deleted = FALSE; + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_email_active + ON users(email) + WHERE is_deleted = FALSE; CREATE TABLE IF NOT EXISTS instance_permissions ( uuid uuid NOT NULL REFERENCES users(uuid), administrator boolean NOT NULL DEFAULT FALSE @@ -68,39 +80,67 @@ async fn main() -> Result<(), Error> { ); CREATE TABLE IF NOT EXISTS access_tokens ( token varchar(32) PRIMARY KEY UNIQUE NOT NULL, - refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token), + refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token) ON DELETE CASCADE, uuid uuid NOT NULL REFERENCES users(uuid), created int8 NOT NULL ); CREATE TABLE IF NOT EXISTS guilds ( uuid uuid PRIMARY KEY NOT NULL, - name VARCHAR(100), - description VARCHAR(300), - created_at int8 NOT NULL + owner_uuid uuid NOT NULL REFERENCES users(uuid), + name VARCHAR(100) NOT NULL, + description VARCHAR(300) ); CREATE TABLE IF NOT EXISTS guild_members ( - guild_uuid uuid NOT NULL REFERENCES guilds(uuid), + uuid uuid PRIMARY KEY NOT NULL, + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, user_uuid uuid NOT NULL REFERENCES users(uuid), + nickname VARCHAR(100) DEFAULT NULL + ); + CREATE TABLE IF NOT EXISTS roles ( + uuid uuid UNIQUE NOT NULL, + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, + name VARCHAR(50) NOT NULL, + color int NOT NULL DEFAULT 16777215, + position int NOT NULL, permissions int8 NOT NULL DEFAULT 0, - PRIMARY KEY (guild_uuid, user_uuid) + PRIMARY KEY (uuid, guild_uuid) + ); + CREATE TABLE IF NOT EXISTS role_members ( + role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE, + member_uuid uuid NOT NULL REFERENCES guild_members(uuid) ON DELETE CASCADE, + PRIMARY KEY (role_uuid, member_uuid) ); CREATE TABLE IF NOT EXISTS channels ( uuid uuid PRIMARY KEY NOT NULL, - guild_uuid NOT NULL REFERENCES guilds(uuid), + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, name varchar(32) NOT NULL, description varchar(500) NOT NULL ); + CREATE TABLE IF NOT EXISTS channel_permissions ( + channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE, + role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE, + permissions int8 NOT NULL DEFAULT 0, + PRIMARY KEY (channel_uuid, role_uuid) + ); CREATE TABLE IF NOT EXISTS messages ( uuid uuid PRIMARY KEY NOT NULL, - channel_uuid uuid NOT NULL REFERENCES channels(uuid), + channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE, user_uuid uuid NOT NULL REFERENCES users(uuid), - message varchar(2000) NOT NULL, - created_at int8 NOT NULL + message varchar(4000) NOT NULL ); + "#, + ) + .execute(&pool) + .await?; + + /* + **Stored for later possible use** + CREATE TABLE IF NOT EXISTS emojis ( uuid uuid PRIMARY KEY NOT NULL, name varchar(32) NOT NULL, - guild_uuid uuid REFERENCES guilds(uuid) + guild_uuid uuid REFERENCES guilds(uuid) ON DELETE SET NULL, + deleted boolean DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS message_reactions ( message_uuid uuid NOT NULL REFERENCES messages(uuid), @@ -108,10 +148,7 @@ async fn main() -> Result<(), Error> { emoji_uuid uuid NOT NULL REFERENCES emojis(uuid), PRIMARY KEY (message_uuid, user_uuid, emoji_uuid) ) - "#, - ) - .execute(&pool) - .await?; + */ let data = Data { pool, From f9e1e276f0c87b1f57902375493b3c3388404fa4 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:16:57 +0200 Subject: [PATCH 2/4] feat: implement guild creation --- src/api/v1/mod.rs | 1 + src/api/v1/servers/mod.rs | 62 +++++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 7e62f06..36bde4a 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -10,4 +10,5 @@ pub fn web() -> Scope { .service(stats::res) .service(auth::web()) .service(users::web()) + .service(servers::web()) } diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 6b88e68..35e9bbe 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -1,30 +1,40 @@ -use actix_web::{Error, HttpResponse, error, post, web}; +use actix_web::{error, post, web, Error, HttpResponse, Scope}; use futures::StreamExt; use log::error; use serde::{Deserialize, Serialize}; +use ::uuid::Uuid; use std::time::{SystemTime, UNIX_EPOCH}; mod uuid; mod channels; -use crate::Data; +use crate::{api::v1::auth::check_access_token, Data}; #[derive(Deserialize)] struct Request { access_token: String, - name: String + name: String, + description: Option, } #[derive(Serialize)] struct Response { - refresh_token: String, - access_token: String, + guild_uuid: Uuid, +} + +impl Response { + fn new(guild_uuid: Uuid) -> Self { + Self { + guild_uuid + } + } } const MAX_SIZE: usize = 262_144; pub fn web() -> Scope { web::scope("/servers") + .service(res) .service(channels::web()) .service(uuid::res) } @@ -43,6 +53,46 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result(&body)?; - Ok(HttpResponse::Unauthorized().finish()) + let authorized = check_access_token(request.access_token, &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let guild_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO guilds (uuid, owner_uuid, name, description) VALUES ('{}', '{}', $1, $2)", guild_uuid, uuid)) + .bind(request.name) + .bind(request.description) + .execute(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()) + } + + let row = sqlx::query(&format!("INSERT INTO guild_members (uuid, guild_uuid, user_uuid) VALUES ('{}', '{}', '{}')", Uuid::now_v7(), guild_uuid, uuid)) + .bind(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64) + .execute(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + let row = sqlx::query(&format!("DELETE FROM guilds WHERE uuid = '{}'", guild_uuid)) + .execute(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + } + + return Ok(HttpResponse::InternalServerError().finish()) + } + + Ok(HttpResponse::Ok().json(Response::new(guild_uuid))) } From 6abd2a9d5202b163649f90315359c6885c68e6f5 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:17:25 +0200 Subject: [PATCH 3/4] feat: implement guild fetching with uuid only returns if you are a member of the guild in question --- src/api/v1/servers/uuid.rs | 123 +++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/api/v1/servers/uuid.rs b/src/api/v1/servers/uuid.rs index e69de29..fdbfeb7 100644 --- a/src/api/v1/servers/uuid.rs +++ b/src/api/v1/servers/uuid.rs @@ -0,0 +1,123 @@ +use actix_web::{error, post, web, Error, HttpResponse}; +use futures::StreamExt; +use log::error; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; +use std::str::FromStr; + +use crate::{api::v1::auth::check_access_token, Data}; + +#[derive(Deserialize)] +struct Request { + access_token: String, +} + +#[derive(Serialize)] +struct Response { + uuid: Uuid, + name: String, + description: Option, + icon: String, + owner_uuid: Uuid, + roles: Vec, + member_count: i64, +} + +#[derive(Serialize, FromRow)] +struct Role { + uuid: String, + name: String, + color: i64, + position: i32, + permissions: i64, +} + +const MAX_SIZE: usize = 262_144; + +#[post("/{uuid}")] +pub async fn res(mut payload: web::Payload, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let mut body = web::BytesMut::new(); + while let Some(chunk) = payload.next().await { + let chunk = chunk?; + // limit max size of in-memory payload + if (body.len() + chunk.len()) > MAX_SIZE { + return Err(error::ErrorBadRequest("overflow")); + } + body.extend_from_slice(&chunk); + } + + let guild_uuid = path.into_inner().0; + + let request = serde_json::from_slice::(&body)?; + + let authorized = check_access_token(request.access_token, &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let row: Result = sqlx::query_scalar(&format!("SELECT CAST(uuid AS VARCHAR) FROM guild_members WHERE guild_uuid = '{}' AND user_uuid = '{}'", guild_uuid, uuid)) + .fetch_one(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Ok(HttpResponse::InternalServerError().finish()) + } + + let member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + + let row = sqlx::query_as(&format!("SELECT CAST(owner_uuid AS VARCHAR), name, description FROM guilds WHERE uuid = '{}'", guild_uuid)) + .fetch_one(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Ok(HttpResponse::InternalServerError().finish()) + } + + let (owner_uuid_raw, name, description): (String, String, Option) = row.unwrap(); + + let owner_uuid = Uuid::from_str(&owner_uuid_raw).unwrap(); + + let row = sqlx::query_scalar(&format!("SELECT COUNT(uuid) FROM guild_members WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_one(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Ok(HttpResponse::InternalServerError().finish()) + } + + let member_count: i64 = row.unwrap(); + + let roles_raw = sqlx::query_as(&format!("SELECT (uuid, name, color, position, permissions) FROM roles WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_all(&data.pool) + .await; + + if let Err(error) = roles_raw { + error!("{}", error); + + return Ok(HttpResponse::InternalServerError().finish()) + } + + let roles: Vec = roles_raw.unwrap(); + + + Ok(HttpResponse::Ok().json(Response { + uuid, + name, + description, + icon: "bogus".to_string(), + owner_uuid, + roles, + member_count, + })) +} + From d72214eb569cf5b5611c3f66174ed28897cfa4ef Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:18:24 +0200 Subject: [PATCH 4/4] fix: make server channel template not error out --- src/api/v1/servers/channels/mod.rs | 4 ++++ src/api/v1/servers/channels/uuid/mod.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/api/v1/servers/channels/mod.rs b/src/api/v1/servers/channels/mod.rs index b9cf990..1f7e940 100644 --- a/src/api/v1/servers/channels/mod.rs +++ b/src/api/v1/servers/channels/mod.rs @@ -1,3 +1,7 @@ +use actix_web::{web, Scope}; + +mod uuid; + pub fn web() -> Scope { web::scope("/channels") } \ No newline at end of file diff --git a/src/api/v1/servers/channels/uuid/mod.rs b/src/api/v1/servers/channels/uuid/mod.rs index e69de29..87a2b7b 100644 --- a/src/api/v1/servers/channels/uuid/mod.rs +++ b/src/api/v1/servers/channels/uuid/mod.rs @@ -0,0 +1 @@ +mod messages;