diff --git a/.env.example b/.env.example index 47b0cab2..b2338a9f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ DISCORD_TOKEN= +MAIN_CHANNEL_ID= diff --git a/.gitignore b/.gitignore index 5391269e..0752c5ff 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,6 @@ dist # ts build/ + +# WebStorm +.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603f..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 1f0d6674..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index d8e95616..00000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml deleted file mode 100644 index b3820067..00000000 --- a/.idea/git_toolbox_prj.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 03d9549e..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208fb..00000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml deleted file mode 100644 index 541945bb..00000000 --- a/.idea/jsLinters/eslint.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 197fcc23..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/oreorebot2.iml b/.idea/oreorebot2.iml deleted file mode 100644 index 0c8867d7..00000000 --- a/.idea/oreorebot2.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml deleted file mode 100644 index 727b8b53..00000000 --- a/.idea/prettier.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/adaptor/discord-output.ts b/src/adaptor/discord-output.ts new file mode 100644 index 00000000..3c2e4f9e --- /dev/null +++ b/src/adaptor/discord-output.ts @@ -0,0 +1,53 @@ +import { Client, MessageEmbed } from 'discord.js'; +import type { EmbedMessage } from '../model/embed-message'; +import type { StandardOutput } from '../service/voice-diff'; + +export class DiscordOutput implements StandardOutput { + constructor( + private readonly client: Client, + private readonly channelId: string + ) {} + + async sendEmbed(embed: EmbedMessage): Promise { + const channel = await this.client.channels.fetch(this.channelId); + if (!channel || !channel.isText()) { + throw new Error(`the channel (${this.channelId}) is not text channel`); + } + + const made = buildEmbed(embed); + await channel.send({ + embeds: [made] + }); + } +} + +function buildEmbed(embed: EmbedMessage) { + const makeEmbed = new MessageEmbed(); + const { title, color, description, fields, url, footer, thumbnail, author } = + embed; + if (author) { + makeEmbed.setAuthor({ name: author.name, iconURL: author.iconUrl }); + } + if (color) { + makeEmbed.setColor(color); + } + if (description) { + makeEmbed.setDescription(description); + } + if (fields) { + makeEmbed.setFields(fields); + } + if (footer) { + makeEmbed.setFooter({ text: footer }); + } + if (title) { + makeEmbed.setTitle(title); + } + if (url) { + makeEmbed.setURL(url); + } + if (thumbnail) { + makeEmbed.setThumbnail(thumbnail.url); + } + return makeEmbed; +} diff --git a/src/adaptor/discord-participant.ts b/src/adaptor/discord-participant.ts new file mode 100644 index 00000000..24ed4931 --- /dev/null +++ b/src/adaptor/discord-participant.ts @@ -0,0 +1,22 @@ +import type { VoiceChannelParticipant } from '../service/voice-diff'; +import type { VoiceState } from 'discord.js'; + +export class DiscordParticipant implements VoiceChannelParticipant { + constructor(private voiceState: VoiceState) {} + + get userName(): string { + return this.voiceState.member?.displayName ?? '名無し'; + } + + get userAvatar(): string { + const avatarURL = this.voiceState.member?.displayAvatarURL(); + if (!avatarURL) { + throw new Error('アバターが取得できませんでした。'); + } + return avatarURL; + } + + get channelName(): string { + return this.voiceState.channel?.name ?? '名無し'; + } +} diff --git a/src/adaptor/index.ts b/src/adaptor/index.ts index a3541c13..255a88b0 100644 --- a/src/adaptor/index.ts +++ b/src/adaptor/index.ts @@ -8,3 +8,5 @@ export * from './mock-voice'; export * from './random'; export * from './transformer'; export * from './voice-room-proxy'; +export * from './discord-participant'; +export * from './discord-output'; diff --git a/src/adaptor/voice-room-proxy.ts b/src/adaptor/voice-room-proxy.ts index f0fd0fdf..f7655f06 100644 --- a/src/adaptor/voice-room-proxy.ts +++ b/src/adaptor/voice-room-proxy.ts @@ -10,11 +10,14 @@ type ObserveExpectation = 'ChangingIntoFalsy' | 'ChangingIntoTruthy' | 'All'; * @class VoiceRoomProxy * @implements {VoiceRoomEventProvider} */ -export class VoiceRoomProxy implements VoiceRoomEventProvider { - constructor(private readonly client: Client) {} +export class VoiceRoomProxy implements VoiceRoomEventProvider { + constructor( + private readonly client: Client, + private readonly map: (voiceState: VoiceState) => V + ) {} private registerHandler( - handler: (v: VoiceState) => Promise, + handler: (v: V) => Promise, toObserve: keyof VoiceState, expected: ObserveExpectation ): void { @@ -28,32 +31,32 @@ export class VoiceRoomProxy implements VoiceRoomEventProvider { (expected === 'ChangingIntoTruthy' && !!newState[toObserve]) || expected === 'All') ) { - await handler(newState); + await handler(this.map(newState)); } }); } - onJoin(handler: (voiceState: VoiceState) => Promise): void { + onJoin(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'channelId', 'ChangingIntoTruthy'); } - onLeave(handler: (voiceState: VoiceState) => Promise): void { + onLeave(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'channelId', 'ChangingIntoFalsy'); } - onMute(handler: (voiceState: VoiceState) => Promise): void { + onMute(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'mute', 'ChangingIntoTruthy'); } - onDeafen(handler: (voiceState: VoiceState) => Promise): void { + onDeafen(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'deaf', 'ChangingIntoTruthy'); } - onUnmute(handler: (voiceState: VoiceState) => Promise): void { + onUnmute(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'mute', 'ChangingIntoFalsy'); } - onUndeafen(handler: (voiceState: VoiceState) => Promise): void { + onUndeafen(handler: (voiceState: V) => Promise): void { this.registerHandler(handler, 'deaf', 'ChangingIntoFalsy'); } } diff --git a/src/server/index.ts b/src/server/index.ts index c894ee5b..84e89f82 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,5 +1,7 @@ import { ActualClock, + DiscordOutput, + DiscordParticipant, DiscordVoiceConnectionFactory, InMemoryReservationRepository, InMemoryTypoRepository, @@ -9,14 +11,17 @@ import { MathRandomGenerator, MessageProxy, MessageUpdateProxy, - DiscordVoiceRoomController + DiscordVoiceRoomController, + VoiceRoomProxy } from '../adaptor'; import { Client, Intents, version } from 'discord.js'; import { MessageResponseRunner, MessageUpdateResponseRunner, - ScheduleRunner + ScheduleRunner, + VoiceRoomResponseRunner } from '../runner'; +import { VoiceChannelParticipant, VoiceDiff } from '../service/voice-diff'; import { allCommandResponder, allMessageEventResponder, @@ -30,7 +35,8 @@ import { join } from 'path'; dotenv.config(); const token = process.env.DISCORD_TOKEN; -if (!token) { +const mainChannelId = process.env.MAIN_CHANNEL_ID; +if (!token || !mainChannelId) { throw new Error( 'Error> Failed to start. You did not specify any environment variables.' ); @@ -101,6 +107,15 @@ commandRunner.addResponder( ) ); +const provider = new VoiceRoomProxy( + client, + (voiceState) => new DiscordParticipant(voiceState) +); +const voiceRunner = new VoiceRoomResponseRunner(provider); +voiceRunner.addResponder( + new VoiceDiff(new DiscordOutput(client, mainChannelId)) +); + client.once('ready', () => { readyLog(client); }); diff --git a/src/service/voice-diff.test.ts b/src/service/voice-diff.test.ts new file mode 100644 index 00000000..feed1fda --- /dev/null +++ b/src/service/voice-diff.test.ts @@ -0,0 +1,47 @@ +import { StandardOutput, VoiceDiff } from './voice-diff'; + +test('use case of VoiceDiff', async () => { + const outputJoin: StandardOutput = { + sendEmbed(message) { + expect(message).toStrictEqual({ + title: 'めるが限界に入りました', + description: '何かが始まる予感がする。', + color: 0x1e63e9, + author: { name: 'はらちょからのお知らせ' }, + thumbnail: { + url: 'https://cdn.discordapp.com/avatars/586824421470109716/9eb541e567f0ce82d34e55a37213c524.webp' + } + }); + return Promise.resolve(); + } + }; + const outputLeave: StandardOutput = { + sendEmbed(message) { + expect(message).toStrictEqual({ + title: 'めるが限界から抜けました', + description: 'あいつは良い奴だったよ...', + color: 0x1e63e9, + author: { name: 'はらちょからのお知らせ' }, + thumbnail: { + url: 'https://cdn.discordapp.com/avatars/586824421470109716/9eb541e567f0ce82d34e55a37213c524.webp' + } + }); + return Promise.resolve(); + } + }; + + const responderJoin = new VoiceDiff(outputJoin); + const responderLeave = new VoiceDiff(outputLeave); + await responderJoin.on('JOIN', { + userName: 'める', + channelName: '限界', + userAvatar: + 'https://cdn.discordapp.com/avatars/586824421470109716/9eb541e567f0ce82d34e55a37213c524.webp' + }); + await responderLeave.on('LEAVE', { + userName: 'める', + channelName: '限界', + userAvatar: + 'https://cdn.discordapp.com/avatars/586824421470109716/9eb541e567f0ce82d34e55a37213c524.webp' + }); +}); diff --git a/src/service/voice-diff.ts b/src/service/voice-diff.ts new file mode 100644 index 00000000..bbaa6c00 --- /dev/null +++ b/src/service/voice-diff.ts @@ -0,0 +1,46 @@ +import { VoiceRoomEvent, VoiceRoomEventResponder } from '../runner'; +import { EmbedMessage } from '../model/embed-message'; + +export interface VoiceChannelParticipant { + userName: string; + userAvatar: string; + channelName: string; +} + +export interface StandardOutput { + sendEmbed(embed: EmbedMessage): Promise; +} + +export class VoiceDiff + implements VoiceRoomEventResponder +{ + constructor(private readonly stdout: StandardOutput) {} + + async on( + event: VoiceRoomEvent, + voiceState: VoiceChannelParticipant + ): Promise { + if (event === 'JOIN') { + // VoiceChannel 入室時 + const { userName, userAvatar, channelName } = voiceState; + await this.stdout.sendEmbed({ + title: userName + 'が' + channelName + 'に入りました', + description: '何かが始まる予感がする。', + color: 0x1e63e9, + author: { name: 'はらちょからのお知らせ' }, + thumbnail: { url: userAvatar } + }); + } + if (event === 'LEAVE') { + // VoiceChannel 退出時 + const { userName, userAvatar, channelName } = voiceState; + await this.stdout.sendEmbed({ + title: userName + 'が' + channelName + 'から抜けました', + description: 'あいつは良い奴だったよ...', + color: 0x1e63e9, + author: { name: 'はらちょからのお知らせ' }, + thumbnail: { url: userAvatar } + }); + } + } +} diff --git a/yarn.lock b/yarn.lock index 88d02eca..acbd46c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1736,9 +1736,9 @@ discord-api-types@^0.26.0, discord-api-types@^0.26.1: integrity sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ== discord.js@^13.5.0: - version "13.5.0" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.5.0.tgz#f9ca9e629f2de0fb138e8c916fa93e40d70631f5" - integrity sha512-K+ZcB0f+wA1ZzDhz3hlaAi4Ap7jSvVEUZ+U29T4DMoiNNUv22F4vu1byrOq8GyyLLDFiZ3iSudea0MvSHu3fQA== + version "13.6.0" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.6.0.tgz#d8a8a591dbf25cbcf9c783d5ddf22c4694860475" + integrity sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g== dependencies: "@discordjs/builders" "^0.11.0" "@discordjs/collection" "^0.4.0"