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 ?? '関連絵文字無し'
});
});
}
}
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;
}
}
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 @@ import {
} 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';
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 @@ import {
VoiceRoomResponseRunner
} from '../runner/index.js';
import { MemberResponseRunner } from '../runner/member.js';
import { StickerResponseRunner } from '../runner/sticker.js';
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 @@ import {
allMessageEventResponder,
allMessageUpdateEventResponder,
allRoleResponder,
allStickerResponder,
registerAllCommandResponder
} from '../service/index.js';
import { startTimeSignal } from '../service/time-signal.js';
Expand Down Expand Up @@ -235,6 +238,11 @@ if (features.includes('EMOJI')) {
emojiRunner.addResponder(allEmojiResponder(standardOutput));
}

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

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 @@ import {
composeRoleEventResponders
} from '../runner/index.js';
import { composeMemberEventResponders } from '../runner/member.js';
import { composeStickerEventResponders } from '../runner/sticker.js';
import {
type BoldItalicCop,
BoldItalicCopReporter
Expand All @@ -28,6 +29,7 @@ import {
type RoleManager
} from './kawaemon-has-all-roles.js';
import type { EntranceOutput, StandardOutput } from './output.js';
import { StickerLog } from './sticker-log.js';
import { WelcomeMessage } from './welcome-message.js';

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

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

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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { VersionBadge } from '../../../molecules/version-badge';

</Callout>

### 早すぎる削除
## 早すぎる削除

<VersionBadge>v1.50.0</VersionBadge> から利用可能

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>

---

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

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

## 表示される情報

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