diff --git a/migrations/00006_guild_lang.sql b/migrations/00006_guild_lang.sql new file mode 100644 index 0000000..db52cec --- /dev/null +++ b/migrations/00006_guild_lang.sql @@ -0,0 +1 @@ +alter table guilds add column lang text not null default 'ko'; \ No newline at end of file diff --git a/src/bot/commands.rs b/src/bot/commands.rs index 39dc124..5e3ac4f 100644 --- a/src/bot/commands.rs +++ b/src/bot/commands.rs @@ -1,24 +1,46 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; -use anyhow::anyhow; +use anyhow::{Context as _, anyhow, bail}; use serenity::all::{ CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand, CreateCommandOption, CreateComponent, CreateContainer, CreateContainerComponent, CreateInputText, CreateInteractionResponseMessage, CreateLabel, CreateModal, CreateModalComponent, CreateTextDisplay, InputTextStyle, MessageFlags, Permissions, - ResolvedTarget, + ResolvedTarget, ResolvedValue, }; -use crate::handler::{Handler, get_guild}; +use crate::{ + handler::{Handler, get_guild}, + lang::{Lang, ParseLocale}, +}; pub fn config_command() -> CreateCommand<'static> { CreateCommand::new("starboard") - .description("스타보드 설정") - .add_option(CreateCommandOption::new( - CommandOptionType::SubCommand, - "script", - "스크립트 수정", - )) + .description("Configure starboard") + .description_localized("ko", "스타보드 설정") + .add_option( + CreateCommandOption::new( + CommandOptionType::SubCommand, + "script", + "Edit starboard script", + ) + .description_localized("ko", "스크립트 설정"), + ) + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "lang", "Change language") + .description_localized("ko", "이 서버에 대한 언어 변경") + .add_sub_option( + CreateCommandOption::new( + CommandOptionType::String, + "lang", + "Set language for this server", + ) + .description_localized("ko", "사용할 언어 선택") + .add_string_choice("한국어", "ko") + .add_string_choice("English", "en") + .required(true), + ), + ) .default_member_permissions(Permissions::ADMINISTRATOR) } @@ -51,7 +73,7 @@ impl Handler { &ctx.http, serenity::all::CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() - .content("관리자 권한이 필요합니다") + .content(interaction.locale().admin_required()) .ephemeral(true), ), ) @@ -59,7 +81,9 @@ impl Handler { return Ok(()); } - for option in interaction.data.options() { + debug!("got options: {:?}", &interaction.data.options()); + + if let Some(option) = interaction.data.options().first() { if option.name == "script" { let script = get_guild(&self.db, guild_id) .await? @@ -84,8 +108,9 @@ impl Handler { 예시 스크립트: ```rs -if reactions["⭐"] >= 3 || removed { - return result("0000000000000000000", reactions["⭐"], "⭐") +let emoji = "⭐"; +if reactions[emoji] >= 3 || removed { + return result("0000000000000000000", reactions[emoji], emoji) } ```"#, )), @@ -99,6 +124,41 @@ if reactions["⭐"] >= 3 || removed { ), ) .await?; + return Ok(()); + } else if option.name == "lang" { + let lang = Some(&option.value) + .and_then(|x| match x { + ResolvedValue::SubCommand(value) => Some(value), + _ => None, + }) + .and_then(|x| x.first()) + .filter(|x| x.name == "lang") + .and_then(|x| match x.value { + ResolvedValue::String(x) => Some(x), + _ => None, + }) + .context("lang option not found")?; + + let Ok(lang) = Lang::from_str(lang) else { + bail!("unknown lang value"); + }; + + sqlx::query( + "insert into guilds (id, lang) values ($1, $2) on conflict (id) do update set lang = excluded.lang", + ).bind(guild_id.to_string()) + .bind(lang) + .execute(&self.db).await?; + + interaction + .create_response( + &ctx.http, + serenity::all::CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content(interaction.locale().lang_changed(lang)) + .ephemeral(true), + ), + ) + .await?; } } diff --git a/src/bot/db.rs b/src/bot/db.rs index 45b20c4..72ef053 100644 --- a/src/bot/db.rs +++ b/src/bot/db.rs @@ -1,9 +1,12 @@ use sqlx::prelude::FromRow; +use crate::lang::Lang; + #[derive(Debug, FromRow)] pub struct GuildRow { // pub id: String, pub script: Option, + pub lang: Lang, } #[derive(Debug, FromRow)] diff --git a/src/bot/handler.rs b/src/bot/handler.rs index d92c6af..ac9acf7 100644 --- a/src/bot/handler.rs +++ b/src/bot/handler.rs @@ -235,7 +235,7 @@ impl Handler { ctx.cache .guild(guild_id) .as_ref() - .and_then(|x| x.channel(channel_id).clone()) + .and_then(|x| x.channel(channel_id)) ); let channel = channel_id @@ -379,10 +379,11 @@ impl Handler { components.push(CreateComponent::TextDisplay(CreateTextDisplay::new( format!( - "-# {} {} · [원본 메시지]({})", + "-# {} {} · [{original_message}]({})", result.icon, result.count, - msg.link().to_string() + msg.link(), + original_message = guild.lang.to_locale().original_message() ), ))); @@ -434,26 +435,22 @@ pub async fn get_guild( executor: impl PgExecutor<'_>, id: GuildId, ) -> anyhow::Result> { - Ok( - sqlx::query_as::<_, GuildRow>("select * from guilds where id = $1") - .bind(id.to_string()) - .fetch_optional(executor) - .await - .context("failed to get guild data from db")?, - ) + sqlx::query_as::<_, GuildRow>("select * from guilds where id = $1") + .bind(id.to_string()) + .fetch_optional(executor) + .await + .context("failed to get guild data from db") } async fn get_message( executor: impl PgExecutor<'_>, message_id: MessageId, ) -> anyhow::Result> { - Ok( - sqlx::query_as::<_, MessageRow>("select * from messages where message_id = $1") - .bind(message_id.to_string()) - .fetch_optional(executor) - .await - .context("failed to get message data from db")?, - ) + sqlx::query_as::<_, MessageRow>("select * from messages where message_id = $1") + .bind(message_id.to_string()) + .fetch_optional(executor) + .await + .context("failed to get message data from db") } async fn get_or_create_webhook( diff --git a/src/bot/lang.rs b/src/bot/lang.rs new file mode 100644 index 0000000..22be2c9 --- /dev/null +++ b/src/bot/lang.rs @@ -0,0 +1,81 @@ +use std::str::FromStr; + +use serenity::all::CommandInteraction; + +#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Default)] +#[sqlx(rename_all = "snake_case", type_name = "TEXT")] +pub enum Lang { + #[default] + Ko, + En, +} + +impl Lang { + pub fn to_locale(self) -> Box { + match self { + Lang::Ko => Box::new(Korean), + Lang::En => Box::new(English), + } + } +} + +pub trait ParseLocale { + fn locale(&self) -> Box; +} + +impl ParseLocale for CommandInteraction { + fn locale(&self) -> Box { + Lang::from_str(&self.locale).unwrap_or_default().to_locale() + } +} + +impl FromStr for Lang { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "ko" => Ok(Lang::Ko), + "en" => Ok(Lang::En), + _ => Err(()), + } + } +} + +pub trait Locale: Send + Sync { + fn original_message(&self) -> &'static str; + fn admin_required(&self) -> &'static str; + + fn lang_changed(&self, new_lang: Lang) -> String; +} + +struct Korean; + +impl Locale for Korean { + fn original_message(&self) -> &'static str { + "원본 메시지" + } + + fn lang_changed(&self, new_lang: Lang) -> String { + format!("서버 언어가 {new_lang:?}(으)로 변경되었습니다") + } + + fn admin_required(&self) -> &'static str { + "관리자 권한이 필요합니다" + } +} + +struct English; + +impl Locale for English { + fn original_message(&self) -> &'static str { + "Original Message" + } + + fn lang_changed(&self, new_lang: Lang) -> String { + format!("Server language changed to {new_lang:?}") + } + + fn admin_required(&self) -> &'static str { + "Administrator permission is required" + } +} diff --git a/src/bot/main.rs b/src/bot/main.rs index cfb2d86..1c8da3f 100644 --- a/src/bot/main.rs +++ b/src/bot/main.rs @@ -20,6 +20,7 @@ mod commands; mod config; mod db; mod handler; +mod lang; mod modal; #[macro_use]