diff --git a/Cargo.toml b/Cargo.toml index 4e2f58d..f4cfea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde_json = "1.0" simple_logger = "5.0.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } redis = { version = "0.30", features= ["tokio-comp"] } +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"] } diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index a5fd58a..36bde4a 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -3,10 +3,12 @@ use actix_web::{Scope, web}; mod auth; mod stats; mod users; +mod servers; pub fn web() -> Scope { web::scope("/v1") .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 new file mode 100644 index 0000000..31ede19 --- /dev/null +++ b/src/api/v1/servers/mod.rs @@ -0,0 +1,46 @@ +use actix_web::{post, web, Error, HttpRequest, HttpResponse, Scope}; +use serde::Deserialize; + +mod uuid; + +use crate::{api::v1::auth::check_access_token, structs::Guild, utils::get_auth_header, Data}; + +#[derive(Deserialize)] +struct GuildInfo { + name: String, + description: Option, +} + +pub fn web() -> Scope { + web::scope("/servers") + .service(res) + .service(uuid::web()) +} + +#[post("")] +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); + + 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 guild = Guild::new(&data.pool, guild_info.name.clone(), guild_info.description.clone(), uuid).await; + + if let Err(error) = guild { + return Ok(error) + } + + 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 new file mode 100644 index 0000000..215f0d2 --- /dev/null +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -0,0 +1,100 @@ +use actix_web::{get, post, 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; + +pub mod uuid; + +#[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(); + + 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!("{}_channels", guild_uuid)).await; + + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) + } + + let channels_result = Channel::fetch_all(&data.pool, guild_uuid).await; + + if let Err(error) = channels_result { + return Ok(error) + } + + 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)) +} + +#[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 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 channel = Channel::new(data.clone(), guild_uuid, channel_info.name.clone(), channel_info.description.clone()).await; + + if let Err(error) = channel { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(channel.unwrap())) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs new file mode 100644 index 0000000..70aa10b --- /dev/null +++ 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/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs new file mode 100644 index 0000000..a223ad1 --- /dev/null +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -0,0 +1,56 @@ +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}; +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 { + 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; + + 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)) +} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs new file mode 100644 index 0000000..ffa1ae7 --- /dev/null +++ b/src/api/v1/servers/uuid/mod.rs @@ -0,0 +1,55 @@ +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}; + +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}")] +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 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 = Guild::fetch_one(&data.pool, guild_uuid).await; + + if let Err(error) = guild { + return Ok(error); + } + + Ok(HttpResponse::Ok().json(guild.unwrap())) +} + 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)) +} diff --git a/src/main.rs b/src/main.rs index 7f21e2e..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, @@ -50,17 +51,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 @@ -76,12 +89,73 @@ async fn main() -> Result<(), Error> { 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_at int8 NOT NULL - ) + ); + CREATE TABLE IF NOT EXISTS guilds ( + uuid uuid PRIMARY KEY 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 ( + 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 (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 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) ON DELETE CASCADE, + user_uuid uuid NOT NULL REFERENCES users(uuid), + 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) ON DELETE SET NULL, + deleted boolean DEFAULT FALSE + ); + 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) + ) + */ + let data = Data { pool, cache_pool, diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..790889b --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,470 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use sqlx::{prelude::FromRow, Pool, Postgres}; +use uuid::Uuid; +use actix_web::HttpResponse; +use log::error; + +use crate::Data; + +#[derive(Serialize, Deserialize, 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, Deserialize, 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) + } + + 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)] +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(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, + permissions: i64, +} + +impl Role { + pub async fn fetch_all(pool: &Pool, guild_uuid: Uuid) -> Result, HttpResponse> { + 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) = role_builders_result { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + 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) + } +} + +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, + }) + } +} + +#[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, +} diff --git a/src/utils.rs b/src/utils.rs index 15e5e2e..424b10f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -70,5 +70,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 + } }