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

feat: スタンプ作成ログを送信する機能の実装 #1258

Merged
merged 10 commits into from
Feb 2, 2024
1 change: 1 addition & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ it('use case of hukueki', async () => {
- `"VOICE_ROOM"` - VC 関連の機能
- `"ROLE"` - ロール関連の機能
- `"EMOJI"` - 絵文字関連の機能
- `"STICKER"` - スタンプ関連の機能
- `"SLASH_COMMAND"` - スラッシュコマンド関連の機能 (指定しない場合はスラッシュコマンドは登録されず、メッセージコマンド形式だけになります。)
- `"MEMBER"` - メンバー関連の機能

Expand Down
2 changes: 1 addition & 1 deletion packages/bot/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ ENTRANCE_CHANNEL_ID=
APPLICATION_ID=
GUILD_ID=
PREFIX=!
FEATURE=MESSAGE_CREATE,MESSAGE_UPDATE,COMMAND,VOICE_ROOM,ROLE,EMOJI,SLASH_COMMAND,MEMBER
FEATURE=MESSAGE_CREATE,MESSAGE_UPDATE,COMMAND,VOICE_ROOM,ROLE,EMOJI,STICKER,SLASH_COMMAND,MEMBER
29 changes: 29 additions & 0 deletions packages/bot/src/adaptor/proxy/sticker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Client } from 'discord.js';

import type { Snowflake } from '../../model/id.js';
import type { StickerEventProvider } from '../../runner/sticker.js';
import type { StickerData } from '../../service/sticker-log.js';

export type StickerHandler<S> = (sticker: S) => Promise<void>;

export class StickerProxy implements StickerEventProvider<StickerData> {
constructor(private readonly client: Client) {}

onStickerCreate(handler: StickerHandler<StickerData>): void {
this.client.on('stickerCreate', async (sticker) => {
const author = await sticker.fetchUser();
if (!author) {
throw new Error('Failed to fetch sticker author');
}

await handler({
name: sticker.name,
imageUrl: sticker.url,
id: sticker.id as Snowflake,
authorId: author.id as Snowflake,
description: sticker.description ?? '説明無し',
tags: sticker.tags ?? '関連絵文字無し'
});
});
}
}

Check warning on line 29 in packages/bot/src/adaptor/proxy/sticker.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/adaptor/proxy/sticker.ts#L2-L29

Added lines #L2 - L29 were not covered by tests
46 changes: 46 additions & 0 deletions packages/bot/src/runner/sticker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export type StickerEvent = 'CREATE' | 'UPDATE' | 'DELETE';

export interface StickerEventResponder<S> {
on(event: StickerEvent, sticker: S): Promise<void>;
}

export const composeStickerEventResponders = <S>(
...responders: readonly StickerEventResponder<S>[]
): StickerEventResponder<S> => ({
async on(event, sticker) {
await Promise.all(
responders.map((responder) => responder.on(event, sticker))
);
}
});

export interface StickerEventProvider<S> {
onStickerCreate(handler: (sticker: S) => Promise<void>): void;
}

export class StickerResponseRunner<
S,
R extends StickerEventResponder<S> = StickerEventResponder<S>
> {
constructor(provider: StickerEventProvider<S>) {
provider.onStickerCreate((sticker) => this.triggerEvent('CREATE', sticker));
}

private async triggerEvent(event: StickerEvent, sticker: S): Promise<void> {
try {
await Promise.all(this.responders.map((res) => res.on(event, sticker)));
} catch (e) {
console.error(e);
}
}

private responders: R[] = [];

addResponder(responder: R) {
this.responders.push(responder);
}

getResponders(): readonly R[] {
return this.responders;
}
}

Check warning on line 46 in packages/bot/src/runner/sticker.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/runner/sticker.ts#L2-L46

Added lines #L2 - L46 were not covered by tests
8 changes: 8 additions & 0 deletions packages/bot/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
} from '../adaptor/index.js';
import { DiscordCommandProxy } from '../adaptor/proxy/command.js';
import { memberProxy } from '../adaptor/proxy/member.js';
import { StickerProxy } from '../adaptor/proxy/sticker.js';

