From 6cc359929ec48be8ace74c20b82420828bad81ed Mon Sep 17 00:00:00 2001
From: Siwoo Jeon <me@migan.co.kr>
Date: Sat, 1 Mar 2025 00:01:21 +0900
Subject: [PATCH] feat: Add learn command

---
 bun.lock              |   3 +
 package.json          |   1 +
 src/commands/learn.ts | 127 ++++++++++++++++++++++++++++++++++++++++++
 src/init.ts           |   7 +++
 4 files changed, 138 insertions(+)
 create mode 100644 src/commands/learn.ts

diff --git a/bun.lock b/bun.lock
index 911789b..a1f6b8b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -11,6 +11,7 @@
         "@sapphire/utilities": "^3.18.2",
         "discord-api-types": "^0.37.119",
         "discord.js": "^14.18.0",
+        "es-hangul": "^2.3.1",
         "mongoose": "^8.10.1",
       },
       "devDependencies": {
@@ -129,6 +130,8 @@
 
     "discord.js": ["discord.js@14.18.0", "", { "dependencies": { "@discordjs/builders": "^1.10.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.0", "@discordjs/rest": "^2.4.3", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.1", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.37.119", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw=="],
 
+    "es-hangul": ["es-hangul@2.3.1", "", {}, "sha512-somwJpQpVP5LLI6DquIvRnoTSqVyfIfT1a8/jxHRueWNiTse7/kJ0JZVpf4KrzhY8CoqEK0LUZXFhGkf1hBZKQ=="],
+
     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 
     "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
diff --git a/package.json b/package.json
index b096b42..fcee70d 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "@sapphire/utilities": "^3.18.2",
     "discord-api-types": "^0.37.119",
     "discord.js": "^14.18.0",
+    "es-hangul": "^2.3.1",
     "mongoose": "^8.10.1"
   },
   "scripts": {
diff --git a/src/commands/learn.ts b/src/commands/learn.ts
new file mode 100644
index 0000000..3131f3d
--- /dev/null
+++ b/src/commands/learn.ts
@@ -0,0 +1,127 @@
+import { Learn } from '../lib/databases'
+import { ApplyOptions } from '@sapphire/decorators'
+import { Args, Command } from '@sapphire/framework'
+import { ChatInputCommandInteraction, codeBlock, Message } from 'discord.js'
+import { josa } from 'es-hangul'
+
+@ApplyOptions<Command.Options>({
+  name: '배워',
+  aliases: ['공부'],
+  description: '단어를 가르치는 명령ㅇ어에요.',
+  detailedDescription: {
+    usage: '머핀아 배워 (등록할 단어) (대답)',
+    examples: [
+      '머핀아 배워 안녕 안녕!',
+      '머핀아 배워 "야 죽을래?" "아니요 ㅠㅠㅠ"',
+      '머핀아 배워 미간은_누구야? 이봇의_개발자요',
+    ],
+  },
+})
+export class LearnCommand extends Command {
+  public registerApplicationCommands(registry: Command.Registry) {
+    registry.registerChatInputCommand(builder =>
+      builder
+        .setName(this.name)
+        .setDescription(this.description)
+        .addStringOption(option =>
+          option
+            .setRequired(true)
+            .setName('단어')
+            .setDescription('등록할 단어를 입력해주세요.'),
+        )
+        .addStringOption(option =>
+          option
+            .setRequired(true)
+            .setName('대답')
+            .setDescription('해당 단어의 대답을 입력해주세요.'),
+        ),
+    )
+  }
+
+  public async messageRun(msg: Message<true>, args: Args) {
+    return await this._run(msg, args)
+  }
+
+  public async chatInputRun(
+    interaction: ChatInputCommandInteraction<'cached'>,
+  ) {
+    return await this._run(interaction)
+  }
+
+  private async _run(
+    ctx: Message<true> | ChatInputCommandInteraction<'cached'>,
+    args?: Args,
+  ) {
+    const userId = ctx instanceof Message ? ctx.author.id : ctx.user.id
+    let command: string | undefined
+    let result: string | undefined
+
+    if (typeof this.detailedDescription === 'string') return
+    if (ctx instanceof ChatInputCommandInteraction) {
+      await ctx.deferReply()
+
+      command = ctx.options.getString('단어', true)
+      result = ctx.options.getString('대답', true)
+    } else {
+      if (!args) return
+      command = (await args.pick('string').catch(() => undefined))?.replaceAll(
+        '_',
+        ' ',
+      )
+      result = (await args.pick('string').catch(() => undefined))?.replaceAll(
+        '_',
+        ' ',
+      )
+    }
+
+    if (!command || !result)
+      return await ctx.reply(
+        codeBlock(
+          'md',
+          `사용법: ${this.detailedDescription.usage}\n` +
+            `예시: ${this.detailedDescription.examples?.map(example => example).join('\n')}`,
+        ),
+      )
+
+    let commands: string[] = []
+    let aliases: string[] = []
+
+    for (const [name, command] of this.container.stores.get('commands')) {
+      commands = [...commands, name]
+      aliases = [...aliases, ...command.aliases]
+    }
+
+    const ignores = [...commands, ...aliases, '미간', 'Migan', 'migan', '간미']
+    const disallows = [
+      '@everyone',
+      '@here',
+      `<@${this.container.config.bot.ownerId}>`,
+    ]
+
+    for (const ignore of ignores) {
+      if (command.includes(ignore))
+        return ctx instanceof Message
+          ? await ctx.reply('해ㄷ당 단어는 배울ㄹ 수 없어요.')
+          : await ctx.editReply('해ㄷ당 단어는 배울ㄹ 수 없어요.')
+    }
+
+    for (const disallowed of disallows) {
+      if (result.includes(disallowed))
+        return ctx instanceof Message
+          ? await ctx.reply('해당 단ㅇ어는 개발자님이 특별히 금지하였ㅇ어요.')
+          : await ctx.editReply(
+              '해당 단ㅇ어는 개발자님이 특별히 금지하였ㅇ어요.',
+            )
+    }
+
+    await new Learn({
+      command,
+      result,
+      user_id: userId,
+    }).save()
+
+    return ctx instanceof Message
+      ? await ctx.reply(`${josa(command, '을/를')} 배웠ㅇ어요.`)
+      : await ctx.editReply(`${josa(command, '을/를')} 배웠ㅇ어요.`)
+  }
+}
diff --git a/src/init.ts b/src/init.ts
index 020996e..3471114 100644
--- a/src/init.ts
+++ b/src/init.ts
@@ -11,6 +11,13 @@ declare module '@sapphire/pieces' {
   }
 }
 
+declare module '@sapphire/framework' {
+  interface DetailedDescriptionCommandObject {
+    usage: string
+    examples?: string[]
+  }
+}
+
 container.dbDisconnect = async () => await disconnect()
 container.config = new Config()
 container.prefix = container.config.bot.prefix