diff --git a/src/infra/repository/nita.js b/src/infra/repository/nita.js index 8f60997..db52db2 100644 --- a/src/infra/repository/nita.js +++ b/src/infra/repository/nita.js @@ -109,5 +109,22 @@ export const postgresNitaRepository = () => { `; return Number(results[0].count); }, + async selectRival(executorDiscordId, rivalDiscordId) { + const results = await sql` + SELECT + nita1.track_code, + nita1.milliseconds AS executor_milliseconds, + nita2.milliseconds AS rival_milliseconds + FROM (SELECT * FROM nita WHERE discord_user_id = ${executorDiscordId}) AS nita1 + INNER JOIN (SELECT * FROM nita WHERE discord_user_id = ${rivalDiscordId}) AS nita2 + ON nita1.track_code = nita2.track_code + ORDER BY nita1.milliseconds - nita2.milliseconds + `; + return results.map((row) => ({ + trackCode: row.track_code, + executorMilliseconds: row.executor_milliseconds, + rivalMilliseconds: row.rival_milliseconds, + })); + }, }; }; diff --git a/src/slash-command/rival.js b/src/slash-command/rival.js new file mode 100644 index 0000000..91a7e7e --- /dev/null +++ b/src/slash-command/rival.js @@ -0,0 +1,73 @@ +// @ts-check +import { SlashCommandBuilder } from 'discord.js'; +import { searchTrack } from '../const/track.js'; +import { displayMilliseconds } from '../util/time.js'; + +/** @type { import('../types').SlashCommand } */ +export default { + data: new SlashCommandBuilder() + .setName('rival') + .setDescription('サーバー内のユーザーと記録を比較します') + .addUserOption((option) => + option.setName('rival').setDescription('比較対象').setRequired(true), + ), + execute: async (interaction, nitaRepository) => { + if (!interaction.guild) { + if (!interaction.inGuild()) { + throw new Error( + 'サーバー内で実行してください。rivalコマンドはDMやグループDMでは実行できません。', + ); + } + // https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/structures/BaseInteraction.js#L180-L182 + // https://github.com/moeyashi/discord-mk8d-nita-record/issues/78 + // https://scrapbox.io/discordjs-japan/%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC%E3%81%8B%E3%82%89%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B + // interaction.guildはキャッシュから取得しているしているため、interaction.guildがfalsyでinteraction.guildIdがtruthyの場合はfetchして取得する + await interaction.client.guilds.fetch(interaction.guildId); + if (!interaction.guild) { + throw new Error( + '不明なエラー:サーバーが見つかりませんでした。スクショして連絡してもらえたらありがたいです!', + ); + } + } + + const userQuery = interaction.options.getUser('rival'); + if (!userQuery) { + throw new Error('比較対象を指定してください'); + } + + await interaction.deferReply(); + + const tracks = await nitaRepository.selectRival( + interaction.user.id, + userQuery.id, + ); + /** @type {import('discord.js').InteractionReplyOptions} */ + const res = { + content: `VS ${userQuery.displayName || userQuery.username}`, + }; + if (tracks.length === 0) { + res.content = res.content + '\n\n記録がありません'; + } else { + /** @type {import('discord.js').APIEmbed[]} */ + const embeds = []; + res.embeds = tracks.reduce( + (pv, { trackCode, executorMilliseconds, rivalMilliseconds }, i) => { + const track = searchTrack(trackCode); + if (i % 25 === 0) { + pv.push({ + fields: [], + }); + } + const diffRival = executorMilliseconds - rivalMilliseconds; + pv[pv.length - 1].fields?.push({ + name: track?.trackName || '', + value: `${displayMilliseconds(executorMilliseconds)} VS ${displayMilliseconds(rivalMilliseconds)} ${diffRival / 1000}秒`, + }); + return pv; + }, + embeds, + ); + } + await interaction.followUp(res); + }, +}; diff --git a/src/types.d.ts b/src/types.d.ts index 9eebee8..bd7e1d2 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -39,6 +39,10 @@ export type NitaRepository = { trackCode: string, discordMembers: GuildMember[] ) => Promise; + selectRival: ( + executorDiscordId: string, + rivalDiscordId: string + ) => Promise<{ trackCode: string; executorMilliseconds: number; rivalMilliseconds: number; }[]>; }; // 参考 https://typescriptbook.jp/reference/functions/overload-functions#%E3%82%A2%E3%83%AD%E3%83%BC%E9%96%A2%E6%95%B0%E3%81%A8%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AD%E3%83%BC%E3%83%89