Compare commits

...
16 Commits
Author SHA1 Message Date
Paul Makles 229d4e2e1d fix: correct ordering for creating ownership change event 2022-07-15 21:46:41 +01:00
Paul Makles 7bdd2d69d6 feat: add message reactions + interactions object 2022-07-15 21:24:49 +01:00
Paul Makles ab2af9b5e5 feat: add group ownership transfer 2022-07-15 17:58:08 +01:00
Paul Makles 5aebda2d16 chore: restrict emojis to lowercase 2022-07-15 17:57:53 +01:00
Paul Makles a7273d0556 feat: allow servers to set discoverable value 2022-07-15 17:57:30 +01:00
Paul Makles 72aed64e5b chore: allow banning users not in server 2022-07-15 16:47:27 +01:00
Paul Makles 00ad4d54e7 feat: add nsfw flag to emotes 2022-07-15 16:34:08 +01:00
Paul Makles cba8c4ef3b fix: add case-insensitive flag to colour regex 2022-07-15 16:13:04 +01:00
Paul Makles b6e395fe0d fix: use default for roles key 2022-07-15 16:02:58 +01:00
Paul Makles 8d07457c58 chore: make joined_at migration shorter 2022-07-15 16:02:46 +01:00
Paul Makles f1171e5358 feat: restrict role colours to regex (increase length limit) 2022-07-15 15:36:00 +01:00
Paul Makles d96c9f62c4 chore: detach emojis on delete 2022-07-15 14:01:59 +01:00
Paul Makles 4f73e43a03 feat: restrict permissions for users in timeout 2022-07-15 13:55:55 +01:00
Paul Makles 741b8ee8fd feat: add role colours to masquerades
feat: add `joined_at` property to members
feat: add `timeout` property to members (non-functional)
2022-07-15 13:42:01 +01:00
Paul Makles 692081b7f0 chore: bump rauth for verification fix 2022-07-14 14:01:10 +01:00
Paul Makles 943b1f08f0 fix: prevent short-circuit on permissions 2022-07-10 18:27:22 +01:00
46 changed files with 2585 additions and 101 deletions
Generated
+7 -6
View File
@@ -1995,9 +1995,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -2505,7 +2505,7 @@ dependencies = [
[[package]] [[package]]
name = "rauth" name = "rauth"
version = "1.0.0" version = "1.0.0"
source = "git+https://github.com/insertish/rauth?rev=c2acaf6b31213e3969f8b48f9deed3daa125d394#c2acaf6b31213e3969f8b48f9deed3daa125d394" source = "git+https://github.com/insertish/rauth?rev=9c3f9bb58a549a51546bd748274130ac4a667cb4#9c3f9bb58a549a51546bd748274130ac4a667cb4"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -2714,7 +2714,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "1.0.6-patch.2" version = "0.5.5"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
@@ -2730,7 +2730,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.4" version = "0.5.5"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-std", "async-std",
@@ -2792,6 +2792,7 @@ dependencies = [
"nanoid", "nanoid",
"num_enum", "num_enum",
"okapi", "okapi",
"once_cell",
"optional_struct", "optional_struct",
"pretty_env_logger", "pretty_env_logger",
"rauth", "rauth",
@@ -2986,7 +2987,7 @@ dependencies = [
[[package]] [[package]]
name = "rocket_rauth" name = "rocket_rauth"
version = "1.0.0" version = "1.0.0"
source = "git+https://github.com/insertish/rauth?rev=c2acaf6b31213e3969f8b48f9deed3daa125d394#c2acaf6b31213e3969f8b48f9deed3daa125d394" source = "git+https://github.com/insertish/rauth?rev=9c3f9bb58a549a51546bd748274130ac4a667cb4#9c3f9bb58a549a51546bd748274130ac4a667cb4"
dependencies = [ dependencies = [
"okapi", "okapi",
"rauth", "rauth",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "1.0.6-patch.2" version = "0.5.5"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.4" version = "0.5.5"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -52,7 +52,7 @@ mobc-redis = { version = "0.7.0", default-features = false, features = ["async-s
# web # web
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] } rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "master" } rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "master" }
rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "c2acaf6b31213e3969f8b48f9deed3daa125d394" } rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "9c3f9bb58a549a51546bd748274130ac4a667cb4" }
# spec generation # spec generation
schemars = "0.8.8" schemars = "0.8.8"
@@ -16,19 +16,18 @@ use validator::Validate;
pub struct DataEditChannel { pub struct DataEditChannel {
/// Channel name /// Channel name
#[validate(length(min = 1, max = 32))] #[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>, name: Option<String>,
/// Channel description /// Channel description
#[validate(length(min = 0, max = 1024))] #[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>, description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon /// Icon
/// ///
/// Provide an Autumn attachment Id. /// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))] #[validate(length(min = 1, max = 128))]
icon: Option<String>, icon: Option<String>,
/// Whether this channel is age-restricted /// Whether this channel is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>, nsfw: Option<bool>,
#[validate(length(min = 1))] #[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>, remove: Option<Vec<FieldsChannel>>,
@@ -59,12 +58,47 @@ pub async fn req(
&& data.description.is_none() && data.description.is_none()
&& data.icon.is_none() && data.icon.is_none()
&& data.nsfw.is_none() && data.nsfw.is_none()
&& data.owner.is_none()
&& data.remove.is_none() && data.remove.is_none()
{ {
return Ok(Json(channel)); return Ok(Json(channel));
} }
let mut partial: PartialChannel = Default::default(); let mut partial: PartialChannel = Default::default();
// Transfer group ownership
if let Some(new_owner) = data.owner {
if let Channel::Group {
owner, recipients, ..
} = &mut channel
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
}
// Transfer ownership
let old_owner = std::mem::replace(owner, new_owner.to_string());
// Notify clients
SystemMessage::ChannelOwnershipChanged {
from: old_owner,
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
match &mut channel { match &mut channel {
Channel::Group { Channel::Group {
id, id,
@@ -0,0 +1,34 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
};
/// # Remove All Reactions from Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
// Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?;
// Clear reactions
message
.update(
db,
PartialMessage {
reactions: Some(Default::default()),
..Default::default()
},
)
.await
.map(|_| EmptyResponse)
}
@@ -0,0 +1,29 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
/// # Add Reaction to Message
///
/// React to a given message.
#[openapi(tag = "Interactions")]
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
pub async fn react_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Add the reaction
message
.add_reaction(db, &user, &emoji.id)
.await
.map(|_| EmptyResponse)
}
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_quark::{ use revolt_quark::{
models::{ models::{
message::{Masquerade, Reply, SendableEmbed}, message::{Interactions, Masquerade, Reply, SendableEmbed},
Message, User, Message, User,
}, },
perms, perms,
@@ -40,6 +40,8 @@ pub struct DataMessageSend {
/// Masquerade to apply to this message /// Masquerade to apply to this message
#[validate] #[validate]
masquerade: Option<Masquerade>, masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
interactions: Option<Interactions>,
} }
lazy_static! { lazy_static! {
@@ -86,6 +88,7 @@ pub async fn message_send(
channel: channel.id().to_string(), channel: channel.id().to_string(),
author: user.id.clone(), author: user.id.clone(),
masquerade: data.masquerade, masquerade: data.masquerade,
interactions: data.interactions.unwrap_or_default(),
..Default::default() ..Default::default()
}; };
@@ -100,13 +103,22 @@ pub async fn message_send(
} }
// 2. Verify permissions for masquerade. // 2. Verify permissions for masquerade.
if message.masquerade.is_some() { if let Some(masq) = &message.masquerade {
permissions permissions
.throw_permission(db, Permission::Masquerade) .throw_permission(db, Permission::Masquerade)
.await?; .await?;
if masq.colour.is_some() {
permissions
.throw_permission(db, Permission::ManageRole)
.await?;
}
} }
// 3. Verify replies are valid. // 3. Ensure interactions information is correct
message.interactions.validate(db).await?;
// 4. Verify replies are valid.
let mut replies = HashSet::new(); let mut replies = HashSet::new();
if let Some(entries) = data.replies { if let Some(entries) = data.replies {
if entries.len() > 5 { if entries.len() > 5 {
@@ -139,7 +151,7 @@ pub async fn message_send(
.replace(replies.into_iter().collect::<Vec<String>>()); .replace(replies.into_iter().collect::<Vec<String>>());
} }
// 4. Process included embeds. // 5. Process included embeds.
let mut embeds = vec![]; let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds { if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds { for sendable_embed in sendable_embeds {
@@ -151,7 +163,7 @@ pub async fn message_send(
message.embeds.replace(embeds); message.embeds.replace(embeds);
} }
// 5. Add attachments to message. // 6. Add attachments to message.
let mut attachments = vec![]; let mut attachments = vec![];
if let Some(ids) = &data.attachments { if let Some(ids) = &data.attachments {
if !ids.is_empty() { if !ids.is_empty() {
@@ -177,10 +189,10 @@ pub async fn message_send(
message.attachments.replace(attachments); message.attachments.replace(attachments);
} }
// 6. Set content // 7. Set content
message.content = data.content; message.content = data.content;
// 7. Pass-through nonce value for clients // 8. Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key()); message.nonce = Some(idempotency.into_key());
message.create(db, &channel, Some(&user)).await?; message.create(db, &channel, Some(&user)).await?;
@@ -0,0 +1,58 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
user_id: Option<String>,
/// Remove all reactions
remove_all: Option<bool>,
}
/// # Remove Reaction(s) to Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` if changing others' reactions.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
pub async fn unreact_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
options: OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Check if we need to escalate permissions
let remove_all = options.remove_all.unwrap_or_default();
if options.user_id.is_some() || remove_all {
permissions
.throw_permission(db, Permission::ManageMessages)
.await?;
}
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Check if we should wipe all of this reaction
if remove_all {
return message
.clear_reaction(db, &emoji.id)
.await
.map(|_| EmptyResponse);
}
// Remove the reaction
message
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
.await
.map(|_| EmptyResponse)
}
+6
View File
@@ -11,13 +11,16 @@ mod group_remove_member;
mod invite_create; mod invite_create;
mod members_fetch; mod members_fetch;
mod message_bulk_delete; mod message_bulk_delete;
mod message_clear_reactions;
mod message_delete; mod message_delete;
mod message_edit; mod message_edit;
mod message_fetch; mod message_fetch;
mod message_query; mod message_query;
mod message_query_stale; mod message_query_stale;
mod message_react;
mod message_search; mod message_search;
mod message_send; mod message_send;
mod message_unreact;
mod permissions_set; mod permissions_set;
mod permissions_set_default; mod permissions_set_default;
mod voice_join; mod voice_join;
@@ -44,5 +47,8 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
voice_join::req, voice_join::req,
permissions_set::req, permissions_set::req,
permissions_set_default::req, permissions_set_default::req,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions
] ]
} }
@@ -14,7 +14,11 @@ pub struct DataCreateEmoji {
/// Server name /// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")] #[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
name: String, name: String,
/// Parent information
parent: EmojiParent, parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
nsfw: bool,
} }
/// # Create New Emoji /// # Create New Emoji
@@ -55,6 +59,7 @@ pub async fn create_emoji(
return Err(Error::TooManyEmoji); return Err(Error::TooManyEmoji);
} }
} }
EmojiParent::Detached => return Err(Error::InvalidOperation),
}; };
// Find the relevant attachment // Find the relevant attachment
@@ -67,6 +72,7 @@ pub async fn create_emoji(
creator_id: user.id, creator_id: user.id,
name: data.name, name: data.name,
animated: "image/gif" == &attachment.content_type, animated: "image/gif" == &attachment.content_type,
nsfw: data.nsfw,
}; };
// Save emoji // Save emoji
@@ -28,6 +28,7 @@ pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse>
.throw_permission(db, Permission::ManageCustomisation) .throw_permission(db, Permission::ManageCustomisation)
.await?; .await?;
} }
EmojiParent::Detached => return Ok(EmptyResponse),
}; };
} }
+1
View File
@@ -80,6 +80,7 @@ fn custom_openapi_spec() -> OpenApi {
"Channel Invites", "Channel Invites",
"Channel Permissions", "Channel Permissions",
"Messaging", "Messaging",
"Interactions",
"Groups", "Groups",
"Voice" "Voice"
] ]
+22 -8
View File
@@ -1,5 +1,5 @@
use revolt_quark::{ use revolt_quark::{
models::{ServerBan, User}, models::{server_member::MemberCompositeKey, ServerBan, User},
perms, Db, Error, Permission, Ref, Result, perms, Db, Error, Permission, Ref, Result,
}; };
@@ -47,13 +47,27 @@ pub async fn req(
.throw_permission(db, Permission::BanMembers) .throw_permission(db, Permission::BanMembers)
.await?; .await?;
let member = target.as_member(db, &server.id).await?; // If member exists, check privileges against them
if let Ok(member) = target.as_member(db, &server.id).await {
if member.get_ranking(permissions.server.get().unwrap())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
}
if member.get_ranking(permissions.server.get().unwrap()) server.ban_member(db, member, data.reason).await.map(Json)
<= permissions.get_member_rank().unwrap_or(i64::MIN) } else {
{ let server_id = server.id.to_string();
return Err(Error::NotElevated); server
.ban_user(
db,
MemberCompositeKey {
server: server_id,
user: target.id,
},
data.reason,
)
.await
.map(Json)
} }
server.ban_member(db, member, data.reason).await.map(Json)
} }
@@ -5,7 +5,7 @@ use revolt_quark::{
server_member::{FieldsMember, PartialMember}, server_member::{FieldsMember, PartialMember},
File, Member, User, File, Member, User,
}, },
perms, Db, Error, Permission, Ref, Result, perms, Db, Error, Permission, Ref, Result, Timestamp,
}; };
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -22,6 +22,8 @@ pub struct DataMemberEdit {
avatar: Option<String>, avatar: Option<String>,
/// Array of role ids /// Array of role ids
roles: Option<Vec<String>>, roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
timeout: Option<Timestamp>,
/// Fields to remove from channel object /// Fields to remove from channel object
#[validate(length(min = 1))] #[validate(length(min = 1))]
remove: Option<Vec<FieldsMember>>, remove: Option<Vec<FieldsMember>>,
@@ -105,13 +107,7 @@ pub async fn req(
// Check permissions against roles in diff // Check permissions against roles in diff
if let Some(roles) = &data.roles { if let Some(roles) = &data.roles {
let fallback = vec![]; let current_roles = member.roles.iter().collect::<HashSet<&String>>();
let current_roles = member
.roles
.as_ref()
.unwrap_or(&fallback)
.iter()
.collect::<HashSet<&String>>();
let new_roles = roles.iter().collect::<HashSet<&String>>(); let new_roles = roles.iter().collect::<HashSet<&String>>();
let added_roles: Vec<&&String> = new_roles.difference(&current_roles).collect(); let added_roles: Vec<&&String> = new_roles.difference(&current_roles).collect();
@@ -132,12 +128,14 @@ pub async fn req(
nickname, nickname,
avatar, avatar,
roles, roles,
timeout,
remove, remove,
} = data; } = data;
let mut partial = PartialMember { let mut partial = PartialMember {
nickname, nickname,
roles, roles,
timeout,
..Default::default() ..Default::default()
}; };
@@ -3,7 +3,9 @@ use revolt_quark::{
server::{FieldsRole, PartialRole, Role}, server::{FieldsRole, PartialRole, Role},
User, User,
}, },
perms, Db, Error, Permission, Ref, Result, perms,
util::regex::RE_COLOUR,
Db, Error, Permission, Ref, Result,
}; };
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -17,7 +19,7 @@ pub struct DataEditRole {
#[validate(length(min = 1, max = 32))] #[validate(length(min = 1, max = 32))]
name: Option<String>, name: Option<String>,
/// Role colour /// Role colour
#[validate(length(min = 1, max = 32))] #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
colour: Option<String>, colour: Option<String>,
/// Whether this role should be displayed separately /// Whether this role should be displayed separately
hoist: Option<bool>, hoist: Option<bool>,
@@ -33,6 +33,8 @@ pub struct DataEditServer {
// Whether this server is age-restricted // Whether this server is age-restricted
// nsfw: Option<bool>, // nsfw: Option<bool>,
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
discoverable: Option<bool>,
/// Whether analytics should be collected for this server /// Whether analytics should be collected for this server
/// ///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg). /// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
@@ -102,6 +104,7 @@ pub async fn req(
categories, categories,
system_messages, system_messages,
// nsfw, // nsfw,
discoverable,
analytics, analytics,
remove, remove,
} = data; } = data;
@@ -112,6 +115,7 @@ pub async fn req(
categories, categories,
system_messages, system_messages,
// nsfw, // nsfw,
discoverable,
analytics, analytics,
..Default::default() ..Default::default()
}; };
+1 -1
View File
@@ -11,4 +11,4 @@ pub static RE_USERNAME: Lazy<Regex> =
/// Regex for valid emoji names /// Regex for valid emoji names
/// ///
/// Alphanumeric and underscores /// Alphanumeric and underscores
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]+$").unwrap()); pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap());
+2 -1
View File
@@ -68,6 +68,7 @@ impl_ops = "0.1.1"
num_enum = "0.5.6" num_enum = "0.5.6"
reqwest = "0.11.10" reqwest = "0.11.10"
bitfield = "0.13.2" bitfield = "0.13.2"
once_cell = "1.13.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
lru = { version = "0.7.6", optional = true } lru = { version = "0.7.6", optional = true }
@@ -84,7 +85,7 @@ rocket_empty = { optional = true, git = "https://github.com/insertish/rocket_emp
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" } rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
# rAuth # rAuth
rauth = { git = "https://github.com/insertish/rauth", rev = "c2acaf6b31213e3969f8b48f9deed3daa125d394", features = [ "async-std-runtime" ] } rauth = { git = "https://github.com/insertish/rauth", rev = "9c3f9bb58a549a51546bd748274130ac4a667cb4", features = [ "async-std-runtime" ] }
# Sentry # Sentry
sentry = "0.25.0" sentry = "0.25.0"
File diff suppressed because it is too large Load Diff
+23
View File
@@ -78,6 +78,29 @@ pub enum EventV1 {
/// Delete message /// Delete message
MessageDelete { id: String, channel: String }, MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages /// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> }, BulkMessageDelete { channel: String, ids: Vec<String> },
+5 -8
View File
@@ -111,6 +111,7 @@ impl State {
// Fetch all memberships with their corresponding servers. // Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?; let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect(); let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?; let servers = db.fetch_servers(&server_ids).await?;
@@ -484,10 +485,8 @@ impl State {
if data.rank.is_some() || data.permissions.is_some() { if data.rank.is_some() || data.permissions.is_some() {
if let Some(member) = self.cache.members.get(id) { if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles { if member.roles.contains(role_id) {
if roles.contains(role_id) { queue_server = Some(id.clone());
queue_server = Some(id.clone());
}
} }
} }
} }
@@ -498,10 +497,8 @@ impl State {
} }
if let Some(member) = self.cache.members.get(id) { if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles { if member.roles.contains(role_id) {
if roles.contains(role_id) { queue_server = Some(id.clone());
queue_server = Some(id.clone());
}
} }
} }
} }
@@ -64,4 +64,22 @@ impl AbstractMessage for DummyDb {
) -> Result<Vec<Message>> { ) -> Result<Vec<Message>> {
Ok(vec![self.fetch_message(channel).await.unwrap()]) Ok(vec![self.fetch_message(channel).await.unwrap()])
} }
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Add to {id} with {emoji} and {user}");
Ok(())
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Remove {emoji} from {id} for {user}");
Ok(())
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
info!("Clear {emoji} on {id}");
Ok(())
}
} }
+4 -3
View File
@@ -14,6 +14,7 @@ impl AbstractEmoji for DummyDb {
parent: EmojiParent::Server { id: id.into() }, parent: EmojiParent::Server { id: id.into() },
creator_id: id.into(), creator_id: id.into(),
animated: false, animated: false,
nsfw: false,
}) })
} }
@@ -33,9 +34,9 @@ impl AbstractEmoji for DummyDb {
Ok(()) Ok(())
} }
/// Delete an emoji by its id /// Detach an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()> { async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Delete {emoji:?}"); info!("Detach {emoji:?}");
Ok(()) Ok(())
} }
} }
@@ -3,6 +3,8 @@ use crate::{AbstractServerMember, Result};
use super::super::DummyDb; use super::super::DummyDb;
use iso8601_timestamp::Timestamp;
#[async_trait] #[async_trait]
impl AbstractServerMember for DummyDb { impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> { async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
@@ -11,9 +13,11 @@ impl AbstractServerMember for DummyDb {
server: server.into(), server: server.into(),
user: user.into(), user: user.into(),
}, },
joined_at: Timestamp::now_utc(),
nickname: None, nickname: None,
avatar: None, avatar: None,
roles: None, roles: vec![],
timeout: None,
}) })
} }
@@ -305,6 +305,15 @@ impl Channel {
vec![], vec![],
) )
.await?; .await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.into(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
} else { } else {
db.delete_channel(self).await?; db.delete_channel(self).await?;
return Ok(()); return Ok(());
@@ -8,9 +8,10 @@ use crate::{
events::client::EventV1, events::client::EventV1,
models::{ models::{
message::{ message::{
AppendMessage, BulkMessageResponse, PartialMessage, SendableEmbed, SystemMessage, AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
SystemMessage,
}, },
Channel, Message, User, Channel, Emoji, Message, User,
}, },
presence::presence_filter_online, presence::presence_filter_online,
tasks::ack::AckEvent, tasks::ack::AckEvent,
@@ -191,6 +192,79 @@ impl Message {
Err(Error::PayloadTooLarge) Err(Error::PayloadTooLarge)
} }
} }
/// Add a reaction to a message
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check if the emoji is whitelisted
if !self.interactions.can_use(emoji) {
return Err(Error::InvalidOperation);
}
// Check if the emoji is usable by us
if !Emoji::can_use(db, emoji).await? {
return Err(Error::InvalidOperation);
}
// Send reaction event
EventV1::MessageReact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.id.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Add emoji
db.add_reaction(&self.id, emoji, &user.id).await
}
/// Remove a reaction from a message
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
// Check if it actually exists
let empty = if let Some(users) = self.reactions.get(emoji) {
if !users.contains(user) {
return Err(Error::NotFound);
}
users.len() == 1
} else {
return Err(Error::NotFound);
};
// Send reaction event
EventV1::MessageUnreact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
if empty {
// If empty, remove the reaction entirely
db.clear_reaction(&self.id, emoji).await
} else {
// Otherwise only remove that one reaction
db.remove_reaction(&self.id, emoji, user).await
}
}
/// Remove a reaction from a message
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
// Send reaction event
EventV1::MessageRemoveReaction {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Write to database
db.clear_reaction(&self.id, emoji).await
}
} }
pub trait IntoUsers { pub trait IntoUsers {
@@ -262,6 +336,9 @@ impl From<SystemMessage> for String {
"Channel description changed.".to_string() "Channel description changed.".to_string()
} }
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(), SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
SystemMessage::ChannelOwnershipChanged { .. } => {
"Channel ownership changed.".to_string()
}
} }
} }
} }
@@ -321,3 +398,40 @@ impl BulkMessageResponse {
} }
} }
} }
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(&self, db: &Database) -> Result<()> {
if let Some(reactions) = &self.reactions {
if reactions.len() > 20 {
return Err(Error::InvalidOperation);
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(Error::InvalidOperation);
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
+24 -2
View File
@@ -1,14 +1,27 @@
use std::{collections::HashSet, str::FromStr};
use ulid::Ulid;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
models::{emoji::EmojiParent, Emoji}, models::{emoji::EmojiParent, Emoji},
Database, Result, Database, Result,
}; };
lazy_static! {
/// Permissible emojis
static ref PERMISSIBLE_EMOJIS: HashSet<String> = include_str!(crate::asset!("emojis.txt"))
.split('\n')
.map(|x| x.into())
.collect();
}
impl Emoji { impl Emoji {
/// Get parent id /// Get parent id
fn parent(&self) -> &str { fn parent(&self) -> &str {
match &self.parent { match &self.parent {
EmojiParent::Server { id } => id, EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
} }
} }
@@ -30,7 +43,16 @@ impl Emoji {
.p(self.parent().to_string()) .p(self.parent().to_string())
.await; .await;
db.mark_attachment_as_deleted(&self.id).await?; db.detach_emoji(&self).await
db.delete_emoji(&self).await }
/// Check whether we can use a given emoji
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
if Ulid::from_str(emoji).is_ok() {
db.fetch_emoji(emoji).await?;
Ok(true)
} else {
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
}
} }
} }
@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use ulid::Ulid; use ulid::Ulid;
use crate::{ use crate::{
@@ -188,7 +189,11 @@ impl Server {
server: self.id.clone(), server: self.id.clone(),
user: user.id.clone(), user: user.id.clone(),
}, },
..Default::default() joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
}; };
db.insert_member(&member).await?; db.insert_member(&member).await?;
@@ -278,6 +283,18 @@ impl Server {
Ok(()) Ok(())
} }
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server /// Ban a member from a server
pub async fn ban_member( pub async fn ban_member(
self, self,
@@ -285,16 +302,10 @@ impl Server {
member: Member, member: Member,
reason: Option<String>, reason: Option<String>,
) -> Result<ServerBan> { ) -> Result<ServerBan> {
let ban = ServerBan { self.remove_member(db, member.clone(), RemovalIntention::Ban)
id: member.id.clone(),
reason,
};
self.remove_member(db, member, RemovalIntention::Ban)
.await?; .await?;
db.insert_ban(&ban).await?; self.ban_user(db, member.id, reason).await
Ok(ban)
} }
} }
@@ -1,3 +1,5 @@
use iso8601_timestamp::Timestamp;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
models::{ models::{
@@ -36,19 +38,24 @@ impl Member {
/// Get this user's current ranking /// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 { pub fn get_ranking(&self, server: &Server) -> i64 {
if let Some(roles) = &self.roles { let mut value = i64::MAX;
let mut value = i64::MAX; for role in &self.roles {
for role in roles { if let Some(role) = server.roles.get(role) {
if let Some(role) = server.roles.get(role) { if role.rank < value {
if role.rank < value { value = role.rank;
value = role.rank;
}
} }
} }
}
value value
}
/// Check whether this member is in timeout
pub fn in_timeout(&self) -> bool {
if let Some(timeout) = self.timeout {
*timeout > *Timestamp::now_utc()
} else { } else {
i64::MAX false
} }
} }
@@ -56,7 +63,8 @@ impl Member {
match field { match field {
FieldsMember::Avatar => self.avatar = None, FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None, FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles = None, FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
} }
} }
} }
@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use bson::Bson; use bson::{Bson, DateTime};
use futures::StreamExt; use futures::StreamExt;
use mongodb::{ use mongodb::{
bson::{doc, from_bson, from_document, to_document, Document}, bson::{doc, from_bson, from_document, to_document, Document},
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 17; pub const LATEST_REVISION: i32 = 18;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -644,6 +644,23 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create emoji parent index."); .expect("Failed to create emoji parent index.");
} }
if revision <= 17 {
info!("Running migration [revision 17 / 15-07-2022]: Initialise `joined_at` property on server members.");
db.col::<Document>("server_members")
.update_many(
doc! {},
doc! {
"$set": {
"joined_at": DateTime::now().to_rfc3339_string()
}
},
None,
)
.await
.expect("Failed to update server members.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
@@ -279,4 +279,70 @@ impl AbstractMessage for MongoDb {
) )
.await .await
} }
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
} }
+24 -3
View File
@@ -1,5 +1,7 @@
use bson::Document;
use crate::models::Emoji; use crate::models::Emoji;
use crate::{AbstractEmoji, Result}; use crate::{AbstractEmoji, Error, Result};
use super::super::MongoDb; use super::super::MongoDb;
@@ -42,7 +44,26 @@ impl AbstractEmoji for MongoDb {
} }
/// Delete an emoji by its id /// Delete an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()> { async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.delete_one_by_id(COL, &emoji.id).await.map(|_| ()) self.col::<Document>(COL)
.update_one(
doc! {
"_id": &emoji.id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "emojis",
})
} }
} }
@@ -129,6 +129,7 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Avatar => "avatar", FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname", FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles", FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
}) })
} }
} }
+36 -1
View File
@@ -1,3 +1,6 @@
use crate::util::regex::RE_COLOUR;
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
@@ -11,6 +14,11 @@ use crate::{
types::january::Embed, types::january::Embed,
}; };
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// # Reply /// # Reply
/// ///
/// Representation of a message reply before it is sent. /// Representation of a message reply before it is sent.
@@ -33,7 +41,7 @@ pub struct SendableEmbed {
#[validate(length(min = 1, max = 2000))] #[validate(length(min = 1, max = 2000))]
pub description: Option<String>, pub description: Option<String>,
pub media: Option<String>, pub media: Option<String>,
#[validate(length(min = 1, max = 64))] #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
pub colour: Option<String>, pub colour: Option<String>,
} }
@@ -61,6 +69,8 @@ pub enum SystemMessage {
ChannelDescriptionChanged { by: String }, ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")] #[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String }, ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
} }
/// Name and / or avatar override information /// Name and / or avatar override information
@@ -74,6 +84,25 @@ pub struct Masquerade {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))] #[validate(length(min = 1, max = 128))]
pub avatar: Option<String>, pub avatar: Option<String>,
/// Replace the display role colour shown on this message
///
/// Must have `ManageRole` permission to use
///
/// This can be any valid CSS colour
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 32))]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)]
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<HashSet<String>>,
/// Whether reactions should be restricted to the given list
#[serde(skip_serializing_if = "if_false", default)]
pub restrict_reactions: bool,
} }
/// Representation of a Message on Revolt /// Representation of a Message on Revolt
@@ -115,6 +144,12 @@ pub struct Message {
/// Array of message ids this message is replying to /// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub reactions: HashMap<String, HashSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// Name and / or avatar overrides for this message /// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>, pub masquerade: Option<Masquerade>,
+10
View File
@@ -1,10 +1,16 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// Information about what owns this emoji /// Information about what owns this emoji
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum EmojiParent { pub enum EmojiParent {
Server { id: String }, Server { id: String },
Detached,
} }
/// Representation of an Emoji on Revolt /// Representation of an Emoji on Revolt
@@ -20,5 +26,9 @@ pub struct Emoji {
/// Emoji name /// Emoji name
pub name: String, pub name: String,
/// Whether the emoji is animated /// Whether the emoji is animated
#[serde(skip_serializing_if = "if_false", default)]
pub animated: bool, pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool,
} }
@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::attachment::File; use crate::models::attachment::File;
@@ -12,7 +13,7 @@ pub struct MemberCompositeKey {
} }
/// Representation of a member of a server on Revolt /// Representation of a member of a server on Revolt
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)] #[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
#[optional_name = "PartialMember"] #[optional_name = "PartialMember"]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
@@ -22,6 +23,9 @@ pub struct Member {
#[serde(rename = "_id")] #[serde(rename = "_id")]
pub id: MemberCompositeKey, pub id: MemberCompositeKey,
/// Time at which this user joined the server
pub joined_at: Timestamp,
/// Member's nickname /// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>, pub nickname: Option<String>,
@@ -30,8 +34,11 @@ pub struct Member {
pub avatar: Option<File>, pub avatar: Option<File>,
/// Member's roles /// Member's roles
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>, pub timeout: Option<Timestamp>,
} }
/// Optional fields on server member object /// Optional fields on server member object
@@ -40,6 +47,7 @@ pub enum FieldsMember {
Nickname, Nickname,
Avatar, Avatar,
Roles, Roles,
Timeout,
} }
/// Member removal intention /// Member removal intention
+17 -2
View File
@@ -45,8 +45,23 @@ impl Override {
impl PermissionValue { impl PermissionValue {
/// Apply a given override to this value /// Apply a given override to this value
pub fn apply(&mut self, v: Override) { pub fn apply(&mut self, v: Override) {
self.0 |= v.allow; self.allow(v.allow);
self.0 &= !v.deny; self.revoke(v.deny);
}
/// Allow given permissions
pub fn allow(&mut self, v: u64) {
self.0 |= v;
}
/// Revoke given permissions
pub fn revoke(&mut self, v: u64) {
self.0 &= !v;
}
/// Restrict to given permissions
pub fn restrict(&mut self, v: u64) {
self.0 &= v;
} }
} }
@@ -65,8 +65,8 @@ pub enum Permission {
UploadFiles = 1 << 27, UploadFiles = 1 << 27,
/// Masquerade messages using custom nickname and avatar /// Masquerade messages using custom nickname and avatar
Masquerade = 1 << 28, Masquerade = 1 << 28,
/// React to messages with emojis
// % 1 bits reserved React = 1 << 29,
// * Voice permissions // * Voice permissions
/// Connect to a voice channel /// Connect to a voice channel
@@ -98,6 +98,7 @@ impl_op_ex!(+ |a: &Permission, b: &Permission| -> u64 { *a as u64 | *b as u64 })
impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 }); impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 });
lazy_static! { lazy_static! {
pub static ref ALLOW_IN_TIMEOUT: u64 = Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 = pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 =
Permission::ViewChannel + Permission::ReadMessageHistory; Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY
+20 -12
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use crate::{ use crate::{
models::Channel, permissions::PermissionCalculator, Override, Permission, PermissionValue, models::Channel, permissions::PermissionCalculator, Override, Permission, PermissionValue,
Permissions, Perms, Result, DEFAULT_PERMISSION_DIRECT_MESSAGE, Permissions, Perms, Result, ALLOW_IN_TIMEOUT, DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY, DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY,
}; };
@@ -55,11 +55,7 @@ async fn calculate_server_permission(
let mut permissions: PermissionValue = server.default_permissions.into(); let mut permissions: PermissionValue = server.default_permissions.into();
// 4. Resolve each role in order. // 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() { let member_roles: HashSet<&String> = member.roles.iter().collect();
roles.iter().collect()
} else {
HashSet::new()
};
if !member_roles.is_empty() { if !member_roles.is_empty() {
let mut roles = server let mut roles = server
@@ -80,6 +76,11 @@ async fn calculate_server_permission(
} }
} }
// 5. Revoke permissions if member is timed out.
if member.in_timeout() {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
Ok(permissions) Ok(permissions)
} }
@@ -110,7 +111,13 @@ async fn calculate_channel_permission(
// 1. Check channel type. // 1. Check channel type.
let value: PermissionValue = match channel { let value: PermissionValue = match channel {
Channel::SavedMessages { .. } => (*DEFAULT_PERMISSION_SAVED_MESSAGES).into(), Channel::SavedMessages { user, .. } => {
if user == &data.perspective.id {
(*DEFAULT_PERMISSION_SAVED_MESSAGES).into()
} else {
0_u64.into()
}
}
Channel::DirectMessage { recipients, .. } => { Channel::DirectMessage { recipients, .. } => {
// 2. Fetch user. // 2. Fetch user.
let other_user = recipients let other_user = recipients
@@ -182,11 +189,7 @@ async fn calculate_channel_permission(
} }
// 4. Resolve each role in order. // 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() { let member_roles: HashSet<&String> = member.roles.iter().collect();
roles.iter().collect()
} else {
HashSet::new()
};
if !member_roles.is_empty() { if !member_roles.is_empty() {
let mut roles = role_permissions let mut roles = role_permissions
@@ -208,6 +211,11 @@ async fn calculate_channel_permission(
} }
} }
// 5. Revoke permissions if member is timed out.
if member.in_timeout() {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions permissions
} else { } else {
(Permission::GrantAllSafe as u64).into() (Permission::GrantAllSafe as u64).into()
@@ -42,4 +42,13 @@ pub trait AbstractMessage: Sync + Send {
after: Option<String>, after: Option<String>,
sort: MessageSort, sort: MessageSort,
) -> Result<Vec<Message>>; ) -> Result<Vec<Message>>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
} }
+2 -2
View File
@@ -15,6 +15,6 @@ pub trait AbstractEmoji: Sync + Send {
/// Insert emoji into database. /// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>; async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Delete an emoji by its id /// Detach an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()>; async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
} }
+1
View File
@@ -3,6 +3,7 @@ pub mod manipulation;
pub mod pfp; pub mod pfp;
pub mod rauth; pub mod rauth;
pub mod r#ref; pub mod r#ref;
pub mod regex;
pub mod result; pub mod result;
pub mod value; pub mod value;
pub mod variables; pub mod variables;
+11 -1
View File
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User}; use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User};
use crate::presence::presence_is_online; use crate::presence::presence_is_online;
use crate::{Database, Result}; use crate::{Database, Error, Result};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -44,6 +44,16 @@ impl Ref {
db.fetch_message(&self.id).await db.fetch_message(&self.id).await
} }
/// Fetch message in channel from Ref
pub async fn as_message_in(&self, db: &Database, channel: &str) -> Result<Message> {
let message = self.as_message(db).await?;
if message.channel != channel {
return Err(Error::NotFound);
}
Ok(message)
}
/// Fetch bot from Ref /// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> { pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await db.fetch_bot(&self.id).await
+21
View File
@@ -0,0 +1,21 @@
use once_cell::sync::Lazy;
use regex::Regex;
/// Regex for valid role colours
///
/// Allows the use of named colours, rgb(a), variables and all gradients.
///
/// Flags:
/// - Case-insensitive (`i`)
///
/// Source:
/// ```regex
/// VALUE = [a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+
/// ADDITIONAL_VALUE = \d+deg
/// STOP = ([ ]+(\d{1,3}%|0))?
///
/// ^(?:VALUE|(repeating-)?(linear|conic|radial)-gradient\((VALUE|ADDITIONAL_VALUE)STOP(,[ ]*(VALUE)STOP)+\))$
/// ```
pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
});
+2
View File
@@ -72,6 +72,7 @@ pub enum Error {
}, },
NotElevated, NotElevated,
CannotGiveMissingPermissions, CannotGiveMissingPermissions,
NotOwner,
// ? General errors. // ? General errors.
DatabaseError { DatabaseError {
@@ -167,6 +168,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::MissingUserPermission { .. } => Status::Forbidden, Error::MissingUserPermission { .. } => Status::Forbidden,
Error::NotElevated => Status::Forbidden, Error::NotElevated => Status::Forbidden,
Error::CannotGiveMissingPermissions => Status::Forbidden, Error::CannotGiveMissingPermissions => Status::Forbidden,
Error::NotOwner => Status::Forbidden,
Error::DatabaseError { .. } => Status::InternalServerError, Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError, Error::InternalError => Status::InternalServerError,
+1
View File
@@ -50,6 +50,7 @@ services:
/usr/bin/mc mb minio/backgrounds; /usr/bin/mc mb minio/backgrounds;
/usr/bin/mc mb minio/icons; /usr/bin/mc mb minio/icons;
/usr/bin/mc mb minio/banners; /usr/bin/mc mb minio/banners;
/usr/bin/mc mb minio/emojis;
exit 0; exit 0;
" "
# File server (autumn) # File server (autumn)