diff --git a/examples/bigbot-19/.env.example b/examples/bigbot-19/.env.example index 5220dc1fa..f3d05e20f 100644 --- a/examples/bigbot-19/.env.example +++ b/examples/bigbot-19/.env.example @@ -2,7 +2,7 @@ # General Configurations # -# For Prisma use, it should be a postgres connection url +# Used by Prisma, it should be a postgres connection string # Template: postgres://[username]:[password]@[host]:[port]/[db] # Replate the [...] from the template with your values # TEMPLATE-SETUP: Add a postgres connection string diff --git a/examples/bigbot-19/package.json b/examples/bigbot-19/package.json index c218cfaf9..549c19301 100644 --- a/examples/bigbot-19/package.json +++ b/examples/bigbot-19/package.json @@ -22,6 +22,7 @@ "@discordeno/bot": "19.0.0-next.ad7e74c", "@fastify/multipart": "^8.3.0", "@influxdata/influxdb-client": "^1.33.2", + "@prisma/client": "^5.15.0", "amqplib": "^0.10.4", "fastify": "^4.27.0" }, @@ -30,6 +31,7 @@ "@swc/core": "^1.5.25", "@types/amqplib": "^0.10.5", "@types/node": "^20.14.2", + "prisma": "^5.15.0", "typescript": "^5.4.5" } } diff --git a/examples/bigbot-19/prisma/migrations/20240615175232_init/migration.sql b/examples/bigbot-19/prisma/migrations/20240615175232_init/migration.sql new file mode 100644 index 000000000..0e75840a6 --- /dev/null +++ b/examples/bigbot-19/prisma/migrations/20240615175232_init/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "Guild" ( + "guildId" BIGINT NOT NULL, + "language" TEXT NOT NULL DEFAULT 'english', + "commands" TEXT, + + CONSTRAINT "Guild_pkey" PRIMARY KEY ("guildId") +); diff --git a/examples/bigbot-19/prisma/migrations/migration_lock.toml b/examples/bigbot-19/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/examples/bigbot-19/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/examples/bigbot-19/prisma/schema.prisma b/examples/bigbot-19/prisma/schema.prisma new file mode 100644 index 000000000..a70a1d4ff --- /dev/null +++ b/examples/bigbot-19/prisma/schema.prisma @@ -0,0 +1,21 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Guild { + // The guild id + guildId BigInt @id + // The language for translations in this server + language String @default("english") + // Hash of the command objects that have been deployed + // Used to check when to deploy the commands to a guild + commands String? +} diff --git a/examples/bigbot-19/src/bot/bot.ts b/examples/bigbot-19/src/bot/bot.ts index eabb64090..6380f9d01 100644 --- a/examples/bigbot-19/src/bot/bot.ts +++ b/examples/bigbot-19/src/bot/bot.ts @@ -1,4 +1,4 @@ -import { Collection, createBot, type Bot } from '@discordeno/bot' +import { Collection, LogDepth, createBot, type Bot, type logger } from '@discordeno/bot' import assert from 'node:assert' import { DISCORD_TOKEN, GATEWAY_INTENTS, REST_AUTHORIZATION, REST_URL } from '../config.js' import type { Command } from './commands.js' @@ -29,6 +29,7 @@ props.interaction.id = true props.interaction.data = true props.interaction.type = true props.interaction.token = true +props.interaction.guildId = true props.message.id = true @@ -36,6 +37,9 @@ props.message.id = true function createCustomBot(rawBot: TBot): CustomBot { const bot = rawBot as CustomBot + // We need to set the log depth for the default discordeno logger or else only the first param will be logged + ;(bot.logger as typeof logger).setDepth(LogDepth.Full) + bot.commands = new Collection() return bot diff --git a/examples/bigbot-19/src/bot/commands.ts b/examples/bigbot-19/src/bot/commands.ts index 2feb17281..5764e5489 100644 --- a/examples/bigbot-19/src/bot/commands.ts +++ b/examples/bigbot-19/src/bot/commands.ts @@ -1,21 +1,46 @@ import type { ApplicationCommandOptionTypes, + ApplicationCommandTypes, Attachment, CamelizedDiscordApplicationCommandOption, ChannelTypes, - CreateApplicationCommand, + CreateSlashApplicationCommand, Interaction, Member, Role, User, } from '@discordeno/bot' import { bot } from './bot.js' +import type { DefaultLocale, TranslationKey } from './languages/languages.js' +import { translate } from './languages/translate.js' export default function createCommand(command: Command): void { - bot.commands.set(command.name, command as Command) + bot.commands.set(translate('english', command.name), command as Command) } -export type Command = CreateApplicationCommand & { +export type Command = + | SlashApplicationCommand + | (Omit, 'options' | 'description' | 'descriptionLocalizations'> & { + /** The type of the command */ + type: ApplicationCommandTypes.Message | ApplicationCommandTypes.User + }) + +// This is needed to properly support ApplicationCommandTypes.Message or ApplicationCommandTypes.User commands +type SlashApplicationCommand = CreateSlashApplicationCommand & { + /** + * @remarks + * The value should be set to the translation key for the name of this command + * + * @inheritdoc + */ + name: TranslationKey + /** + * @remarks + * The value should be set to the translation key for the name of this command + * + * @inheritdoc + */ + description: TranslationKey /** @inheritdoc */ options?: TOptions /** @@ -34,7 +59,23 @@ export type GetCommandOptions = T extends CommandOptio ? { [Prop in keyof BuildOptions as Prop]: BuildOptions[Prop] } : never -export type CommandOption = CamelizedDiscordApplicationCommandOption +export type CommandOption = CamelizedDiscordApplicationCommandOption & { + /** + * @remarks + * The value should be set to the translation key for the name of this command + * + * @inheritdoc + */ + name: TranslationKey + /** + * @remarks + * The value should be set to the translation key for the name of this command + * + * @inheritdoc + */ + description: TranslationKey +} + export type CommandOptions = CommandOption[] // Option parsing @@ -73,7 +114,7 @@ interface TypeToResolvedMap { type ConvertTypeToResolved = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup -type GetOptionName = T extends { name: string } ? T['name'] : never +type GetOptionName = T extends { name: TranslationKey } ? (DefaultLocale[T['name']] extends string ? DefaultLocale[T['name']] : never) : never type GetOptionValue = T extends { type: ApplicationCommandOptionTypes; required?: boolean } ? T extends { type: SubCommandApplicationCommand; options?: CommandOptions } ? BuildOptions diff --git a/examples/bigbot-19/src/bot/commands/language.ts b/examples/bigbot-19/src/bot/commands/language.ts new file mode 100644 index 000000000..5d88ac2b9 --- /dev/null +++ b/examples/bigbot-19/src/bot/commands/language.ts @@ -0,0 +1,47 @@ +import { ApplicationCommandOptionTypes, DiscordInteractionContextType } from '@discordeno/bot' +import assert from 'node:assert' +import createCommand from '../commands.js' +import type { LanguageNames } from '../languages/languages.js' +import languages from '../languages/languages.js' +import { languageCache, translate } from '../languages/translate.js' +import prisma from '../prisma.js' +import { updateCommands } from '../utils/updateCommands.js' + +createCommand({ + name: 'languageCommandName', + description: 'languageCommandDescription', + // Allow this command only in Guilds, it will not appear in DMs + contexts: [DiscordInteractionContextType.Guild], + // By default, allow only someone with MANAGE_GUILD to run this command + defaultMemberPermissions: ['MANAGE_GUILD'], + options: [ + { + name: 'languageCommandOptionName', + description: 'languageCommandDescription', + type: ApplicationCommandOptionTypes.String, + choices: Object.keys(languages).map((key) => ({ name: key, value: key })), + required: true, + }, + ], + async run(interaction, options) { + assert(interaction.guildId, '/language - The guildId is missing in the interaction') + + await interaction.defer(true) + const language = options.language as LanguageNames + + // Update the db + await prisma.guild.upsert({ + where: { guildId: interaction.guildId }, + create: { language, guildId: interaction.guildId }, + update: { language }, + }) + + // Update the cache + languageCache.set(interaction.guildId, language) + // Update the commands for this guild so they get the new translation + await updateCommands(interaction.guildId) + + // Let the user know its been updated. + await interaction.respond(translate(interaction.guildId, 'languageCommandUpdated', language)) + }, +}) diff --git a/examples/bigbot-19/src/bot/commands/ping.ts b/examples/bigbot-19/src/bot/commands/ping.ts index 15fe7ddb0..a941337aa 100644 --- a/examples/bigbot-19/src/bot/commands/ping.ts +++ b/examples/bigbot-19/src/bot/commands/ping.ts @@ -1,16 +1,17 @@ import { snowflakeToTimestamp } from '@discordeno/bot' import { bot } from '../bot.js' import createCommand from '../commands.js' +import { translate } from '../languages/translate.js' createCommand({ - name: 'ping', - description: 'The ping command', + name: 'pingCommandName', + description: 'pingCommandDescription', async run(interaction) { - await interaction.respond(`🏓 Ping?`) + await interaction.respond(translate(interaction.guildId, 'pingCommandInitialResponse')) const response = await bot.helpers.getOriginalInteractionResponse(interaction.token) const ping = snowflakeToTimestamp(response.id) - snowflakeToTimestamp(interaction.id) - await interaction.edit(`🏓 Pong! Gateway Latency: TBD, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`) + await interaction.edit(translate(interaction.guildId, 'pingCommandResponseWithLatencies', 0, ping)) }, }) diff --git a/examples/bigbot-19/src/bot/events/interactions/create.ts b/examples/bigbot-19/src/bot/events/interactions/create.ts index 3d54fd09d..f31b48d81 100644 --- a/examples/bigbot-19/src/bot/events/interactions/create.ts +++ b/examples/bigbot-19/src/bot/events/interactions/create.ts @@ -1,5 +1,6 @@ import { InteractionTypes, commandOptionsParser } from '@discordeno/bot' import { bot } from '../../bot.js' +import { loadLocale } from '../../languages/translate.js' bot.events.interactionCreate = async (interaction) => { const isCommandOrAutocomplete = @@ -7,6 +8,10 @@ bot.events.interactionCreate = async (interaction) => { if (!interaction.data || !isCommandOrAutocomplete) return + if (interaction.guildId) { + await loadLocale(interaction.guildId) + } + const command = bot.commands.get(interaction.data.name) if (!command) { @@ -15,6 +20,7 @@ bot.events.interactionCreate = async (interaction) => { } // TODO: log the command was triggered + // TODO: handle autocomplete const options = commandOptionsParser(interaction) diff --git a/examples/bigbot-19/src/bot/events/raw.ts b/examples/bigbot-19/src/bot/events/raw.ts new file mode 100644 index 000000000..a5e2005ab --- /dev/null +++ b/examples/bigbot-19/src/bot/events/raw.ts @@ -0,0 +1,34 @@ +import { hasProperty, type DiscordGatewayPayload } from '@discordeno/bot' +import { bot } from '../bot.js' +import { updateCommands, usesLatestCommands } from '../utils/updateCommands.js' + +bot.events.raw = async (payload) => { + // Check if the guild needs a command update + const guildId = attemptToGetGuildId(payload) + + if (guildId && !(await usesLatestCommands(guildId))) { + await updateCommands(guildId) + } +} + +function attemptToGetGuildId(payload: DiscordGatewayPayload): bigint | undefined { + const data = payload.d + + if (payload.t === 'GUILD_CREATE' || payload.t === 'GUILD_UPDATE') { + // Attempt to find the guild_id + + if (typeof data !== 'object' || !data || !hasProperty(data, 'id') || typeof data.id !== 'string') return + + return BigInt(data.id) + } + + // Attempt to find the guild_id in another object + if (typeof data !== 'object' || !data || !hasProperty(data, 'guild_id') || typeof data.guild_id !== 'string') return + + // The bigint constructor throws an error if you pass in something that isn't a number + const isNumber = Number.isInteger(Number.parseInt(data.guild_id)) + + if (!isNumber) return undefined + + return BigInt(data.guild_id) +} diff --git a/examples/bigbot-19/src/bot/index.ts b/examples/bigbot-19/src/bot/index.ts index 685411e70..5e0eb4867 100644 --- a/examples/bigbot-19/src/bot/index.ts +++ b/examples/bigbot-19/src/bot/index.ts @@ -13,7 +13,9 @@ import { import { bot } from './bot.js' import { buildFastifyApp } from './fastify.js' import importDirectory from './utils/loader.js' -import { updateCommands } from './utils/updateCommands.js' + +// Initialize the prisma client +import './prisma.js' assert(EVENT_HANDLER_AUTHORIZATION, 'The EVENT_HANDLER_AUTHORIZATION environment variable is missing') assert(EVENT_HANDLER_HOST, 'The EVENT_HANDLER_HOST environment variable is missing') @@ -26,8 +28,6 @@ assert(!Number.isNaN(portNumber), 'The EVENT_HANDLER_PORT environment variable s await importDirectory('./dist/bot/commands') await importDirectory('./dist/bot/events') -await updateCommands() - if (MESSAGEQUEUE_ENABLE) { await connectToRabbitMQ() } diff --git a/examples/bigbot-19/src/bot/languages/english.ts b/examples/bigbot-19/src/bot/languages/english.ts new file mode 100644 index 000000000..eac2b5c47 --- /dev/null +++ b/examples/bigbot-19/src/bot/languages/english.ts @@ -0,0 +1,27 @@ +import type { LanguageLocale } from './languages.js' + +export default { + // + // slash command handler + // + executeCommandNotFound: '❌ Something went wrong. I was not able to find this command.', + executeCommandError: '❌ Something went wrong. The command execution has thrown an error.', + + // + // /language command + // + languageCommandName: 'language', + languageCommandDescription: '⚙️ Change the bot language.', + languageCommandOptionName: 'language', + languageCommandOptionDescription: 'What language would you like to set?', + languageCommandUpdated: (language: string) => `The language has been updated to ${language}`, + + // + // /ping command + // + pingCommandName: 'ping', + pingCommandDescription: '🏓 Check whether the bot is online and responsive.', + pingCommandInitialResponse: '🏓 Pong! I am online and responsive! 🕙', + pingCommandResponseWithLatencies: (shardLatency, restLatency) => + `🏓 Pong! Gateway Latency: ${shardLatency}ms, Roundtrip Latency: ${restLatency}ms. I am online and responsive! 🕙`, +} as const satisfies LanguageLocale diff --git a/examples/bigbot-19/src/bot/languages/languages.ts b/examples/bigbot-19/src/bot/languages/languages.ts new file mode 100644 index 000000000..cd03bbba7 --- /dev/null +++ b/examples/bigbot-19/src/bot/languages/languages.ts @@ -0,0 +1,41 @@ +import english from './english.js' +// TEMPLATE-SETUP: Import other locales if you have them + +const languages: Record = { + english, +} + +export default languages + +// TEMPLATE-SETUP: when adding a new locale, you should add them to this type with an OR +export type LanguageNames = 'english' + +// TEMPLATE-SETUP: when adding new translation keys, you should add them to ensure all the locales have a translation +// When the translation does not need any parameters you can type it as `string`, if there is the need for parameters you can type is a function that returns a string +export interface LanguageLocale { + // + // slash command handler + // + executeCommandNotFound: string + executeCommandError: string + + // + // /language command + // + languageCommandName: string + languageCommandDescription: string + languageCommandOptionName: string + languageCommandOptionDescription: string + languageCommandUpdated: (language: LanguageNames) => string + + // + // /ping command + // + pingCommandName: string + pingCommandDescription: string + pingCommandInitialResponse: string + pingCommandResponseWithLatencies: (shardLatency: number, restLatency: number) => string +} + +export type TranslationKey = keyof LanguageLocale +export type DefaultLocale = typeof english diff --git a/examples/bigbot-19/src/bot/languages/translate.ts b/examples/bigbot-19/src/bot/languages/translate.ts new file mode 100644 index 000000000..5399cc318 --- /dev/null +++ b/examples/bigbot-19/src/bot/languages/translate.ts @@ -0,0 +1,53 @@ +import { Collection } from '@discordeno/bot' +import prisma from '../prisma.js' +import languages, { type LanguageLocale, type LanguageNames, type TranslationKey } from './languages.js' + +export const languageCache = new Collection() + +export function translate( + guildIdOrLanguage: bigint | LanguageNames | undefined, + key: TKey, + ...params: GetTranslationArguments +): string { + const locale = getLocale(guildIdOrLanguage ?? 'english') + const translation = locale[key] + + if (typeof translation === 'function') { + // This type cast is needed to avoid TS doing stuff with the statically typed functions union + const translationFunction = translation as (...args: unknown[]) => string + + return translationFunction(...params) + } + + return translation +} + +export function getLocale(guildIdOrLanguage: bigint | LanguageNames): LanguageLocale { + return languages[getLanguage(guildIdOrLanguage)] +} + +export function getLanguage(guildIdOrLanguage: bigint | LanguageNames): LanguageNames { + const language = + typeof guildIdOrLanguage === 'string' + ? // guildIdOrLanguage is actually a language, so we can return it as is + guildIdOrLanguage + : // guildIdOrLanguage is a guildId, so we need to get from the cache what is the language for that server + languageCache.get(guildIdOrLanguage) ?? 'english' + + return language +} + +export async function loadLocale(guildId: bigint): Promise { + if (languageCache.has(guildId)) return + + const dbLanguage = await prisma.guild.findFirst({ + where: { guildId }, + }) + + const language = (dbLanguage?.language ?? 'english') as LanguageNames + + // set the cache for the next time + languageCache.set(guildId, language) +} + +type GetTranslationArguments = LanguageLocale[TKey] extends (...args: infer U) => unknown ? U : [] diff --git a/examples/bigbot-19/src/bot/prisma.ts b/examples/bigbot-19/src/bot/prisma.ts new file mode 100644 index 000000000..c4e22e4a2 --- /dev/null +++ b/examples/bigbot-19/src/bot/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client' + +export default new PrismaClient() diff --git a/examples/bigbot-19/src/bot/utils/updateCommands.ts b/examples/bigbot-19/src/bot/utils/updateCommands.ts index 52ed9b3a4..dfe0a0fd2 100644 --- a/examples/bigbot-19/src/bot/utils/updateCommands.ts +++ b/examples/bigbot-19/src/bot/utils/updateCommands.ts @@ -1,19 +1,112 @@ -import assert from 'node:assert' +import { type CamelizedDiscordApplicationCommandOption, type CreateApplicationCommand, type CreateSlashApplicationCommand } from '@discordeno/bot' +import { createHash } from 'node:crypto' import { DEVELOPMENT, DEV_SERVER_ID } from '../../config.js' import { bot } from '../bot.js' +import type { Command } from '../commands.js' +import type { TranslationKey } from '../languages/languages.js' +import { loadLocale, translate } from '../languages/translate.js' +import prisma from '../prisma.js' -export async function updateCommands(): Promise { - bot.logger.info('Updating commands') +const commandCache = new Map() - const userCommands = bot.commands.filter((x) => !x.devOnly).array() - await bot.helpers.upsertGlobalApplicationCommands(userCommands) +// TODO: add some comments in this file, it is currently quite hard to understand what is going on - if (DEVELOPMENT) { - assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing') +export async function updateCommands(guildId: bigint): Promise { + bot.logger.info(`Updating commands for guildId ${guildId}`) - bot.logger.info('Updating developer commands') + await loadLocale(guildId) - const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array() - await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands) + const userCommands = bot.commands + .filter((x) => (guildId === BigInt(DEV_SERVER_ID ?? -1n) && DEVELOPMENT ? true : !x.devOnly)) + .array() + .map((x) => translateCommands(guildId, x)) + + await bot.helpers.upsertGuildApplicationCommands(guildId, userCommands) + + bot.logger.info(`Saving the command hash for guildId ${guildId}`) + await prisma.guild.upsert({ + where: { guildId }, + create: { guildId, commands: currentCommandHash }, + update: { commands: currentCommandHash }, + }) +} + +let currentCommandHash: string + +const guildCommandHashes = new Map() + +export async function usesLatestCommands(guildId: bigint): Promise { + const current = await getCurrentCommandHash(guildId) + + return current === currentCommandHash +} + +export async function getCurrentCommandHash(guildId: bigint): Promise { + if (!currentCommandHash) { + const serializedCommands = JSON.stringify(bot.commands.array()) + currentCommandHash = createHash('sha1').update(serializedCommands).digest('hex') + } + + const cached = guildCommandHashes.get(guildId) + + if (cached) return cached + + const commandVersion = await prisma.guild.findFirst({ where: { guildId } }) + + if (commandVersion?.commands) guildCommandHashes.set(guildId, commandVersion.commands) + + return commandVersion?.commands ?? null +} + +function translateCommands(guildId: bigint, command: Command): CreateApplicationCommand { + const cached = commandCache.get(command) + + if (cached) return cached + + // we don't want to modify the original command, so we transform it and copying it in the process + const appCommand = transformCommand(command) + + appCommand.name = translate(guildId, appCommand.name as TranslationKey) + if ('description' in appCommand) { + appCommand.description = translate(guildId, appCommand.description as TranslationKey) + + if (appCommand.options) translateOptions(guildId, appCommand.options) + } + + commandCache.set(command, appCommand) + + return appCommand +} + +function translateOptions(guildId: bigint, options: CamelizedDiscordApplicationCommandOption[]): void { + for (const option of options) { + option.name = translate(guildId, option.name as TranslationKey) + option.description = translate(guildId, option.description as TranslationKey) + + if (option.options) translateOptions(guildId, option.options) } } + +// Transform our custom Command object to the discordeno CreateApplicationCommand to avoid issues +function transformCommand(command: Command): CreateApplicationCommand { + const discordPayload = {} as CreateApplicationCommand + + if (command.name) discordPayload.name = command.name + if (command.defaultMemberPermissions) discordPayload.defaultMemberPermissions = command.defaultMemberPermissions + if (command.dmPermission) discordPayload.dmPermission = command.dmPermission + if (command.contexts) discordPayload.contexts = command.contexts + if (command.integrationTypes) discordPayload.integrationTypes = command.integrationTypes + if (command.nameLocalizations) discordPayload.nameLocalizations = command.nameLocalizations + if (command.nsfw) discordPayload.nsfw = command.nsfw + if (command.type) discordPayload.type = command.type + + if ('description' in command) { + const payload = discordPayload as CreateSlashApplicationCommand + + if (command.description) payload.description = command.description + if (command.descriptionLocalizations) payload.descriptionLocalizations = command.descriptionLocalizations + if (command.options) payload.options = command.options + } + + return discordPayload +} diff --git a/examples/bigbot-19/src/gateway/worker/worker.ts b/examples/bigbot-19/src/gateway/worker/worker.ts index a293626ea..5583ae204 100644 --- a/examples/bigbot-19/src/gateway/worker/worker.ts +++ b/examples/bigbot-19/src/gateway/worker/worker.ts @@ -102,6 +102,11 @@ function createShard(shardId: number): DiscordenoShard { return await promise } + // We do not want to camelize the packet, so we need to override the function as the default behavior is to camelize + shard.forwardToBot = (packet) => { + shard.events.message?.(shard, packet) + } + shard.events.message = async (shard, payload) => { const body = JSON.stringify({ payload, shardId: shard.id }) diff --git a/examples/bigbot-19/yarn.lock b/examples/bigbot-19/yarn.lock index fb39e81f1..bf4679536 100644 --- a/examples/bigbot-19/yarn.lock +++ b/examples/bigbot-19/yarn.lock @@ -224,6 +224,64 @@ __metadata: languageName: node linkType: hard +"@prisma/client@npm:^5.15.0": + version: 5.15.0 + resolution: "@prisma/client@npm:5.15.0" + peerDependencies: + prisma: "*" + peerDependenciesMeta: + prisma: + optional: true + checksum: 6948885425a62fef5a90f039761acbd1f61c9713e8e49f30892244926ad402b3489c0b527d5d6f6469ed1057848dd6d052eec51c99d279d4f956fd021279330f + languageName: node + linkType: hard + +"@prisma/debug@npm:5.15.0": + version: 5.15.0 + resolution: "@prisma/debug@npm:5.15.0" + checksum: a0464ae8ba938ad611cf00101f8725977e822f48ad8d1ca5cd17b98e586264b8936db79b0b3521d9859651802f73da92508856aea363c9767fa418d749af72b7 + languageName: node + linkType: hard + +"@prisma/engines-version@npm:5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022": + version: 5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022 + resolution: "@prisma/engines-version@npm:5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + checksum: 5e4e29c701134e395fb06ddb94471356fa0cee3abc237726443643bf6462e8781d2f8de536f2b5ccdb6c00d9a9a6fb4f9d68e0a8ee744648ebd1924c359e865c + languageName: node + linkType: hard + +"@prisma/engines@npm:5.15.0": + version: 5.15.0 + resolution: "@prisma/engines@npm:5.15.0" + dependencies: + "@prisma/debug": "npm:5.15.0" + "@prisma/engines-version": "npm:5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + "@prisma/fetch-engine": "npm:5.15.0" + "@prisma/get-platform": "npm:5.15.0" + checksum: bbe3c942793602fc3cfbd9a5b772cdb5cece2b2515a41fe49e503b7c21c76c0215d4c3861d3db29677b5e1bce0baf98e226c9c3239946f3909b8cc72ac422da3 + languageName: node + linkType: hard + +"@prisma/fetch-engine@npm:5.15.0": + version: 5.15.0 + resolution: "@prisma/fetch-engine@npm:5.15.0" + dependencies: + "@prisma/debug": "npm:5.15.0" + "@prisma/engines-version": "npm:5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + "@prisma/get-platform": "npm:5.15.0" + checksum: fd3696095cd4e780806655cfd23332235eda51231840467d90d663937583ed5ac2950e4aa193f1586fd19d62028eb52d4ddacff9b0a247f76eb701621e43098e + languageName: node + linkType: hard + +"@prisma/get-platform@npm:5.15.0": + version: 5.15.0 + resolution: "@prisma/get-platform@npm:5.15.0" + dependencies: + "@prisma/debug": "npm:5.15.0" + checksum: b969d502aea4a9bb3cae2f751e8c9729f161ebe0442dd44c4a2fd68e0a940111ee133e96e402a7ca9156a231d1e197bdd4966823f8fa610e623a6bae7bb55c8d + languageName: node + linkType: hard + "@sindresorhus/is@npm:^4.0.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" @@ -829,12 +887,14 @@ __metadata: "@discordeno/bot": "npm:19.0.0-next.ad7e74c" "@fastify/multipart": "npm:^8.3.0" "@influxdata/influxdb-client": "npm:^1.33.2" + "@prisma/client": "npm:^5.15.0" "@swc/cli": "npm:^0.3.12" "@swc/core": "npm:^1.5.25" "@types/amqplib": "npm:^0.10.5" "@types/node": "npm:^20.14.2" amqplib: "npm:^0.10.4" fastify: "npm:^4.27.0" + prisma: "npm:^5.15.0" typescript: "npm:^5.4.5" languageName: unknown linkType: soft @@ -1984,6 +2044,17 @@ __metadata: languageName: node linkType: hard +"prisma@npm:^5.15.0": + version: 5.15.0 + resolution: "prisma@npm:5.15.0" + dependencies: + "@prisma/engines": "npm:5.15.0" + bin: + prisma: build/index.js + checksum: 8baa30db5edcc244adf4c0a6af36f4b5b3e5fd6b15db75bea6ddc636d2ebdbae9c3c2824a7a6dab6eec9630646e2ea9a5140e5806c86963bad6484560dc27b5d + languageName: node + linkType: hard + "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0"