カスタムコマンド(Custom Commands)とカスタム返信(Custom Reply)は、サーバー管理者がダッシュボードから自由に定義できる動的な応答機能です。
| 機能 | トリガー | 応答手段 |
|---|---|---|
| カスタムコマンド | スラッシュコマンド(/c_<name>) | 静的テキスト/埋め込み / コードノード |
| カスタム返信 | メッセージ内容のパターンマッチ | 静的テキスト/埋め込み / コードノード |
両機能ともコードノード(QuickJS サンドボックス)での JavaScript 実行に対応し、
ボタン・セレクトメニューなどのインタラクティブコンポーネントを返せます。
コードノードは QuickJS(WASM)上でユーザー定義の JavaScript を実行する隔離環境です。
| 項目 | 値 |
|---|---|
| タイムアウト | 2 秒 |
| メモリ制限 | 16 MB |
| ネットワーク | 不可(fetch / http 等の API 未注入) |
| ファイルシステム | 不可(require / process 等の API 未注入) |
| 実行エンジン | QuickJS(WASM)— Node.js プロセスとは完全分離 |
コードは同期的に実行されます。fetch、require、process、setTimeoutなどは利用できず、Promiseやawaitを前提とした非同期処理も行えません。
ctx オブジェクト(SandboxContext)コードに注入されるグローバル変数 ctx の構造:
ctx = {
guild: {
id: string; // サーバーID
name: string; // サーバー名
memberCount: number; // メンバー数(Bot 含む)
ownerId: string; // サーバー所有者のID
ownerName: string; // サーバー所有者のユーザー名
humans: number; // 人間メンバー数
bots: number; // Bot メンバー数
roles: Array<{ // ロール一覧(最大100、ポジション降順)
id: string;
name: string;
color: string; // 16進カラーコード (#XXXXXX)
memberCount: number;
position: number;
}>;
channels: Array<{ // チャンネル一覧(最大100)
id: string;
name: string;
type: string; // Discord チャンネルタイプ
}>;
createdAt: string; // ISO 8601 形式
};
user: {
id: string;
username: string; // Discord ユーザー名
displayName: string; // サーバーニックネーム(なければユーザー名)
mention: string; // メンション文字列 (<@userID>)
isAdmin: boolean; // Administrator 権限を持つか
isOwner: boolean; // サーバー所有者か
roles: Array<{ id: string; name: string }>;
joinedAt: string | null; // 参加日時(ISO 8601)
};
channel: {
id: string;
name: string;
mention: string; // メンション文字列 (<#channelID>)
};
args: Record<string, string | number | boolean | null>;
// カスタムコマンドの初回実行時のみ。user/channel/role 型引数のDiscord ID
resolved?: Record<string, string | null>;
// インタラクション(ボタン・セレクト)応答時のみ設定
interaction?: {
type: "button" | "select";
customId: string; // コードで指定した元の customId
values?: string[]; // セレクトメニューの選択値
user: { id: string; username: string; displayName: string };
messageId: string;
channelId: string;
};
};
コードノードは関数の戻り値として以下の型を返せます。
// 単純な文字列
return "こんにちは!";
// => { type: "text", content: "こんにちは!" }
// 明示的なオブジェクト
return {
type: "text",
content: "こんにちは!",
ephemeral: true, // カスタムコマンドの初回実行では自分だけに表示
};
ephemeral: true はカスタムコマンドの初回実行でだけ利用できます。カスタム返信は通常メッセージへの返信のため、初回応答をエフェメラルにはできません。ボタン・セレクトメニュー操作では、両機能とも responseMode: "ephemeral" を使ってクリックした人だけに返信できます。
return {
type: "embed",
ephemeral: false,
embed: {
title: "タイトル",
description: "説明文",
color: "#ff0000",
fields: [
{ name: "フィールド名", value: "フィールド値", inline: true }
],
authorName: "作成者名",
authorIconUrl: "https://example.com/icon.png",
footerText: "フッター",
footerIconUrl: "https://example.com/footer.png",
imageUrl: "https://example.com/image.png",
thumbnailUrl: "https://example.com/thumb.png",
}
};
return {
type: "both",
content: "テキスト本文",
embed: {
title: "埋め込みタイトル",
description: "埋め込み説明",
}
};
// メッセージを変更せず、応答があったことだけ確認する
return { type: "defer" };
※ カスタムコマンドの初回実行では defer は無視されます。
※ カスタム返信では defer は利用できません。
⚠ 初回コマンド実行で defer を返すと、Discord への応答が何も送られず、ユーザーに「このインタラクションに失敗しました」と表示されます。
// インタラクション応答で元のメッセージを更新する
return { type: "update", content: "更新後のテキスト" };
return { type: "update", embed: { title: "更新後の埋め込み" } };
※ ボタン・セレクトメニューのインタラクション応答でのみ有効です。
ボタン・セレクトメニューが操作されたときの応答方法を responseMode で指定できます。
指定しない場合は元のメッセージを編集します(後方互換)。
| 値 | 動作 |
|---|---|
"update"(デフォルト) | 元のメッセージを編集(interaction.update()) |
"reply" | 新しい公開メッセージとして返信(元メッセージはそのまま) |
"ephemeral" | クリックした人だけ見える返信(元メッセージはそのまま) |
// 元のメッセージを編集(デフォルト)
return {
type: "text",
content: "更新しました",
// responseMode 未指定 → update
};
// 新しい返信を送る
return {
type: "both",
content: "詳細情報",
embed: { title: "追加情報", description: "..." },
responseMode: "reply",
};
// 自分だけに見える返信
return {
type: "text",
content: "あなただけのメッセージ",
responseMode: "ephemeral",
};
※ responseMode はコンポーネント(ボタン・セレクトメニュー)のインタラクション応答時のみ有効です。初回のコマンド実行・静的返信では無視されます。
カスタムコマンドでは、通常の応答に actions を追加できます。操作は QuickJS から直接行われず、Bot側で同一サーバー内か、必要な権限があるか、Botより下位の管理可能なロールかを再検証してから実行されます。
return {
type: "text",
content: "処理を受け付けました。",
actions: [
// 実行チャンネル以外にも送信できます
{ type: "sendMessage", channelId: "123456789012345678", content: "お知らせです" },
// user/channel/role 型の引数IDは ctx.resolved.<引数名> で取得できます
{ type: "addRole", roleId: ctx.resolved.role, userIds: [ctx.resolved.target] },
{ type: "removeRole", roleId: "234567890123456789", userIds: [ctx.user.id] },
],
};
sendMessageではユーザー・ロール・@everyone・@hereへの通知を抑止します。メンション形式の文字列が名前として表示される場合はあります。@everyone、連携サービス管理ロール、Botの最上位ロール以上は操作できません。command.allowRole(...)などでコマンドの実行者を制限してください。各フィールドには Discord API 準拠の文字数制限が自動適用されます:
| フィールド | 上限 |
|---|---|
content(テキスト) | 2000 文字 |
embed.title | 256 文字 |
embed.description | 4096 文字 |
embed.fields[].name | 256 文字 |
embed.fields[].value | 1024 文字 |
embed.fields 配列長 | 25 個まで |
embed.footerText | 2048 文字 |
embed.authorName | 256 文字 |
コードノードは Discord のボタン・セレクトメニューを動的に生成できます。
type SandboxButton = {
type: "button";
customId: string; // 識別子(最大100文字。現行実装ではURLボタンでも必須)
label: string; // 表示ラベル(最大80文字)
style?: "primary" // 青(デフォルト)
| "secondary" // グレー
| "success" // 緑
| "danger"; // 赤
emoji?: string; // 絵文字(カスタム絵文字IDも可、最大32文字)
disabled?: boolean; // 無効化
url?: string; // URLボタン(Linkスタイル)
};
type SandboxSelect = {
type: "select" | "stringSelect";
customId: string; // 識別子(最大100文字)
placeholder?: string; // プレースホルダー(最大150文字)
options: Array<{
label: string; // 選択肢ラベル(最大100文字)
value: string; // 選択肢値(最大100文字)
description?: string; // 説明(最大100文字)
emoji?: string; // 絵文字(最大32文字)
default?: boolean; // デフォルト選択
}>;
minValues?: number; // 最小選択数
maxValues?: number; // 最大選択数
disabled?: boolean; // 無効化
};
デフォルトは 単一選択(minValues: 1, maxValues: 1)です。複数選択を有効にするには maxValues を 2 以上に設定します。
// 単一選択(デフォルト)— 選択肢から1つだけ選ぶドロップダウン
{
type: "select",
customId: "color",
placeholder: "色を1つ選択してください",
options: [
{ label: "赤", value: "red" },
{ label: "青", value: "blue" },
{ label: "緑", value: "green" },
],
// minValues/maxValues 未指定 → 単一選択
}
// 複数選択 — 選択肢から複数選べる(チェックボックス形式)
{
type: "select",
customId: "colors",
placeholder: "色を複数選択可(最大3つ)",
minValues: 1,
maxValues: 3,
options: [
{ label: "赤", value: "red" },
{ label: "青", value: "blue" },
{ label: "緑", value: "green" },
{ label: "黄", value: "yellow" },
],
}
選択された値は ctx.interaction.values で配列として取得できます。
if (ctx.interaction?.type === "select") {
const selected = ctx.interaction.values; // 例: ["red"]
const firstValue = ctx.interaction.values[0];
return {
type: "text",
content: `選択されました: ${firstValue}`,
responseMode: "reply",
};
}
return {
type: "text",
content: "操作を選んでください:",
components: [
// 1 行目(最大5個のコンポーネント / 最大5行)
[
{ type: "button", customId: "yes", label: "はい", style: "success" },
{ type: "button", customId: "no", label: "いいえ", style: "danger" },
{ type: "button", customId: "info", label: "詳細", style: "secondary" },
],
// 2 行目
[
{
type: "select",
customId: "menu",
placeholder: "選択してください",
options: [
{ label: "オプション1", value: "opt1", description: "説明1" },
{ label: "オプション2", value: "opt2", description: "説明2" },
],
},
],
],
};
ボタン・セレクトメニューがクリックされると、再度同じコードノードが実行されます。
このとき ctx.interaction が設定されます。
ctx.interaction.type — "button" または "select"ctx.interaction.customId — コードで指定した元の customIdctx.interaction.values — セレクトメニューの場合の選択値の配列ctx.args — カスタムコマンドでは空オブジェクト({})、カスタム返信では { trigger: "(インタラクション)" }// ボタンインタラクションの処理例
if (ctx.interaction) {
const id = ctx.interaction.customId;
if (id === "yes") {
return { type: "update", content: "「はい」が押されました!" };
}
if (id === "no") {
return { type: "update", content: "「いいえ」が押されました。" };
}
if (ctx.interaction.type === "select") {
const selected = ctx.interaction.values?.[0];
return { type: "update", content: `選択されました: ${selected}` };
}
// responseMode を使った応答方法の変更
if (id === "details") {
return {
type: "both",
content: "詳細情報",
embed: { title: "追加情報", description: "ここに詳細が入ります" },
responseMode: "reply", // 新しい返信として送信
};
}
if (id === "secret") {
return {
type: "text",
content: "あなただけに表示されるメッセージです",
responseMode: "ephemeral", // 自分だけ見える返信
};
}
}
| 項目 | 上限 |
|---|---|
| 行数(ActionRow) | 5 行 |
| 1行あたりのボタン数 | 5 個 |
| 1行あたりのセレクトメニュー数 | 1 個(ボタンとは別の行に配置) |
| セレクトメニューのオプション数 | 25 個 |
type CustomCommand = {
id?: string; // 自動生成される一意ID
name: string; // コマンド名(c_ プレフィックス自動付与)
description: string; // 説明(スラッシュコマンドの説明文)
options?: CommandOption[]; // 引数定義
executionMode?: "static" // 静的応答
| "code"; // コードノード
code?: string; // コードノードのJSコード
type?: "text" // テキスト返信
| "embed" // 埋め込み返信
| "both"; // テキスト+埋め込み
content?: string; // 静的テキスト内容
ephemeral?: boolean; // エフェメラル(自分だけ表示)
enabled?: boolean; // 有効/無効
allowedRoleIds?: string[]; // 実行許可ロール。コードノードではコードから派生
// 埋め込みフィールド(静的応答)
embedTitle?: string;
embedDescription?: string;
embedColor?: string; // 16進カラーコード(例: "#ff0000")
embedAuthorName?: string;
embedAuthorIconUrl?: string;
embedFooterText?: string;
embedFooterIconUrl?: string;
embedImageUrl?: string;
embedThumbnailUrl?: string;
embedFields?: Array<{
name: string;
value: string;
inline?: boolean;
}>;
};
c_ プレフィックスが付与されます(例: greet → /c_greet)- に変換されます。スペース以外の使用不可文字は除去されますa-z, 0-9, _, -type CommandOptionType = "string" | "integer" | "boolean" | "user" | "channel" | "role";
type CommandOption = {
name: string; // 引数名
type: CommandOptionType; // 型
description: string; // 説明
required?: boolean; // 必須
};
コードノードでは ctx.args.<name> でアクセスできます:
// /c_greet user:@someone
// ctx.args.user → ユーザー名(文字列)
// /c_add x:123 y:456
// ctx.args.x → 123(数値)
command.option)カスタムコマンド(コードノード)では、ダッシュボードの「引数」エディタを使わずに、コード内で command.option(...) を呼び出すだけで引数を定義できます。
// コード冒頭などで宣言(実行順序の制約はなし)
command.option("target", "user", "対象ユーザー", false);
command.option("message", "string", "メッセージ内容", true);
const target = ctx.args.target; // ユーザー名(文字列)/未指定時は null
const message = ctx.args.message; // メッセージ内容(文字列)
return { type: "text", content: `${target} へ: ${message}` };
シグネチャ:
command.option(name: string, type: CommandOptionType, description: string, required?: boolean): void
| 引数 | 説明 |
|---|---|
name | 引数名。小文字化され、不正文字(a-z0-9_- 以外)は除去、最大 32 文字 |
type | 型。string / integer / boolean / user / channel / role のいずれか(それ以外は無視) |
description | 説明(最大 100 文字) |
required | 必須にするか(省略時は false) |
仕組みと注意点:
options として DB に反映されます。抽出時にコードは実行されません。optionsもコードから再生成されます。command は何もしない(no-op)グローバルとして注入されるため、コード内に書いても副作用はありません。static)の場合は、この機能は使えず、引数はダッシュボードのエディタで設定します。command.allowRole)コードノードでは、コマンドを実行できるロールもコード内で宣言できます。判定はコードの実行前に行われます。
command.allowRole("123456789012345678");
command.allowRole("234567890123456789");
return { type: "text", content: "許可されたユーザーです。" };
複数宣言した場合は、いずれか1つのロールを持つユーザーが実行できます(OR条件)。宣言がない場合は全員が実行できます。サーバーオーナーとAdministrator権限保持者は宣言にかかわらず実行できます。
command.allowRole(roleId: string): void
command.allowRole(...)自体は何もしないno-opです。allowedRoleIdsも更新され、宣言が0件なら全員許可へ戻ります。複数のロールをすべて要求する場合(AND条件)は、入口となる許可ロールに加えてctx.user.rolesを確認します。
command.allowRole("123456789012345678");
command.allowRole("234567890123456789");
const requiredRoleIds = ["123456789012345678", "234567890123456789"];
const userRoleIds = new Set(ctx.user.roles.map((role) => role.id));
const hasAllRoles = requiredRoleIds.every((roleId) => userRoleIds.has(roleId));
if (!hasAllRoles && !ctx.user.isAdmin && !ctx.user.isOwner) {
return { type: "text", content: "必要なロールが不足しています。", ephemeral: true };
}
return { type: "text", content: "すべての必要ロールを確認しました。", ephemeral: true };
静的テキスト・埋め込みでは以下のプレースホルダーが自動展開されます:
| プレースホルダー | 展開結果 |
|---|---|
{user.name} | ユーザー名 |
{user.displayName} | サーバーニックネーム |
{user.id} | ユーザーID |
{user.mention} | メンション |
{guild.name} | サーバー名 |
{guild.id} | サーバーID |
{guild.memberCount} | メンバー数 |
{channel.name} | チャンネル名 |
{channel.id} | チャンネルID |
{channel.mention} | チャンネルメンション |
{arg:<name>} | コマンド引数の値 |
type CustomReply = {
id?: string; // 自動生成される一意ID
trigger: string; // トリガーワード/パターン
matchType?: "exact" // 完全一致
| "contains" // 部分一致(デフォルト)
| "startsWith" // 前方一致
| "endsWith" // 後方一致
| "regex"; // 正規表現
caseSensitive?: boolean; // 大文字小文字区別(デフォルト: false)
allowedChannelIds?: string[]; // 許可チャンネルID一覧(空=全チャンネル)
executionMode?: "static" // 静的応答
| "code"; // コードノード
code?: string; // コードノードのJSコード
type?: "text" | "embed" | "both";
content?: string;
enabled?: boolean;
// 埋め込みフィールド(静的応答)
embedTitle?: string;
embedDescription?: string;
embedColor?: string;
embedAuthorName?: string;
embedAuthorIconUrl?: string;
embedFooterText?: string;
embedFooterIconUrl?: string;
embedImageUrl?: string;
embedThumbnailUrl?: string;
embedFields?: Array<{
name: string;
value: string;
inline?: boolean;
}>;
};
カスタム返信はメッセージが送信されるたびに、定義順にトリガーマッチングを行います。
最初にマッチした返信のみが送信されます。
| マッチタイプ | 動作 |
|---|---|
exact | メッセージ内容がトリガーと完全一致 |
contains | メッセージ内容にトリガーが含まれる(デフォルト) |
startsWith | メッセージ内容がトリガーで始まる |
endsWith | メッセージ内容がトリガーで終わる |
regex | トリガーを正規表現としてマッチング |
ctx.args.trigger に元のメッセージ全文が設定されます。allowedChannelIds)が設定されている場合、指定チャンネルでのみマッチします。カスタム返信の静的応答はカスタムコマンドと異なり、{arg:<name>} 引数プレースホルダーは利用できません。
ただし、ユーザー・サーバー・チャンネル情報のプレースホルダー({user.name}、{guild.name}、{channel.name} 等)は展開可能です。
コードノードからボタン・セレクトメニューを含む応答を返すと、コンポーネントが操作された際に同じコードノードが再実行されます。この仕組みにより、ボタンクリックやメニュー選択に応じた動的な応答が実現できます。
components を含む結果を返すctx.interaction 付きで再実行される各コンポーネントの識別子(customId)は内部で一意に管理され、正しいコードノードにルーティングされます。customId の重複を気にせず、各コードノード内で一意であれば問題ありません。
ルーティング情報の有効期間は24時間です。期限切れ後は、ユーザーへ再度コマンドを実行するよう案内されます。設定が無効化・削除・変更された場合も、操作時に現在の設定を再確認します。
URLドメイン許可リストに基づくフィルタリングが適用されます。現行実装ではコードノードの本文・埋め込み・リンクボタン、および静的応答の本文が対象です。静的応答の埋め込みURLフィールドは対象外です。
content 内の URLtitle, description, fields[].name/value, authorName, footerText 内の URLauthorIconUrl, footerIconUrl, imageUrl, thumbnailUrl)のドメインundefined になります| 項目 | 上限 |
|---|---|
| Sandbox タイムアウト | 2 秒 |
| Sandbox メモリ | 16 MB |
| 埋め込みフィールド数 | 25 個 |
| コンポーネント行数 | 5 行 |
| 1行あたりのボタン数 | 5 個 |
| 1行あたりのセレクトメニュー数 | 1 個(ボタンとは別の行) |
| セレクトメニューオプション数 | 25 個 |
| 1回のコード実行で要求できる操作 | 5 件 |
| 別チャンネルへのメッセージ送信 | 3 件 |
| ロール付与・剥奪の対象メンバー | 合計25人 |
| 項目 | 上限 |
|---|---|
コマンド名(c_ 除く実質部分) | 32 文字 |
| 引数オプション数 | 25 個(Discord 制限) |
| コマンド説明 | 100 文字 |
| 自動プレフィックス | c_ |
| 実行許可ロール | 1コマンド10ロール(未指定は全員) |
| 項目 | 値 |
|---|---|
| トリガーマッチ順序 | 定義順(最初にマッチしたものが優先) |
| Bot メッセージへの反応 | なし |
| 静的応答の変数展開 | ユーザー/サーバー/チャンネル情報のみ対応({arg:<name>} は非対応) |
コードノードの ctx.args.trigger | 元のメッセージ全文 |
コードノードから利用できるキーバリューストアです。データは永続化され、同じサーバー内の異なるコードノード実行間で共有されます。
コードノード内でグローバル変数 kv を通じてアクセスできます。
| メソッド | 戻り値 | 説明 |
|---|---|---|
kv.get(key) | any | 値を取得(JSON自動パース)。なければ undefined |
kv.set(key, value) | void | 値を保存(自動 JSON.stringify) |
kv.delete(key) | void | キーを削除 |
kv.keys() | string[] | 全キー名の配列を取得 |
// カウンター
const count = (kv.get("count") ?? 0) + 1;
kv.set("count", count);
return { type: "text", content: `${count}回目の実行です` };
// 設定の保存
let config = kv.get("config") ?? { color: "#5865F2", prefix: "!" };
if (ctx.args.color) config.color = ctx.args.color;
kv.set("config", config);
return {
type: "embed",
embed: {
title: "現在の設定",
fields: [
{ name: "色", value: config.color, inline: true },
{ name: "接頭辞", value: config.prefix, inline: true },
],
color: config.color,
},
};
// アクセス履歴
const log = kv.get("access_log") ?? [];
log.push({ user: ctx.user.username, at: new Date().toISOString() });
kv.set("access_log", log.slice(-50)); // 最新50件
return {
type: "text",
content: `${ctx.user.mention} さん、履歴を記録しました(全${log.length}件)`,
};
// キーの削除
kv.delete("temp_data");
return { type: "text", content: "一時データを削除しました" };
// 全キーの一覧
const keys = kv.keys();
return {
type: "text",
content: `保存されているキー: ${keys.join(", ") || "(なし)"}`,
};
| 項目 | 上限 |
|---|---|
| 1サーバーあたりの最大キー数 | 500 |
| 1キーの最大値サイズ(JSON化後) | 256 KB |
| キー名の長さ | 128 文字 |
| キー名に使える文字 | a-zA-Z0-9._- のみ |
| 1実行あたりの最大書き込み回数 | 100 回(set/delete 合計) |
| スコープ | サーバー単位(他サーバーのデータにはアクセス不可) |
42 は数値として保存・取得され、"hello" は文字列として返ります。KVストアは、Bot のコードノードだけでなく HTTP API からも読み書きできます。
外部のアプリケーション(Webサービス・自作ツールなど)からこのサーバーの KV データを直接操作したい場合に利用します。
⚠ APIキーの発行・失効はサーバーオーナーのみ行えます。
kkv_ から始まるランダム文字列(例: kkv_AbC123...)| メソッド | パス | 用途 |
|---|---|---|
GET | /api/public/kv/:guildId | 全キー一覧取得 |
GET | /api/public/kv/:guildId/:key | 単一キー取得 |
PUT | /api/public/kv/:guildId/:key | キーの作成・更新 |
DELETE | /api/public/kv/:guildId/:key | キー削除 |
:guildId はサーバーID、:key はキー名ですAuthorization: Bearer <APIキー> ヘッダー(または X-API-Key ヘッダー)で行いますa-zA-Z0-9._-(最大128文字)。値は 256KB 以内# 全キー取得
curl -H "Authorization: Bearer kkv_AbC123..." \
https://frex.kokonatsu.net/api/public/kv/123456789012345678
# 単一キー取得
curl -H "Authorization: Bearer kkv_AbC123..." \
https://frex.kokonatsu.net/api/public/kv/123456789012345678/count
# キーの作成・更新(value は JSON 文字列)
curl -X PUT \
-H "Authorization: Bearer kkv_AbC123..." \
-H "Content-Type: application/json" \
-d '{"value":"42"}' \
https://frex.kokonatsu.net/api/public/kv/123456789012345678/count
# キー削除
curl -X DELETE \
-H "Authorization: Bearer kkv_AbC123..." \
https://frex.kokonatsu.net/api/public/kv/123456789012345678/count
API のベース URL は https://frex.kokonatsu.net(/api/public/kv/...)です。
// GET 全キー取得
{
"success": true,
"data": [
{ "key": "count", "value": "42", "updatedAt": "2026-08-05T12:00:00.000Z" }
]
}
// GET 単一キー取得
{
"success": true,
"data": { "key": "count", "value": "42", "updatedAt": "2026-08-05T12:00:00.000Z" }
}
// PUT / DELETE 成功時
{ "success": true }
value は JSON化された文字列 として返ります(42 は "42"、文字列は "\"hello\"")401 Unauthorized / 他サーバーへのアクセス: 403 Forbidden400 / 存在しないキーの取得: 404|