Check warning on line 35 in packages/bot/src/server/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/server/index.ts#L35

Added line #L35 was not covered by tests
import { loadSchedule } from '../adaptor/signal-schedule.js';
import { GenVersionFetcher } from '../adaptor/version/fetch.js';
import type { Snowflake } from '../model/id.js';
Expand All @@ -45,6 +46,7 @@
VoiceRoomResponseRunner
} from '../runner/index.js';
import { MemberResponseRunner } from '../runner/member.js';
import { StickerResponseRunner } from '../runner/sticker.js';

Check warning on line 49 in packages/bot/src/server/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/server/index.ts#L49

Added line #L49 was not covered by tests
import type { GyokuonAssetKey } from '../service/command/gyokuon.js';
import type { KaereMusicKey } from '../service/command/kaere.js';
import type { AssetKey } from '../service/command/party.js';
Expand All @@ -55,6 +57,7 @@
allMessageEventResponder,
allMessageUpdateEventResponder,
allRoleResponder,
allStickerResponder,

Check warning on line 60 in packages/bot/src/server/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/server/index.ts#L60

Added line #L60 was not covered by tests
registerAllCommandResponder
} from '../service/index.js';
import { startTimeSignal } from '../service/time-signal.js';
Expand Down Expand Up @@ -235,6 +238,11 @@
emojiRunner.addResponder(allEmojiResponder(standardOutput));
}

const stickerRunner = new StickerResponseRunner(new StickerProxy(client));
if (features.includes('STICKER')) {
stickerRunner.addResponder(allStickerResponder(standardOutput));
}

Check warning on line 245 in packages/bot/src/server/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/server/index.ts#L241-L245

Added lines #L241 - L245 were not covered by tests
const memberRunner = new MemberResponseRunner();
if (features.includes('MEMBER')) {
memberRunner.addResponder(allMemberResponder(entranceOutput));
Expand Down
5 changes: 5 additions & 0 deletions packages/bot/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
composeRoleEventResponders
} from '../runner/index.js';
import { composeMemberEventResponders } from '../runner/member.js';
import { composeStickerEventResponders } from '../runner/sticker.js';

Check warning on line 9 in packages/bot/src/service/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/service/index.ts#L9

Added line #L9 was not covered by tests
import {
type BoldItalicCop,
BoldItalicCopReporter
Expand All @@ -28,6 +29,7 @@
type RoleManager
} from './kawaemon-has-all-roles.js';
import type { EntranceOutput, StandardOutput } from './output.js';
import { StickerLog } from './sticker-log.js';

Check warning on line 32 in packages/bot/src/service/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/service/index.ts#L32

Added line #L32 was not covered by tests
import { WelcomeMessage } from './welcome-message.js';

const stfuIgnorePredicate = (content: string): boolean => content === '!stfu';
Expand Down Expand Up @@ -67,5 +69,8 @@
export const allEmojiResponder = (output: StandardOutput) =>
composeEmojiEventResponders(new EmojiLog(output));

export const allStickerResponder = (output: StandardOutput) =>
composeStickerEventResponders(new StickerLog(output));

Check warning on line 74 in packages/bot/src/service/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/bot/src/service/index.ts#L72-L74

Added lines #L72 - L74 were not covered by tests
export const allMemberResponder = (output: EntranceOutput) =>
composeMemberEventResponders(new WelcomeMessage(output));
51 changes: 51 additions & 0 deletions packages/bot/src/service/sticker-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, it, vi } from 'vitest';

import type { Snowflake } from '../model/id.js';
import { StickerLog } from './sticker-log.js';

