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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revolt_models::v0::{
|
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;
|
use crate::Database;
|
||||||
@@ -219,6 +219,12 @@ pub enum EventV1 {
|
|||||||
/// New emoji
|
/// New emoji
|
||||||
EmojiCreate(Emoji),
|
EmojiCreate(Emoji),
|
||||||
|
|
||||||
|
/// Update existing emoji
|
||||||
|
EmojiUpdate {
|
||||||
|
id: String,
|
||||||
|
data: PartialEmoji,
|
||||||
|
},
|
||||||
|
|
||||||
/// Delete emoji
|
/// Delete emoji
|
||||||
EmojiDelete { id: String },
|
EmojiDelete { id: String },
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use revolt_models::v0;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
@@ -41,6 +42,12 @@ auto_derived!(
|
|||||||
Server { id: String },
|
Server { id: String },
|
||||||
Detached,
|
Detached,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Partial representation of an emoji
|
||||||
|
pub struct PartialEmoji {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
@@ -75,6 +82,26 @@ impl Emoji {
|
|||||||
db.detach_emoji(&self).await
|
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
|
/// Check whether we can use a given emoji
|
||||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||||
if Ulid::from_str(emoji).is_ok() {
|
if Ulid::from_str(emoji).is_ok() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::Emoji;
|
use crate::{Emoji, PartialEmoji};
|
||||||
|
|
||||||
#[cfg(feature = "mongodb")]
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
@@ -20,6 +20,9 @@ pub trait AbstractEmojis: Sync + Send {
|
|||||||
/// Fetch emoji by their parent ids
|
/// Fetch emoji by their parent ids
|
||||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
|
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
|
/// Detach an emoji by its id
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use bson::Document;
|
use bson::Document;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::Emoji;
|
use crate::{Emoji, PartialEmoji};
|
||||||
use crate::MongoDb;
|
use crate::MongoDb;
|
||||||
|
|
||||||
use super::AbstractEmojis;
|
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
|
/// Detach an emoji by its id
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||||
self.col::<Document>(COL)
|
self.col::<Document>(COL)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::Emoji;
|
use crate::{Emoji, PartialEmoji};
|
||||||
use crate::EmojiParent;
|
use crate::EmojiParent;
|
||||||
use crate::ReferenceDb;
|
use crate::ReferenceDb;
|
||||||
|
|
||||||
@@ -54,6 +54,19 @@ impl AbstractEmojis for ReferenceDb {
|
|||||||
.collect())
|
.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
|
/// Detach an emoji by its id
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||||
let mut emojis = self.emojis.lock().await;
|
let mut emojis = self.emojis.lock().await;
|
||||||
|
|||||||
@@ -54,4 +54,22 @@ auto_derived!(
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub nsfw: bool,
|
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_create;
|
||||||
mod emoji_delete;
|
mod emoji_delete;
|
||||||
|
mod emoji_edit;
|
||||||
mod emoji_fetch;
|
mod emoji_fetch;
|
||||||
|
|
||||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||||
openapi_get_routes_spec![
|
openapi_get_routes_spec![
|
||||||
emoji_create::create_emoji,
|
emoji_create::create_emoji,
|
||||||
emoji_delete::delete_emoji,
|
emoji_delete::delete_emoji,
|
||||||
|
emoji_edit::edit_emoji,
|
||||||
emoji_fetch::fetch_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
|
### EmojiDelete
|
||||||
|
|
||||||
Emoji has been deleted.
|
Emoji has been deleted.
|
||||||
|
|||||||
Reference in New Issue
Block a user