Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: スキーマを利用した新コマンドシステム #479

Merged
merged 55 commits into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
04b1f5c
feat: Add Schema
MikuroXina Sep 12, 2022
027912e
docs: Add docs and tidy up
MikuroXina Sep 12, 2022
b512977
fix: Add paramsOrder
MikuroXina Sep 12, 2022
dffb103
fix: Improve SubCommand
MikuroXina Sep 12, 2022
2d081c0
fix: Fix error payload
MikuroXina Sep 13, 2022
93b51ad
fix: Add NEED_MORE_ARGS error
MikuroXina Sep 13, 2022
c9e2058
fix: Add type parameters
MikuroXina Sep 13, 2022
fa8446d
fix: Fix arg of ParsedSubCommand
MikuroXina Sep 13, 2022
1fe2083
fix: Improve parsed types
MikuroXina Sep 13, 2022
cbf7d94
fix: Support params on Schema
MikuroXina Sep 13, 2022
13bfa6d
refactor: Update CommandMessage
MikuroXina Sep 13, 2022
6d8094d
feat: Impl parseStrings
MikuroXina Sep 13, 2022
f96a45a
fix: Fix to check range
MikuroXina Sep 13, 2022
d0e7852
fix: Add makeError
MikuroXina Sep 13, 2022
dd4585c
fix: Weaken type bound
MikuroXina Sep 13, 2022
48dd144
test: Add simple test case
MikuroXina Sep 13, 2022
9513f80
fix: Fix some keys and test case
MikuroXina Sep 13, 2022
3f84f28
fix: Remove parsed key
MikuroXina Sep 13, 2022
85a8bed
test: Enforce test case
MikuroXina Sep 13, 2022
9a6d167
test: Enforce test cases
MikuroXina Sep 13, 2022
425ebeb
fix: Fix DIGITS and update output
MikuroXina Sep 13, 2022
5ffb452
feat: Add MESSAGE type
MikuroXina Sep 13, 2022
36ed3fb
fix: Make subCommand and params required
MikuroXina Sep 13, 2022
5536a57
fix: Weaken type bound
MikuroXina Sep 13, 2022
a8e7148
fix: Improve ParsedSchema
MikuroXina Sep 13, 2022
88e767b
fix: Fix fallback type
MikuroXina Sep 13, 2022
51e048a
fix: Fix type on single arg
MikuroXina Sep 13, 2022
bbebab3
fix: Fix type pattern match
MikuroXina Sep 14, 2022
6b1bbfb
feat: Add ParamBase
MikuroXina Sep 14, 2022
ef8d855
fix: Fix SubCommand definition
MikuroXina Sep 14, 2022
4290a9f
fix: Support variadic arguments
MikuroXina Sep 14, 2022
9f8b683
fix: Make subCommand optional
MikuroXina Sep 14, 2022
ae017b1
feat: Add SCHEMA
MikuroXina Sep 14, 2022
cf24cb0
fix: Improve ParamsValues
MikuroXina Sep 14, 2022
d167bce
fix: Fix registration
MikuroXina Sep 14, 2022
5fca69d
feat: Add parseStringsOrThrow
MikuroXina Sep 14, 2022
d8b0095
fix: Fix type bound
MikuroXina Sep 14, 2022
7d26ecb
fix: Improve createMockMessage args
MikuroXina Sep 14, 2022
d51c494
fix: Remove empty subCommand from output
MikuroXina Sep 14, 2022
7780b86
fix: Weaken type of param
MikuroXina Sep 14, 2022
591d8ca
fix: Fix on no args but has sub command
MikuroXina Sep 14, 2022
eb4f792
fix: Fix to report error
MikuroXina Sep 14, 2022
165e508
test: Update command tests
MikuroXina Sep 14, 2022
47cbd23
fix: Guard on other commands
MikuroXina Sep 14, 2022
6dcd9ca
fix: Fix typing with Equal
MikuroXina Sep 14, 2022
a057868
feat: Improve with CommandRunner
MikuroXina Sep 15, 2022
63eb76f
fix: Remove redundant test
MikuroXina Sep 15, 2022
3ce2a2e
doc: Update CONTRIBUTING for newer method
MikuroXina Sep 15, 2022
bbbb2e8
fix: Fix not to throw error
MikuroXina Sep 15, 2022
eb76425
fix: Fix parsing variadic params
MikuroXina Sep 15, 2022
ca9b86a
fix: Fix decoupling
MikuroXina Sep 15, 2022
caa0164
feat: Add more hibikiness
MikuroXina Sep 15, 2022
81c1cf5
fix: Guard with checking editable
MikuroXina Sep 15, 2022
ce47c1f
feat: Add bug report link
MikuroXina Sep 17, 2022
af0dcc5
Merge branch 'main' into refactor/#461
MikuroXina Sep 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ import { Meme } from './meme.js';
import { createMockMessage } from './command-message.js';

