refactor(quark): port channel_ack, channel_delete, channel_ edit, group_remove_member, invite_create, members_fetch, message_bulk_delete, message_clear_reactions, message_delete

#283
This commit is contained in:
Paul Makles
2024-04-07 15:41:43 +01:00
parent 50c36dcefd
commit 54a4eff623
27 changed files with 500 additions and 216 deletions

1
Cargo.lock generated
View File

@@ -3712,6 +3712,7 @@ dependencies = [
"revolt-config", "revolt-config",
"revolt-permissions", "revolt-permissions",
"revolt_optional_struct", "revolt_optional_struct",
"rocket",
"schemars", "schemars",
"serde", "serde",
"validator 0.16.0", "validator 0.16.0",

View File

@@ -1,12 +1,12 @@
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::Database; use crate::{Channel, Database, User};
/* static ALPHABET: [char; 54] = [ static ALPHABET: [char; 54] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
]; */ ];
auto_derived!( auto_derived!(
/// Invite /// Invite
@@ -56,9 +56,13 @@ impl Invite {
} }
/// Create a new invite from given information /// Create a new invite from given information
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> { pub async fn create_channel_invite(
db: &Database,
creator: &User,
channel: &Channel,
) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET); let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &target { let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group { Channel::Group { id, .. } => Ok(Invite::Group {
code, code,
creator: creator.id.clone(), creator: creator.id.clone(),
@@ -72,12 +76,12 @@ impl Invite {
channel: id.clone(), channel: id.clone(),
}) })
} }
_ => Err(Error::InvalidOperation), _ => Err(create_error!(InvalidOperation)),
}?; }?;
db.insert_invite(&invite).await?; db.insert_invite(&invite).await?;
Ok(invite) Ok(invite)
}*/ }
/// Resolve an invite by its ID or by a public server ID /// Resolve an invite by its ID or by a public server ID
pub async fn find(db: &Database, code: &str) -> Result<Invite> { pub async fn find(db: &Database, code: &str) -> Result<Invite> {

View File

@@ -7,8 +7,8 @@ use revolt_result::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server, events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
SystemMessage, User, Server, SystemMessage, User,
}; };
auto_derived!( auto_derived!(
@@ -582,6 +582,28 @@ impl Channel {
} }
} }
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
EventV1::ChannelAck {
id: self.id().to_string(),
user: user.to_string(),
message_id: message.to_string(),
}
.private(user.to_string())
.await;
crate::tasks::ack::queue(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
id: message.to_string(),
},
)
.await;
Ok(())
}
/// Remove user from a group /// Remove user from a group
pub async fn remove_user_from_group( pub async fn remove_user_from_group(
&self, &self,
@@ -627,8 +649,7 @@ impl Channel {
.await .await
.ok(); .ok();
} else { } else {
db.delete_channel(self).await?; return self.delete(db).await;
return Ok(());
} }
} }

View File

@@ -441,6 +441,22 @@ impl Message {
Ok(()) Ok(())
} }
/// Update message data
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
self.apply_options(partial.clone());
db.update_message(&self.id, &partial).await?;
EventV1::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial.into(),
}
.p(self.channel.clone())
.await;
Ok(())
}
/// Append content to message /// Append content to message
pub async fn append( pub async fn append(
db: &Database, db: &Database,
@@ -518,6 +534,48 @@ impl Message {
Err(create_error!(PayloadTooLarge)) Err(create_error!(PayloadTooLarge))
} }
} }
/// 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(),
}
.p(self.channel)
.await;
Ok(())
}
/// Bulk delete messages
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
let valid_ids = db
.fetch_messages_by_id(&ids)
.await?
.into_iter()
.filter(|msg| msg.channel == channel)
.map(|msg| msg.id)
.collect::<Vec<String>>();
db.delete_messages(channel, &valid_ids).await?;
EventV1::BulkMessageDelete {
channel: channel.to_string(),
ids: valid_ids,
}
.p(channel.to_string())
.await;
Ok(())
}
} }
impl SystemMessage { impl SystemMessage {

View File

@@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send {
/// Fetch multiple messages by given query /// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>; async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
/// Update a given message with new information /// 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) -> Result<()>;

View File

@@ -159,6 +159,21 @@ impl AbstractMessages for MongoDb {
} }
} }
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
self.find_with_options(
COL,
doc! {
"ids": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))
}
/// Update a given message with new information /// 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) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ()) query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())

