From baf9cf43d70be8ba616b78bcb8ed2dba6a90b23b Mon Sep 17 00:00:00 2001 From: paring Date: Wed, 4 Feb 2026 20:02:47 +0900 Subject: [PATCH] feat: replied message view --- src/bot/handler.rs | 65 +++-------------------- src/bot/lang.rs | 9 ++++ src/bot/main.rs | 1 + src/bot/message_builder.rs | 102 +++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 57 deletions(-) create mode 100644 src/bot/message_builder.rs diff --git a/src/bot/handler.rs b/src/bot/handler.rs index ac9acf7..61d8260 100644 --- a/src/bot/handler.rs +++ b/src/bot/handler.rs @@ -4,14 +4,13 @@ use anyhow::{Context as _, bail}; use dashmap::DashMap; use serenity::{ all::{ - Attachment, CacheHttp, ChannelId, Component, Context, CreateComponent, CreateMediaGallery, + CacheHttp, ChannelId, Component, Context, CreateComponent, CreateMediaGallery, CreateMediaGalleryItem, CreateSeparator, CreateTextDisplay, CreateUnfurledMediaItem, CreateWebhook, EditWebhookMessage, EventHandler, ExecuteWebhook, FullEvent, GenericChannelId, GuildId, Interaction, Message, MessageFlags, MessageId, MessageSnapshot, - Reaction, StickerFormatType, StickerItem, Webhook, WebhookId, + Reaction, Webhook, WebhookId, }, async_trait, - small_fixed_array::{FixedArray, FixedString}, }; use sqlx::{PgExecutor, PgPool}; use tokio::sync::Semaphore; @@ -19,20 +18,17 @@ use tokio::sync::Semaphore; use crate::{ commands, db::{GuildRow, MessageRow, WebhookRow}, + message_builder::{CloneBuilderMessage, get_message_components}, }; -struct CloneBuilderMessage { - pub content: FixedString, - pub attachments: FixedArray, - pub sticker_items: FixedArray, -} - impl From for CloneBuilderMessage { fn from(value: Message) -> Self { Self { + link: Some(value.link()), content: value.content, attachments: value.attachments, sticker_items: value.sticker_items, + replied_message: value.referenced_message.map(|m| Box::new(Self::from(*m))), } } } @@ -40,9 +36,11 @@ impl From for CloneBuilderMessage { impl From for CloneBuilderMessage { fn from(value: MessageSnapshot) -> Self { CloneBuilderMessage { + link: None, content: value.content, attachments: value.attachments, sticker_items: value.sticker_items, + replied_message: None, } } } @@ -325,54 +323,7 @@ impl Handler { } } } else { - if !reference_msg.content.is_empty() { - components.push(CreateComponent::TextDisplay(CreateTextDisplay::new( - &reference_msg.content, - ))); - } - - let images = reference_msg - .attachments - .iter() - .filter(|x| x.width.is_some()) - .collect::>(); - - if !images.is_empty() { - components.push(CreateComponent::MediaGallery(CreateMediaGallery::new( - images - .into_iter() - .map(|x| { - CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(x.url.clone())) - }) - .collect::>(), - ))); - } - - if let Some(sticker_url) = reference_msg - .sticker_items - .iter() - .filter_map(|x| { - let ext = match x.format_type { - StickerFormatType::Png | StickerFormatType::Apng => "png", - StickerFormatType::Gif => "gif", - _ => return None, - }; - - let url = format!( - "https://media.discordapp.net/stickers/{}.{}?size=160&quality=lossless", - &x.id, ext - ); - - Some(url) - }) - .next() - { - let item = CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(sticker_url)); - - components.push(CreateComponent::MediaGallery(CreateMediaGallery::new( - vec![item], - ))); - } + get_message_components(&reference_msg, &mut components, &guild.lang); } components.push(CreateComponent::Separator(CreateSeparator::new(true))); diff --git a/src/bot/lang.rs b/src/bot/lang.rs index 22be2c9..b329923 100644 --- a/src/bot/lang.rs +++ b/src/bot/lang.rs @@ -45,6 +45,7 @@ pub trait Locale: Send + Sync { fn original_message(&self) -> &'static str; fn admin_required(&self) -> &'static str; + fn replied_message(&self, link: &str) -> String; fn lang_changed(&self, new_lang: Lang) -> String; } @@ -62,6 +63,10 @@ impl Locale for Korean { fn admin_required(&self) -> &'static str { "관리자 권한이 필요합니다" } + + fn replied_message(&self, link: &str) -> String { + format!("[답장한 메시지]({link})") + } } struct English; @@ -78,4 +83,8 @@ impl Locale for English { fn admin_required(&self) -> &'static str { "Administrator permission is required" } + + fn replied_message(&self, link: &str) -> String { + format!("[Replied Message]({link})") + } } diff --git a/src/bot/main.rs b/src/bot/main.rs index 1c8da3f..147b916 100644 --- a/src/bot/main.rs +++ b/src/bot/main.rs @@ -21,6 +21,7 @@ mod config; mod db; mod handler; mod lang; +mod message_builder; mod modal; #[macro_use] diff --git a/src/bot/message_builder.rs b/src/bot/message_builder.rs new file mode 100644 index 0000000..d8885df --- /dev/null +++ b/src/bot/message_builder.rs @@ -0,0 +1,102 @@ +use serenity::{all::*, small_fixed_array::*}; + +use crate::lang::Lang; + +pub struct CloneBuilderMessage { + pub link: Option, + pub content: FixedString, + pub attachments: FixedArray, + pub sticker_items: FixedArray, + pub replied_message: Option>, +} + +pub fn get_message_components( + reference_msg: &CloneBuilderMessage, + components: &mut Vec>, + lang: &Lang, +) { + if let Some(replied) = &reference_msg.replied_message + && let Some(link) = &reference_msg.link + { + let mut replied_components = vec![]; + get_message_components(replied, &mut replied_components, lang); + + let mut inner = vec![]; + + inner.push(CreateContainerComponent::TextDisplay( + CreateTextDisplay::new(lang.to_locale().replied_message(&link.to_string())), + )); + + inner.extend(replied_components.into_iter().filter_map(|x| match x { + CreateComponent::ActionRow(create_action_row) => { + Some(CreateContainerComponent::ActionRow(create_action_row)) + } + CreateComponent::Section(create_section) => { + Some(CreateContainerComponent::Section(create_section)) + } + CreateComponent::TextDisplay(create_text_display) => { + Some(CreateContainerComponent::TextDisplay(create_text_display)) + } + CreateComponent::MediaGallery(create_media_gallery) => { + Some(CreateContainerComponent::MediaGallery(create_media_gallery)) + } + CreateComponent::File(create_file) => Some(CreateContainerComponent::File(create_file)), + CreateComponent::Separator(create_separator) => { + Some(CreateContainerComponent::Separator(create_separator)) + } + CreateComponent::Container(_create_container) => None, + CreateComponent::Label(_create_label) => None, + })); + + let comp = CreateComponent::Container(CreateContainer::new(inner)); + components.push(comp); + components.push(CreateComponent::Separator(CreateSeparator::new(false))); + } + + if !reference_msg.content.is_empty() { + components.push(CreateComponent::TextDisplay(CreateTextDisplay::new( + reference_msg.content.clone(), + ))); + } + + let images = reference_msg + .attachments + .iter() + .filter(|x| x.width.is_some()) + .collect::>(); + + if !images.is_empty() { + components.push(CreateComponent::MediaGallery(CreateMediaGallery::new( + images + .into_iter() + .map(|x| CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(x.url.clone()))) + .collect::>(), + ))); + } + + if let Some(sticker_url) = reference_msg + .sticker_items + .iter() + .filter_map(|x| { + let ext = match x.format_type { + StickerFormatType::Png | StickerFormatType::Apng => "png", + StickerFormatType::Gif => "gif", + _ => return None, + }; + + let url = format!( + "https://media.discordapp.net/stickers/{}.{}?size=160&quality=lossless", + &x.id, ext + ); + + Some(url) + }) + .next() + { + let item = CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(sticker_url)); + + components.push(CreateComponent::MediaGallery(CreateMediaGallery::new( + vec![item], + ))); + } +}