feat: script test command
All checks were successful
Build / Build (push) Successful in 46s
Build / docker (push) Successful in 16s

This commit is contained in:
파링 2026-01-06 13:22:19 +09:00
parent e739c443ac
commit f3eb1dc188
Signed by: paring
SSH key fingerprint: SHA256:8uCHhCpn/gVOLEaTolmbub9kfM6XBxWkIWmHxUZoWWk
3 changed files with 158 additions and 8 deletions

View file

@ -1,7 +1,12 @@
use std::{borrow::Cow, collections::HashMap};
use anyhow::anyhow;
use serenity::all::{ use serenity::all::{
CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand,
CreateInputText, CreateLabel, CreateModal, CreateModalComponent, CreateTextDisplay, CreateCommandOption, CreateComponent, CreateContainer, CreateContainerComponent,
InputTextStyle, Permissions, CreateInputText, CreateInteractionResponseMessage, CreateLabel, CreateModal,
CreateModalComponent, CreateTextDisplay, InputTextStyle, MessageFlags, Permissions,
ResolvedTarget,
}; };
use crate::handler::{Handler, get_guild}; use crate::handler::{Handler, get_guild};
@ -17,6 +22,13 @@ pub fn config_command() -> CreateCommand<'static> {
.default_member_permissions(Permissions::ADMINISTRATOR) .default_member_permissions(Permissions::ADMINISTRATOR)
} }
pub fn script_test_command() -> CreateCommand<'static> {
CreateCommand::new("test_script")
.name_localized("ko", "스크립트 테스트")
.kind(CommandType::Message)
.default_member_permissions(Permissions::ADMINISTRATOR)
}
impl Handler { impl Handler {
pub async fn process_starboard_command( pub async fn process_starboard_command(
&self, &self,
@ -27,6 +39,26 @@ impl Handler {
return Ok(()); return Ok(());
}; };
if !interaction
.member
.as_ref()
.and_then(|x| x.permissions)
.map(|x| x.contains(Permissions::ADMINISTRATOR))
.unwrap_or(false)
{
interaction
.create_response(
&ctx.http,
serenity::all::CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("관리자 권한이 필요합니다")
.ephemeral(true),
),
)
.await?;
return Ok(());
}
// if let CommandType interaction.data.kind {} // if let CommandType interaction.data.kind {}
for option in interaction.data.options() { for option in interaction.data.options() {
@ -72,4 +104,102 @@ if reactions["⭐"] >= 3 {
Ok(()) Ok(())
} }
pub async fn process_script_test_command(
&self,
ctx: &Context,
interaction: &CommandInteraction,
) -> anyhow::Result<()> {
let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
if !interaction
.member
.as_ref()
.and_then(|x| x.permissions)
.map(|x| x.contains(Permissions::ADMINISTRATOR))
.unwrap_or(false)
{
interaction
.create_response(
&ctx.http,
serenity::all::CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("관리자 권한이 필요합니다")
.ephemeral(true),
),
)
.await?;
return Ok(());
}
let target = interaction.data.target();
let target_message = target
.and_then(|x| match x {
ResolvedTarget::Message(msg) => Some(msg),
_ => None,
})
.ok_or_else(|| anyhow!("no target message"))?;
let script = get_guild(&self.db, guild_id)
.await?
.and_then(|x| x.script)
.unwrap_or("".to_string());
let channel = interaction
.channel
.as_ref()
.ok_or_else(|| anyhow!("no channel data"))?;
let channel = ctx.http.get_channel(channel.id()).await?;
debug!("selected message: {target_message:?}");
let reactions: HashMap<String, i64> = target_message
.reactions
.iter()
.map(|x| (x.reaction_type.to_string(), x.count as i64))
.collect();
let (result, buffer) = paringboard::script::check(
&script,
reactions,
interaction.channel_id.to_string(),
channel
.guild()
.and_then(|x| x.parent_id)
.map(|x| x.to_string()),
)
.unwrap_or_else(|err| (None, format!("run failed: {err:?}")));
interaction
.create_response(
&ctx.http,
serenity::all::CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.flags(MessageFlags::IS_COMPONENTS_V2)
.components(vec![
CreateComponent::Container(
// result text
CreateContainer::new(vec![CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(format!(
"## Result \n```rs\n{result:?}\n```"
)),
)]),
),
CreateComponent::Container(CreateContainer::new(vec![
CreateContainerComponent::TextDisplay(CreateTextDisplay::new(
format!("## Console Output \n```rs\n{}\n```", &buffer),
)),
])),
])
.ephemeral(true),
),
)
.await?;
Ok(())
}
} }

