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