View File

@@ -1,3 +1,4 @@
use futures::future::try_join_all;
use indexmap::IndexSet; use indexmap::IndexSet;
use revolt_result::Result; use revolt_result::Result;
@@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb {
}*/ }*/
} }
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
}
/// Update a given message with new information /// 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) -> Result<()> {
let mut messages = self.messages.lock().await; let mut messages = self.messages.lock().await;

View File

@@ -727,6 +727,13 @@ impl crate::User {
id: self.id, id: self.id,
} }
} }
pub fn as_author_for_system(&self) -> MessageAuthor {
MessageAuthor::System {
username: &self.username,
avatar: self.avatar.as_ref().map(|file| file.id.as_ref()),
}
}
} }
impl From<crate::PartialUser> for PartialUser { impl From<crate::PartialUser> for PartialUser {

View File

@@ -2,6 +2,4 @@ pub mod bridge;
pub mod idempotency; pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
#[cfg(test)]
pub mod test_fixtures; pub mod test_fixtures;

View File

@@ -42,6 +42,16 @@ impl Reference {
db.fetch_message(&self.id).await db.fetch_message(&self.id).await
} }
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
Ok(msg)
}
/// Fetch server from Ref /// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> { pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await db.fetch_server(&self.id).await

View File

@@ -92,7 +92,7 @@ macro_rules! fixture {
let fixtures = $crate::util::test_fixtures::load_fixture( let fixtures = $crate::util::test_fixtures::load_fixture(
&$database, &$database,
include_str!(concat!("../../../fixtures/", $name, ".json")), include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")),
) )
.await; .await;

View File

@@ -12,9 +12,10 @@ description = "Revolt Backend: API Models"
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"] serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"] schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = ["dep:validator"] validator = ["dep:validator"]
rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"] partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = ["serde", "partials"] default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
@@ -26,6 +27,9 @@ regex = "1"
indexmap = "1.9.3" indexmap = "1.9.3"
once_cell = "1.17.1" once_cell = "1.17.1"
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
# Serialisation # Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true } revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }

View File

@@ -3,6 +3,9 @@ use super::File;
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived!( auto_derived!(
/// Channel /// Channel
#[serde(tag = "channel_type")] #[serde(tag = "channel_type")]
@@ -258,6 +261,13 @@ auto_derived!(
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
} }
/// Options when deleting a channel
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
); );
impl Channel { impl Channel {

View File

@@ -169,16 +169,19 @@ auto_derived!(
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed { pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon_url: Option<String>, pub icon_url: Option<String>,
#[validate(length(min = 1, max = 256))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>, pub url: Option<String>,
#[validate(length(min = 1, max = 100))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
pub title: Option<String>, pub title: Option<String>,
#[validate(length(min = 1, max = 2000))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub description: Option<String>, pub description: Option<String>,
pub media: Option<String>, pub media: Option<String>,
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")] #[cfg_attr(
feature = "validator",
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
)]
pub colour: Option<String>, pub colour: Option<String>,
} }
@@ -196,11 +199,11 @@ auto_derived!(
/// Unique token to prevent duplicate message sending /// Unique token to prevent duplicate message sending
/// ///
/// **This is deprecated and replaced by `Idempotency-Key`!** /// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
pub nonce: Option<String>, pub nonce: Option<String>,
/// Message content to send /// Message content to send
#[validate(length(min = 0, max = 2000))] #[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
pub content: Option<String>, pub content: Option<String>,
/// Attachments to include in message /// Attachments to include in message
pub attachments: Option<Vec<String>>, pub attachments: Option<Vec<String>>,
@@ -209,14 +212,25 @@ auto_derived!(
/// Embeds to include in message /// Embeds to include in message
/// ///
/// Text embed content contributes to the content length cap /// Text embed content contributes to the content length cap
#[validate] #[cfg_attr(feature = "validator", validate)]
pub embeds: Option<Vec<SendableEmbed>>, pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message /// Masquerade to apply to this message
#[validate] #[cfg_attr(feature = "validator", validate)]
pub masquerade: Option<Masquerade>, pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with /// Information about how this message should be interacted with
pub interactions: Option<Interactions>, pub interactions: Option<Interactions>,
} }
/// Options for bulk deleting messages
#[cfg_attr(
feature = "validator",
cfg_attr(feature = "validator", derive(Validate))
)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
pub ids: Vec<String>,
}
); );
/// Message Author Abstraction /// Message Author Abstraction

View File

@@ -74,7 +74,11 @@ revolt-database = { path = "../core/database", features = [
"rocket-impl", "rocket-impl",
"redis-is-patched", "redis-is-patched",
] } ] }
revolt-models = { path = "../core/models", features = ["schemas", "validator"] } revolt-models = { path = "../core/models", features = [
"schemas",
"validator",
"rocket",
] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] }

View File

@@ -1,23 +1,84 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Acknowledge Message /// # Acknowledge Message
/// ///
/// Lets the server and all other clients know that we've seen this message id in this channel. /// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")] #[openapi(tag = "Messaging")]
#[put("/<target>/ack/<message>")] #[put("/<target>/ack/<message>")]
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> { pub async fn ack(
db: &State<Database>,
user: User,
target: Reference,
message: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission(db, Permission::ViewChannel) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel channel
.ack(&user.id, &message.id) .ack(&user.id, &message.id)
.await .await
.map(|_| EmptyResponse) .map(|_| EmptyResponse)
} }
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_ack_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let message_id = ulid::Ulid::new().to_string();
let response = harness
.client
.put(format!("/channels/{}/ack/{}", group.id(), message_id))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelAck { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelAck {
message_id: m_id, ..
} => assert_eq!(m_id, message_id),
_ => unreachable!(),
};
}
}

View File

@@ -1,34 +1,32 @@
use revolt_quark::{ use revolt_database::{
models::{channel::PartialChannel, Channel, User}, util::{permissions::DatabasePermissionQuery, reference::Reference},
perms, Db, EmptyResponse, Error, Permission, Ref, Result, Channel, Database, PartialChannel, User,
}; };
use serde::{Deserialize, Serialize}; use revolt_models::v0;
use validator::Validate; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
/// # Query Parameters use rocket::State;
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)] use rocket_empty::EmptyResponse;
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
/// # Close Channel /// # Close Channel
/// ///
/// Deletes a server channel, leaves a group or closes a group. /// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")] #[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")] #[delete("/<target>?<options..>")]
pub async fn req( pub async fn delete(
db: &Db, db: &State<Database>,
user: User, user: User,
target: Ref, target: Reference,
options: OptionsChannelDelete, options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?; let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel); let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?; let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
match &channel { match &channel {
Channel::SavedMessages { .. } => Err(Error::NoEffect), Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
Channel::DirectMessage { .. } => channel Channel::DirectMessage { .. } => channel
.update( .update(
db, db,
@@ -41,20 +39,58 @@ pub async fn req(
.await .await
.map(|_| EmptyResponse), .map(|_| EmptyResponse),
Channel::Group { .. } => channel Channel::Group { .. } => channel
.remove_user_from_group( .remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.await .await
.map(|_| EmptyResponse), .map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => { Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
perms permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
.throw_permission(db, Permission::ManageChannel)
.await?;
channel.delete(db).await.map(|_| EmptyResponse) channel.delete(db).await.map(|_| EmptyResponse)
} }
} }
} }
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_delete_group() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let response = harness
.client
.delete(format!("/channels/{}", group.id()))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == &group.id(),
_ => false,
})
.await;
}
// TEST: member leaves group (no delete)
// TEST: no effect with saved messages
// TEST: DM set to inactive
// TEST: server channel deleted
}

View File

