Compare commits

...

40 Commits

Author SHA1 Message Date
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 3436 additions and 165 deletions

21
Cargo.lock generated
View File

@@ -1995,9 +1995,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "opaque-debug"
@@ -2505,7 +2505,7 @@ dependencies = [
[[package]]
name = "rauth"
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 = [
"async-std",
"async-trait",
@@ -2714,7 +2714,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "1.0.6-patch.2"
version = "0.5.5"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2730,7 +2730,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.5.3-7"
version = "0.5.5"
dependencies = [
"async-channel",
"async-std",
@@ -2792,6 +2792,7 @@ dependencies = [
"nanoid",
"num_enum",
"okapi",
"once_cell",
"optional_struct",
"pretty_env_logger",
"rauth",
@@ -2986,23 +2987,15 @@ dependencies = [
[[package]]
name = "rocket_rauth"
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 = [
"async-std",
"base32",
"chrono",
"iso8601-timestamp",
"mongodb",
"okapi",
"rauth",
"regex",
"reqwest",
"rocket",
"rocket_empty",
"rocket_okapi",
"schemars",
"serde",
"serde_json",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "1.0.6-patch.2"
version = "0.5.5"
license = "AGPL-3.0-or-later"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.5.3-7"
version = "0.5.5"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -52,7 +52,7 @@ mobc-redis = { version = "0.7.0", default-features = false, features = ["async-s
# web
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
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
schemars = "0.8.8"

View File

@@ -16,19 +16,18 @@ use validator::Validate;
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
/// Whether this channel is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
@@ -59,12 +58,47 @@ pub async fn req(
&& data.description.is_none()
&& data.icon.is_none()
&& data.nsfw.is_none()
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
}
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
*owner = new_owner.to_string();
// Notify clients
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
match &mut channel {
Channel::Group {
id,

View File

@@ -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)
}

View File

@@ -36,6 +36,8 @@ pub async fn req(
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?;
if message.channel != target {
return Err(Error::NotFound);

View File

@@ -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)
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_quark::{
models::{
message::{Masquerade, Reply, SendableEmbed},
message::{Interactions, Masquerade, Reply, SendableEmbed},
Message, User,
},
perms,
@@ -21,6 +21,7 @@ pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
nonce: Option<String>,
/// Message content to send
@@ -32,11 +33,15 @@ pub struct DataMessageSend {
/// Messages to reply to
replies: Option<Vec<Reply>>,
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate(length(min = 1, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
interactions: Option<Interactions>,
}
lazy_static! {
@@ -60,6 +65,8 @@ pub async fn message_send(
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&data.content, &data.embeds)?;
idempotency.consume_nonce(data.nonce).await?;
let channel = target.as_channel(db).await?;
@@ -81,6 +88,7 @@ pub async fn message_send(
channel: channel.id().to_string(),
author: user.id.clone(),
masquerade: data.masquerade,
interactions: data.interactions.unwrap_or_default(),
..Default::default()
};
@@ -95,13 +103,22 @@ pub async fn message_send(
}
// 2. Verify permissions for masquerade.
if message.masquerade.is_some() {
if let Some(masq) = &message.masquerade {
permissions
.throw_permission(db, Permission::Masquerade)
.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();
if let Some(entries) = data.replies {
if entries.len() > 5 {
@@ -134,7 +151,7 @@ pub async fn message_send(
.replace(replies.into_iter().collect::<Vec<String>>());
}
// 4. Process included embeds.
// 5. Process included embeds.
let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds {
@@ -146,7 +163,7 @@ pub async fn message_send(
message.embeds.replace(embeds);
}
// 5. Add attachments to message.
// 6. Add attachments to message.
let mut attachments = vec![];
if let Some(ids) = &data.attachments {
if !ids.is_empty() {
@@ -172,10 +189,10 @@ pub async fn message_send(
message.attachments.replace(attachments);
}
// 6. Set content
// 7. Set 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.create(db, &channel, Some(&user)).await?;

View File

@@ -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)
}

View File

@@ -11,13 +11,16 @@ mod group_remove_member;
mod invite_create;
mod members_fetch;
mod message_bulk_delete;
mod message_clear_reactions;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_react;
mod message_search;
mod message_send;
mod message_unreact;
mod permissions_set;
mod permissions_set_default;
mod voice_join;
@@ -44,5 +47,8 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
voice_join::req,
permissions_set::req,
permissions_set_default::req,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions
]
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
]
}

View File

@@ -5,6 +5,7 @@ use rocket_okapi::{okapi::openapi3::OpenApi, settings::OpenApiSettings};
mod bots;
mod channels;
mod customisation;
mod invites;
mod onboard;
mod push;
@@ -25,6 +26,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/auth/account" => rocket_rauth::routes::account::routes(),
"/auth/session" => rocket_rauth::routes::session::routes(),
"/auth/mfa" => rocket_rauth::routes::mfa::routes(),
@@ -78,6 +80,7 @@ fn custom_openapi_spec() -> OpenApi {
"Channel Invites",
"Channel Permissions",
"Messaging",
"Interactions",
"Groups",
"Voice"
]
@@ -96,6 +99,12 @@ fn custom_openapi_spec() -> OpenApi {
"Invites"
]
},
{
"name": "Customisation",
"tags": [
"Emojis"
]
},
{
"name": "Authentication",
"tags": [

View File

@@ -32,13 +32,10 @@ pub async fn req(
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if db.is_username_taken(&data.username).await? {
return Err(Error::UsernameTaken);
}
let username = User::validate_username(db, data.username).await?;
let user = User {
id: session.user_id,
username: data.username,
username,
..Default::default()
};

View File

@@ -1,5 +1,5 @@
use revolt_quark::{
models::{ServerBan, User},
models::{server_member::MemberCompositeKey, ServerBan, User},
perms, Db, Error, Permission, Ref, Result,
};
@@ -47,13 +47,27 @@ pub async fn req(
.throw_permission(db, Permission::BanMembers)
.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())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
server.ban_member(db, member, data.reason).await.map(Json)
} else {
let server_id = server.id.to_string();
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)
}

View File

@@ -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)
}

View File

@@ -5,7 +5,7 @@ use revolt_quark::{
server_member::{FieldsMember, PartialMember},
File, Member, User,
},
perms, Db, Error, Permission, Ref, Result,
perms, Db, Error, Permission, Ref, Result, Timestamp,
};
use rocket::serde::json::Json;
@@ -22,6 +22,8 @@ pub struct DataMemberEdit {
avatar: Option<String>,
/// Array of role ids
roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsMember>>,
@@ -105,13 +107,7 @@ pub async fn req(
// Check permissions against roles in diff
if let Some(roles) = &data.roles {
let fallback = vec![];
let current_roles = member
.roles
.as_ref()
.unwrap_or(&fallback)
.iter()
.collect::<HashSet<&String>>();
let current_roles = member.roles.iter().collect::<HashSet<&String>>();
let new_roles = roles.iter().collect::<HashSet<&String>>();
let added_roles: Vec<&&String> = new_roles.difference(&current_roles).collect();
@@ -132,12 +128,14 @@ pub async fn req(
nickname,
avatar,
roles,
timeout,
remove,
} = data;
let mut partial = PartialMember {
nickname,
roles,
timeout,
..Default::default()
};

View File

@@ -1,30 +1,26 @@
use rocket::Route;
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_list;
mod ban_remove;
mod channel_create;
mod emoji_list;
mod invites_fetch;
mod member_edit;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
mod permissions_set;
mod permissions_set_default;
mod roles_create;
mod roles_delete;
mod roles_edit;
mod server_ack;
mod server_create;
mod server_delete;
mod server_edit;
mod server_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
@@ -46,6 +42,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
roles_edit::req,
roles_delete::req,
permissions_set::req,
permissions_set_default::req
permissions_set_default::req,
emoji_list::list_emoji
]
}

View File

@@ -3,7 +3,9 @@ use revolt_quark::{
server::{FieldsRole, PartialRole, Role},
User,
},
perms, Db, Error, Permission, Ref, Result,
perms,
util::regex::RE_COLOUR,
Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
@@ -17,7 +19,7 @@ pub struct DataEditRole {
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Role colour
#[validate(length(min = 1, max = 32))]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
colour: Option<String>,
/// Whether this role should be displayed separately
hoist: Option<bool>,

View File

@@ -33,6 +33,8 @@ pub struct DataEditServer {
// Whether this server is age-restricted
// 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
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
@@ -102,6 +104,7 @@ pub async fn req(
categories,
system_messages,
// nsfw,
discoverable,
analytics,
remove,
} = data;
@@ -112,6 +115,7 @@ pub async fn req(
categories,
system_messages,
// nsfw,
discoverable,
analytics,
..Default::default()
};

View File

@@ -10,11 +10,7 @@ use rocket::State;
#[openapi(tag = "Relationships")]
#[put("/<target>/friend")]
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 {
user
} else {
target.as_user(db).await?
};
let mut target = target.as_user(db).await?;
if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot);

View File

@@ -6,4 +6,9 @@ use regex::Regex;
/// Block zero width space
/// Block lookalike characters
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());

View File

@@ -68,6 +68,7 @@ impl_ops = "0.1.1"
num_enum = "0.5.6"
reqwest = "0.11.10"
bitfield = "0.13.2"
once_cell = "1.13.0"
lazy_static = "1.4.0"
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" }
# 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 = "0.25.0"

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ use crate::models::message::{AppendMessage, PartialMessage};
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer};
use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember};
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;
/// WebSocket Client Errors
@@ -52,6 +52,7 @@ pub enum EventV1 {
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
},
/// Ping response
@@ -77,6 +78,29 @@ pub enum EventV1 {
/// Delete message
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
BulkMessageDelete { channel: String, ids: Vec<String> },
@@ -170,4 +194,10 @@ pub enum EventV1 {
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
}

View File

@@ -111,6 +111,7 @@ impl State {
// Fetch all memberships with their corresponding servers.
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 servers = db.fetch_servers(&server_ids).await?;
@@ -151,6 +152,17 @@ impl State {
)
.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.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache
@@ -202,6 +214,7 @@ impl State {
servers,
channels,
members,
emojis,
})
}
@@ -472,10 +485,8 @@ impl State {
if data.rank.is_some() || data.permissions.is_some() {
if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles {
if roles.contains(role_id) {
queue_server = Some(id.clone());
}
if member.roles.contains(role_id) {
queue_server = Some(id.clone());
}
}
}
@@ -486,10 +497,8 @@ impl State {
}
if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles {
if roles.contains(role_id) {
queue_server = Some(id.clone());
}
if member.roles.contains(role_id) {
queue_server = Some(id.clone());
}
}
}

View File

@@ -64,4 +64,22 @@ impl AbstractMessage for DummyDb {
) -> Result<Vec<Message>> {
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(())
}
}

View File

@@ -39,4 +39,9 @@ impl AbstractAttachment for DummyDb {
info!("Marked {id} as deleted");
Ok(())
}
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
info!("Marked {ids:?} as deleted");
Ok(())
}
}

View File

@@ -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(())
}
}

View File

@@ -4,8 +4,9 @@ pub mod admin {
pub mod migrations;
}
pub mod autumn {
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {

View File

@@ -3,6 +3,8 @@ use crate::{AbstractServerMember, Result};
use super::super::DummyDb;
use iso8601_timestamp::Timestamp;
#[async_trait]
impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
@@ -11,9 +13,11 @@ impl AbstractServerMember for DummyDb {
server: server.into(),
user: user.into(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: None,
roles: vec![],
timeout: None,
})
}

View File

@@ -242,7 +242,12 @@ impl Channel {
/// Add user to a group
pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> {
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 {
@@ -300,6 +305,15 @@ impl Channel {
vec![],
)
.await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.into(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());

View File

@@ -8,9 +8,10 @@ use crate::{
events::client::EventV1,
models::{
message::{
AppendMessage, BulkMessageResponse, PartialMessage, SendableEmbed, SystemMessage,
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
SystemMessage,
},
Channel, Message, User,
Channel, Emoji, Message, User,
},
presence::presence_filter_online,
tasks::ack::AckEvent,
@@ -135,7 +136,17 @@ impl Message {
/// Delete a message
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?;
EventV1::MessageDelete {
id: self.id,
channel: self.channel.clone(),
@@ -156,6 +167,104 @@ impl Message {
.await;
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 {
@@ -227,6 +336,9 @@ impl From<SystemMessage> for String {
"Channel description 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()
}
}

View File

@@ -30,4 +30,9 @@ impl File {
db.find_and_use_attachment(id, "banners", "server", parent)
.await
}
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "emojis", "object", parent)
.await
}
}

View File

@@ -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))
}
}
}

View File

@@ -4,8 +4,9 @@ pub mod admin {
pub mod migrations;
}
pub mod autumn {
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {

View File

@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use ulid::Ulid;
use crate::{
@@ -188,7 +189,11 @@ impl Server {
server: self.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?;
@@ -278,6 +283,18 @@ impl Server {
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
pub async fn ban_member(
self,
@@ -285,16 +302,10 @@ impl Server {
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan {
id: member.id.clone(),
reason,
};
self.remove_member(db, member, RemovalIntention::Ban)
self.remove_member(db, member.clone(), RemovalIntention::Ban)
.await?;
db.insert_ban(&ban).await?;
Ok(ban)
self.ban_user(db, member.id, reason).await
}
}

View File

@@ -1,3 +1,5 @@
use iso8601_timestamp::Timestamp;
use crate::{
events::client::EventV1,
models::{
@@ -36,19 +38,24 @@ impl Member {
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
if let Some(roles) = &self.roles {
let mut value = i64::MAX;
for role in roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
value = role.rank;
}
let mut value = i64::MAX;
for role in &self.roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
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 {
i64::MAX
false
}
}
@@ -56,7 +63,8 @@ impl Member {
match field {
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
}
}
}

View File

@@ -150,8 +150,8 @@ impl User {
Ok(db.fetch_server_count(&self.id).await? <= 100)
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
/// Sanitise and validate a username can be used
pub async fn validate_username(db: &Database, username: String) -> Result<String> {
// Trim surrounding spaces
let username = username.trim().to_string();
@@ -173,7 +173,7 @@ impl User {
}
// 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 {
if username_lowercase.contains(substr) {
@@ -186,10 +186,15 @@ impl User {
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(
db,
PartialUser {
username: Some(username),
username: Some(User::validate_username(db, username).await?),
..Default::default()
},
vec![],

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use bson::Bson;
use bson::{Bson, DateTime};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_bson, from_document, to_document, Document},
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 16;
pub const LATEST_REVISION: i32 = 18;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -611,6 +611,56 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.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`.
// Reminder to update LATEST_REVISION when adding new migrations.

View File

@@ -37,30 +37,46 @@ impl AbstractChannelUnread for MongoDb {
}
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.update_one(
.delete_many(
doc! {
"_id.channel": {
"$in": channels
},
"_id.user": user,
"_id.user": user
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": Ulid::new().to_string()
}
},
UpdateOptions::builder().upsert(true).build(),
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update",
with: "channel_unread",
operation: "delete_many",
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>(

View File

@@ -14,7 +14,7 @@ impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachment",
"attachments",
doc! {
"$exists": 1_i32
},
@@ -126,10 +126,7 @@ impl AbstractMessage for MongoDb {
}
async fn delete_message(&self, id: &str) -> Result<()> {
self.delete_bulk_messages(doc! {
"_id": id
})
.await
self.delete_one_by_id(COL, id).await.map(|_| ())
}
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
@@ -282,4 +279,70 @@ impl AbstractMessage for MongoDb {
)
.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",
})
}
}

View File

@@ -122,4 +122,27 @@ impl AbstractAttachment for MongoDb {
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",
})
}
}

View File

@@ -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",
})
}
}

View File

@@ -15,8 +15,9 @@ pub mod admin {
pub mod migrations;
}
pub mod autumn {
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {

View File

@@ -18,6 +18,20 @@ impl MongoDb {
})
.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.
self.col::<Document>("channels")
.delete_many(

View File

@@ -129,6 +129,7 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
})
}
}

View File

@@ -1,3 +1,6 @@
use crate::util::regex::RE_COLOUR;
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -11,6 +14,11 @@ use crate::{
types::january::Embed,
};
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// # Reply
///
/// Representation of a message reply before it is sent.
@@ -33,7 +41,7 @@ pub struct SendableEmbed {
#[validate(length(min = 1, max = 2000))]
pub description: 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>,
}
@@ -61,6 +69,8 @@ pub enum SystemMessage {
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
/// Name and / or avatar override information
@@ -74,6 +84,25 @@ pub struct Masquerade {
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
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
@@ -115,6 +144,12 @@ pub struct Message {
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
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
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,

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,
}

View File

@@ -3,8 +3,9 @@ mod admin {
pub mod simple;
}
mod autumn {
mod media {
pub mod attachment;
pub mod emoji;
}
mod channels {
@@ -27,8 +28,8 @@ mod users {
}
pub use admin::*;
pub use autumn::*;
pub use channels::*;
pub use media::*;
pub use servers::*;
pub use users::*;
@@ -37,6 +38,7 @@ pub use bot::Bot;
pub use channel::Channel;
pub use channel_invite::Invite;
pub use channel_unread::ChannelUnread;
pub use emoji::Emoji;
pub use message::Message;
pub use migrations::MigrationInfo;
pub use server::Server;

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>

View File

@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
use crate::models::attachment::File;
@@ -12,7 +13,7 @@ pub struct MemberCompositeKey {
}
/// 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_name = "PartialMember"]
#[opt_skip_serializing_none]
@@ -22,6 +23,9 @@ pub struct Member {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
pub joined_at: Timestamp,
/// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
@@ -30,8 +34,11 @@ pub struct Member {
pub avatar: Option<File>,
/// 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")]
pub roles: Option<Vec<String>>,
pub timeout: Option<Timestamp>,
}
/// Optional fields on server member object
@@ -40,6 +47,7 @@ pub enum FieldsMember {
Nickname,
Avatar,
Roles,
Timeout,
}
/// Member removal intention

View File

@@ -45,8 +45,23 @@ impl Override {
impl PermissionValue {
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.0 |= v.allow;
self.0 &= !v.deny;
self.allow(v.allow);
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;
}
}

View File

@@ -21,8 +21,10 @@ pub enum Permission {
ManagePermissions = 1 << 2,
/// Manage roles on server
ManageRole = 1 << 3,
/// Manage server customisation (includes emoji)
ManageCustomisation = 1 << 4,
// % 2 bits reserved
// % 1 bits reserved
// * Member permissions
/// Kick other members below their ranking
@@ -63,8 +65,8 @@ pub enum Permission {
UploadFiles = 1 << 27,
/// Masquerade messages using custom nickname and avatar
Masquerade = 1 << 28,
// % 1 bits reserved
/// React to messages with emojis
React = 1 << 29,
// * Voice permissions
/// 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 });
lazy_static! {
pub static ref ALLOW_IN_TIMEOUT: u64 = Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 =
Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY
@@ -122,6 +125,7 @@ bitfield! {
pub can_manage_server, _: 62;
pub can_manage_permissions, _: 61;
pub can_manage_roles, _: 60;
pub can_manage_customisation, _: 59;
// * Member permissions
pub can_kick_members, _: 57;

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use crate::{
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,
};
@@ -55,11 +55,7 @@ async fn calculate_server_permission(
let mut permissions: PermissionValue = server.default_permissions.into();
// 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() {
roles.iter().collect()
} else {
HashSet::new()
};
let member_roles: HashSet<&String> = member.roles.iter().collect();
if !member_roles.is_empty() {
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)
}
@@ -110,7 +111,13 @@ async fn calculate_channel_permission(
// 1. Check channel type.
let value: PermissionValue = match channel {
Channel::SavedMessages { .. } => (*DEFAULT_PERMISSION_SAVED_MESSAGES).into(),
Channel::SavedMessages { user, .. } => {
if user == &data.perspective.id {
(*DEFAULT_PERMISSION_SAVED_MESSAGES).into()
} else {
0_u64.into()
}
}
Channel::DirectMessage { recipients, .. } => {
// 2. Fetch user.
let other_user = recipients
@@ -182,11 +189,7 @@ async fn calculate_channel_permission(
}
// 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() {
roles.iter().collect()
} else {
HashSet::new()
};
let member_roles: HashSet<&String> = member.roles.iter().collect();
if !member_roles.is_empty() {
let mut roles = role_permissions
@@ -208,6 +211,11 @@ async fn calculate_channel_permission(
}
}
// 5. Revoke permissions if member is timed out.
if member.in_timeout() {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
} else {
(Permission::GrantAllSafe as u64).into()

View File

@@ -42,4 +42,13 @@ pub trait AbstractMessage: Sync + Send {
after: Option<String>,
sort: MessageSort,
) -> 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<()>;
}

View File

@@ -3,6 +3,7 @@ use crate::Result;
#[async_trait]
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(
&self,
id: &str,
@@ -10,7 +11,16 @@ pub trait AbstractAttachment: Sync + Send {
parent_type: &str,
parent_id: &str,
) -> Result<File>;
/// Insert attachment into database.
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<()>;
/// Mark an attachment as having been deleted.
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<()>;
}

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<()>;
}

View File

@@ -2,8 +2,9 @@ mod admin {
pub mod migrations;
}
mod autumn {
mod media {
pub mod attachment;
pub mod emoji;
}
mod channels {
@@ -27,7 +28,8 @@ mod users {
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_invite::AbstractChannelInvite;
@@ -42,14 +44,12 @@ pub use users::bot::AbstractBot;
pub use users::user::AbstractUser;
pub use users::user_settings::AbstractUserSettings;
// pub trait AbstractEventEmitter {}
// + AbstractEventEmitter
pub trait AbstractDatabase:
Sync
+ Send
+ AbstractMigrations
+ AbstractAttachment
+ AbstractEmoji
+ AbstractChannel
+ AbstractChannelInvite
+ AbstractChannelUnread

View File

@@ -202,12 +202,22 @@ impl Embed {
let mut finder = LinkFinder::new();
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)
.map(|x| {
x.as_str()
.chars()
.take_while(|&ch| ch != '#')
.collect::<String>()
})
.collect::<HashSet<String>>()
.into_iter()
.take(max_embeds)
.map(|x| x.as_str().to_string())
.collect();
// If no links, fail out.
if links.is_empty() {
return Err(Error::LabelMe);
}

View File

@@ -3,6 +3,7 @@ pub mod manipulation;
pub mod pfp;
pub mod rauth;
pub mod r#ref;
pub mod regex;
pub mod result;
pub mod value;
pub mod variables;

View File

@@ -4,9 +4,9 @@ use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
use schemars::JsonSchema;
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::{Database, Result};
use crate::{Database, Error, Result};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
@@ -44,6 +44,16 @@ impl Ref {
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
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
@@ -63,6 +73,11 @@ impl Ref {
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
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 {

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()
});

View File

@@ -41,6 +41,7 @@ pub enum Error {
TooManyAttachments,
TooManyReplies,
EmptyMessage,
PayloadTooLarge,
CannotRemoveYourself,
GroupTooLarge {
max: usize,
@@ -55,6 +56,7 @@ pub enum Error {
TooManyServers {
max: usize,
},
TooManyEmoji,
// ? Bot related errors.
ReachedMaximumBots,
@@ -70,6 +72,7 @@ pub enum Error {
},
NotElevated,
CannotGiveMissingPermissions,
NotOwner,
// ? General errors.
DatabaseError {
@@ -145,6 +148,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::TooManyAttachments => Status::BadRequest,
Error::TooManyReplies => Status::BadRequest,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::PayloadTooLarge => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict,
@@ -154,6 +158,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::InvalidRole => Status::NotFound,
Error::Banned => Status::Forbidden,
Error::TooManyServers { .. } => Status::Forbidden,
Error::TooManyEmoji => Status::BadRequest,
Error::ReachedMaximumBots => Status::BadRequest,
Error::IsBot => Status::BadRequest,
@@ -163,6 +168,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::MissingUserPermission { .. } => Status::Forbidden,
Error::NotElevated => Status::Forbidden,
Error::CannotGiveMissingPermissions => Status::Forbidden,
Error::NotOwner => Status::Forbidden,
Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError,

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) {
let resource = request.routed_segment(1);
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),
("channels", Some(id)) => {
if request.method() == Method::Post {
@@ -137,6 +143,7 @@ fn resolve_bucket_limit(bucket: &str) -> u8 {
"servers" => 5,
"auth" => 15,
"auth_delete" => 255,
"default_avatar" => 255,
"swagger" => 100,
_ => 20,
}

View File

@@ -1,18 +1,74 @@
version: "3.3"
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.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:
image: mongo-express
ports:
- "8081:8081"
environment:
- 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