From 838947a7ca9d0b6ab484a6cee25daeeca5c331a8 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:27:38 +0200 Subject: [PATCH 01/32] 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"] } From 34b984a1b552336754289615c4c924de1241c7db Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:31:35 +0200 Subject: [PATCH 02/32] 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) ) "#, ) From 8241196284001839641c0bcfede39b4740c59ed6 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Sat, 3 May 2025 05:32:22 +0200 Subject: [PATCH 03/32] 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 From 8883ff6400ffca79112066b8178b0d6e9e2fcf21 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 01:16:14 +0200 Subject: [PATCH 04/32] 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 05/32] 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 06/32] 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 07/32] 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; From 776750578dda790b32bb3c5e3ae9df8f86d64340 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 06:25:01 +0200 Subject: [PATCH 08/32] 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 { From fb76e6df08ff07e992f765e746e8ad897e82fc6c Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 23:39:36 +0200 Subject: [PATCH 09/32] 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)) From beb9fc10bae87c332c4c6e03edd7d8c944ed947a Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 23:40:03 +0200 Subject: [PATCH 10/32] 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) From cf1476f64116b92900b1278cc80d0d41786f40f8 Mon Sep 17 00:00:00 2001 From: Radiicall Date: Mon, 5 May 2025 21:16:09 +0200 Subject: [PATCH 11/32] 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, From 67af0c1e74d6d36b9c52bbd34527468c557436ab Mon Sep 17 00:00:00 2001 From: Radiicall Date: Wed, 7 May 2025 17:24:56 +0200 Subject: [PATCH 12/32] 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}")] From 358a7f83369a63fdfbb24852fa3fd823de00b335 Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Wed, 7 May 2025 19:01:10 +0200 Subject: [PATCH 13/32] 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(); From 7ee500bf10e8d941690175b8e52bf18c4a6cb825 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:46:40 +0200 Subject: [PATCH 14/32] 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")] From caee16005d6c20d434212ba018c06ec9058d3f34 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:46:55 +0200 Subject: [PATCH 15/32] 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)) } From c79451a8510af021e1cd490db51c28821eb37e83 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 23:47:07 +0200 Subject: [PATCH 16/32] 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)) +} From 1de99306a2f60fa160db983380d67235ac91b4e0 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:03:18 +0200 Subject: [PATCH 17/32] 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) } From 8821287cbe61f4be518ef67758b7175ea29e4307 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:09:30 +0200 Subject: [PATCH 18/32] 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; From ef5fc96d67db56a64cfd1ae9709340eacea99309 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:34:08 +0200 Subject: [PATCH 19/32] 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); From 1aabe9e524a02fbc9f4301dd530eb8b87614d2cf Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:53:59 +0200 Subject: [PATCH 20/32] 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 + } } From 2e4860323e02509eea88721a37e41cf57147d199 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 00:54:21 +0200 Subject: [PATCH 21/32] 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)) +} From 6374963e2ff1909ad293427a06cc1d2c310b1785 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 13:21:54 +0000 Subject: [PATCH 22/32] 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); From daf61e0275fe138c0696438e028b5e501e9710c2 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 19:53:35 +0200 Subject: [PATCH 23/32] 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, +} From cb22bd8026c4be4ec9c2d079d1525d7778d8c979 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 21:38:53 +0200 Subject: [PATCH 24/32] 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}")] From facfd95ed89ed273ab02b7f432997c09a8dcb555 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 22:14:41 +0200 Subject: [PATCH 25/32] 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) } } From 6a608343965559f6d7b1a1ddf0df302e315b33b6 Mon Sep 17 00:00:00 2001 From: Radical Date: Thu, 8 May 2025 22:16:21 +0200 Subject: [PATCH 26/32] 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)) +} From 773f4ca9776bb0f938f328de020ea538b3b72856 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 9 May 2025 19:26:13 +0200 Subject: [PATCH 27/32] feat: add invites table --- src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.rs b/src/main.rs index 6a0863e..37011ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -134,6 +134,11 @@ async fn main() -> Result<(), Error> { user_uuid uuid NOT NULL REFERENCES users(uuid), message varchar(4000) NOT NULL ); + CREATE TABLE IF NOT EXISTS invites ( + id varchar(32) PRIMARY KEY NOT NULL, + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, + user_uuid uuid NOT NULL REFERENCES users(uuid) + ); "#, ) .execute(&pool) From c693e89853a3d0e2cc30fe73d15dab962af666ea Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 9 May 2025 19:26:49 +0200 Subject: [PATCH 28/32] feat: add fetching and making invites --- Cargo.toml | 1 + src/api/v1/mod.rs | 2 + src/api/v1/servers/uuid/invites/id.rs | 0 src/api/v1/servers/uuid/invites/mod.rs | 100 +++++++++++++++++++++++++ src/api/v1/servers/uuid/mod.rs | 7 ++ src/structs.rs | 72 ++++++++++++++++++ 6 files changed, 182 insertions(+) create mode 100644 src/api/v1/servers/uuid/invites/id.rs create mode 100644 src/api/v1/servers/uuid/invites/mod.rs diff --git a/Cargo.toml b/Cargo.toml index f4cfea7..3209341 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ 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"] } +random-string = "1.1" [dependencies.tokio] version = "1.44" diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 36bde4a..60c0b56 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -4,6 +4,7 @@ mod auth; mod stats; mod users; mod servers; +mod invites; pub fn web() -> Scope { web::scope("/v1") @@ -11,4 +12,5 @@ pub fn web() -> Scope { .service(auth::web()) .service(users::web()) .service(servers::web()) + .service(invites::web()) } diff --git a/src/api/v1/servers/uuid/invites/id.rs b/src/api/v1/servers/uuid/invites/id.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/v1/servers/uuid/invites/mod.rs b/src/api/v1/servers/uuid/invites/mod.rs new file mode 100644 index 0000000..2026a1a --- /dev/null +++ b/src/api/v1/servers/uuid/invites/mod.rs @@ -0,0 +1,100 @@ +use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; + +#[derive(Deserialize)] +struct InviteRequest { + custom_id: String, +} + +#[get("{uuid}/invites")] +pub async fn get_invites(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 guild_result = Guild::fetch_one(&data.pool, guild_uuid).await; + + if let Err(error) = guild_result { + return Ok(error); + } + + let guild = guild_result.unwrap(); + + let invites = guild.get_invites(&data.pool).await; + + if let Err(error) = invites { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(invites.unwrap())) +} + +#[post("{uuid}/invites")] +pub async fn create_invite(req: HttpRequest, path: web::Path<(Uuid,)>, invite_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 = 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_result = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member_result { + return Ok(error); + } + + let member = member_result.unwrap(); + + let guild_result = Guild::fetch_one(&data.pool, guild_uuid).await; + + if let Err(error) = guild_result { + return Ok(error); + } + + let guild = guild_result.unwrap(); + + let custom_id = invite_request.as_ref().and_then(|ir| Some(ir.custom_id.clone())); + + let invite = guild.create_invite(&data.pool, &member, custom_id).await; + + if let Err(error) = invite { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(invite.unwrap())) +} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index ffa1ae7..e9c14a8 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -3,19 +3,26 @@ use uuid::Uuid; mod channels; mod roles; +mod invites; use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; pub fn web() -> Scope { web::scope("") + // Servers .service(res) + // Channels .service(channels::response) .service(channels::response_post) .service(channels::uuid::res) .service(channels::uuid::messages::res) + // Roles .service(roles::response) .service(roles::response_post) .service(roles::uuid::res) + // Invites + .service(invites::get_invites) + .service(invites::create_invite) } #[get("/{uuid}")] diff --git a/src/structs.rs b/src/structs.rs index 790889b..1b6ef84 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -296,6 +296,50 @@ impl Guild { member_count: 1 }) } + + pub async fn get_invites(&self, pool: &Pool) -> Result, HttpResponse> { + let invites = sqlx::query_as(&format!("SELECT (id, guild_uuid, user_uuid) FROM invites WHERE guild_uuid = '{}'", self.uuid)) + .fetch_all(pool) + .await; + + if let Err(error) = invites { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(invites.unwrap().iter().map(|b: &InviteBuilder| b.build()).collect()) + } + + pub async fn create_invite(&self, pool: &Pool, member: &Member, custom_id: Option) -> Result { + let invite_id; + + if custom_id.is_none() { + let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + invite_id = random_string::generate(8, charset); + } else { + invite_id = custom_id.unwrap(); + if invite_id.len() > 32 { + return Err(HttpResponse::BadRequest().finish()) + } + } + + let result = sqlx::query(&format!("INSERT INTO invites (id, guild_uuid, user_uuid) VALUES ($1, '{}', '{}'", self.uuid, member.user_uuid)) + .bind(&invite_id) + .execute(pool) + .await; + + if let Err(error) = result { + error!("{}", error); + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(Invite { + id: invite_id, + user_uuid: member.user_uuid, + guild_uuid: self.uuid, + }) + } } #[derive(FromRow)] @@ -468,3 +512,31 @@ pub struct Message { user_uuid: Uuid, message: String, } + +#[derive(FromRow)] +pub struct InviteBuilder { + id: String, + user_uuid: String, + guild_uuid: String, +} + +impl InviteBuilder { + fn build(&self) -> Invite { + Invite { + id: self.id.clone(), + user_uuid: Uuid::from_str(&self.user_uuid).unwrap(), + guild_uuid: Uuid::from_str(&self.guild_uuid).unwrap(), + } + } +} + +/// Server invite struct +#[derive(Serialize)] +pub struct Invite { + /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites + id: String, + /// User that created the invite + user_uuid: Uuid, + /// UUID of the guild that the invite belongs to + guild_uuid: Uuid, +} From 8935c2d4964a18bf83fc305db153843f01e3e1ef Mon Sep 17 00:00:00 2001 From: Radical Date: Sat, 10 May 2025 00:09:59 +0200 Subject: [PATCH 29/32] feat: add way to fetch and join invites from /invites/{id} --- src/api/v1/invites/id.rs | 79 +++++++++++++++++++++++++++++++++++++++ src/api/v1/invites/mod.rs | 9 +++++ src/structs.rs | 46 +++++++++++++++++++++-- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/api/v1/invites/id.rs create mode 100644 src/api/v1/invites/mod.rs diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs new file mode 100644 index 0000000..7903b26 --- /dev/null +++ b/src/api/v1/invites/id.rs @@ -0,0 +1,79 @@ +use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; + +use crate::{api::v1::auth::check_access_token, structs::{Guild, Invite, Member}, utils::get_auth_header, Data}; + +#[get("{id}")] +pub async fn get(req: HttpRequest, path: web::Path<(String,)>, 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 invite_id = path.into_inner().0; + + let result = Invite::fetch_one(&data.pool, invite_id).await; + + if let Err(error) = result { + return Ok(error) + } + + let invite = result.unwrap(); + + let guild_result = Guild::fetch_one(&data.pool, invite.guild_uuid).await; + + if let Err(error) = guild_result { + return Ok(error); + } + + let guild = guild_result.unwrap(); + + Ok(HttpResponse::Ok().json(guild)) +} + +#[post("{id}")] +pub async fn join(req: HttpRequest, path: web::Path<(String,)>, 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 invite_id = 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 result = Invite::fetch_one(&data.pool, invite_id).await; + + if let Err(error) = result { + return Ok(error) + } + + let invite = result.unwrap(); + + let guild_result = Guild::fetch_one(&data.pool, invite.guild_uuid).await; + + if let Err(error) = guild_result { + return Ok(error); + } + + let guild = guild_result.unwrap(); + + let member = Member::new(&data.pool, uuid, guild.uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(guild)) +} diff --git a/src/api/v1/invites/mod.rs b/src/api/v1/invites/mod.rs new file mode 100644 index 0000000..89e7466 --- /dev/null +++ b/src/api/v1/invites/mod.rs @@ -0,0 +1,9 @@ +use actix_web::{web, Scope}; + +mod id; + +pub fn web() -> Scope { + web::scope("/invites") + .service(id::get) + .service(id::join) +} diff --git a/src/structs.rs b/src/structs.rs index 1b6ef84..0f72aaf 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -444,7 +444,7 @@ impl Role { pub struct Member { pub uuid: Uuid, - pub nickname: String, + pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, } @@ -475,15 +475,36 @@ impl Member { return Err(HttpResponse::InternalServerError().finish()) } - let (uuid, nickname): (String, String) = row.unwrap(); + let (uuid, nickname): (String, Option) = row.unwrap(); - Ok(Member { + Ok(Self { uuid: Uuid::from_str(&uuid).unwrap(), nickname, user_uuid, guild_uuid, }) } + + pub async fn new(pool: &Pool, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let member_uuid = Uuid::now_v7(); + + let row = sqlx::query(&format!("INSERT INTO guild_members uuid, guild_uuid, user_uuid VALUES ('{}', '{}', '{}')", member_uuid, guild_uuid, user_uuid)) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(Self { + uuid: member_uuid, + nickname: None, + user_uuid, + guild_uuid, + }) + } } #[derive(FromRow)] @@ -538,5 +559,22 @@ pub struct Invite { /// User that created the invite user_uuid: Uuid, /// UUID of the guild that the invite belongs to - guild_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl Invite { + pub async fn fetch_one(pool: &Pool, invite_id: String) -> Result { + let invite: Result = sqlx::query_as("SELECT id, user_uuid, guild_uuid FROM invites WHERE id = $1") + .bind(invite_id) + .fetch_one(pool) + .await; + + if let Err(error) = invite { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(invite.unwrap().build()) + } } From a5ec2704e65e982a6c4b649de29a63f2594aa4e4 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 12 May 2025 00:08:21 +0200 Subject: [PATCH 30/32] style: get object from cache more cleanly --- src/api/v1/servers/uuid/channels/uuid/messages.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs index 70aa10b..eb999be 100644 --- a/src/api/v1/servers/uuid/channels/uuid/messages.rs +++ b/src/api/v1/servers/uuid/channels/uuid/messages.rs @@ -38,22 +38,20 @@ pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, message_reques let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await; - let mut channel_raw: Option = None; + let channel: Channel; if let Ok(cache_hit) = cache_result { - channel_raw = Some(serde_json::from_str(&cache_hit).unwrap()) - } - - if channel_raw.is_none() { + channel = serde_json::from_str(&cache_hit).unwrap() + } else { 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()); + channel = channel_result.unwrap(); - let cache_result = data.set_cache_key(format!("{}", channel_uuid), channel_raw.clone().unwrap(), 60).await; + let cache_result = data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60).await; if let Err(error) = cache_result { error!("{}", error); @@ -61,8 +59,6 @@ pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, message_reques } } - let channel = channel_raw.unwrap(); - let messages = channel.fetch_messages(&data.pool, message_request.amount, message_request.offset).await; if let Err(error) = messages { From 8fdbf8cc488353e64efda9d3b46656eddcddc862 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 12 May 2025 00:09:17 +0200 Subject: [PATCH 31/32] feat: add channel deletion --- src/api/v1/servers/uuid/channels/uuid/mod.rs | 62 +++++++++++++++++++- src/api/v1/servers/uuid/mod.rs | 3 +- src/structs.rs | 14 +++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs index a223ad1..277052c 100644 --- a/src/api/v1/servers/uuid/channels/uuid/mod.rs +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -1,12 +1,12 @@ pub mod messages; -use actix_web::{get, web, Error, HttpRequest, HttpResponse}; +use actix_web::{delete, get, web, Error, HttpRequest, HttpResponse}; use crate::{api::v1::auth::check_access_token, structs::{Channel, Member}, utils::get_auth_header, Data}; use ::uuid::Uuid; use log::error; #[get("{uuid}/channels/{channel_uuid}")] -pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { +pub async fn get(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); @@ -54,3 +54,61 @@ pub async fn res(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Dat Ok(HttpResponse::Ok().json(channel)) } + +#[delete("{uuid}/channels/{channel_uuid}")] +pub async fn delete(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 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 channel: Channel; + + if let Ok(cache_hit) = cache_result { + channel = serde_json::from_str(&cache_hit).unwrap(); + + let result = data.del_cache_key(format!("{}", channel_uuid)).await; + + if let Err(error) = result { + error!("{}", error) + } + } else { + let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await; + + if let Err(error) = channel_result { + return Ok(error) + } + + channel = channel_result.unwrap(); + } + + let delete_result = channel.delete(&data.pool).await; + + if let Err(error) = delete_result { + return Ok(error) + } + + Ok(HttpResponse::Ok().finish()) +} + diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index e9c14a8..0f567d0 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -14,7 +14,8 @@ pub fn web() -> Scope { // Channels .service(channels::response) .service(channels::response_post) - .service(channels::uuid::res) + .service(channels::uuid::get) + .service(channels::uuid::delete) .service(channels::uuid::messages::res) // Roles .service(roles::response) diff --git a/src/structs.rs b/src/structs.rs index 0f72aaf..ac34e9d 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -156,6 +156,20 @@ impl Channel { Ok(channel) } + pub async fn delete(self, pool: &Pool) -> Result<(), HttpResponse> { + let result = sqlx::query(&format!("DELETE FROM channels WHERE channel_uuid = '{}'", self.uuid)) + .execute(pool) + .await; + + if let Err(error) = result { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + Ok(()) + } + 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) From b499ff1cf82a66e1cfef1826804913eae33b4cb5 Mon Sep 17 00:00:00 2001 From: Radical Date: Mon, 12 May 2025 17:18:55 +0200 Subject: [PATCH 32/32] style: use better function names in server --- src/api/v1/servers/uuid/channels/mod.rs | 4 ++-- .../v1/servers/uuid/channels/uuid/messages.rs | 2 +- src/api/v1/servers/uuid/invites/mod.rs | 4 ++-- src/api/v1/servers/uuid/mod.rs | 16 ++++++++-------- src/api/v1/servers/uuid/roles/mod.rs | 4 ++-- src/api/v1/servers/uuid/roles/uuid.rs | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 215f0d2..a5e49ca 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -13,7 +13,7 @@ struct ChannelInfo { } #[get("{uuid}/channels")] -pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn get(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); @@ -63,7 +63,7 @@ pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Dat } #[post("{uuid}/channels")] -pub async fn response_post(req: HttpRequest, channel_info: web::Json, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn create(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); diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs index eb999be..3bfeaaa 100644 --- a/src/api/v1/servers/uuid/channels/uuid/messages.rs +++ b/src/api/v1/servers/uuid/channels/uuid/messages.rs @@ -11,7 +11,7 @@ struct MessageRequest { } #[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 { +pub async fn get(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); diff --git a/src/api/v1/servers/uuid/invites/mod.rs b/src/api/v1/servers/uuid/invites/mod.rs index 2026a1a..94b81b0 100644 --- a/src/api/v1/servers/uuid/invites/mod.rs +++ b/src/api/v1/servers/uuid/invites/mod.rs @@ -10,7 +10,7 @@ struct InviteRequest { } #[get("{uuid}/invites")] -pub async fn get_invites(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn get(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); @@ -53,7 +53,7 @@ pub async fn get_invites(req: HttpRequest, path: web::Path<(Uuid,)>, data: web:: } #[post("{uuid}/invites")] -pub async fn create_invite(req: HttpRequest, path: web::Path<(Uuid,)>, invite_request: web::Json>, data: web::Data) -> Result { +pub async fn create(req: HttpRequest, path: web::Path<(Uuid,)>, invite_request: web::Json>, 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 0f567d0..8c69b92 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -12,18 +12,18 @@ pub fn web() -> Scope { // Servers .service(res) // Channels - .service(channels::response) - .service(channels::response_post) + .service(channels::get) + .service(channels::create) .service(channels::uuid::get) .service(channels::uuid::delete) - .service(channels::uuid::messages::res) + .service(channels::uuid::messages::get) // Roles - .service(roles::response) - .service(roles::response_post) - .service(roles::uuid::res) + .service(roles::get) + .service(roles::create) + .service(roles::uuid::get) // Invites - .service(invites::get_invites) - .service(invites::create_invite) + .service(invites::get) + .service(invites::create) } #[get("/{uuid}")] diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs index 23499de..da73b38 100644 --- a/src/api/v1/servers/uuid/roles/mod.rs +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -12,7 +12,7 @@ struct RoleInfo { } #[get("{uuid}/roles")] -pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn get(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers); @@ -62,7 +62,7 @@ pub async fn response(req: HttpRequest, path: web::Path<(Uuid,)>, data: web::Dat } #[post("{uuid}/roles")] -pub async fn response_post(req: HttpRequest, role_info: web::Json, path: web::Path<(Uuid,)>, data: web::Data) -> Result { +pub async fn create(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); diff --git a/src/api/v1/servers/uuid/roles/uuid.rs b/src/api/v1/servers/uuid/roles/uuid.rs index 3e55d5a..7edcbf0 100644 --- a/src/api/v1/servers/uuid/roles/uuid.rs +++ b/src/api/v1/servers/uuid/roles/uuid.rs @@ -4,7 +4,7 @@ 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 { +pub async fn get(req: HttpRequest, path: web::Path<(Uuid, Uuid)>, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers);