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

View File

@@ -1,12 +1,12 @@
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',
'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',
]; */
];
auto_derived!(
/// Invite
@@ -56,9 +56,13 @@ impl Invite {
}
/// 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 invite = match &target {
let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
@@ -72,12 +76,12 @@ impl Invite {
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}?;
db.insert_invite(&invite).await?;
Ok(invite)
}*/
}
/// Resolve an invite by its ID or by a public server ID
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 crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User,
};
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
pub async fn remove_user_from_group(
&self,
@@ -627,8 +649,7 @@ impl Channel {
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
return self.delete(db).await;
}
}

View File

@@ -441,6 +441,22 @@ impl Message {
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
pub async fn append(
db: &Database,
@@ -518,6 +534,48 @@ impl Message {
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 {

View File

@@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send {
/// Fetch multiple messages by given query
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
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
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
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 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
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
let mut messages = self.messages.lock().await;

View File

@@ -727,6 +727,13 @@ impl crate::User {
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 {

View File

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

View File

@@ -42,6 +42,16 @@ impl Reference {
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
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await

View File

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

View File

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

View File

@@ -3,6 +3,9 @@ use super::File;
use revolt_permissions::OverrideField;
use std::collections::{HashMap, HashSet};
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived!(
/// Channel
#[serde(tag = "channel_type")]
@@ -258,6 +261,13 @@ auto_derived!(
#[serde(skip_serializing_if = "Option::is_none")]
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 {

View File

@@ -169,16 +169,19 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
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>,
#[validate(length(min = 1, max = 100))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
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 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>,
}
@@ -196,11 +199,11 @@ auto_derived!(
/// Unique token to prevent duplicate message sending
///
/// **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>,
/// Message content to send
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
pub content: Option<String>,
/// Attachments to include in message
pub attachments: Option<Vec<String>>,
@@ -209,14 +212,25 @@ auto_derived!(
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
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

View File

@@ -74,7 +74,11 @@ revolt-database = { path = "../core/database", features = [
"rocket-impl",
"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-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
///
/// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")]
#[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() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel
.ack(&user.id, &message.id)
.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_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::{
models::{channel::PartialChannel, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
pub async fn delete(
db: &State<Database>,
user: User,
target: Ref,
options: OptionsChannelDelete,
target: Reference,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
match &channel {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
Channel::DirectMessage { .. } => channel
.update(
db,
@@ -41,20 +39,58 @@ pub async fn req(
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
perms
.throw_permission(db, Permission::ManageChannel)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
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::{
models::{
channel::{Channel, FieldsChannel, PartialChannel},
message::SystemMessage,
File, User,
},
perms, Database, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, File, PartialChannel, SystemMessage, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
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 a channel object by its id.
#[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")]
pub async fn req(
pub async fn edit(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataEditChannel>,
) -> Result<Json<Channel>> {
target: Reference,
data: Json<v0::DataEditChannel>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if data.name.is_none()
&& data.description.is_none()
@@ -63,7 +39,7 @@ pub async fn req(
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
return Ok(Json(channel.into()));
}
let mut partial: PartialChannel = Default::default();
@@ -76,12 +52,12 @@ pub async fn req(
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
return Err(create_error!(NotOwner));
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
// Transfer ownership
@@ -94,10 +70,10 @@ pub async fn req(
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -128,7 +104,7 @@ pub async fn req(
..
} => {
if let Some(fields) = &data.remove {
if fields.contains(&FieldsChannel::Icon) {
if fields.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?;
}
@@ -136,10 +112,10 @@ pub async fn req(
for field in fields {
match field {
FieldsChannel::Description => {
v0::FieldsChannel::Description => {
description.take();
}
FieldsChannel::Icon => {
v0::FieldsChannel::Icon => {
icon.take();
}
_ => {}
@@ -175,7 +151,7 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -185,26 +161,36 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
if partial.icon.is_some() {
SystemMessage::ChannelIconChanged { by: user.id }
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
SystemMessage::ChannelIconChanged {
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
}
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?;
}
_ => 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.
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn fetch_channel(
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,

View File

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

View File

@@ -1,16 +1,23 @@
use revolt_quark::{
models::{Channel, User},
Db, EmptyResponse, Error, Permission, Ref, Result,
};
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove Member from Group
///
/// Removes a user from the group.
#[openapi(tag = "Groups")]
#[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() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
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, ..
} => {
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?;
if user.id == member.id {
return Err(Error::CannotRemoveYourself);
return Err(create_error!(CannotRemoveYourself));
}
if !recipients.iter().any(|x| *x == member.id) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
channel
.remove_user_from_group(db, &member.id, Some(&user.id), false)
.remove_user_from_group(db, &member, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}
}

View File

@@ -1,9 +1,12 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
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
///
@@ -12,16 +15,23 @@ use rocket::serde::json::Json;
/// Channel must be a `TextChannel`.
#[openapi(tag = "Channel 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() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.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::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
use futures::future::join_all;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, 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};
/// # Fetch Group Members
///
/// Retrieves all users who are part of this group.
#[openapi(tag = "Groups")]
#[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?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
if let Channel::Group { recipients, .. } = channel {
Ok(Json(
db.fetch_users(&recipients)
.await?
.into_iter()
.map(|x| x.with_relationship(&user))
.collect::<Vec<User>>(),
join_all(
db.fetch_users(&recipients)
.await?
.into_iter()
.map(|other_user| other_user.into(db, &user)),
)
.await,
))
} else {
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}

View File

@@ -1,20 +1,16 @@
use chrono::Utc;
use revolt_quark::{
models::{Message, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
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 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
///
/// 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.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn req(
db: &Db,
pub async fn bulk_delete_messages(
db: &State<Database>,
user: User,
target: Ref,
options: Json<OptionsBulkDelete>,
target: Reference,
options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> {
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
for id in &options.ids {
if ulid::Ulid::from_string(id)
.map_err(|_| Error::InvalidOperation)?
.map_err(|_| create_error!(InvalidOperation))?
.datetime()
.signed_duration_since(Utc::now())
.num_days()
.abs()
> 7
{
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
Message::bulk_delete(db, &target.id, options.ids)
.await

View File

@@ -1,7 +1,11 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
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
///
@@ -10,15 +14,20 @@ use revolt_quark::{
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
pub async fn clear_reactions(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
// 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
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 a message you've sent or one you have permission to delete.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/<msg>", rank = 2)]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let message = msg.as_message(db).await?;
if message.channel != target.id {
return Err(Error::NotFound);
}
pub async fn delete(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, &target.id).await?;
if message.author != user.id {
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
message.delete(db).await.map(|_| EmptyResponse)

View File

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