Merge remote-tracking branch 'origin/main' into livekit

This commit is contained in:
Zomatree
2025-01-26 20:36:40 +00:00
227 changed files with 16549 additions and 2943 deletions

View File

@@ -56,6 +56,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes", None)
.await
.expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
@@ -146,7 +150,14 @@ pub async fn create_database(db: &MongoDb) {
"author": 1_i32
},
"name": "author"
}
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
},
]
},
None,
@@ -202,6 +213,46 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create server_members index.");
db.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
db.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.await
.expect("Failed to create attachment_hashes index.");
db.collection("migrations")
.insert_one(
doc! {

View File

@@ -5,11 +5,13 @@ use crate::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::oid::ObjectId;
use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
@@ -19,7 +21,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 27;
pub const LATEST_REVISION: i32 = 31;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -984,25 +986,266 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
if revision <= 26 {
info!("Running migration [revision 26 / 17-04-2024]: Add `can_publish` and `can_receive` to members");
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
auto_derived!(
pub enum OldInvite {
Server {
#[serde(rename = "_id")]
code: String,
server: String,
creator: String,
channel: String,
},
Group {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
},
}
);
#[derive(serde::Serialize, serde::Deserialize)]
struct Outer {
_id: ObjectId,
#[serde(flatten)]
invite: OldInvite,
}
let invites = db
.db()
.collection::<Outer>("channel_invites")
.find(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites")
.filter_map(|s| async { s.ok() })
.collect::<Vec<Outer>>()
.await
.into_iter()
.map(|invite| match invite.invite {
OldInvite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
OldInvite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
})
.collect::<Vec<Invite>>();
if !invites.is_empty() {
db.db()
.collection("channel_invites")
.insert_many(invites, None)
.await
.expect("failed to insert corrected invite");
db.db()
.collection::<Outer>("channel_invites")
.delete_many(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites");
}
}
if revision <= 27 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db()
.create_collection("attachment_hashes", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
db.db()
.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.await
.expect("Failed to create attachment_hashes index.");
}
// Revision 29 omitted due to bug.
if revision <= 30 {
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
}
if revision <= 31 {
info!("Running migration [revision 31 / 31-10-2024]: Add creator_id to webhooks and delete those whose channels don't exist.");
#[derive(serde::Serialize, serde::Deserialize)]
struct WebhookShell {
_id: String,
channel_id: String,
}
let webhooks = db
.db()
.collection::<WebhookShell>("channel_webhooks")
.find(doc! {}, None)
.await
.expect("webhooks")
.filter_map(|s| async { s.ok() })
.collect::<Vec<WebhookShell>>()
.await;
for webhook in webhooks {
match db.fetch_channel(&webhook.channel_id).await {
Ok(channel) => {
let creator_id = match channel {
Channel::Group { owner, .. } => owner,
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
let server = db.fetch_server(&server).await.expect("server");
server.owner
}
_ => unreachable!("not server or group channel!"),
};
db.db()
.collection::<Document>("channel_webhooks")
.update_one(
doc! {
"_id": webhook._id,
},
doc! {
"$set" : {
"creator_id": creator_id
}
},
None,
)
.await
.expect("update webhook");
}
Err(Error {
error_type: ErrorType::NotFound,
..
}) => {
db.db()
.collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }, None)
.await
.expect("failed to delete invalid webhook");
}
Err(err) => panic!("{err:?}"),
}
}
}
if revision <= 32 {
info!("Running migration [revision 32 / 26-01-2025]: Add `is_publishing` and `is_receiving` to members");
db.col::<Document>("server_members")
.update_many(
doc! {},
doc! {
"$set": {
"can_publish": true,
"can_receive": true
"is_publishing": true,
"is_receiving": true
}
},
None
None,
)
.await
.expect("Failed to update members");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}

View File

@@ -2,7 +2,7 @@ use revolt_config::config;
use revolt_result::Result;
use ulid::Ulid;
use crate::{BotInformation, Database, PartialUser, User};
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
auto_derived_partial!(
/// Bot
@@ -72,7 +72,12 @@ impl Default for Bot {
#[allow(clippy::disallowed_methods)]
impl Bot {
/// Create a new bot
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
pub async fn create<D>(
db: &Database,
username: String,
owner: &User,
data: D,
) -> Result<(Bot, User)>
where
D: Into<Option<PartialBot>>,
{
@@ -80,14 +85,13 @@ impl Bot {
return Err(create_error!(IsBot));
}
let config = config().await;
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
return Err(create_error!(ReachedMaximumBots));
}
let id = Ulid::new().to_string();
User::create(
let user = User::create(
db,
username,
Some(id.to_string()),
@@ -112,7 +116,7 @@ impl Bot {
}
db.insert_bot(&bot).await?;
Ok(bot)
Ok((bot, user))
}
/// Remove a field from this object
@@ -142,6 +146,10 @@ impl Bot {
db.update_bot(&self.id, &partial, remove).await?;
if partial.token.is_some() {
EventV1::Logout.private(self.id.clone()).await;
}
self.apply_options(partial);
Ok(())
}
@@ -164,7 +172,7 @@ mod tests {
.await
.unwrap();
let bot = Bot::create(
let (bot, _) = Bot::create(
&db,
"Bot Name".to_string(),
&owner,

View File

@@ -10,6 +10,7 @@ static ALPHABET: [char; 54] = [
auto_derived!(
/// Invite
#[serde(tag = "type")]
pub enum Invite {
/// Invite to a specific server channel
Server {

View File

@@ -7,13 +7,13 @@ mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message.
/// Acknowledge a message, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()>;
) -> Result<Option<ChannelUnread>>;
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
@@ -26,6 +26,12 @@ pub trait AbstractChannelUnreads: Sync + Send {
message_ids: &[String],
) -> Result<()>;
/// Fetch all unreads with mentions for a user.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
}

View File

@@ -1,4 +1,6 @@
use bson::Document;
use mongodb::options::FindOneAndUpdateOptions;
use mongodb::options::ReturnDocument;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid;
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnreads for MongoDb {
/// Acknowledge a message.
/// Acknowledge a message, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
) -> Result<Option<ChannelUnread>> {
self.col::<ChannelUnread>(COL)
.find_one_and_update(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$unset": {
"mentions": 1_i32
"$pull": {
"mentions": {
"$lte": message_id
}
},
"$set": {
"last_id": message_id
}
},
UpdateOptions::builder().upsert(true).build(),
FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(ReturnDocument::After)
.build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
@@ -116,4 +122,29 @@ impl AbstractChannelUnreads for MongoDb {
}
)
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query! {
self,
find,
COL,
doc! {
"_id.user": user_id,
"mentions": {"$ne": null}
}
}
}
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
query!(
self,
find_one,
COL,
doc! {
"_id.user": user_id,
"_id.channel": channel_id
}
)
}
}

View File

@@ -13,7 +13,7 @@ impl AbstractChannelUnreads for ReferenceDb {
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
) -> Result<Option<ChannelUnread>> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
@@ -27,14 +27,14 @@ impl AbstractChannelUnreads for ReferenceDb {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
id: key.clone(),
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(())
Ok(unreads.get(&key).cloned())
}
/// Acknowledge many channels.
@@ -78,6 +78,15 @@ impl AbstractChannelUnreads for ReferenceDb {
Ok(())
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.values()
.filter(|unread| unread.id.user == user_id && unread.mentions.is_some())
.cloned()
.collect())
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
@@ -87,4 +96,16 @@ impl AbstractChannelUnreads for ReferenceDb {
.cloned()
.collect())
}
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.get(&ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
})
.cloned())
}
}

View File

@@ -17,6 +17,9 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// User that created this webhook
pub creator_id: String,
/// The channel this webhook belongs to
pub channel_id: String,
@@ -43,6 +46,7 @@ impl Default for Webhook {
id: Default::default(),
name: Default::default(),
avatar: None,
creator_id: Default::default(),
channel_id: Default::default(),
permissions: Default::default(),
token: Default::default(),
@@ -70,7 +74,7 @@ impl Webhook {
if self.token.as_deref() == Some(token) {
Ok(())
} else {
Err(create_error!(InvalidCredentials))
Err(create_error!(NotAuthenticated))
}
}

View File

@@ -9,7 +9,7 @@ use ulid::Ulid;
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User,
Server, SystemMessage, User, AMQP,
};
auto_derived!(
@@ -205,9 +205,9 @@ impl Channel {
update_server: bool,
) -> Result<Channel> {
let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels {
if server.channels.len() > config.features.limits.global.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels,
max: config.features.limits.global.server_channels,
}));
};
@@ -268,9 +268,9 @@ impl Channel {
data.users.insert(owner_id.to_string());
let config = config().await;
if data.users.len() > config.features.limits.default.group_size {
if data.users.len() > config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
max: config.features.limits.global.group_size,
}));
}
@@ -342,6 +342,7 @@ impl Channel {
pub async fn add_user_to_group(
&mut self,
db: &Database,
amqp: &AMQP,
user: &User,
by_id: &str,
) -> Result<()> {
@@ -351,9 +352,9 @@ impl Channel {
}
let config = config().await;
if recipients.len() >= config.features.limits.default.group_size {
if recipients.len() >= config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size
max: config.features.limits.global.group_size
}));
}
@@ -378,10 +379,13 @@ impl Channel {
.into_message(id.to_string())
.send(
db,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
self,
false,
)
@@ -420,13 +424,13 @@ impl Channel {
}
/// Clone this channel's id
pub fn id(&self) -> String {
pub fn id(&self) -> &str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id.clone(),
| Channel::VoiceChannel { id, .. } => id,
}
}
@@ -659,7 +663,7 @@ impl Channel {
.private(user.to_string())
.await;
crate::tasks::ack::queue(
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
@@ -675,6 +679,7 @@ impl Channel {
pub async fn remove_user_from_group(
&self,
db: &Database,
amqp: &AMQP,
user: &User,
by_id: Option<&str>,
silent: bool,
@@ -706,10 +711,13 @@ impl Channel {
.into_message(id.to_string())
.send(
db,
Some(amqp),
MessageAuthor::System {
username: name,
avatar: None,
},
None,
None,
self,
false,
)
@@ -720,6 +728,8 @@ impl Channel {
}
}
db.remove_user_from_group(id, &user.id).await?;
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user.id.to_string(),
@@ -741,10 +751,13 @@ impl Channel {
.into_message(id.to_string())
.send(
db,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
self,
false,
)

View File

@@ -261,12 +261,12 @@ impl AbstractChannels for MongoDb {
// Delete associated attachments
self.delete_many_attachments(doc! {
"object_id": &id
"used_for.id": &id
})
.await?;
// Delete the channel itself
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
}
}

View File

@@ -11,7 +11,7 @@ impl AbstractChannels for ReferenceDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
entry.insert(channel.clone());
Ok(())
} else {
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if channels.remove(&channel.id()).is_some() {
if channels.remove(channel.id()).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,92 @@
use iso8601_timestamp::Timestamp;
use crate::File;
auto_derived_partial!(
/// File hash
pub struct FileHash {
/// Sha256 hash of the file
#[serde(rename = "_id")]
pub id: String,
/// Sha256 hash of file after it has been processed
pub processed_hash: String,
/// When this file was created in system
pub created_at: Timestamp,
/// The bucket this file is stored in
pub bucket_id: String,
/// The path at which this file exists in
pub path: String,
/// Cryptographic nonce used to encrypt this file
pub iv: String,
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
},
"PartialFile"
);
auto_derived!(
/// Metadata associated with a file
#[serde(tag = "type")]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
#[default]
File,
/// File contains textual data and should be displayed as such
Text,
/// File is an image with specific dimensions
Image {
width: isize,
height: isize,
// animated: bool // TODO: https://docs.rs/image/latest/image/trait.AnimationDecoder.html for APNG support
},
/// File is a video with specific dimensions
Video { width: isize, height: isize },
/// File is audio
Audio,
}
);
impl FileHash {
/// Create a file from a file hash
pub fn into_file(
&self,
id: String,
tag: String,
filename: String,
uploader_id: String,
) -> File {
File {
id,
tag,
filename,
hash: Some(self.id.clone()),
uploaded_at: Some(Timestamp::now_utc()),
uploader_id: Some(uploader_id),
used_for: None,
deleted: None,
reported: None,
// TODO: remove this data
metadata: self.metadata.clone(),
content_type: self.content_type.clone(),
size: self.size,
// TODO: superseded by "used_for"
message_id: None,
object_id: None,
server_id: None,
user_id: None,
}
}
}

View File

@@ -0,0 +1,18 @@
use revolt_result::Result;
use crate::FileHash;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAttachmentHashes: Sync + Send {
/// Insert a new attachment hash into the database.
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()>;
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash>;
/// Update an attachment hash nonce value.
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>;
}

View File

@@ -0,0 +1,51 @@
use revolt_result::Result;
use crate::FileHash;
use crate::MongoDb;
use super::AbstractAttachmentHashes;
static COL: &str = "attachment_hashes";
#[async_trait]
impl AbstractAttachmentHashes for MongoDb {
/// Insert a new attachment hash into the database.
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> {
query!(self, insert_one, COL, &hash).map(|_| ())
}
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
query!(
self,
find_one,
COL,
doc! {
"$or": [
{"_id": hash},
{"processed_hash": hash}
]
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Update an attachment hash nonce value.
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> {
self.col::<FileHash>(COL)
.update_one(
doc! {
"_id": hash
},
doc! {
"$set": {
"iv": nonce
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}

View File

@@ -0,0 +1,41 @@
use revolt_result::Result;
use crate::FileHash;
use crate::ReferenceDb;
use super::AbstractAttachmentHashes;
#[async_trait]
impl AbstractAttachmentHashes for ReferenceDb {
/// Insert a new attachment hash into the database.
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> {
let mut hashes = self.file_hashes.lock().await;
if hashes.contains_key(&hash.id) {
Err(create_database_error!("insert", "attachment"))
} else {
hashes.insert(hash.id.to_string(), hash.clone());
Ok(())
}
}
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash_value: &str) -> Result<FileHash> {
let hashes = self.file_hashes.lock().await;
hashes
.values()
.cloned()
.find(|hash| hash.id == hash_value || hash.processed_hash == hash_value)
.ok_or(create_error!(NotFound))
}
/// Update an attachment hash nonce value.
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> {
let mut hashes = self.file_hashes.lock().await;
if let Some(file) = hashes.get_mut(hash) {
file.iv = nonce.to_owned();
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::Database;
use crate::{Database, FileHash, Metadata};
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
auto_derived_partial!(
@@ -12,12 +13,18 @@ auto_derived_partial!(
pub tag: String,
/// Original filename
pub filename: String,
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
/// Hash of this file
pub hash: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
/// When this file was uploaded
pub uploaded_at: Option<Timestamp>, // these are Option<>s to not break file uploads on legacy Autumn
/// ID of user who uploaded this file
#[serde(skip_serializing_if = "Option::is_none")]
pub uploader_id: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
/// What the file was used for
#[serde(skip_serializing_if = "Option::is_none")]
pub used_for: Option<FileUsedFor>,
/// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")]
@@ -26,6 +33,14 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "Option::is_none")]
pub reported: Option<bool>,
// !!! DEPRECATED:
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
// TODO: migrate this mess to having:
// - author_id
// - parent: Parent { Message(id), User(id), etc }
@@ -44,64 +59,184 @@ auto_derived_partial!(
);
auto_derived!(
/// Metadata associated with a file
#[serde(tag = "type")]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
#[default]
File,
/// File contains textual data and should be displayed as such
Text,
/// File is an image with specific dimensions
Image { width: isize, height: isize },
/// File is a video with specific dimensions
Video { width: isize, height: isize },
/// File is audio
Audio,
/// Type of object file was used for
pub enum FileUsedForType {
Message,
ServerBanner,
Emoji,
UserAvatar,
WebhookAvatar,
UserProfileBackground,
LegacyGroupIcon,
ChannelIcon,
ServerIcon,
}
/// Information about what the file was used for
pub struct FileUsedFor {
/// Type of the object
#[serde(rename = "type")]
pub object_type: FileUsedForType,
/// ID of the object
pub id: String,
}
);
impl File {
/// Get the hash entry for this file
pub async fn as_hash(&self, db: &Database) -> Result<FileHash> {
db.fetch_attachment_hash(self.hash.as_ref().unwrap()).await
}
/// Use a file for a message attachment
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "attachments", "message", parent)
.await
pub async fn use_attachment(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"attachments",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::Message,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for a user profile background
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "backgrounds", "user", parent)
.await
pub async fn use_background(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"backgrounds",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::UserProfileBackground,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for a user avatar
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "avatars", "user", parent)
.await
pub async fn use_user_avatar(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"avatars",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::UserAvatar,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for an icon
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
/// Use a file for a webhook avatar
pub async fn use_webhook_avatar(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"avatars",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::WebhookAvatar,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for a server icon
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
pub async fn use_server_icon(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"icons",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::ServerIcon,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for a channel icon
pub async fn use_channel_icon(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"icons",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::ChannelIcon,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for a server banner
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "banners", "server", parent)
.await
pub async fn use_server_banner(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"banners",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::ServerBanner,
},
uploader_id.to_owned(),
)
.await
}
/// Use a file for an emoji
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "emojis", "object", parent)
.await
pub async fn use_emoji(
db: &Database,
id: &str,
parent: &str,
uploader_id: &str,
) -> Result<File> {
db.find_and_use_attachment(
id,
"emojis",
FileUsedFor {
id: parent.to_owned(),
object_type: FileUsedForType::Emoji,
},
uploader_id.to_owned(),
)
.await
}
}

View File

@@ -2,6 +2,8 @@ use revolt_result::Result;
use crate::File;
use super::FileUsedFor;
mod mongodb;
mod reference;
@@ -10,13 +12,16 @@ pub trait AbstractAttachments: Sync + Send {
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File>;
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
used_for: FileUsedFor,
uploader_id: String,
) -> Result<File>;
/// Mark an attachment as having been reported.

View File

@@ -1,7 +1,10 @@
use bson::to_document;
use bson::Document;
use revolt_config::report_internal_error;
use revolt_result::Result;
use crate::File;
use crate::FileUsedFor;
use crate::MongoDb;
use super::AbstractAttachments;
@@ -15,15 +18,28 @@ impl AbstractAttachments for MongoDb {
query!(self, insert_one, COL, &attachment).map(|_| ())
}
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
query!(
self,
find_one,
COL,
doc! {
"_id": file_id,
"tag": tag
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
used_for: FileUsedFor,
uploader_id: String,
) -> Result<File> {
let key = format!("{parent_type}_id");
let file = query!(
self,
find_one,
@@ -31,7 +47,7 @@ impl AbstractAttachments for MongoDb {
doc! {
"_id": id,
"tag": tag,
&key: {
"used_for": {
"$exists": false
}
}
@@ -45,7 +61,8 @@ impl AbstractAttachments for MongoDb {
},
doc! {
"$set": {
key: parent_id
"used_for": report_internal_error!(to_document(&used_for))?,
"uploader_id": uploader_id
}
},
None,

View File

@@ -1,6 +1,7 @@
use revolt_result::Result;
use crate::File;
use crate::FileUsedFor;
use crate::ReferenceDb;
use super::AbstractAttachments;
@@ -18,24 +19,33 @@ impl AbstractAttachments for ReferenceDb {
}
}
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
let files = self.files.lock().await;
if let Some(file) = files.get(file_id) {
if file.tag == tag {
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
used_for: FileUsedFor,
uploader_id: String,
) -> Result<File> {
let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) {
if file.tag == tag {
match parent_type {
"message" => file.message_id = Some(parent_id.to_owned()),
"user" => file.user_id = Some(parent_id.to_owned()),
"object" => file.object_id = Some(parent_id.to_owned()),
"server" => file.server_id = Some(parent_id.to_owned()),
_ => unreachable!(),
}
file.uploader_id = Some(uploader_id);
file.used_for = Some(used_for);
Ok(file.clone())
} else {

View File

@@ -1,11 +1,11 @@
use std::collections::HashSet;
use std::{collections::HashSet, hash::RandomState};
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
@@ -15,8 +15,8 @@ use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File, User,
util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey},
Channel, Database, Emoji, File, User, AMQP,
};
auto_derived_partial!(
@@ -65,6 +65,13 @@ auto_derived_partial!(
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
/// Whether or not the message in pinned
#[serde(skip_serializing_if = "crate::if_option_false")]
pub pinned: Option<bool>,
/// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
},
"PartialMessage"
);
@@ -95,6 +102,10 @@ auto_derived!(
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
#[serde(rename = "message_pinned")]
MessagePinned { id: String, by: String },
#[serde(rename = "message_unpinned")]
MessageUnpinned { id: String, by: String },
}
/// Name and / or avatar override information
@@ -164,6 +175,8 @@ auto_derived!(
pub author: Option<String>,
/// Search query
pub query: Option<String>,
/// Search for pinned
pub pinned: Option<bool>,
}
/// Message Query
@@ -179,6 +192,11 @@ auto_derived!(
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
/// Optional fields on message
pub enum FieldsMessage {
Pinned,
}
);
#[allow(clippy::derivable_impls)]
@@ -200,6 +218,8 @@ impl Default for Message {
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
flags: None,
pinned: None,
}
}
}
@@ -207,11 +227,16 @@ impl Default for Message {
#[allow(clippy::disallowed_methods)]
impl Message {
/// Create message from API data
#[allow(clippy::too_many_arguments)]
pub async fn create_from_api(
db: &Database,
amqp: Option<&AMQP>,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
limits: FeaturesLimits,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
allow_mentions: bool,
@@ -221,7 +246,7 @@ impl Message {
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
limits.message_length,
)?;
idempotency
@@ -237,6 +262,13 @@ impl Message {
return Err(create_error!(EmptyMessage));
}
// Ensure flags are either not set or have permissible values
if let Some(flags) = &data.flags {
if flags != &0 && flags != &1 {
return Err(create_error!(InvalidProperty));
}
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
@@ -262,7 +294,7 @@ impl Message {
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
channel: channel.id().to_string(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
@@ -270,6 +302,7 @@ impl Message {
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
flags: data.flags.map(|v| v as i32),
..Default::default()
};
@@ -288,9 +321,9 @@ impl Message {
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
max: config.features.limits.global.message_replies,
}));
}
@@ -305,6 +338,50 @@ impl Message {
}
}
// Validate the mentions go to users in the channel/server
if !mentions.is_empty() {
match channel {
Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => {
let recipients_hash: HashSet<&String, RandomState> =
HashSet::from_iter(recipients);
mentions.retain(|m| recipients_hash.contains(m));
}
Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members {
let valid_mentions: HashSet<&String, RandomState> =
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user));
mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
if !mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
} else {
revolt_config::capture_error(&valid_members.unwrap_err());
return Err(create_error!(InternalError));
}
}
Channel::SavedMessages { .. } => {
mentions.clear();
}
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
@@ -320,28 +397,26 @@ impl Message {
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
.is_some_and(|v| v.len() > limits.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
max: limits.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
max: config.features.limits.global.message_embeds,
}));
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
attachments
.push(File::use_attachment(db, attachment_id, &message_id, author.id()).await?);
}
if !attachments.is_empty() {
@@ -360,7 +435,9 @@ impl Message {
message.nonce = Some(idempotency.into_key());
// Send the message
message.send(db, author, &channel, generate_embeds).await?;
message
.send(db, amqp, author, user, member, &channel, generate_embeds)
.await?;
Ok(message)
}
@@ -369,13 +446,18 @@ impl Message {
pub async fn send_without_notifications(
&mut self,
db: &Database,
user: Option<v0::User>,
member: Option<v0::Member>,
is_dm: bool,
generate_embeds: bool,
// This determines if this function should queue the mentions task or if somewhere else will.
// If this is true, you MUST call tasks::ack::queue yourself.
mentions_elsewhere: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone().into())
EventV1::Message(self.clone().into_model(user, member))
.p(self.channel.to_string())
.await;
@@ -383,13 +465,12 @@ impl Message {
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
if let Some(mentions) = &self.mentions {
for user in mentions {
tasks::ack::queue(
if !mentions_elsewhere {
if let Some(mentions) = &self.mentions {
tasks::ack::queue_message(
self.channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
AckEvent::ProcessMessage {
messages: vec![(None, self.clone(), mentions.clone(), true)],
},
)
.await;
@@ -412,33 +493,56 @@ impl Message {
}
/// Send a message
#[allow(clippy::too_many_arguments)]
pub async fn send(
&mut self,
db: &Database,
amqp: Option<&AMQP>, // this is optional mostly for tests.
author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
user.clone(),
member.clone(),
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
true,
)
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
if !self.has_suppressed_notifications() {
// send Push notifications
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
messages: vec![(
Some(
PushNotification::from(
self.clone().into_model(user, member),
Some(author),
channel.to_owned().into(),
)
.await,
),
self.clone(),
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => {
self.mentions.clone().unwrap_or_default()
}
_ => vec![],
},
self.has_suppressed_notifications(),
)],
},
)
.await;
}
Ok(())
}
@@ -452,10 +556,7 @@ impl Message {
})?;
let media = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?,
)
Some(File::use_attachment(db, &id, &self.id, &self.author).await?)
} else {
None
};
@@ -470,15 +571,37 @@ impl Message {
}))
}
/// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as i32
== MessageFlags::SuppressNotifications as i32
} else {
false
}
}
/// Update message data
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
pub async fn update(
&mut self,
db: &Database,
partial: PartialMessage,
remove: Vec<FieldsMessage>,
) -> Result<()> {
self.apply_options(partial.clone());
db.update_message(&self.id, &partial).await?;
for field in &remove {
self.remove_field(field);
}
db.update_message(&self.id, &partial, remove.clone())
.await?;
EventV1::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial.into(),
clear: remove.into_iter().map(|field| field.into()).collect(),
}
.p(self.channel.clone())
.await;
@@ -498,13 +621,47 @@ impl Message {
.fetch_messages(query)
.await?
.into_iter()
.map(Into::into)
.map(|msg| msg.into_model(None, None))
.collect();
if let Some(true) = include_users {
let user_ids = messages
.iter()
.map(|m| m.author.clone())
.flat_map(|m| {
let mut users = vec![m.author.clone()];
if let Some(system) = &m.system {
match system {
v0::SystemMessage::ChannelDescriptionChanged { by } => {
users.push(by.clone())
}
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
users.push(from.clone());
users.push(to.clone())
}
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
v0::SystemMessage::UserAdded { by, id, .. }
| v0::SystemMessage::UserRemove { by, id, .. } => {
users.push(by.clone());
users.push(id.clone());
}
v0::SystemMessage::UserBanned { id, .. }
| v0::SystemMessage::UserKicked { id, .. }
| v0::SystemMessage::UserJoined { id, .. }
| v0::SystemMessage::UserLeft { id, .. } => {
users.push(id.clone());
}
v0::SystemMessage::Text { .. } => {}
v0::SystemMessage::MessagePinned { by, .. } => {
users.push(by.clone());
}
v0::SystemMessage::MessageUnpinned { by, .. } => {
users.push(by.clone());
}
}
}
users
})
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>();
@@ -558,7 +715,7 @@ impl Message {
) -> Result<()> {
let media: Option<v0::File> = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
File::use_attachment(db, &id, &self.id, &self.author)
.await?
.into(),
)
@@ -588,7 +745,7 @@ impl Message {
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message
let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions
if self.reactions.len() >= config.features.limits.global.message_reactions
&& !self.reactions.contains_key(emoji)
{
return Err(create_error!(InvalidOperation));
@@ -730,6 +887,12 @@ impl Message {
// Write to database
db.clear_reaction(&self.id, emoji).await
}
pub fn remove_field(&mut self, field: &FieldsMessage) {
match field {
FieldsMessage::Pinned => self.pinned = None,
}
}
}
impl SystemMessage {
@@ -753,7 +916,7 @@ impl Interactions {
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
if reactions.len() > config.features.limits.global.message_reactions {
return Err(create_error!(InvalidOperation));
}

View File

@@ -1,6 +1,6 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
@@ -20,7 +20,7 @@ pub trait AbstractMessages: Sync + Send {
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()>;
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;

View File

@@ -5,7 +5,8 @@ use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
MessageTimePeriod, MongoDb, PartialMessage,
};
use super::AbstractMessages;
@@ -50,6 +51,10 @@ impl AbstractMessages for MongoDb {
false
};
if let Some(pinned) = query.filter.pinned {
filter.insert("pinned", pinned);
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
@@ -166,7 +171,7 @@ impl AbstractMessages for MongoDb {
self.find_with_options(
COL,
doc! {
"ids": {
"_id": {
"$in": ids
}
},
@@ -177,8 +182,22 @@ impl AbstractMessages for MongoDb {
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
async fn update_message(
&self,
id: &str,
message: &PartialMessage,
remove: Vec<FieldsMessage>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
message,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Append information to a given message
@@ -296,6 +315,14 @@ impl AbstractMessages for MongoDb {
}
}
impl IntoDocumentPath for FieldsMessage {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsMessage::Pinned => "pinned",
})
}
}
impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();

View File

@@ -2,7 +2,7 @@ use futures::future::try_join_all;
use indexmap::IndexSet;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use super::AbstractMessages;
@@ -56,6 +56,12 @@ impl AbstractMessages for ReferenceDb {
}
}
if let Some(pinned) = query.filter.pinned {
if message.pinned.unwrap_or_default() == pinned {
return false
}
}
true
})
.cloned()
@@ -183,10 +189,15 @@ impl AbstractMessages for ReferenceDb {
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message_data) = messages.get_mut(id) {
message_data.apply_options(message.to_owned());
for field in remove {
#[allow(clippy::disallowed_methods)]
message_data.remove_field(&field);
}
Ok(())
} else {
Err(create_error!(NotFound))

View File

@@ -5,6 +5,7 @@ mod channel_unreads;
mod channel_webhooks;
mod channels;
mod emojis;
mod file_hashes;
mod files;
mod messages;
mod ratelimit_events;
@@ -23,6 +24,7 @@ pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use emojis::*;
pub use file_hashes::*;
pub use files::*;
pub use messages::*;
pub use ratelimit_events::*;
@@ -46,6 +48,7 @@ pub trait AbstractDatabase:
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ file_hashes::AbstractAttachmentHashes
+ files::AbstractAttachments
+ messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents

View File

@@ -92,7 +92,7 @@ impl Member {
server: &Server,
user: &User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
) -> Result<(Member, Vec<Channel>)> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
}
@@ -161,12 +161,12 @@ impl Member {
id: user.id.clone(),
}
.into_message(id.to_string())
.send_without_notifications(db, false, false)
.send_without_notifications(db, None, None, false, false, false)
.await
.ok();
}
Ok(channels)
Ok((member, channels))
}
/// Update member data
@@ -202,7 +202,7 @@ impl Member {
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
FieldsMember::CanReceive => self.can_receive = None,
FieldsMember::CanPublish => self.can_publish = None
FieldsMember::CanPublish => self.can_publish = None,
}
}
@@ -242,6 +242,7 @@ impl Member {
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
user: self.id.user.to_string(),
reason: intention.clone().into(),
}
.p(self.id.server.to_string())
.await;
@@ -263,7 +264,7 @@ impl Member {
}
.into_message(id.to_string())
// TODO: support notifications here in the future?
.send_without_notifications(db, false, false)
.send_without_notifications(db, None, None, false, false, false)
.await
.ok();
}

View File

@@ -173,8 +173,8 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
FieldsMember::CanPublish => "can_publish",
FieldsMember::CanReceive => "can_receive"
FieldsMember::CanPublish => "is_publishing",
FieldsMember::CanReceive => "is_receiving",
})
}
}

View File

@@ -53,17 +53,17 @@ impl AbstractServerMembers for ReferenceDb {
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
ids.iter()
.map(|id| {
Ok(ids
.iter()
.filter_map(|id| {
server_members
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
.collect())
}
/// Fetch member count of a server

View File

@@ -175,7 +175,7 @@ impl Server {
vec![]
};
server.channels = channels.iter().map(|c| c.id()).collect();
server.channels = channels.iter().map(|c| c.id().to_string()).collect();
db.insert_server(&server).await?;
Ok((server, channels))
}

View File

@@ -214,14 +214,21 @@ impl MongoDb {
// Delete all emoji.
self.col::<Document>("emojis")
.delete_many(
.update_many(
doc! {
"parent.id": &server_id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
.map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
@@ -253,7 +260,7 @@ impl MongoDb {
// Update many attachments with parent id.
self.delete_many_attachments(doc! {
"object_id": &server_id
"used_for.id": &server_id
})
.await?;

View File

@@ -0,0 +1,24 @@
use axum::{extract::FromRequestParts, http::request::Parts};
use revolt_result::{create_error, Error, Result};
use crate::{Database, User};
#[async_trait::async_trait]
impl FromRequestParts<Database> for User {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await
} else if let Some(Ok(session_token)) =
parts.headers.get("x-session-token").map(|v| v.to_str())
{
let session = db.fetch_session_by_token(session_token).await?;
db.fetch_user(&session.user_id).await
} else {
Err(create_error!(NotAuthenticated))
}
}
}

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "axum-impl")]
mod axum;
mod model;
mod ops;
#[cfg(feature = "rocket-impl")]
@@ -5,6 +7,8 @@ mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
#[cfg(feature = "axum-impl")]
pub use self::axum::*;
#[cfg(feature = "rocket-impl")]
pub use self::rocket::*;
#[cfg(feature = "rocket-impl")]

View File

@@ -1,14 +1,17 @@
use std::{collections::HashSet, time::Duration};
use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
use authifier::config::{EmailVerificationConfig, Template};
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use redis_kiss::{get_connection, AsyncCommands};
use revolt_config::config;
use revolt_models::v0;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{self, UserFlags};
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use serde_json::json;
use ulid::Ulid;
auto_derived_partial!(
@@ -50,6 +53,10 @@ auto_derived_partial!(
/// Bot information
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
/// Time until user is unsuspended
#[serde(skip_serializing_if = "Option::is_none")]
pub suspended_until: Option<Timestamp>,
},
"PartialUser"
);
@@ -62,6 +69,11 @@ auto_derived!(
StatusPresence,
ProfileContent,
ProfileBackground,
DisplayName,
// internal fields
Suspension,
None,
}
/// User's relationship with another user (or themselves)
@@ -165,6 +177,7 @@ impl Default for User {
flags: Default::default(),
privileged: Default::default(),
bot: Default::default(),
suspended_until: Default::default(),
}
}
}
@@ -198,6 +211,22 @@ impl User {
Ok(user)
}
/// Get limits for this user
pub async fn limits(&self) -> FeaturesLimits {
let config = config().await;
if ulid::Ulid::from_str(&self.id)
.expect("`ulid`")
.datetime()
.elapsed()
.expect("time went backwards")
<= Duration::from_secs(3600u64 * config.features.limits.global.new_user_hours as u64)
{
config.features.limits.new_user
} else {
config.features.limits.default
}
}
/// Get the relationship with another user
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
if self.id == user_b {
@@ -236,12 +265,11 @@ impl User {
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await;
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
Ok(())
} else {
Err(create_error!(TooManyServers {
max: config.features.limits.default.servers
max: self.limits().await.servers
}))
}
}
@@ -277,23 +305,27 @@ impl User {
Ok(username)
}
/// Find a user from a given token and hint
/// Find a user and session ID from a given token and hint
#[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
match hint {
UserHint::Bot => {
UserHint::Bot => Ok((
db.fetch_user(
&db.fetch_bot_by_token(token)
.await
.map_err(|_| create_error!(InvalidSession))?
.id,
)
.await
.await?,
String::new(),
)),
UserHint::User => {
let session = db.fetch_session_by_token(token).await?;
Ok((db.fetch_user(&session.user_id).await?, session.id))
}
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
Ok(user)
if let Ok(result) = User::from_token(db, token, UserHint::User).await {
Ok(result)
} else {
User::from_token(db, token, UserHint::Bot).await
}
@@ -466,7 +498,12 @@ impl User {
}
/// Add another user as a friend
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
pub async fn add_friend(
&mut self,
db: &Database,
amqp: &AMQP,
target: &mut User,
) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User => Err(create_error!(NoEffect)),
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
@@ -474,6 +511,9 @@ impl User {
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => {
// Accept incoming friend request
_ = amqp.friend_request_accepted(self, target).await;
self.apply_relationship(
db,
target,
@@ -483,6 +523,28 @@ impl User {
.await
}
RelationshipStatus::None => {
// Get this user's current count of outgoing friend requests
let count = self
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
.count()
})
.unwrap_or_default();
// If we're over the limit, don't allow creating more requests
if count >= self.limits().await.outgoing_friend_requests {
return Err(create_error!(TooManyPendingFriendRequests {
max: self.limits().await.outgoing_friend_requests
}));
}
_ = amqp.friend_request_received(target, self).await;
// Send the friend request
self.apply_relationship(
db,
target,
@@ -631,9 +693,107 @@ impl User {
x.background = None;
}
}
FieldsUser::DisplayName => self.display_name = None,
FieldsUser::Suspension => self.suspended_until = None,
FieldsUser::None => {}
}
}
/// Suspend the user
///
/// - If a duration is specified, the user will be automatically unsuspended after the given time.
/// - If a reason is specified, an email will be sent.
pub async fn suspend(
&mut self,
db: &Database,
duration_days: Option<usize>,
reason: Option<Vec<String>>,
) -> Result<()> {
let authifier = db.clone().to_authifier().await;
let mut account = authifier
.database
.find_account(&self.id)
.await
.map_err(|_| create_error!(InternalError))?;
account
.disable(&authifier)
.await
.map_err(|_| create_error!(InternalError))?;
account
.delete_all_sessions(&authifier, None)
.await
.map_err(|_| create_error!(InternalError))?;
self.update(
db,
PartialUser {
flags: Some(UserFlags::SuspendedUntil as i32),
suspended_until: duration_days.and_then(|dur| {
Timestamp::now_utc().checked_add(iso8601_timestamp::Duration::days(dur as i64))
}),
..Default::default()
},
vec![],
)
.await?;
if let Some(reason) = reason {
if let EmailVerificationConfig::Enabled { smtp, .. } =
authifier.config.email_verification
{
smtp.send_email(
account.email.clone(),
// maybe move this to common area?
&Template {
title: "Account Suspension".to_string(),
html: Some(include_str!("../../../templates/suspension.html").to_owned()),
text: include_str!("../../../templates/suspension.txt").to_owned(),
url: Default::default(),
},
json!({
"email": account.email,
"list": reason.join(", "),
"duration": duration_days,
"duration_display": if duration_days.is_some() {
"block"
} else {
"none"
}
}),
)
.map_err(|_| create_error!(InternalError))?;
}
}
Ok(())
}
/// Unsuspend the user
pub async fn unsuspend(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
flags: Some(0),
suspended_until: None,
..Default::default()
},
vec![],
)
.await?;
unimplemented!()
}
/// Permanently ban the user
///
/// - If a reason is specified, an email will be sent.
pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
// Send ban email (if reason provided)
unimplemented!()
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
@@ -649,6 +809,7 @@ impl User {
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
FieldsUser::Suspension,
],
)
.await

View File

@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -16,8 +17,8 @@ pub trait AbstractUsers: Sync + Send {
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
/// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
@@ -57,4 +58,7 @@ pub trait AbstractUsers: Sync + Send {
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()>;
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
}

View File

@@ -46,10 +46,9 @@ impl AbstractUsers for MongoDb {
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
let session = self
.col::<Session>("sessions")
/// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
self.col::<Session>("sessions")
.find_one(
doc! {
"token": token
@@ -58,9 +57,7 @@ impl AbstractUsers for MongoDb {
)
.await
.map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?;
self.fetch_user(&session.user_id).await
.ok_or_else(|| create_error!(InvalidSession))
}
/// Fetch multiple users by their ids
@@ -319,6 +316,25 @@ impl AbstractUsers for MongoDb {
async fn delete_user(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()> {
self.col::<User>("sessions")
.update_one(
doc! {
"_id": session_id
},
doc! {
"$unset": {
"subscription": 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}
impl IntoDocumentPath for FieldsUser {
@@ -329,6 +345,9 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
FieldsUser::Suspension => "suspended_until",
FieldsUser::None => "none",
})
}
}

View File

@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -40,8 +41,8 @@ impl AbstractUsers for ReferenceDb {
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
/// Fetch a session from the database by token
async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
todo!()
}
@@ -162,4 +163,9 @@ impl AbstractUsers for ReferenceDb {
Err(create_error!(NotFound))
}
}
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
todo!()
}
}

View File

@@ -38,7 +38,7 @@ impl<'r> FromRequest<'r> for User {
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((Status::Unauthorized, authifier::Error::InvalidSession))
Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession))
}
}
}