From d1b44a311f3a5498c6b105b341d43510a181742e Mon Sep 17 00:00:00 2001 From: Paul Makles <paulmakles@gmail.com> Date: Sat, 11 Apr 2020 21:48:10 +0100 Subject: [PATCH] Add invite creation, removal, fetch and usage. --- src/database/guild.rs | 4 +- src/database/permissions.rs | 2 +- src/guards/channel.rs | 47 +++++---- src/guards/guild.rs | 46 ++++++++- src/routes/account.rs | 9 +- src/routes/guild.rs | 188 +++++++++++++++++++++++++++++++++++- src/routes/mod.rs | 5 + src/util/mod.rs | 8 ++ 8 files changed, 271 insertions(+), 38 deletions(-) diff --git a/src/database/guild.rs b/src/database/guild.rs index 5ae9bc2..bbce3b1 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 3b34c02..29ebdbf 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 a05db4f..ace3096 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<String>, pub last_message: Option<LastMessage>, // information required for permission calculations @@ -25,6 +26,31 @@ pub struct ChannelRef { } impl ChannelRef { + pub fn from(id: String) -> Option<ChannelRef> { + 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<Document> { 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<Self, Self::Error> { - 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 cf23d50..9ca04cf 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<Member> { +pub fn get_member(guild_id: &String, member: &String) -> Option<Member> { 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<Member> { 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 eb854d6..7ea4610 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::<String>() -} - #[derive(Serialize, Deserialize)] pub struct Create { username: String, diff --git a/src/routes/guild.rs b/src/routes/guild.rs index 5fbb9fb..1e0e74b 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("/<target>/channels/<channel>/invite", data = "<_options>")] +pub fn create_invite( + user: UserRef, + target: GuildRef, + channel: ChannelRef, + _options: Json<InviteOptions>, +) -> Option<Response> { + 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("/<target>/invites/<code>")] +pub fn remove_invite(user: UserRef, target: GuildRef, code: String) -> Option<Response> { + 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("/<target>/invites")] +pub fn fetch_invites(user: UserRef, target: GuildRef) -> Option<Response> { + 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/<code>", 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/<code>", 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<Response> { pub fn fetch_member(user: UserRef, target: GuildRef, other: String) -> Option<Response> { 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<Res return Some(Response::LackingPermission(Permission::KickMembers)); } - if get_member(&target, &other).is_none() { + if get_member(&target.id, &other).is_none() { return Some(Response::BadRequest( json!({ "error": "User not part of guild." }), )); @@ -406,7 +584,7 @@ pub fn ban_member( return Some(Response::LackingPermission(Permission::BanMembers)); } - if get_member(&target, &other).is_none() { + if get_member(&target.id, &other).is_none() { return Some(Response::BadRequest( json!({ "error": "User not part of guild." }), )); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 6e68b41..c847d79 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -107,6 +107,11 @@ pub fn mount(rocket: Rocket) -> 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 c2f6c42..038b896 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<T: Clone + Eq + std::hash::Hash>(data: &[T]) -> HashSet<T> { HashSet::from_iter(data.iter().cloned()) } + +pub fn gen_token(l: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(l) + .collect::<String>() +} -- GitLab