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, +}