@@ -1,60 +1,36 @@
use revolt_quark::{ use revolt_database::{
models::{ util::{permissions::DatabasePermissionQuery, reference::Reference},
channel::{Channel, FieldsChannel, PartialChannel}, Channel, Database, File, PartialChannel, SystemMessage, User,
message::SystemMessage,
File, User,
},
perms, Database, Error, Permission, Ref, Result,
}; };
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
/// # Channel Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
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
nsfw: Option<bool>,
/// Whether this channel is archived
archived: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
}
/// # Edit Channel /// # Edit Channel
/// ///
/// Edit a channel object by its id. /// Edit a channel object by its id.
#[openapi(tag = "Channel Information")] #[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")] #[patch("/<target>", data = "<data>")]
pub async fn req( pub async fn edit(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Ref, target: Reference,
data: Json<DataEditChannel>, data: Json<v0::DataEditChannel>,
) -> Result<Json<Channel>> { ) -> Result<Json<v0::Channel>> {
let data = data.into_inner(); let data = data.into_inner();
data.validate() data.validate().map_err(|error| {
.map_err(|error| Error::FailedValidation { error })?; create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut channel = target.as_channel(db).await?; let mut channel = target.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission_and_view_channel(db, Permission::ManageChannel) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if data.name.is_none() if data.name.is_none()
&& data.description.is_none() && data.description.is_none()
@@ -63,7 +39,7 @@ pub async fn req(
&& data.owner.is_none() && data.owner.is_none()
&& data.remove.is_none() && data.remove.is_none()
{ {
return Ok(Json(channel)); return Ok(Json(channel.into()));
} }
let mut partial: PartialChannel = Default::default(); let mut partial: PartialChannel = Default::default();
@@ -76,12 +52,12 @@ pub async fn req(
{ {
// Make sure we are the owner of this group // Make sure we are the owner of this group
if owner != &user.id { if owner != &user.id {
return Err(Error::NotOwner); return Err(create_error!(NotOwner));
} }
// Ensure user is part of group // Ensure user is part of group
if !recipients.contains(&new_owner) { if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup); return Err(create_error!(NotInGroup));
} }
// Transfer ownership // Transfer ownership
@@ -94,10 +70,10 @@ pub async fn req(
to: new_owner, to: new_owner,
} }
} else { } else {
return Err(Error::InvalidOperation); return Err(create_error!(InvalidOperation));
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.create(db, &channel, None) .send(db, user.as_author_for_system(), &channel, false)
.await .await
.ok(); .ok();
} }
@@ -128,7 +104,7 @@ pub async fn req(
.. ..
} => { } => {
if let Some(fields) = &data.remove { if let Some(fields) = &data.remove {
if fields.contains(&FieldsChannel::Icon) { if fields.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon { if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?; db.mark_attachment_as_deleted(&icon.id).await?;
} }
@@ -136,10 +112,10 @@ pub async fn req(
for field in fields { for field in fields {
match field { match field {
FieldsChannel::Description => { v0::FieldsChannel::Description => {
description.take(); description.take();
} }
FieldsChannel::Icon => { v0::FieldsChannel::Icon => {
icon.take(); icon.take();
} }
_ => {} _ => {}
@@ -175,7 +151,7 @@ pub async fn req(
by: user.id.clone(), by: user.id.clone(),
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.create(db, &channel, None) .send(db, user.as_author_for_system(), &channel, false)
.await .await
.ok(); .ok();
} }
@@ -185,26 +161,36 @@ pub async fn req(
by: user.id.clone(), by: user.id.clone(),
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.create(db, &channel, None) .send(db, user.as_author_for_system(), &channel, false)
.await .await
.ok(); .ok();
} }
if partial.icon.is_some() { if partial.icon.is_some() {
SystemMessage::ChannelIconChanged { by: user.id } SystemMessage::ChannelIconChanged {
.into_message(channel.id().to_string()) by: user.id.clone(),
.create(db, &channel, None) }
.await .into_message(channel.id().to_string())
.ok(); .send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
} }
} }
channel channel
.update(db, partial, data.remove.unwrap_or_default()) .update(
db,
partial,
data.remove
.unwrap_or_default()
.into_iter()
.map(|f| f.into())
.collect(),
)
.await?; .await?;
} }
_ => return Err(Error::InvalidOperation), _ => return Err(create_error!(InvalidOperation)),
}; };
Ok(Json(channel)) Ok(Json(channel.into()))
} }

View File

@@ -13,7 +13,7 @@ use rocket::{serde::json::Json, State};
/// Fetch channel by its id. /// Fetch channel by its id.
#[openapi(tag = "Channel Information")] #[openapi(tag = "Channel Information")]
#[get("/<target>")] #[get("/<target>")]
pub async fn fetch_channel( pub async fn fetch(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference, target: Reference,

View File

@@ -13,7 +13,7 @@ use rocket_empty::EmptyResponse;
/// Adds another user to the group. /// Adds another user to the group.
#[openapi(tag = "Groups")] #[openapi(tag = "Groups")]
#[put("/<group_id>/recipients/<member_id>")] #[put("/<group_id>/recipients/<member_id>")]
pub async fn req( pub async fn add_member(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
group_id: Reference, group_id: Reference,

View File

@@ -1,16 +1,23 @@
use revolt_quark::{ use revolt_database::{util::reference::Reference, Channel, Database, User};
models::{Channel, User}, use revolt_permissions::ChannelPermission;
Db, EmptyResponse, Error, Permission, Ref, Result, use revolt_result::{create_error, Result};
};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove Member from Group /// # Remove Member from Group
/// ///
/// Removes a user from the group. /// Removes a user from the group.
#[openapi(tag = "Groups")] #[openapi(tag = "Groups")]
#[delete("/<target>/recipients/<member>")] #[delete("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> { pub async fn remove_member(
db: &State<Database>,
user: User,
target: Reference,
member: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
@@ -20,24 +27,26 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
owner, recipients, .. owner, recipients, ..
} => { } => {
if &user.id != owner { if &user.id != owner {
return Error::from_permission(Permission::ManageChannel); return Err(create_error!(MissingPermission {
permission: ChannelPermission::ManageChannel.to_string()
}));
} }
let member = member.as_user(db).await?; let member = member.as_user(db).await?;
if user.id == member.id { if user.id == member.id {
return Err(Error::CannotRemoveYourself); return Err(create_error!(CannotRemoveYourself));
} }
if !recipients.iter().any(|x| *x == member.id) { if !recipients.iter().any(|x| *x == member.id) {
return Err(Error::NotInGroup); return Err(create_error!(NotInGroup));
} }
channel channel
.remove_user_from_group(db, &member.id, Some(&user.id), false) .remove_user_from_group(db, &member, Some(&user.id), false)
.await .await
.map(|_| EmptyResponse) .map(|_| EmptyResponse)
} }
_ => Err(Error::InvalidOperation), _ => Err(create_error!(InvalidOperation)),
} }
} }

