Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

サーバーごとの権限管理システム (#92) #99

Merged
merged 14 commits into from
May 9, 2024
29 changes: 23 additions & 6 deletions core/common/CompoundCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
AutocompleteInteraction,
CacheType,
ChatInputCommandInteraction,
Client,
CommandInteractionOptionResolver,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
} from 'discord.js';
Expand Down Expand Up @@ -110,15 +112,30 @@ export class CompoundCommand implements Command {
interaction: ChatInputCommandInteraction<CacheType>,
client: Client<true>,
): Promise<void> {
const subcommand = this.getSubcommand(interaction);
subcommand.execute(interaction);
}

async autocomplete(
interaction: AutocompleteInteraction<CacheType>,
): Promise<void> {
const subcommand = this.getSubcommand(interaction);
subcommand.autocomplete(interaction);
}

getSubcommand(interaction: {
options: Pick<CommandInteractionOptionResolver, 'getSubcommand'>;
}) {
const subcommandName = interaction.options.getSubcommand(true);
const subcommand = this.#subcommands.get(subcommandName);
if (subcommand == null) {
await interaction.reply({
ephemeral: true,
content: 'Invalid subcommand: ' + subcommandName,
});
return;
const subcommands = Array.from(this.#subcommands.keys())
.map((s) => `'${s}'`)
.join(', ');
throw new TypeError(
`Invalid subcommand: '${subcommandName}', subcommands: ${subcommands}`,
);
}
subcommand.execute(interaction);
return subcommand;
}
}
87 changes: 85 additions & 2 deletions core/common/SimpleCommand.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
APIApplicationCommandOptionChoice,
ApplicationCommandOptionWithChoicesAndAutocompleteMixin,
AutocompleteInteraction,
CacheType,
GuildMember,
Role,
SharedSlashCommandOptions,
SlashCommandSubcommandBuilder,
User,
} from 'discord.js';
import { ChatInputCommandInteraction } from 'discord.js';
import { Command } from '../util/types';
Expand All @@ -26,7 +30,7 @@ interface SimpleCommandOptionData<T, Required extends boolean = boolean> {

interface SimpleChoiceOptionData<T> {
choices?: APIApplicationCommandOptionChoice<T>[];
autocomplete?: boolean;
autocomplete?(interaction: AutocompleteInteraction): PromiseLike<void> | void;
}

interface SimpleRangeOptionData {
Expand All @@ -50,6 +54,11 @@ interface SimpleStringOptionData<
min_length?: number;
}

type Mentionable = GuildMember | Role | User;

type SimpleMentionableOptionData<Required extends boolean = boolean> =
SimpleCommandOptionData<Mentionable, Required>;

export interface Option<T = unknown, Required extends boolean = boolean> {
/** オプションの名前 */
name: string;
Expand All @@ -62,6 +71,8 @@ export interface Option<T = unknown, Required extends boolean = boolean> {
* @param interaction コマンドのインタラクション
*/
get(interaction: ChatInputCommandInteraction): Value<T, Required>;

autocomplete?(interaction: AutocompleteInteraction): PromiseLike<void> | void;
}

function setChoices<T extends string | number>(
Expand All @@ -73,7 +84,7 @@ function setChoices<T extends string | number>(
option.addChoices(...choices);
}
if (autocomplete != null) {
option.setAutocomplete(autocomplete);
option.setAutocomplete(true);
}
}

Expand All @@ -84,13 +95,18 @@ class IntegerOption<T extends number, Required extends boolean = boolean>

required: Required;

autocomplete?(
interaction: AutocompleteInteraction<CacheType>,
): void | PromiseLike<void>;

constructor(
builder: SharedSlashCommandOptions,
input: SimpleIntegerOptionData<T, Required>,
) {
const { name, required } = input;
this.name = name;
this.required = required;
this.autocomplete = input.autocomplete;
builder.addIntegerOption((option) => {
option
.setName(name)
Expand Down Expand Up @@ -127,12 +143,17 @@ class StringOption<

required: Required;

autocomplete?(
interaction: AutocompleteInteraction<CacheType>,
): void | PromiseLike<void>;

constructor(
builder: SharedSlashCommandOptions,
input: SimpleStringOptionData<T, Required>,
) {
this.name = input.name;
this.required = input.required;
this.autocomplete = input.autocomplete;
builder.addStringOption((option) => {
option
.setName(input.name)
Expand All @@ -157,6 +178,49 @@ class StringOption<
}
}

class MentionableOption<Required extends boolean = boolean>
implements Option<Mentionable, Required>
{
name: string;

required: Required;

constructor(
builder: SharedSlashCommandOptions,
input: SimpleMentionableOptionData<Required>,
) {
const name = input.name;
const description = input.description;
const required = input.required;
this.name = name;
this.required = required;
try {
builder.addMentionableOption((input) => {
return input
.setName(name)
.setDescription(description)
.setRequired(required);
});
} catch (e) {
console.error(e);
}
}

get(
interaction: ChatInputCommandInteraction<CacheType>,
): Value<Mentionable, Required> {
return this.required
? (interaction.options.getMentionable(this.name, true) as Value<
Mentionable,
Required
>)
: (interaction.options.getMentionable(this.name) as Value<
Mentionable,
Required
>);
}
}

/**
* シンプルな SlashCommandBuilder(?)
*/
Expand Down Expand Up @@ -231,6 +295,12 @@ export class SimpleSlashCommandBuilder<
return this.addOption(new StringOption(this.handle, input));
}

addMentionableOption<Required extends boolean = boolean>(
input: SimpleMentionableOptionData<Required>,
) {
return this.addOption(new MentionableOption(this.handle, input));
}

build(
action: (
interaction: ChatInputCommandInteraction,
Expand Down Expand Up @@ -274,4 +344,17 @@ export class SimpleCommand<Options extends Option<unknown, boolean>[]>
) as OptionValueMap<Options>;
await this.action(interaction, ...optionValues);
}

async autocomplete(
interaction: AutocompleteInteraction<CacheType>,
): Promise<void> {
const focusedValue = interaction.options.getFocused(true);
const focusedOption = this.builder.options.find(
(option) => option.name == focusedValue.name,
);
if (focusedOption == null) {
throw new TypeError(`Unknown option: '${focusedValue.name}'`);
}
focusedOption.autocomplete?.(interaction);
}
}
41 changes: 28 additions & 13 deletions core/internal/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fs from 'fs/promises';
import path from 'path';
import { strFormat, LANG } from '../util/languages';
import { ChatInputCommandInteraction, Client } from 'discord.js';
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
Client,
} from 'discord.js';
import { Command } from '../util/types';

export class CommandManager {
Expand All @@ -24,7 +28,9 @@ export class CommandManager {
await client.application.commands.set(commands);
client.on('interactionCreate', (interaction) => {
if (interaction.isChatInputCommand()) {
this.#handleInteraction(interaction, client);
this.#handleChatInputCommand(interaction, client);
} else if (interaction.isAutocomplete()) {
this.#handleAutocomplete(interaction);
}
});
}
Expand Down Expand Up @@ -60,22 +66,26 @@ export class CommandManager {
}
}

/**
* コマンドの処理を行う。
*/
async #handleInteraction(
interaction: ChatInputCommandInteraction,
client: Client<true>,
) {
const command = this.#commands.get(interaction.commandName);
#get(name: string): Command {
const command = this.#commands.get(name);
if (!command) {
console.error(
throw new Error(
strFormat(LANG.discordbot.interactionCreate.unsupportedCommandError, [
interaction.commandName,
name,
]),
);
return;
}
return command;
}

/**
* チャット入力コマンドの処理を行う。
*/
async #handleChatInputCommand(
interaction: ChatInputCommandInteraction,
client: Client<true>,
) {
const command = this.#get(interaction.commandName);
try {
await command.execute(interaction, client);
} catch (error) {
Expand All @@ -93,4 +103,9 @@ export class CommandManager {
throw error;
}
}

async #handleAutocomplete(interaction: AutocompleteInteraction) {
const command = this.#get(interaction.commandName);
await command.autocomplete?.(interaction);
}
}
6 changes: 6 additions & 0 deletions core/util/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
Client,
RESTPostAPIChatInputApplicationCommandsJSONBody,
Expand All @@ -25,6 +26,11 @@ export interface Command {
interaction: ChatInputCommandInteraction,
client: Client<true>,
): Promise<void>;

/**
* 自動補完の処理
*/
autocomplete?(interaction: AutocompleteInteraction): Promise<void>;
}