View file

@ -61,7 +61,10 @@ impl EventHandler for Handler {
if let Err(e) = context if let Err(e) = context
.http .http
.create_global_commands(&vec![commands::config_command()]) .create_global_commands(&vec![
commands::config_command(),
commands::script_test_command(),
])
.await .await
{ {
error!("Failed to register config command: {e:?}"); error!("Failed to register config command: {e:?}");
@ -108,6 +111,11 @@ impl Handler {
if command.data.name == "starboard" { if command.data.name == "starboard" {
self.process_starboard_command(ctx, command).await?; self.process_starboard_command(ctx, command).await?;
return Ok(());
}
if command.data.name == "test_script" {
self.process_script_test_command(ctx, command).await?;
return Ok(()); return Ok(());
} }
} }
@ -164,6 +172,7 @@ impl Handler {
.and_then(|x| x.parent_id) .and_then(|x| x.parent_id)
.map(|x| x.to_string()), .map(|x| x.to_string()),
) )
.map(|x| x.0)
.inspect_err(|res| { .inspect_err(|res| {
error!("check failed: {res:?}"); error!("check failed: {res:?}");
})? })?

View file

@ -1,4 +1,7 @@
use std::collections::HashMap; use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use anyhow::anyhow; use anyhow::anyhow;
use rhai::{Dynamic, Engine, Map, Scope}; use rhai::{Dynamic, Engine, Map, Scope};
@ -17,9 +20,10 @@ pub fn check(
reactions: HashMap<String, i64>, reactions: HashMap<String, i64>,
channel: String, channel: String,
category: Option<String>, category: Option<String>,
) -> anyhow::Result<Option<ReactionResult>> { ) -> anyhow::Result<(Option<ReactionResult>, String)> {
debug!("script: {script}"); debug!("script: {script}");
let mut engine = Engine::new(); let mut engine = Engine::new();
let buffer = Arc::new(RwLock::new(String::new()));
engine.set_max_operations(1000); engine.set_max_operations(1000);
engine.register_type::<ReactionResult>(); engine.register_type::<ReactionResult>();
engine.register_fn("result", |webhook_url: String, count: i64, icon: String| { engine.register_fn("result", |webhook_url: String, count: i64, icon: String| {
@ -30,6 +34,8 @@ pub fn check(
} }
}); });
let logger = buffer.clone();
engine engine
.disable_symbol("for") .disable_symbol("for")
.disable_symbol("while") .disable_symbol("while")
@ -37,7 +43,10 @@ pub fn check(
.set_max_expr_depths(50, 5) .set_max_expr_depths(50, 5)
.set_max_string_size(60) .set_max_string_size(60)
.set_max_map_size(512) .set_max_map_size(512)
.set_max_array_size(512); .set_max_array_size(512)
.on_print(move |line| {
logger.write().unwrap().push_str(line);
});
let mut emotes_input = Map::new(); let mut emotes_input = Map::new();
for (emoji, count) in reactions { for (emoji, count) in reactions {
@ -57,6 +66,8 @@ pub fn check(
}, },
); );
debug!("current scope: {scope:?}");
let result: Dynamic = engine let result: Dynamic = engine
.eval_with_scope(&mut scope, script) .eval_with_scope(&mut scope, script)
.map_err(|e| anyhow!("{e:?}"))?; .map_err(|e| anyhow!("{e:?}"))?;
@ -73,5 +84,5 @@ pub fn check(
) )
}; };
return Ok(result); return Ok((result, buffer.read().unwrap().to_string()));
} }