it('create sticker', async () => {
const sendEmbed = vi.fn(() => Promise.resolve());
const responder = new StickerLog({ sendEmbed });
await responder.on('CREATE', {
name: 'なないミーム',
authorId: '596121630930108426' as Snowflake,
id: '723382133388738601' as Snowflake,
imageUrl: 'https://cdn.discordapp.com/embed/avatars/0.png',
description: 'チュピチュピチャパチャパwwwドゥビドゥビダバダバwww',
tags: '🐱'
});

expect(sendEmbed).toHaveBeenCalledWith({
title: 'スタンプ警察',
description: `<@596121630930108426> が **なないミーム** を作成しました`,
thumbnail: {
url: 'https://cdn.discordapp.com/embed/avatars/0.png'
},
footer: `ID: 723382133388738601`,
fields: [
{
name: '説明',
value: 'チュピチュピチャパチャパwwwドゥビドゥビダバダバwww'
},
{
name: '関連絵文字',
value: '🐱'
}
]
});
});

it('does not call non-CREATE event', async () => {
const sendEmbed = vi.fn(() => Promise.resolve());
const responder = new StickerLog({ sendEmbed });
await responder.on('UPDATE', {
name: 'なないミーム',
authorId: '596121630930108426' as Snowflake,
id: '723382133388738601' as Snowflake,
imageUrl: 'https://cdn.discordapp.com/embed/avatars/0.png',
description: 'チュピチュピチャパチャパwwwドゥビドゥビダバダバwww',
tags: '🐱'
});

expect(sendEmbed).not.toHaveBeenCalled();
});
58 changes: 58 additions & 0 deletions packages/bot/src/service/sticker-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Snowflake } from '../model/id.js';
import type { StickerEvent, StickerEventResponder } from '../runner/sticker.js';
import type { StandardOutput } from './output.js';

export interface StickerData {
/**
* 初期名が自動で割り当てられる絵文字とは異なり,
* スタンプは名前を決めてから登録するので名前がuniqueとして有効
*/
name: string; // 絵文字名
imageUrl: string; // 絵文字の画像URL
id: Snowflake; // 絵文字ID
authorId: Snowflake; // 絵文字を作成したユーザーID
description: string; // 絵文字の説明
tags: string; // 絵文字のタグ (Autocomplete用)
}

export class StickerLog implements StickerEventResponder<StickerData> {
constructor(private readonly output: StandardOutput) {}

async on(event: StickerEvent, sticker: StickerData): Promise<void> {
if (event !== 'CREATE') {
return;
}

await this.output.sendEmbed(this.buildEmbed(sticker));
}

private buildEmbed({
name,
imageUrl,
id,
authorId,
description,
tags
}: StickerData) {
const fields = [
{
name: '説明',
value: description
},
{
name: '関連絵文字',
value: tags
}
];

return {
title: 'スタンプ警察',
description: `<@${authorId}> が **${name}** を作成しました`,
thumbnail: {
url: imageUrl
},
footer: `ID: ${id}`,
fields
};
}
}
1 change: 1 addition & 0 deletions packages/docs/src/pages/references/features/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"delete-diff": "削除 Diff",
"edit-diff": "編集 Diff",
"emoji-create-log": "絵文字作成ログ",
"sticker-create-log": "スタンプ作成ログ",
"kawaemon": "Kawaemon has given a new role",
"typo": "今日の Typo",
"vc-diff": "VC 接続 Diff",
Expand Down
21 changes: 21 additions & 0 deletions packages/docs/src/pages/references/features/sticker-create-log.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FeatureBadge } from '../../../molecules/feature-badge';
import { VersionBadge } from '../../../molecules/version-badge';

# スタンプ作成ログ

<FeatureBadge>ログ</FeatureBadge>,<VersionBadge>v1.51.0</VersionBadge>

---

スタンプを作成するとログを送信します。

作成されたスタンプについて、以下の情報を表示します。

### 表示される情報
m1sk9 marked this conversation as resolved.
Show resolved Hide resolved

- 作成者
- 名前
- 画像
- 説明
- 関連した絵文字
- Autocomplete用。絵文字を送信すると候補として表示されます。
Loading