/**
Expand Down
39 changes: 38 additions & 1 deletion language/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@
"result": "結果:",
"notPlayableError": "えー実行したくないなぁー...だってVCに君が居ないんだもん...",
"playerTrack": "${title} (${duration})",
"noTracksPlayed": "再生されている曲がありません!"
"noTracksPlayed": "再生されている曲がありません!",
"useCommandInGuild": "このコマンドはサーバー内で使用してください!",
"noPermission": "権限がありません!"
},
"defaultValues": {
"graphLabel": "value"
Expand Down Expand Up @@ -570,6 +572,40 @@
"playerPaused": "音楽を一時停止しました!",
"pauseFailed": "一時停止できませんでした"
},
"perm": {
"name": "perm",
"description": "権限の設定",
"subcommands": {
"set": {
"name": "set",
"description": "値の更新"
},
"get": {
"name": "get",
"description": "値の取得"
},
"remove": {
"name": "remove",
"description": "値の削除"
}
},
"options": {
"permission": {
"name": "permission",
"description": "権限名"
},
"group": {
"name": "group",
"description": "対象のロールまたはユーザー"
}
},
"noSuchPermission": ["権限名: ${0}", "その名前の権限はありません!"],
"permissionInformation": "権限情報",
"permissionName": "権限名",
"permissionGroup": "ロール/メンバー",
"permissionSet": "権限を追加しました!",
"permissionRemoved": "権限を削除しました"
},
"pieChart": {
"name": "piechart",
"description": "円グラフを生成します。",
Expand Down Expand Up @@ -701,6 +737,7 @@
"emptyMessage": "メッセージが設定されていません"
}
},
"replyCustomizePermission": "自動応答の設定",
"notInGuildError": "このコマンドはサーバー内でのみ使用できます!",
"permissionError": "このコマンドを使用する権限がありません!"
},
Expand Down
Loading