diff --git a/src/app.sagas.ts b/src/app.sagas.ts index 2486b479..feb33d07 100644 --- a/src/app.sagas.ts +++ b/src/app.sagas.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { type ICommand, Saga, ofType } from '@nestjs/cqrs'; import { Observable, filter, map, mergeMap } from 'rxjs'; +import { BlacklistSearchCommand } from './blacklist/blacklist.commands.js'; import { RemoveRolesCommand } from './signup/commands/signup.commands.js'; import { SignupApprovedEvent, @@ -23,9 +24,10 @@ class AppSagas { handleSignupCreated = (event$: Observable): Observable => event$.pipe( ofType(SignupCreatedEvent), - map( - ({ signup, guildId }) => new SendSignupReviewCommand(signup, guildId), - ), + mergeMap(({ signup, guildId }) => [ + new SendSignupReviewCommand(signup, guildId), + new BlacklistSearchCommand(signup, guildId), + ]), ); /** diff --git a/src/blacklist/blacklist.commands.ts b/src/blacklist/blacklist.commands.ts index 0009e06a..611b6ea9 100644 --- a/src/blacklist/blacklist.commands.ts +++ b/src/blacklist/blacklist.commands.ts @@ -1,4 +1,5 @@ import type { ChatInputCommandInteraction } from 'discord.js'; +import type { SignupDocument } from '../firebase/models/signup.model.js'; import type { DiscordCommand } from '../slash-commands/slash-commands.interfaces.js'; export class BlacklistAddCommand implements DiscordCommand { @@ -18,3 +19,10 @@ export class BlacklistDisplayCommand implements DiscordCommand { public readonly interaction: ChatInputCommandInteraction<'raw' | 'cached'>, ) {} } + +export class BlacklistSearchCommand { + constructor( + public readonly signup: SignupDocument, + public readonly guildId: string, + ) {} +} diff --git a/src/blacklist/blacklist.module.ts b/src/blacklist/blacklist.module.ts index cbf62dc9..9bb17278 100644 --- a/src/blacklist/blacklist.module.ts +++ b/src/blacklist/blacklist.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { DiscordModule } from '../discord/discord.module.js'; import { FirebaseModule } from '../firebase/firebase.module.js'; +import { BlacklistSearchCommandHandler } from './commands/handlers/blacklist-search.command-handler.js'; import { BlacklistUpdatedEventHandler } from './events/handlers/blacklist-updated.event-handler.js'; import { BlacklistAddCommandHandler } from './subcommands/add/blacklist-add.command-handler.js'; import { BlacklistDisplayCommandHandler } from './subcommands/display/blacklist-display.command-handler.js'; @@ -17,6 +18,7 @@ import { BlacklistRemoveCommandHandler } from './subcommands/remove/blacklist-re BlacklistRemoveCommandHandler, BlacklistDisplayCommandHandler, BlacklistUpdatedEventHandler, + BlacklistSearchCommandHandler, ], }) class BlacklistModule {} diff --git a/src/blacklist/blacklist.utils.ts b/src/blacklist/blacklist.utils.ts index b9b59198..ec3fe167 100644 --- a/src/blacklist/blacklist.utils.ts +++ b/src/blacklist/blacklist.utils.ts @@ -1,4 +1,8 @@ -import type { CacheType, ChatInputCommandInteraction } from 'discord.js'; +import type { + APIEmbedField, + CacheType, + ChatInputCommandInteraction, +} from 'discord.js'; import type { BlacklistDocument } from '../firebase/models/blacklist.model.js'; export function getDiscordId( @@ -24,3 +28,16 @@ export function getDisplayName({ return characterName!; } + +export function createBlacklistEmbedFields({ + characterName, + discordId, + reason, +}: BlacklistDocument): APIEmbedField[] { + const displayName = getDisplayName({ characterName, discordId }); + return [ + { name: 'Player', value: displayName, inline: true }, + { name: 'Reason', value: reason, inline: true }, + { name: '\u200b', value: '\u200b', inline: true }, + ]; +} diff --git a/src/blacklist/commands/handlers/blacklist-search.command-handler.ts b/src/blacklist/commands/handlers/blacklist-search.command-handler.ts new file mode 100644 index 00000000..87264ff4 --- /dev/null +++ b/src/blacklist/commands/handlers/blacklist-search.command-handler.ts @@ -0,0 +1,95 @@ +import { Logger } from '@nestjs/common'; +import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; +import { EmbedBuilder } from 'discord.js'; +import { EMPTY, EmptyError, catchError, lastValueFrom, mergeMap } from 'rxjs'; +import { getMessageLink } from '../../../discord/discord.consts.js'; +import { DiscordService } from '../../../discord/discord.service.js'; +import { BlacklistCollection } from '../../../firebase/collections/blacklist-collection.js'; +import { SettingsCollection } from '../../../firebase/collections/settings-collection.js'; +import type { BlacklistDocument } from '../../../firebase/models/blacklist.model.js'; +import type { SignupDocument } from '../../../firebase/models/signup.model.js'; +import { BlacklistSearchCommand } from '../../blacklist.commands.js'; +import { createBlacklistEmbedFields } from '../../blacklist.utils.js'; + +@CommandHandler(BlacklistSearchCommand) +class BlacklistSearchCommandHandler + implements ICommandHandler +{ + private readonly logger = new Logger(BlacklistSearchCommandHandler.name); + constructor( + private readonly settingsCollection: SettingsCollection, + private readonly blacklistCollection: BlacklistCollection, + private readonly discordService: DiscordService, + ) {} + + async execute({ signup, guildId }: BlacklistSearchCommand) { + const settings = await this.settingsCollection.getSettings(guildId); + if (!settings?.modChannelId) { + this.logger.warn('No mod channel set for guild ${guildId}'); + return; + } + + const { modChannelId } = settings; + + // search to see if the signup is in the blacklist + const matches$ = this.blacklistCollection.search({ + guildId, + discordId: signup.discordId, + characterName: signup.character, + }); + + const pipeline$ = matches$.pipe( + mergeMap(async (entry) => { + const channel = await this.discordService.getTextChannel({ + guildId, + channelId: modChannelId, + }); + + const embed = this.createBlacklistEmbed(entry, signup, { + guildId, + modChannelId, + }); + + return channel?.send({ embeds: [embed] }); + }), + catchError((err) => { + if (err instanceof EmptyError) { + return EMPTY; + } + throw err; + }), + ); + + await lastValueFrom(pipeline$, { defaultValue: undefined }); + } + + private createBlacklistEmbed( + entry: BlacklistDocument, + { reviewMessageId }: SignupDocument, + { guildId, modChannelId }: { guildId: string; modChannelId: string }, + ) { + const fields = createBlacklistEmbedFields(entry); + + if (reviewMessageId) { + fields.push({ + name: 'Signup', + value: getMessageLink({ + guildId, + channelId: modChannelId, + id: reviewMessageId, + }), + inline: true, + }); + } + + const embed = new EmbedBuilder() + .setTitle('Blacklisted User Detected') + .setDescription('A blacklisted user has been detected signing up') + .addFields(fields) + .setTimestamp(); + + return embed; + } +} + +export { BlacklistSearchCommandHandler }; diff --git a/src/blacklist/subcommands/display/blacklist-display.command-handler.ts b/src/blacklist/subcommands/display/blacklist-display.command-handler.ts index 660e470f..f64ebf6c 100644 --- a/src/blacklist/subcommands/display/blacklist-display.command-handler.ts +++ b/src/blacklist/subcommands/display/blacklist-display.command-handler.ts @@ -1,9 +1,8 @@ import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; -import { type APIEmbedField, EmbedBuilder } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { BlacklistCollection } from '../../../firebase/collections/blacklist-collection.js'; -import type { BlacklistDocument } from '../../../firebase/models/blacklist.model.js'; import { BlacklistDisplayCommand } from '../../blacklist.commands.js'; -import { getDisplayName } from '../../blacklist.utils.js'; +import { createBlacklistEmbedFields } from '../../blacklist.utils.js'; @CommandHandler(BlacklistDisplayCommand) class BlacklistDisplayCommandHandler @@ -18,26 +17,15 @@ class BlacklistDisplayCommandHandler interaction.guildId, ); - const fields = results.flatMap((result) => this.getFields(result)); + const fields = results.flatMap((result) => + createBlacklistEmbedFields(result), + ); // for each result create an Embed field item for it, only displaying the fields that are defined in the document const embed = new EmbedBuilder().setTitle('Blacklist').addFields(fields); await interaction.editReply({ embeds: [embed] }); } - - private getFields({ - characterName, - discordId, - reason, - }: BlacklistDocument): APIEmbedField[] { - const displayName = getDisplayName({ characterName, discordId }); - return [ - { name: 'Player', value: displayName, inline: true }, - { name: 'Reason', value: reason, inline: true }, - { name: '\u200b', value: '\u200b', inline: true }, - ]; - } } export { BlacklistDisplayCommandHandler }; diff --git a/src/discord/discord.consts.ts b/src/discord/discord.consts.ts index 7c664c75..3463f802 100644 --- a/src/discord/discord.consts.ts +++ b/src/discord/discord.consts.ts @@ -1,9 +1,4 @@ -import { - GatewayIntentBits, - Message, - type PartialMessage, - Partials, -} from 'discord.js'; +import { GatewayIntentBits, Message, Partials } from 'discord.js'; export const INTENTS = [ // GatewayIntentBits.DirectMessages, @@ -33,7 +28,7 @@ export function getMessageLink({ guildId, channelId, id, -}: Message | PartialMessage) { +}: Pick) { return `https://discord.com/channels/${guildId}/${channelId}/${id}`; } diff --git a/src/firebase/collections/blacklist-collection.ts b/src/firebase/collections/blacklist-collection.ts index a0632e81..c0f1f6e5 100644 --- a/src/firebase/collections/blacklist-collection.ts +++ b/src/firebase/collections/blacklist-collection.ts @@ -8,6 +8,7 @@ import { first, from, lastValueFrom, + map, mergeMap, } from 'rxjs'; import { InjectFirestore } from '../firebase.decorators.js'; @@ -86,6 +87,19 @@ class BlacklistCollection { return lastValueFrom(pipeline$, { defaultValue: undefined }); } + public search({ + guildId, + discordId, + characterName, + }: { guildId: string } & Pick< + BlacklistDocument, + 'discordId' | 'characterName' + >) { + return this.query$(guildId, { discordId, characterName }).pipe( + map((result) => result.docs[0]!.data()), + ); + } + /** * Query the collection for the given data and emit the first result * @param data