View File

@@ -1,9 +1,12 @@
use revolt_quark::{ use revolt_database::{
models::{Invite, User}, util::{permissions::DatabasePermissionQuery, reference::Reference},
perms, Db, Error, Permission, Ref, Result, Database, Invite, User,
}; };
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use rocket::serde::json::Json; use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Create Invite /// # Create Invite
/// ///
@@ -12,16 +15,23 @@ use rocket::serde::json::Json;
/// Channel must be a `TextChannel`. /// Channel must be a `TextChannel`.
#[openapi(tag = "Channel Invites")] #[openapi(tag = "Channel Invites")]
#[post("/<target>/invites")] #[post("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> { pub async fn create_invite(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::Invite>> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission_and_view_channel(db, Permission::InviteOthers) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
Invite::create(db, &user, &channel).await.map(Json) Invite::create_channel_invite(db, &user, &channel)
.await
.map(|invite| invite.into())
.map(Json)
} }

View File

@@ -1,31 +1,40 @@
use revolt_quark::{ use futures::future::join_all;
models::{Channel, User}, use revolt_database::{
perms, Db, Error, Permission, Ref, Result, util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
}; };
use revolt_models::v0;
use rocket::serde::json::Json; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Fetch Group Members /// # Fetch Group Members
/// ///
/// Retrieves all users who are part of this group. /// Retrieves all users who are part of this group.
#[openapi(tag = "Groups")] #[openapi(tag = "Groups")]
#[get("/<target>/members")] #[get("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> { pub async fn fetch_members(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<Vec<v0::User>>> {
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission(db, Permission::ViewChannel) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
if let Channel::Group { recipients, .. } = channel { if let Channel::Group { recipients, .. } = channel {
Ok(Json( Ok(Json(
db.fetch_users(&recipients) join_all(
.await? db.fetch_users(&recipients)
.into_iter() .await?
.map(|x| x.with_relationship(&user)) .into_iter()
.collect::<Vec<User>>(), .map(|other_user| other_user.into(db, &user)),
)
.await,
)) ))
} else { } else {
Err(Error::InvalidOperation) Err(create_error!(InvalidOperation))
} }
} }

View File

