From 1f1d9613e2be5caf1229b2e01d2fcb2dcbc493e1 Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Sun, 6 Jun 2021 14:57:55 +0100 Subject: [PATCH] Servers: Fetch and edit members. Permissions: Use bit representation for permissions. --- src/database/entities/server.rs | 2 +- src/database/guards/reference.rs | 22 +++ src/database/permissions/channel.rs | 12 +- src/database/permissions/server.rs | 18 ++- src/database/permissions/user.rs | 8 +- src/notifications/events.rs | 14 +- src/routes/servers/member_edit.rs | 134 ++++++++++++++++++ .../{members_fetch.rs => member_fetch.rs} | 10 +- src/routes/servers/member_fetch_all.rs | 51 +++++++ src/routes/servers/mod.rs | 8 +- 10 files changed, 256 insertions(+), 23 deletions(-) create mode 100644 src/routes/servers/member_edit.rs rename src/routes/servers/{members_fetch.rs => member_fetch.rs} (55%) create mode 100644 src/routes/servers/member_fetch_all.rs diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs index c9d5b6c..cdea974 100644 --- a/src/database/entities/server.rs +++ b/src/database/entities/server.rs @@ -207,7 +207,7 @@ impl Server { Ok(()) } - pub async fn fetch_members(id: &str) -> Result<Vec<String>> { + pub async fn fetch_member_ids(id: &str) -> Result<Vec<String>> { Ok(get_collection("server_members") .find( doc! { diff --git a/src/database/guards/reference.rs b/src/database/guards/reference.rs index 513042b..38a2be0 100644 --- a/src/database/guards/reference.rs +++ b/src/database/guards/reference.rs @@ -62,6 +62,28 @@ impl Ref { self.fetch("invites").await } + pub async fn fetch_member(&self, server: &str) -> Result<Member> { + let doc = get_collection("server_members") + .find_one( + doc! { + "_id.user": &self.id, + "_id.server": server + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "find_one", + with: "server_member", + })? + .ok_or_else(|| Error::NotFound)?; + + Ok(from_document::<Member>(doc).map_err(|_| Error::DatabaseError { + operation: "from_document", + with: "server_member", + })?) + } + pub async fn fetch_message(&self, channel: &Channel) -> Result<Message> { let message: Message = self.fetch("messages").await?; if &message.channel != channel.id() { diff --git a/src/database/permissions/channel.rs b/src/database/permissions/channel.rs index 83b8b21..1f30288 100644 --- a/src/database/permissions/channel.rs +++ b/src/database/permissions/channel.rs @@ -9,12 +9,12 @@ use std::ops; #[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)] #[repr(u32)] pub enum ChannelPermission { - View = 1, - SendMessage = 2, - ManageMessages = 4, - ManageChannel = 8, - VoiceCall = 16, - InviteOthers = 32, + View = 0b00000000000000000000000000000001, // 1 + SendMessage = 0b00000000000000000000000000000010, // 2 + ManageMessages = 0b00000000000000000000000000000100, // 4 + ManageChannel = 0b00000000000000000000000000001000, // 8 + VoiceCall = 0b00000000000000000000000000010000, // 16 + InviteOthers = 0b00000000000000000000000000100000, // 32 } impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 }); diff --git a/src/database/permissions/server.rs b/src/database/permissions/server.rs index b232100..2e73aee 100644 --- a/src/database/permissions/server.rs +++ b/src/database/permissions/server.rs @@ -8,8 +8,15 @@ use std::ops; #[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)] #[repr(u32)] pub enum ServerPermission { - View = 1, - ManageServer = 8, + View = 0b00000000000000000000000000000001, // 1 + // 2 bits of space + ManageServer = 0b00000000000000000000000000001000, // 8 + // 8 bits of space + ChangeNickname = 0b00000000000000000001000000000000, // 4096 + ManageNicknames = 0b00000000000000000010000000000000, // 8192 + ChangeAvatar = 0b00000000000000000100000000000000, // 16392 + RemoveAvatars = 0b00000000000000001000000000000000, // 32784 + // 16 bits of space } impl_op_ex!(+ |a: &ServerPermission, b: &ServerPermission| -> u32 { *a as u32 | *b as u32 }); @@ -20,6 +27,11 @@ bitfield! { u32; pub get_view, _: 31; pub get_manage_server, _: 28; + + pub get_change_nickname, _: 19; + pub get_manage_nicknames, _: 18; + pub get_change_avatar, _: 17; + pub get_remove_avatars, _: 16; } impl<'a> PermissionCalculator<'a> { @@ -33,7 +45,7 @@ impl<'a> PermissionCalculator<'a> { if self.perspective.id == server.owner { Ok(u32::MAX) } else { - Ok(ServerPermission::View as u32) + Ok(ServerPermission::View + ServerPermission::ChangeNickname + ServerPermission::ChangeAvatar) } } diff --git a/src/database/permissions/user.rs b/src/database/permissions/user.rs index c4d523a..11f9fc9 100644 --- a/src/database/permissions/user.rs +++ b/src/database/permissions/user.rs @@ -10,10 +10,10 @@ use std::ops; #[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)] #[repr(u32)] pub enum UserPermission { - Access = 1, - ViewProfile = 2, - SendMessage = 4, - Invite = 8, + Access = 0b00000000000000000000000000000001, // 1 + ViewProfile = 0b00000000000000000000000000000010, // 2 + SendMessage = 0b00000000000000000000000000000100, // 4 + Invite = 0b00000000000000000000000000001000, // 8 } bitfield! { diff --git a/src/notifications/events.rs b/src/notifications/events.rs index 06a19b5..4d7ec25 100644 --- a/src/notifications/events.rs +++ b/src/notifications/events.rs @@ -50,6 +50,12 @@ pub enum RemoveServerField { Description, } +#[derive(Serialize, Deserialize, Debug)] +pub enum RemoveMemberField { + Nickname, + Avatar +} + #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] pub enum ClientboundNotification { @@ -109,6 +115,12 @@ pub enum ClientboundNotification { ServerDelete { id: String, }, + ServerMemberUpdate { + id: MemberCompositeKey, + data: JsonValue, + #[serde(skip_serializing_if = "Option::is_none")] + clear: Option<RemoveMemberField>, + }, ServerMemberJoin { id: String, user: String, @@ -168,7 +180,7 @@ pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<() } Channel::TextChannel { server, .. } => { // ! FIXME: write a better algorithm? - let members = Server::fetch_members(server).await?; + let members = Server::fetch_member_ids(server).await?; for member in members { subscribe_if_exists(member.clone(), channel_id.to_string()).ok(); } diff --git a/src/routes/servers/member_edit.rs b/src/routes/servers/member_edit.rs new file mode 100644 index 0000000..29f930b --- /dev/null +++ b/src/routes/servers/member_edit.rs @@ -0,0 +1,134 @@ +use crate::notifications::events::ClientboundNotification; +use crate::util::result::{Error, Result}; +use crate::{database::*, notifications::events::RemoveMemberField}; + +use mongodb::bson::{doc, to_document}; +use rocket_contrib::json::Json; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Validate, Serialize, Deserialize)] +pub struct Data { + #[validate(length(min = 1, max = 32))] + nickname: Option<String>, + avatar: Option<String>, + remove: Option<RemoveMemberField>, +} + +#[patch("/<server>/members/<target>", data = "<data>")] +pub async fn req(user: User, server: Ref, target: String, data: Json<Data>) -> Result<()> { + let data = data.into_inner(); + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + if data.nickname.is_none() && data.avatar.is_none() && data.remove.is_none() + { + return Ok(()); + } + + let server = server.fetch_server().await?; + let target = Ref::from(target)?.fetch_member(&server.id).await?; + + let perm = permissions::PermissionCalculator::new(&user) + .with_server(&server) + .for_server() + .await?; + + if target.id.user == user.id { + if (data.nickname.is_some() && !perm.get_change_nickname()) || + (data.avatar.is_some() && !perm.get_change_avatar()) { + return Err(Error::MissingPermission) + } + + if let Some(remove) = &data.remove { + if match remove { + RemoveMemberField::Avatar => !perm.get_change_avatar(), + RemoveMemberField::Nickname => !perm.get_change_nickname() + } { + return Err(Error::MissingPermission) + } + } + } else { + if data.avatar.is_some() || (data.nickname.is_some() && !perm.get_manage_nicknames()) { + return Err(Error::MissingPermission) + } + + if let Some(remove) = &data.remove { + if match remove { + RemoveMemberField::Avatar => !perm.get_remove_avatars(), + RemoveMemberField::Nickname => !perm.get_manage_nicknames() + } { + return Err(Error::MissingPermission) + } + } + } + + let mut set = doc! {}; + let mut unset = doc! {}; + + let mut remove_avatar = false; + if let Some(remove) = &data.remove { + match remove { + RemoveMemberField::Avatar => { + unset.insert("avatar", 1); + remove_avatar = true; + } + RemoveMemberField::Nickname => { + unset.insert("nickname", 1); + } + } + } + + if let Some(name) = &data.nickname { + set.insert("nickname", name); + } + + if let Some(attachment_id) = &data.avatar { + let attachment = File::find_and_use(&attachment_id, "avatars", "user", &target.id.user).await?; + set.insert( + "avatar", + to_document(&attachment).map_err(|_| Error::DatabaseError { + operation: "to_document", + with: "attachment", + })?, + ); + + remove_avatar = true; + } + + let mut operations = doc! {}; + if set.len() > 0 { + operations.insert("$set", &set); + } + + if unset.len() > 0 { + operations.insert("$unset", unset); + } + + if operations.len() > 0 { + get_collection("server_members") + .update_one(doc! { "_id.server": &server.id, "_id.user": &target.id.user }, operations, None) + .await + .map_err(|_| Error::DatabaseError { + operation: "update_one", + with: "server_member", + })?; + } + + ClientboundNotification::ServerMemberUpdate { + id: target.id.clone(), + data: json!(set), + clear: data.remove, + } + .publish(server.id.clone()); + + let Member { avatar, .. } = target; + + if remove_avatar { + if let Some(old_avatar) = avatar { + old_avatar.delete().await?; + } + } + + Ok(()) +} diff --git a/src/routes/servers/members_fetch.rs b/src/routes/servers/member_fetch.rs similarity index 55% rename from src/routes/servers/members_fetch.rs rename to src/routes/servers/member_fetch.rs index dfe9a37..2f8d21e 100644 --- a/src/routes/servers/members_fetch.rs +++ b/src/routes/servers/member_fetch.rs @@ -1,12 +1,11 @@ use crate::database::*; use crate::util::result::{Error, Result}; +use mongodb::bson::doc; use rocket_contrib::json::JsonValue; -// ! FIXME: this is a temporary route while permissions are being worked on. - -#[get("/<target>/members")] -pub async fn req(user: User, target: Ref) -> Result<JsonValue> { +#[get("/<target>/members/<member>")] +pub async fn req(user: User, target: Ref, member: String) -> Result<JsonValue> { let target = target.fetch_server().await?; let perm = permissions::PermissionCalculator::new(&user) @@ -18,6 +17,5 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { Err(Error::MissingPermission)? } - let members = Server::fetch_members(&target.id).await?; - Ok(json!(user.fetch_multiple_users(members).await?)) + Ok(json!(Ref::from(member)?.fetch_member(&target.id).await?)) } diff --git a/src/routes/servers/member_fetch_all.rs b/src/routes/servers/member_fetch_all.rs new file mode 100644 index 0000000..c12c3d1 --- /dev/null +++ b/src/routes/servers/member_fetch_all.rs @@ -0,0 +1,51 @@ +use crate::database::*; +use crate::util::result::{Error, Result}; + +use futures::StreamExt; +use rocket_contrib::json::JsonValue; +use mongodb::bson::{Document, doc, from_document}; + +// ! FIXME: this is a temporary route while permissions are being worked on. + +#[get("/<target>/members")] +pub async fn req(user: User, target: Ref) -> Result<JsonValue> { + let target = target.fetch_server().await?; + + let perm = permissions::PermissionCalculator::new(&user) + .with_server(&target) + .for_server() + .await?; + + if !perm.get_view() { + Err(Error::MissingPermission)? + } + + let members = get_collection("server_members") + .find( + doc! { + "_id.server": target.id + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "find", + with: "server_members", + })? + .filter_map(async move |s| s.ok()) + .collect::<Vec<Document>>() + .await + .into_iter() + .filter_map(|x| from_document(x).ok()) + .collect::<Vec<Member>>(); + + let member_ids = members + .iter() + .map(|m| m.id.user.clone()) + .collect::<Vec<String>>(); + + Ok(json!({ + "members": members, + "users": user.fetch_multiple_users(member_ids).await? + })) +} diff --git a/src/routes/servers/mod.rs b/src/routes/servers/mod.rs index bfc704f..195081e 100644 --- a/src/routes/servers/mod.rs +++ b/src/routes/servers/mod.rs @@ -7,7 +7,9 @@ mod server_edit; mod channel_create; -mod members_fetch; +mod member_fetch_all; +mod member_fetch; +mod member_edit; mod invites_fetch; @@ -18,7 +20,9 @@ pub fn routes() -> Vec<Route> { server_fetch::req, server_edit::req, channel_create::req, - members_fetch::req, + member_fetch_all::req, + member_fetch::req, + member_edit::req, invites_fetch::req ] } -- GitLab