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:
Gabriel
2026-05-09 00:23:57 +02:00
committed by GitHub
parent d46c7f7f3c
commit 23ad135983
9 changed files with 306 additions and 4 deletions

View 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);
}
}

View File

@@ -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
]
}