@femshima/djs-interaction v0.2.1
djs-interaction
discord.jsのコマンドやComponentの定義と Interactionのハンドラを近い位置に書けるようにするフレームワークです。
使い方
詳しい使い方はsample/を見てください。
| Interactionの種類 | 説明 | 
|---|---|
Command | スラッシュコマンド | 
SubCommandGroup | スラッシュコマンド関連、ApplicationCommandSubGroupDataに相当 | 
SubCommand | スラッシュコマンド関連、ApplicationCommandSubCommandDataに相当 | 
MessageContextMenu | メッセージを右クリックすると出てくるコンテキストメニュー | 
UserContextMenu | ユーザーを右クリックすると出てくるコンテキストメニュー | 
Button | ボタン | 
SelectMenu | 選択ボックス | 
Modal | モーダルウィンドウ | 
Setup
djs-interactionのCommand、SubCommandGroup、SubCommand、MessageContextMenu、UserContextMenu、Button、SelectMenu、Modalを継承したクラスを一つでもインスタンス化する前にframe.setup()を呼び出す必要があります。
また、frame.setup()を複数回呼び出すとその都度コマンドの登録が行われるため、問題が発生する可能性があります。
import { frame } from 'djs-interaction';
//...
await frame.setup({
  client,
  commands: {
    ...Command,
    ...ContextMenu,
  },
  components: Component,
  guilds: !env.production,
  subscribeToEvent: true,
  async fallback(interaction) {
    if ('replied' in interaction && !interaction.replied) {
      await interaction.reply('Unknown interaction.');
    }
  },
});frame.setup()の引数は一つで、次のようなオブジェクトです。
| キー | 説明 | 
|---|---|
| client | Discord.jsのclientです。 | 
| commands | 登録するコマンド(Command、MessageContextMenu、UserContextMenuを継承したもの)をすべてここに指定します。コマンドのクラス(インスタンス化してあっても、する前のものでも構いません)の配列または、keyを文字列、それらのクラスをvalueとするオブジェクトを渡してください。 | 
| components | 使用時に都度インスタンス化して使うもの(Button、SelectMenu、Modalを継承したもの)をすべてここに指定します。コマンドのクラス(インスタンス化する前のものである必要があります)の配列または、keyを文字列、それらのクラスをvalueとするオブジェクトを渡してください。 | 
| subscribeToEvent | setupメソッドの中で自動的にinteractionCreateイベントにハンドラを登録するかどうかbooleanで指定します。falseをセットした場合、別の場所でframe.interactionCreateをハンドラとして登録する必要があります。 | 
| fallback | 受信したinteractionのハンドラが見つからなかった場合や、ハンドラが応答しなかった場合に呼び出される関数です。 | 
| database | データベースとの連携を使用する場合、このオプションを使います。詳しくはデータベースと連携させるを参照してください。 | 
| idGen | idを生成するクラスのインスタンスを指定します。ユニークなID(文字列)を生成して返すgenerateIDメソッドを実装している必要があります。 | 
スラッシュコマンド(ChatInputApplicationCommand)
すべてのコマンド定義はCommandを継承している必要があります。
constructorでコマンド定義をsuperに渡して呼び出します。
handlerを実装していなくてもTypeScriptのエラーは出ませんが、interactionに応答しないとDiscord側でエラーメッセージが表示されるため、SubCommandを使用する時以外は実装することが推奨されます。
import { CommandInteraction } from 'discord.js';
import { Command } from 'djs-interaction';
export default class Ping extends Command {
  constructor() {
    super({
      name: 'ping',
      description: 'Ping!',
    });
  }
  async handle(interaction: CommandInteraction<'cached'>) {
    await interaction.reply({ content: 'Pong!' });
  }
}サブコマンド
djs-interactionはサブコマンドにも対応しています。
サブコマンドではSubCommandを継承してください。
通常のコマンドと同じように、constructorでコマンド定義をsuperに渡して呼び出します。
ただし、通常のコマンドと異なり、handlerを定義しないとTypeScriptでエラーになります。
import { ChatInputCommandInteraction } from 'discord.js';
import { SubCommand } from 'djs-interaction';
export default class Locale extends SubCommand {
  constructor() {
    super({
      name: 'locale',
      description: 'shows supported locales',
    });
  }
  async handle(interaction: ChatInputCommandInteraction<'cached'>) {
    await interaction.reply('en,ja');
  }
}サブコマンドを定義したら、それらをオプションとしてまとめたコマンドを定義します。
import { Command } from 'djs-interaction';
import Admin from './admin';
import Langs from './langs';
import Locale from './locales';
export default class Greet extends Command {
  constructor() {
    super({
      name: 'greet',
      description: 'Commands about greetings',
      options: [new Langs(), new Locale(), new Admin()],
    });
  }
}なお、サブコマンドをもつコマンドでも、通常のコマンド同様handlerを定義することができます。handlerはCommand、(存在する場合は)SubCommandGroup、SubCommandの順に呼び出されます。
handler内でdjs-interactionからインポートしたAbortErrorをthrowすると、そのhandler以降のhandlerは実行されません。すなわち、CommandのhandlerでAbortErrorをthrowすると、SubCommandGroup、SubCommandのhandlerは実行されません。
//...
async handle(interaction: ChatInputCommandInteraction<'cached'>) {
  if (
    !interaction.member.permissions.has(PermissionFlagsBits.Administrator)
  ) {
    await interaction.reply(
      'You are not admin, so you cannot use this command.'
    );
    throw new AbortError();
  }
}
//...サブコマンドグループ
サブコマンドをまとめるコマンドと同様に使います。ただし、CommandではなくSubCommandGroupを継承してください。
ContextMenu
ContextMenuには、ユーザーを右クリックしたときに実行されるUserApplicationCommandとメッセージを右クリックしたときに実行されるMessageApplicationCommandがあります。
基本的にはコマンド定義と同様ですが、UserApplicationCommandはUserContextMenuを、MessageApplicationCommandはMessageContextMenuを継承したクラスを作成してください。
Component
ここで、ComponentはButton、SelectMenu、Modalのことを指しています(Modalは微妙かもしれませんが)。
これらもコマンド同様、定義をconstructor内でsuperに渡すだけです。
Buttonはhandlerを定義しなくてもエラーにはなりませんが、これはstyleがLinkのときにhandlerを定義しなくてもいいためです。そうでない場合は定義すべきです。
データベースと連携させる
デフォルトではInteractionの定義はメモリに保存されるため、プログラムを終了させた時点で蒸発します。これを防ぐには、データベースなどに保存しておく必要があります。
djs-interactionでデータベースを使うには、frame.setupの実行時にdatabaseオプションを指定します。
Prismaを使う場合
  まず、スキーマの例を示します。重要なのはmodel Interactionの部分だけですので、他の部分は適宜変更してください。また、列名と型が同じであればテーブル名を変えても構いません。
generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native"]
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model Interaction {
  id           String  @id
  classKey     String
  classVersion String?
  data         Json
}setup部分は次のように書きます。テーブル名はスキーマに合わせてください。
const prisma = new PrismaClient()
await frame.setup({
  //...
  database: prisma.interaction
});Prismaを使わない場合
await frame.setup({
    //...
    database: {
      findUnique(options) {
        // options.where.idがidに一致するレコードを探して返します。
        // レコードの形式は次のようになっています。
        // {
        //   id: string; // depends on what kind of idgen you use.
        //   classKey: string; // the key set in class or the name of the class
        //   classVersion: string | null; // version set in class or null
        //   data: JsonObject;
        // }
        //
        // 例:
        // {
        //   id: 'id-1',
        //   classKey: 'Target',
        //   classVersion: null,
        //   data: {
        //     type: 'MODAL',
        //     message: 'msg',
        //     data: { d: 'X' },
        //   },
        // }
      },
      create(options) {
        // findUniqueで説明したようなレコードがoptions.dataとして渡されるので、データベースに登録します。
        // idが重複することは想定されていませんので、重複した場合は例外を投げるべきです。
      }
    }
  });