forked from jmug/stoatchat
feat: add emoji rename endpoint (#714)
* feat: add rename endpoint with rename-only update Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> * fix: enforce detached emoji edit restrictions Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> * fix: always enforce emoji edit permissions Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> --------- Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com>
This commit is contained in:
@@ -3,7 +3,7 @@ use revolt_result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
|
||||
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
|
||||
};
|
||||
|
||||
use crate::Database;
|
||||
@@ -219,6 +219,12 @@ pub enum EventV1 {
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Update existing emoji
|
||||
EmojiUpdate {
|
||||
id: String,
|
||||
data: PartialEmoji,
|
||||
},
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_models::v0;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -41,6 +42,12 @@ auto_derived!(
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
|
||||
/// Partial representation of an emoji
|
||||
pub struct PartialEmoji {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
@@ -75,6 +82,26 @@ impl Emoji {
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Update an emoji
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialEmoji) -> Result<()> {
|
||||
if let Some(name) = partial.name.clone() {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
db.update_emoji(&self.id, &partial).await?;
|
||||
|
||||
EventV1::EmojiUpdate {
|
||||
id: self.id.clone(),
|
||||
data: v0::PartialEmoji {
|
||||
name: partial.name.clone(),
|
||||
},
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether we can use a given emoji
|
||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||
if Ulid::from_str(emoji).is_ok() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
@@ -20,6 +20,9 @@ pub trait AbstractEmojis: Sync + Send {
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()>;
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractEmojis;
|
||||
@@ -46,6 +46,11 @@ impl AbstractEmojis for MongoDb {
|
||||
)
|
||||
}
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, emoji_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
use crate::EmojiParent;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
@@ -54,6 +54,19 @@ impl AbstractEmojis for ReferenceDb {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
if let Some(emoji) = emojis.get_mut(emoji_id) {
|
||||
if let Some(name) = partial.name.clone() {
|
||||
emoji.name = name;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
|
||||
@@ -54,4 +54,22 @@ auto_derived!(
|
||||
#[serde(default)]
|
||||
pub nsfw: bool,
|
||||
}
|
||||
|
||||
/// Partial emoji representation
|
||||
#[derive(Default)]
|
||||
pub struct PartialEmoji {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// Edit emoji information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditEmoji {
|
||||
/// Emoji name
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 1, max = 32), regex = "RE_EMOJI")
|
||||
)]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
212
crates/delta/src/routes/customisation/emoji_edit.rs
Normal file
212
crates/delta/src/routes/customisation/emoji_edit.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, EmojiParent, PartialEmoji, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Edit Emoji
|
||||
///
|
||||
/// Edit an emoji by its id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[patch("/emoji/<emoji_id>", data = "<data>")]
|
||||
pub async fn edit_emoji(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
emoji_id: Reference<'_>,
|
||||
data: Json<v0::DataEditEmoji>,
|
||||
) -> Result<Json<v0::Emoji>> {
|
||||
let data = data.into_inner();
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut emoji = emoji_id.as_emoji(db).await?;
|
||||
|
||||
match &emoji.parent {
|
||||
EmojiParent::Server { id } => {
|
||||
let server = db.fetch_server(id.as_str()).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
calculate_server_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?;
|
||||
}
|
||||
EmojiParent::Detached => return Err(create_error!(NotAuthenticated)),
|
||||
}
|
||||
|
||||
if data.name.is_none() {
|
||||
return Ok(Json(emoji.into()));
|
||||
}
|
||||
|
||||
let partial = PartialEmoji { name: data.name };
|
||||
emoji.update(db, partial).await?;
|
||||
|
||||
Ok(Json(emoji.into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::util::test::TestHarness;
|
||||
use revolt_database::{Emoji, EmojiParent, Member};
|
||||
use revolt_models::v0;
|
||||
use rocket::http::{ContentType, Header, Status};
|
||||
use ulid::Ulid;
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn edit_emoji_name_as_creator() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
let (server, _) = harness.new_server(&user).await;
|
||||
|
||||
let emoji_id = Ulid::new().to_string();
|
||||
let emoji = Emoji {
|
||||
id: emoji_id.clone(),
|
||||
parent: EmojiParent::Server {
|
||||
id: server.id.clone(),
|
||||
},
|
||||
creator_id: user.id.clone(),
|
||||
name: "initial_name".to_string(),
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
};
|
||||
emoji.create(&harness.db).await.expect("`Emoji` created");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.patch(format!("/custom/emoji/{emoji_id}"))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.header(ContentType::JSON)
|
||||
.body(
|
||||
json!(v0::DataEditEmoji {
|
||||
name: Some("renamed_emoji".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let edited: v0::Emoji = response.into_json().await.expect("`Emoji`");
|
||||
assert_eq!(edited.name, "renamed_emoji");
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn reject_invalid_emoji_name() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
let (server, _) = harness.new_server(&user).await;
|
||||
|
||||
let emoji_id = Ulid::new().to_string();
|
||||
let emoji = Emoji {
|
||||
id: emoji_id.clone(),
|
||||
parent: EmojiParent::Server {
|
||||
id: server.id.clone(),
|
||||
},
|
||||
creator_id: user.id.clone(),
|
||||
name: "valid_name".to_string(),
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
};
|
||||
emoji.create(&harness.db).await.expect("`Emoji` created");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.patch(format!("/custom/emoji/{emoji_id}"))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.header(ContentType::JSON)
|
||||
.body(
|
||||
json!(v0::DataEditEmoji {
|
||||
name: Some("Invalid Name".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::BadRequest);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn reject_edit_for_detached_emoji() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let emoji_id = Ulid::new().to_string();
|
||||
let emoji = Emoji {
|
||||
id: emoji_id.clone(),
|
||||
parent: EmojiParent::Detached,
|
||||
creator_id: user.id.clone(),
|
||||
name: "detached_name".to_string(),
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
};
|
||||
emoji.create(&harness.db).await.expect("`Emoji` created");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.patch(format!("/custom/emoji/{emoji_id}"))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.header(ContentType::JSON)
|
||||
.body(
|
||||
json!(v0::DataEditEmoji {
|
||||
name: Some("should_not_apply".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Unauthorized);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn reject_edit_for_creator_without_manage_customisation() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, _, owner) = harness.new_user().await;
|
||||
let (_, creator_session, creator) = harness.new_user().await;
|
||||
let (server, _) = harness.new_server(&owner).await;
|
||||
|
||||
Member::create(&harness.db, &server, &creator, None)
|
||||
.await
|
||||
.expect("`Member` created");
|
||||
|
||||
let emoji_id = Ulid::new().to_string();
|
||||
let emoji = Emoji {
|
||||
id: emoji_id.clone(),
|
||||
parent: EmojiParent::Server {
|
||||
id: server.id.clone(),
|
||||
},
|
||||
creator_id: creator.id.clone(),
|
||||
name: "member_uploaded_name".to_string(),
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
};
|
||||
emoji.create(&harness.db).await.expect("`Emoji` created");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.patch(format!("/custom/emoji/{emoji_id}"))
|
||||
.header(Header::new(
|
||||
"x-session-token",
|
||||
creator_session.token.to_string(),
|
||||
))
|
||||
.header(ContentType::JSON)
|
||||
.body(
|
||||
json!(v0::DataEditEmoji {
|
||||
name: Some("renamed_without_permission".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Forbidden);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ use rocket::Route;
|
||||
|
||||
mod emoji_create;
|
||||
mod emoji_delete;
|
||||
mod emoji_edit;
|
||||
mod emoji_fetch;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
emoji_create::create_emoji,
|
||||
emoji_delete::delete_emoji,
|
||||
emoji_edit::edit_emoji,
|
||||
emoji_fetch::fetch_emoji
|
||||
]
|
||||
}
|
||||
|
||||
@@ -539,6 +539,22 @@ Emoji created, the event object has the same schema as the Emoji object in the A
|
||||
}
|
||||
```
|
||||
|
||||
### EmojiUpdate
|
||||
|
||||
Emoji has been updated.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "EmojiUpdate",
|
||||
"id": "{emoji_id}",
|
||||
"data": {
|
||||
"name"?: "{emoji_name}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `data` field contains a partial Emoji object.
|
||||
|
||||
### EmojiDelete
|
||||
|
||||
Emoji has been deleted.
|
||||
|
||||
Reference in New Issue
Block a user