init
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
export type SkinTone = 1 | 2 | 3 | 4 | 5
|
||||||
|
|
||||||
|
const SKIN_TONE_BASE = 0x1f3fb
|
||||||
|
const SKIN_TONE_MIN = 0x1f3fb
|
||||||
|
const SKIN_TONE_MAX = 0x1f3ff
|
||||||
|
const VARIATION_SELECTOR_16 = 0xfe0f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a Mattermost-style skin tone to an RGI emoji sequence.
|
||||||
|
*
|
||||||
|
* The tone is inserted after the first emoji code point and before an
|
||||||
|
* optional presentation selector, which also works for ZWJ sequences such
|
||||||
|
* as woman technologist.
|
||||||
|
*/
|
||||||
|
export function applySkinTone(sequence: string, tone: SkinTone | null): string {
|
||||||
|
if (tone === null) return sequence
|
||||||
|
|
||||||
|
const codePoints = Array.from(sequence)
|
||||||
|
.map((character) => character.codePointAt(0)!)
|
||||||
|
.filter((codePoint) => codePoint < SKIN_TONE_MIN || codePoint > SKIN_TONE_MAX)
|
||||||
|
|
||||||
|
const modifier = SKIN_TONE_BASE + tone - 1
|
||||||
|
const insertionIndex = codePoints[1] === VARIATION_SELECTOR_16 ? 1 : codePoints.length > 0 ? 1 : 0
|
||||||
|
codePoints.splice(insertionIndex, 0, modifier)
|
||||||
|
|
||||||
|
return String.fromCodePoint(...codePoints)
|
||||||
|
}
|
||||||
@@ -712,6 +712,81 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
seed_unicode_emojis(manager).await?;
|
seed_unicode_emojis(manager).await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("message_reaction"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Alias::new("message_id")).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(Alias::new("user_id")).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(Alias::new("emoji_id")).uuid().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("skin_tone"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(0),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_reaction_message")
|
||||||
|
.from(Alias::new("message_reaction"), Alias::new("message_id"))
|
||||||
|
.to(Alias::new("message"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_reaction_user")
|
||||||
|
.from(Alias::new("message_reaction"), Alias::new("user_id"))
|
||||||
|
.to(Alias::new("user"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_reaction_emoji")
|
||||||
|
.from(Alias::new("message_reaction"), Alias::new("emoji_id"))
|
||||||
|
.to(Alias::new("emoji"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("uq_message_reaction")
|
||||||
|
.table(Alias::new("message_reaction"))
|
||||||
|
.col(Alias::new("message_id"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.col(Alias::new("emoji_id"))
|
||||||
|
.col(Alias::new("skin_tone"))
|
||||||
|
.unique()
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_message_reaction_message_id")
|
||||||
|
.table(Alias::new("message_reaction"))
|
||||||
|
.col(Alias::new("message_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
@@ -976,6 +1051,7 @@ impl MigrationTrait for Migration {
|
|||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
let tables = [
|
let tables = [
|
||||||
"computed_permission",
|
"computed_permission",
|
||||||
|
"message_reaction",
|
||||||
"emoji",
|
"emoji",
|
||||||
"channel_user_read_state",
|
"channel_user_read_state",
|
||||||
"channel_user_permission",
|
"channel_user_permission",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::domain::dto::reaction::ReactionGroupResponse;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
@@ -13,6 +14,7 @@ pub struct MessageResponse {
|
|||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: Option<DateTime<Utc>>,
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
pub reply_to_id: Option<Uuid>,
|
pub reply_to_id: Option<Uuid>,
|
||||||
|
pub reactions: Vec<ReactionGroupResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod channel;
|
|||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod reaction;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub struct CreateReactionRequest {
|
||||||
|
pub emoji_id: Uuid,
|
||||||
|
pub skin_tone: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct DeleteReactionQuery {
|
||||||
|
pub skin_tone: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
|
pub struct ReactionResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub message_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub emoji_id: Uuid,
|
||||||
|
pub skin_tone: Option<u8>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ReactionGroupResponse {
|
||||||
|
pub emoji_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub unicode_sequence: Option<String>,
|
||||||
|
pub asset_url: Option<String>,
|
||||||
|
pub skin_tone: Option<u8>,
|
||||||
|
pub count: u64,
|
||||||
|
pub user_ids: Vec<Uuid>,
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::models::message;
|
use crate::models::message;
|
||||||
|
use crate::models::message_reaction;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -21,3 +22,17 @@ pub struct MessageDeletedEvent {
|
|||||||
pub channel_id: Uuid,
|
pub channel_id: Uuid,
|
||||||
pub message: message::Model,
|
pub message: message::Model,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MessageReactionAddedEvent {
|
||||||
|
pub server_id: Option<Uuid>,
|
||||||
|
pub channel_id: Uuid,
|
||||||
|
pub reaction: message_reaction::Model,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MessageReactionRemovedEvent {
|
||||||
|
pub server_id: Option<Uuid>,
|
||||||
|
pub channel_id: Uuid,
|
||||||
|
pub reaction: message_reaction::Model,
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct Model {
|
|||||||
pub sha256: Option<String>,
|
pub sha256: Option<String>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub reactions: HasMany<super::message_reaction::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ pub struct Model {
|
|||||||
pub reply_to_id: Option<Uuid>,
|
pub reply_to_id: Option<Uuid>,
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub attachments: HasMany<super::attachment::Entity>,
|
pub attachments: HasMany<super::attachment::Entity>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub reactions: HasMany<super::message_reaction::Entity>,
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "channel_id",
|
from = "channel_id",
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use sea_orm::Set;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "message_reaction")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub message_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub emoji_id: Uuid,
|
||||||
|
pub skin_tone: i32,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
#[sea_orm(belongs_to, from = "message_id", to = "id", on_delete = "Cascade")]
|
||||||
|
pub message: HasOne<super::message::Entity>,
|
||||||
|
#[sea_orm(belongs_to, from = "user_id", to = "id", on_delete = "Cascade")]
|
||||||
|
pub user: HasOne<super::user::Entity>,
|
||||||
|
#[sea_orm(belongs_to, from = "emoji_id", to = "id", on_delete = "Cascade")]
|
||||||
|
pub emoji: HasOne<super::emoji::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::now_v7()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ pub mod channel_user_read_state;
|
|||||||
pub mod computed_permission;
|
pub mod computed_permission;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod message_reaction;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod role_user;
|
pub mod role_user;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub use super::channel_user_read_state::Entity as ChannelUserReadState;
|
|||||||
pub use super::computed_permission::Entity as ComputedPermission;
|
pub use super::computed_permission::Entity as ComputedPermission;
|
||||||
pub use super::emoji::Entity as Emoji;
|
pub use super::emoji::Entity as Emoji;
|
||||||
pub use super::message::Entity as Message;
|
pub use super::message::Entity as Message;
|
||||||
|
pub use super::message_reaction::Entity as MessageReaction;
|
||||||
pub use super::role::Entity as Group;
|
pub use super::role::Entity as Group;
|
||||||
pub use super::role_user::Entity as GroupMember;
|
pub use super::role_user::Entity as GroupMember;
|
||||||
pub use super::server::Entity as Server;
|
pub use super::server::Entity as Server;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub struct Model {
|
|||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
pub messages: HasMany<super::message::Entity>,
|
pub messages: HasMany<super::message::Entity>,
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
|
pub message_reactions: HasMany<super::message_reaction::Entity>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
pub server_users: HasMany<super::server_user::Entity>,
|
pub server_users: HasMany<super::server_user::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::models::message_reaction;
|
||||||
|
use crate::repositories::{AnyResult, RepositoryContext};
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MessageReactionRepository {
|
||||||
|
pub context: Arc<RepositoryContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageReactionRepository {
|
||||||
|
pub async fn find(
|
||||||
|
&self,
|
||||||
|
message_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
emoji_id: Uuid,
|
||||||
|
skin_tone: i32,
|
||||||
|
) -> AnyResult<Option<message_reaction::Model>> {
|
||||||
|
Ok(message_reaction::Entity::find()
|
||||||
|
.filter(message_reaction::Column::MessageId.eq(message_id))
|
||||||
|
.filter(message_reaction::Column::UserId.eq(user_id))
|
||||||
|
.filter(message_reaction::Column::EmojiId.eq(emoji_id))
|
||||||
|
.filter(message_reaction::Column::SkinTone.eq(skin_tone))
|
||||||
|
.one(&self.context.db)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_by_message_ids(
|
||||||
|
&self,
|
||||||
|
message_ids: &[Uuid],
|
||||||
|
) -> AnyResult<Vec<message_reaction::Model>> {
|
||||||
|
if message_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
Ok(message_reaction::Entity::find()
|
||||||
|
.filter(message_reaction::Column::MessageId.is_in(message_ids.iter().copied()))
|
||||||
|
.all(&self.context.db)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
&self,
|
||||||
|
active: message_reaction::ActiveModel,
|
||||||
|
) -> AnyResult<message_reaction::Model> {
|
||||||
|
Ok(active.insert(&self.context.db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(
|
||||||
|
&self,
|
||||||
|
message_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
emoji_id: Uuid,
|
||||||
|
skin_tone: i32,
|
||||||
|
) -> AnyResult<Option<message_reaction::Model>> {
|
||||||
|
let existing = self.find(message_id, user_id, emoji_id, skin_tone).await?;
|
||||||
|
let Some(existing) = existing else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
message_reaction::Entity::delete_by_id(existing.id)
|
||||||
|
.exec(&self.context.db)
|
||||||
|
.await?;
|
||||||
|
Ok(Some(existing))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use crate::repositories::channel::ChannelRepository;
|
|||||||
use crate::repositories::computed_permission::ComputedPermissionRepository;
|
use crate::repositories::computed_permission::ComputedPermissionRepository;
|
||||||
use crate::repositories::emoji::EmojiRepository;
|
use crate::repositories::emoji::EmojiRepository;
|
||||||
use crate::repositories::message::MessageRepository;
|
use crate::repositories::message::MessageRepository;
|
||||||
|
use crate::repositories::message_reaction::MessageReactionRepository;
|
||||||
use crate::repositories::read_state::ReadStateRepository;
|
use crate::repositories::read_state::ReadStateRepository;
|
||||||
use crate::repositories::role::RoleRepository;
|
use crate::repositories::role::RoleRepository;
|
||||||
use crate::repositories::server::ServerRepository;
|
use crate::repositories::server::ServerRepository;
|
||||||
@@ -19,6 +20,7 @@ mod channel;
|
|||||||
mod computed_permission;
|
mod computed_permission;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
mod message;
|
mod message;
|
||||||
|
mod message_reaction;
|
||||||
mod read_state;
|
mod read_state;
|
||||||
mod role;
|
mod role;
|
||||||
mod server;
|
mod server;
|
||||||
@@ -39,6 +41,7 @@ pub struct Repositories {
|
|||||||
pub channel: ChannelRepository,
|
pub channel: ChannelRepository,
|
||||||
pub role: RoleRepository,
|
pub role: RoleRepository,
|
||||||
pub message: MessageRepository,
|
pub message: MessageRepository,
|
||||||
|
pub message_reaction: MessageReactionRepository,
|
||||||
pub read_state: ReadStateRepository,
|
pub read_state: ReadStateRepository,
|
||||||
pub user: UserRepository,
|
pub user: UserRepository,
|
||||||
pub computed_permission: ComputedPermissionRepository,
|
pub computed_permission: ComputedPermissionRepository,
|
||||||
@@ -67,6 +70,9 @@ impl Repositories {
|
|||||||
message: MessageRepository {
|
message: MessageRepository {
|
||||||
context: context.clone(),
|
context: context.clone(),
|
||||||
},
|
},
|
||||||
|
message_reaction: MessageReactionRepository {
|
||||||
|
context: context.clone(),
|
||||||
|
},
|
||||||
read_state: ReadStateRepository {
|
read_state: ReadStateRepository {
|
||||||
context: context.clone(),
|
context: context.clone(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use crate::domain::events::message::{
|
use crate::domain::events::message::{
|
||||||
MessageCreatedEvent, MessageDeletedEvent, MessageUpdatedEvent,
|
MessageCreatedEvent, MessageDeletedEvent, MessageReactionAddedEvent,
|
||||||
|
MessageReactionRemovedEvent, MessageUpdatedEvent,
|
||||||
};
|
};
|
||||||
use crate::models::user::Model as User;
|
use crate::models::user::Model as User;
|
||||||
use crate::routes::category::mapper::category_model_to_category_response;
|
use crate::routes::category::mapper::category_model_to_category_response;
|
||||||
use crate::routes::channel::mapper::channel_model_to_channel_response;
|
use crate::routes::channel::mapper::channel_model_to_channel_response;
|
||||||
use crate::routes::message::mapper::message_model_to_message_response_with_server_id;
|
use crate::routes::message::mapper::{
|
||||||
|
message_model_to_message_response_with_reactions,
|
||||||
|
message_model_to_message_response_with_server_id, reaction_model_to_response,
|
||||||
|
};
|
||||||
use crate::routes::server::mapper::server_model_to_server_response;
|
use crate::routes::server::mapper::server_model_to_server_response;
|
||||||
use crate::services::Services;
|
use crate::services::Services;
|
||||||
use axum::extract::ws::Message;
|
use axum::extract::ws::Message;
|
||||||
@@ -70,12 +74,22 @@ impl GatewayManager {
|
|||||||
event_bus.on_async::<MessageUpdatedEvent, _, _>("message_updated", move |event| {
|
event_bus.on_async::<MessageUpdatedEvent, _, _>("message_updated", move |event| {
|
||||||
let manager = Arc::clone(&manager);
|
let manager = Arc::clone(&manager);
|
||||||
async move {
|
async move {
|
||||||
|
let message_id = event.message.id;
|
||||||
|
let reactions = manager
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.grouped_for_messages(&[message_id])
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut groups| groups.remove(&message_id))
|
||||||
|
.unwrap_or_default();
|
||||||
manager.broadcast_message(
|
manager.broadcast_message(
|
||||||
event.channel_id,
|
event.channel_id,
|
||||||
"update",
|
"update",
|
||||||
message_model_to_message_response_with_server_id(
|
message_model_to_message_response_with_reactions(
|
||||||
event.message,
|
event.message,
|
||||||
event.server_id,
|
event.server_id,
|
||||||
|
reactions,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -88,6 +102,36 @@ impl GatewayManager {
|
|||||||
manager.broadcast_message(event.channel_id, "remove", event.message.id);
|
manager.broadcast_message(event.channel_id, "remove", event.message.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let manager = Arc::clone(self);
|
||||||
|
event_bus.on_async::<MessageReactionAddedEvent, _, _>(
|
||||||
|
"message_reaction_added",
|
||||||
|
move |event| {
|
||||||
|
let manager = Arc::clone(&manager);
|
||||||
|
async move {
|
||||||
|
manager.broadcast_reaction(
|
||||||
|
event.channel_id,
|
||||||
|
"add",
|
||||||
|
reaction_model_to_response(event.reaction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let manager = Arc::clone(self);
|
||||||
|
event_bus.on_async::<MessageReactionRemovedEvent, _, _>(
|
||||||
|
"message_reaction_removed",
|
||||||
|
move |event| {
|
||||||
|
let manager = Arc::clone(&manager);
|
||||||
|
async move {
|
||||||
|
manager.broadcast_reaction(
|
||||||
|
event.channel_id,
|
||||||
|
"remove",
|
||||||
|
reaction_model_to_response(event.reaction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_client(&self, gateway_client: GatewayClient) {
|
pub(crate) fn add_client(&self, gateway_client: GatewayClient) {
|
||||||
@@ -126,6 +170,33 @@ impl GatewayManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn broadcast_reaction<T: serde::Serialize>(
|
||||||
|
&self,
|
||||||
|
channel_id: Uuid,
|
||||||
|
action: &'static str,
|
||||||
|
content: T,
|
||||||
|
) {
|
||||||
|
let event = GatewayEvent {
|
||||||
|
namespace: "Reaction",
|
||||||
|
action,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
let Ok(json) = serde_json::to_string(&event) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let users = self
|
||||||
|
.services
|
||||||
|
.realtime_registry
|
||||||
|
.users_for_channel(channel_id);
|
||||||
|
let clients = self.clients.read();
|
||||||
|
for (key, client) in clients.iter() {
|
||||||
|
if users.contains(&key.user_id) {
|
||||||
|
let _ = client.sender.send(Message::Text(json.clone().into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GatewayClient {
|
impl GatewayClient {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::domain::dto::message::{
|
|||||||
CreateMessageRequest, MessagePageResponse, MessageQueryParams, MessageResponse,
|
CreateMessageRequest, MessagePageResponse, MessageQueryParams, MessageResponse,
|
||||||
UpdateMessageRequest,
|
UpdateMessageRequest,
|
||||||
};
|
};
|
||||||
|
use crate::domain::dto::reaction::{CreateReactionRequest, DeleteReactionQuery, ReactionResponse};
|
||||||
use crate::http::context::CurrentUser;
|
use crate::http::context::CurrentUser;
|
||||||
use crate::http::error::HTTPError;
|
use crate::http::error::HTTPError;
|
||||||
use crate::routes::message::mapper;
|
use crate::routes::message::mapper;
|
||||||
@@ -39,6 +40,12 @@ pub async fn get_all(
|
|||||||
|
|
||||||
let params = mapper::query_params_to_message_filter(filters);
|
let params = mapper::query_params_to_message_filter(filters);
|
||||||
let page = state.repositories.message.filter(params).await?;
|
let page = state.repositories.message.filter(params).await?;
|
||||||
|
let message_ids: Vec<_> = page.messages.iter().map(|message| message.id).collect();
|
||||||
|
let mut reactions = state
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.grouped_for_messages(&message_ids)
|
||||||
|
.await?;
|
||||||
let oldest_id = page.messages.first().map(|message| message.id);
|
let oldest_id = page.messages.first().map(|message| message.id);
|
||||||
let newest_id = page.messages.last().map(|message| message.id);
|
let newest_id = page.messages.last().map(|message| message.id);
|
||||||
|
|
||||||
@@ -46,7 +53,10 @@ pub async fn get_all(
|
|||||||
messages: page
|
messages: page
|
||||||
.messages
|
.messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(mapper::message_model_to_message_response)
|
.map(|message| {
|
||||||
|
let groups = reactions.remove(&message.id).unwrap_or_default();
|
||||||
|
mapper::message_model_to_message_response_with_reactions(message, None, groups)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
oldest_id,
|
oldest_id,
|
||||||
newest_id,
|
newest_id,
|
||||||
@@ -80,7 +90,17 @@ pub async fn get_by_id(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
Ok(Json(mapper::message_model_to_message_response(message)))
|
let reactions = state
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.grouped_for_messages(&[id])
|
||||||
|
.await?
|
||||||
|
.remove(&id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Json(
|
||||||
|
mapper::message_model_to_message_response_with_reactions(message, None, reactions),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crée un nouveau message
|
/// Crée un nouveau message
|
||||||
@@ -181,7 +201,81 @@ pub async fn update(
|
|||||||
.update_message(id, payload.content)
|
.update_message(id, payload.content)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(mapper::message_model_to_message_response(message)))
|
let reactions = state
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.grouped_for_messages(&[id])
|
||||||
|
.await?
|
||||||
|
.remove(&id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(Json(
|
||||||
|
mapper::message_model_to_message_response_with_reactions(message, None, reactions),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/messages/{message_id}/reactions",
|
||||||
|
request_body = CreateReactionRequest,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Réaction ajoutée", body = ReactionResponse),
|
||||||
|
(status = 200, description = "Réaction déjà présente", body = ReactionResponse),
|
||||||
|
(status = 400, description = "Emoji ou ton invalide"),
|
||||||
|
(status = 404, description = "Message non trouvé")
|
||||||
|
),
|
||||||
|
params(("message_id" = Uuid, Path)),
|
||||||
|
tag = "Messages",
|
||||||
|
security(("bearerAuth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn add_reaction(
|
||||||
|
user: CurrentUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(message_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<CreateReactionRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<ReactionResponse>), HTTPError> {
|
||||||
|
let (reaction, created) = state
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.add(message_id, user.id, payload.emoji_id, payload.skin_tone)
|
||||||
|
.await?;
|
||||||
|
Ok((
|
||||||
|
if created {
|
||||||
|
StatusCode::CREATED
|
||||||
|
} else {
|
||||||
|
StatusCode::OK
|
||||||
|
},
|
||||||
|
Json(mapper::reaction_model_to_response(reaction)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/messages/{message_id}/reactions/{emoji_id}",
|
||||||
|
responses(
|
||||||
|
(status = 204, description = "Réaction supprimée"),
|
||||||
|
(status = 400, description = "Emoji ou ton invalide"),
|
||||||
|
(status = 404, description = "Réaction non trouvée")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("message_id" = Uuid, Path),
|
||||||
|
("emoji_id" = Uuid, Path),
|
||||||
|
DeleteReactionQuery
|
||||||
|
),
|
||||||
|
tag = "Messages",
|
||||||
|
security(("bearerAuth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn remove_reaction(
|
||||||
|
user: CurrentUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((message_id, emoji_id)): Path<(Uuid, Uuid)>,
|
||||||
|
Query(query): Query<DeleteReactionQuery>,
|
||||||
|
) -> Result<StatusCode, HTTPError> {
|
||||||
|
state
|
||||||
|
.services
|
||||||
|
.message_reaction
|
||||||
|
.remove(message_id, user.id, emoji_id, query.skin_tone)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime un message
|
/// Supprime un message
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use crate::domain::dto::message::{
|
use crate::domain::dto::message::{
|
||||||
CreateMessageRequest, MessageQueryParams, MessageResponse, UpdateMessageRequest,
|
CreateMessageRequest, MessageQueryParams, MessageResponse, UpdateMessageRequest,
|
||||||
};
|
};
|
||||||
|
use crate::domain::dto::reaction::ReactionGroupResponse;
|
||||||
|
use crate::domain::dto::reaction::ReactionResponse;
|
||||||
use crate::models::message;
|
use crate::models::message;
|
||||||
|
use crate::models::message_reaction;
|
||||||
use crate::repositories::types::MessageFilter;
|
use crate::repositories::types::MessageFilter;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sea_orm::Set;
|
use sea_orm::Set;
|
||||||
@@ -14,6 +17,14 @@ pub fn message_model_to_message_response(model: message::Model) -> MessageRespon
|
|||||||
pub fn message_model_to_message_response_with_server_id(
|
pub fn message_model_to_message_response_with_server_id(
|
||||||
model: message::Model,
|
model: message::Model,
|
||||||
server_id: Option<Uuid>,
|
server_id: Option<Uuid>,
|
||||||
|
) -> MessageResponse {
|
||||||
|
message_model_to_message_response_with_reactions(model, server_id, Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message_model_to_message_response_with_reactions(
|
||||||
|
model: message::Model,
|
||||||
|
server_id: Option<Uuid>,
|
||||||
|
reactions: Vec<ReactionGroupResponse>,
|
||||||
) -> MessageResponse {
|
) -> MessageResponse {
|
||||||
MessageResponse {
|
MessageResponse {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
@@ -24,6 +35,7 @@ pub fn message_model_to_message_response_with_server_id(
|
|||||||
created_at: model.created_at,
|
created_at: model.created_at,
|
||||||
updated_at: model.updated_at,
|
updated_at: model.updated_at,
|
||||||
reply_to_id: model.reply_to_id,
|
reply_to_id: model.reply_to_id,
|
||||||
|
reactions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,3 +74,14 @@ pub fn query_params_to_message_filter(params: MessageQueryParams) -> MessageFilt
|
|||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reaction_model_to_response(model: message_reaction::Model) -> ReactionResponse {
|
||||||
|
ReactionResponse {
|
||||||
|
id: model.id,
|
||||||
|
message_id: model.message_id,
|
||||||
|
user_id: model.user_id,
|
||||||
|
emoji_id: model.emoji_id,
|
||||||
|
skin_tone: (model.skin_tone != 0).then_some(model.skin_tone as u8),
|
||||||
|
created_at: model.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,4 +11,12 @@ pub fn router() -> Router<AppState> {
|
|||||||
.put(handlers::update)
|
.put(handlers::update)
|
||||||
.delete(handlers::delete),
|
.delete(handlers::delete),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/messages/{message_id}/reactions",
|
||||||
|
axum::routing::post(handlers::add_reaction),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/messages/{message_id}/reactions/{emoji_id}",
|
||||||
|
axum::routing::delete(handlers::remove_reaction),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ use utoipa::{Modify, OpenApi};
|
|||||||
message::handlers::create,
|
message::handlers::create,
|
||||||
message::handlers::update,
|
message::handlers::update,
|
||||||
message::handlers::delete,
|
message::handlers::delete,
|
||||||
|
message::handlers::add_reaction,
|
||||||
|
message::handlers::remove_reaction,
|
||||||
core::handlers::join,
|
core::handlers::join,
|
||||||
emoji::handlers::get_all,
|
emoji::handlers::get_all,
|
||||||
emoji::handlers::get_by_id,
|
emoji::handlers::get_by_id,
|
||||||
@@ -73,6 +75,9 @@ use utoipa::{Modify, OpenApi};
|
|||||||
crate::domain::dto::message::MessageResponse,
|
crate::domain::dto::message::MessageResponse,
|
||||||
crate::domain::dto::message::CreateMessageRequest,
|
crate::domain::dto::message::CreateMessageRequest,
|
||||||
crate::domain::dto::message::UpdateMessageRequest,
|
crate::domain::dto::message::UpdateMessageRequest,
|
||||||
|
crate::domain::dto::reaction::CreateReactionRequest,
|
||||||
|
crate::domain::dto::reaction::ReactionResponse,
|
||||||
|
crate::domain::dto::reaction::ReactionGroupResponse,
|
||||||
crate::domain::dto::core::JoinRequest,
|
crate::domain::dto::core::JoinRequest,
|
||||||
ChannelType,
|
ChannelType,
|
||||||
crate::domain::dto::emoji::EmojiResponse,
|
crate::domain::dto::emoji::EmojiResponse,
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use crate::domain::dto::reaction::ReactionGroupResponse;
|
||||||
|
use crate::domain::events::message::{MessageReactionAddedEvent, MessageReactionRemovedEvent};
|
||||||
|
use crate::http::error::HTTPError;
|
||||||
|
use crate::models::{emoji, message_reaction};
|
||||||
|
use crate::services::ServicesContext;
|
||||||
|
use event_bus::Scope;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MessageReactionService {
|
||||||
|
context: Arc<ServicesContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageReactionService {
|
||||||
|
pub fn new(context: Arc<ServicesContext>) -> Self {
|
||||||
|
Self { context }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_skin_tone(skin_tone: Option<u8>) -> Result<i32, HTTPError> {
|
||||||
|
match skin_tone {
|
||||||
|
None => Ok(0),
|
||||||
|
Some(tone @ 1..=5) => Ok(tone as i32),
|
||||||
|
Some(_) => Err(HTTPError::BadRequest(
|
||||||
|
"skin_tone must be between 1 and 5".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
message_id: Uuid,
|
||||||
|
emoji_id: Uuid,
|
||||||
|
skin_tone: Option<u8>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
crate::models::message::Model,
|
||||||
|
emoji::Model,
|
||||||
|
i32,
|
||||||
|
Option<Uuid>,
|
||||||
|
),
|
||||||
|
HTTPError,
|
||||||
|
> {
|
||||||
|
let message = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.message
|
||||||
|
.get_by_id(message_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
let channel = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.channel
|
||||||
|
.get_by_id(message.channel_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
let emoji = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.emoji
|
||||||
|
.get_by_id(emoji_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| HTTPError::BadRequest("Emoji not found".into()))?;
|
||||||
|
|
||||||
|
if emoji.server_id.is_some() && emoji.server_id != channel.server_id {
|
||||||
|
return Err(HTTPError::BadRequest(
|
||||||
|
"Emoji is not available in this message scope".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized_tone = Self::normalize_skin_tone(skin_tone)?;
|
||||||
|
if normalized_tone != 0 && (emoji.emoji_type != "unicode" || !emoji.supports_skin_tone) {
|
||||||
|
return Err(HTTPError::BadRequest(
|
||||||
|
"This emoji does not support skin tones".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((message, emoji, normalized_tone, channel.server_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add(
|
||||||
|
&self,
|
||||||
|
message_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
emoji_id: Uuid,
|
||||||
|
skin_tone: Option<u8>,
|
||||||
|
) -> Result<(message_reaction::Model, bool), HTTPError> {
|
||||||
|
let (message, _, skin_tone, server_id) =
|
||||||
|
self.validate(message_id, emoji_id, skin_tone).await?;
|
||||||
|
|
||||||
|
if let Some(existing) = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.message_reaction
|
||||||
|
.find(message_id, user_id, emoji_id, skin_tone)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok((existing, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let reaction = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.message_reaction
|
||||||
|
.create(message_reaction::ActiveModel {
|
||||||
|
id: Set(Uuid::now_v7()),
|
||||||
|
message_id: Set(message_id),
|
||||||
|
user_id: Set(user_id),
|
||||||
|
emoji_id: Set(emoji_id),
|
||||||
|
skin_tone: Set(skin_tone),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut scopes = vec![Scope::uuid("channel", message.channel_id)];
|
||||||
|
if let Some(server_id) = server_id {
|
||||||
|
scopes.push(Scope::uuid("server", server_id));
|
||||||
|
}
|
||||||
|
self.context.event_bus.emit_scoped(
|
||||||
|
"message_reaction_added",
|
||||||
|
scopes,
|
||||||
|
MessageReactionAddedEvent {
|
||||||
|
server_id,
|
||||||
|
channel_id: message.channel_id,
|
||||||
|
reaction: reaction.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((reaction, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(
|
||||||
|
&self,
|
||||||
|
message_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
emoji_id: Uuid,
|
||||||
|
skin_tone: Option<u8>,
|
||||||
|
) -> Result<message_reaction::Model, HTTPError> {
|
||||||
|
let (message, _, skin_tone, server_id) =
|
||||||
|
self.validate(message_id, emoji_id, skin_tone).await?;
|
||||||
|
let reaction = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.message_reaction
|
||||||
|
.delete(message_id, user_id, emoji_id, skin_tone)
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
let mut scopes = vec![Scope::uuid("channel", message.channel_id)];
|
||||||
|
if let Some(server_id) = server_id {
|
||||||
|
scopes.push(Scope::uuid("server", server_id));
|
||||||
|
}
|
||||||
|
self.context.event_bus.emit_scoped(
|
||||||
|
"message_reaction_removed",
|
||||||
|
scopes,
|
||||||
|
MessageReactionRemovedEvent {
|
||||||
|
server_id,
|
||||||
|
channel_id: message.channel_id,
|
||||||
|
reaction: reaction.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(reaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn grouped_for_messages(
|
||||||
|
&self,
|
||||||
|
message_ids: &[Uuid],
|
||||||
|
) -> Result<HashMap<Uuid, Vec<ReactionGroupResponse>>, HTTPError> {
|
||||||
|
let reactions = self
|
||||||
|
.context
|
||||||
|
.repositories
|
||||||
|
.message_reaction
|
||||||
|
.list_by_message_ids(message_ids)
|
||||||
|
.await?;
|
||||||
|
if reactions.is_empty() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
let emoji_ids: Vec<_> = reactions.iter().map(|reaction| reaction.emoji_id).collect();
|
||||||
|
let emojis: HashMap<_, _> = emoji::Entity::find()
|
||||||
|
.filter(emoji::Column::Id.is_in(emoji_ids))
|
||||||
|
.all(&self.context.repositories.emoji.context.db)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|emoji| (emoji.id, emoji))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut grouped: HashMap<Uuid, HashMap<(Uuid, i32), ReactionGroupResponse>> =
|
||||||
|
HashMap::new();
|
||||||
|
for reaction in reactions {
|
||||||
|
let Some(emoji) = emojis.get(&reaction.emoji_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let groups = grouped.entry(reaction.message_id).or_default();
|
||||||
|
let group = groups
|
||||||
|
.entry((reaction.emoji_id, reaction.skin_tone))
|
||||||
|
.or_insert_with(|| ReactionGroupResponse {
|
||||||
|
emoji_id: emoji.id,
|
||||||
|
name: emoji.name.clone(),
|
||||||
|
unicode_sequence: emoji.unicode_sequence.clone(),
|
||||||
|
asset_url: emoji
|
||||||
|
.file_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| format!("/api/emojis/{}/asset", emoji.id)),
|
||||||
|
skin_tone: (reaction.skin_tone != 0).then_some(reaction.skin_tone as u8),
|
||||||
|
count: 0,
|
||||||
|
user_ids: Vec::new(),
|
||||||
|
});
|
||||||
|
group.count += 1;
|
||||||
|
group.user_ids.push(reaction.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(grouped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(message_id, groups)| {
|
||||||
|
let mut groups: Vec<_> = groups.into_values().collect();
|
||||||
|
for group in &mut groups {
|
||||||
|
group.user_ids.sort_unstable();
|
||||||
|
}
|
||||||
|
groups.sort_by(|left, right| {
|
||||||
|
left.name
|
||||||
|
.cmp(&right.name)
|
||||||
|
.then(left.skin_tone.cmp(&right.skin_tone))
|
||||||
|
});
|
||||||
|
(message_id, groups)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::MessageReactionService;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skin_tone_validation_accepts_only_the_supported_range() {
|
||||||
|
assert_eq!(
|
||||||
|
MessageReactionService::normalize_skin_tone(None).unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
for tone in 1..=5 {
|
||||||
|
assert_eq!(
|
||||||
|
MessageReactionService::normalize_skin_tone(Some(tone)).unwrap(),
|
||||||
|
tone as i32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(MessageReactionService::normalize_skin_tone(Some(0)).is_err());
|
||||||
|
assert!(MessageReactionService::normalize_skin_tone(Some(6)).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use crate::services::category::CategoryService;
|
|||||||
use crate::services::channel::ChannelService;
|
use crate::services::channel::ChannelService;
|
||||||
use crate::services::emoji::EmojiService;
|
use crate::services::emoji::EmojiService;
|
||||||
use crate::services::message::MessageService;
|
use crate::services::message::MessageService;
|
||||||
|
use crate::services::message_reaction::MessageReactionService;
|
||||||
use crate::services::permission::PermissionService;
|
use crate::services::permission::PermissionService;
|
||||||
use crate::services::permission_sync::PermissionSyncService;
|
use crate::services::permission_sync::PermissionSyncService;
|
||||||
use crate::services::role::RoleService;
|
use crate::services::role::RoleService;
|
||||||
@@ -16,6 +17,7 @@ pub mod category;
|
|||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod message_reaction;
|
||||||
mod permission;
|
mod permission;
|
||||||
pub mod permission_sync;
|
pub mod permission_sync;
|
||||||
pub mod realtime_registry;
|
pub mod realtime_registry;
|
||||||
@@ -40,6 +42,7 @@ pub struct Services {
|
|||||||
pub server: Arc<ServerService>,
|
pub server: Arc<ServerService>,
|
||||||
pub category: Arc<CategoryService>,
|
pub category: Arc<CategoryService>,
|
||||||
pub message: Arc<MessageService>,
|
pub message: Arc<MessageService>,
|
||||||
|
pub message_reaction: Arc<MessageReactionService>,
|
||||||
pub user: Arc<UserService>,
|
pub user: Arc<UserService>,
|
||||||
pub role: Arc<RoleService>,
|
pub role: Arc<RoleService>,
|
||||||
pub permission: Arc<PermissionService>,
|
pub permission: Arc<PermissionService>,
|
||||||
@@ -60,6 +63,7 @@ impl Services {
|
|||||||
let server = Arc::new(ServerService::new(service_context.clone()));
|
let server = Arc::new(ServerService::new(service_context.clone()));
|
||||||
let category = Arc::new(CategoryService::new(service_context.clone()));
|
let category = Arc::new(CategoryService::new(service_context.clone()));
|
||||||
let message = Arc::new(MessageService::new(service_context.clone()));
|
let message = Arc::new(MessageService::new(service_context.clone()));
|
||||||
|
let message_reaction = Arc::new(MessageReactionService::new(service_context.clone()));
|
||||||
let user = Arc::new(UserService::new(service_context.clone()));
|
let user = Arc::new(UserService::new(service_context.clone()));
|
||||||
let role = Arc::new(RoleService::new(service_context.clone()));
|
let role = Arc::new(RoleService::new(service_context.clone()));
|
||||||
let permission = Arc::new(PermissionService::new(service_context.clone()));
|
let permission = Arc::new(PermissionService::new(service_context.clone()));
|
||||||
@@ -73,6 +77,7 @@ impl Services {
|
|||||||
server,
|
server,
|
||||||
category,
|
category,
|
||||||
message,
|
message,
|
||||||
|
message_reaction,
|
||||||
user,
|
user,
|
||||||
role,
|
role,
|
||||||
permission,
|
permission,
|
||||||
|
|||||||
Reference in New Issue
Block a user