feat: replied message view
All checks were successful
Build / Build (push) Successful in 1m59s
Build / docker (push) Successful in 22s

This commit is contained in:
파링 2026-02-04 20:02:47 +09:00
parent e633c61820
commit baf9cf43d7
Signed by: paring
SSH key fingerprint: SHA256:8uCHhCpn/gVOLEaTolmbub9kfM6XBxWkIWmHxUZoWWk
4 changed files with 120 additions and 57 deletions

View file

@ -4,14 +4,13 @@ use anyhow::{Context as _, bail};
use dashmap::DashMap; use dashmap::DashMap;
use serenity::{ use serenity::{
all::{ all::{
Attachment, CacheHttp, ChannelId, Component, Context, CreateComponent, CreateMediaGallery, CacheHttp, ChannelId, Component, Context, CreateComponent, CreateMediaGallery,
CreateMediaGalleryItem, CreateSeparator, CreateTextDisplay, CreateUnfurledMediaItem, CreateMediaGalleryItem, CreateSeparator, CreateTextDisplay, CreateUnfurledMediaItem,
CreateWebhook, EditWebhookMessage, EventHandler, ExecuteWebhook, FullEvent, CreateWebhook, EditWebhookMessage, EventHandler, ExecuteWebhook, FullEvent,
GenericChannelId, GuildId, Interaction, Message, MessageFlags, MessageId, MessageSnapshot, GenericChannelId, GuildId, Interaction, Message, MessageFlags, MessageId, MessageSnapshot,
Reaction, StickerFormatType, StickerItem, Webhook, WebhookId, Reaction, Webhook, WebhookId,
}, },
async_trait, async_trait,
small_fixed_array::{FixedArray, FixedString},
}; };
use sqlx::{PgExecutor, PgPool}; use sqlx::{PgExecutor, PgPool};
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
@ -19,20 +18,17 @@ use tokio::sync::Semaphore;
use crate::{ use crate::{
commands, commands,
db::{GuildRow, MessageRow, WebhookRow}, db::{GuildRow, MessageRow, WebhookRow},
message_builder::{CloneBuilderMessage, get_message_components},
}; };
struct CloneBuilderMessage {
pub content: FixedString<u16>,
pub attachments: FixedArray<Attachment>,
pub sticker_items: FixedArray<StickerItem>,
}
impl From<Message> for CloneBuilderMessage { impl From<Message> for CloneBuilderMessage {
fn from(value: Message) -> Self { fn from(value: Message) -> Self {
Self { Self {
link: Some(value.link()),
content: value.content, content: value.content,
attachments: value.attachments, attachments: value.attachments,
sticker_items: value.sticker_items, sticker_items: value.sticker_items,
replied_message: value.referenced_message.map(|m| Box::new(Self::from(*m))),
} }
} }
} }
@ -40,9 +36,11 @@ impl From<Message> for CloneBuilderMessage {
impl From<MessageSnapshot> for CloneBuilderMessage { impl From<MessageSnapshot> for CloneBuilderMessage {
fn from(value: MessageSnapshot) -> Self { fn from(value: MessageSnapshot) -> Self {
CloneBuilderMessage { CloneBuilderMessage {
link: None,
content: value.content, content: value.content,
attachments: value.attachments, attachments: value.attachments,
sticker_items: value.sticker_items, sticker_items: value.sticker_items,
replied_message: None,
} }
} }
} }
@ -325,54 +323,7 @@ impl Handler {
} }
} }
} else { } else {
if !reference_msg.content.is_empty() { get_message_components(&reference_msg, &mut components, &guild.lang);
components.push(CreateComponent::TextDisplay(CreateTextDisplay::new(
&reference_msg.content,
)));
}
let images = reference_msg
.attachments
.iter()
.filter(|x| x.width.is_some())
.collect::<Vec<_>>();
if !images.is_empty() {
components.push(CreateComponent::MediaGallery(CreateMediaGallery::new(
images
.into_iter()
.map(|x| {
CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(x.url.clone()))
})
.collect::<Vec<_>>(),
)));
}
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],
)));
}
} }
components.push(CreateComponent::Separator(CreateSeparator::new(true))); components.push(CreateComponent::Separator(CreateSeparator::new(true)));

View file

@ -45,6 +45,7 @@ pub trait Locale: Send + Sync {
fn original_message(&self) -> &'static str; fn original_message(&self) -> &'static str;
fn admin_required(&self) -> &'static str; fn admin_required(&self) -> &'static str;
fn replied_message(&self, link: &str) -> String;
fn lang_changed(&self, new_lang: Lang) -> String; fn lang_changed(&self, new_lang: Lang) -> String;
} }
@ -62,6 +63,10 @@ impl Locale for Korean {
fn admin_required(&self) -> &'static str { fn admin_required(&self) -> &'static str {
"관리자 권한이 필요합니다" "관리자 권한이 필요합니다"
} }
fn replied_message(&self, link: &str) -> String {
format!("[답장한 메시지]({link})")
}
} }
struct English; struct English;
@ -78,4 +83,8 @@ impl Locale for English {
fn admin_required(&self) -> &'static str { fn admin_required(&self) -> &'static str {
"Administrator permission is required" "Administrator permission is required"
} }
fn replied_message(&self, link: &str) -> String {
format!("[Replied Message]({link})")
}
} }

View file

@ -21,6 +21,7 @@ mod config;
mod db; mod db;
mod handler; mod handler;
mod lang; mod lang;
mod message_builder;
mod modal; mod modal;
#[macro_use] #[macro_use]

102
src/bot/message_builder.rs Normal file
View file

@ -0,0 +1,102 @@
use serenity::{all::*, small_fixed_array::*};
use crate::lang::Lang;
pub struct CloneBuilderMessage {
pub link: Option<MessageLink>,
pub content: FixedString<u16>,
pub attachments: FixedArray<Attachment>,
pub sticker_items: FixedArray<StickerItem>,
pub replied_message: Option<Box<CloneBuilderMessage>>,
}
pub fn get_message_components(
reference_msg: &CloneBuilderMessage,
components: &mut Vec<CreateComponent<'_>>,
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::<Vec<_>>();
if !images.is_empty() {
components.push(CreateComponent::MediaGallery(CreateMediaGallery::new(
images
.into_iter()
.map(|x| CreateMediaGalleryItem::new(CreateUnfurledMediaItem::new(x.url.clone())))
.collect::<Vec<_>>(),
)));
}
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],
)));
}
}