Skip to content

Commit

Permalink
feat(back): Discord webhooks for published maps
Browse files Browse the repository at this point in the history
  • Loading branch information
GordiNoki committed Oct 26, 2024
1 parent 390681a commit 484975e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 5 deletions.
4 changes: 3 additions & 1 deletion apps/backend/src/app/config/config.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as pino from 'pino';
import { GamemodeCategory } from '@momentum/constants';
import * as pino from 'pino';

export enum Environment {
DEVELOPMENT = 'dev',
Expand Down Expand Up @@ -48,5 +49,6 @@ export interface ConfigInterface {
maxCreditsExceptTesters: number;
preSignedUrlExpTime: number;
};
discordWebhooks: Record<GamemodeCategory, string>;
logLevel: pino.LevelWithSilent;
}
12 changes: 11 additions & 1 deletion apps/backend/src/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
JWT_WEB_EXPIRY_TIME,
JWT_REFRESH_EXPIRY_TIME,
MAX_MAP_IMAGE_SIZE,
PRE_SIGNED_URL_EXPIRE_TIME
PRE_SIGNED_URL_EXPIRE_TIME,
GamemodeCategory
} from '@momentum/constants';
import * as Enum from '@momentum/enum';
import { ConfigInterface, Environment } from './config.interface';
import * as process from 'node:process';
import * as pino from 'pino';
Expand Down Expand Up @@ -69,6 +71,14 @@ export const ConfigFactory = (): ConfigInterface => {
maxCreditsExceptTesters: MAX_CREDITS_EXCEPT_TESTERS,
preSignedUrlExpTime: PRE_SIGNED_URL_EXPIRE_TIME
},
discordWebhooks: Object.fromEntries(
Enum.values(GamemodeCategory).map((cat) => [
cat,
isTest
? ''
: (process.env[`DISCORD_WEBHOOK_${GamemodeCategory[cat]}_URL`] ?? '')
])
) as Record<GamemodeCategory, string>,
logLevel: (process.env['LOG_LEVEL'] ??
(isTest ? 'warn' : 'info')) as pino.LevelWithSilent
};
Expand Down
168 changes: 168 additions & 0 deletions apps/backend/src/app/modules/maps/map-webhooks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
Leaderboard,
MapCredit,
MapInfo,
MapSubmission,
MMap,
User
} from '@prisma/client';
import { HttpService } from '@nestjs/axios';
import {
MapSubmissionSuggestion,
GamemodeCategories,
GamemodeInfo,
TrackType,
imgLargePath,
MapCreditType,
MapSubmissionPlaceholder,
LeaderboardType,
Gamemode,
steamAvatarUrl
} from '@momentum/constants';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class MapWebhooksService {
constructor(
private config: ConfigService,
private http: HttpService
) {}

private readonly logger = new Logger('Discord Webhooks');

async sendPublicTestingDiscordEmbed(
extendedMap: MMap & {
info: MapInfo;
submission: MapSubmission;
submitter: User;
credits: Array<MapCredit & { user: User }>;
}
) {
const suggestions =
(extendedMap.submission
.suggestions as unknown as MapSubmissionSuggestion[]) ?? []; // TODO: #855

const placeholders =
(extendedMap.submission
.placeholders as unknown as MapSubmissionPlaceholder[]) ?? []; // TODO: #855

const mainTrackSuggestions = suggestions.filter(
({ trackType }) => trackType === TrackType.MAIN
);

const mainTrackGamemodes = new Set(
mainTrackSuggestions.map(({ gamemode }) => gamemode)
);

const mapAuthors = [
...placeholders
.filter(({ type }) => type === MapCreditType.AUTHOR)
.map(({ alias }) => alias),
...extendedMap.credits
.filter(({ type }) => type === MapCreditType.AUTHOR)
.map(({ user }) => user.alias)
].sort();

const webhookBody = this.createMapUpdateWebhookBody(
'A new map is open for public testing!',
extendedMap,
mapAuthors,
mainTrackSuggestions
);

await this.broadcastWebhookToCategories(mainTrackGamemodes, webhookBody);
}

async sendApprovedDiscordEmbed(
extendedMap: MMap & {
info: MapInfo;
leaderboards: Array<Leaderboard>;
submitter: User;
credits: Array<MapCredit & { user: User }>;
}
) {
const mainTrackLeaderboards = extendedMap.leaderboards.filter(
({ trackType, type }) =>
trackType === TrackType.MAIN && type !== LeaderboardType.HIDDEN
);

const mainTrackGamemodes = new Set(
mainTrackLeaderboards.map(({ gamemode }) => gamemode)
);

const mapAuthors = extendedMap.credits
.filter(({ type }) => type === MapCreditType.AUTHOR)
.map(({ user }) => user.alias)
.sort();

const webhookBody = this.createMapUpdateWebhookBody(
'A new map has been added!',
extendedMap,
mapAuthors,
mainTrackLeaderboards
);

await this.broadcastWebhookToCategories(mainTrackGamemodes, webhookBody);
}

createMapUpdateWebhookBody(
text: string,
extendedMap: MMap & { info: MapInfo; submitter: User },
authors: Array<string>,
gamemodes: MapSubmissionSuggestion[] | Leaderboard[]
) {
const frontendUrl = this.config.getOrThrow('url.frontend');
const cdnUrl = this.config.getOrThrow('url.cdn');

return {
content: text,
embeds: [
{
title: extendedMap.name,
description: 'By ' + authors.map((a) => `**${a}**`).join(', '),
url: `${frontendUrl}/maps/${extendedMap.id}`,
timestamp: extendedMap.info.creationDate.toISOString(),
color: 1611475,
image: {
url: `${cdnUrl}/${imgLargePath(extendedMap.images[0])}`
},
fields: gamemodes.map(
(gm: MapSubmissionSuggestion | Leaderboard) => ({
name: GamemodeInfo.get(gm.gamemode).name,
value: `Tier ${gm.tier}${gm.type === LeaderboardType.UNRANKED ? ' (Unranked)' : ''}`,
inline: true
})
),
footer: {
icon_url: steamAvatarUrl(extendedMap.submitter.avatar),
text: extendedMap.submitter.alias
}
}
]
};
}

async broadcastWebhookToCategories(
gamemodes: Set<Gamemode>,
webhookBody: object
): Promise<void> {
await Promise.all(
[...GamemodeCategories.entries()]
.filter(([, modes]) => modes.some((gm) => gamemodes.has(gm)))
.map(([category]) => {
const webhookUrl = this.config.getOrThrow(
`discordWebhooks.${category}`
);
if (webhookUrl === '') return;

return firstValueFrom(
this.http.post(webhookUrl, webhookBody, {
headers: { 'Content-Type': 'application/json' }
})
);
})
).catch((error) => this.logger.error('Failed to post to webhook', error));
}
}
8 changes: 6 additions & 2 deletions apps/backend/src/app/modules/maps/maps.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { MapTestInviteService } from './map-test-invite.service';
import { MapListService } from './map-list.service';
import { MapReviewModule } from '../map-review/map-review.module';
import { KillswitchModule } from '../killswitch/killswitch.module';
import { MapWebhooksService } from './map-webhooks.service';
import { HttpModule } from '@nestjs/axios';

