Skip to content

Commit

Permalink
feat(signups): publishes alerts on blacklisted signups (#460)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssilve1989 authored Sep 10, 2024
1 parent 800396f commit 14366de
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 28 deletions.
8 changes: 5 additions & 3 deletions src/app.sagas.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,9 +24,10 @@ class AppSagas {
handleSignupCreated = (event$: Observable<any>): Observable<ICommand> =>
event$.pipe(
ofType(SignupCreatedEvent),
map(
({ signup, guildId }) => new SendSignupReviewCommand(signup, guildId),
),
mergeMap(({ signup, guildId }) => [
new SendSignupReviewCommand(signup, guildId),
new BlacklistSearchCommand(signup, guildId),
]),
);

/**
Expand Down
8 changes: 8 additions & 0 deletions src/blacklist/blacklist.commands.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
) {}
}
2 changes: 2 additions & 0 deletions src/blacklist/blacklist.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,6 +18,7 @@ import { BlacklistRemoveCommandHandler } from './subcommands/remove/blacklist-re
BlacklistRemoveCommandHandler,
BlacklistDisplayCommandHandler,
BlacklistUpdatedEventHandler,
BlacklistSearchCommandHandler,
],
})
class BlacklistModule {}
Expand Down
19 changes: 18 additions & 1 deletion src/blacklist/blacklist.utils.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 },
];
}
Original file line number Diff line number Diff line change
@@ -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<BlacklistSearchCommand>
{
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 };
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 };
9 changes: 2 additions & 7 deletions src/discord/discord.consts.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -33,7 +28,7 @@ export function getMessageLink({
guildId,
channelId,
id,
}: Message | PartialMessage) {
}: Pick<Message, 'guildId' | 'channelId' | 'id'>) {
return `https://discord.com/channels/${guildId}/${channelId}/${id}`;
}

Expand Down
14 changes: 14 additions & 0 deletions src/firebase/collections/blacklist-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
first,
from,
lastValueFrom,
map,
mergeMap,
} from 'rxjs';
import { InjectFirestore } from '../firebase.decorators.js';
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 14366de

Please sign in to comment.