Compare commits

...
43 Commits
Author SHA1 Message Date
Paul Makles fa82be74b5 fix: adjust permission checks for member_edit 2022-07-26 14:06:53 +01:00
Paul Makles 8491ced13d fix: ensure list matches 2022-07-18 12:46:33 +01:00
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
Paul Makles 0e5c8ed59b chore: bump version 2022-07-09 17:44:05 +01:00
Paul Makles a7e0c42ee4 feat: add emoji 2022-07-07 13:23:31 +01:00
Paul Makles 386f027a5a chore: deprecate old add friend route 2022-07-07 13:20:03 +01:00
Paul Makles 30c7f553d8 chore: refactor autumn to media 2022-07-06 16:46:18 +01:00
Paul Makles e291eef364 chore: create entity relationship diagram [skip ci] 2022-07-06 16:43:17 +01:00
Martin Löffler 73443ce883 revert: bump rauth for acc. verification fix
the "fix" caused emails to not send
2022-06-23 01:30:54 +02:00
Paul Makles 47297e243f fix: enforce min. password length
fixes #188
2022-06-21 11:06:02 +01:00
Paul Makles 4188b6d2f2 fix: ignore anchor links when parsing messages
fixes #183
2022-06-21 10:38:59 +01:00
Paul Makles 165380c7b4 chore: bump rauth for acc. verification fix 2022-06-21 10:28:14 +01:00
Paul Makles 49ca24ac9f chore: further restrict usernames
closes #186
2022-06-21 10:27:58 +01:00
Paul Makles 35f956ce7d fix: add separate bucket for default_avatar
closes #166
2022-06-20 11:13:15 +01:00
Paul Makles 4baab5d5d5 feat(messaging): cap total text content to 2k characters 2022-06-20 11:09:14 +01:00
Paul Makles 7fc4fb2df7 fix: rewrite attachment deletion logic 2022-06-20 10:49:09 +01:00
Paul Makles ef757aa2fb chore: bump quark 2022-06-20 10:27:26 +01:00
Paul Makles 0585dd0c20 fix: consistent username validation across routes
fixes #187
2022-06-20 10:27:22 +01:00
Paul Makles f96541efab fix: add additional validation on legacy nonce value 2022-06-14 17:35:44 +01:00
Paul Makles c6414338b6 fix: marking server as read would not mark it as read
fixes #169

