Skip to content

Commit

Permalink
fix: Meme における位置引数のサポートとリファクタ (#749)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikuroXina authored Feb 24, 2023
1 parent aa1d53f commit 56b2316
Show file tree
Hide file tree
Showing 23 changed files with 315 additions and 193 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.14.0",
"cli-argparse": "^1.1.2",
"date-fns": "^2.29.3",
"discord.js": "^14.7.1",
"dotenv": "^16.0.3",
"fast-diff": "^1.2.0",
"nanoid": "^4.0.0",
"tweetnacl": "^1.0.3",
"yaml": "^2.1.3"
"yaml": "^2.1.3",
"yargs": "^17.7.1"
},
"devDependencies": {
"@codedependant/semantic-release-docker": "^4.1.0",
"@types/node": "^18.11.17",
"@types/yargs": "^17.0.22",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.45.1",
"@vitest/coverage-c8": "^0.28.0",
Expand Down
31 changes: 23 additions & 8 deletions src/model/meme-template.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
export interface ParsedArgs<
FLAGS_KEY extends string,
OPTIONS_KEY extends string
FLAGS_KEY extends string = never,
OPTIONS_KEY extends string = never,
REQ_POSITIONAL_KEY extends string = never,
OPT_POSITIONAL_KEY extends string = never
> {
flags: Record<FLAGS_KEY, boolean | undefined>;
options: Record<OPTIONS_KEY, string | undefined>;
body: string;
requiredPositionals: Record<REQ_POSITIONAL_KEY, string>;
optionalPositionals: Record<OPT_POSITIONAL_KEY, string | undefined>;
}

export interface MemeTemplate<
FLAGS_KEY extends string,
OPTIONS_KEY extends string
FLAGS_KEY extends string = never,
OPTIONS_KEY extends string = never,
REQ_POSITIONAL_KEY extends string = never,
OPT_POSITIONAL_KEY extends string = never
> {
commandNames: readonly string[];
description: string;
flagsKeys: readonly FLAGS_KEY[];
optionsKeys: readonly OPTIONS_KEY[];
flagsKeys?: readonly FLAGS_KEY[];
optionsKeys?: readonly OPTIONS_KEY[];
requiredPositionalKeys?: readonly REQ_POSITIONAL_KEY[];
optionalPositionalKeys?: readonly OPT_POSITIONAL_KEY[];
errorMessage: string;
generate(args: ParsedArgs<FLAGS_KEY, OPTIONS_KEY>, author: string): string;
generate(
args: ParsedArgs<
FLAGS_KEY,
OPTIONS_KEY,
REQ_POSITIONAL_KEY,
OPT_POSITIONAL_KEY
>,
author: string
): string;
}
10 changes: 1 addition & 9 deletions src/service/command/meme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';

import { parseStringsOrThrow } from '../../adaptor/proxy/command/schema.js';
import { createMockMessage } from './command-message.js';
import { Meme, sanitizeArgs } from './meme.js';
import { Meme } from './meme.js';

describe('meme', () => {
const responder = new Meme();
Expand All @@ -23,11 +23,3 @@ describe('meme', () => {
expect(fn).not.toHaveBeenCalled();
});
});

describe('sanitizeArgs', () => {
it('rids pollution', () => {
expect(
sanitizeArgs(['--yes', '--__proto__=0', '-n', '-constructor', 'hoge'])
).toStrictEqual(['--yes', '-n', 'hoge']);
});
});
112 changes: 76 additions & 36 deletions src/service/command/meme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import parse from 'cli-argparse';
import yargs from 'yargs';

import type { MemeTemplate } from '../../model/meme-template.js';
import type {
Expand All @@ -10,7 +10,7 @@ import { memes } from './meme/index.js';

const memesByCommandName: Record<
string,
MemeTemplate<string, string> | undefined
MemeTemplate<string, string, string, string> | undefined
> = Object.fromEntries(
memes.flatMap((meme) => meme.commandNames.map((name) => [name, meme]))
);
Expand Down Expand Up @@ -46,58 +46,98 @@ export class Meme implements CommandResponder<typeof SCHEMA> {
if (!meme) {
return;
}
const sanitizedArgs = sanitizeArgs(commandArgs);
const hyphen = (key: string) => (key.length <= 1 ? `-${key}` : `--${key}`);
const { flags, options, unparsed } = parse(sanitizedArgs, {
flags: meme.flagsKeys.map(hyphen),
options: meme.optionsKeys.map(hyphen)

const builder = yargs(`${commandName} ${commandArgs.join(' ')}`);
builder.help('info', meme.description);
const flagsKeys = meme.flagsKeys ?? [];
for (const flagKey of flagsKeys) {
builder.boolean(flagKey);
}
const optionsKeys = meme.optionsKeys ?? [];
for (const optionKey of optionsKeys) {
builder.string(optionKey);
}
const requiredPositionalKeys = meme.requiredPositionalKeys ?? [];
const optionalPositionalKeys = meme.optionalPositionalKeys ?? [];
const formattedPositionalKeys = requiredPositionalKeys
.map((key) => `<${key}>`)
.concat(optionalPositionalKeys.map((key) => `[${key}]`))
.join(' ');
const formattedCommand = `${commandName} ${formattedPositionalKeys}`;

builder.command(formattedCommand, meme.description, (subBuilder) => {
for (const key of requiredPositionalKeys.concat(optionalPositionalKeys)) {
subBuilder.positional(key, {
type: 'string'
});
}
});
const body = unparsed.join(' ');
if (flags['help'] || options['help']) {
builder.fail(() => {
void reportError(message, meme);
});

const argv = await builder.parseAsync();

if (argv.help) {
await message.reply({
title: meme.commandNames.map((name) => `\`${name}\``).join('/'),
description: meme.description
});
return;
}
if (body === '') {
await message.reply({
title: '引数が不足してるみたいだ。',
description: meme.errorMessage
});
return;

const requiredPositionalsUnsafe = extract(argv, requiredPositionalKeys);
for (const key of requiredPositionalKeys) {
if (!Object.hasOwn(requiredPositionalsUnsafe, key)) {
await reportError(message, meme);
return;
}
const value = requiredPositionalsUnsafe[key] as string;
if (value.startsWith('"') && value.endsWith('"')) {
requiredPositionalsUnsafe[key] = value.slice(1, -1);
}
}
const splitOptions = split(options);
const generated = meme.generate(
{
flags,
options: splitOptions,
body
flags: extract(argv, flagsKeys) as Record<string, boolean | undefined>,
options: extract(argv, optionsKeys) as Record<
string,
string | undefined
>,
requiredPositionals: requiredPositionalsUnsafe as Record<
string,
string
>,
optionalPositionals: extract(argv, optionalPositionalKeys) as Record<
string,
string | undefined
>
},
message.senderName
);
await message.reply({ description: generated });
}
}

const TO_RID = /^(-+)?(__proto__|prototype|constructor)/g;

export function sanitizeArgs(args: readonly string[]): string[] {
return args.flatMap((arg) => {
if (TO_RID.test(arg)) {
return [];
}
return [arg];
async function reportError(
message: CommandMessage<typeof SCHEMA>,
meme: MemeTemplate<string, string, string, string>
) {
await message.reply({
title: '引数が不足してるみたいだ。',
description: meme.errorMessage
});
}

function split(
options: Record<string, string | string[] | undefined>
): Record<string, string | undefined> {
return Object.fromEntries(
Object.entries(options).map(([key, value]) => [
key,
Array.isArray(value) ? value.join(' ') : value
])
);
function extract(
obj: Record<string, unknown>,
keys: readonly string[]
): Record<string, unknown> {
const ret: Record<string, unknown> = {};
for (const key of keys) {
if (Object.hasOwn(obj, key)) {
ret[key] = obj[key];
}
}
return ret;
}
16 changes: 10 additions & 6 deletions src/service/command/meme/clang.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { MemeTemplate } from '../../../model/meme-template.js';

export const clang: MemeTemplate<never, never> = {
const positionalKeys = ['domain', 'way'] as const;

export const clang: MemeTemplate<
never,
never,
(typeof positionalKeys)[number]
> = {
commandNames: ['clang', 'c'],
description: '〜の天才\n9つの〜を操る',
flagsKeys: [],
optionsKeys: [],
requiredPositionalKeys: positionalKeys,
errorMessage: 'エラーの天才\n9つの引数エラーを操る',
generate(args) {
const [option1, option2] = args.body.split(' ');
return `${option1}の天才\n9つの${option2}を操る`;
generate({ requiredPositionals: { domain, way } }) {
return `${domain}の天才\n9つの${way}を操る`;
}
};
13 changes: 9 additions & 4 deletions src/service/command/meme/dousurya.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { MemeTemplate } from '../../../model/meme-template.js';

export const dousurya: MemeTemplate<never, never> = {
const positionalKeys = ['living'] as const;

export const dousurya: MemeTemplate<
never,
never,
(typeof positionalKeys)[number]
> = {
commandNames: ['dousurya', 'dousureba'],
description: '限界みたいな鯖に住んでる〜はどうすりゃいいですか?',
flagsKeys: [],
optionsKeys: [],
requiredPositionalKeys: positionalKeys,
errorMessage: 'どうしようもない。',
generate(args) {
return `限界みたいな鯖に住んでる${args.body}はどうすりゃいいですか?`;
return `限界みたいな鯖に住んでる${args.requiredPositionals.living}はどうすりゃいいですか?`;
}
};
11 changes: 8 additions & 3 deletions src/service/command/meme/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import type { MemeTemplate } from '../../../model/meme-template.js';
const sourceLink = 'https://dic.nicovideo.jp/id/5671528';

const failureOption = ['k'] as const;
const positionalKeys = ['explanation'] as const;

export const failure: MemeTemplate<never, (typeof failureOption)[number]> = {
export const failure: MemeTemplate<
never,
(typeof failureOption)[number],
(typeof positionalKeys)[number]
> = {
commandNames: ['failure', 'fail'],
description: `「〜〜〜」\n「わかりました。それは一般に失敗と言います、ありがとうございます」\n* \`-k <失敗部分> <説明>\` で失敗部分を変更できます。 \n [元ネタ](${sourceLink})`,
flagsKeys: [],
optionsKeys: failureOption,
requiredPositionalKeys: positionalKeys,
errorMessage:
'「わかりました。それは一般に引数エラーと言います、ありがとうございます」',
generate(args) {
const failureArgs = {
explanation: args.body,
explanation: args.requiredPositionals.explanation,
impoliteness: args.options.k ?? '失敗'
};
return `「${failureArgs.explanation}」\n「わかりました。それは一般に${failureArgs.impoliteness}と言います、ありがとうございます」`;
Expand Down
13 changes: 9 additions & 4 deletions src/service/command/meme/hukueki.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { MemeTemplate } from '../../../model/meme-template.js';

export const hukueki: MemeTemplate<never, never> = {
const positionalKeys = ['badThing'] as const;

export const hukueki: MemeTemplate<
never,
never,
(typeof positionalKeys)[number]
> = {
commandNames: ['hukueki'],
description: 'ねぇ、将来何してるだろうね\n〜はしてないといいね\n困らないでよ',
flagsKeys: [],
optionsKeys: [],
requiredPositionalKeys: positionalKeys,
errorMessage: '服役できなかった。',
generate(args) {
return `ねぇ、将来何してるだろうね\n${args.body}はしてないといいね\n困らないでよ`;
return `ねぇ、将来何してるだろうね\n${args.requiredPositionals.badThing}はしてないといいね\n困らないでよ`;
}
};
13 changes: 9 additions & 4 deletions src/service/command/meme/kenjou.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { MemeTemplate } from '../../../model/meme-template.js';

export const kenjou: MemeTemplate<never, never> = {
const positionalKeys = ['title'] as const;

export const kenjou: MemeTemplate<
never,
never,
(typeof positionalKeys)[number]
> = {
commandNames: ['kenjou'],
description:
'[健常者エミュレーター](https://healthy-person-emulator.memo.wiki/)の構文ジェネレーター。\n健常者エミュレーターWikiにありそうなタイトルを指定すればうまくいきます。',
flagsKeys: [],
optionsKeys: [],
requiredPositionalKeys: positionalKeys,
errorMessage:
'はらちょのミーム機能を使うときは引数を忘れない方がいい - 健常者エミュレータ事例集Wiki',
generate(args) {
return `${args.body} - 健常者エミュレータ事例集Wiki`;
return `${args.requiredPositionals.title} - 健常者エミュレータ事例集Wiki`;
}
};
16 changes: 10 additions & 6 deletions src/service/command/meme/koume.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import type { MemeTemplate } from '../../../model/meme-template.js';

export const koume: MemeTemplate<never, never> = {
const positionalKeys = ['expected', 'actual'] as const;

export const koume: MemeTemplate<
never,
never,
(typeof positionalKeys)[number]
> = {
commandNames: ['koume'],
description: '〜と思ったら〜♪\n\n〜でした〜♪\n\n引数は2つ必要です。',
flagsKeys: [],
optionsKeys: [],
requiredPositionalKeys: positionalKeys,
errorMessage:
'MEMEを表示しようと思ったら〜♪ 引数が足りませんでした〜♪ チクショー!!',
generate(args) {
const [option1, option2] = args.body.split(' ');
generate({ requiredPositionals: { expected, actual } }) {
// Reason: 構文とコウメ太夫に敬意を払い、元ネタを尊重することから全角スペースを使用したいのでeslintの警告をBANします。
// eslint-disable-next-line no-irregular-whitespace
return `${option1}と思ったら〜♪\n\n${option2}でした〜♪\n\nチクショー!! #まいにちチクショー`;
return `${expected}と思ったら〜♪\n\n${actual}でした〜♪\n\nチクショー!! #まいにちチクショー`;
}
};
Loading

0 comments on commit 56b2316

Please sign in to comment.