From b7dd0c4b5d6720330e85dea92d28df5c9730259b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 23:38:32 +0800 Subject: [PATCH 01/47] feat(milk): properly name the initialize methods and do not require sentry --- milk/src/connections/fastify.ts | 6 ++---- milk/src/connections/kafka.ts | 6 ++---- milk/src/connections/prisma.ts | 6 ++---- milk/src/connections/sentry.ts | 11 ++++------- milk/src/index.ts | 16 ++++++++-------- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 5416cd9..e3d3833 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -16,7 +16,7 @@ export const attachables: Attachable[] = [ DefaultRoute ] -async function init() { +export async function initializeFastify() { if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { Logger.error(TAG, 'Server Port is not configured, discarding request to start.') process.exit() @@ -40,6 +40,4 @@ async function init() { antispam: link + '/antispam/', messages: link + '/messages/' })) -} - -export const Fastified = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index 2bc5b62..eae5e18 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -5,7 +5,7 @@ import {KafkaClients} from "../kafka/clients.js"; export let kafka: KafkaConnection -async function init() { +export async function initializeKafka() { process.env.KAFKAJS_NO_PARTITIONER_WARNING = "1" if (!process.env.KAFKA_HOST) { Logger.error(TAG, 'Kafka is not configured, discarding request to start.') @@ -18,6 +18,4 @@ async function init() { await kafka.start() KafkaClients.init(kafka) -} - -export const Koffaka = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/connections/prisma.ts b/milk/src/connections/prisma.ts index 7c561e0..8939dbf 100644 --- a/milk/src/connections/prisma.ts +++ b/milk/src/connections/prisma.ts @@ -1,7 +1,7 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {prisma, TAG} from "../index.js"; -async function init() { +export async function initializePrisma() { try { if (process.env.DATABASE_URL == null) { Logger.error(TAG, 'Prisma is not configured, discarding request to start.') @@ -15,6 +15,4 @@ async function init() { console.error(ex) process.exit() } -} - -export const Prismae = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/connections/sentry.ts b/milk/src/connections/sentry.ts index a12faea..1965aae 100644 --- a/milk/src/connections/sentry.ts +++ b/milk/src/connections/sentry.ts @@ -2,14 +2,11 @@ import * as Sentry from '@sentry/node' import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; -function init() { +export function initializeSentry() { if (process.env.SENTRY_DSN == null) { - Logger.error(TAG, 'Sentry is not configured, discarding request to start.') - process.exit() + Logger.error(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') return } - Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, }) -} - -export const Sentryboo = { init: init } \ No newline at end of file + Sentry.init({dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0,}) +} \ No newline at end of file diff --git a/milk/src/index.ts b/milk/src/index.ts index 8889f15..20c0185 100644 --- a/milk/src/index.ts +++ b/milk/src/index.ts @@ -1,11 +1,11 @@ import {PrismaClient} from "@prisma/client"; import dotenv from 'dotenv' -import {Sentryboo} from "./connections/sentry.js"; -import {Koffaka} from "./connections/kafka.js"; -import {Prismae} from "./connections/prisma.js"; +import {initializeSentry} from "./connections/sentry.js"; +import {initializeKafka} from "./connections/kafka.js"; +import {initializePrisma} from "./connections/prisma.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {Fastified} from "./connections/fastify.js"; +import {initializeFastify} from "./connections/fastify.js"; import * as Sentry from '@sentry/node' dotenv.config() @@ -17,16 +17,16 @@ export const CONFIGURATION = { READS_ENABLED: process.env.READS_ENABLED?.toLowerCase() === 'true' } async function main() { - Sentryboo.init() - await Prismae.init() + initializeSentry() + await initializePrisma() Logger.info(TAG, 'Starting milk under the following conditions ' + JSON.stringify(CONFIGURATION)) if (CONFIGURATION.WRITES_ENABLED) { - await Koffaka.init() + await initializeKafka() } if (CONFIGURATION.READS_ENABLED) { - await Fastified.init() + await initializeFastify() } } From 486de9018b10664d1751e44700ecbe5dff20c819 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:05:58 +0800 Subject: [PATCH 02/47] feat(milk): more consistent file names and simpler interfaces --- milk/src/connections/fastify.ts | 29 ++++++++++--------- milk/src/hooks/ErrorAndNotFoundHook.ts | 16 ---------- milk/src/hooks/LogHook.ts | 10 ------- milk/src/hooks/error_hook.ts | 18 ++++++++++++ milk/src/hooks/log_hook.ts | 11 +++++++ milk/src/routes/DefaultRoute.ts | 4 --- milk/src/routes/default_route.ts | 7 +++++ .../{GetAntispam.ts => get_antispam.ts} | 10 +++---- milk/src/types/fastify.ts | 4 +-- 9 files changed, 56 insertions(+), 53 deletions(-) delete mode 100644 milk/src/hooks/ErrorAndNotFoundHook.ts delete mode 100644 milk/src/hooks/LogHook.ts create mode 100644 milk/src/hooks/error_hook.ts create mode 100644 milk/src/hooks/log_hook.ts delete mode 100644 milk/src/routes/DefaultRoute.ts create mode 100644 milk/src/routes/default_route.ts rename milk/src/routes/{GetAntispam.ts => get_antispam.ts} (88%) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index e3d3833..94998b9 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -1,16 +1,16 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; -import Fastify, {FastifyInstance} from "fastify"; -import {GetAntispam} from "../routes/GetAntispam.js"; -import {LogHook} from "../hooks/LogHook.js"; -import {ErrorAndNotFoundHook} from "../hooks/ErrorAndNotFoundHook.js"; -import {DefaultRoute} from "../routes/DefaultRoute.js"; +import Fastify from "fastify"; +import GetAntispam from "../routes/get_antispam.js"; +import LogHook from "../hooks/log_hook.js"; +import ErrorHook from "../hooks/error_hook.js"; +import DefaultRoute from "../routes/default_route.js"; import {Attachable} from "../types/fastify.js"; -export const server: FastifyInstance = Fastify.default({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true }) -export const attachables: Attachable[] = [ - ErrorAndNotFoundHook, +const server = Fastify.default({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true }) +const attachables: Attachable[] = [ + ErrorHook, LogHook, GetAntispam, DefaultRoute @@ -18,14 +18,15 @@ export const attachables: Attachable[] = [ export async function initializeFastify() { if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { - Logger.error(TAG, 'Server Port is not configured, discarding request to start.') - process.exit() + Logger.error(TAG, 'You need to configure a server port for the service to work.') return } - for (const attachable of attachables) { - await attachable.attach(server) - } + server.register(fastify => { + for (const attachable of attachables) { + attachable(fastify) + } + }) const port = Number.parseInt(process.env.SERVER_PORT) const link = 'http://localhost:' + port @@ -35,7 +36,7 @@ export async function initializeFastify() { host: '0.0.0.0' }) - Logger.info(TAG, 'Fastify Server is now running ' + JSON.stringify({ + Logger.info(TAG, 'Milk service is now serving. ' + JSON.stringify({ port: port, antispam: link + '/antispam/', messages: link + '/messages/' diff --git a/milk/src/hooks/ErrorAndNotFoundHook.ts b/milk/src/hooks/ErrorAndNotFoundHook.ts deleted file mode 100644 index 493ba1e..0000000 --- a/milk/src/hooks/ErrorAndNotFoundHook.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {FastifyInstance} from "fastify"; -import * as Sentry from "@sentry/node"; - -const attach = (server: FastifyInstance) => server - .setNotFoundHandler((_, reply) => { reply.code(404).send('404 Not Found') }) - .setErrorHandler((error, request, reply) => { - if (reply.statusCode === 429) { - reply.send('You are sending too many requests, slow down!') - return - } - - Sentry.captureException(error) - reply.code(500).send('An error occurred on the server-side, the hive has been notified.') - }) - -export const ErrorAndNotFoundHook = { attach: attach } \ No newline at end of file diff --git a/milk/src/hooks/LogHook.ts b/milk/src/hooks/LogHook.ts deleted file mode 100644 index 2852fa7..0000000 --- a/milk/src/hooks/LogHook.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; -import {FastifyInstance} from "fastify"; - -const attach = (server: FastifyInstance) => server.addHook( - 'preHandler', - async (request) => Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) -) -export const LogHook = { attach: attach } \ No newline at end of file diff --git a/milk/src/hooks/error_hook.ts b/milk/src/hooks/error_hook.ts new file mode 100644 index 0000000..89037af --- /dev/null +++ b/milk/src/hooks/error_hook.ts @@ -0,0 +1,18 @@ +import {FastifyInstance} from "fastify"; +import * as Sentry from "@sentry/node"; + +export default async (fastify: FastifyInstance) => { + fastify + .setNotFoundHandler((_, reply) => { reply.code(404).send('404 Not Found') }) + .setErrorHandler((error, request, reply) => { + if (reply.statusCode === 429) { + reply.send('You are sending too many requests, slow down!') + return + } + + if (process.env.SENTRY_DSN != null) { + Sentry.captureException(error) + } + reply.code(500).send('An error occurred on the server-side, the hive has been notified.') + }) +} \ No newline at end of file diff --git a/milk/src/hooks/log_hook.ts b/milk/src/hooks/log_hook.ts new file mode 100644 index 0000000..cd900fc --- /dev/null +++ b/milk/src/hooks/log_hook.ts @@ -0,0 +1,11 @@ +import {Logger} from "@beemobot/common"; +// ^ This needs to be updated; Probably @beemobot/cafe +import {TAG} from "../index.js"; +import {FastifyInstance} from "fastify"; + +export default async (fastify: FastifyInstance) => { + fastify.addHook( + 'preHandler', + async (request) => Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) + ) +} \ No newline at end of file diff --git a/milk/src/routes/DefaultRoute.ts b/milk/src/routes/DefaultRoute.ts deleted file mode 100644 index 2a8ba43..0000000 --- a/milk/src/routes/DefaultRoute.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {FastifyInstance} from "fastify"; - -const attach = (server: FastifyInstance) => server.get('/', (request, reply) => reply.redirect('https://beemo.gg')) -export const DefaultRoute = { attach: attach } \ No newline at end of file diff --git a/milk/src/routes/default_route.ts b/milk/src/routes/default_route.ts new file mode 100644 index 0000000..e9cfa37 --- /dev/null +++ b/milk/src/routes/default_route.ts @@ -0,0 +1,7 @@ +import {FastifyInstance} from "fastify"; + +export default (fastify: FastifyInstance) => { + fastify.get('/', (_, reply) => { + reply.redirect('https://beemo.gg') + }) +} \ No newline at end of file diff --git a/milk/src/routes/GetAntispam.ts b/milk/src/routes/get_antispam.ts similarity index 88% rename from milk/src/routes/GetAntispam.ts rename to milk/src/routes/get_antispam.ts index c6eae2c..e13c680 100644 --- a/milk/src/routes/GetAntispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -5,9 +5,9 @@ import {DateUtil} from "../utils/date.js"; import {AntispamLogsCache} from "../cache/antispamLogsCache.js"; import {FastifyInstance} from "fastify"; -const attach = (server: FastifyInstance) => { - server.get('/antispam', (request, reply) => reply.send('You came to the wrong spot, buddy!')) - server.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { +export default async (fastify: FastifyInstance) => { + fastify.get('/antispam', (_, reply) => reply.send('You came to the wrong spot, buddy!')) + fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { try { const { id } = request.params const cache = AntispamLogsCache.get(id) @@ -50,6 +50,4 @@ const attach = (server: FastifyInstance) => { console.error(ex) } }) -} - -export const GetAntispam = { attach: attach } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/types/fastify.ts b/milk/src/types/fastify.ts index 2c606de..ce43dcd 100644 --- a/milk/src/types/fastify.ts +++ b/milk/src/types/fastify.ts @@ -1,5 +1,3 @@ import {FastifyInstance} from "fastify"; -export type Attachable = { - attach: (server: FastifyInstance) => any -} \ No newline at end of file +export type Attachable = (fastify: FastifyInstance) => void \ No newline at end of file From 0cf9691e4c62d639850c4902e483fe0e9458fa5a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:09:55 +0800 Subject: [PATCH 03/47] feat(milk): add trust proxy support --- milk/.env.example | 3 ++- milk/src/connections/fastify.ts | 15 ++++++--------- milk/src/types/fastify.ts | 3 --- 3 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 milk/src/types/fastify.ts diff --git a/milk/.env.example b/milk/.env.example index 307c5c9..e6582fa 100644 --- a/milk/.env.example +++ b/milk/.env.example @@ -35,4 +35,5 @@ KAFKA_HOST= # # IMPORTANT: To configure the server port for Docker Swarm, PLEASE LOOK INTO THE DOCKER COMPOSE FILE! # ---------------------- -# SERVER_PORT=7732 \ No newline at end of file +# SERVER_PORT=7732 +# TRUST_PROXY=true \ No newline at end of file diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 94998b9..ed81853 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -6,15 +6,12 @@ import GetAntispam from "../routes/get_antispam.js"; import LogHook from "../hooks/log_hook.js"; import ErrorHook from "../hooks/error_hook.js"; import DefaultRoute from "../routes/default_route.js"; -import {Attachable} from "../types/fastify.js"; -const server = Fastify.default({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true }) -const attachables: Attachable[] = [ - ErrorHook, - LogHook, - GetAntispam, - DefaultRoute -] +const server = Fastify.default({ + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: true, + trustProxy: (process.env.TRUST_PROXY ?? 'false').toLowerCase() === 'true' +}) export async function initializeFastify() { if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { @@ -23,7 +20,7 @@ export async function initializeFastify() { } server.register(fastify => { - for (const attachable of attachables) { + for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { attachable(fastify) } }) diff --git a/milk/src/types/fastify.ts b/milk/src/types/fastify.ts deleted file mode 100644 index ce43dcd..0000000 --- a/milk/src/types/fastify.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {FastifyInstance} from "fastify"; - -export type Attachable = (fastify: FastifyInstance) => void \ No newline at end of file From 52fa3dcf4bfca69ada839ff18c1ee93945816ed3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:12:52 +0800 Subject: [PATCH 04/47] feat(milk): improve log messages --- milk/src/connections/kafka.ts | 8 +++++--- milk/src/connections/prisma.ts | 5 ++--- milk/src/connections/sentry.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index eae5e18..6c6eb2a 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -8,12 +8,14 @@ export let kafka: KafkaConnection export async function initializeKafka() { process.env.KAFKAJS_NO_PARTITIONER_WARNING = "1" if (!process.env.KAFKA_HOST) { - Logger.error(TAG, 'Kafka is not configured, discarding request to start.') - process.exit() + Logger.error(TAG, + 'Kafka is needed to start this service. If you need to run this for read-only, ' + + 'please properly configure that on the configuration.' + ) return } - Logger.info(TAG, "Attempting to start Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) + Logger.info(TAG, "Attempting to connect to Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) kafka = new KafkaConnection(process.env.KAFKA_HOST, "milk", "milk", "-5") await kafka.start() diff --git a/milk/src/connections/prisma.ts b/milk/src/connections/prisma.ts index 8939dbf..638b4c0 100644 --- a/milk/src/connections/prisma.ts +++ b/milk/src/connections/prisma.ts @@ -4,14 +4,13 @@ import {prisma, TAG} from "../index.js"; export async function initializePrisma() { try { if (process.env.DATABASE_URL == null) { - Logger.error(TAG, 'Prisma is not configured, discarding request to start.') - process.exit() + Logger.error(TAG, 'No database URI has been found on the configuration. Please configure it as the service cannot run without it.') return } await prisma.$connect() } catch (ex) { - Logger.error(TAG, 'Failed to connect to Prisma, closing startup.') + Logger.error(TAG, 'Failed to connect to the database, closing service.') console.error(ex) process.exit() } diff --git a/milk/src/connections/sentry.ts b/milk/src/connections/sentry.ts index 1965aae..2af855f 100644 --- a/milk/src/connections/sentry.ts +++ b/milk/src/connections/sentry.ts @@ -4,7 +4,7 @@ import {Logger} from "@beemobot/common"; import {TAG} from "../index.js"; export function initializeSentry() { if (process.env.SENTRY_DSN == null) { - Logger.error(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') + Logger.warn(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') return } From cf7bdf07fe5881b8c2770e1ed4db2121efeef921 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:13:55 +0800 Subject: [PATCH 05/47] feat(milk): separate types from the kafka client --- milk/src/kafka/clients/raids.ts | 21 +-------------------- milk/src/types/raid.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 milk/src/types/raid.ts diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 3e1aad2..f7479b3 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -3,6 +3,7 @@ import {BrokerClient, KafkaConnection} from "@beemobot/common"; import {prisma} from "../../index.js"; import {StringUtil} from "../../utils/string.js"; import {retriable} from "../../utils/retriable.js"; +import {RaidManagementData} from "../../types/raid.js"; export const KEY_BATCH_INSERT_RAID_USERS = "batch-insert-raid-users" export class RaidManagementClient extends BrokerClient { @@ -78,24 +79,4 @@ export class RaidManagementClient extends BrokerClient { await m.respond({ response: { externalId: raid!.external_id }, request: null }) }) } -} -export type RaidManagementData = { - request: RaidManagementRequest | null, - response: RaidManagementResponse | null -} -export type RaidManagementRequest = { - raidId: string, - guildIdString: string, - users: RaidManagementUser[], - concluded_at: (Date | string) | null -} -export type RaidManagementResponse = { - externalId: string -} -export type RaidManagementUser = { - idString: string, - name: string, - avatarHash: string | null, - createdAt: Date | string, - joinedAt: Date | string } \ No newline at end of file diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts new file mode 100644 index 0000000..791b65a --- /dev/null +++ b/milk/src/types/raid.ts @@ -0,0 +1,23 @@ +export type RaidManagementData = { + request: RaidManagementRequest | null, + response: RaidManagementResponse | null +} + +export type RaidManagementRequest = { + raidId: string, + guildIdString: string, + users: RaidManagementUser[], + concluded_at: (Date | string) | null +} + +export type RaidManagementResponse = { + externalId: string +} + +export type RaidManagementUser = { + idString: string, + name: string, + avatarHash: string | null, + createdAt: Date | string, + joinedAt: Date | string +} \ No newline at end of file From d8df49829cfd45330551e2d3f5314464084cfd58 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:24:19 +0800 Subject: [PATCH 06/47] feat(milk): add more logging to raid management kafka client and simplify code --- milk/src/kafka/clients/raids.ts | 77 +++++++++++++++------------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index f7479b3..b003b33 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,6 +1,6 @@ -import {BrokerClient, KafkaConnection} from "@beemobot/common"; +import {BrokerClient, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma} from "../../index.js"; +import {prisma, TAG} from "../../index.js"; import {StringUtil} from "../../utils/string.js"; import {retriable} from "../../utils/retriable.js"; import {RaidManagementData} from "../../types/raid.js"; @@ -9,74 +9,67 @@ export const KEY_BATCH_INSERT_RAID_USERS = "batch-insert-raid-users" export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { super(conn, "raid-management"); - this.on(KEY_BATCH_INSERT_RAID_USERS, async (m) => { - if (m.value == null) { + this.on(KEY_BATCH_INSERT_RAID_USERS, async (message) => { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${KEY_BATCH_INSERT_RAID_USERS} but no request details was found.`) return } - const request = m.value.request - if (request == null) { - return - } + const request = message.value.request if (request.users.length > 0) { + Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) + const users = request.users.map((user) => { + return { + internal_raid_id: request.raidId, + user_id: BigInt(user.idString), + name: user.name, + avatar_hash: user.avatarHash, + created_at: user.createdAt, + joined_at: user.joinedAt + } + }) + await retriable( 'insert_raid_users', - async () => { - prisma.raidUser.createMany({ - data: request.users.map((user) => { - return { - internal_raid_id: request.raidId, - user_id: BigInt(user.idString), - name: user.name, - avatar_hash: user.avatarHash, - created_at: user.createdAt, - joined_at: user.joinedAt - } - }), - skipDuplicates: true - }) - }, + async () => prisma.raidUser.createMany({data: users, skipDuplicates: true}), 2, 25 ) } - let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}).then((result) => result); + let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}) if (raid == null) { + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) raid = await retriable( 'create_raid', - async () => { - const uuid = StringUtil.random(12) - return prisma.raid.create({ - data: { - internal_id: request.raidId, - external_id: uuid, - guild_id: BigInt(request.guildIdString), - concluded_at: request.concluded_at - } - }); - }, + async () => prisma.raid.create({ + data: { + internal_id: request.raidId, + external_id: StringUtil.random(12), + guild_id: BigInt(request.guildIdString), + concluded_at: request.concluded_at + } + }), 1, 25 ) } else { if (request.concluded_at != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concluded_at))) { + Logger.info(TAG, `Concluding raid ${request.raidId} from guild ${request.guildIdString}.`) raid = await retriable( 'conclude_raid', - async () => { - return prisma.raid.update({ - where: { external_id: raid!.external_id, internal_id: request.raidId }, - data: { concluded_at: request.concluded_at } - }) - }, + async () => prisma.raid.update({ + where: { external_id: raid!.external_id, internal_id: request.raidId }, + data: { concluded_at: request.concluded_at } + }), 0.2, 25 ) } } - await m.respond({ response: { externalId: raid!.external_id }, request: null }) + await message.respond({ response: { externalId: raid!.external_id }, request: null }) }) } } \ No newline at end of file From 3980fc76d5959f4a49b1977b3f3065ca04397774 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:26:33 +0800 Subject: [PATCH 07/47] feat(milk): inline cache creation --- milk/src/cache/antispamLogsCache.ts | 5 ----- milk/src/routes/get_antispam.ts | 8 +++++--- 2 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 milk/src/cache/antispamLogsCache.ts diff --git a/milk/src/cache/antispamLogsCache.ts b/milk/src/cache/antispamLogsCache.ts deleted file mode 100644 index 21852f8..0000000 --- a/milk/src/cache/antispamLogsCache.ts +++ /dev/null @@ -1,5 +0,0 @@ -import NodeCache from "node-cache"; - -export const AntispamLogsCache = new NodeCache({ - stdTTL: 10 * 1000 * 60 -}) \ No newline at end of file diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index e13c680..9373453 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -2,15 +2,17 @@ import {prisma, TAG} from "../index.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {DateUtil} from "../utils/date.js"; -import {AntispamLogsCache} from "../cache/antispamLogsCache.js"; import {FastifyInstance} from "fastify"; +import NodeCache from "node-cache"; + +const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) export default async (fastify: FastifyInstance) => { fastify.get('/antispam', (_, reply) => reply.send('You came to the wrong spot, buddy!')) fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { try { const { id } = request.params - const cache = AntispamLogsCache.get(id) + const cache = logsCache.get(id) if (cache != null) { return reply.send(cache) @@ -43,7 +45,7 @@ export default async (fastify: FastifyInstance) => { response += '\n Raw IDs:' response += '\n' response += userIds - AntispamLogsCache.set(id, response) + logsCache.set(id, response) } return reply.send(response) } catch (ex) { From 7e68899ab3d558df45e6de5eeceea0ebb45b97a3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:32:47 +0800 Subject: [PATCH 08/47] feat(milk): add support for JSON content type. --- milk/src/routes/get_antispam.ts | 56 +++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index 9373453..23727f6 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -12,7 +12,10 @@ export default async (fastify: FastifyInstance) => { fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { try { const { id } = request.params - const cache = logsCache.get(id) + const isJsonContentType = request.headers["content-type"] === "application/json" + + const cacheKey = isJsonContentType ? id + ".json" : id + const cache = logsCache.get(cacheKey) if (cache != null) { return reply.send(cache) @@ -26,27 +29,40 @@ export default async (fastify: FastifyInstance) => { const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) - let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + DateUtil.toDateString(users[0].joined_at); - if (users.length === 0) { - Logger.warn(TAG, "Raid " + id + " reported no users.") - response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" - } else { - response += '\nRaid size: ' + users.length + ' accounts' - response += '\n' - response += '\n Joined at: ID: Username:' - response += '\n' - let userIds = ''; - for (const user of users) { - response += DateUtil.toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name - userIds += user.user_id - } + if (!isJsonContentType) { + let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + DateUtil.toDateString(users[0].joined_at); + if (users.length === 0) { + Logger.warn(TAG, "Raid " + id + " reported no users.") + response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" + } else { + response += '\nRaid size: ' + users.length + ' accounts' + response += '\n' + response += '\n Joined at: ID: Username:' + response += '\n' + let userIds = ''; + for (const user of users) { + response += DateUtil.toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name + userIds += user.user_id + } - response += '\n' - response += '\n Raw IDs:' - response += '\n' - response += userIds - logsCache.set(id, response) + response += '\n' + response += '\n Raw IDs:' + response += '\n' + response += userIds + logsCache.set(cacheKey, response) + } + return reply.send(response) } + + let response = JSON.stringify({ + size: users.length, + started_at: users[0]?.joined_at, + concluded_at: raid.concluded_at, + guild: raid.guild_id, + accounts: users + }) + + logsCache.set(cacheKey, response) return reply.send(response) } catch (ex) { console.error(ex) From 7bb7a419f61f1c8966e069e73033bfc260ab408a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:34:50 +0800 Subject: [PATCH 09/47] feat(milk): minimize date util and fix potential index out of bounds --- milk/src/routes/get_antispam.ts | 11 ++++++++--- milk/src/utils/date.ts | 10 ++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index 23727f6..e811c80 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -1,7 +1,7 @@ import {prisma, TAG} from "../index.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {DateUtil} from "../utils/date.js"; +import {toDateString, toTimeString} from "../utils/date.js"; import {FastifyInstance} from "fastify"; import NodeCache from "node-cache"; @@ -30,7 +30,12 @@ export default async (fastify: FastifyInstance) => { const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) if (!isJsonContentType) { - let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + DateUtil.toDateString(users[0].joined_at); + let startedDate = "N/A" + if (users.length > 0) { + startedDate = toDateString(users[0].joined_at) + } + + let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; if (users.length === 0) { Logger.warn(TAG, "Raid " + id + " reported no users.") response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" @@ -41,7 +46,7 @@ export default async (fastify: FastifyInstance) => { response += '\n' let userIds = ''; for (const user of users) { - response += DateUtil.toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name + response += toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name userIds += user.user_id } diff --git a/milk/src/utils/date.ts b/milk/src/utils/date.ts index f460395..b0b5c81 100644 --- a/milk/src/utils/date.ts +++ b/milk/src/utils/date.ts @@ -1,15 +1,13 @@ -const toDateString = (date: Date) => date.getUTCFullYear() + '/' +export const toDateString = (date: Date) => date.getUTCFullYear() + '/' + pad(2, date.getUTCMonth() + 1) + '/' + pad(2, date.getUTCDate()) const pad = (length: number = 2, number: number) => number.toString().padStart(length, "0") -const toISOString = (date: Date) => date.toISOString().replace('Z', '+0000') -const toTimeString = (date: Date) => +export const toISOString = (date: Date) => date.toISOString().replace('Z', '+0000') +export const toTimeString = (date: Date) => pad(2, date.getUTCHours()) + ':' + pad(2, date.getUTCMinutes()) + ':' + pad(2, date.getUTCSeconds()) + '.' + pad(3, date.getUTCMilliseconds()) + - '+0000' - -export const DateUtil = { toDateString: toDateString, toISOString: toISOString, toTimeString: toTimeString } \ No newline at end of file + '+0000' \ No newline at end of file From d96068731c45b1894755f5f566fcc4e3ba60f2e0 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:36:34 +0800 Subject: [PATCH 10/47] feat(milk): minimize utils such as string and numbers --- milk/src/kafka/clients/raids.ts | 4 ++-- milk/src/utils/number.ts | 6 ++---- milk/src/utils/string.ts | 16 +++++++--------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index b003b33..eb61129 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,7 +1,7 @@ import {BrokerClient, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {prisma, TAG} from "../../index.js"; -import {StringUtil} from "../../utils/string.js"; +import {randomString} from "../../utils/string.js"; import {retriable} from "../../utils/retriable.js"; import {RaidManagementData} from "../../types/raid.js"; @@ -46,7 +46,7 @@ export class RaidManagementClient extends BrokerClient { async () => prisma.raid.create({ data: { internal_id: request.raidId, - external_id: StringUtil.random(12), + external_id: randomString(12), guild_id: BigInt(request.guildIdString), concluded_at: request.concluded_at } diff --git a/milk/src/utils/number.ts b/milk/src/utils/number.ts index 2546edb..f78d914 100644 --- a/milk/src/utils/number.ts +++ b/milk/src/utils/number.ts @@ -1,7 +1,5 @@ -const random = (min: number, max: number): number => { +export const randomNumber = (min: number, max: number): number => { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min) + min) -} - -export const NumberUtil = { random: random } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/utils/string.ts b/milk/src/utils/string.ts index 21e23dd..9d639bb 100644 --- a/milk/src/utils/string.ts +++ b/milk/src/utils/string.ts @@ -1,28 +1,26 @@ -import {NumberUtil} from "./number.js"; +import {randomNumber} from "./number.js"; -export const random = (length: number): string => { +export const randomString = (length: number): string => { let contents = ''; while (contents.length < length) { - let seed = NumberUtil.random(1, 4); + let seed = randomNumber(1, 4); switch (seed) { case 1: { - seed = NumberUtil.random(65, 91); + seed = randomNumber(65, 91); contents += String.fromCharCode(seed); break; } case 2: { - seed = NumberUtil.random(97, 123); + seed = randomNumber(97, 123); contents += String.fromCharCode(seed); break; } case 3: { - seed = NumberUtil.random(0, 10); + seed = randomNumber(0, 10); contents += seed; break; } } } return contents -} - -export const StringUtil = { random: random } \ No newline at end of file +} \ No newline at end of file From 455099f3355142c9820bc1d2d3444f6affea7010 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:42:36 +0800 Subject: [PATCH 11/47] feat(milk): fix formatting for user ids --- milk/src/routes/get_antispam.ts | 97 +++++++++++++++++---------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index e811c80..6e5374f 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -10,67 +10,70 @@ const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) export default async (fastify: FastifyInstance) => { fastify.get('/antispam', (_, reply) => reply.send('You came to the wrong spot, buddy!')) fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { - try { - const { id } = request.params - const isJsonContentType = request.headers["content-type"] === "application/json" + const { id } = request.params + const isJsonContentType = request.headers["content-type"] === "application/json" - const cacheKey = isJsonContentType ? id + ".json" : id - const cache = logsCache.get(cacheKey) + const cacheKey = isJsonContentType ? id + ".json" : id + const cache = logsCache.get(cacheKey) - if (cache != null) { - return reply.send(cache) - } - - const raid = await prisma.raid.findUnique({ where: { external_id: id } }) - - if (raid == null) { - return reply.code(404).send('404 Not Found') - } + if (cache != null) { + return reply.send(cache) + } - const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) + const raid = await prisma.raid.findUnique({ where: { external_id: id } }) - if (!isJsonContentType) { - let startedDate = "N/A" - if (users.length > 0) { - startedDate = toDateString(users[0].joined_at) - } + if (raid == null) { + return reply.code(404).send('404 Not Found') + } - let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; - if (users.length === 0) { - Logger.warn(TAG, "Raid " + id + " reported no users.") - response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" - } else { - response += '\nRaid size: ' + users.length + ' accounts' - response += '\n' - response += '\n Joined at: ID: Username:' - response += '\n' - let userIds = ''; - for (const user of users) { - response += toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name - userIds += user.user_id - } + const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) - response += '\n' - response += '\n Raw IDs:' - response += '\n' - response += userIds - logsCache.set(cacheKey, response) - } - return reply.send(response) - } + let response: string + let shouldCache: boolean = true - let response = JSON.stringify({ + if (isJsonContentType) { + response = JSON.stringify({ size: users.length, started_at: users[0]?.joined_at, concluded_at: raid.concluded_at, guild: raid.guild_id, accounts: users }) + } else { + let startedDate = "N/A" + if (users.length > 0) { + startedDate = toDateString(users[0].joined_at) + } + + response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; - logsCache.set(cacheKey, response) - return reply.send(response) - } catch (ex) { - console.error(ex) + if (users.length === 0) { + shouldCache = false + + Logger.warn(TAG, `Raid ${id} reported no users.`) + response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" + } else { + response += '\nRaid size: ' + users.length + ' accounts' + response += '\n' + response += '\n Joined at: ID: Username:' + response += '\n' + let userIds = ''; + for (const user of users) { + response += toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name + if (userIds !== '') { + userIds += '\n' + } + userIds += user.user_id + } + + response += '\n' + response += '\n Raw IDs:' + response += '\n' + response += userIds + } } + + if (shouldCache) logsCache.set(cacheKey, response) + return reply.send(response) }) } \ No newline at end of file From 6aab24b09cc932b55e804ac2e9c59f53373ec727 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:43:37 +0800 Subject: [PATCH 12/47] feat(milk): removed non-existent route in logging --- milk/src/connections/fastify.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index ed81853..9a5fdaa 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -35,7 +35,6 @@ export async function initializeFastify() { Logger.info(TAG, 'Milk service is now serving. ' + JSON.stringify({ port: port, - antispam: link + '/antispam/', - messages: link + '/messages/' + antispam: link + '/antispam/' })) } \ No newline at end of file From c317c2a04fa78ab470e31d27b5eb7ba364bbf923 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 20 Oct 2023 00:44:55 +0800 Subject: [PATCH 13/47] feat(milk): disable request logging --- milk/src/connections/fastify.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 9a5fdaa..720c4a4 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -10,7 +10,8 @@ import DefaultRoute from "../routes/default_route.js"; const server = Fastify.default({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true, - trustProxy: (process.env.TRUST_PROXY ?? 'false').toLowerCase() === 'true' + trustProxy: (process.env.TRUST_PROXY ?? 'false').toLowerCase() === 'true', + disableRequestLogging: true }) export async function initializeFastify() { From 02cce1a4a60f397850fd7dd641ba7f3caae04ab8 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 29 Oct 2023 18:05:50 +0800 Subject: [PATCH 14/47] feat(milk): support both `.json` path or `Accept` header --- milk/src/routes/get_antispam.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index 6e5374f..c313d36 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -10,10 +10,15 @@ const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) export default async (fastify: FastifyInstance) => { fastify.get('/antispam', (_, reply) => reply.send('You came to the wrong spot, buddy!')) fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { - const { id } = request.params - const isJsonContentType = request.headers["content-type"] === "application/json" + let { id } = request.params + const isJsonContentType = id.endsWith(".json") || request.headers.accept === "application/json" const cacheKey = isJsonContentType ? id + ".json" : id + + if (id.includes(".")) { + id = id.split(".")[0] + } + const cache = logsCache.get(cacheKey) if (cache != null) { From 9c1217820a57b80839537a8115ef962190e14f52 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 29 Oct 2023 18:08:39 +0800 Subject: [PATCH 15/47] feat(milk): minor readability changes --- milk/src/routes/get_antispam.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_antispam.ts index c313d36..4bfc044 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_antispam.ts @@ -14,17 +14,15 @@ export default async (fastify: FastifyInstance) => { const isJsonContentType = id.endsWith(".json") || request.headers.accept === "application/json" const cacheKey = isJsonContentType ? id + ".json" : id - - if (id.includes(".")) { - id = id.split(".")[0] - } - const cache = logsCache.get(cacheKey) - if (cache != null) { return reply.send(cache) } + if (id.includes(".")) { + id = id.split(".")[0] + } + const raid = await prisma.raid.findUnique({ where: { external_id: id } }) if (raid == null) { @@ -34,7 +32,7 @@ export default async (fastify: FastifyInstance) => { const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) let response: string - let shouldCache: boolean = true + let shouldCache: boolean = users.length !== 0 if (isJsonContentType) { response = JSON.stringify({ @@ -53,8 +51,6 @@ export default async (fastify: FastifyInstance) => { response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; if (users.length === 0) { - shouldCache = false - Logger.warn(TAG, `Raid ${id} reported no users.`) response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" } else { From 7fb89415f3849f5e98cc6354e751e235d57b4706 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 30 Oct 2023 20:29:27 +0800 Subject: [PATCH 16/47] feat(milk): use new `/raid` route over `/antispam` --- milk/src/routes/{get_antispam.ts => get_raid.ts} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename milk/src/routes/{get_antispam.ts => get_raid.ts} (88%) diff --git a/milk/src/routes/get_antispam.ts b/milk/src/routes/get_raid.ts similarity index 88% rename from milk/src/routes/get_antispam.ts rename to milk/src/routes/get_raid.ts index 4bfc044..3e9c197 100644 --- a/milk/src/routes/get_antispam.ts +++ b/milk/src/routes/get_raid.ts @@ -7,9 +7,15 @@ import NodeCache from "node-cache"; const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) +type IdParameter = {Params:{ id: string } } export default async (fastify: FastifyInstance) => { - fastify.get('/antispam', (_, reply) => reply.send('You came to the wrong spot, buddy!')) - fastify.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { + fastify.get('/antispam/:id', (request, reply) => { + let { id } = request.params + reply.redirect('/raid/' + encodeURIComponent(id)) + }) + + fastify.get('/raid', (_, reply) => reply.send('You came to the wrong spot, buddy!')) + fastify.get('/raid/:id', async (request, reply) => { let { id } = request.params const isJsonContentType = id.endsWith(".json") || request.headers.accept === "application/json" From 7d649d9b5e3e020e3e293e6f212a228401f09c86 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 30 Oct 2023 23:06:02 +0800 Subject: [PATCH 17/47] feat(milk): hide `internal_raid_id` from json output --- milk/src/connections/fastify.ts | 2 +- milk/src/routes/get_raid.ts | 20 +++++++++++++++----- milk/src/types/raid.ts | 8 ++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 720c4a4..c110fce 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -2,7 +2,7 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; import Fastify from "fastify"; -import GetAntispam from "../routes/get_antispam.js"; +import GetAntispam from "../routes/get_raid.js"; import LogHook from "../hooks/log_hook.js"; import ErrorHook from "../hooks/error_hook.js"; import DefaultRoute from "../routes/default_route.js"; diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 3e9c197..070c03c 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -4,6 +4,7 @@ import {Logger} from "@beemobot/common"; import {toDateString, toTimeString} from "../utils/date.js"; import {FastifyInstance} from "fastify"; import NodeCache from "node-cache"; +import {PublicRaidUser} from "../types/raid.js"; const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) @@ -35,7 +36,16 @@ export default async (fastify: FastifyInstance) => { return reply.code(404).send('404 Not Found') } - const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) + const users = (await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } })) + .map(user => { + return { + id: user.user_id, + name: user.name, + joinedAt: user.joined_at, + createdAt: user.created_at, + avatarHash: user.avatar_hash + } satisfies PublicRaidUser + }) let response: string let shouldCache: boolean = users.length !== 0 @@ -43,7 +53,7 @@ export default async (fastify: FastifyInstance) => { if (isJsonContentType) { response = JSON.stringify({ size: users.length, - started_at: users[0]?.joined_at, + started_at: users[0]?.joinedAt, concluded_at: raid.concluded_at, guild: raid.guild_id, accounts: users @@ -51,7 +61,7 @@ export default async (fastify: FastifyInstance) => { } else { let startedDate = "N/A" if (users.length > 0) { - startedDate = toDateString(users[0].joined_at) + startedDate = toDateString(users[0].joinedAt) } response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; @@ -66,11 +76,11 @@ export default async (fastify: FastifyInstance) => { response += '\n' let userIds = ''; for (const user of users) { - response += toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name + response += toTimeString(user.joinedAt) + ' ' + user.id + ' ' + user.name if (userIds !== '') { userIds += '\n' } - userIds += user.user_id + userIds += user.id } response += '\n' diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index 791b65a..f7303d7 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -20,4 +20,12 @@ export type RaidManagementUser = { avatarHash: string | null, createdAt: Date | string, joinedAt: Date | string +} + +export type PublicRaidUser = { + id: bigint, + name: string, + avatarHash: string | null, + createdAt: Date, + joinedAt: Date } \ No newline at end of file From dde88d831e56f609225cbda1f8311bf12c8aa6e2 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 30 Oct 2023 23:07:51 +0800 Subject: [PATCH 18/47] feat(milk): consistency with camelCase for non-auto-generated fields --- milk/src/kafka/clients/raids.ts | 6 +++--- milk/src/types/raid.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index eb61129..a7a4693 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -48,20 +48,20 @@ export class RaidManagementClient extends BrokerClient { internal_id: request.raidId, external_id: randomString(12), guild_id: BigInt(request.guildIdString), - concluded_at: request.concluded_at + concluded_at: request.concludedAt } }), 1, 25 ) } else { - if (request.concluded_at != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concluded_at))) { + if (request.concludedAt != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concludedAt))) { Logger.info(TAG, `Concluding raid ${request.raidId} from guild ${request.guildIdString}.`) raid = await retriable( 'conclude_raid', async () => prisma.raid.update({ where: { external_id: raid!.external_id, internal_id: request.raidId }, - data: { concluded_at: request.concluded_at } + data: { concluded_at: request.concludedAt } }), 0.2, 25 diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index f7303d7..c1d4063 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -7,7 +7,7 @@ export type RaidManagementRequest = { raidId: string, guildIdString: string, users: RaidManagementUser[], - concluded_at: (Date | string) | null + concludedAt: (Date | string) | null } export type RaidManagementResponse = { From 31b45d4323488f18103a0f6708fab1eb27d4e5cf Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 30 Oct 2023 23:14:58 +0800 Subject: [PATCH 19/47] feat(milk): maintain camel-case consistency with `.json` output --- milk/src/routes/get_raid.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 070c03c..41c444f 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -53,10 +53,10 @@ export default async (fastify: FastifyInstance) => { if (isJsonContentType) { response = JSON.stringify({ size: users.length, - started_at: users[0]?.joinedAt, - concluded_at: raid.concluded_at, + startedAt: users[0]?.joinedAt, + concludedAt: raid.concluded_at, guild: raid.guild_id, - accounts: users + users }) } else { let startedDate = "N/A" From 14d9b7dd14539e154c0db0e2787d400998006271 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 30 Oct 2023 23:34:55 +0800 Subject: [PATCH 20/47] feat(milk): use string instead of `bigint` for public json --- milk/src/routes/get_raid.ts | 4 ++-- milk/src/types/raid.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 41c444f..c38cbf6 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -39,7 +39,7 @@ export default async (fastify: FastifyInstance) => { const users = (await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } })) .map(user => { return { - id: user.user_id, + id: user.user_id.toString(), name: user.name, joinedAt: user.joined_at, createdAt: user.created_at, @@ -55,7 +55,7 @@ export default async (fastify: FastifyInstance) => { size: users.length, startedAt: users[0]?.joinedAt, concludedAt: raid.concluded_at, - guild: raid.guild_id, + guild: raid.guild_id.toString(), users }) } else { diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index c1d4063..f4ddfa7 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -23,7 +23,7 @@ export type RaidManagementUser = { } export type PublicRaidUser = { - id: bigint, + id: string, name: string, avatarHash: string | null, createdAt: Date, From 0a883a6619fe862010e63d0dfe230846a8e8553d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 00:19:44 +0800 Subject: [PATCH 21/47] feat(milk): add kafka and openapi documentation --- milk/docs/kafka.md | 115 +++++++++++++++++++++++++++++++++++++++++ milk/docs/openapi.yaml | 88 +++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 milk/docs/kafka.md create mode 100644 milk/docs/openapi.yaml diff --git a/milk/docs/kafka.md b/milk/docs/kafka.md new file mode 100644 index 0000000..4baafc3 --- /dev/null +++ b/milk/docs/kafka.md @@ -0,0 +1,115 @@ +# Developer Documentations for Kafka client + +This documentation is primarily for developers of Beemo. The Kafka client isn't something +accessible to third-parties, therefore, more than likely, this is of no-use for third-parties. + +### Key Points +- [`Client Specifications`](#client-specifications) talks about the different parts of the client. + - [`overview`](#overview) summarizes some key points of the client. + - [`keys`](#keys) + - [`batch-insert-raid-users`](#batch-insert-raid-users) used to insert one or more bots detected; + creating a raid, and concluding based on the information provided. + - [`schemas`](#schemas) + - [`RaidManagementData`](#raid-management-data) is the primary type transported between clients. + - [`RaidManagementRequest`](#raid-management-request) is used by a requesting client to add more raid users, + start a raid or conclude a raid. Primarily created clients such as Tea. + - [`RaidManagementResponse`](#raid-management-response) is used by a responding client after processing a request. + This is primarily used by clients such as Milk. + - [`RaidManagementUser`](#raid-management-user) is used to hold information about a bot detected. + +## Client Specifications + +In this section, the different specifications of the Kafka client will be discussed and understood to +provide some understanding over how the Kafka client of Milk processes requests. + +### Overview +- Topic: `raid-management` +- Keys: + - `batch-insert-raid-users` +- Transport Type: + - [`RaidManagementData`](#raid-management-data) + +## Keys + +### Batch Insert Raid Users + +This is a specific key, or endpoint, in the Kafka client where clients can insert +bots detected, start a raid or conclude a raid. It is expected that this creates a new raid +when the `raidId` provided does not exist already. In addition, if the raid hasn't been concluded +but a `concludedAt` property is provided then it will conclude the raid, if the raid has been +concluded before, but a newer date has been provided then it will conclude the raid. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + +## Schemas + +### Raid Management Data + +```json +{ + "request": "nullable(RaidManagementRequest)", + "response": "nullable(RaidManagementResponse)" +} +``` + +Used by the clients to transport either a request or a response without the need to perform additional identification. + +- `request` is a nullable property containing the request details, used by the requesting client. This should be +guaranteed from a request, otherwise there is a bug with that client. +- `response` is a nullable property containing the response details, used by the responding client. This should be +guaranteed from a response of a client, otherwise there is a bug with that client. + + +### Raid Management Request +```json +{ + "raidId": "string", + "guildIdString": "string", + "users": "array(RaidManagementUser)", + "concludedAt": "nullable(date as string)" +} +``` + +Used by a requesting client to start a raid, insert bots detected or conclude a raid. + +- `raidId` refers to the internal raid id of the raid. Clients shouldn't use the external raid id as that is created +and used only by Milk itself. +- `guildIdString` refers to the id of the guild that this raid belonged to. It must be of `string` type due to +the nature of JavaScript not inherently supporting `int64` or `long` type. +- `users` refers to the bots detected in the raid, this can be an empty array when simply concluding a raid. +- `concludedAt` refers to the date when the raid should be declared as concluded. + +### Raid Management Response +```json +{ + "externalId": "string" +} +``` + +Used by a responding client to notify that the request was processed, and a publicly accessible id is now +available to be shared in the log channels. + +- `externalId` refers to the publicly accessible id that can be used in `/raid/:id` + +### Raid Management User +```json +{ + "idString": "string", + "name": "string", + "avatarHash": "nullable(string)", + "createdAt": "date as string", + "joinedAt": "date as string" +} +``` + +Contains information about a bot that was detected in a raid. + +- `idString` refers to the id of the bot's account. +- `name` refers to the name of the bot during detection. +- `avatarHash` refers to the hash of the bot's avatar during detection. +- `createdAt` refers to the creation time of the bot. +- `joinedAt` refers to when the bot joined the server. \ No newline at end of file diff --git a/milk/docs/openapi.yaml b/milk/docs/openapi.yaml new file mode 100644 index 0000000..af526a2 --- /dev/null +++ b/milk/docs/openapi.yaml @@ -0,0 +1,88 @@ +openapi: 3.0.3 +info: + title: Milk by Beemo + description: |- + The official raid log manager of Beemo. It is used to store raid logs to the database, and also to allow people to view raid logs through a text or JSON format. + + In this documentation, we will be primarily describing the JSON output of the service as this is primarily why other developers would visit this. + termsOfService: https://beemo.gg/terms + contact: + email: hello@beemo.gg + version: 1.0.0 +servers: + - url: https://logs.beemo.gg +tags: + - name: raid + description: Everything public about raids caught by Beemo. +paths: + /raid/{id}.json: + get: + tags: + - raid + summary: Gets information about a raid in JSON format. + description: Returns more detailed information about a raid in JSON format. + operationId: getRaidById + parameters: + - name: id + in: path + description: ID of the raid. + required: true + schema: + type: string + responses: + '200': + description: Raid found + content: + application/json: + schema: + $ref: '#/components/schemas/Raid' + '404': + description: Raid not found +components: + schemas: + Raid: + type: object + properties: + size: + type: integer + format: int32 + example: 32 + startedAt: + type: string + format: date + example: '2023-10-30T15:29:57.697Z' + nullable: true + concludedAt: + type: string + format: date + example: '2023-10-30T16:10:57.697Z' + nullable: true + guild: + type: string + format: int64 + example: '697474023914733575' + users: + type: array + items: + $ref: '#/components/schemas/RaidUser' + RaidUser: + type: object + properties: + id: + type: string + format: int64 + example: '584322030934032393' + name: + type: string + example: 'Shindou Mihou' + avatarHash: + type: string + nullable: true + createdAt: + type: string + format: datetime + example: '2023-10-30T16:10:57.697Z' + joinedAt: + type: string + format: datetime + example: '2023-10-30T16:10:57.697Z' \ No newline at end of file From 99821b149f15c443fe7f6b6365f99b0296db1508 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 22:55:55 +0800 Subject: [PATCH 22/47] feat(milk): use `async` for `retriable` --- milk/src/utils/retriable.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/milk/src/utils/retriable.ts b/milk/src/utils/retriable.ts index 062d69c..092b98d 100644 --- a/milk/src/utils/retriable.ts +++ b/milk/src/utils/retriable.ts @@ -3,20 +3,21 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; -export function retriable(task: string, action: () => Promise, retryEverySeconds: number = 10, maxRetries: number = -1, retries: number = 1): Promise { - return action() - .catch(async (exception) => { - if (retries === 1) { - Sentry.captureException(exception) - } +export async function retriable(task: string, action: () => Promise, retryEverySeconds: number = 10, maxRetries: number = -1, retries: number = 1): Promise { + try { + return await action(); + } catch (exception) { + if (retries === 1) { + Sentry.captureException(exception) + } - if (maxRetries !== -1 && retries >= maxRetries) { - return exception - } + if (maxRetries !== -1 && retries >= maxRetries) { + throw exception + } - Logger.error(TAG, 'Failed to complete ' + task + '. Retrying in ' + (retryEverySeconds * retries) + ' seconds.\n', exception) - await new Promise((resolve) => setTimeout(resolve, (retryEverySeconds * retries) * 1000)) - return retriable(task, action, retryEverySeconds, retries + 1) - }) + Logger.error(TAG, 'Failed to complete ' + task + '. Retrying in ' + (retryEverySeconds * retries) + ' seconds.\n', exception) + await new Promise((resolve) => setTimeout(resolve, (retryEverySeconds * retries) * 1000)) + return retriable(task, action, retryEverySeconds, retries + 1) + } } From de3e460a55313927d1c9e2f3f43e0181e015804a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:03:20 +0800 Subject: [PATCH 23/47] feat(milk): simplify the code for `run` (formerly `retry`) --- milk/src/kafka/clients/raids.ts | 8 ++++---- milk/src/utils/retriable.ts | 23 ----------------------- milk/src/utils/retry.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 27 deletions(-) delete mode 100644 milk/src/utils/retriable.ts create mode 100644 milk/src/utils/retry.ts diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index a7a4693..5807c61 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -2,7 +2,7 @@ import {BrokerClient, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {prisma, TAG} from "../../index.js"; import {randomString} from "../../utils/string.js"; -import {retriable} from "../../utils/retriable.js"; +import {run} from "../../utils/retry.js"; import {RaidManagementData} from "../../types/raid.js"; export const KEY_BATCH_INSERT_RAID_USERS = "batch-insert-raid-users" @@ -30,7 +30,7 @@ export class RaidManagementClient extends BrokerClient { } }) - await retriable( + await run( 'insert_raid_users', async () => prisma.raidUser.createMany({data: users, skipDuplicates: true}), 2, @@ -41,7 +41,7 @@ export class RaidManagementClient extends BrokerClient { let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}) if (raid == null) { Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) - raid = await retriable( + raid = await run( 'create_raid', async () => prisma.raid.create({ data: { @@ -57,7 +57,7 @@ export class RaidManagementClient extends BrokerClient { } else { if (request.concludedAt != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concludedAt))) { Logger.info(TAG, `Concluding raid ${request.raidId} from guild ${request.guildIdString}.`) - raid = await retriable( + raid = await run( 'conclude_raid', async () => prisma.raid.update({ where: { external_id: raid!.external_id, internal_id: request.raidId }, diff --git a/milk/src/utils/retriable.ts b/milk/src/utils/retriable.ts deleted file mode 100644 index 092b98d..0000000 --- a/milk/src/utils/retriable.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Sentry from '@sentry/node'; -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; - -export async function retriable(task: string, action: () => Promise, retryEverySeconds: number = 10, maxRetries: number = -1, retries: number = 1): Promise { - try { - return await action(); - } catch (exception) { - if (retries === 1) { - Sentry.captureException(exception) - } - - if (maxRetries !== -1 && retries >= maxRetries) { - throw exception - } - - Logger.error(TAG, 'Failed to complete ' + task + '. Retrying in ' + (retryEverySeconds * retries) + ' seconds.\n', exception) - await new Promise((resolve) => setTimeout(resolve, (retryEverySeconds * retries) * 1000)) - return retriable(task, action, retryEverySeconds, retries + 1) - } -} - diff --git a/milk/src/utils/retry.ts b/milk/src/utils/retry.ts new file mode 100644 index 0000000..d05fff5 --- /dev/null +++ b/milk/src/utils/retry.ts @@ -0,0 +1,32 @@ +import * as Sentry from '@sentry/node'; +import {Logger} from "@beemobot/common"; +// ^ This needs to be updated; Probably @beemobot/cafe +import {TAG} from "../index.js"; + +export async function run( + taskName: string, + action: () => Promise, + retryEverySeconds: number = 2, + maxRetries: number = 25, + retries: number = 1 +): Promise { + try { + return await action(); + } catch (exception) { + // Capture exception once to Sentry, we don't want to possibly send so many exceptions + if (retries === 1) { + Sentry.captureException(exception) + } + + if (maxRetries !== -1 && retries >= maxRetries) { + throw exception + } + + const secondsTillRetry = (retryEverySeconds * retries) + Logger.error(TAG, `Failed to complete ${taskName}. Retrying in ${secondsTillRetry} seconds.`, exception) + + await new Promise((resolve) => setTimeout(resolve, secondsTillRetry * 1000)) + return run(taskName, action, retryEverySeconds, retries + 1) + } +} + From 6294bfe82b6b9d4880a3ffd963aab93374b26105 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:14:43 +0800 Subject: [PATCH 24/47] feat(milk): separate raid conclusion from `batch-insert` in `RaidManagementClient` --- milk/src/kafka/clients/raids.ts | 153 ++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 59 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 5807c61..730c194 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,75 +1,110 @@ -import {BrokerClient, KafkaConnection, Logger} from "@beemobot/common"; +import {BrokerClient, BrokerMessage, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {prisma, TAG} from "../../index.js"; import {randomString} from "../../utils/string.js"; import {run} from "../../utils/retry.js"; +import * as Sentry from '@sentry/node'; import {RaidManagementData} from "../../types/raid.js"; -export const KEY_BATCH_INSERT_RAID_USERS = "batch-insert-raid-users" +export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" +export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" +export const RAID_MANAGEMENT_CONCLUDE_RAID = "conclude-raid" + export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { - super(conn, "raid-management"); - this.on(KEY_BATCH_INSERT_RAID_USERS, async (message) => { - if (message.value == null || message.value.request == null) { - Logger.warn(TAG, `Received a message on ${KEY_BATCH_INSERT_RAID_USERS} but no request details was found.`) - return - } + super(conn, RAID_MANAGEMENT_CLIENT_TOPIC); + this.on(RAID_MANAGEMENT_BATCH_INSERT_KEY, this.onBatchInsertRaidUsers) + this.on(RAID_MANAGEMENT_CONCLUDE_RAID, this.onConcludeRaid) + } - const request = message.value.request + private async onConcludeRaid(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_CONCLUDE_RAID} but no request details was found.`) + return + } - if (request.users.length > 0) { - Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) - const users = request.users.map((user) => { - return { - internal_raid_id: request.raidId, - user_id: BigInt(user.idString), - name: user.name, - avatar_hash: user.avatarHash, - created_at: user.createdAt, - joined_at: user.joinedAt - } - }) + let {raidId, concludedAt, guildIdString} = message.value.request + if (concludedAt == null) { + // Assume that the raid was just concluded, theoretically, the raid just concluded the moment we receive + // this message as there is no other reason to send a message to this key if not for concluding a raid. + concludedAt = new Date() + } + + let raid = await prisma.raid.findUnique({where: {internal_id: raidId}}) + if (raid == null) { + const error = Error(`Received a request to conclude a raid, but the raid is not in the database. [raid=${raidId}]`) + + Logger.error(TAG, error.message) + Sentry.captureException(error) + return + } + + if (raid.concluded_at != null) { + Logger.warn(TAG, `Received a request to conclude a raid, but the raid is already concluded. [raid=${raidId}]`) + return + } - await run( - 'insert_raid_users', - async () => prisma.raidUser.createMany({data: users, skipDuplicates: true}), - 2, - 25 - ) - } + Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildIdString}.`) + raid = await run( + 'conclude_raid', + async () => prisma.raid.update({ + where: { external_id: raid!.external_id, internal_id: raidId }, + data: { concluded_at: concludedAt } + }), + 0.2, + 25 + ) - let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}) - if (raid == null) { - Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) - raid = await run( - 'create_raid', - async () => prisma.raid.create({ - data: { - internal_id: request.raidId, - external_id: randomString(12), - guild_id: BigInt(request.guildIdString), - concluded_at: request.concludedAt - } - }), - 1, - 25 - ) - } else { - if (request.concludedAt != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concludedAt))) { - Logger.info(TAG, `Concluding raid ${request.raidId} from guild ${request.guildIdString}.`) - raid = await run( - 'conclude_raid', - async () => prisma.raid.update({ - where: { external_id: raid!.external_id, internal_id: request.raidId }, - data: { concluded_at: request.concludedAt } - }), - 0.2, - 25 - ) + await message.respond({ response: { externalId: raid!.external_id }, request: null }) + } + + private async onBatchInsertRaidUsers(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_BATCH_INSERT_KEY} but no request details was found.`) + return + } + + const request = message.value.request + + if (request.users.length > 0) { + Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) + const users = request.users.map((user) => { + return { + internal_raid_id: request.raidId, + user_id: BigInt(user.idString), + name: user.name, + avatar_hash: user.avatarHash, + created_at: user.createdAt, + joined_at: user.joinedAt } - } + }) + + await run( + 'insert_raid_users', + async () => prisma.raidUser.createMany({data: users, skipDuplicates: true}), + 2, + 25 + ) + } + + let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}) + if (raid == null) { + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) + raid = await run( + 'create_raid', + async () => prisma.raid.create({ + data: { + internal_id: request.raidId, + external_id: randomString(12), + guild_id: BigInt(request.guildIdString), + concluded_at: request.concludedAt + } + }), + 1, + 25 + ) + } - await message.respond({ response: { externalId: raid!.external_id }, request: null }) - }) + await message.respond({ response: { externalId: raid!.external_id }, request: null }) } } \ No newline at end of file From 0f8934195a37f499d830f2ab7559beb97aaff837 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:19:56 +0800 Subject: [PATCH 25/47] feat(milk): document `conclude-raid` key --- milk/docs/kafka.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/milk/docs/kafka.md b/milk/docs/kafka.md index 4baafc3..ee5a11c 100644 --- a/milk/docs/kafka.md +++ b/milk/docs/kafka.md @@ -7,8 +7,8 @@ accessible to third-parties, therefore, more than likely, this is of no-use for - [`Client Specifications`](#client-specifications) talks about the different parts of the client. - [`overview`](#overview) summarizes some key points of the client. - [`keys`](#keys) - - [`batch-insert-raid-users`](#batch-insert-raid-users) used to insert one or more bots detected; - creating a raid, and concluding based on the information provided. + - [`batch-insert-raid-users`](#batch-insert-raid-users) used to insert one or more bots detected; creates the Raid if it doesn't exist. + - [`conclude-raid`](#conclude-raid) used to conclude an existing raid; if no date provided, uses current time. - [`schemas`](#schemas) - [`RaidManagementData`](#raid-management-data) is the primary type transported between clients. - [`RaidManagementRequest`](#raid-management-request) is used by a requesting client to add more raid users, @@ -33,6 +33,10 @@ provide some understanding over how the Kafka client of Milk processes requests. ### Batch Insert Raid Users +```yaml +key: batch-insert-raid-users +``` + This is a specific key, or endpoint, in the Kafka client where clients can insert bots detected, start a raid or conclude a raid. It is expected that this creates a new raid when the `raidId` provided does not exist already. In addition, if the raid hasn't been concluded @@ -45,6 +49,24 @@ following the [`RaidManagementRequest`](#raid-management-request) schema. After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. +### Conclude Raid + +```yaml +key: conclude-raid +``` + +This is a specific key, or endpoint, in the Kafka client where clients can declare an existing raid as concluded. +It is not needed for the `concludedAt` property to be provided as it will use the current time if not provided. +Although you cannot modify the `concludedAt` of an existing raid, if a raid is already concluded then it will skip. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. Unlike [`batch-insert-raid-users`](#batch-insert-raid-users), +this doesn't expect the `users` property to not be empty, even `concludedAt` can be of a zero value or even +null as long as the `raidId` and `guildIdString` are not null. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + ## Schemas ### Raid Management Data From 6e01207aa22b210fc8fcf1d047a1973deb63b38d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:33:28 +0800 Subject: [PATCH 26/47] feat(milk): changes to general error handling --- milk/src/connections/fastify.ts | 43 ++++++++++++++++++--------------- milk/src/connections/kafka.ts | 13 +++++----- milk/src/connections/prisma.ts | 10 ++++---- milk/src/connections/sentry.ts | 16 ++++++++++-- milk/src/hooks/log_hook.ts | 3 ++- milk/src/index.ts | 24 ++++++++++++------ milk/src/kafka/clients.ts | 6 ++--- milk/src/kafka/clients/raids.ts | 6 ++--- milk/src/utils/retry.ts | 2 +- 9 files changed, 73 insertions(+), 50 deletions(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index c110fce..9f1c000 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -6,6 +6,7 @@ import GetAntispam from "../routes/get_raid.js"; import LogHook from "../hooks/log_hook.js"; import ErrorHook from "../hooks/error_hook.js"; import DefaultRoute from "../routes/default_route.js"; +import {logError, logIssue} from "./sentry.js"; const server = Fastify.default({ ignoreTrailingSlash: true, @@ -15,27 +16,31 @@ const server = Fastify.default({ }) export async function initializeFastify() { - if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { - Logger.error(TAG, 'You need to configure a server port for the service to work.') - return - } - - server.register(fastify => { - for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { - attachable(fastify) + try { + if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { + logIssue('You need to configure a server port for the service to work.') + return } - }) - const port = Number.parseInt(process.env.SERVER_PORT) - const link = 'http://localhost:' + port + server.register(fastify => { + for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { + attachable(fastify) + } + }) - await server.listen({ - port: port, - host: '0.0.0.0' - }) + const port = Number.parseInt(process.env.SERVER_PORT) + const link = 'http://localhost:' + port - Logger.info(TAG, 'Milk service is now serving. ' + JSON.stringify({ - port: port, - antispam: link + '/antispam/' - })) + await server.listen({ + port: port, + host: '0.0.0.0' + }) + + Logger.info(TAG, 'Milk service is now serving. ' + JSON.stringify({ + port: port, + antispam: link + '/antispam/' + })) + } catch (ex) { + logError('An issue occurred while trying to start Fastify.', ex) + } } \ No newline at end of file diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index 6c6eb2a..076c250 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -1,23 +1,24 @@ import {KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; -import {KafkaClients} from "../kafka/clients.js"; +import {initKafkaClients} from "../kafka/clients.js"; +import {logIssue} from "./sentry.js"; export let kafka: KafkaConnection export async function initializeKafka() { process.env.KAFKAJS_NO_PARTITIONER_WARNING = "1" + if (!process.env.KAFKA_HOST) { - Logger.error(TAG, - 'Kafka is needed to start this service. If you need to run this for read-only, ' + - 'please properly configure that on the configuration.' - ) + logIssue('Kafka is needed to start this service. If you need to run this for read-only, ' + + 'please properly configure that on the configuration.') return } Logger.info(TAG, "Attempting to connect to Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) + kafka = new KafkaConnection(process.env.KAFKA_HOST, "milk", "milk", "-5") await kafka.start() - KafkaClients.init(kafka) + initKafkaClients(kafka) } \ No newline at end of file diff --git a/milk/src/connections/prisma.ts b/milk/src/connections/prisma.ts index 638b4c0..a6fbd53 100644 --- a/milk/src/connections/prisma.ts +++ b/milk/src/connections/prisma.ts @@ -1,17 +1,17 @@ -import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma, TAG} from "../index.js"; +import {prisma} from "../index.js"; +import {logError, logIssue} from "./sentry.js"; + export async function initializePrisma() { try { if (process.env.DATABASE_URL == null) { - Logger.error(TAG, 'No database URI has been found on the configuration. Please configure it as the service cannot run without it.') + logIssue('No database URI has been found on the configuration. Please configure it as the service cannot run without it.') return } await prisma.$connect() } catch (ex) { - Logger.error(TAG, 'Failed to connect to the database, closing service.') - console.error(ex) + logError('Failed to connect to the database, closing service.', ex) process.exit() } } \ No newline at end of file diff --git a/milk/src/connections/sentry.ts b/milk/src/connections/sentry.ts index 2af855f..b7905fe 100644 --- a/milk/src/connections/sentry.ts +++ b/milk/src/connections/sentry.ts @@ -2,11 +2,23 @@ import * as Sentry from '@sentry/node' import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {TAG} from "../index.js"; -export function initializeSentry() { +export function initializeSentry() { if (process.env.SENTRY_DSN == null) { Logger.warn(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') return } Sentry.init({dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0,}) -} \ No newline at end of file +} + +export function logIssue(message: string) { + const error = Error(message) + + Sentry.captureException(error) + Logger.error(TAG, error.message) +} + +export function logError(message: string, ex: any) { + Sentry.captureException(ex) + Logger.error(TAG, message, ex) +} diff --git a/milk/src/hooks/log_hook.ts b/milk/src/hooks/log_hook.ts index cd900fc..4141aef 100644 --- a/milk/src/hooks/log_hook.ts +++ b/milk/src/hooks/log_hook.ts @@ -6,6 +6,7 @@ import {FastifyInstance} from "fastify"; export default async (fastify: FastifyInstance) => { fastify.addHook( 'preHandler', - async (request) => Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) + async (request) => + Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) ) } \ No newline at end of file diff --git a/milk/src/index.ts b/milk/src/index.ts index 20c0185..61813af 100644 --- a/milk/src/index.ts +++ b/milk/src/index.ts @@ -1,6 +1,6 @@ import {PrismaClient} from "@prisma/client"; import dotenv from 'dotenv' -import {initializeSentry} from "./connections/sentry.js"; +import {initializeSentry, logIssue} from "./connections/sentry.js"; import {initializeKafka} from "./connections/kafka.js"; import {initializePrisma} from "./connections/prisma.js"; import {Logger} from "@beemobot/common"; @@ -12,20 +12,28 @@ dotenv.config() export let prisma = new PrismaClient() export const TAG = "Milk" -export const CONFIGURATION = { - WRITES_ENABLED: process.env.WRITES_ENABLED?.toLowerCase() === 'true', - READS_ENABLED: process.env.READS_ENABLED?.toLowerCase() === 'true' -} + async function main() { initializeSentry() await initializePrisma() - Logger.info(TAG, 'Starting milk under the following conditions ' + JSON.stringify(CONFIGURATION)) - if (CONFIGURATION.WRITES_ENABLED) { + const configuration = { + writesEnabled: process.env.WRITES_ENABLED?.toLowerCase() === 'true', + readsEnabled: process.env.READS_ENABLED?.toLowerCase() === 'true' + } + + Logger.info(TAG, 'Starting milk under the following conditions ' + JSON.stringify(configuration)) + + if (!configuration.readsEnabled && !configuration.writesEnabled) { + logIssue('Milk needs to be in at least read or write mode to function.') + return + } + + if (configuration.writesEnabled) { await initializeKafka() } - if (CONFIGURATION.READS_ENABLED) { + if (configuration.readsEnabled) { await initializeFastify() } } diff --git a/milk/src/kafka/clients.ts b/milk/src/kafka/clients.ts index 7c9844f..242911e 100644 --- a/milk/src/kafka/clients.ts +++ b/milk/src/kafka/clients.ts @@ -3,8 +3,6 @@ import {KafkaConnection} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe export let raidManagementClient: RaidManagementClient -function init(connection: KafkaConnection) { +export function initKafkaClients(connection: KafkaConnection) { raidManagementClient = new RaidManagementClient(connection) -} - -export const KafkaClients = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 730c194..a762929 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -5,6 +5,7 @@ import {randomString} from "../../utils/string.js"; import {run} from "../../utils/retry.js"; import * as Sentry from '@sentry/node'; import {RaidManagementData} from "../../types/raid.js"; +import {logIssue} from "../../connections/sentry.js"; export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" @@ -32,10 +33,7 @@ export class RaidManagementClient extends BrokerClient { let raid = await prisma.raid.findUnique({where: {internal_id: raidId}}) if (raid == null) { - const error = Error(`Received a request to conclude a raid, but the raid is not in the database. [raid=${raidId}]`) - - Logger.error(TAG, error.message) - Sentry.captureException(error) + logIssue(`Received a request to conclude a raid, but the raid is not in the database. [raid=${raidId}]`) return } diff --git a/milk/src/utils/retry.ts b/milk/src/utils/retry.ts index d05fff5..ce3befd 100644 --- a/milk/src/utils/retry.ts +++ b/milk/src/utils/retry.ts @@ -13,7 +13,7 @@ export async function run( try { return await action(); } catch (exception) { - // Capture exception once to Sentry, we don't want to possibly send so many exceptions + // Raise exception once to Sentry, we don't want to possibly send so many exceptions if (retries === 1) { Sentry.captureException(exception) } From 87083632f8d4290aba3295058b319aa9c96480e0 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:35:28 +0800 Subject: [PATCH 27/47] feat(milk): fix somehow `infinity` cache --- milk/src/routes/get_raid.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index c38cbf6..5879ea7 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -6,7 +6,8 @@ import {FastifyInstance} from "fastify"; import NodeCache from "node-cache"; import {PublicRaidUser} from "../types/raid.js"; -const logsCache = new NodeCache({ stdTTL: 10 * 1000 * 60 }) +const TEN_MINUTES = 60 * 10 +const logsCache = new NodeCache({ stdTTL: TEN_MINUTES }) type IdParameter = {Params:{ id: string } } export default async (fastify: FastifyInstance) => { @@ -48,7 +49,7 @@ export default async (fastify: FastifyInstance) => { }) let response: string - let shouldCache: boolean = users.length !== 0 + const shouldCache: boolean = users.length !== 0 if (isJsonContentType) { response = JSON.stringify({ From 6514b34fdd8e6694aef398625b79d6f56119028d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:40:51 +0800 Subject: [PATCH 28/47] feat(milk): remove `/raid` empty route and some minor simplifications --- milk/src/connections/fastify.ts | 7 +------ milk/src/routes/get_raid.ts | 8 +++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 9f1c000..4260a88 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -29,17 +29,12 @@ export async function initializeFastify() { }) const port = Number.parseInt(process.env.SERVER_PORT) - const link = 'http://localhost:' + port - await server.listen({ port: port, host: '0.0.0.0' }) - Logger.info(TAG, 'Milk service is now serving. ' + JSON.stringify({ - port: port, - antispam: link + '/antispam/' - })) + Logger.info(TAG, `Milk is now serving logs under port ${port}.`) } catch (ex) { logError('An issue occurred while trying to start Fastify.', ex) } diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 5879ea7..f28f260 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -15,16 +15,14 @@ export default async (fastify: FastifyInstance) => { let { id } = request.params reply.redirect('/raid/' + encodeURIComponent(id)) }) - - fastify.get('/raid', (_, reply) => reply.send('You came to the wrong spot, buddy!')) fastify.get('/raid/:id', async (request, reply) => { let { id } = request.params const isJsonContentType = id.endsWith(".json") || request.headers.accept === "application/json" const cacheKey = isJsonContentType ? id + ".json" : id - const cache = logsCache.get(cacheKey) - if (cache != null) { - return reply.send(cache) + const cachedResult = logsCache.get(cacheKey) + if (cachedResult != null) { + return reply.send(cachedResult) } if (id.includes(".")) { From 8cabb9e171a40a924e6d2ee7661b5ed57535654f Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:47:36 +0800 Subject: [PATCH 29/47] feat(milk): move constants to dedicated directory --- milk/src/connections/fastify.ts | 2 +- milk/src/connections/kafka.ts | 2 +- milk/src/connections/prisma.ts | 4 +++- milk/src/connections/sentry.ts | 3 ++- milk/src/constants/logging.ts | 1 + milk/src/constants/raid_management_kafka.ts | 3 +++ milk/src/constants/time.ts | 1 + milk/src/hooks/error_hook.ts | 2 +- milk/src/hooks/log_hook.ts | 2 +- milk/src/index.ts | 7 ++----- milk/src/kafka/clients/raids.ts | 13 +++++++------ milk/src/routes/get_raid.ts | 7 ++++--- milk/src/utils/retry.ts | 3 ++- 13 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 milk/src/constants/logging.ts create mode 100644 milk/src/constants/raid_management_kafka.ts create mode 100644 milk/src/constants/time.ts diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 4260a88..9e51099 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -1,12 +1,12 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; import Fastify from "fastify"; import GetAntispam from "../routes/get_raid.js"; import LogHook from "../hooks/log_hook.js"; import ErrorHook from "../hooks/error_hook.js"; import DefaultRoute from "../routes/default_route.js"; import {logError, logIssue} from "./sentry.js"; +import {TAG} from "../constants/logging.js"; const server = Fastify.default({ ignoreTrailingSlash: true, diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index 076c250..815d482 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -1,8 +1,8 @@ import {KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; import {initKafkaClients} from "../kafka/clients.js"; import {logIssue} from "./sentry.js"; +import {TAG} from "../constants/logging.js"; export let kafka: KafkaConnection diff --git a/milk/src/connections/prisma.ts b/milk/src/connections/prisma.ts index a6fbd53..e8187e5 100644 --- a/milk/src/connections/prisma.ts +++ b/milk/src/connections/prisma.ts @@ -1,6 +1,8 @@ // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma} from "../index.js"; import {logError, logIssue} from "./sentry.js"; +import {PrismaClient} from "@prisma/client"; + +export let prisma = new PrismaClient() export async function initializePrisma() { try { diff --git a/milk/src/connections/sentry.ts b/milk/src/connections/sentry.ts index b7905fe..31e9e4a 100644 --- a/milk/src/connections/sentry.ts +++ b/milk/src/connections/sentry.ts @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/node' import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; + +import {TAG} from "../constants/logging.js"; export function initializeSentry() { if (process.env.SENTRY_DSN == null) { Logger.warn(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') diff --git a/milk/src/constants/logging.ts b/milk/src/constants/logging.ts new file mode 100644 index 0000000..6485508 --- /dev/null +++ b/milk/src/constants/logging.ts @@ -0,0 +1 @@ +export const TAG = "Milk" \ No newline at end of file diff --git a/milk/src/constants/raid_management_kafka.ts b/milk/src/constants/raid_management_kafka.ts new file mode 100644 index 0000000..c652479 --- /dev/null +++ b/milk/src/constants/raid_management_kafka.ts @@ -0,0 +1,3 @@ +export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" +export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" +export const RAID_MANAGEMENT_CONCLUDE_RAID = "conclude-raid" \ No newline at end of file diff --git a/milk/src/constants/time.ts b/milk/src/constants/time.ts new file mode 100644 index 0000000..a7139f3 --- /dev/null +++ b/milk/src/constants/time.ts @@ -0,0 +1 @@ +export const TEN_MINUTES = 60 * 10 diff --git a/milk/src/hooks/error_hook.ts b/milk/src/hooks/error_hook.ts index 89037af..c4f739d 100644 --- a/milk/src/hooks/error_hook.ts +++ b/milk/src/hooks/error_hook.ts @@ -4,7 +4,7 @@ import * as Sentry from "@sentry/node"; export default async (fastify: FastifyInstance) => { fastify .setNotFoundHandler((_, reply) => { reply.code(404).send('404 Not Found') }) - .setErrorHandler((error, request, reply) => { + .setErrorHandler((error, _, reply) => { if (reply.statusCode === 429) { reply.send('You are sending too many requests, slow down!') return diff --git a/milk/src/hooks/log_hook.ts b/milk/src/hooks/log_hook.ts index 4141aef..66f01b3 100644 --- a/milk/src/hooks/log_hook.ts +++ b/milk/src/hooks/log_hook.ts @@ -1,7 +1,7 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; import {FastifyInstance} from "fastify"; +import {TAG} from "../constants/logging.js"; export default async (fastify: FastifyInstance) => { fastify.addHook( diff --git a/milk/src/index.ts b/milk/src/index.ts index 61813af..9f6faf7 100644 --- a/milk/src/index.ts +++ b/milk/src/index.ts @@ -1,18 +1,15 @@ -import {PrismaClient} from "@prisma/client"; import dotenv from 'dotenv' import {initializeSentry, logIssue} from "./connections/sentry.js"; import {initializeKafka} from "./connections/kafka.js"; -import {initializePrisma} from "./connections/prisma.js"; +import {initializePrisma, prisma} from "./connections/prisma.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {initializeFastify} from "./connections/fastify.js"; import * as Sentry from '@sentry/node' +import {TAG} from "./constants/logging.js"; dotenv.config() -export let prisma = new PrismaClient() -export const TAG = "Milk" - async function main() { initializeSentry() await initializePrisma() diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index a762929..4014a50 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,15 +1,16 @@ import {BrokerClient, BrokerMessage, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma, TAG} from "../../index.js"; import {randomString} from "../../utils/string.js"; import {run} from "../../utils/retry.js"; -import * as Sentry from '@sentry/node'; import {RaidManagementData} from "../../types/raid.js"; import {logIssue} from "../../connections/sentry.js"; - -export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" -export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" -export const RAID_MANAGEMENT_CONCLUDE_RAID = "conclude-raid" +import {prisma} from "../../connections/prisma.js"; +import {TAG} from "../../constants/logging.js"; +import { + RAID_MANAGEMENT_BATCH_INSERT_KEY, + RAID_MANAGEMENT_CLIENT_TOPIC, + RAID_MANAGEMENT_CONCLUDE_RAID +} from "../../constants/raid_management_kafka.js"; export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index f28f260..58eed2f 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -1,15 +1,16 @@ -import {prisma, TAG} from "../index.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe import {toDateString, toTimeString} from "../utils/date.js"; import {FastifyInstance} from "fastify"; import NodeCache from "node-cache"; import {PublicRaidUser} from "../types/raid.js"; +import {prisma} from "../connections/prisma.js"; +import {TAG} from "../constants/logging.js"; +import {TEN_MINUTES} from "../constants/time.js"; -const TEN_MINUTES = 60 * 10 const logsCache = new NodeCache({ stdTTL: TEN_MINUTES }) -type IdParameter = {Params:{ id: string } } +type IdParameter = { Params: { id: string } } export default async (fastify: FastifyInstance) => { fastify.get('/antispam/:id', (request, reply) => { let { id } = request.params diff --git a/milk/src/utils/retry.ts b/milk/src/utils/retry.ts index ce3befd..6ea22e5 100644 --- a/milk/src/utils/retry.ts +++ b/milk/src/utils/retry.ts @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/node'; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; + +import {TAG} from "../constants/logging.js"; export async function run( taskName: string, From c9772bfbcba282dd0fa75fc3b2d0e28bf2791f57 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:48:55 +0800 Subject: [PATCH 30/47] feat(milk): fix cache misses on json route --- milk/src/routes/get_raid.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 58eed2f..dec86db 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -18,9 +18,11 @@ export default async (fastify: FastifyInstance) => { }) fastify.get('/raid/:id', async (request, reply) => { let { id } = request.params - const isJsonContentType = id.endsWith(".json") || request.headers.accept === "application/json" - const cacheKey = isJsonContentType ? id + ".json" : id + const acceptsJson = request.headers.accept === "application/json" + const isJsonContentType = id.endsWith(".json") || acceptsJson + + const cacheKey = acceptsJson ? id + ".json" : id const cachedResult = logsCache.get(cacheKey) if (cachedResult != null) { return reply.send(cachedResult) From e5db2dcfa0a8a4d73ed645b2731f629fa57acc9e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 23:56:31 +0800 Subject: [PATCH 31/47] feat(milk): add cache headers --- milk/src/routes/get_raid.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index dec86db..a9bb99b 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -25,7 +25,10 @@ export default async (fastify: FastifyInstance) => { const cacheKey = acceptsJson ? id + ".json" : id const cachedResult = logsCache.get(cacheKey) if (cachedResult != null) { - return reply.send(cachedResult) + return reply + .header('X-Cache', 'HIT') + .header('X-Cache-Expires', logsCache.getTtl(cacheKey)) + .send(cachedResult) } if (id.includes(".")) { @@ -93,6 +96,9 @@ export default async (fastify: FastifyInstance) => { } if (shouldCache) logsCache.set(cacheKey, response) - return reply.send(response) + return reply + .status(200) + .header('X-Cache', 'MISS') + .send(response) }) } \ No newline at end of file From 9b03a2deae83170993ebaa38db7819b80b589c16 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 00:01:36 +0800 Subject: [PATCH 32/47] feat(milk): document cache headers --- milk/docs/openapi.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/milk/docs/openapi.yaml b/milk/docs/openapi.yaml index af526a2..c3be834 100644 --- a/milk/docs/openapi.yaml +++ b/milk/docs/openapi.yaml @@ -36,6 +36,19 @@ paths: application/json: schema: $ref: '#/components/schemas/Raid' + headers: + 'X-Cache': + description: 'Indicates whether the response was cached or not.' + schema: + type: string + example: + - 'HIT' + - 'MISS' + 'X-Cache-Expires': + description: 'Indicates the timestamp when the cache will expire.' + schema: + type: integer + example: '1698767766556' '404': description: Raid not found components: From 8423333a5fb7e742d9d2d2c153f4f6817f4d23ea Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 10:24:02 +0800 Subject: [PATCH 33/47] feat(milk): separate `/raid/:id` and `/raid/:id.json` into their own files. This also restructures things a bit to make code more reusable such as moving database queries to their respective files and relevant things. --- milk/src/database/raid.ts | 10 ++ milk/src/database/raid_users.ts | 30 ++++++ milk/src/fastify/serve_cached.ts | 27 +++++ milk/src/routes/get_raid.ts | 108 ++----------------- milk/src/routes/get_raid/get_antispam.ts | 7 ++ milk/src/routes/get_raid/get_raid_as_json.ts | 28 +++++ milk/src/routes/get_raid/get_raid_as_text.ts | 53 +++++++++ 7 files changed, 164 insertions(+), 99 deletions(-) create mode 100644 milk/src/database/raid.ts create mode 100644 milk/src/database/raid_users.ts create mode 100644 milk/src/fastify/serve_cached.ts create mode 100644 milk/src/routes/get_raid/get_antispam.ts create mode 100644 milk/src/routes/get_raid/get_raid_as_json.ts create mode 100644 milk/src/routes/get_raid/get_raid_as_text.ts diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts new file mode 100644 index 0000000..0d17992 --- /dev/null +++ b/milk/src/database/raid.ts @@ -0,0 +1,10 @@ +import {prisma} from "../connections/prisma.js"; +import {Raid} from "@prisma/client"; + +/** + * Gets information about a given {@link Raid}. + * @param externalId the external id of the raid. + * @return {@link Raid} or null if it doesn't exist. + */ +export const getRaid = async (externalId: string): Promise => + (await prisma.raid.findUnique({where: {external_id: externalId}})) \ No newline at end of file diff --git a/milk/src/database/raid_users.ts b/milk/src/database/raid_users.ts new file mode 100644 index 0000000..a5cddee --- /dev/null +++ b/milk/src/database/raid_users.ts @@ -0,0 +1,30 @@ +import {prisma} from "../connections/prisma.js"; +import {PublicRaidUser} from "../types/raid.js"; +import {RaidUser} from "@prisma/client"; + +/** + * Gets the users involved in a {@link Raid}. + * + * @param internalId the internal id + * @return the users involved in a {@link Raid}. + */ +export const getRaidUsers = async (internalId: string): Promise => + (await prisma.raidUser.findMany({where: {internal_raid_id: internalId}})) + +/** + * Gets the users involved in a {@link Raid} as a {@link PublicRaidUser} type which does not include the + * `internal_raid_id`. + * + * @param internalId the internal id + * @return the users involved in a {@link Raid}. + */ +export const getPublicRaidUsers = async (internalId: string): Promise => (await getRaidUsers(internalId)) + .map(user => { + return { + id: user.user_id.toString(), + name: user.name, + joinedAt: user.joined_at, + createdAt: user.created_at, + avatarHash: user.avatar_hash + } satisfies PublicRaidUser + }) \ No newline at end of file diff --git a/milk/src/fastify/serve_cached.ts b/milk/src/fastify/serve_cached.ts new file mode 100644 index 0000000..836f41c --- /dev/null +++ b/milk/src/fastify/serve_cached.ts @@ -0,0 +1,27 @@ +import NodeCache from "node-cache"; +import {FastifyReply} from "fastify"; +import {TEN_MINUTES} from "../constants/time.js"; + +const cache = new NodeCache({ stdTTL: TEN_MINUTES }) +export const useCacheWhenPossible = async ( + reply: FastifyReply, + key: string, + computation: () => Promise<{ result: string, shouldCache: boolean }> +): Promise => { + const cachedResult = cache.get(key) + if (cachedResult != null) { + return reply + .header('X-Cache', 'HIT') + .header('X-Cache-Expires', cache.getTtl(key)) + .send(cachedResult) + } + + const { result, shouldCache } = await computation() + if (shouldCache) { + cache.set(key, result) + } + return reply + .status(200) + .header('X-Cache', 'MISS') + .send(result) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index a9bb99b..557341b 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -1,104 +1,14 @@ -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {toDateString, toTimeString} from "../utils/date.js"; import {FastifyInstance} from "fastify"; -import NodeCache from "node-cache"; -import {PublicRaidUser} from "../types/raid.js"; -import {prisma} from "../connections/prisma.js"; -import {TAG} from "../constants/logging.js"; -import {TEN_MINUTES} from "../constants/time.js"; +import {route$GetRaidAsJson} from "./get_raid/get_raid_as_json.js"; +import {route$GetRaidAsText} from "./get_raid/get_raid_as_text.js"; +import {route$GetAntispam} from "./get_raid/get_antispam.js"; -const logsCache = new NodeCache({ stdTTL: TEN_MINUTES }) +export type RaidParameter = { + Params: { id: string } +} -type IdParameter = { Params: { id: string } } export default async (fastify: FastifyInstance) => { - fastify.get('/antispam/:id', (request, reply) => { - let { id } = request.params - reply.redirect('/raid/' + encodeURIComponent(id)) - }) - fastify.get('/raid/:id', async (request, reply) => { - let { id } = request.params - - const acceptsJson = request.headers.accept === "application/json" - const isJsonContentType = id.endsWith(".json") || acceptsJson - - const cacheKey = acceptsJson ? id + ".json" : id - const cachedResult = logsCache.get(cacheKey) - if (cachedResult != null) { - return reply - .header('X-Cache', 'HIT') - .header('X-Cache-Expires', logsCache.getTtl(cacheKey)) - .send(cachedResult) - } - - if (id.includes(".")) { - id = id.split(".")[0] - } - - const raid = await prisma.raid.findUnique({ where: { external_id: id } }) - - if (raid == null) { - return reply.code(404).send('404 Not Found') - } - - const users = (await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } })) - .map(user => { - return { - id: user.user_id.toString(), - name: user.name, - joinedAt: user.joined_at, - createdAt: user.created_at, - avatarHash: user.avatar_hash - } satisfies PublicRaidUser - }) - - let response: string - const shouldCache: boolean = users.length !== 0 - - if (isJsonContentType) { - response = JSON.stringify({ - size: users.length, - startedAt: users[0]?.joinedAt, - concludedAt: raid.concluded_at, - guild: raid.guild_id.toString(), - users - }) - } else { - let startedDate = "N/A" - if (users.length > 0) { - startedDate = toDateString(users[0].joinedAt) - } - - response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; - - if (users.length === 0) { - Logger.warn(TAG, `Raid ${id} reported no users.`) - response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" - } else { - response += '\nRaid size: ' + users.length + ' accounts' - response += '\n' - response += '\n Joined at: ID: Username:' - response += '\n' - let userIds = ''; - for (const user of users) { - response += toTimeString(user.joinedAt) + ' ' + user.id + ' ' + user.name - if (userIds !== '') { - userIds += '\n' - } - userIds += user.id - } - - response += '\n' - response += '\n Raw IDs:' - response += '\n' - response += userIds - } - } - - if (shouldCache) logsCache.set(cacheKey, response) - return reply - .status(200) - .header('X-Cache', 'MISS') - .send(response) - }) + fastify.get('/antispam/:id', route$GetAntispam) + fastify.get('/raid/:id', route$GetRaidAsText) + fastify.get('/raid/:id.json', route$GetRaidAsJson) } \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_antispam.ts b/milk/src/routes/get_raid/get_antispam.ts new file mode 100644 index 0000000..4962ab4 --- /dev/null +++ b/milk/src/routes/get_raid/get_antispam.ts @@ -0,0 +1,7 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {RaidParameter} from "../get_raid.js"; + +export async function route$GetAntispam(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + return reply.redirect('/raid/' + encodeURIComponent(id)) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts new file mode 100644 index 0000000..3ebb544 --- /dev/null +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -0,0 +1,28 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; +import {getRaid} from "../../database/raid.js"; +import {getPublicRaidUsers} from "../../database/raid_users.js"; +import {RaidParameter} from "../get_raid.js"; + +export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + return await useCacheWhenPossible(reply, `${id}.json`, async () => { + const raid = await getRaid(id) + + if (raid == null) { + return reply.code(404).send('404 Not Found') + } + + const users = await getPublicRaidUsers(raid.internal_id) + return { + result: JSON.stringify({ + size: users.length, + startedAt: users[0]?.joinedAt, + concludedAt: raid.concluded_at, + guild: raid.guild_id.toString(), + users + }), + shouldCache: users.length !== 0 + } + }) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts new file mode 100644 index 0000000..becffe1 --- /dev/null +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -0,0 +1,53 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; +import {getRaid} from "../../database/raid.js"; +import {getPublicRaidUsers} from "../../database/raid_users.js"; +import {toDateString, toTimeString} from "../../utils/date.js"; +import {Logger} from "@beemobot/common"; +import {TAG} from "../../constants/logging.js"; +import {RaidParameter} from "../get_raid.js"; + +export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + return await useCacheWhenPossible(reply, id, async () => { + const raid = await getRaid(id) + + if (raid == null) { + return reply.code(404).send('404 Not Found') + } + + const users = await getPublicRaidUsers(raid.internal_id) + + let response: string + let startedDate = "N/A" + if (users.length > 0) { + startedDate = toDateString(users[0].joinedAt) + } + + response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; + + if (users.length === 0) { + Logger.warn(TAG, `Raid ${id} reported no users.`) + response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" + } else { + response += '\nRaid size: ' + users.length + ' accounts' + response += '\n' + response += '\n Joined at: ID: Username:' + response += '\n' + let userIds = ''; + for (const user of users) { + response += toTimeString(user.joinedAt) + ' ' + user.id + ' ' + user.name + if (userIds !== '') { + userIds += '\n' + } + userIds += user.id + } + + response += '\n' + response += '\n Raw IDs:' + response += '\n' + response += userIds + } + return {result: response, shouldCache: users.length !== 0} + }) +} \ No newline at end of file From 53d82012e71f496f5fa545f568d48c65083a4f15 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 10:39:49 +0800 Subject: [PATCH 34/47] feat(milk): separate database queries into their own files --- milk/src/database/raid.ts | 46 ++++++++++++++++++-- milk/src/database/raid_users.ts | 9 +++- milk/src/kafka/clients/raids.ts | 27 ++++-------- milk/src/routes/get_raid/get_raid_as_json.ts | 4 +- milk/src/routes/get_raid/get_raid_as_text.ts | 4 +- milk/src/types/raid.ts | 6 +-- 6 files changed, 67 insertions(+), 29 deletions(-) diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts index 0d17992..8373542 100644 --- a/milk/src/database/raid.ts +++ b/milk/src/database/raid.ts @@ -1,10 +1,50 @@ import {prisma} from "../connections/prisma.js"; import {Raid} from "@prisma/client"; +import {randomString} from "../utils/string.js"; /** - * Gets information about a given {@link Raid}. + * Given an {@link externalId}, gets information about a given {@link Raid}. * @param externalId the external id of the raid. * @return {@link Raid} or null if it doesn't exist. */ -export const getRaid = async (externalId: string): Promise => - (await prisma.raid.findUnique({where: {external_id: externalId}})) \ No newline at end of file +export const getRaidByExternalId = async (externalId: string): Promise => + (await prisma.raid.findUnique({where: {external_id: externalId}})) + +/** + * Given an {@link internalId}, gets information about a given {@link Raid}. + * @param internalId the external id of the raid. + * @return {@link Raid} or null if it doesn't exist. + */ +export const getRaidByInternalId = async (internalId: string): Promise => + (await prisma.raid.findUnique({where: {internal_id: internalId}})) + +/** + * Concludes a {@link Raid} in the database. + * + * @param externalId the external id of the raid. + * @param internalId the internal id of the raid. + * @param concludedAt the time when the raid was concluded. + */ +export const concludeRaid = async (externalId: string, internalId: string, concludedAt: Date | null) => + (await prisma.raid.update({ + where: { external_id: externalId, internal_id: internalId }, + data: { concluded_at: concludedAt } + })) + +/** + * Creates a new {@link Raid}. + * + * @param internalId the internal id of the raid. + * @param guildId the id of the guild being raided. + * @param concludedAt the conclusion time of the raid, if provided. + */ +export const createRaid = async (internalId: string, guildId: string, concludedAt: Date | null) => ( + prisma.raid.create({ + data: { + internal_id: internalId, + external_id: randomString(12), + guild_id: BigInt(guildId), + concluded_at: concludedAt + } + }) +) \ No newline at end of file diff --git a/milk/src/database/raid_users.ts b/milk/src/database/raid_users.ts index a5cddee..e6a15be 100644 --- a/milk/src/database/raid_users.ts +++ b/milk/src/database/raid_users.ts @@ -27,4 +27,11 @@ export const getPublicRaidUsers = async (internalId: string): Promise + (await prisma.raidUser.createMany({data: users, skipDuplicates: true})) \ No newline at end of file diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 4014a50..fa1b4c6 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,16 +1,17 @@ import {BrokerClient, BrokerMessage, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {randomString} from "../../utils/string.js"; import {run} from "../../utils/retry.js"; import {RaidManagementData} from "../../types/raid.js"; import {logIssue} from "../../connections/sentry.js"; -import {prisma} from "../../connections/prisma.js"; import {TAG} from "../../constants/logging.js"; import { RAID_MANAGEMENT_BATCH_INSERT_KEY, RAID_MANAGEMENT_CLIENT_TOPIC, RAID_MANAGEMENT_CONCLUDE_RAID } from "../../constants/raid_management_kafka.js"; +import {concludeRaid, createRaid, getRaidByInternalId} from "../../database/raid.js"; +import {RaidUser} from "@prisma/client"; +import {insertRaidUsers} from "../../database/raid_users.js"; export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { @@ -32,7 +33,7 @@ export class RaidManagementClient extends BrokerClient { concludedAt = new Date() } - let raid = await prisma.raid.findUnique({where: {internal_id: raidId}}) + let raid = await getRaidByInternalId(raidId) if (raid == null) { logIssue(`Received a request to conclude a raid, but the raid is not in the database. [raid=${raidId}]`) return @@ -46,10 +47,7 @@ export class RaidManagementClient extends BrokerClient { Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildIdString}.`) raid = await run( 'conclude_raid', - async () => prisma.raid.update({ - where: { external_id: raid!.external_id, internal_id: raidId }, - data: { concluded_at: concludedAt } - }), + async () => concludeRaid(raid!.external_id, raid!.internal_id, concludedAt), 0.2, 25 ) @@ -75,30 +73,23 @@ export class RaidManagementClient extends BrokerClient { avatar_hash: user.avatarHash, created_at: user.createdAt, joined_at: user.joinedAt - } + } satisfies RaidUser }) await run( 'insert_raid_users', - async () => prisma.raidUser.createMany({data: users, skipDuplicates: true}), + async () => insertRaidUsers(users), 2, 25 ) } - let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}) + let raid = await getRaidByInternalId(request.raidId) if (raid == null) { Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) raid = await run( 'create_raid', - async () => prisma.raid.create({ - data: { - internal_id: request.raidId, - external_id: randomString(12), - guild_id: BigInt(request.guildIdString), - concluded_at: request.concludedAt - } - }), + async () => createRaid(request.raidId, request.guildIdString, request.concludedAt), 1, 25 ) diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts index 3ebb544..c38ba6f 100644 --- a/milk/src/routes/get_raid/get_raid_as_json.ts +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -1,13 +1,13 @@ import {FastifyReply, FastifyRequest} from "fastify"; import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; -import {getRaid} from "../../database/raid.js"; +import {getRaidByExternalId} from "../../database/raid.js"; import {getPublicRaidUsers} from "../../database/raid_users.js"; import {RaidParameter} from "../get_raid.js"; export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params return await useCacheWhenPossible(reply, `${id}.json`, async () => { - const raid = await getRaid(id) + const raid = await getRaidByExternalId(id) if (raid == null) { return reply.code(404).send('404 Not Found') diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index becffe1..2a427ea 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -1,6 +1,6 @@ import {FastifyReply, FastifyRequest} from "fastify"; import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; -import {getRaid} from "../../database/raid.js"; +import {getRaidByExternalId} from "../../database/raid.js"; import {getPublicRaidUsers} from "../../database/raid_users.js"; import {toDateString, toTimeString} from "../../utils/date.js"; import {Logger} from "@beemobot/common"; @@ -10,7 +10,7 @@ import {RaidParameter} from "../get_raid.js"; export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params return await useCacheWhenPossible(reply, id, async () => { - const raid = await getRaid(id) + const raid = await getRaidByExternalId(id) if (raid == null) { return reply.code(404).send('404 Not Found') diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index f4ddfa7..691f301 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -7,7 +7,7 @@ export type RaidManagementRequest = { raidId: string, guildIdString: string, users: RaidManagementUser[], - concludedAt: (Date | string) | null + concludedAt: Date | null } export type RaidManagementResponse = { @@ -18,8 +18,8 @@ export type RaidManagementUser = { idString: string, name: string, avatarHash: string | null, - createdAt: Date | string, - joinedAt: Date | string + createdAt: Date, + joinedAt: Date } export type PublicRaidUser = { From ab9aac918bf4581522085304a80f73dd055bfbf3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 10:43:54 +0800 Subject: [PATCH 35/47] feat(milk): fix handling of dates in Kafka client --- milk/src/kafka/clients/raids.ts | 15 ++++++--------- milk/src/types/raid.ts | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index fa1b4c6..15678ed 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -27,11 +27,7 @@ export class RaidManagementClient extends BrokerClient { } let {raidId, concludedAt, guildIdString} = message.value.request - if (concludedAt == null) { - // Assume that the raid was just concluded, theoretically, the raid just concluded the moment we receive - // this message as there is no other reason to send a message to this key if not for concluding a raid. - concludedAt = new Date() - } + let conclusionDate: Date = new Date(concludedAt ?? Date.now()) let raid = await getRaidByInternalId(raidId) if (raid == null) { @@ -47,7 +43,7 @@ export class RaidManagementClient extends BrokerClient { Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildIdString}.`) raid = await run( 'conclude_raid', - async () => concludeRaid(raid!.external_id, raid!.internal_id, concludedAt), + async () => concludeRaid(raid!.external_id, raid!.internal_id, conclusionDate), 0.2, 25 ) @@ -71,8 +67,8 @@ export class RaidManagementClient extends BrokerClient { user_id: BigInt(user.idString), name: user.name, avatar_hash: user.avatarHash, - created_at: user.createdAt, - joined_at: user.joinedAt + created_at: new Date(user.createdAt), + joined_at: new Date(user.joinedAt) } satisfies RaidUser }) @@ -86,10 +82,11 @@ export class RaidManagementClient extends BrokerClient { let raid = await getRaidByInternalId(request.raidId) if (raid == null) { + let conclusionDate = new Date(request.concludedAt ?? Date.now()) Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) raid = await run( 'create_raid', - async () => createRaid(request.raidId, request.guildIdString, request.concludedAt), + async () => createRaid(request.raidId, request.guildIdString, conclusionDate), 1, 25 ) diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index 691f301..09944e5 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -7,7 +7,7 @@ export type RaidManagementRequest = { raidId: string, guildIdString: string, users: RaidManagementUser[], - concludedAt: Date | null + concludedAt: string | null } export type RaidManagementResponse = { @@ -18,8 +18,8 @@ export type RaidManagementUser = { idString: string, name: string, avatarHash: string | null, - createdAt: Date, - joinedAt: Date + createdAt: string, + joinedAt: string } export type PublicRaidUser = { From 9de2f1bed1b5fbea86023286d398123142ac84d6 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 22:58:06 +0800 Subject: [PATCH 36/47] feat(milk): fix prisma schema --- milk/prisma/schema.prisma | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/milk/prisma/schema.prisma b/milk/prisma/schema.prisma index 5b623dc..afeb395 100644 --- a/milk/prisma/schema.prisma +++ b/milk/prisma/schema.prisma @@ -8,17 +8,21 @@ datasource db { } model RaidUser { - internal_raid_id String @db.Uuid - user_id BigInt @unique - name String - avatar_hash String? - created_at DateTime @db.Timestamptz - joined_at DateTime @db.Timestamptz + raid Raid? @relation(fields: [internal_raid_id], references: [internal_id]) + internal_raid_id String @db.Uuid + user_id BigInt + name String + avatar_hash String? + created_at DateTime @db.Timestamptz + joined_at DateTime @db.Timestamptz + + @@id([internal_raid_id, user_id]) } model Raid { - internal_id String @unique @db.Uuid - external_id String @unique - guild_id BigInt - concluded_at DateTime? @db.Timestamptz -} \ No newline at end of file + internal_id String @unique @db.Uuid + external_id String @unique + guild_id BigInt + concluded_at DateTime? @db.Timestamptz + users RaidUser[] +} From af3473ea4861b0b870bebb552e63f0f2d85205aa Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 23:01:25 +0800 Subject: [PATCH 37/47] feat(milk): update prisma to latest version --- milk/package.json | 4 +- milk/yarn.lock | 288 ++++++++++++++++++++++++---------------------- 2 files changed, 154 insertions(+), 138 deletions(-) diff --git a/milk/package.json b/milk/package.json index 929c7a6..ea02566 100644 --- a/milk/package.json +++ b/milk/package.json @@ -8,7 +8,7 @@ "type": "module", "dependencies": { "@beemobot/common": "^1.0.0", - "@prisma/client": "4.8.1", + "@prisma/client": "^5.5.2", "@sentry/node": "^7.29.0", "dotenv": "^16.0.3", "fastify": "^4.11.0", @@ -22,7 +22,7 @@ }, "devDependencies": { "@types/node": "^18.11.18", - "prisma": "^4.8.1", + "prisma": "^5.5.2", "rimraf": "^5.0.1" } } diff --git a/milk/yarn.lock b/milk/yarn.lock index f7f6495..6d0d582 100644 --- a/milk/yarn.lock +++ b/milk/yarn.lock @@ -4,14 +4,14 @@ "@beemobot/common@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@beemobot/common/-/common-1.0.0.tgz#d7b4ef1c47d9d3974b81433760ca70aa7cdb8167" + resolved "https://registry.npmjs.org/@beemobot/common/-/common-1.0.0.tgz" integrity sha512-7cnly7AIW+Ag1/NfQ8Ji1o6WcXLecDQDYLR18GwFhf2k9Hva9oZo6u2e/qAed0Ixij1/pJzE53DZHAczgvQaSA== dependencies: kafkajs "^2.2.3" "@fastify/ajv-compiler@^3.3.1": version "3.5.0" - resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz#459bff00fefbf86c96ec30e62e933d2379e46670" + resolved "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz" integrity sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA== dependencies: ajv "^8.11.0" @@ -20,24 +20,24 @@ "@fastify/deepmerge@^1.0.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" + resolved "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz" integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== "@fastify/error@^3.0.0": version "3.2.0" - resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.2.0.tgz#9010e0acfe07965f5fc7d2b367f58f042d0f4106" + resolved "https://registry.npmjs.org/@fastify/error/-/error-3.2.0.tgz" integrity sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ== "@fastify/fast-json-stringify-compiler@^4.1.0": version "4.2.0" - resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.2.0.tgz#52d047fac76b0d75bd660f04a5dd606659f57c5a" + resolved "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.2.0.tgz" integrity sha512-ypZynRvXA3dibfPykQN3RB5wBdEUgSGgny8Qc6k163wYPLD4mEGEDkACp+00YmqkGvIm8D/xYoHajwyEdWD/eg== dependencies: fast-json-stringify "^5.0.0" "@isaacs/cliui@^8.0.2": version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: string-width "^5.1.2" @@ -49,29 +49,29 @@ "@pkgjs/parseargs@^0.11.0": version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.8.1.tgz#51c16488dfac4e74a275a2753bf20262a65f2a2b" - integrity sha512-d4xhZhETmeXK/yZ7K0KcVOzEfI5YKGGEr4F5SBV04/MU4ncN/HcE28sy3e4Yt8UFW0ZuImKFQJE+9rWt9WbGSQ== +"@prisma/client@^5.5.2": + version "5.5.2" + resolved "https://registry.npmjs.org/@prisma/client/-/client-5.5.2.tgz" + integrity sha512-54XkqR8M+fxbzYqe+bIXimYnkkcGqgOh0dn0yWtIk6CQT4IUCAvNFNcQZwk2KqaLU+/1PHTSWrcHtx4XjluR5w== dependencies: - "@prisma/engines-version" "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" + "@prisma/engines-version" "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" -"@prisma/engines-version@4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe": - version "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz#30401aba1029e7d32e3cb717e705a7c92ccc211e" - integrity sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw== +"@prisma/engines-version@5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a": + version "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" + resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a.tgz" + integrity sha512-O+qHFnZvAyOFk1tUco2/VdiqS0ym42a3+6CYLScllmnpbyiTplgyLt2rK/B9BTjYkSHjrgMhkG47S0oqzdIckA== -"@prisma/engines@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.8.1.tgz#8428f7dcd7912c6073024511376595017630dc85" - integrity sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw== +"@prisma/engines@5.5.2": + version "5.5.2" + resolved "https://registry.npmjs.org/@prisma/engines/-/engines-5.5.2.tgz" + integrity sha512-Be5hoNF8k+lkB3uEMiCHbhbfF6aj1GnrTBnn5iYFT7GEr3TsOEp1soviEcBR0tYCgHbxjcIxJMhdbvxALJhAqg== "@sentry/core@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.29.0.tgz#bc4b54d56cf7652598d4430cf43ea97cc069f6fe" + resolved "https://registry.npmjs.org/@sentry/core/-/core-7.29.0.tgz" integrity sha512-+e9aIp2ljtT4EJq3901z6TfEVEeqZd5cWzbKEuQzPn2UO6If9+Utd7kY2Y31eQYb4QnJgZfiIEz1HonuYY6zqQ== dependencies: "@sentry/types" "7.29.0" @@ -80,7 +80,7 @@ "@sentry/node@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.29.0.tgz#721aab15faef98f291b5a3fcb9b303565deb1e74" + resolved "https://registry.npmjs.org/@sentry/node/-/node-7.29.0.tgz" integrity sha512-s/bN/JS5gPTmwzVms4FtI5YNYtC9aGY4uqdx/llVrIiVv7G6md/oJJzKtO1C4dt6YshjGjSs5KCpEn1NM4+1iA== dependencies: "@sentry/core" "7.29.0" @@ -93,12 +93,12 @@ "@sentry/types@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.29.0.tgz#ed829b6014ee19049035fec6af2b4fea44ff28b8" + resolved "https://registry.npmjs.org/@sentry/types/-/types-7.29.0.tgz" integrity sha512-DmoEpoqHPty3VxqubS/5gxarwebHRlcBd/yuno+PS3xy++/i9YPjOWLZhU2jYs1cW68M9R6CcCOiC9f2ckJjdw== "@sentry/utils@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.29.0.tgz#cbf8f87dd851b0fdc7870db9c68014c321c3bab8" + resolved "https://registry.npmjs.org/@sentry/utils/-/utils-7.29.0.tgz" integrity sha512-ICcBwTiBGK8NQA8H2BJo0JcMN6yCeKLqNKNMVampRgS6wSfSk1edvcTdhRkW3bSktIGrIPZrKskBHyMwDGF2XQ== dependencies: "@sentry/types" "7.29.0" @@ -106,38 +106,38 @@ "@types/node@^18.11.18": version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + resolved "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz" integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== abort-controller@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== dependencies: event-target-shim "^5.0.0" abstract-logging@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz" integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0: version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" @@ -147,39 +147,39 @@ ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0: ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== ansi-styles@^4.0.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^6.1.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== archy@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== atomic-sleep@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== avvio@^8.2.0: version "8.2.0" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.2.0.tgz#aff28b0266617bf07ffc1c2d5f4220c3663ce1c2" + resolved "https://registry.npmjs.org/avvio/-/avvio-8.2.0.tgz" integrity sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg== dependencies: archy "^1.0.0" @@ -188,24 +188,24 @@ avvio@^8.2.0: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -213,95 +213,95 @@ buffer@^6.0.3: clone@2.x: version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== content-type@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== cookie@^0.4.1: version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== cookie@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== cross-spawn@^7.0.0: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -debug@4, debug@^4.0.0: +debug@^4.0.0, debug@4: version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" dotenv@^16.0.3: version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== event-target-shim@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== events@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== fast-decode-uri-component@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-json-stringify@^5.0.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.5.0.tgz#6655cb944df8da43f6b15312a9564b81c55dadab" + resolved "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.5.0.tgz" integrity sha512-rmw2Z8/mLkND8zI+3KTYIkNPEoF5v6GqDP/o+g7H3vjdWjBwuKpgAYFHIzL6ORRB+iqDjjtJnLIW9Mzxn5szOA== dependencies: "@fastify/deepmerge" "^1.0.0" @@ -313,24 +313,24 @@ fast-json-stringify@^5.0.0: fast-querystring@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.0.tgz#bb645c365db88a3b6433fb6484f7e9e66764cfb9" + resolved "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.0.tgz" integrity sha512-LWkjBCZlxjnSanuPpZ6mHswjy8hQv3VcPJsQB3ltUF2zjvrycr0leP3TSTEEfvQ1WEMSRl5YNsGqaft9bjLqEw== dependencies: fast-decode-uri-component "^1.0.1" fast-redact@^3.1.1: version "3.1.2" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" + resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz" integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== fast-uri@^2.0.0, fast-uri@^2.1.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz" integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg== fastify@^4.11.0: version "4.11.0" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.11.0.tgz#7fa5614c81a618e67a7a467f0f1b33c43f4ff7d2" + resolved "https://registry.npmjs.org/fastify/-/fastify-4.11.0.tgz" integrity sha512-JteZ8pjEqd+6n+azQnQfSJV8MUMxAmxbvC2Dx/Mybj039Lf/u3kda9Kq84uy/huCpqCzZoyHIZS5JFGF3wLztw== dependencies: "@fastify/ajv-compiler" "^3.3.1" @@ -351,14 +351,14 @@ fastify@^4.11.0: fastq@^1.6.1: version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" find-my-way@^7.3.0: version "7.4.0" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-7.4.0.tgz#22363e6cd1c466f88883703e169a20c983f9c9cc" + resolved "https://registry.npmjs.org/find-my-way/-/find-my-way-7.4.0.tgz" integrity sha512-JFT7eURLU5FumlZ3VBGnveId82cZz7UR7OUu+THQJOwdQXxmS/g8v0KLoFhv97HreycOrmAbqjXD/4VG2j0uMQ== dependencies: fast-deep-equal "^3.1.3" @@ -367,7 +367,7 @@ find-my-way@^7.3.0: foreground-child@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: cross-spawn "^7.0.0" @@ -375,12 +375,12 @@ foreground-child@^3.1.0: forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== glob@^10.2.5: version "10.2.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.6.tgz#1e27edbb3bbac055cb97113e27a066c100a4e5e1" + resolved "https://registry.npmjs.org/glob/-/glob-10.2.6.tgz" integrity sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA== dependencies: foreground-child "^3.1.0" @@ -391,7 +391,7 @@ glob@^10.2.5: https-proxy-agent@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -399,27 +399,27 @@ https-proxy-agent@^5.0.0: ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== jackspeak@^2.0.3: version "2.2.1" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz" integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw== dependencies: "@isaacs/cliui" "^8.0.2" @@ -428,77 +428,77 @@ jackspeak@^2.0.3: json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== kafkajs@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.3.tgz#a1ab1b7c4a27699871a89b3978b5cfe5b05c6f3e" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.3.tgz" integrity sha512-JmzIiLHE/TdQ7b4I2B/DNMtfhTh66fmEaEg7gGkyQXBC6f1A7I2jSjeUsVIJfC8d9YcEIURyBjtOEKBO5OHVhg== light-my-request@^5.6.1: version "5.8.0" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.8.0.tgz#93b28615d4cd134b4e2370bcf2ff7e35b51c8d29" + resolved "https://registry.npmjs.org/light-my-request/-/light-my-request-5.8.0.tgz" integrity sha512-4BtD5C+VmyTpzlDPCZbsatZMJVgUIciSOwYhJDCbLffPZ35KoDkDj4zubLeHDEb35b4kkPeEv5imbh+RJxK/Pg== dependencies: cookie "^0.5.0" process-warning "^2.0.0" set-cookie-parser "^2.4.1" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" lru-cache@^9.1.1: version "9.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz" integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A== -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - minimatch@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz" integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== dependencies: brace-expansion "^2.0.1" "minipass@^5.0.0 || ^6.0.2": version "6.0.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81" + resolved "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz" integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w== ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== node-cache@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz" integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== dependencies: clone "2.x" on-exit-leak-free@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz" integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-scurry@^1.7.0: version "1.9.2" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.9.2.tgz#90f9d296ac5e37e608028e28a447b11d385b3f63" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz" integrity sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg== dependencies: lru-cache "^9.1.1" @@ -506,7 +506,7 @@ path-scurry@^1.7.0: pino-abstract-transport@v1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz" integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== dependencies: readable-stream "^4.0.0" @@ -514,12 +514,12 @@ pino-abstract-transport@v1.0.0: pino-std-serializers@^6.0.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844" + resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz" integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== pino@^8.5.0: version "8.8.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.8.0.tgz#1f0d6695a224aa06afc7ad60f2ccc4772d3b9233" + resolved "https://registry.npmjs.org/pino/-/pino-8.8.0.tgz" integrity sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ== dependencies: atomic-sleep "^1.0.0" @@ -534,26 +534,26 @@ pino@^8.5.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -prisma@^4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.8.1.tgz#ef93cd908809b7d02e9f4bead5eea7733ba50c68" - integrity sha512-ZMLnSjwulIeYfaU1O6/LF6PEJzxN5par5weykxMykS9Z6ara/j76JH3Yo2AH3bgJbPN4Z6NeCK9s5fDkzf33cg== +prisma@*, prisma@^5.5.2: + version "5.5.2" + resolved "https://registry.npmjs.org/prisma/-/prisma-5.5.2.tgz" + integrity sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w== dependencies: - "@prisma/engines" "4.8.1" + "@prisma/engines" "5.5.2" process-warning@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.1.0.tgz" integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg== process@^0.11.10: version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== proxy-addr@^2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -561,17 +561,17 @@ proxy-addr@^2.0.7: punycode@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== quick-format-unescaped@^4.0.3: version "4.0.4" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== readable-stream@^4.0.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz" integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ== dependencies: abort-controller "^3.0.0" @@ -581,97 +581,106 @@ readable-stream@^4.0.0: real-require@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== ret@~0.2.0: version "0.2.2" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" + resolved "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.2.0, rfdc@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== rimraf@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz" integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg== dependencies: glob "^10.2.5" safe-regex2@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" + resolved "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz" integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== dependencies: ret "~0.2.0" safe-stable-stringify@^2.3.1: version "2.4.2" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz" integrity sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA== secure-json-parse@^2.5.0: version "2.6.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.6.0.tgz#95d89f84adf32d76ff7800e68a673b129fe918b0" + resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.6.0.tgz" integrity sha512-B9osKohb6L+EZ6Kve3wHKfsAClzOC/iISA2vSuCe5Jx5NAKiwitfxx8ZKYapHXr0sYRj7UZInT7pLb3rp2Yx6A== semver@^7.3.7: version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" set-cookie-parser@^2.4.1: version "2.5.1" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz" integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz" integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== sonic-boom@^3.1.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38" + resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.1.tgz" integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A== dependencies: atomic-sleep "^1.0.0" split2@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + resolved "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz" integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -680,66 +689,73 @@ split2@^4.0.0: string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: eastasianwidth "^0.2.0" emoji-regex "^9.2.2" strip-ansi "^7.0.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== dependencies: ansi-regex "^6.0.1" thread-stream@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz" integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== dependencies: real-require "^0.2.0" tiny-lru@^10.0.0: version "10.0.1" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-10.0.1.tgz#aaf5d22207e641ed1b176ac2e616d6cc2fc9ef66" + resolved "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.0.1.tgz" integrity sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA== tslib@^1.9.3: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== typescript@^4.9.4: version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -748,7 +764,7 @@ which@^2.0.1: wrap-ansi@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: ansi-styles "^6.1.0" @@ -757,5 +773,5 @@ wrap-ansi@^8.1.0: yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 1d4d2aef2e4d640131855be55a862fb7cbcca519 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 23:29:14 +0800 Subject: [PATCH 38/47] feat(milk): use relationships, rename fields and update caching logic --- milk/prisma/schema.prisma | 20 +++++------ milk/src/database/raid.ts | 35 +++++++++++--------- milk/src/database/raid_users.ts | 32 ++++++------------ milk/src/routes/get_raid/get_raid_as_json.ts | 10 +++--- milk/src/routes/get_raid/get_raid_as_text.ts | 20 ++++++----- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/milk/prisma/schema.prisma b/milk/prisma/schema.prisma index afeb395..8ac4251 100644 --- a/milk/prisma/schema.prisma +++ b/milk/prisma/schema.prisma @@ -8,20 +8,20 @@ datasource db { } model RaidUser { - raid Raid? @relation(fields: [internal_raid_id], references: [internal_id]) - internal_raid_id String @db.Uuid - user_id BigInt - name String - avatar_hash String? - created_at DateTime @db.Timestamptz - joined_at DateTime @db.Timestamptz + raid Raid? @relation(fields: [raid_id], references: [id]) + raid_id String @db.Uuid + id BigInt + name String + avatar_hash String? + created_at DateTime @db.Timestamptz + joined_at DateTime @db.Timestamptz - @@id([internal_raid_id, user_id]) + @@id([raid_id, id]) } model Raid { - internal_id String @unique @db.Uuid - external_id String @unique + id String @unique @db.Uuid + public_id String @unique guild_id BigInt concluded_at DateTime? @db.Timestamptz users RaidUser[] diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts index 8373542..cc35ea9 100644 --- a/milk/src/database/raid.ts +++ b/milk/src/database/raid.ts @@ -3,46 +3,49 @@ import {Raid} from "@prisma/client"; import {randomString} from "../utils/string.js"; /** - * Given an {@link externalId}, gets information about a given {@link Raid}. - * @param externalId the external id of the raid. + * Given an {@link externalId}, gets information about a given {@link Raid} with the {@link RaidUser} + * relation included. + * + * @param publicId the external id of the raid. * @return {@link Raid} or null if it doesn't exist. */ -export const getRaidByExternalId = async (externalId: string): Promise => - (await prisma.raid.findUnique({where: {external_id: externalId}})) +export const getRaidByPublicId = async (publicId: string) => { + return (await prisma.raid.findUnique({where: {public_id: publicId}, include: {users: true}})); +} /** - * Given an {@link internalId}, gets information about a given {@link Raid}. - * @param internalId the external id of the raid. + * Given an {@link id}, gets information about a given {@link Raid}. + * @param id the external id of the raid. * @return {@link Raid} or null if it doesn't exist. */ -export const getRaidByInternalId = async (internalId: string): Promise => - (await prisma.raid.findUnique({where: {internal_id: internalId}})) +export const getRaidByInternalId = async (id: string): Promise => + (await prisma.raid.findUnique({where: {id}})) /** * Concludes a {@link Raid} in the database. * - * @param externalId the external id of the raid. - * @param internalId the internal id of the raid. + * @param publicId the public id of the raid. + * @param id the internal id of the raid. * @param concludedAt the time when the raid was concluded. */ -export const concludeRaid = async (externalId: string, internalId: string, concludedAt: Date | null) => +export const concludeRaid = async (publicId: string, id: string, concludedAt: Date | null) => (await prisma.raid.update({ - where: { external_id: externalId, internal_id: internalId }, + where: { id, public_id: publicId }, data: { concluded_at: concludedAt } })) /** * Creates a new {@link Raid}. * - * @param internalId the internal id of the raid. + * @param id the internal id of the raid. * @param guildId the id of the guild being raided. * @param concludedAt the conclusion time of the raid, if provided. */ -export const createRaid = async (internalId: string, guildId: string, concludedAt: Date | null) => ( +export const createRaid = async (id: string, guildId: string, concludedAt: Date | null) => ( prisma.raid.create({ data: { - internal_id: internalId, - external_id: randomString(12), + id: id, + public_id: randomString(12), guild_id: BigInt(guildId), concluded_at: concludedAt } diff --git a/milk/src/database/raid_users.ts b/milk/src/database/raid_users.ts index e6a15be..45b330e 100644 --- a/milk/src/database/raid_users.ts +++ b/milk/src/database/raid_users.ts @@ -1,33 +1,23 @@ import {prisma} from "../connections/prisma.js"; import {PublicRaidUser} from "../types/raid.js"; -import {RaidUser} from "@prisma/client"; - -/** - * Gets the users involved in a {@link Raid}. - * - * @param internalId the internal id - * @return the users involved in a {@link Raid}. - */ -export const getRaidUsers = async (internalId: string): Promise => - (await prisma.raidUser.findMany({where: {internal_raid_id: internalId}})) +import {Raid, RaidUser} from "@prisma/client"; /** * Gets the users involved in a {@link Raid} as a {@link PublicRaidUser} type which does not include the * `internal_raid_id`. * - * @param internalId the internal id + * @param raid the raid to get the data from * @return the users involved in a {@link Raid}. */ -export const getPublicRaidUsers = async (internalId: string): Promise => (await getRaidUsers(internalId)) - .map(user => { - return { - id: user.user_id.toString(), - name: user.name, - joinedAt: user.joined_at, - createdAt: user.created_at, - avatarHash: user.avatar_hash - } satisfies PublicRaidUser - }) +export const getPublicRaidUsers = (raid: { users: RaidUser[] } & Raid): PublicRaidUser[] => raid.users.map(user => { + return { + id: user.id.toString(), + name: user.name, + joinedAt: user.joined_at, + createdAt: user.created_at, + avatarHash: user.avatar_hash + } satisfies PublicRaidUser +}) /** * Inserts many {@link RaidUser} to the database. diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts index c38ba6f..4d83a37 100644 --- a/milk/src/routes/get_raid/get_raid_as_json.ts +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -1,19 +1,19 @@ import {FastifyReply, FastifyRequest} from "fastify"; import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; -import {getRaidByExternalId} from "../../database/raid.js"; -import {getPublicRaidUsers} from "../../database/raid_users.js"; +import {getRaidByPublicId} from "../../database/raid.js"; import {RaidParameter} from "../get_raid.js"; +import {getPublicRaidUsers} from "../../database/raid_users.js"; export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params return await useCacheWhenPossible(reply, `${id}.json`, async () => { - const raid = await getRaidByExternalId(id) + const raid = await getRaidByPublicId(id) if (raid == null) { return reply.code(404).send('404 Not Found') } - const users = await getPublicRaidUsers(raid.internal_id) + const users = getPublicRaidUsers(raid) return { result: JSON.stringify({ size: users.length, @@ -22,7 +22,7 @@ export async function route$GetRaidAsJson(request: FastifyRequest guild: raid.guild_id.toString(), users }), - shouldCache: users.length !== 0 + shouldCache: raid.concluded_at != null || users.length > 2_000 } }) } \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index 2a427ea..6de2ae7 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -1,7 +1,6 @@ import {FastifyReply, FastifyRequest} from "fastify"; import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; -import {getRaidByExternalId} from "../../database/raid.js"; -import {getPublicRaidUsers} from "../../database/raid_users.js"; +import {getRaidByPublicId} from "../../database/raid.js"; import {toDateString, toTimeString} from "../../utils/date.js"; import {Logger} from "@beemobot/common"; import {TAG} from "../../constants/logging.js"; @@ -10,18 +9,20 @@ import {RaidParameter} from "../get_raid.js"; export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params return await useCacheWhenPossible(reply, id, async () => { - const raid = await getRaidByExternalId(id) + const raid = await getRaidByPublicId(id) if (raid == null) { return reply.code(404).send('404 Not Found') } - const users = await getPublicRaidUsers(raid.internal_id) + const users = raid.users let response: string let startedDate = "N/A" - if (users.length > 0) { - startedDate = toDateString(users[0].joinedAt) + + const firstUser = users.at(0) + if (firstUser != null) { + startedDate = toDateString(firstUser.joined_at) } response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; @@ -36,7 +37,7 @@ export async function route$GetRaidAsText(request: FastifyRequest response += '\n' let userIds = ''; for (const user of users) { - response += toTimeString(user.joinedAt) + ' ' + user.id + ' ' + user.name + response += toTimeString(user.joined_at) + ' ' + user.id + ' ' + user.name if (userIds !== '') { userIds += '\n' } @@ -48,6 +49,9 @@ export async function route$GetRaidAsText(request: FastifyRequest response += '\n' response += userIds } - return {result: response, shouldCache: users.length !== 0} + return { + result: response, + shouldCache: raid.concluded_at != null || users.length > 2_000 + } }) } \ No newline at end of file From 085b6cc3b8570949986033c29e50b5b250fd0148 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 2 Nov 2023 00:20:44 +0800 Subject: [PATCH 39/47] feat(milk): fix improper renaming of fields and fix missing `public_id` --- milk/package.json | 5 +++++ milk/prisma/.gitignore | 2 ++ milk/prisma/schema.prisma | 4 ++-- milk/src/connections/fastify.ts | 8 +++----- milk/src/database/raid.ts | 4 ++-- milk/src/kafka/clients/raids.ts | 10 +++++----- milk/src/types/raid.ts | 2 +- milk/yarn.lock | 5 +++++ 8 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 milk/prisma/.gitignore diff --git a/milk/package.json b/milk/package.json index ea02566..5bf8dd4 100644 --- a/milk/package.json +++ b/milk/package.json @@ -12,17 +12,22 @@ "@sentry/node": "^7.29.0", "dotenv": "^16.0.3", "fastify": "^4.11.0", + "fastify-plugin": "^4.5.1", "node-cache": "^5.1.2", "typescript": "^4.9.4" }, "scripts": { "build": "rimraf dist && tsc", "serve": "node --enable-source-maps dist/index.js", + "dev": "rimraf dist && tsc && node --enable-source-maps dist/index.js", "clean": "rimraf dist" }, "devDependencies": { "@types/node": "^18.11.18", "prisma": "^5.5.2", "rimraf": "^5.0.1" + }, + "prisma": { + "seed": "node ./prisma/seed.js" } } diff --git a/milk/prisma/.gitignore b/milk/prisma/.gitignore new file mode 100644 index 0000000..7eafe31 --- /dev/null +++ b/milk/prisma/.gitignore @@ -0,0 +1,2 @@ +# Temporarily ignored until a better seeding method is present. +seed.js \ No newline at end of file diff --git a/milk/prisma/schema.prisma b/milk/prisma/schema.prisma index 8ac4251..7ed2692 100644 --- a/milk/prisma/schema.prisma +++ b/milk/prisma/schema.prisma @@ -9,7 +9,7 @@ datasource db { model RaidUser { raid Raid? @relation(fields: [raid_id], references: [id]) - raid_id String @db.Uuid + raid_id String id BigInt name String avatar_hash String? @@ -20,7 +20,7 @@ model RaidUser { } model Raid { - id String @unique @db.Uuid + id String @unique public_id String @unique guild_id BigInt concluded_at DateTime? @db.Timestamptz diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 9e51099..ef0cb19 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -22,11 +22,9 @@ export async function initializeFastify() { return } - server.register(fastify => { - for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { - attachable(fastify) - } - }) + for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { + attachable(server) + } const port = Number.parseInt(process.env.SERVER_PORT) await server.listen({ diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts index cc35ea9..9aae2aa 100644 --- a/milk/src/database/raid.ts +++ b/milk/src/database/raid.ts @@ -3,7 +3,7 @@ import {Raid} from "@prisma/client"; import {randomString} from "../utils/string.js"; /** - * Given an {@link externalId}, gets information about a given {@link Raid} with the {@link RaidUser} + * Given an {@link publicId}, gets information about a given {@link Raid} with the {@link RaidUser} * relation included. * * @param publicId the external id of the raid. @@ -42,7 +42,7 @@ export const concludeRaid = async (publicId: string, id: string, concludedAt: Da * @param concludedAt the conclusion time of the raid, if provided. */ export const createRaid = async (id: string, guildId: string, concludedAt: Date | null) => ( - prisma.raid.create({ + await prisma.raid.create({ data: { id: id, public_id: randomString(12), diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 15678ed..7084bf1 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -43,12 +43,12 @@ export class RaidManagementClient extends BrokerClient { Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildIdString}.`) raid = await run( 'conclude_raid', - async () => concludeRaid(raid!.external_id, raid!.internal_id, conclusionDate), + async () => concludeRaid(raid!.public_id, raid!.id, conclusionDate), 0.2, 25 ) - await message.respond({ response: { externalId: raid!.external_id }, request: null }) + await message.respond({ response: { publicId: raid!.public_id }, request: null }) } private async onBatchInsertRaidUsers(message: BrokerMessage) { @@ -63,8 +63,8 @@ export class RaidManagementClient extends BrokerClient { Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) const users = request.users.map((user) => { return { - internal_raid_id: request.raidId, - user_id: BigInt(user.idString), + raid_id: request.raidId, + id: BigInt(user.idString), name: user.name, avatar_hash: user.avatarHash, created_at: new Date(user.createdAt), @@ -92,6 +92,6 @@ export class RaidManagementClient extends BrokerClient { ) } - await message.respond({ response: { externalId: raid!.external_id }, request: null }) + await message.respond({ response: { publicId: raid!.public_id }, request: null }) } } \ No newline at end of file diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index 09944e5..5240069 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -11,7 +11,7 @@ export type RaidManagementRequest = { } export type RaidManagementResponse = { - externalId: string + publicId: string } export type RaidManagementUser = { diff --git a/milk/yarn.lock b/milk/yarn.lock index 6d0d582..7a984b2 100644 --- a/milk/yarn.lock +++ b/milk/yarn.lock @@ -328,6 +328,11 @@ fast-uri@^2.0.0, fast-uri@^2.1.0: resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz" integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg== +fastify-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz" + integrity sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ== + fastify@^4.11.0: version "4.11.0" resolved "https://registry.npmjs.org/fastify/-/fastify-4.11.0.tgz" From dea97f2a69746bb338d0f01dc1074fb64fdb47c1 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 2 Nov 2023 00:33:53 +0800 Subject: [PATCH 40/47] feat(milk): fix formatting of text route and send `Content-Type` headers --- milk/src/fastify/serve_cached.ts | 3 +++ milk/src/routes/get_raid/get_raid_as_json.ts | 2 +- milk/src/routes/get_raid/get_raid_as_text.ts | 18 +++++++++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/milk/src/fastify/serve_cached.ts b/milk/src/fastify/serve_cached.ts index 836f41c..c1ddc1e 100644 --- a/milk/src/fastify/serve_cached.ts +++ b/milk/src/fastify/serve_cached.ts @@ -6,6 +6,7 @@ const cache = new NodeCache({ stdTTL: TEN_MINUTES }) export const useCacheWhenPossible = async ( reply: FastifyReply, key: string, + contentType: string, computation: () => Promise<{ result: string, shouldCache: boolean }> ): Promise => { const cachedResult = cache.get(key) @@ -13,6 +14,7 @@ export const useCacheWhenPossible = async ( return reply .header('X-Cache', 'HIT') .header('X-Cache-Expires', cache.getTtl(key)) + .header('Content-Type', contentType) .send(cachedResult) } @@ -23,5 +25,6 @@ export const useCacheWhenPossible = async ( return reply .status(200) .header('X-Cache', 'MISS') + .header('Content-Type', contentType) .send(result) } \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts index 4d83a37..38dc0c0 100644 --- a/milk/src/routes/get_raid/get_raid_as_json.ts +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -6,7 +6,7 @@ import {getPublicRaidUsers} from "../../database/raid_users.js"; export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params - return await useCacheWhenPossible(reply, `${id}.json`, async () => { + return await useCacheWhenPossible(reply, `${id}.json`, 'application/json', async () => { const raid = await getRaidByPublicId(id) if (raid == null) { diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index 6de2ae7..bf42cfb 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -8,7 +8,7 @@ import {RaidParameter} from "../get_raid.js"; export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params - return await useCacheWhenPossible(reply, id, async () => { + return await useCacheWhenPossible(reply, id, 'text', async () => { const raid = await getRaidByPublicId(id) if (raid == null) { @@ -37,10 +37,10 @@ export async function route$GetRaidAsText(request: FastifyRequest response += '\n' let userIds = ''; for (const user of users) { - response += toTimeString(user.joined_at) + ' ' + user.id + ' ' + user.name - if (userIds !== '') { - userIds += '\n' - } + response += '\n' + response += toTimeString(user.joined_at) + " " + user.id + spaces((18 - user.id.toString().length + 3)) + user.name + + userIds += '\n' userIds += user.id } @@ -54,4 +54,12 @@ export async function route$GetRaidAsText(request: FastifyRequest shouldCache: raid.concluded_at != null || users.length > 2_000 } }) +} + +function spaces(num: number): string { + let whitespace = "" + while (whitespace.length < num) { + whitespace += " " + } + return whitespace } \ No newline at end of file From 729fccb3ebf386ab58df366dd076ebe42ce0f6e3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 2 Nov 2023 00:38:55 +0800 Subject: [PATCH 41/47] feat(milk): properly format numbers in text format --- milk/src/routes/get_raid/get_raid_as_text.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index bf42cfb..38672d1 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -6,6 +6,8 @@ import {Logger} from "@beemobot/common"; import {TAG} from "../../constants/logging.js"; import {RaidParameter} from "../get_raid.js"; +const numberFormatter = new Intl.NumberFormat('en-US') + export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params return await useCacheWhenPossible(reply, id, 'text', async () => { @@ -31,7 +33,7 @@ export async function route$GetRaidAsText(request: FastifyRequest Logger.warn(TAG, `Raid ${id} reported no users.`) response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" } else { - response += '\nRaid size: ' + users.length + ' accounts' + response += '\nRaid size: ' + numberFormatter.format(users.length) + ' accounts' response += '\n' response += '\n Joined at: ID: Username:' response += '\n' From b000f0b7f224c7de1558e18e1b43ccf2ed08fa30 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 2 Nov 2023 10:29:54 +0800 Subject: [PATCH 42/47] feat(milk): resource-efficiency related changes, commit details for more info. In this commit, we introduce pagination for the JSON API and a limiting of displayed users to 2,000 users for the Text API, in doing so, we performed additional changes to our queries and our schema, adding a `created_at` property to `Raid` and indexing `joined_at` for `RaidUser`. Furthermore, we now grab `RaidUser` and the total count of the `Raid` at the same time. Documentations have also been updated accordingly to accomodate these changes with an additional new documentation introduced to discuss the JSON API. --- milk/docs/{ => [beemo-devs]}/kafka.md | 0 milk/docs/json_api.md | 201 +++++++++++++++++++ milk/docs/openapi.yaml | 37 +++- milk/prisma/schema.prisma | 2 + milk/src/constants/errors.ts | 2 + milk/src/constants/links.ts | 1 + milk/src/constants/pagination.ts | 1 + milk/src/database/raid.ts | 2 +- milk/src/database/raid_users.ts | 52 ++++- milk/src/fastify/serve_cached.ts | 7 +- milk/src/routes/get_raid.ts | 7 +- milk/src/routes/get_raid/get_raid_as_json.ts | 45 +++-- milk/src/routes/get_raid/get_raid_as_text.ts | 21 +- 13 files changed, 336 insertions(+), 42 deletions(-) rename milk/docs/{ => [beemo-devs]}/kafka.md (100%) create mode 100644 milk/docs/json_api.md create mode 100644 milk/src/constants/errors.ts create mode 100644 milk/src/constants/links.ts create mode 100644 milk/src/constants/pagination.ts diff --git a/milk/docs/kafka.md b/milk/docs/[beemo-devs]/kafka.md similarity index 100% rename from milk/docs/kafka.md rename to milk/docs/[beemo-devs]/kafka.md diff --git a/milk/docs/json_api.md b/milk/docs/json_api.md new file mode 100644 index 0000000..7c1b70e --- /dev/null +++ b/milk/docs/json_api.md @@ -0,0 +1,201 @@ +# Documentations for Third-Party Developers + +This documentation primarily discusses the JSON API (Application Interface) available to everyone. + +## Limitations of Text API + +```text +logs.beemo.gg/raid/:id +``` + +Following some recent testings, we've discovered that raid logs reaching over 500,000 users took **tens of megabytes** +to load, and that isn't ideal for the general users and for our systems. As such, after careful consideration, we've +decided to limit the Text API (`/raid/:id`) to display, at maximum, 2,000 users, which is about as much as we expect +the public to skim at maximum without using a tool to aggregate the data. + +For people who aggregates the data using specialized tools, we have a JSON API (`/raid/:id.json`) that is paginated +available to use. You can find details about it in the following: +1. [`OpenAPI Schema`](openapi.yaml) +2. [`JSON API`](json_api.md) + +## JSON API + +```text +logs.beemo.gg/raid/:id.json +``` + +In this section, the specifications of the JSON API will be discussed and understood to provide clarity and understanding +over how one can use this to collect data about their raid. + +> **Warning** +> +> Please note that this documentation is written for people who have some understanding of general +> data types and JSON as this is intended for people who are usually developing their own in-house tools. + +### Specifications + +Our JSON API is different from the Text API as the data here is chunked into different pages by 200 users each page, +and contains more detailed information of users, such as their avatar hash, when the account was created. + +### Date Format + +Dates are formatted under the [`ISO 8601`](https://en.wikipedia.org/wiki/ISO_8601) specification[^1], which are as follows: +```text +YYYY-MM-DDTHH:mm:ss.sssZ +``` + +[^1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + +### Response Schema +```json +{ + "raid": { + "startedAt": "date", + "concludedAt": "nullable(date)", + "guild": "int64 as string", + "size": "int32" + }, + "users": { + "next": "nullable(date)", + "size": "int16", + "data": "array of User" + } +} +``` + +### User Schema +```json +{ + "id": "int64 as string", + "name": "string", + "joinedAt": "date", + "createdAt": "date", + "avatarHash": "nullable(string)" +} +``` + +### Query Parameters +- `cursor`: an ISO-8601 date that indicates what next set of data to get. + - **description**: an ISO-8601 date that indicates what next set of data to get. + - **type**: ISO-8601 date or `null`. + - **example**: `?cursor=2023-11-02T01:02:36.978Z` + +### Querying data + +To query the initial data, one needs to send a request to `logs.beemo.gg/raid/:id.json` where `:id` refers to the +raid identifier provided by Beemo, it should look gibberish and random, such as: `Ht2Erx76nj13`. In this example, we'll +use `Ht2Erx76nj13` as our `:id`. + +> **Note** +> +> `Ht2Erx76nj13` is a sample raid from our tests. It may or may not exist on the actual `logs.beemo.gg` domain as +> of writing, but it could exist at some point. We recommend using your own raid identifier to follow along with +> our example. + +Depending on your tooling, this may be different, but in this example, we'll be using `curl` in our command-line +to demonstrate. +```shell +curl http://logs.beemo.gg/raid/Ht2Erx76nj13.json +``` + +Running the above command gives us a very long JSON response, which follows our [schema](#response-schema) containing +information about the raid and the users. +```json +{ + "raid": { + "startedAt": "2023-11-02T01:25:36.970Z", + "concludedAt": null, + "guild": "697474023914733575", + "size": 5000 + }, + "users": { + "next": "2023-11-02T01:02:36.978Z", + "size": 200, + "data": [ + { + "id": "972441010812461952", + "name": "9hsr6JA5jk7vVA35", + "joinedAt": "2023-11-02T01:01:36.975Z", + "createdAt": "2023-11-14T01:25:36.975Z", + "avatarHash": null + }, + { + "id": "495325603979310784", + "name": "TW871dd7YTb7sZv9", + "joinedAt": "2023-11-02T01:01:36.975Z", + "createdAt": "2023-11-03T01:25:36.975Z", + "avatarHash": null + }, + ..., + { + "id": "732275137613676288", + "name": "EbWp0ORBYH33BOX5", + "joinedAt": "2023-11-02T01:02:36.978Z", + "createdAt": "2023-11-21T01:25:36.978Z", + "avatarHash": null + } + ] + } +} +``` + +From the above output, we can see that the `next` property is filled, which means, we can use that to possibly query +the next set of data, if there is any. This `next` property also happens to be the same as the `joinedAt` time of the +**last user in the array**, in this case, the user with the id of "732275137613676288" and the name of "EbWp0ORBYH33BOX5". + +To query the next set of information, all we need to do is add `?cursor=` to the link. In our +example, our `` is `2023-11-02T01:02:36.978Z`, which means, we have to add `?cursor=2023-11-02T01:02:36.978Z` to our link. + +```shell +curl http://logs.beemo.gg/raid/Ht2Erx76nj13.json?cursor=2023-11-02T01:02:36.978Z +``` + +Running that command on our command-line gives us a similar output to the above, but instead, we get a different set +of users and a different `next` property value that directly links with the **last user in the array**'s `joinedAt`. + +```json +{ + "raid": { + "startedAt": "2023-11-02T01:25:36.970Z", + "concludedAt": null, + "guild": "697474023914733575", + "size": 5000 + }, + "users": { + "next": "2023-11-02T01:03:36.982Z", + "size": 200, + "data": [ + { + "id": "819796273096448256", + "name": "676kq1AUwXh8Ygwf", + "joinedAt": "2023-11-02T01:02:36.979Z", + "createdAt": "2023-11-09T01:25:36.979Z", + "avatarHash": null + }, + { + "id": "679522196993485440", + "name": "93j0HK2MvEud9n1Y", + "joinedAt": "2023-11-02T01:02:36.979Z", + "createdAt": "2023-11-26T01:25:36.979Z", + "avatarHash": null + }, + ..., + { + "id": "151281220654798656", + "name": "W04Qy8a4p1bZxSMu", + "joinedAt": "2023-11-02T01:03:36.982Z", + "createdAt": "2023-11-25T01:25:36.982Z", + "avatarHash": null + } + ] + } +} +``` + +And similar to the previous one, you can then request the next set of data by replacing the value of `cursor` on your +link to the value of `next` and keep repeating until you get all the data that you want. + +## Have more questions? + +If you have more questions, feel free to come over at our [Discord Server](https://beemo.gg/discord) and ask over there, +we'll happily answer to our best capacity! \ No newline at end of file diff --git a/milk/docs/openapi.yaml b/milk/docs/openapi.yaml index c3be834..e39f9b8 100644 --- a/milk/docs/openapi.yaml +++ b/milk/docs/openapi.yaml @@ -29,13 +29,21 @@ paths: required: true schema: type: string + - name: cursor + in: query + description: The last `joined_at` date to query the next page, found in the `next` property. + required: false + schema: + type: string + format: date + example: '2023-10-30T16:10:57.697Z' responses: '200': description: Raid found content: application/json: schema: - $ref: '#/components/schemas/Raid' + $ref: '#/components/schemas/CursoredRaid' headers: 'X-Cache': description: 'Indicates whether the response was cached or not.' @@ -51,8 +59,31 @@ paths: example: '1698767766556' '404': description: Raid not found + '400': + description: Invalid date provided for `cursor` query string. components: schemas: + CursoredRaid: + type: object + properties: + raid: + $ref: '#/components/schemas/Raid' + users: + type: object + properties: + next: + type: string + format: date + example: '2023-10-30T15:29:57.697Z' + nullable: true + size: + type: integer + format: int32 + example: 200 + data: + type: array + items: + $ref: '#/components/schemas/RaidUser' Raid: type: object properties: @@ -74,10 +105,6 @@ components: type: string format: int64 example: '697474023914733575' - users: - type: array - items: - $ref: '#/components/schemas/RaidUser' RaidUser: type: object properties: diff --git a/milk/prisma/schema.prisma b/milk/prisma/schema.prisma index 7ed2692..0385d07 100644 --- a/milk/prisma/schema.prisma +++ b/milk/prisma/schema.prisma @@ -17,12 +17,14 @@ model RaidUser { joined_at DateTime @db.Timestamptz @@id([raid_id, id]) + @@index([joined_at]) } model Raid { id String @unique public_id String @unique guild_id BigInt + created_at DateTime @default(now()) @db.Timestamptz() concluded_at DateTime? @db.Timestamptz users RaidUser[] } diff --git a/milk/src/constants/errors.ts b/milk/src/constants/errors.ts new file mode 100644 index 0000000..029350a --- /dev/null +++ b/milk/src/constants/errors.ts @@ -0,0 +1,2 @@ +const createError = (message: string) => JSON.stringify({ error: message }) +export const INVALID_CURSOR_QUERY_STRING = createError('Provided `cursor` query string is not a valid date.') \ No newline at end of file diff --git a/milk/src/constants/links.ts b/milk/src/constants/links.ts new file mode 100644 index 0000000..8827c39 --- /dev/null +++ b/milk/src/constants/links.ts @@ -0,0 +1 @@ +export const JSON_API_DOCUMENTATIONS = "https://github.com/beemobot/cafe/blob/main/milk/docs/json_api.md" \ No newline at end of file diff --git a/milk/src/constants/pagination.ts b/milk/src/constants/pagination.ts new file mode 100644 index 0000000..3035242 --- /dev/null +++ b/milk/src/constants/pagination.ts @@ -0,0 +1 @@ +export const PAGINATION_LIMIT = 200 \ No newline at end of file diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts index 9aae2aa..fb06e8d 100644 --- a/milk/src/database/raid.ts +++ b/milk/src/database/raid.ts @@ -10,7 +10,7 @@ import {randomString} from "../utils/string.js"; * @return {@link Raid} or null if it doesn't exist. */ export const getRaidByPublicId = async (publicId: string) => { - return (await prisma.raid.findUnique({where: {public_id: publicId}, include: {users: true}})); + return (await prisma.raid.findUnique({where: {public_id: publicId}})); } /** diff --git a/milk/src/database/raid_users.ts b/milk/src/database/raid_users.ts index 45b330e..ca8775e 100644 --- a/milk/src/database/raid_users.ts +++ b/milk/src/database/raid_users.ts @@ -2,14 +2,7 @@ import {prisma} from "../connections/prisma.js"; import {PublicRaidUser} from "../types/raid.js"; import {Raid, RaidUser} from "@prisma/client"; -/** - * Gets the users involved in a {@link Raid} as a {@link PublicRaidUser} type which does not include the - * `internal_raid_id`. - * - * @param raid the raid to get the data from - * @return the users involved in a {@link Raid}. - */ -export const getPublicRaidUsers = (raid: { users: RaidUser[] } & Raid): PublicRaidUser[] => raid.users.map(user => { +const transformToPublicRaidUser = (user: RaidUser): PublicRaidUser => { return { id: user.id.toString(), name: user.name, @@ -17,7 +10,48 @@ export const getPublicRaidUsers = (raid: { users: RaidUser[] } & Raid): PublicRa createdAt: user.created_at, avatarHash: user.avatar_hash } satisfies PublicRaidUser -}) +} + +/** + * Gets the users involved in a {@link Raid} as a {@link PublicRaidUser} type which does not include the + * `internal_raid_id`. + * + * @param raid the raid to get the data from + * @return the users involved in a {@link Raid}. + */ +export const getPublicRaidUsers = (raid: { users: RaidUser[] } & Raid): PublicRaidUser[] => raid.users + .map(transformToPublicRaidUser) + +/** + * Paginates over the {@link RaidUser} associated in a given {@link Raid}. + * + * @param raidId the raid id + * @param limit the maximum users to return + * @param cursor the cursor (joined_at) to use. + * @return all the users associated in a {@link Raid} given a cursor. + */ +export const paginateUsers = async (raidId: string, limit: number, cursor: Date | null): Promise<{ users: PublicRaidUser[], count: number }> => { + const results = await prisma.$transaction([ + prisma.raidUser.findMany({ + where: { + raid_id: raidId, + joined_at: cursor == null ? undefined : { + gt: cursor + } + }, + take: limit, + orderBy: { + joined_at: 'asc' + } + }), + prisma.raidUser.count({ + where: { + raid_id: raidId + } + }) + ]) + return { users: results[0].map(transformToPublicRaidUser), count: results[1] } +} /** * Inserts many {@link RaidUser} to the database. diff --git a/milk/src/fastify/serve_cached.ts b/milk/src/fastify/serve_cached.ts index c1ddc1e..83bd66e 100644 --- a/milk/src/fastify/serve_cached.ts +++ b/milk/src/fastify/serve_cached.ts @@ -3,11 +3,14 @@ import {FastifyReply} from "fastify"; import {TEN_MINUTES} from "../constants/time.js"; const cache = new NodeCache({ stdTTL: TEN_MINUTES }) +export type CacheResult = { result: string | null, shouldCache: boolean } + +const discordCacheResult = { result: null, shouldCache: false } export const useCacheWhenPossible = async ( reply: FastifyReply, key: string, contentType: string, - computation: () => Promise<{ result: string, shouldCache: boolean }> + computation: (discard: CacheResult) => Promise ): Promise => { const cachedResult = cache.get(key) if (cachedResult != null) { @@ -18,7 +21,7 @@ export const useCacheWhenPossible = async ( .send(cachedResult) } - const { result, shouldCache } = await computation() + const { result, shouldCache } = await computation(discordCacheResult) if (shouldCache) { cache.set(key, result) } diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts index 557341b..9bde62a 100644 --- a/milk/src/routes/get_raid.ts +++ b/milk/src/routes/get_raid.ts @@ -7,8 +7,13 @@ export type RaidParameter = { Params: { id: string } } +export type CursoredRaidParameter = { + Params: { id: string }, + Querystring: { cursor: string | null } +} + export default async (fastify: FastifyInstance) => { fastify.get('/antispam/:id', route$GetAntispam) fastify.get('/raid/:id', route$GetRaidAsText) - fastify.get('/raid/:id.json', route$GetRaidAsJson) + fastify.get('/raid/:id.json', route$GetRaidAsJson) } \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts index 38dc0c0..142bd18 100644 --- a/milk/src/routes/get_raid/get_raid_as_json.ts +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -1,28 +1,49 @@ import {FastifyReply, FastifyRequest} from "fastify"; import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; import {getRaidByPublicId} from "../../database/raid.js"; -import {RaidParameter} from "../get_raid.js"; -import {getPublicRaidUsers} from "../../database/raid_users.js"; +import {CursoredRaidParameter} from "../get_raid.js"; +import {paginateUsers} from "../../database/raid_users.js"; +import {PAGINATION_LIMIT} from "../../constants/pagination.js"; -export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { +export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params - return await useCacheWhenPossible(reply, `${id}.json`, 'application/json', async () => { + + let cursorQuery = request.query.cursor + let cursor: Date | null = null + + if (cursorQuery != null) { + try { + cursor = new Date(cursorQuery) + } catch (ex) { + reply.code(400).send() + } + } + + const cacheKey = `${id}.json$cursor=${cursor?.toISOString() ?? 'null'}` + return await useCacheWhenPossible(reply, cacheKey, 'application/json', async (discard) => { const raid = await getRaidByPublicId(id) if (raid == null) { - return reply.code(404).send('404 Not Found') + reply.code(404).send('404 Not Found') + return discard } - const users = getPublicRaidUsers(raid) + const { users, count } = await paginateUsers(raid.id, PAGINATION_LIMIT, cursor) return { result: JSON.stringify({ - size: users.length, - startedAt: users[0]?.joinedAt, - concludedAt: raid.concluded_at, - guild: raid.guild_id.toString(), - users + raid: { + startedAt: raid.created_at, + concludedAt: raid.concluded_at, + guild: raid.guild_id.toString(), + size: count + }, + users: { + next: users.at(users.length - 1)?.joinedAt ?? null, + size: users.length, + data: users + }, }), - shouldCache: raid.concluded_at != null || users.length > 2_000 + shouldCache: users.length >= PAGINATION_LIMIT } }) } \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index 38672d1..459ddbd 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -5,27 +5,24 @@ import {toDateString, toTimeString} from "../../utils/date.js"; import {Logger} from "@beemobot/common"; import {TAG} from "../../constants/logging.js"; import {RaidParameter} from "../get_raid.js"; +import {paginateUsers} from "../../database/raid_users.js"; +import {JSON_API_DOCUMENTATIONS} from "../../constants/links.js"; const numberFormatter = new Intl.NumberFormat('en-US') export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { let { id } = request.params - return await useCacheWhenPossible(reply, id, 'text', async () => { + return await useCacheWhenPossible(reply, id, 'text', async (discard) => { const raid = await getRaidByPublicId(id) if (raid == null) { - return reply.code(404).send('404 Not Found') + reply.code(404).send('404 Not Found') + return discard } - const users = raid.users - + const { users, count } = await paginateUsers(raid.id, 2_000, null) let response: string - let startedDate = "N/A" - - const firstUser = users.at(0) - if (firstUser != null) { - startedDate = toDateString(firstUser.joined_at) - } + let startedDate = toDateString(raid.created_at) response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; @@ -33,14 +30,14 @@ export async function route$GetRaidAsText(request: FastifyRequest Logger.warn(TAG, `Raid ${id} reported no users.`) response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" } else { - response += '\nRaid size: ' + numberFormatter.format(users.length) + ' accounts' + response += '\nRaid size: ' + numberFormatter.format(count) + ' accounts' + (count > 2_000 ? ` (Limited to 2,000 accounts. Read ${JSON_API_DOCUMENTATIONS} for more information).` : '.') response += '\n' response += '\n Joined at: ID: Username:' response += '\n' let userIds = ''; for (const user of users) { response += '\n' - response += toTimeString(user.joined_at) + " " + user.id + spaces((18 - user.id.toString().length + 3)) + user.name + response += toTimeString(user.joinedAt) + " " + user.id + spaces((18 - user.id.toString().length + 3)) + user.name userIds += '\n' userIds += user.id From b76ae78719230ad218aef401117c484727b98be8 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 19 Nov 2023 09:09:41 +0800 Subject: [PATCH 43/47] feat(milk): different wording for beyond 2,000 accounts warning --- milk/src/routes/get_raid/get_raid_as_text.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts index 459ddbd..7fc94e5 100644 --- a/milk/src/routes/get_raid/get_raid_as_text.ts +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -30,7 +30,10 @@ export async function route$GetRaidAsText(request: FastifyRequest Logger.warn(TAG, `Raid ${id} reported no users.`) response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" } else { - response += '\nRaid size: ' + numberFormatter.format(count) + ' accounts' + (count > 2_000 ? ` (Limited to 2,000 accounts. Read ${JSON_API_DOCUMENTATIONS} for more information).` : '.') + response += '\nRaid size: ' + numberFormatter.format(count) + ' accounts' + if (count > 2_000) { + response += `\nTo view beyond 2,000 accounts, see ${JSON_API_DOCUMENTATIONS} for more information.` + } response += '\n' response += '\n Joined at: ID: Username:' response += '\n' From 7bf581ad5539bf4c43d1d4ff483c2014f4c8307e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 20 Nov 2023 09:23:25 +0800 Subject: [PATCH 44/47] feat(milk): init kafka clients before starting --- milk/src/connections/kafka.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index 815d482..7d7bd50 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -18,7 +18,7 @@ export async function initializeKafka() { Logger.info(TAG, "Attempting to connect to Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) kafka = new KafkaConnection(process.env.KAFKA_HOST, "milk", "milk", "-5") - await kafka.start() - initKafkaClients(kafka) + + await kafka.start() } \ No newline at end of file From 056559e17162917b7d876b21b80c9560be003a02 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 23:39:47 +0800 Subject: [PATCH 45/47] feat(milk): use epoch millis over date string when passing from kafka --- milk/src/kafka/clients/raids.ts | 18 +++++++++--------- milk/src/types/raid.ts | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 7084bf1..a000fad 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -26,8 +26,8 @@ export class RaidManagementClient extends BrokerClient { return } - let {raidId, concludedAt, guildIdString} = message.value.request - let conclusionDate: Date = new Date(concludedAt ?? Date.now()) + let {raidId, concludedAtMillis, guildId} = message.value.request + let conclusionDate: Date = new Date(concludedAtMillis ?? Date.now()) let raid = await getRaidByInternalId(raidId) if (raid == null) { @@ -40,7 +40,7 @@ export class RaidManagementClient extends BrokerClient { return } - Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildIdString}.`) + Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildId}.`) raid = await run( 'conclude_raid', async () => concludeRaid(raid!.public_id, raid!.id, conclusionDate), @@ -64,11 +64,11 @@ export class RaidManagementClient extends BrokerClient { const users = request.users.map((user) => { return { raid_id: request.raidId, - id: BigInt(user.idString), + id: BigInt(user.id), name: user.name, avatar_hash: user.avatarHash, - created_at: new Date(user.createdAt), - joined_at: new Date(user.joinedAt) + created_at: new Date(user.createdAtMillis), + joined_at: new Date(user.joinedAtMillis) } satisfies RaidUser }) @@ -82,11 +82,11 @@ export class RaidManagementClient extends BrokerClient { let raid = await getRaidByInternalId(request.raidId) if (raid == null) { - let conclusionDate = new Date(request.concludedAt ?? Date.now()) - Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildIdString}.`) + let conclusionDate = new Date(request.concludedAtMillis ?? Date.now()) + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) raid = await run( 'create_raid', - async () => createRaid(request.raidId, request.guildIdString, conclusionDate), + async () => createRaid(request.raidId, request.guildId, conclusionDate), 1, 25 ) diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index 5240069..7d97c30 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -5,9 +5,9 @@ export type RaidManagementData = { export type RaidManagementRequest = { raidId: string, - guildIdString: string, + guildId: string, users: RaidManagementUser[], - concludedAt: string | null + concludedAtMillis: number | null } export type RaidManagementResponse = { @@ -15,11 +15,11 @@ export type RaidManagementResponse = { } export type RaidManagementUser = { - idString: string, + id: string, name: string, avatarHash: string | null, - createdAt: string, - joinedAt: string + createdAtMillis: number, + joinedAtMillis: number } export type PublicRaidUser = { From a8798141e5c78685108d01d26273d92f232c4473 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 22:40:42 +0800 Subject: [PATCH 46/47] feat(milk): fix foreign key not available issue --- milk/src/kafka/clients/raids.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index a000fad..7662443 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -59,6 +59,18 @@ export class RaidManagementClient extends BrokerClient { const request = message.value.request + let raid = await getRaidByInternalId(request.raidId) + if (raid == null) { + let conclusionDate = new Date(request.concludedAtMillis ?? Date.now()) + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) + raid = await run( + 'create_raid', + async () => createRaid(request.raidId, request.guildId, conclusionDate), + 1, + 25 + ) + } + if (request.users.length > 0) { Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) const users = request.users.map((user) => { @@ -80,18 +92,6 @@ export class RaidManagementClient extends BrokerClient { ) } - let raid = await getRaidByInternalId(request.raidId) - if (raid == null) { - let conclusionDate = new Date(request.concludedAtMillis ?? Date.now()) - Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) - raid = await run( - 'create_raid', - async () => createRaid(request.raidId, request.guildId, conclusionDate), - 1, - 25 - ) - } - await message.respond({ response: { publicId: raid!.public_id }, request: null }) } } \ No newline at end of file From 1ea580087770670fe8c9cc035e5251012dd651d7 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 23:35:35 +0800 Subject: [PATCH 47/47] feat(milk): add `create_raid` key and do not query for raid during `batch_insert_users` --- milk/docs/[beemo-devs]/kafka.md | 20 ++++++++- milk/src/constants/raid_management_kafka.ts | 1 + milk/src/kafka/clients/raids.ts | 45 +++++++++++++-------- milk/src/types/raid.ts | 3 +- milk/src/utils/retry.ts | 7 +++- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/milk/docs/[beemo-devs]/kafka.md b/milk/docs/[beemo-devs]/kafka.md index ee5a11c..37b9c9b 100644 --- a/milk/docs/[beemo-devs]/kafka.md +++ b/milk/docs/[beemo-devs]/kafka.md @@ -7,6 +7,7 @@ accessible to third-parties, therefore, more than likely, this is of no-use for - [`Client Specifications`](#client-specifications) talks about the different parts of the client. - [`overview`](#overview) summarizes some key points of the client. - [`keys`](#keys) + - [`create-raid`](#create-raid) used to create a raid. - [`batch-insert-raid-users`](#batch-insert-raid-users) used to insert one or more bots detected; creates the Raid if it doesn't exist. - [`conclude-raid`](#conclude-raid) used to conclude an existing raid; if no date provided, uses current time. - [`schemas`](#schemas) @@ -67,6 +68,22 @@ null as long as the `raidId` and `guildIdString` are not null. After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. +### Create Raid + +```yaml +key: create-raid +``` + +This is a specific key, or endpoint, in the Kafka client where clients can create a new raid in the database. This should be +done at the start before the users are added, and should be awaited otherwise it will lead to a foreign key issue. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. Unlike [`batch-insert-raid-users`](#batch-insert-raid-users), +this doesn't expect the `users` property to not be empty as long as the `raidId` and `guildIdString` are not null. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + ## Schemas ### Raid Management Data @@ -108,7 +125,8 @@ the nature of JavaScript not inherently supporting `int64` or `long` type. ### Raid Management Response ```json { - "externalId": "string" + "publicId": "nullable(string)", + "acknowledged": true } ``` diff --git a/milk/src/constants/raid_management_kafka.ts b/milk/src/constants/raid_management_kafka.ts index c652479..d3035be 100644 --- a/milk/src/constants/raid_management_kafka.ts +++ b/milk/src/constants/raid_management_kafka.ts @@ -1,3 +1,4 @@ export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" +export const RAID_MANAGEMENT_CREATE_RAID = "create-raid" export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" export const RAID_MANAGEMENT_CONCLUDE_RAID = "conclude-raid" \ No newline at end of file diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 7662443..2c9dd78 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -7,7 +7,7 @@ import {TAG} from "../../constants/logging.js"; import { RAID_MANAGEMENT_BATCH_INSERT_KEY, RAID_MANAGEMENT_CLIENT_TOPIC, - RAID_MANAGEMENT_CONCLUDE_RAID + RAID_MANAGEMENT_CONCLUDE_RAID, RAID_MANAGEMENT_CREATE_RAID } from "../../constants/raid_management_kafka.js"; import {concludeRaid, createRaid, getRaidByInternalId} from "../../database/raid.js"; import {RaidUser} from "@prisma/client"; @@ -16,10 +16,33 @@ import {insertRaidUsers} from "../../database/raid_users.js"; export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { super(conn, RAID_MANAGEMENT_CLIENT_TOPIC); + this.on(RAID_MANAGEMENT_CREATE_RAID, this.onCreateRaid) this.on(RAID_MANAGEMENT_BATCH_INSERT_KEY, this.onBatchInsertRaidUsers) this.on(RAID_MANAGEMENT_CONCLUDE_RAID, this.onConcludeRaid) } + private async onCreateRaid(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_CREATE_RAID} but no request details was found.`) + return + } + + const request = message.value.request + + let raid = await getRaidByInternalId(request.raidId) + if (raid == null) { + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) + raid = await run( + 'create_raid', + async () => createRaid(request.raidId, request.guildId, null), + 1, + 12 + ) + } + + await message.respond({ response: { publicId: raid?.public_id, acknowledged: true }, request: null }) + } + private async onConcludeRaid(message: BrokerMessage) { if (message.value == null || message.value.request == null) { Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_CONCLUDE_RAID} but no request details was found.`) @@ -45,10 +68,10 @@ export class RaidManagementClient extends BrokerClient { 'conclude_raid', async () => concludeRaid(raid!.public_id, raid!.id, conclusionDate), 0.2, - 25 + 12 ) - await message.respond({ response: { publicId: raid!.public_id }, request: null }) + await message.respond({ response: { publicId: raid!.public_id, acknowledged: true }, request: null }) } private async onBatchInsertRaidUsers(message: BrokerMessage) { @@ -59,18 +82,6 @@ export class RaidManagementClient extends BrokerClient { const request = message.value.request - let raid = await getRaidByInternalId(request.raidId) - if (raid == null) { - let conclusionDate = new Date(request.concludedAtMillis ?? Date.now()) - Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) - raid = await run( - 'create_raid', - async () => createRaid(request.raidId, request.guildId, conclusionDate), - 1, - 25 - ) - } - if (request.users.length > 0) { Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) const users = request.users.map((user) => { @@ -88,10 +99,10 @@ export class RaidManagementClient extends BrokerClient { 'insert_raid_users', async () => insertRaidUsers(users), 2, - 25 + 12 ) } - await message.respond({ response: { publicId: raid!.public_id }, request: null }) + await message.respond({ response: { publicId: null, acknowledged: true }, request: null }) } } \ No newline at end of file diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts index 7d97c30..718a957 100644 --- a/milk/src/types/raid.ts +++ b/milk/src/types/raid.ts @@ -11,7 +11,8 @@ export type RaidManagementRequest = { } export type RaidManagementResponse = { - publicId: string + publicId: string | null, + acknowledged: boolean } export type RaidManagementUser = { diff --git a/milk/src/utils/retry.ts b/milk/src/utils/retry.ts index 6ea22e5..5a278d7 100644 --- a/milk/src/utils/retry.ts +++ b/milk/src/utils/retry.ts @@ -24,7 +24,12 @@ export async function run( } const secondsTillRetry = (retryEverySeconds * retries) - Logger.error(TAG, `Failed to complete ${taskName}. Retrying in ${secondsTillRetry} seconds.`, exception) + const logMessage = `Failed to complete ${taskName}. Retrying in ${secondsTillRetry} seconds.` + if (retries === 1) { + Logger.error(TAG, logMessage, exception) + } else { + Logger.error(TAG, logMessage) + } await new Promise((resolve) => setTimeout(resolve, secondsTillRetry * 1000)) return run(taskName, action, retryEverySeconds, retries + 1)