diff --git a/src/database/guild.rs b/src/database/guild.rs index 5ae9bc27..bbce3b17 100644 --- a/src/database/guild.rs +++ b/src/database/guild.rs @@ -16,8 +16,8 @@ pub struct Member { #[derive(Serialize, Deserialize, Debug)] pub struct Invite { - pub id: String, - pub custom: bool, + pub code: String, + pub creator: String, pub channel: String, } diff --git a/src/database/permissions.rs b/src/database/permissions.rs index 3b34c020..29ebdbfd 100644 --- a/src/database/permissions.rs +++ b/src/database/permissions.rs @@ -137,7 +137,7 @@ impl PermissionCalculator { }; if let Some(guild) = &guild { - self.member = get_member(guild, &self.user.id); + self.member = get_member(&guild.id, &self.user.id); } self.guild = guild; diff --git a/src/guards/channel.rs b/src/guards/channel.rs index a05db4f9..ace30967 100644 --- a/src/guards/channel.rs +++ b/src/guards/channel.rs @@ -16,6 +16,7 @@ pub struct ChannelRef { #[serde(rename = "type")] pub channel_type: u8, + pub name: Option, pub last_message: Option, // information required for permission calculations @@ -25,6 +26,31 @@ pub struct ChannelRef { } impl ChannelRef { + pub fn from(id: String) -> Option { + match database::get_collection("channels").find_one( + doc! { "_id": id }, + FindOneOptions::builder() + .projection(doc! { + "_id": 1, + "type": 1, + "name": 1, + "last_message": 1, + "recipients": 1, + "guild": 1, + "owner": 1, + }) + .build(), + ) { + Ok(result) => match result { + Some(doc) => { + Some(from_bson(bson::Bson::Document(doc)).expect("Failed to unwrap channel.")) + } + None => None, + }, + Err(_) => None, + } + } + pub fn fetch_data(&self, projection: Document) -> Option { database::get_collection("channels") .find_one( @@ -39,25 +65,8 @@ impl<'r> FromParam<'r> for ChannelRef { type Error = &'r RawStr; fn from_param(param: &'r RawStr) -> Result { - let id = param.to_string(); - let result = database::get_collection("channels") - .find_one( - doc! { "_id": id }, - FindOneOptions::builder() - .projection(doc! { - "_id": 1, - "type": 1, - "last_message": 1, - "recipients": 1, - "guild": 1, - "owner": 1, - }) - .build(), - ) - .unwrap(); - - if let Some(channel) = result { - Ok(from_bson(bson::Bson::Document(channel)).expect("Failed to deserialize channel.")) + if let Some(channel) = ChannelRef::from(param.to_string()) { + Ok(channel) } else { Err(param) } diff --git a/src/guards/guild.rs b/src/guards/guild.rs index cf23d509..9ca04cf2 100644 --- a/src/guards/guild.rs +++ b/src/guards/guild.rs @@ -5,7 +5,7 @@ use rocket::request::FromParam; use serde::{Deserialize, Serialize}; use crate::database; -use crate::database::guild::{Ban, Member}; +use crate::database::guild::{Ban, Invite, Member}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GuildRef { @@ -76,10 +76,10 @@ impl<'r> FromParam<'r> for GuildRef { } } -pub fn get_member(guild: &GuildRef, member: &String) -> Option { +pub fn get_member(guild_id: &String, member: &String) -> Option { if let Ok(result) = database::get_collection("members").find_one( doc! { - "_id.guild": &guild.id, + "_id.guild": &guild_id, "_id.user": &member, }, None, @@ -93,3 +93,43 @@ pub fn get_member(guild: &GuildRef, member: &String) -> Option { None } } + +pub fn get_invite(code: &String) -> Option<(String, String, Invite)> { + if let Ok(result) = database::get_collection("guilds").find_one( + doc! { + "invites": { + "$elemMatch": { + "code": &code + } + } + }, + FindOneOptions::builder() + .projection(doc! { + "_id": 1, + "name": 1, + "invites.$": 1, + }) + .build(), + ) { + if let Some(doc) = result { + let invite = doc + .get_array("invites") + .unwrap() + .iter() + .next() + .unwrap() + .as_document() + .unwrap(); + + Some(( + doc.get_str("_id").unwrap().to_string(), + doc.get_str("name").unwrap().to_string(), + from_bson(Bson::Document(invite.clone())).unwrap(), + )) + } else { + None + } + } else { + None + } +} diff --git a/src/routes/account.rs b/src/routes/account.rs index eb854d69..7ea46100 100644 --- a/src/routes/account.rs +++ b/src/routes/account.rs @@ -1,24 +1,17 @@ use super::Response; use crate::database; use crate::email; +use crate::util::gen_token; use bcrypt::{hash, verify}; use bson::{doc, from_bson, Bson::UtcDatetime}; use chrono::prelude::*; use database::user::User; -use rand::{distributions::Alphanumeric, Rng}; use rocket_contrib::json::Json; use serde::{Deserialize, Serialize}; use ulid::Ulid; use validator::validate_email; -fn gen_token(l: usize) -> String { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(l) - .collect::() -} - #[derive(Serialize, Deserialize)] pub struct Create { username: String, diff --git a/src/routes/guild.rs b/src/routes/guild.rs index 5fbb9fbb..1e0e74ba 100644 --- a/src/routes/guild.rs +++ b/src/routes/guild.rs @@ -2,10 +2,12 @@ use super::channel::ChannelType; use super::Response; use crate::database::{self, channel::Channel, Permission, PermissionCalculator}; use crate::guards::auth::UserRef; -use crate::guards::guild::{get_member, GuildRef}; +use crate::guards::channel::ChannelRef; +use crate::guards::guild::{get_invite, get_member, GuildRef}; +use crate::util::gen_token; use bson::{doc, from_bson, Bson}; -use mongodb::options::FindOptions; +use mongodb::options::{FindOneOptions, FindOptions}; use rocket::request::Form; use rocket_contrib::json::Json; use serde::{Deserialize, Serialize}; @@ -194,6 +196,182 @@ pub fn create_channel( } } +#[derive(Serialize, Deserialize)] +pub struct InviteOptions { + // ? TODO: add options +} + +/// create a new invite +#[post("//channels//invite", data = "<_options>")] +pub fn create_invite( + user: UserRef, + target: GuildRef, + channel: ChannelRef, + _options: Json, +) -> Option { + let (permissions, _) = with_permissions!(user, target); + + if !permissions.get_create_invite() { + return Some(Response::LackingPermission(Permission::CreateInvite)); + } + + let code = gen_token(7); + if database::get_collection("guilds") + .update_one( + doc! { "_id": target.id }, + doc! { + "$push": { + "invites": { + "code": &code, + "creator": user.id, + "channel": channel.id, + } + } + }, + None, + ) + .is_ok() + { + Some(Response::Success(json!({ "code": code }))) + } else { + Some(Response::BadRequest( + json!({ "error": "Failed to create invite." }), + )) + } +} + +/// remove an invite +#[delete("//invites/")] +pub fn remove_invite(user: UserRef, target: GuildRef, code: String) -> Option { + let (permissions, _) = with_permissions!(user, target); + + if let Some((guild_id, _, invite)) = get_invite(&code) { + if invite.creator != user.id { + if !permissions.get_manage_server() { + return Some(Response::LackingPermission(Permission::ManageServer)); + } + } + + if database::get_collection("guilds") + .update_one( + doc! { + "_id": &guild_id, + }, + doc! { + "$pull": { + "invites": { + "code": &code + } + } + }, + None, + ) + .is_ok() + { + Some(Response::Result(super::Status::Ok)) + } else { + Some(Response::BadRequest( + json!({ "error": "Failed to delete invite." }), + )) + } + } else { + Some(Response::NotFound( + json!({ "error": "Failed to fetch invite or code is invalid." }), + )) + } +} + +/// fetch all guild invites +#[get("//invites")] +pub fn fetch_invites(user: UserRef, target: GuildRef) -> Option { + let (permissions, _) = with_permissions!(user, target); + + if !permissions.get_manage_server() { + return Some(Response::LackingPermission(Permission::ManageServer)); + } + + if let Some(doc) = target.fetch_data(doc! { + "invites": 1, + }) { + Some(Response::Success(json!(doc.get_array("invites").unwrap()))) + } else { + Some(Response::InternalServerError( + json!({ "error": "Failed to fetch invites." }), + )) + } +} + +/// view an invite before joining +#[get("/join/", rank = 1)] +pub fn fetch_invite(_user: UserRef, code: String) -> Response { + if let Some((guild_id, name, invite)) = get_invite(&code) { + if let Some(channel) = ChannelRef::from(invite.channel) { + Response::Success(json!({ + "guild": { + "id": guild_id, + "name": name, + }, + "channel": { + "id": channel.id, + "name": channel.name, + } + })) + } else { + Response::BadRequest(json!({ "error": "Failed to fetch channel." })) + } + } else { + Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." })) + } +} + +/// join a guild using an invite +#[post("/join/", rank = 1)] +pub fn use_invite(user: UserRef, code: String) -> Response { + if let Some((guild_id, _, invite)) = get_invite(&code) { + if let Ok(result) = database::get_collection("members").find_one( + doc! { + "_id.guild": &guild_id, + "_id.user": &user.id + }, + FindOneOptions::builder() + .projection(doc! { "_id": 1 }) + .build(), + ) { + if result.is_none() { + if database::get_collection("members") + .insert_one( + doc! { + "_id": { + "guild": &guild_id, + "user": &user.id + } + }, + None, + ) + .is_ok() + { + Response::Success(json!({ + "guild": &guild_id, + "channel": &invite.channel, + })) + } else { + Response::InternalServerError( + json!({ "error": "Failed to add you to the guild." }), + ) + } + } else { + Response::BadRequest(json!({ "error": "Already in the guild." })) + } + } else { + Response::InternalServerError( + json!({ "error": "Failed to check if you're in the guild." }), + ) + } + } else { + Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." })) + } +} + #[derive(Serialize, Deserialize)] pub struct CreateGuild { name: String, @@ -323,7 +501,7 @@ pub fn fetch_members(user: UserRef, target: GuildRef) -> Option { pub fn fetch_member(user: UserRef, target: GuildRef, other: String) -> Option { with_permissions!(user, target); - if let Some(member) = get_member(&target, &other) { + if let Some(member) = get_member(&target.id, &other) { Some(Response::Success(json!({ "id": member.id.user, "nickname": member.nickname, @@ -350,7 +528,7 @@ pub fn kick_member(user: UserRef, target: GuildRef, other: String) -> Option Rocket { guild::my_guilds, guild::guild, guild::create_channel, + guild::create_invite, + guild::remove_invite, + guild::fetch_invites, + guild::fetch_invite, + guild::use_invite, guild::create_guild, guild::fetch_members, guild::fetch_member, diff --git a/src/util/mod.rs b/src/util/mod.rs index c2f6c42c..038b896d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,6 +1,14 @@ use hashbrown::HashSet; +use rand::{distributions::Alphanumeric, Rng}; use std::iter::FromIterator; pub fn vec_to_set(data: &[T]) -> HashSet { HashSet::from_iter(data.iter().cloned()) } + +pub fn gen_token(l: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(l) + .collect::() +}