disbord
v2.3.9
Published
bo-yakitarako専用のオレオレDiscord botフレームワーク
Readme
disbord
bo-yakitarako専用のオレオレDiscord botフレームワーク。プラグイン機構は持たず、気に入った機能は都度フレームワーク本体に直接組み込む方針(拡張性より一体性を優先)。
新規bot作成は create-disbord-app から始めてください。このREADMEはdisbordパッケージ自体(CLI・ランタイム)の使い方をまとめたものです。
インストール
create-disbord-appが生成するbotのpackage.jsonにはdisbordが依存として自動的に追加されます。手動で追加する場合は次の通りです。
bun add disbordTypeScript v7系をpeerDependenciesとして要求します。
クイックスタート
bunx create-disbord-app my-bot
cd my-bot
bun run devbun run dev(disbord dev)は起動時にslashCommandのREST登録・(DB有効時は)migrationを自動で行い、そのままbotを起動します。
disbord.config.ts
プロジェクトルートに1つだけ置く設定ファイルです。
import type { Config } from 'disbord';
export default {
intents: ['Guilds', 'GuildMessages'],
botErrorMessage: 'エラっちゃったサンプル',
} satisfies Config;token/clientId/guildId/db.tursoDatabaseUrl/db.tursoAuthToken: 値そのもの(process.env.ALT_TOKENのような任意の環境変数を参照する式でもよい)を書く。未指定時はキー名をUPPER_SNAKE_CASEにしたデフォルトの環境変数名(clientId→CLIENT_ID)へ自動フォールバックするguildId: 指定するとdisbord commands pushがguild単位登録(反映が即時)になる。未指定ならglobal登録(反映まで最大1時間)coreClass/db: それぞれdisbord enable core-class/disbord enable dbで有効化する(後述)botErrorMessage:BotError(message未指定のもの)がthrowされた際に返信する文言。固定文字列または(error: Error) => stringargsSplitter: customIdへ引数を埋め込む際の区切り文字のグローバル既定値(未指定時-)
CLIコマンド
| コマンド | 説明 |
| --- | --- |
| disbord dev | 開発サーバーを起動する(bun --watchのラッパー。起動時にcommands push・(DB有効時)migrateを自動実行) |
| disbord build | 本番デプロイ用にdist/main.js・dist/.envを生成する |
| disbord commands push [--production] | slashCommandをDiscordへREST登録する |
| disbord commands delete [--production] | 登録済みslashCommandを削除する |
| disbord env [--production\|--all] | env/配下の環境変数を暗号化⇔復号にtoggleする |
| disbord env encrypt [--production\|--all] | env/配下を暗号化する(固定) |
| disbord env decrypt [--production\|--all] | env/配下を復号する(固定) |
| disbord generate event <name> | src/events/<name>.tsのひな形を追加生成する |
| disbord generate once <name> | src/once/<name>.tsのひな形を追加生成する |
| disbord generate component <button\|selectMenu> | src/components/buttons.ts・selectMenus.tsを追加生成する(未生成なら) |
| disbord generate workflow ssh | .github/workflows/deploy.yamlを(再)生成する(SSH+systemd userサービスへのデプロイ用) |
| disbord once <name> [--production] | src/once/<name>.tsをbotとして1回だけ起動して実行する |
| disbord generate model <Name> | src/db/models/<Name>.tsにdecorator付きモデルクラスを追加生成する(DB有効時のみ) |
| disbord migrate [--production] | モデル定義からschema.ts・migrationファイルを生成し、DBに適用する(DB有効時のみ) |
| disbord model type | 各モデルファイル末尾のnamespace Data型ブロックだけを再生成する(schema.ts再生成・migration・DB接続はしない。DB有効時のみ) |
| disbord studio | .disbord/db/dev.dbを対象にdrizzle studioサーバーを起動する(DB有効時のみ) |
| disbord enable db / disbord enable core-class [ClassName] | 後からdb/coreClassを個別に有効化する |
| disbord disable db / disbord disable core-class | 有効化したdb/coreClassを個別に無効化する(確認プロンプトあり) |
| disbord --version, -v | バージョンを表示する |
| disbord --help, -h / disbord help | コマンド一覧を表示する(DB有効時のみDB系コマンドも表示) |
本番実行はdisbord buildが生成したdist/main.jsをbunで直接叩くだけで、disbord startのようなコマンドは存在しません。
components(src/components/)
buttons.ts / selectMenus.ts / slashCommands.tsにそれぞれexport default { ... } satisfies XxxRegistrationの形でルーティングを宣言します。discord.jsのBuilderは直書きせず、素朴なオブジェクト(例: { label, style?, disabled?, args? })または(...args) => componentの関数で書きます。
slashCommands.tsだけは必須で、buttons.ts/selectMenus.tsは任意です。使う時だけdisbord generate component button / disbord generate component selectMenuで追加生成してください。
// src/components/buttons.ts
import type { ButtonRegistration } from 'disbord';
export default {
ping: {
component: { label: 'Ping' },
execute: async (interaction) => {
await interaction.reply('pong');
},
},
} satisfies ButtonRegistration;executeの第1引数はdisbordが軽くラップしたInteraction(replyと同じシグネチャのephemeralメンバーを追加で持つ)executeの第2引数はCoreクラスのインスタンス(coreClass.enable時)、それ以外は...args: string[](customIdに埋め込んだ引数の復元値)argsSplitter?: stringをentryごとに指定でき、customIdの区切り文字を上書きできるslashCommands.tsは{ description?, options?, execute }のオブジェクト形に加え、execute関数を直接指定する形(例:ping: async (interaction) => {...})も書ける
makeButtonRow / makeSelectMenuRowはdisbordから直接importして使い、ジェネリクスは書きません。
events(src/events/)
1ファイル1イベント、ファイル名はdiscord.jsのイベント名(例: messageCreate.ts)。disbord generate event <name>で追加生成します。interactionCreate用のイベントファイルは存在しません(componentsのルーティングに統合済み)。
// src/events/messageCreate.ts
import type { Message } from 'disbord';
export default async function (message: Message) {
if (message.author.bot) return;
};once(src/once/)
常駐せず1回だけ処理を実行して終了するbot用の入り口です。disbord generate once <name>でsrc/once/<name>.tsを生成し、disbord once <name> [--production]で実行します(disbord buildはdist/<name>.jsとしても出力します。mainはbot本体のエントリdist/main.jsと衝突するため予約済みで使えません)。DB有効時はdb/Model、coreClass有効時はcoreStoreもそのままimportして使えます。
// src/once/notice.ts
import type { Client } from 'discord.js';
export default async function (client: Client<true>) {
//
}Core機構
複数のcomponentsで使い回す制御クラス(Core)を、guild/category/channel/user/globalいずれかの単位でインスタンス管理する仕組みです。disbord enable core-classで有効化するとsrc/{ClassName}.tsが生成されます。
import { coreStore } from 'disbord';
// 任意のタイミング(例: 開始用slashCommandのexecute内)でインスタンスを登録する
const core = coreStore.create(interaction);instanceLevelに対応するキーがinteractionから解決できない場合(例: guild単位のCoreをDM上で使おうとした)はBotError(instanceInvalidMessage)がthrowされます。
DB層(src/db/models/)
Drizzle + libSQL。モデルクラスを1つ書くだけでdisbord migrateがschema.ts・migrationファイルを自動生成します。disbord enable dbで有効化します。
// src/db/models/Job.ts
import type { Dayjs } from 'dayjs';
import { Model, Table, Column, Relate } from 'disbord';
import { User } from './User';
@Table('jobs')
export class Job extends Model<Job.Data> {
@Relate(() => User, { onDelete: 'cascade' })
accessor userId!: string;
@Column('text')
accessor displayName!: string;
@Column('integer', { mode: 'timestamp_ms' })
accessor actAt!: Dayjs;
}
// AUTO-GENERATED by disbord. Do not edit below this line — regenerated by `disbord generate model` / `disbord migrate` / `disbord model type`.
export namespace Job {
export type Data = { userId: string; displayName: string; actAt: Date | Dayjs };
}id / createdAt / updatedAtは全モデル共通で自動付与されます。Model<Job.Data>が要求するnamespace Job { export type Data = ... }ブロックは手書き不要で、disbord generate model / disbord migrate / disbord model type実行のたびに@Column/@Relateのメタデータから機械的に導出してファイル末尾へ自動生成されます(timestamp_msのaccessorは読み取り時Dayjs固定ですが、Job.Data側は読み取ったDayjs値をそのまま渡し回せるようDate | Dayjsにします。Job.create()/find()/update()等の入力型はこのJob.Dataを経由するため、キー名のtypoやカラム追加漏れがコンパイルエラーで検知できます。defaultが指定されたカラムは?付きのoptionalになり、create()時に省略できます)。接続先はTurso用の環境変数の有無で自動判定され(未設定ならローカルsqlite.disbord/db/dev.db)、中身はdisbord studioで確認できます。
@Columnのdefaultオプションは値の種類でDB側/JS側どちらのdefaultになるか変わります。固定値(例: default: 'pending')はDBスキーマのDEFAULT句として、関数(例: default: () => crypto.randomUUID())はdrizzle-orm(JS側)がINSERT時に評価する値として扱われます(生SQLでのINSERTには効きません)。type/modeごとにdefaultの型も絞られており(例: mode: 'boolean'ならbooleanのみ)、mode: 'timestamp_ms'のカラムはDateではなくDayjs(固定値・() => Dayjs関数どちらも)で指定します(DBへ渡す際は内部でDateへ変換されます)。さらにmode: 'timestamp_ms'のカラムに限り特別な値default: 'now'も使え、DB側のDEFAULT (unixepoch('subsec') * 1000)(挿入時刻のミリ秒unix時間)になります。defaultが指定されたカラムは自動生成されるJob.Data側でも?付きのoptionalになるため、Job.create()呼び出し時にその値の指定を省略できます(省略した場合はDB/drizzle側のdefaultがそのまま使われます)。
デプロイ(.github/workflows/)
disbord generate workflow sshで.github/workflows/deploy.yamlを生成します。pushをトリガーにビルド後、SSH経由でリモートホストへ配置し、systemd(--user)のサービスとして起動・再起動します。onceスクリプトがある場合はそれぞれtimerユニットも合わせてデプロイし、disbord.config.tsのtimerで指定したスケジュールで定期実行します(timerが未設定のonceスクリプトは手動実行専用とみなし、disbord.config.tsへの自動補完もdeploy.yamlへのデプロイ対象への追加も行いません)。lefthook.ymlがあればpre-commitにも再生成コマンドが追加され、config変更などがdeploy.yamlへ自動で反映されます。
エラーハンドリング
BotErrorがthrowされると大元のtry/catchでcatchされ、messageがあればそれを、無ければdisbord.config.tsのbotErrorMessageを返信します。BotError以外のdiscord.js由来のエラーは素通しされます。
Lint / Format / テスト
bun run lint # oxlint --fix + oxfmt --write
bun run test # bun test
bunx tsc --noEmitlint設定はdisbord/lintをextendsするoxlint.config.ts(TS製)。create-disbord-appが生成するlefthook.ymlにより、pre-commitでlint→fmt→env暗号化(--all)が自動実行されます。