@Module({
imports: [
Expand All @@ -23,15 +25,17 @@ import { KillswitchModule } from '../killswitch/killswitch.module';
forwardRef(() => RunsModule),
forwardRef(() => AdminModule),
forwardRef(() => MapReviewModule),
KillswitchModule
KillswitchModule,
HttpModule
],
controllers: [MapsController],
providers: [
MapsService,
MapCreditsService,
MapImageService,
MapTestInviteService,
MapListService
MapListService,
MapWebhooksService
],
exports: [MapsService]
})
Expand Down
58 changes: 57 additions & 1 deletion apps/backend/src/app/modules/maps/maps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
import { MapListService } from './map-list.service';
import { MapReviewService } from '../map-review/map-review.service';
import { createHash } from 'node:crypto';
import { MapWebhooksService } from './map-webhooks.service';

@Injectable()
export class MapsService {
Expand All @@ -105,7 +106,8 @@ export class MapsService {
@Inject(forwardRef(() => MapReviewService))
private readonly mapReviewService: MapReviewService,
private readonly adminActivityService: AdminActivityService,
private readonly mapListService: MapListService
private readonly mapListService: MapListService,
private readonly discordWebhookService: MapWebhooksService
) {}

//#region Gets
Expand Down Expand Up @@ -1064,6 +1066,33 @@ export class MapsService {
) as Prisma.MMapUpdateInput
});
statusChanged = statusHandler[1] !== statusHandler[2];

// Ensure that discord notification will be sent after update
if (statusHandler[2] === MapStatus.PUBLIC_TESTING) {
const extendedMap = await tx.mMap.findUnique({
where: { id: map.id },
include: {
info: true,
submission: true,
submitter: true,
credits: { include: { user: true } }
}
});
void this.discordWebhookService.sendPublicTestingDiscordEmbed(
extendedMap
);
} else if (statusHandler[2] === MapStatus.APPROVED) {
const extendedMap = await tx.mMap.findUnique({
where: { id: map.id },
include: {
info: true,
leaderboards: true,
submitter: true,
credits: { include: { user: true } }
}
});
void this.discordWebhookService.sendApprovedDiscordEmbed(extendedMap);
}
} else {
await tx.mMap.update({
where: { id: mapID },
Expand Down Expand Up @@ -1170,6 +1199,33 @@ export class MapsService {

oldStatus = statusHandler[1];
newStatus = statusHandler[2];

// Ensure that discord notification will be sent after update
if (statusHandler[2] === MapStatus.PUBLIC_TESTING) {
const extendedMap = await tx.mMap.findUnique({
where: { id: map.id },
include: {
info: true,
submission: true,
submitter: true,
credits: { include: { user: true } }
}
});
void this.discordWebhookService.sendPublicTestingDiscordEmbed(
extendedMap
);
} else if (statusHandler[2] === MapStatus.APPROVED) {
const extendedMap = await tx.mMap.findUnique({
where: { id: map.id },
include: {
info: true,
leaderboards: true,
submitter: true,
credits: { include: { user: true } }
}
});
void this.discordWebhookService.sendApprovedDiscordEmbed(extendedMap);
}
} else {
updatedMap = await tx.mMap.update({
where: { id: mapID },
Expand Down

0 comments on commit 484975e

Please sign in to comment.