@@ -1,20 +1,16 @@
use chrono::Utc; use chrono::Utc;
use revolt_quark::{ use revolt_database::{
models::{Message, User}, util::{permissions::DatabasePermissionQuery, reference::Reference},
perms, Db, EmptyResponse, Error, Permission, Ref, Result, Database, Message, User,
}; };
use rocket::serde::json::Json; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use serde::Deserialize; use serde::Deserialize;
use validator::Validate; use validator::Validate;
/// # Search Parameters
#[derive(Validate, Deserialize, JsonSchema)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
ids: Vec<String>,
}
/// # Bulk Delete Messages /// # Bulk Delete Messages
/// ///
/// Delete multiple messages you've sent or one you have permission to delete. /// Delete multiple messages you've sent or one you have permission to delete.
@@ -24,34 +20,37 @@ pub struct OptionsBulkDelete {
/// Messages must have been sent within the past 1 week. /// Messages must have been sent within the past 1 week.
#[openapi(tag = "Messaging")] #[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)] #[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn req( pub async fn bulk_delete_messages(
db: &Db, db: &State<Database>,
user: User, user: User,
target: Ref, target: Reference,
options: Json<OptionsBulkDelete>, options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
let options = options.into_inner(); let options = options.into_inner();
options options.validate().map_err(|error| {
.validate() create_error!(FailedValidation {
.map_err(|error| Error::FailedValidation { error })?; error: error.to_string()
})
})?;
for id in &options.ids { for id in &options.ids {
if ulid::Ulid::from_string(id) if ulid::Ulid::from_string(id)
.map_err(|_| Error::InvalidOperation)? .map_err(|_| create_error!(InvalidOperation))?
.datetime() .datetime()
.signed_duration_since(Utc::now()) .signed_duration_since(Utc::now())
.num_days() .num_days()
.abs() .abs()
> 7 > 7
{ {
return Err(Error::InvalidOperation); return Err(create_error!(InvalidOperation));
} }
} }
perms(&user) let channel = target.as_channel(db).await?;
.channel(&target.as_channel(db).await?) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.throw_permission(db, Permission::ManageMessages) calculate_channel_permissions(&mut query)
.await?; .await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
Message::bulk_delete(db, &target.id, options.ids) Message::bulk_delete(db, &target.id, options.ids)
.await .await

View File

@@ -1,7 +1,11 @@
use revolt_quark::{ use revolt_database::{
models::{message::PartialMessage, User}, util::{permissions::DatabasePermissionQuery, reference::Reference},
perms, Db, EmptyResponse, Permission, Ref, Result, Database, PartialMessage, User,
}; };
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove All Reactions from Message /// # Remove All Reactions from Message
/// ///
@@ -10,15 +14,20 @@ use revolt_quark::{
/// Requires `ManageMessages` permission. /// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")] #[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")] #[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> { pub async fn clear_reactions(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission_and_view_channel(db, Permission::ManageMessages) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
// Fetch relevant message // Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?; let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
// Clear reactions // Clear reactions
message message

View File

@@ -1,21 +1,31 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Message /// # Delete Message
/// ///
/// Delete a message you've sent or one you have permission to delete. /// Delete a message you've sent or one you have permission to delete.
#[openapi(tag = "Messaging")] #[openapi(tag = "Messaging")]
#[delete("/<target>/messages/<msg>", rank = 2)] #[delete("/<target>/messages/<msg>", rank = 2)]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> { pub async fn delete(
let message = msg.as_message(db).await?; db: &State<Database>,
if message.channel != target.id { user: User,
return Err(Error::NotFound); target: Reference,
} msg: Reference,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, &target.id).await?;
if message.author != user.id { if message.author != user.id {
perms(&user) let channel = target.as_channel(db).await?;
.channel(&target.as_channel(db).await?) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.throw_permission(db, Permission::ManageMessages) calculate_channel_permissions(&mut query)
.await?; .await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
} }
message.delete(db).await.map(|_| EmptyResponse) message.delete(db).await.map(|_| EmptyResponse)

View File

@@ -28,22 +28,22 @@ mod webhook_fetch_all;
pub fn routes() -> (Vec<Route>, OpenApi) { pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![ openapi_get_routes_spec![
channel_ack::req, channel_ack::ack,
channel_fetch::fetch_channel, channel_fetch::fetch,
members_fetch::req, members_fetch::fetch_members,
channel_delete::req, channel_delete::delete,
channel_edit::req, channel_edit::edit,
invite_create::req, invite_create::create_invite,
message_send::message_send, message_send::message_send,
message_query::req, message_query::req,
message_search::req, message_search::req,
message_fetch::req, message_fetch::req,
message_edit::req, message_edit::req,
message_bulk_delete::req, message_bulk_delete::bulk_delete_messages,
message_delete::req, message_delete::delete,
group_create::create_group, group_create::create_group,
group_add_member::req, group_add_member::add_member,
group_remove_member::req, group_remove_member::remove_member,
voice_join::req, voice_join::req,
permissions_set::req, permissions_set::req,
permissions_set_default::req, permissions_set_default::req,