Porting code forwards from an older revision of the codebase; https://github.com/revoltchat/backend/blob/0.5.3-alpha.10/src/database/entities/server.rs
2022-06-14 17:32:43 +01:00
Paul Makles 4c4eb60cdb fix: don't allow members to be added more than once
fixes #182
2022-06-14 17:27:15 +01:00
Paul Makles 11d89b3bf0 feat: enable 2FA login 2022-06-12 18:50:30 +01:00
Paul Makles 64bb171cc8 fix: remove test flag from rauth 2022-06-12 18:03:49 +01:00
Paul Makles 6de5ad15c5 chore: bump rauth 2022-06-12 17:48:12 +01:00
Paul Makles b5ab16d66f fix: bump quark to fix is_disabled check 2022-06-12 17:07:22 +01:00
Paul Makles b9aad6d38c chore: bump rauth 2022-06-11 20:49:19 +01:00
Paul Makles ed5c8159e9 chore: bump rauth 2022-06-10 17:47:13 +01:00
Paul Makles 2e6e1fdd44 chore: bump quark; changes deletion period 2022-06-10 17:22:34 +01:00
69 changed files with 3468 additions and 182 deletions
Generated
+7 -14
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=7a579daa6a90845fe86139edcadbd01639b330b6#7a579daa6a90845fe86139edcadbd01639b330b6" 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.3-7" 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,23 +2987,15 @@ 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=7a579daa6a90845fe86139edcadbd01639b330b6#7a579daa6a90845fe86139edcadbd01639b330b6" source = "git+https://github.com/insertish/rauth?rev=9c3f9bb58a549a51546bd748274130ac4a667cb4#9c3f9bb58a549a51546bd748274130ac4a667cb4"
dependencies = [ dependencies = [
"async-std",
"base32",
"chrono",
"iso8601-timestamp",
"mongodb",
"okapi", "okapi",
"rauth", "rauth",
"regex",
"reqwest",
"rocket", "rocket",
"rocket_empty", "rocket_empty",
"rocket_okapi", "rocket_okapi",
"schemars", "schemars",
"serde", "serde",
"serde_json",
] ]
[[package]] [[package]]
+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.3-7" 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 = "7a579daa6a90845fe86139edcadbd01639b330b6" } 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)
}
@@ -36,6 +36,8 @@ pub async fn req(
edit.validate() edit.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?; let mut message = msg.as_message(db).await?;
if message.channel != target { if message.channel != target {
return Err(Error::NotFound); return Err(Error::NotFound);
@@ -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,
@@ -21,6 +21,7 @@ pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending /// Unique token to prevent duplicate message sending
/// ///
/// **This is deprecated and replaced by `Idempotency-Key`!** /// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
nonce: Option<String>, nonce: Option<String>,
/// Message content to send /// Message content to send
@@ -32,11 +33,15 @@ pub struct DataMessageSend {
/// Messages to reply to /// Messages to reply to
replies: Option<Vec<Reply>>, replies: Option<Vec<Reply>>,
/// Embeds to include in message /// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate(length(min = 1, max = 10))] #[validate(length(min = 1, max = 10))]
embeds: Option<Vec<SendableEmbed>>, embeds: Option<Vec<SendableEmbed>>,
/// 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! {
@@ -60,6 +65,8 @@ pub async fn message_send(
data.validate() data.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&data.content, &data.embeds)?;
idempotency.consume_nonce(data.nonce).await?; idempotency.consume_nonce(data.nonce).await?;
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
@@ -81,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()
}; };
@@ -95,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 {
@@ -134,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 {
@@ -146,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() {
@@ -172,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
] ]
} }
@@ -0,0 +1,81 @@
use revolt_quark::models::emoji::EmojiParent;
use revolt_quark::models::{Emoji, File, User};
use revolt_quark::{perms, Db, Error, Permission, Result};
use serde::Deserialize;
use validator::Validate;
use crate::util::regex::RE_EMOJI;
use rocket::serde::json::Json;
/// # Emoji Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateEmoji {
/// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
name: String,
/// Parent information
parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
nsfw: bool,
}
/// # Create New Emoji
///
/// Create an emoji by its Autumn upload id.
#[openapi(tag = "Emojis")]
#[put("/emoji/<id>", data = "<data>")]
pub async fn create_emoji(
db: &Db,
user: User,
id: String,
data: Json<DataCreateEmoji>,
) -> Result<Json<Emoji>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Bots cannot manage emojis
if user.bot.is_some() {
return Err(Error::IsBot);
}
// Validate we have permission to write into parent
match &data.parent {
EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
// Check that there are no more than 100 emoji
// ! FIXME: hardcoded upper limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() > 100 {
return Err(Error::TooManyEmoji);
}
}
EmojiParent::Detached => return Err(Error::InvalidOperation),
};
// Find the relevant attachment
let attachment = File::use_emoji(db, &id, &id).await?;
// Create the emoji object
let emoji = Emoji {
id,
parent: data.parent,
creator_id: user.id,
name: data.name,
animated: "image/gif" == &attachment.content_type,
nsfw: data.nsfw,
};
// Save emoji
emoji.create(db).await?;
Ok(Json(emoji))
}
@@ -0,0 +1,37 @@
use revolt_quark::models::{emoji::EmojiParent, User};
use revolt_quark::{perms, Db, EmptyResponse, Error, Permission, Ref, Result};
/// # Delete Emoji
///
/// Delete an emoji by its id.
#[openapi(tag = "Emojis")]
#[delete("/emoji/<id>")]
pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse> {
// Bots cannot manage emoji
if user.bot.is_some() {
return Err(Error::IsBot);
}
// Fetch the emoji
let emoji = id.as_emoji(db).await?;
// If we uploaded the emoji, then we have permission to delete it
if emoji.creator_id != user.id {
// Otherwise, validate we have permission to delete from parent
match &emoji.parent {
EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
}
EmojiParent::Detached => return Ok(EmptyResponse),
};
}
// Delete the emoji
emoji.delete(db).await.map(|_| EmptyResponse)
}
@@ -0,0 +1,13 @@
use revolt_quark::models::{Emoji, User};
use revolt_quark::{Db, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Emoji
///
/// Fetch an emoji by its id.
#[openapi(tag = "Emojis")]
#[get("/emoji/<id>")]
pub async fn fetch_emoji(db: &Db, _user: User, id: Ref) -> Result<Json<Emoji>> {
id.as_emoji(db).await.map(Json)
}
@@ -0,0 +1,14 @@
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod emoji_create;
mod emoji_delete;
mod emoji_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
emoji_create::create_emoji,
emoji_delete::delete_emoji,
emoji_fetch::fetch_emoji
]
}
+9
View File
@@ -5,6 +5,7 @@ use rocket_okapi::{okapi::openapi3::OpenApi, settings::OpenApiSettings};
mod bots; mod bots;
mod channels; mod channels;
mod customisation;
mod invites; mod invites;
mod onboard; mod onboard;
mod push; mod push;
@@ -25,6 +26,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
"/channels" => channels::routes(), "/channels" => channels::routes(),
"/servers" => servers::routes(), "/servers" => servers::routes(),
"/invites" => invites::routes(), "/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/auth/account" => rocket_rauth::routes::account::routes(), "/auth/account" => rocket_rauth::routes::account::routes(),
"/auth/session" => rocket_rauth::routes::session::routes(), "/auth/session" => rocket_rauth::routes::session::routes(),
"/auth/mfa" => rocket_rauth::routes::mfa::routes(), "/auth/mfa" => rocket_rauth::routes::mfa::routes(),
@@ -78,6 +80,7 @@ fn custom_openapi_spec() -> OpenApi {
"Channel Invites", "Channel Invites",
"Channel Permissions", "Channel Permissions",
"Messaging", "Messaging",
"Interactions",
"Groups", "Groups",
"Voice" "Voice"
] ]
@@ -96,6 +99,12 @@ fn custom_openapi_spec() -> OpenApi {
"Invites" "Invites"
] ]
}, },
{
"name": "Customisation",
"tags": [
"Emojis"
]
},
{ {
"name": "Authentication", "name": "Authentication",
"tags": [ "tags": [
+2 -5
View File
@@ -32,13 +32,10 @@ pub async fn req(
data.validate() data.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
if db.is_username_taken(&data.username).await? { let username = User::validate_username(db, data.username).await?;
return Err(Error::UsernameTaken);
}
let user = User { let user = User {
id: session.user_id, id: session.user_id,
username: data.username, username,
..Default::default() ..Default::default()
}; };
+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)
} }
@@ -0,0 +1,17 @@
use revolt_quark::models::{Emoji, User};
use revolt_quark::{perms, Db, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Server Emoji
///
/// Fetch all emoji on a server.
#[openapi(tag = "Server Customisation")]
#[get("/<target>/emojis")]
pub async fn list_emoji(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Emoji>>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
// Fetch all emoji from server if we can view it
db.fetch_emoji_by_parent_id(&server.id).await.map(Json)
}
+16 -8
View File
@@ -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>>,
@@ -89,6 +91,16 @@ pub async fn req(
required.push(Permission::AssignRoles); required.push(Permission::AssignRoles);
} }
if data.timeout.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Timeout))
.unwrap_or_default()
{
required.push(Permission::TimeoutMembers);
}
for permission in required { for permission in required {
permissions.throw_permission(db, permission).await?; permissions.throw_permission(db, permission).await?;
} }
@@ -105,13 +117,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 +138,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()
}; };
+13 -16
View File
@@ -1,30 +1,26 @@
use rocket::Route; use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::okapi::openapi3::OpenApi;
mod server_ack;
mod server_create;
mod server_delete;
mod server_edit;
mod server_fetch;
mod channel_create;
mod member_edit;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
mod ban_create; mod ban_create;
mod ban_list; mod ban_list;
mod ban_remove; mod ban_remove;
mod channel_create;
mod emoji_list;
mod invites_fetch; mod invites_fetch;
mod member_edit;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
mod permissions_set; mod permissions_set;
mod permissions_set_default; mod permissions_set_default;
mod roles_create; mod roles_create;
mod roles_delete; mod roles_delete;
mod roles_edit; mod roles_edit;
mod server_ack;
mod server_create;
mod server_delete;
mod server_edit;
mod server_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) { pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![ openapi_get_routes_spec![
@@ -46,6 +42,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
roles_edit::req, roles_edit::req,
roles_delete::req, roles_delete::req,
permissions_set::req, permissions_set::req,
permissions_set_default::req permissions_set_default::req,
emoji_list::list_emoji
] ]
} }
@@ -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 -5
View File
@@ -10,11 +10,7 @@ use rocket::State;
#[openapi(tag = "Relationships")] #[openapi(tag = "Relationships")]
#[put("/<target>/friend")] #[put("/<target>/friend")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<User>> { pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<User>> {
let mut target = if let Ok(user) = db.fetch_user_by_username(&target.id).await { let mut target = target.as_user(db).await?;
user
} else {
target.as_user(db).await?
};
if user.bot.is_some() || target.bot.is_some() { if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot); return Err(Error::IsBot);
+6 -1
View File
@@ -6,4 +6,9 @@ use regex::Regex;
/// Block zero width space /// Block zero width space
/// Block lookalike characters /// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = pub static RE_USERNAME: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω]+$").unwrap()); Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω@#:\n\r\[\]]+$").unwrap());
/// Regex for valid emoji names
///
/// Alphanumeric and underscores
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 = "7a579daa6a90845fe86139edcadbd01639b330b6", 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
+31 -1
View File
@@ -5,7 +5,7 @@ use crate::models::message::{AppendMessage, PartialMessage};
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer}; use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer};
use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember}; use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember};
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus}; use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus};
use crate::models::{Channel, Member, Message, Server, User, UserSettings}; use crate::models::{Channel, Emoji, Member, Message, Server, User, UserSettings};
use crate::Error; use crate::Error;
/// WebSocket Client Errors /// WebSocket Client Errors
@@ -52,6 +52,7 @@ pub enum EventV1 {
servers: Vec<Server>, servers: Vec<Server>,
channels: Vec<Channel>, channels: Vec<Channel>,
members: Vec<Member>, members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
}, },
/// Ping response /// Ping response
@@ -77,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> },
@@ -170,4 +194,10 @@ pub enum EventV1 {
/// Settings updated remotely /// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings }, UserSettingsUpdate { id: String, update: UserSettings },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
} }
+17 -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?;
@@ -151,6 +152,17 @@ impl State {
) )
.await?; .await?;
// Fetch customisations.
let emojis = Some(
db.fetch_emoji_by_parent_ids(
&servers
.iter()
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?,
);
// Copy data into local state cache. // Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect(); self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache self.cache
@@ -202,6 +214,7 @@ impl State {
servers, servers,
channels, channels,
members, members,
emojis,
}) })
} }
@@ -472,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());
}
} }
} }
} }
@@ -486,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(())
}
} }
@@ -39,4 +39,9 @@ impl AbstractAttachment for DummyDb {
info!("Marked {id} as deleted"); info!("Marked {id} as deleted");
Ok(()) Ok(())
} }
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
info!("Marked {ids:?} as deleted");
Ok(())
}
} }
@@ -0,0 +1,42 @@
use crate::models::emoji::EmojiParent;
use crate::models::Emoji;
use crate::{AbstractEmoji, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractEmoji for DummyDb {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
Ok(Emoji {
id: id.into(),
name: id.into(),
parent: EmojiParent::Server { id: id.into() },
creator_id: id.into(),
animated: false,
nsfw: false,
})
}
/// Fetch emoji by their ids
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
Ok(vec![self.fetch_emoji(parent_id).await?])
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, _parent_ids: &[String]) -> Result<Vec<Emoji>> {
Ok(vec![])
}
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Insert {emoji:?}");
Ok(())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Detach {emoji:?}");
Ok(())
}
}
+2 -1
View File
@@ -4,8 +4,9 @@ pub mod admin {
pub mod migrations; pub mod migrations;
} }
pub mod autumn { pub mod media {
pub mod attachment; pub mod attachment;
pub mod emoji;
} }
pub mod channels { pub mod channels {
@@ -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,
}) })
} }
@@ -242,7 +242,12 @@ impl Channel {
/// Add user to a group /// Add user to a group
pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> { pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> {
if let Channel::Group { recipients, .. } = self { if let Channel::Group { recipients, .. } = self {
recipients.push(user.to_string()); let user = user.to_string();
if recipients.contains(&user) {
return Err(Error::AlreadyInGroup);
}
recipients.push(user);
} }
match &self { match &self {
@@ -300,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,
@@ -135,7 +136,17 @@ impl Message {
/// Delete a message /// Delete a message
pub async fn delete(self, db: &Database) -> Result<()> { pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
}
db.delete_message(&self.id).await?; db.delete_message(&self.id).await?;
EventV1::MessageDelete { EventV1::MessageDelete {
id: self.id, id: self.id,
channel: self.channel.clone(), channel: self.channel.clone(),
@@ -156,6 +167,104 @@ impl Message {
.await; .await;
Ok(()) Ok(())
} }
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
embeds: &Option<Vec<SendableEmbed>>,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
if let Some(embeds) = embeds {
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
}
if running_total <= 2000 {
Ok(())
} else {
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 {
@@ -227,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()
}
} }
} }
} }
@@ -286,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()
}
}
@@ -30,4 +30,9 @@ impl File {
db.find_and_use_attachment(id, "banners", "server", parent) db.find_and_use_attachment(id, "banners", "server", parent)
.await .await
} }
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "emojis", "object", parent)
.await
}
} }
@@ -0,0 +1,58 @@
use std::{collections::HashSet, str::FromStr};
use ulid::Ulid;
use crate::{
events::client::EventV1,
models::{emoji::EmojiParent, Emoji},
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 {
/// Get parent id
fn parent(&self) -> &str {
match &self.parent {
EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
}
}
/// Create an emoji
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_emoji(self).await?;
EventV1::EmojiCreate(self.clone())
.p(self.parent().to_string())
.await;
Ok(())
}
/// Delete an emoji
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_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))
}
}
}
+2 -1
View File
@@ -4,8 +4,9 @@ pub mod admin {
pub mod migrations; pub mod migrations;
} }
pub mod autumn { pub mod media {
pub mod attachment; pub mod attachment;
pub mod emoji;
} }
pub mod channels { pub mod channels {
@@ -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,
} }
} }
} }
+9 -4
View File
@@ -150,8 +150,8 @@ impl User {
Ok(db.fetch_server_count(&self.id).await? <= 100) Ok(db.fetch_server_count(&self.id).await? <= 100)
} }
/// Update a user's username /// Sanitise and validate a username can be used
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> { pub async fn validate_username(db: &Database, username: String) -> Result<String> {
// Trim surrounding spaces // Trim surrounding spaces
let username = username.trim().to_string(); let username = username.trim().to_string();
@@ -173,7 +173,7 @@ impl User {
} }
// Ensure none of the following substrings show up in the username // Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["@", "#", ":", "```", "\n"]; const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
for substr in BLOCKED_SUBSTRINGS { for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) { if username_lowercase.contains(substr) {
@@ -186,10 +186,15 @@ impl User {
return Err(Error::UsernameTaken); return Err(Error::UsernameTaken);
} }
Ok(username)
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
self.update( self.update(
db, db,
PartialUser { PartialUser {
username: Some(username), username: Some(User::validate_username(db, username).await?),
..Default::default() ..Default::default()
}, },
vec![], vec![],
@@ -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 = 16; 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");
@@ -611,6 +611,56 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap(); .unwrap();
} }
if revision <= 16 {
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and rAuth migration.");
let rauth_db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
rauth_db
.run_migration(rauth::Migration::M2022_06_09AddIndexForDeletion)
.await
.unwrap();
db.db()
.create_collection("emojis", None)
.await
.expect("Failed to create emojis collection.");
db.db()
.run_command(
doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
},
None,
)
.await
.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.
@@ -37,30 +37,46 @@ impl AbstractChannelUnread for MongoDb {
} }
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> { async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL) self.col::<Document>(COL)
.update_one( .delete_many(
doc! { doc! {
"_id.channel": { "_id.channel": {
"$in": channels "$in": channels
}, },
"_id.user": user, "_id.user": user
}, },
doc! { None,
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": Ulid::new().to_string()
}
},
UpdateOptions::builder().upsert(true).build(),
) )
.await .await
.map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
operation: "update", operation: "delete_many",
with: "channel_unread", with: "channel_unreads",
})?;
self.col::<Document>(COL)
.insert_many(
channels
.iter()
.map(|channel| {
doc! {
"_id": {
"channel": channel,
"user": user
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
}) })
.map(|_| ())
} }
async fn add_mention_to_unread<'a>( async fn add_mention_to_unread<'a>(
@@ -14,7 +14,7 @@ impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> { pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone(); let mut for_attachments = projection.clone();
for_attachments.insert( for_attachments.insert(
"attachment", "attachments",
doc! { doc! {
"$exists": 1_i32 "$exists": 1_i32
}, },
@@ -126,10 +126,7 @@ impl AbstractMessage for MongoDb {
} }
async fn delete_message(&self, id: &str) -> Result<()> { async fn delete_message(&self, id: &str) -> Result<()> {
self.delete_bulk_messages(doc! { self.delete_one_by_id(COL, id).await.map(|_| ())
"_id": id
})
.await
} }
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> { async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
@@ -282,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",
})
}
} }
@@ -122,4 +122,27 @@ impl AbstractAttachment for MongoDb {
with: "attachment", with: "attachment",
}) })
} }
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id": {
"$in": ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update",
with: "attachments",
})
}
} }
@@ -0,0 +1,69 @@
use bson::Document;
use crate::models::Emoji;
use crate::{AbstractEmoji, Error, Result};
use super::super::MongoDb;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmoji for MongoDb {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
self.find_one_by_id(COL, id).await
}
/// Fetch emoji by their ids
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
self.find(
COL,
doc! {
"parent.id": parent_id
},
)
.await
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
self.find(
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
},
)
.await
}
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
self.insert_one(COL, emoji).await.map(|_| ())
}
/// Delete an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
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",
})
}
}
+2 -1
View File
@@ -15,8 +15,9 @@ pub mod admin {
pub mod migrations; pub mod migrations;
} }
pub mod autumn { pub mod media {
pub mod attachment; pub mod attachment;
pub mod emoji;
} }
pub mod channels { pub mod channels {
@@ -18,6 +18,20 @@ impl MongoDb {
}) })
.await?; .await?;
// Delete all emoji.
self.col::<Document>("emojis")
.delete_many(
doc! {
"parent.id": &server.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "emojis",
})?;
// Delete all channels. // Delete all channels.
self.col::<Document>("channels") self.col::<Document>("channels")
.delete_many( .delete_many(
@@ -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>,
+34
View File
@@ -0,0 +1,34 @@
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
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
/// Representation of an Emoji on Revolt
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct Emoji {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// What owns this emoji
pub parent: EmojiParent,
/// Uploader user id
pub creator_id: String,
/// Emoji name
pub name: String,
/// Whether the emoji is animated
#[serde(skip_serializing_if = "if_false", default)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool,
}
+4 -2
View File
@@ -3,8 +3,9 @@ mod admin {
pub mod simple; pub mod simple;
} }
mod autumn { mod media {
pub mod attachment; pub mod attachment;
pub mod emoji;
} }
mod channels { mod channels {
@@ -27,8 +28,8 @@ mod users {
} }
pub use admin::*; pub use admin::*;
pub use autumn::*;
pub use channels::*; pub use channels::*;
pub use media::*;
pub use servers::*; pub use servers::*;
pub use users::*; pub use users::*;
@@ -37,6 +38,7 @@ pub use bot::Bot;
pub use channel::Channel; pub use channel::Channel;
pub use channel_invite::Invite; pub use channel_invite::Invite;
pub use channel_unread::ChannelUnread; pub use channel_unread::ChannelUnread;
pub use emoji::Emoji;
pub use message::Message; pub use message::Message;
pub use migrations::MigrationInfo; pub use migrations::MigrationInfo;
pub use server::Server; pub use server::Server;
+205
View File
@@ -0,0 +1,205 @@
<mxfile host="65bd71144e">
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
<mxGraphModel dx="1033" dy="710" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="56" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="6" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="57" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="275" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="58" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="342" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="59" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="300" y="250" as="targetPoint"/>
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="300" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
</mxCell>
<mxCell id="49" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="3" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="195" y="340"/>
<mxPoint x="245" y="340"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="290" y="160" as="targetPoint"/>
<Array as="points">
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
<mxPoint x="290" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="150" y="400" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="400" width="100" height="30" as="geometry"/>
</mxCell>
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="45" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="46" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="310" y="355"/>
<mxPoint x="260" y="355"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="6" value="Message" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="280" y="400" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="54" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="7" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="505" y="350"/>
<mxPoint x="355" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="7" value="Server Ban" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="470" y="400" width="70" height="30" as="geometry"/>
</mxCell>
<mxCell id="47" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="405" y="350"/>
<mxPoint x="355" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="48" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="405" y="350"/>
<mxPoint x="310" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="8" value="Server Member" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="360" y="400" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="29" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;startArrow=diamondThin;startFill=1;" parent="1" source="10" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="240" y="145"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="10" value="Bot" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="200" y="130" width="40" height="30" as="geometry"/>
</mxCell>
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="11" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="User Settings" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="150" y="210" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="15" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="12" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" value="User" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="280" y="130" width="40" height="30" as="geometry"/>
</mxCell>
<mxCell id="13" value="Account" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="310" y="40" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="14" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="14" value="Session" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="350" y="130" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="44" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="310" y="160" as="targetPoint"/>
<Array as="points">
<mxPoint x="355" y="220"/>
<mxPoint x="310" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9" value="Server" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="330" y="280" width="50" height="30" as="geometry"/>
</mxCell>
<mxCell id="55" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="4" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="290" y="160" as="targetPoint"/>
<mxPoint x="200" y="400" as="sourcePoint"/>
<Array as="points">
<mxPoint x="90" y="370"/>
<mxPoint x="195" y="370"/>
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
<mxPoint x="290" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="63" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#FFFFFF;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="61" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="584.25" y="335" as="targetPoint"/>
<Array as="points">
<mxPoint x="584" y="340"/>
<mxPoint x="390" y="340"/>
<mxPoint x="390" y="210"/>
<mxPoint x="320" y="210"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="64" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="61" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="584" y="340"/>
<mxPoint x="368" y="340"/>
<mxPoint x="368" y="310"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="61" value="Emoji" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="558.5" y="400" width="51.5" height="30" as="geometry"/>
</mxCell>
<mxCell id="65" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="61" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="500" as="sourcePoint"/>
<mxPoint x="320" y="440" as="targetPoint"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -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;
} }
} }
@@ -21,8 +21,10 @@ pub enum Permission {
ManagePermissions = 1 << 2, ManagePermissions = 1 << 2,
/// Manage roles on server /// Manage roles on server
ManageRole = 1 << 3, ManageRole = 1 << 3,
/// Manage server customisation (includes emoji)
ManageCustomisation = 1 << 4,
// % 2 bits reserved // % 1 bits reserved
// * Member permissions // * Member permissions
/// Kick other members below their ranking /// Kick other members below their ranking
@@ -63,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
@@ -96,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
@@ -122,6 +125,7 @@ bitfield! {
pub can_manage_server, _: 62; pub can_manage_server, _: 62;
pub can_manage_permissions, _: 61; pub can_manage_permissions, _: 61;
pub can_manage_roles, _: 60; pub can_manage_roles, _: 60;
pub can_manage_customisation, _: 59;
// * Member permissions // * Member permissions
pub can_kick_members, _: 57; pub can_kick_members, _: 57;
+42 -29
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,25 +111,36 @@ 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, .. } => {
Channel::DirectMessage { recipients, .. } => { if user == &data.perspective.id {
// 2. Fetch user. (*DEFAULT_PERMISSION_SAVED_MESSAGES).into()
let other_user = recipients
.iter()
.find(|x| x != &&data.perspective.id)
.unwrap();
let user = db.fetch_user(other_user).await?;
data.user.set(user);
// 3. Calculate user permissions.
let perms = data.calc_user(db).await;
// 4. Check if the user can send messages.
if perms.get_send_message() {
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else { } else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into() 0_u64.into()
}
}
Channel::DirectMessage { recipients, .. } => {
// 2. Ensure we are a recipient.
if recipients.contains(&data.perspective.id) {
// 3. Fetch user.
let other_user = recipients
.iter()
.find(|x| x != &&data.perspective.id)
.unwrap();
let user = db.fetch_user(other_user).await?;
data.user.set(user);
// 4. Calculate user permissions.
let perms = data.calc_user(db).await;
// 5. Check if the user can send messages.
if perms.get_send_message() {
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
}
} else {
0_u64.into()
} }
} }
Channel::Group { Channel::Group {
@@ -182,11 +194,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 +216,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<()>;
} }
@@ -3,6 +3,7 @@ use crate::Result;
#[async_trait] #[async_trait]
pub trait AbstractAttachment: Sync + Send { pub trait AbstractAttachment: Sync + Send {
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment( async fn find_and_use_attachment(
&self, &self,
id: &str, id: &str,
@@ -10,7 +11,16 @@ pub trait AbstractAttachment: Sync + Send {
parent_type: &str, parent_type: &str,
parent_id: &str, parent_id: &str,
) -> Result<File>; ) -> Result<File>;
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()>; async fn insert_attachment(&self, attachment: &File) -> Result<()>;
/// Mark an attachment as having been reported.
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>; async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
/// Mark an attachment as having been deleted.
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>; async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
} }
+20
View File
@@ -0,0 +1,20 @@
use crate::models::Emoji;
use crate::Result;
#[async_trait]
pub trait AbstractEmoji: Sync + Send {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>>;
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}
+5 -5
View File
@@ -2,8 +2,9 @@ mod admin {
pub mod migrations; pub mod migrations;
} }
mod autumn { mod media {
pub mod attachment; pub mod attachment;
pub mod emoji;
} }
mod channels { mod channels {
@@ -27,7 +28,8 @@ mod users {
pub use admin::migrations::AbstractMigrations; pub use admin::migrations::AbstractMigrations;
pub use autumn::attachment::AbstractAttachment; pub use media::attachment::AbstractAttachment;
pub use media::emoji::AbstractEmoji;
pub use channels::channel::AbstractChannel; pub use channels::channel::AbstractChannel;
pub use channels::channel_invite::AbstractChannelInvite; pub use channels::channel_invite::AbstractChannelInvite;
@@ -42,14 +44,12 @@ pub use users::bot::AbstractBot;
pub use users::user::AbstractUser; pub use users::user::AbstractUser;
pub use users::user_settings::AbstractUserSettings; pub use users::user_settings::AbstractUserSettings;
// pub trait AbstractEventEmitter {}
// + AbstractEventEmitter
pub trait AbstractDatabase: pub trait AbstractDatabase:
Sync Sync
+ Send + Send
+ AbstractMigrations + AbstractMigrations
+ AbstractAttachment + AbstractAttachment
+ AbstractEmoji
+ AbstractChannel + AbstractChannel
+ AbstractChannelInvite + AbstractChannelInvite
+ AbstractChannelUnread + AbstractChannelUnread
+12 -2
View File
@@ -202,12 +202,22 @@ impl Embed {
let mut finder = LinkFinder::new(); let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]); finder.kinds(&[LinkKind::Url]);
let links: HashSet<String> = finder // Process all links, stripping anchors and
// only taking up to `max_embeds` of links.
let links: Vec<String> = finder
.links(&content) .links(&content)
.map(|x| {
x.as_str()
.chars()
.take_while(|&ch| ch != '#')
.collect::<String>()
})
.collect::<HashSet<String>>()
.into_iter()
.take(max_embeds) .take(max_embeds)
.map(|x| x.as_str().to_string())
.collect(); .collect();
// If no links, fail out.
if links.is_empty() { if links.is_empty() {
return Err(Error::LabelMe); return Err(Error::LabelMe);
} }
+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;
+17 -2
View File
@@ -4,9 +4,9 @@ use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::{Bot, Channel, 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
@@ -63,6 +73,11 @@ impl Ref {
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> { pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await db.fetch_ban(server, &self.id).await
} }
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
}
} }
impl<'r> FromParam<'r> for Ref { impl<'r> FromParam<'r> for Ref {
+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()
});
+6
View File
@@ -41,6 +41,7 @@ pub enum Error {
TooManyAttachments, TooManyAttachments,
TooManyReplies, TooManyReplies,
EmptyMessage, EmptyMessage,
PayloadTooLarge,
CannotRemoveYourself, CannotRemoveYourself,
GroupTooLarge { GroupTooLarge {
max: usize, max: usize,
@@ -55,6 +56,7 @@ pub enum Error {
TooManyServers { TooManyServers {
max: usize, max: usize,
}, },
TooManyEmoji,
// ? Bot related errors. // ? Bot related errors.
ReachedMaximumBots, ReachedMaximumBots,
@@ -70,6 +72,7 @@ pub enum Error {
}, },
NotElevated, NotElevated,
CannotGiveMissingPermissions, CannotGiveMissingPermissions,
NotOwner,
// ? General errors. // ? General errors.
DatabaseError { DatabaseError {
@@ -145,6 +148,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::TooManyAttachments => Status::BadRequest, Error::TooManyAttachments => Status::BadRequest,
Error::TooManyReplies => Status::BadRequest, Error::TooManyReplies => Status::BadRequest,
Error::EmptyMessage => Status::UnprocessableEntity, Error::EmptyMessage => Status::UnprocessableEntity,
Error::PayloadTooLarge => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest, Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden, Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict, Error::AlreadyInGroup => Status::Conflict,
@@ -154,6 +158,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::InvalidRole => Status::NotFound, Error::InvalidRole => Status::NotFound,
Error::Banned => Status::Forbidden, Error::Banned => Status::Forbidden,
Error::TooManyServers { .. } => Status::Forbidden, Error::TooManyServers { .. } => Status::Forbidden,
Error::TooManyEmoji => Status::BadRequest,
Error::ReachedMaximumBots => Status::BadRequest, Error::ReachedMaximumBots => Status::BadRequest,
Error::IsBot => Status::BadRequest, Error::IsBot => Status::BadRequest,
@@ -163,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,
+8 -1
View File
@@ -100,7 +100,13 @@ fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r
if let Some(segment) = request.routed_segment(0) { if let Some(segment) = request.routed_segment(0) {
let resource = request.routed_segment(1); let resource = request.routed_segment(1);
match (segment, resource) { match (segment, resource) {
("users", _) => ("users", None), ("users", _) => {
if let Some("default_avatar") = request.routed_segment(2) {
return ("default_avatar", None);
}
("users", None)
}
("bots", _) => ("bots", None), ("bots", _) => ("bots", None),
("channels", Some(id)) => { ("channels", Some(id)) => {
if request.method() == Method::Post { if request.method() == Method::Post {
@@ -137,6 +143,7 @@ fn resolve_bucket_limit(bucket: &str) -> u8 {
"servers" => 5, "servers" => 5,
"auth" => 15, "auth" => 15,
"auth_delete" => 255, "auth_delete" => 255,
"default_avatar" => 255,
"swagger" => 100, "swagger" => 100,
_ => 20, _ => 20,
} }
+56
View File
@@ -1,18 +1,74 @@
version: "3.3" version: "3.3"
services: services:
# Redis
redis: redis:
image: eqalpha/keydb image: eqalpha/keydb
ports: ports:
- "6379:6379" - "6379:6379"
# MongoDB
database: database:
image: mongo image: mongo
ports: ports:
- "27017:27017" - "27017:27017"
volumes: volumes:
- ./.data/db:/data/db - ./.data/db:/data/db
# MinIO
minio:
image: minio/minio
command: server /data
env_file: .env
volumes:
- ./.data/minio:/data
ports:
- "10000:9000"
restart: always
# Mongo Express
mongo-express: mongo-express:
image: mongo-express image: mongo-express
ports: ports:
- "8081:8081" - "8081:8081"
environment: environment:
- ME_CONFIG_MONGODB_SERVER=database - ME_CONFIG_MONGODB_SERVER=database
depends_on:
- database
# Create buckets for minio.
createbuckets:
image: minio/mc
depends_on:
- minio
env_file: .env
entrypoint: >
/bin/sh -c "
while ! curl -s --output /dev/null --connect-timeout 1 http://minio:9000; do echo 'Waiting minio...' && sleep 0.1; done;
/usr/bin/mc alias set minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
/usr/bin/mc mb minio/attachments;
/usr/bin/mc mb minio/avatars;
/usr/bin/mc mb minio/backgrounds;
/usr/bin/mc mb minio/icons;
/usr/bin/mc mb minio/banners;
/usr/bin/mc mb minio/emojis;
exit 0;
"
# File server (autumn)
autumn:
image: ghcr.io/revoltchat/autumn:1.1.5
env_file: .env
depends_on:
- database
- createbuckets
environment:
- AUTUMN_MONGO_URI=mongodb://database
ports:
- "3000:3000"
restart: always
# Metadata and image proxy (january)
january:
image: ghcr.io/revoltchat/january:0.3.4
ports:
- "7000:7000"
restart: always