From 838947a7ca9d0b6ab484a6cee25daeeca5c331a8 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:27:38 +0200 Subject: [PATCH 01/26] build: add tokio-tungstenite dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index e34d9b6..68f4c0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } +tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } uuid = { version = "1.16", features = ["serde", "v7"] } -- 2.47.2 From 34b984a1b552336754289615c4c924de1241c7db Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:31:35 +0200 Subject: [PATCH 02/26] feat: add tables for guilds, members, channels, messages, emojis and reactions --- src/main.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main.rs b/src/main.rs index 4c909b1..a591c1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,42 @@ async fn main() -> Result<(), Error> { refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token), 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 + ); + CREATE TABLE IF NOT EXISTS guild_members ( + guild_uuid uuid NOT NULL REFERENCES guilds(uuid), + user_uuid uuid NOT NULL REFERENCES users(uuid), + permissions int8 NOT NULL DEFAULT 0, + PRIMARY KEY (guild_uuid, user_uuid) + ); + CREATE TABLE IF NOT EXISTS channels ( + uuid uuid PRIMARY KEY NOT NULL, + guild_uuid NOT NULL REFERENCES guilds(uuid), + name varchar(32) NOT NULL, + description varchar(500) NOT NULL + ); + CREATE TABLE IF NOT EXISTS messages ( + uuid uuid PRIMARY KEY NOT NULL, + channel_uuid uuid NOT NULL REFERENCES channels(uuid), + user_uuid uuid NOT NULL REFERENCES users(uuid), + message varchar(2000) NOT NULL, + created_at int8 NOT NULL + ); + CREATE TABLE IF NOT EXISTS emojis ( + uuid uuid PRIMARY KEY NOT NULL, + name varchar(32) NOT NULL, + guild_uuid uuid REFERENCES guilds(uuid) + ); + CREATE TABLE IF NOT EXISTS message_reactions ( + message_uuid uuid NOT NULL REFERENCES messages(uuid), + user_uuid uuid NOT NULL REFERENCES users(uuid), + emoji_uuid uuid NOT NULL REFERENCES emojis(uuid), + PRIMARY KEY (message_uuid, user_uuid, emoji_uuid) ) "#, ) -- 2.47.2 From 8241196284001839641c0bcfede39b4740c59ed6 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:32:22 +0200 Subject: [PATCH 03/26] feat: add boilerplate rust files --- src/api/v1/mod.rs | 1 + src/api/v1/servers/channels/mod.rs | 3 ++ src/api/v1/servers/channels/uuid/messages.rs | 0 src/api/v1/servers/channels/uuid/mod.rs | 0 src/api/v1/servers/mod.rs | 48 ++++++++++++++++++++ src/api/v1/servers/uuid.rs | 0 6 files changed, 52 insertions(+) create mode 100644 src/api/v1/servers/channels/mod.rs create mode 100644 src/api/v1/servers/channels/uuid/messages.rs create mode 100644 src/api/v1/servers/channels/uuid/mod.rs create mode 100644 src/api/v1/servers/mod.rs create mode 100644 src/api/v1/servers/uuid.rs diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index a5fd58a..7e62f06 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -3,6 +3,7 @@ use actix_web::{Scope, web}; mod auth; mod stats; mod users; +mod servers; pub fn web() -> Scope { web::scope("/v1") diff --git a/src/api/v1/servers/channels/mod.rs b/src/api/v1/servers/channels/mod.rs new file mode 100644 index 0000000..b9cf990 --- /dev/null +++ b/src/api/v1/servers/channels/mod.rs @@ -0,0 +1,3 @@ +pub fn web() -> Scope { + web::scope("/channels") +} \ No newline at end of file diff --git a/src/api/v1/servers/channels/uuid/messages.rs b/src/api/v1/servers/channels/uuid/messages.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/v1/servers/channels/uuid/mod.rs b/src/api/v1/servers/channels/uuid/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs new file mode 100644 index 0000000..6b88e68 --- /dev/null +++ b/src/api/v1/servers/mod.rs @@ -0,0 +1,48 @@ +use actix_web::{Error, HttpResponse, error, post, web}; +use futures::StreamExt; +use log::error; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +mod uuid; +mod channels; + +use crate::Data; + +#[derive(Deserialize)] +struct Request { + access_token: String, + name: String +} + +#[derive(Serialize)] +struct Response { + refresh_token: String, + access_token: String, +} + +const MAX_SIZE: usize = 262_144; + +pub fn web() -> Scope { + web::scope("/servers") + .service(channels::web()) + .service(uuid::res) +} + +#[post("")] +pub async fn res(mut payload: web::Payload, 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 request = serde_json::from_slice::(&body)?; + + Ok(HttpResponse::Unauthorized().finish()) +} + diff --git a/src/api/v1/servers/uuid.rs b/src/api/v1/servers/uuid.rs new file mode 100644 index 0000000..e69de29 -- 2.47.2 From 8883ff6400ffca79112066b8178b0d6e9e2fcf21 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:16:14 +0200 Subject: [PATCH 04/26] 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, -- 2.47.2 From f9e1e276f0c87b1f57902375493b3c3388404fa4 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:16:57 +0200 Subject: [PATCH 05/26] 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))) } -- 2.47.2 From 6abd2a9d5202b163649f90315359c6885c68e6f5 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:17:25 +0200 Subject: [PATCH 06/26] 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, + })) +} + -- 2.47.2 From d72214eb569cf5b5611c3f66174ed28897cfa4ef Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:18:24 +0200 Subject: [PATCH 07/26] 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; -- 2.47.2 From 776750578dda790b32bb3c5e3ae9df8f86d64340 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 06:25:01 +0200 Subject: [PATCH 08/26] style: :art: restructure server folder --- src/api/v1/servers/mod.rs | 4 +--- src/api/v1/servers/{ => uuid}/channels/mod.rs | 0 .../v1/servers/{ => uuid}/channels/uuid/messages.rs | 0 src/api/v1/servers/{ => uuid}/channels/uuid/mod.rs | 0 src/api/v1/servers/{uuid.rs => uuid/mod.rs} | 12 ++++++++++-- 5 files changed, 11 insertions(+), 5 deletions(-) rename src/api/v1/servers/{ => uuid}/channels/mod.rs (100%) rename src/api/v1/servers/{ => uuid}/channels/uuid/messages.rs (100%) rename src/api/v1/servers/{ => uuid}/channels/uuid/mod.rs (100%) rename src/api/v1/servers/{uuid.rs => uuid/mod.rs} (94%) diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 35e9bbe..2cdd2e0 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -6,7 +6,6 @@ use ::uuid::Uuid; use std::time::{SystemTime, UNIX_EPOCH}; mod uuid; -mod channels; use crate::{api::v1::auth::check_access_token, Data}; @@ -35,8 +34,7 @@ const MAX_SIZE: usize = 262_144; pub fn web() -> Scope { web::scope("/servers") .service(res) - .service(channels::web()) - .service(uuid::res) + .service(uuid::web()) } #[post("")] diff --git a/src/api/v1/servers/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs similarity index 100% rename from src/api/v1/servers/channels/mod.rs rename to src/api/v1/servers/uuid/channels/mod.rs diff --git a/src/api/v1/servers/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs similarity index 100% rename from src/api/v1/servers/channels/uuid/messages.rs rename to src/api/v1/servers/uuid/channels/uuid/messages.rs diff --git a/src/api/v1/servers/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs similarity index 100% rename from src/api/v1/servers/channels/uuid/mod.rs rename to src/api/v1/servers/uuid/channels/uuid/mod.rs diff --git a/src/api/v1/servers/uuid.rs b/src/api/v1/servers/uuid/mod.rs similarity index 94% rename from src/api/v1/servers/uuid.rs rename to src/api/v1/servers/uuid/mod.rs index fdbfeb7..02ad3a1 100644 --- a/src/api/v1/servers/uuid.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -1,4 +1,4 @@ -use actix_web::{error, post, web, Error, HttpResponse}; +use actix_web::{error, post, web, Error, HttpResponse, Scope}; use futures::StreamExt; use log::error; use serde::{Deserialize, Serialize}; @@ -6,6 +6,8 @@ use sqlx::FromRow; use uuid::Uuid; use std::str::FromStr; +mod channels; + use crate::{api::v1::auth::check_access_token, Data}; #[derive(Deserialize)] @@ -35,7 +37,13 @@ struct Role { const MAX_SIZE: usize = 262_144; -#[post("/{uuid}")] +pub fn web() -> Scope { + web::scope("/") + .service(res) + .service(channels::web()) +} + +#[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 { -- 2.47.2 From fb76e6df08ff07e992f765e746e8ad897e82fc6c Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 23:39:36 +0200 Subject: [PATCH 09/26] feat: use new auth --- src/api/v1/servers/mod.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 2cdd2e0..9a933f7 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -1,4 +1,4 @@ -use actix_web::{error, post, web, Error, HttpResponse, Scope}; +use actix_web::{error, post, web, Error, HttpRequest, HttpResponse, Scope}; use futures::StreamExt; use log::error; use serde::{Deserialize, Serialize}; @@ -7,11 +7,10 @@ use std::time::{SystemTime, UNIX_EPOCH}; mod uuid; -use crate::{api::v1::auth::check_access_token, Data}; +use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; #[derive(Deserialize)] struct Request { - access_token: String, name: String, description: Option, } @@ -38,8 +37,25 @@ pub fn web() -> Scope { } #[post("")] -pub async fn res(mut payload: web::Payload, data: web::Data) -> Result { +pub async fn res(req: HttpRequest, mut payload: web::Payload, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + let mut body = web::BytesMut::new(); + while let Some(chunk) = payload.next().await { let chunk = chunk?; // limit max size of in-memory payload @@ -51,14 +67,6 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result(&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 guild_uuid = Uuid::now_v7(); let row = sqlx::query(&format!("INSERT INTO guilds (uuid, owner_uuid, name, description) VALUES ('{}', '{}', $1, $2)", guild_uuid, uuid)) -- 2.47.2 From beb9fc10bae87c332c4c6e03edd7d8c944ed947a Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 23:40:03 +0200 Subject: [PATCH 10/26] feat: use new auth and convert to get request --- src/api/v1/servers/uuid/mod.rs | 36 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 02ad3a1..022f0da 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -1,19 +1,13 @@ -use actix_web::{error, post, web, Error, HttpResponse, Scope}; -use futures::StreamExt; +use actix_web::{get, web, Error, HttpRequest, HttpResponse, Scope}; use log::error; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use sqlx::FromRow; use uuid::Uuid; use std::str::FromStr; mod channels; -use crate::{api::v1::auth::check_access_token, Data}; - -#[derive(Deserialize)] -struct Request { - access_token: String, -} +use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; #[derive(Serialize)] struct Response { @@ -35,31 +29,25 @@ struct Role { permissions: i64, } -const MAX_SIZE: usize = 262_144; - pub fn web() -> Scope { web::scope("/") .service(res) .service(channels::web()) } -#[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); +#[get("{uuid}")] +pub async fn res(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) } 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; + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; if let Err(error) = authorized { return Ok(error) -- 2.47.2 From cf1476f64116b92900b1278cc80d0d41786f40f8 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Mon, 5 May 2025 21:16:09 +0200 Subject: [PATCH 11/26] fix: correct merge error --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index fa86172..a8e41b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,7 +83,7 @@ async fn main() -> Result<(), Error> { token varchar(32) PRIMARY KEY UNIQUE NOT NULL, refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token) ON UPDATE CASCADE ON DELETE CASCADE, uuid uuid NOT NULL REFERENCES users(uuid), - created int8 NOT NULL + created_at int8 NOT NULL ); CREATE TABLE IF NOT EXISTS guilds ( uuid uuid PRIMARY KEY NOT NULL, -- 2.47.2 From 67af0c1e74d6d36b9c52bbd34527468c557436ab Mon Sep 17 00:00:00 2001 From: Radiicall Date: Wed, 7 May 2025 17:24:56 +0200 Subject: [PATCH 12/26] feat: add channel endpoint --- src/api/v1/servers/uuid/channels/mod.rs | 110 +++++++++++++++++++++++- src/api/v1/servers/uuid/mod.rs | 2 +- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 1f7e940..3786151 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,7 +1,109 @@ -use actix_web::{web, Scope}; +use std::str::FromStr; + +use actix_web::{get, web, Error, HttpRequest, HttpResponse}; +use serde::Serialize; +use sqlx::{prelude::FromRow, Pool, Postgres}; +use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use ::uuid::Uuid; +use log::error; mod uuid; -pub fn web() -> Scope { - web::scope("/channels") -} \ No newline at end of file +#[derive(Serialize, FromRow)] +struct ChannelPermission { + role_uuid: String, + permissions: i32 +} + +#[derive(Serialize)] +struct Channel { + uuid: String, + name: String, + description: Option, + permissions: Vec +} + +impl Channel { + async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { + let row = sqlx::query_as(&format!("SELECT uuid, name, description FROM channels WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let channels: Vec<(String, String, Option)> = row.unwrap(); + + let futures = channels.iter().map(async |t| { + let (uuid, name, description) = t.to_owned(); + + let row = sqlx::query_as(&format!("SELECT role_uuid, permissions FROM channel_permissions WHERE channel_uuid = '{}'", uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(Self { + uuid, + name, + description, + permissions: row.unwrap(), + }) + }); + + let channels = futures::future::join_all(futures).await; + + let channels: Result, HttpResponse> = channels.into_iter().collect(); + + Ok(channels?) + } +} + +#[get("{uuid}/channels")] +pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let guild_uuid = path.into_inner().0; + + let authorized = check_access_token(auth_header.unwrap(), &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 channels = Channel::fetch_all(&data.pool, guild_uuid).await; + + if let Err(error) = channels { + return Ok(error) + } + + Ok(HttpResponse::Ok().json(channels.unwrap())) +} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 022f0da..e85016a 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -32,7 +32,7 @@ struct Role { pub fn web() -> Scope { web::scope("/") .service(res) - .service(channels::web()) + .service(channels::response) } #[get("{uuid}")] -- 2.47.2 From 358a7f83369a63fdfbb24852fa3fd823de00b335 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:01:10 +0200 Subject: [PATCH 13/26] fix: fetching of servers and channels by uuid Co-authored-by: Radical --- src/api/v1/servers/uuid/channels/mod.rs | 4 ++-- src/api/v1/servers/uuid/mod.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 3786151..163234b 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -25,7 +25,7 @@ struct Channel { impl Channel { async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { - let row = sqlx::query_as(&format!("SELECT uuid, name, description FROM channels WHERE guild_uuid = '{}'", guild_uuid)) + let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}'", guild_uuid)) .fetch_all(pool) .await; @@ -40,7 +40,7 @@ impl Channel { let futures = channels.iter().map(async |t| { let (uuid, name, description) = t.to_owned(); - let row = sqlx::query_as(&format!("SELECT role_uuid, permissions FROM channel_permissions WHERE channel_uuid = '{}'", uuid)) + let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", uuid)) .fetch_all(pool) .await; diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index e85016a..8e04721 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -30,12 +30,13 @@ struct Role { } pub fn web() -> Scope { - web::scope("/") - .service(res) + web::scope("") .service(channels::response) + .service(res) + } -#[get("{uuid}")] +#[get("/{uuid}")] pub async fn res(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { let headers = req.headers(); -- 2.47.2 From 7ee500bf10e8d941690175b8e52bf18c4a6cb825 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:46:40 +0200 Subject: [PATCH 14/26] feat: add fetch_one() function to Channel struct --- src/api/v1/servers/uuid/channels/mod.rs | 37 +++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 163234b..242320d 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -9,14 +9,14 @@ use log::error; mod uuid; -#[derive(Serialize, FromRow)] +#[derive(Serialize, Clone, FromRow)] struct ChannelPermission { role_uuid: String, permissions: i32 } -#[derive(Serialize)] -struct Channel { +#[derive(Serialize, Clone)] +pub struct Channel { uuid: String, name: String, description: Option, @@ -64,6 +64,37 @@ impl Channel { Ok(channels?) } + + pub async fn fetch_one(pool: &Pool, guild_uuid: Uuid, channel_uuid: Uuid) -> Result { + let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, channel_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let (uuid, name, description): (String, String, Option) = row.unwrap(); + + let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, channel_uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(Self { + uuid, + name, + description, + permissions: row.unwrap(), + }) + } } #[get("{uuid}/channels")] -- 2.47.2 From caee16005d6c20d434212ba018c06ec9058d3f34 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:46:55 +0200 Subject: [PATCH 15/26] feat: implement caching for channels endpoint --- src/api/v1/servers/uuid/channels/mod.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 242320d..6ecda6f 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -127,14 +127,28 @@ pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Dat return Ok(HttpResponse::InternalServerError().finish()) } - let member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + let _member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + let cache_result = data.get_cache_key(format!("{}_channels", guild_uuid)).await; - let channels = Channel::fetch_all(&data.pool, guild_uuid).await; + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) + } - if let Err(error) = channels { + let channels_result = Channel::fetch_all(&data.pool, guild_uuid).await; + + if let Err(error) = channels_result { return Ok(error) } - Ok(HttpResponse::Ok().json(channels.unwrap())) + let channels = channels_result.unwrap(); + + let cache_result = data.set_cache_key(format!("{}_channels", guild_uuid), channels.clone(), 1800).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(channels)) } -- 2.47.2 From c79451a8510af021e1cd490db51c28821eb37e83 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:47:07 +0200 Subject: [PATCH 16/26] feat: allow fetching a single channel --- src/api/v1/servers/uuid/channels/uuid/mod.rs | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index 87a2b7b..028ac99 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -1 +1,64 @@ mod messages; +use std::str::FromStr; + +use actix_web::{get, web, Error, HttpRequest, HttpResponse}; +use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use ::uuid::Uuid; +use log::error; +use super::Channel; + +#[get("{uuid}/channels/{channel_uuid}")] +pub async fn response(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let (guild_uuid, channel_uuid) = path.into_inner(); + + let authorized = check_access_token(auth_header.unwrap(), &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 cache_result = data.get_cache_key(format!("{}", channel_uuid)).await; + + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) + } + + let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await; + + if let Err(error) = channel_result { + return Ok(error) + } + + let channel = channel_result.unwrap(); + + let cache_result = data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(channel)) +} -- 2.47.2 From 1de99306a2f60fa160db983380d67235ac91b4e0 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:03:18 +0200 Subject: [PATCH 17/26] fix: add api/v1/servers/uuid/channels/uuid as a service --- src/api/v1/servers/uuid/channels/mod.rs | 2 +- src/api/v1/servers/uuid/channels/uuid/mod.rs | 2 +- src/api/v1/servers/uuid/mod.rs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 6ecda6f..1e03bea 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -7,7 +7,7 @@ use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; use ::uuid::Uuid; use log::error; -mod uuid; +pub mod uuid; #[derive(Serialize, Clone, FromRow)] struct ChannelPermission { diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index 028ac99..6699127 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -8,7 +8,7 @@ use log::error; use super::Channel; #[get("{uuid}/channels/{channel_uuid}")] -pub async fn response(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { +pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 8e04721..2011911 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -31,8 +31,9 @@ struct Role { pub fn web() -> Scope { web::scope("") - .service(channels::response) - .service(res) + .service(res) + .service(channels::response) + .service(channels::uuid::res) } -- 2.47.2 From 8821287cbe61f4be518ef67758b7175ea29e4307 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:09:30 +0200 Subject: [PATCH 18/26] fix: use correct query for channel_permissions in fetch_one() --- src/api/v1/servers/uuid/channels/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 1e03bea..e8cdb14 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -78,7 +78,7 @@ impl Channel { let (uuid, name, description): (String, String, Option) = row.unwrap(); - let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, channel_uuid)) + let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", channel_uuid)) .fetch_all(pool) .await; -- 2.47.2 From ef5fc96d67db56a64cfd1ae9709340eacea99309 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:34:08 +0200 Subject: [PATCH 19/26] feat: add permissions enum --- src/utils.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index 15e5e2e..b28407d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,20 @@ use serde::Serialize; use crate::Data; +enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); -- 2.47.2 From 1aabe9e524a02fbc9f4301dd530eb8b87614d2cf Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:53:59 +0200 Subject: [PATCH 20/26] feat: add del_cache_key() function --- src/utils.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index b28407d..be96e45 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -84,5 +84,13 @@ impl Data { redis::cmd("GET").arg(key_encoded).query_async(&mut conn).await } + + pub async fn del_cache_key(&self, key: String) -> Result<(), RedisError> { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + + let key_encoded = encode(key); + + redis::cmd("DEL").arg(key_encoded).query_async(&mut conn).await + } } -- 2.47.2 From 2e4860323e02509eea88721a37e41cf57147d199 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:54:21 +0200 Subject: [PATCH 21/26] feat: add post request to make channels --- src/api/v1/servers/uuid/channels/mod.rs | 82 ++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index e8cdb14..49fb2e9 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,7 +1,7 @@ use std::str::FromStr; -use actix_web::{get, web, Error, HttpRequest, HttpResponse}; -use serde::Serialize; +use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; use sqlx::{prelude::FromRow, Pool, Postgres}; use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; use ::uuid::Uuid; @@ -97,6 +97,12 @@ impl Channel { } } +#[derive(Deserialize)] +struct ChannelInfo { + name: String, + description: Option +} + #[get("{uuid}/channels")] pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { let headers = req.headers(); @@ -152,3 +158,75 @@ pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Dat Ok(HttpResponse::Ok().json(channels)) } + +#[post("{uuid}/channels")] +pub async fn response_post(req: HttpRequest, channel_info: web::Json, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let guild_uuid = path.into_inner().0; + + let authorized = check_access_token(auth_header.unwrap(), &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()) + } + + // FIXME: Logic to check permissions, should probably be done in utils.rs + + let _member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + + let channel_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO channels (uuid, guild_uuid, name, description) VALUES ('{}', '{}', $1, $2)", channel_uuid, guild_uuid)) + .bind(&channel_info.name) + .bind(&channel_info.description) + .execute(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()) + } + + let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await; + + if let Err(error) = channel_result { + return Ok(error) + } + + let channel = channel_result.unwrap(); + + let cache_result = data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + let cache_deletion_result = data.del_cache_key(format!("{}_channels", guild_uuid)).await; + + if let Err(error) = cache_deletion_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(channel)) +} -- 2.47.2 From 6374963e2ff1909ad293427a06cc1d2c310b1785 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 13:21:54 +0000 Subject: [PATCH 22/26] feat: add structs.rs Moved all server related structs into new file, added implementations to create, fetch, etc. --- src/api/v1/servers/mod.rs | 76 +--- src/api/v1/servers/uuid/channels/mod.rs | 156 +-------- src/api/v1/servers/uuid/channels/uuid/mod.rs | 16 +- src/api/v1/servers/uuid/mod.rs | 87 +---- src/main.rs | 3 +- src/structs.rs | 351 +++++++++++++++++++ src/utils.rs | 14 - 7 files changed, 386 insertions(+), 317 deletions(-) create mode 100644 src/structs.rs diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs index 9a933f7..31ede19 100644 --- a/src/api/v1/servers/mod.rs +++ b/src/api/v1/servers/mod.rs @@ -1,35 +1,16 @@ -use actix_web::{error, post, web, Error, HttpRequest, HttpResponse, Scope}; -use futures::StreamExt; -use log::error; -use serde::{Deserialize, Serialize}; -use ::uuid::Uuid; -use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{post, web, Error, HttpRequest, HttpResponse, Scope}; +use serde::Deserialize; mod uuid; -use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use crate::{api::v1::auth::check_access_token, structs::Guild, utils::get_auth_header, Data}; #[derive(Deserialize)] -struct Request { +struct GuildInfo { name: String, description: Option, } -#[derive(Serialize)] -struct Response { - 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) @@ -37,7 +18,7 @@ pub fn web() -> Scope { } #[post("")] -pub async fn res(req: HttpRequest, mut payload: web::Payload, data: web::Data) -> Result { +pub async fn res(req: HttpRequest, guild_info: web::Json, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); @@ -54,51 +35,12 @@ pub async fn res(req: HttpRequest, mut payload: web::Payload, data: web::Data MAX_SIZE { - return Err(error::ErrorBadRequest("overflow")); - } - body.extend_from_slice(&chunk); + if let Err(error) = guild { + return Ok(error) } - let request = serde_json::from_slice::(&body)?; - - 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))) + Ok(HttpResponse::Ok().json(guild.unwrap())) } diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 49fb2e9..215f0d2 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,102 +1,11 @@ -use std::str::FromStr; - use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::{prelude::FromRow, Pool, Postgres}; -use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use serde::Deserialize; +use crate::{api::v1::auth::check_access_token, structs::{Channel, Member}, utils::get_auth_header, Data}; use ::uuid::Uuid; use log::error; pub mod uuid; -#[derive(Serialize, Clone, FromRow)] -struct ChannelPermission { - role_uuid: String, - permissions: i32 -} - -#[derive(Serialize, Clone)] -pub struct Channel { - uuid: String, - name: String, - description: Option, - permissions: Vec -} - -impl Channel { - async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { - let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}'", guild_uuid)) - .fetch_all(pool) - .await; - - if let Err(error) = row { - error!("{}", error); - - return Err(HttpResponse::InternalServerError().finish()) - } - - let channels: Vec<(String, String, Option)> = row.unwrap(); - - let futures = channels.iter().map(async |t| { - let (uuid, name, description) = t.to_owned(); - - let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", uuid)) - .fetch_all(pool) - .await; - - if let Err(error) = row { - error!("{}", error); - - return Err(HttpResponse::InternalServerError().finish()) - } - - Ok(Self { - uuid, - name, - description, - permissions: row.unwrap(), - }) - }); - - let channels = futures::future::join_all(futures).await; - - let channels: Result, HttpResponse> = channels.into_iter().collect(); - - Ok(channels?) - } - - pub async fn fetch_one(pool: &Pool, guild_uuid: Uuid, channel_uuid: Uuid) -> Result { - let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, channel_uuid)) - .fetch_one(pool) - .await; - - if let Err(error) = row { - error!("{}", error); - - return Err(HttpResponse::InternalServerError().finish()) - } - - let (uuid, name, description): (String, String, Option) = row.unwrap(); - - let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", channel_uuid)) - .fetch_all(pool) - .await; - - if let Err(error) = row { - error!("{}", error); - - return Err(HttpResponse::InternalServerError().finish()) - } - - Ok(Self { - uuid, - name, - description, - permissions: row.unwrap(), - }) - } -} - #[derive(Deserialize)] struct ChannelInfo { name: String, @@ -123,18 +32,12 @@ pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Dat 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; + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; - if let Err(error) = row { - error!("{}", error); - - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = member { + return Ok(error); } - let _member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); - let cache_result = data.get_cache_key(format!("{}_channels", guild_uuid)).await; if let Ok(cache_hit) = cache_result { @@ -179,54 +82,19 @@ pub async fn response_post(req: HttpRequest, channel_info: web::Json = 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; + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; - if let Err(error) = row { - error!("{}", error); - - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = member { + return Ok(error); } // FIXME: Logic to check permissions, should probably be done in utils.rs - let _member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + let channel = Channel::new(data.clone(), guild_uuid, channel_info.name.clone(), channel_info.description.clone()).await; - let channel_uuid = Uuid::now_v7(); - - let row = sqlx::query(&format!("INSERT INTO channels (uuid, guild_uuid, name, description) VALUES ('{}', '{}', $1, $2)", channel_uuid, guild_uuid)) - .bind(&channel_info.name) - .bind(&channel_info.description) - .execute(&data.pool) - .await; - - if let Err(error) = row { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = channel { + return Ok(error); } - let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await; - - if let Err(error) = channel_result { - return Ok(error) - } - - let channel = channel_result.unwrap(); - - let cache_result = data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800).await; - - if let Err(error) = cache_result { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().finish()); - } - - let cache_deletion_result = data.del_cache_key(format!("{}_channels", guild_uuid)).await; - - if let Err(error) = cache_deletion_result { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().finish()); - } - - Ok(HttpResponse::Ok().json(channel)) + Ok(HttpResponse::Ok().json(channel.unwrap())) } diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index 6699127..608f672 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -1,11 +1,9 @@ mod messages; -use std::str::FromStr; use actix_web::{get, web, Error, HttpRequest, HttpResponse}; -use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; +use crate::{api::v1::auth::check_access_token, structs::{Channel, Member}, utils::get_auth_header, Data}; use ::uuid::Uuid; use log::error; -use super::Channel; #[get("{uuid}/channels/{channel_uuid}")] pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { @@ -27,18 +25,12 @@ pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Dat 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; + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; - if let Err(error) = row { - error!("{}", error); - - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = member { + return Ok(error); } - let _member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); - let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await; if let Ok(cache_hit) = cache_result { diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 2011911..4861e77 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -1,33 +1,9 @@ use actix_web::{get, web, Error, HttpRequest, HttpResponse, Scope}; -use log::error; -use serde::Serialize; -use sqlx::FromRow; use uuid::Uuid; -use std::str::FromStr; mod channels; -use crate::{api::v1::auth::check_access_token, utils::get_auth_header, Data}; - -#[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, -} +use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; pub fn web() -> Scope { web::scope("") @@ -57,65 +33,18 @@ pub async fn res(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data = 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; + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; - if let Err(error) = row { - error!("{}", error); - - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = member { + return Ok(error); } - let member_uuid = Uuid::from_str(&row.unwrap()).unwrap(); + let guild = Guild::fetch_one(&data.pool, guild_uuid).await; - 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()) + if let Err(error) = guild { + return Ok(error); } - 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, - })) + Ok(HttpResponse::Ok().json(guild.unwrap())) } diff --git a/src/main.rs b/src/main.rs index d349729..6a0863e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use config::{Config, ConfigBuilder}; mod api; pub mod utils; +pub mod structs; type Error = Box; @@ -21,7 +22,7 @@ struct Args { } #[derive(Clone)] -struct Data { +pub struct Data { pub pool: Pool, pub cache_pool: redis::Client, pub _config: Config, diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..0826c42 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,351 @@ +use std::str::FromStr; + +use serde::Serialize; +use sqlx::{prelude::FromRow, Pool, Postgres}; +use uuid::Uuid; +use actix_web::HttpResponse; +use log::error; + +use crate::Data; + +#[derive(Serialize, Clone)] +pub struct Channel { + pub uuid: Uuid, + pub guild_uuid: Uuid, + name: String, + description: Option, + pub permissions: Vec +} + +#[derive(Serialize, Clone, FromRow)] +struct ChannelPermissionBuilder { + role_uuid: String, + permissions: i32 +} + +impl ChannelPermissionBuilder { + fn build(&self) -> ChannelPermission { + ChannelPermission { + role_uuid: Uuid::from_str(&self.role_uuid).unwrap(), + permissions: self.permissions, + } + } +} + +#[derive(Serialize, Clone, FromRow)] +pub struct ChannelPermission { + pub role_uuid: Uuid, + pub permissions: i32 +} + +impl Channel { + pub async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { + let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), name, description FROM channels WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let channels: Vec<(String, String, Option)> = row.unwrap(); + + let futures = channels.iter().map(async |t| { + let (uuid, name, description) = t.to_owned(); + + let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let channel_permission_builders: Vec = row.unwrap(); + + Ok(Self { + uuid: Uuid::from_str(&uuid).unwrap(), + guild_uuid, + name, + description, + permissions: channel_permission_builders.iter().map(|b| b.build()).collect(), + }) + }); + + let channels = futures::future::join_all(futures).await; + + let channels: Result, HttpResponse> = channels.into_iter().collect(); + + Ok(channels?) + } + + pub async fn fetch_one(pool: &Pool, guild_uuid: Uuid, channel_uuid: Uuid) -> Result { + let row = sqlx::query_as(&format!("SELECT name, description FROM channels WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, channel_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let (name, description): (String, Option) = row.unwrap(); + + let row = sqlx::query_as(&format!("SELECT CAST(role_uuid AS VARCHAR), permissions FROM channel_permissions WHERE channel_uuid = '{}'", channel_uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let channel_permission_builders: Vec = row.unwrap(); + + Ok(Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + permissions: channel_permission_builders.iter().map(|b| b.build()).collect(), + }) + } + + pub async fn new(data: actix_web::web::Data, guild_uuid: Uuid, name: String, description: Option) -> Result { + let channel_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO channels (uuid, guild_uuid, name, description) VALUES ('{}', '{}', $1, $2)", channel_uuid, guild_uuid)) + .bind(&name) + .bind(&description) + .execute(&data.pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()) + } + + let channel = Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + permissions: vec![], + }; + + let cache_result = data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()); + } + + let cache_deletion_result = data.del_cache_key(format!("{}_channels", guild_uuid)).await; + + if let Err(error) = cache_deletion_result { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()); + } + + Ok(channel) + } +} + +#[derive(Clone, Copy)] +pub enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + +impl Permissions { + pub fn fetch_permissions(permissions: i64) -> Vec { + let all_perms = vec![ + Self::SendMessage, + Self::CreateChannel, + Self::DeleteChannel, + Self::ManageChannel, + Self::CreateRole, + Self::DeleteRole, + Self::ManageRole, + Self::CreateInvite, + Self::ManageInvite, + Self::ManageServer, + Self::ManageMember, + ]; + + all_perms.into_iter() + .filter(|p| permissions & (*p as i64) != 0) + .collect() + } +} + +#[derive(Serialize)] +pub struct Guild { + pub uuid: Uuid, + name: String, + description: Option, + icon: String, + owner_uuid: Uuid, + pub roles: Vec, + member_count: i64, +} + +impl Guild { + pub async fn fetch_one(pool: &Pool, guild_uuid: Uuid) -> Result { + let row = sqlx::query_as(&format!("SELECT CAST(owner_uuid AS VARCHAR), name, description FROM guilds WHERE uuid = '{}'", guild_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(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 member_count = Member::count(pool, guild_uuid).await?; + + let roles = Role::fetch_all(pool, guild_uuid).await?; + + Ok(Self { + uuid: guild_uuid, + name, + description, + // FIXME: This isnt supposed to be bogus + icon: String::from("bogus"), + owner_uuid, + roles, + member_count, + }) + } + + pub async fn new(pool: &Pool, name: String, description: Option, owner_uuid: Uuid) -> Result { + let guild_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO guilds (uuid, owner_uuid, name, description) VALUES ('{}', '{}', $1, $2)", guild_uuid, owner_uuid)) + .bind(&name) + .bind(&description) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()) + } + + let row = sqlx::query(&format!("INSERT INTO guild_members (uuid, guild_uuid, user_uuid) VALUES ('{}', '{}', '{}')", Uuid::now_v7(), guild_uuid, owner_uuid)) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + let row = sqlx::query(&format!("DELETE FROM guilds WHERE uuid = '{}'", guild_uuid)) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + } + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(Guild { + uuid: guild_uuid, + name, + description, + icon: "bogus".to_string(), + owner_uuid, + roles: vec![], + member_count: 1 + }) + } +} + +#[derive(Serialize, FromRow)] +pub struct Role { + uuid: String, + name: String, + color: i64, + position: i32, + permissions: i64, +} + +impl Role { + pub async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { + let roles = sqlx::query_as(&format!("SELECT (uuid, name, color, position, permissions) FROM roles WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = roles { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(roles.unwrap()) + } +} + +pub struct Member { + pub uuid: Uuid, + pub nickname: String, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl Member { + async fn count(pool: &Pool, guild_uuid: Uuid) -> Result { + let member_count = sqlx::query_scalar(&format!("SELECT COUNT(uuid) FROM guild_members WHERE guild_uuid = '{}'", guild_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = member_count { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(member_count.unwrap()) + } + + pub async fn fetch_one(pool: &Pool, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let row = sqlx::query_as(&format!("SELECT CAST(uuid AS VARCHAR), nickname FROM guild_members WHERE guild_uuid = '{}' AND user_uuid = '{}'", guild_uuid, user_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let (uuid, nickname): (String, String) = row.unwrap(); + + Ok(Member { + uuid: Uuid::from_str(&uuid).unwrap(), + nickname, + user_uuid, + guild_uuid, + }) + } +} diff --git a/src/utils.rs b/src/utils.rs index be96e45..424b10f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,20 +6,6 @@ use serde::Serialize; use crate::Data; -enum Permissions { - SendMessage = 1, - CreateChannel = 2, - DeleteChannel = 4, - ManageChannel = 8, - CreateRole = 16, - DeleteRole = 32, - ManageRole = 64, - CreateInvite = 128, - ManageInvite = 256, - ManageServer = 512, - ManageMember = 1024, -} - pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); -- 2.47.2 From daf61e0275fe138c0696438e028b5e501e9710c2 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 19:53:35 +0200 Subject: [PATCH 23/26] feat: implement message fetching --- .../v1/servers/uuid/channels/uuid/messages.rs | 73 +++++++++++++++++++ src/structs.rs | 50 ++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs index e69de29..70aa10b 100644 --- a/src/api/v1/servers/uuid/channels/uuid/messages.rs +++ b/src/api/v1/servers/uuid/channels/uuid/messages.rs @@ -0,0 +1,73 @@ +use actix_web::{get, web, Error, HttpRequest, HttpResponse}; +use serde::Deserialize; +use crate::{api::v1::auth::check_access_token, structs::{Channel, Member}, utils::get_auth_header, Data}; +use ::uuid::Uuid; +use log::error; + +#[derive(Deserialize)] +struct MessageRequest { + amount: i64, + offset: i64, +} + +#[get("{uuid}/channels/{channel_uuid}/messages")] +pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, message_request: web::Json, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let (guild_uuid, channel_uuid) = path.into_inner(); + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await; + + let mut channel_raw: Option = None; + + if let Ok(cache_hit) = cache_result { + channel_raw = Some(serde_json::from_str(&cache_hit).unwrap()) + } + + if channel_raw.is_none() { + let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await; + + if let Err(error) = channel_result { + return Ok(error) + } + + channel_raw = Some(channel_result.unwrap()); + + let cache_result = data.set_cache_key(format!("{}", channel_uuid), channel_raw.clone().unwrap(), 60).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + } + + let channel = channel_raw.unwrap(); + + let messages = channel.fetch_messages(&data.pool, message_request.amount, message_request.offset).await; + + if let Err(error) = messages { + return Ok(error) + } + + Ok(HttpResponse::Ok().json(messages.unwrap())) +} diff --git a/src/structs.rs b/src/structs.rs index 0826c42..b5ad6ea 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::{prelude::FromRow, Pool, Postgres}; use uuid::Uuid; use actix_web::HttpResponse; @@ -8,7 +8,7 @@ use log::error; use crate::Data; -#[derive(Serialize, Clone)] +#[derive(Serialize, Deserialize, Clone)] pub struct Channel { pub uuid: Uuid, pub guild_uuid: Uuid, @@ -32,7 +32,7 @@ impl ChannelPermissionBuilder { } } -#[derive(Serialize, Clone, FromRow)] +#[derive(Serialize, Deserialize, Clone, FromRow)] pub struct ChannelPermission { pub role_uuid: Uuid, pub permissions: i32 @@ -155,6 +155,23 @@ impl Channel { Ok(channel) } + + pub async fn fetch_messages(&self, pool: &Pool, amount: i64, offset: i64) -> Result, HttpResponse> { + let row = sqlx::query_as(&format!("SELECT uuid, user_uuid, message FROM channels WHERE channel_uuid = '{}' ORDER BY uuid LIMIT $1 OFFSET $2", self.uuid)) + .bind(amount) + .bind(offset) + .fetch_all(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()); + } + + let message_builders: Vec = row.unwrap(); + + Ok(message_builders.iter().map(|b| b.build()).collect()) + } } #[derive(Clone, Copy)] @@ -349,3 +366,30 @@ impl Member { }) } } + +#[derive(FromRow)] +struct MessageBuilder { + uuid: String, + channel_uuid: String, + user_uuid: String, + message: String, +} + +impl MessageBuilder { + fn build(&self) -> Message { + Message { + uuid: Uuid::from_str(&self.uuid).unwrap(), + channel_uuid: Uuid::from_str(&self.channel_uuid).unwrap(), + user_uuid: Uuid::from_str(&self.user_uuid).unwrap(), + message: self.message.clone(), + } + } +} + +#[derive(Serialize)] +pub struct Message { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, +} -- 2.47.2 From cb22bd8026c4be4ec9c2d079d1525d7778d8c979 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 21:38:53 +0200 Subject: [PATCH 24/26] fix: import messages endpoint --- src/api/v1/servers/uuid/channels/uuid/mod.rs | 2 +- src/api/v1/servers/uuid/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index 608f672..a223ad1 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -1,4 +1,4 @@ -mod messages; +pub mod messages; use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use crate::{api::v1::auth::check_access_token, structs::{Channel, Member}, utils::get_auth_header, Data}; diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 4861e77..2c6ebc7 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -7,10 +7,10 @@ use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils:: pub fn web() -> Scope { web::scope("") - .service(res) - .service(channels::response) - .service(channels::uuid::res) - + .service(res) + .service(channels::response) + .service(channels::uuid::res) + .service(channels::uuid::messages::res) } #[get("/{uuid}")] -- 2.47.2 From facfd95ed89ed273ab02b7f432997c09a8dcb555 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 22:14:41 +0200 Subject: [PATCH 25/26] feat: implement functions for role struct --- src/structs.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/src/structs.rs b/src/structs.rs index b5ad6ea..790889b 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -298,9 +298,33 @@ impl Guild { } } -#[derive(Serialize, FromRow)] -pub struct Role { +#[derive(FromRow)] +struct RoleBuilder { uuid: String, + guild_uuid: String, + name: String, + color: i64, + position: i32, + permissions: i64, +} + +impl RoleBuilder { + fn build(&self) -> Role { + Role { + uuid: Uuid::from_str(&self.uuid).unwrap(), + guild_uuid: Uuid::from_str(&self.guild_uuid).unwrap(), + name: self.name.clone(), + color: self.color, + position: self.position, + permissions: self.permissions, + } + } +} + +#[derive(Serialize, Clone)] +pub struct Role { + uuid: Uuid, + guild_uuid: Uuid, name: String, color: i64, position: i32, @@ -309,17 +333,68 @@ pub struct Role { impl Role { pub async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { - let roles = sqlx::query_as(&format!("SELECT (uuid, name, color, position, permissions) FROM roles WHERE guild_uuid = '{}'", guild_uuid)) + let role_builders_result = sqlx::query_as(&format!("SELECT (uuid, guild_uuid, name, color, position, permissions) FROM roles WHERE guild_uuid = '{}'", guild_uuid)) .fetch_all(pool) .await; - if let Err(error) = roles { + if let Err(error) = role_builders_result { error!("{}", error); return Err(HttpResponse::InternalServerError().finish()) } - Ok(roles.unwrap()) + let role_builders: Vec = role_builders_result.unwrap(); + + Ok(role_builders.iter().map(|b| b.build()).collect()) + } + + pub async fn fetch_one(pool: &Pool, role_uuid: Uuid, guild_uuid: Uuid) -> Result { + let row = sqlx::query_as(&format!("SELECT (name, color, position, permissions) FROM roles WHERE guild_uuid = '{}' AND uuid = '{}'", guild_uuid, role_uuid)) + .fetch_one(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let (name, color, position, permissions) = row.unwrap(); + + Ok(Role { + uuid: role_uuid, + guild_uuid, + name, + color, + position, + permissions, + }) + } + + pub async fn new(pool: &Pool, guild_uuid: Uuid, name: String) -> Result { + let role_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO channels (uuid, guild_uuid, name, position) VALUES ('{}', '{}', $1, $2)", role_uuid, guild_uuid)) + .bind(&name) + .bind(0) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()) + } + + let role = Self { + uuid: role_uuid, + guild_uuid, + name, + color: 16777215, + position: 0, + permissions: 0, + }; + + Ok(role) } } -- 2.47.2 From 6a608343965559f6d7b1a1ddf0df302e315b33b6 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 22:16:21 +0200 Subject: [PATCH 26/26] feat: add role creation/lookup --- src/api/v1/servers/uuid/mod.rs | 5 ++ src/api/v1/servers/uuid/roles/mod.rs | 99 +++++++++++++++++++++++++++ src/api/v1/servers/uuid/roles/uuid.rs | 54 +++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 src/api/v1/servers/uuid/roles/mod.rs create mode 100644 src/api/v1/servers/uuid/roles/uuid.rs diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 2c6ebc7..ffa1ae7 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -2,6 +2,7 @@ use actix_web::{get, web, Error, HttpRequest, HttpResponse, Scope}; use uuid::Uuid; mod channels; +mod roles; use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; @@ -9,8 +10,12 @@ pub fn web() -> Scope { web::scope("") .service(res) .service(channels::response) + .service(channels::response_post) .service(channels::uuid::res) .service(channels::uuid::messages::res) + .service(roles::response) + .service(roles::response_post) + .service(roles::uuid::res) } #[get("/{uuid}")] diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs new file mode 100644 index 0000000..23499de --- /dev/null +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -0,0 +1,99 @@ +use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; +use serde::Deserialize; +use crate::{api::v1::auth::check_access_token, structs::{Member, Role}, utils::get_auth_header, Data}; +use ::uuid::Uuid; +use log::error; + +pub mod uuid; + +#[derive(Deserialize)] +struct RoleInfo { + name: String, +} + +#[get("{uuid}/roles")] +pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let guild_uuid = path.into_inner().0; + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + let cache_result = data.get_cache_key(format!("{}_roles", guild_uuid)).await; + + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) + } + + let roles_result = Role::fetch_all(&data.pool, guild_uuid).await; + + if let Err(error) = roles_result { + return Ok(error) + } + + let roles = roles_result.unwrap(); + + let cache_result = data.set_cache_key(format!("{}_roles", guild_uuid), roles.clone(), 1800).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(roles)) +} + +#[post("{uuid}/roles")] +pub async fn response_post(req: HttpRequest, role_info: web::Json, path: web::Path<(Uuid,)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let guild_uuid = path.into_inner().0; + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + // FIXME: Logic to check permissions, should probably be done in utils.rs + + let role = Role::new(&data.pool, guild_uuid, role_info.name.clone()).await; + + if let Err(error) = role { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(role.unwrap())) +} diff --git a/src/api/v1/servers/uuid/roles/uuid.rs b/src/api/v1/servers/uuid/roles/uuid.rs new file mode 100644 index 0000000..3e55d5a --- /dev/null +++ b/src/api/v1/servers/uuid/roles/uuid.rs @@ -0,0 +1,54 @@ +use actix_web::{get, web, Error, HttpRequest, HttpResponse}; +use crate::{api::v1::auth::check_access_token, structs::{Member, Role}, utils::get_auth_header, Data}; +use ::uuid::Uuid; +use log::error; + +#[get("{uuid}/roles/{role_uuid}")] +pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error) + } + + let (guild_uuid, role_uuid) = path.into_inner(); + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error) + } + + let uuid = authorized.unwrap(); + + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + let cache_result = data.get_cache_key(format!("{}", role_uuid)).await; + + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) + } + + let role_result = Role::fetch_one(&data.pool, guild_uuid, role_uuid).await; + + if let Err(error) = role_result { + return Ok(error) + } + + let role = role_result.unwrap(); + + let cache_result = data.set_cache_key(format!("{}", role_uuid), role.clone(), 60).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(role)) +} -- 2.47.2