it('use case of hukueki', async () => {
const fn = vi.fn<[EmbedMessage]>(() => Promise.resolve());
const fn = vi.fn();
const responder = new Meme();
await responder.on(
'CREATE',
createMockMessage(
{
args: ['hukueki', 'こるく']
Expand Down
181 changes: 181 additions & 0 deletions src/adaptor/proxy/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
type APIActionRowComponent,
type APIMessageActionRowComponent,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
Client,
Message,
type MessageActionRowComponentBuilder
} from 'discord.js';
import type {
CommandProxy,
MessageCreateListener
} from '../../runner/command.js';
import { Schema, makeError } from '../../model/command-schema.js';
import type { EmbedPage } from '../../model/embed-message.js';
import type { RawMessage } from './middleware.js';
import type { Snowflake } from '../../model/id.js';
import { convertEmbed } from '../embed-convert.js';
import { parseStrings } from './command/schema.js';

const SPACES = /\s+/;

export class DiscordCommandProxy implements CommandProxy {
constructor(client: Client, private readonly prefix: string) {
client.on('messageCreate', (message) => this.onMessageCreate(message));
}

private readonly listenerMap = new Map<
string,
[Schema, MessageCreateListener]
>();

addMessageCreateListener(
schema: Schema,
listener: MessageCreateListener
): void {
for (const name of schema.names) {
if (this.listenerMap.has(name)) {
throw new Error(`command name conflicted: ${name}`);
}
this.listenerMap.set(name, [schema, listener]);
}
}

private async onMessageCreate(message: Message): Promise<void> {
if (message.author.bot || message.author.system) {
return;
}
await message.fetch();

if (!message.content?.trimStart().startsWith(this.prefix)) {
return;
}
const args = message.content
?.trim()
.slice(this.prefix.length)
.split(SPACES);

const entry = this.listenerMap.get(args[0]);
if (!entry) {
return;
}
const [schema, listener] = entry;
const [tag, parsedArgs] = parseStrings(args, schema);
if (tag === 'Err') {
const error = makeError(parsedArgs);
await message.reply(error.message);
return;
}

await listener({
senderId: message.author.id as Snowflake,
senderGuildId: message.guildId as Snowflake,
senderChannelId: message.channelId as Snowflake,
get senderVoiceChannelId(): Snowflake | null {
const id = message.member?.voice.channelId ?? null;
return id ? (id as Snowflake) : null;
},
senderName: message.author?.username ?? '名無し',
m1sk9 marked this conversation as resolved.
Show resolved Hide resolved
args: parsedArgs,
async reply(embed) {
const mes = await message.reply({ embeds: [convertEmbed(embed)] });
return {
edit: async (embed) => {
await mes.edit({ embeds: [convertEmbed(embed)] });
}
};
},
replyPages: replyPages(message),
async react(emoji) {
await message.react(emoji);
}
});
}
}

const ONE_MINUTE_MS = 60_000;
const CONTROLS: APIActionRowComponent<APIMessageActionRowComponent> =
new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setCustomId('prev')
.setLabel('戻る')
.setEmoji('⏪'),
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setCustomId('next')
.setLabel('進む')
.setEmoji('⏩')
)
.toJSON();
const CONTROLS_DISABLED: APIActionRowComponent<APIMessageActionRowComponent> =
new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setCustomId('prev')
.setLabel('戻る')
.setEmoji('⏪')
.setDisabled(true),
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setCustomId('next')
.setLabel('進む')
.setEmoji('⏩')
.setDisabled(true)
)
.toJSON();

const pagesFooter = (currentPage: number, pagesLength: number) =>
`ページ ${currentPage + 1}/${pagesLength}`;
m1sk9 marked this conversation as resolved.
Show resolved Hide resolved

const replyPages = (message: RawMessage) => async (pages: EmbedPage[]) => {
if (pages.length === 0) {
throw new Error('pages must not be empty array');
}

const generatePage = (index: number) =>
convertEmbed(pages[index]).setFooter({
text: pagesFooter(index, pages.length)
});

const paginated = await message.reply({
embeds: [generatePage(0)],
components: [CONTROLS]
});

const collector = paginated.createMessageComponentCollector({
time: ONE_MINUTE_MS
});

let currentPage = 0;
collector.on('collect', async (interaction) => {
switch (interaction.customId) {
case 'prev':
if (0 < currentPage) {
currentPage -= 1;
} else {
currentPage = pages.length - 1;
}
break;
case 'next':
if (currentPage < pages.length - 1) {
currentPage += 1;
} else {
currentPage = 0;
}
break;
default:
return;
}
await interaction.update({ embeds: [generatePage(currentPage)] });
});
collector.on('end', async () => {
if (paginated.editable) {
await paginated.edit({ components: [CONTROLS_DISABLED] });
}
m1sk9 marked this conversation as resolved.
Show resolved Hide resolved
});
};
139 changes: 139 additions & 0 deletions src/adaptor/proxy/command/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { expect, test } from 'vitest';

import { parseStrings } from './schema.js';

test('no args', () => {
const SERVER_INFO_SCHEMA = {
names: ['serverinfo'],
subCommands: {}
} as const;

const noParamRes = parseStrings(['serverinfo'], SERVER_INFO_SCHEMA);

expect(noParamRes).toStrictEqual([
'Ok',
{
name: 'serverinfo',
params: []
}
]);
});

test('single arg', () => {
const TIME_OPTION = [
{ name: 'at', description: '', type: 'STRING' }
] as const;
const KAERE_SCHEMA = {
names: ['kaere'],
subCommands: {
start: {
type: 'SUB_COMMAND'
},
bed: {
type: 'SUB_COMMAND_GROUP',
subCommands: {
enable: {
type: 'SUB_COMMAND'
},
disable: {
type: 'SUB_COMMAND'
},
status: {
type: 'SUB_COMMAND'
}
}
},
reserve: {
type: 'SUB_COMMAND_GROUP',
subCommands: {
add: {
type: 'SUB_COMMAND',
params: TIME_OPTION
},
cancel: {
type: 'SUB_COMMAND',
params: TIME_OPTION
},
list: {
type: 'SUB_COMMAND'
}
}
}
}
} as const;

const noParamRes = parseStrings(['kaere'], KAERE_SCHEMA);

expect(noParamRes).toStrictEqual([
'Ok',
{
name: 'kaere',
params: []
}
]);

const oneParamRes = parseStrings(['kaere', 'start'], KAERE_SCHEMA);

expect(oneParamRes).toStrictEqual([
'Ok',
{
name: 'kaere',
params: [],
subCommand: {
name: 'start',
type: 'PARAMS',
params: []
}
}
]);

const subCommandRes = parseStrings(
['kaere', 'reserve', 'add', '01:12'],
KAERE_SCHEMA
);

expect(subCommandRes).toStrictEqual([
'Ok',
{
name: 'kaere',
params: [],
subCommand: {
name: 'reserve',
type: 'SUB_COMMAND',
subCommand: {
name: 'add',
type: 'PARAMS',
params: ['01:12']
}
}
}
]);
});

test('multi args', () => {
const ROLE_CREATE_SCHEMA = {
names: ['rolecreate'],
subCommands: {},
params: [
{ type: 'USER', name: 'target', description: '' },
{ type: 'STRING', name: 'color', description: '', defaultValue: 'random' }
]
} as const;

const noParamRes = parseStrings(['rolecreate'], ROLE_CREATE_SCHEMA);

expect(noParamRes).toStrictEqual(['Err', ['NEED_MORE_ARGS']]);

const oneParamRes = parseStrings(
['rolecreate', '0123456789'],
ROLE_CREATE_SCHEMA
);

expect(oneParamRes).toStrictEqual([
'Ok',
{
name: 'rolecreate',
params: ['0123456789', 'random']
}
]);
});