'use strict'; const runner = require('@beet-bot/runner'); const discord_js = require('discord.js'); const https = require('https'); const fs = require('fs/promises'); function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs); const version = "0.22.0"; const HELP_COMMAND = new discord_js.SlashCommandBuilder().setName("bbhelp").setDescription("Show the beet bot help message"); const BUILTIN_COMMANDS = [ new discord_js.SlashCommandBuilder().setName("bbinfo").setDescription("Show information about the beet bot"), new discord_js.SlashCommandBuilder().setName("bbaction").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Manage beet bot actions").addStringOption( (option) => option.setName("action").setDescription("The id of the action") ), new discord_js.SlashCommandBuilder().setName("bbexport").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Export beet bot action database"), new discord_js.SlashCommandBuilder().setName("bbimport").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Import beet bot action database").addAttachmentOption( (option) => option.setName("database").setDescription("The json file describing configured actions").setRequired(true) ), new discord_js.SlashCommandBuilder().setName("bbresolve").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Debug the resolved config of a beet bot action").addStringOption( (option) => option.setName("action").setDescription("The id of the action").setRequired(true) ), new discord_js.SlashCommandBuilder().setName("bbstop").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Stop the beet bot") ]; const generateRefreshCommand = (environmentNames) => new discord_js.SlashCommandBuilder().setName("bbrefresh").setDefaultMemberPermissions(discord_js.PermissionFlagsBits.Administrator).setDescription("Refresh beet bot environment").addStringOption( (option) => option.setName("environment").setDescription("The name of the environment to refresh").setRequired(true).addChoices(...environmentNames.map((name) => ({ name, value: name }))) ); const generateGuildCommands = ({ actions }) => [ ...Object.entries(actions).flatMap(([actionId, action]) => actionId.startsWith("menu:") ? [new discord_js.ContextMenuCommandBuilder().setName(action.title).setType(discord_js.ApplicationCommandType.Message)] : []), generateRefreshCommand([...new Set(Object.values(actions).map(({ runner }) => runner))]), ..."help" in actions ? [HELP_COMMAND] : [], ...BUILTIN_COMMANDS ]; const createActionChoice = (guildInfo) => { const options = Object.entries(guildInfo.actions).flatMap( ([actionId, action]) => actionId.startsWith("menu:") ? [] : [{ label: action.title, value: actionId }] ); return { components: [ { type: discord_js.ComponentType.ActionRow, components: [ options.length > 0 ? { type: discord_js.ComponentType.SelectMenu, customId: "actionChoice.actionId", placeholder: "Select action", options } : { type: discord_js.ComponentType.SelectMenu, customId: "actionChoice.actionIdPlaceholder", placeholder: "Use /bbaction my_new_action to create an action", disabled: true, options: [{ label: "x", value: "x" }] } ] } ] }; }; const createActionChoiceDisabled = (message) => { return { components: [ { type: discord_js.ComponentType.ActionRow, components: [ { type: discord_js.ComponentType.SelectMenu, customId: "actionChoice.actionIdDisabled", placeholder: message, disabled: true, options: [{ label: message, value: "x" }] } ] } ] }; }; const createActionDashboard = ({ guildInfo, selected, success, error }) => { const options = Object.entries(guildInfo.actions).map(([actionId, action]) => ({ label: action.title, description: actionId, value: actionId, default: actionId === selected })); return { ephemeral: true, embeds: success ? [{ description: success, color: 65280 }] : error ? [{ description: error, color: 16711680 }] : [], components: [ { type: discord_js.ComponentType.ActionRow, components: [ options.length > 0 ? { type: discord_js.ComponentType.SelectMenu, customId: "actionDashboard.actionId", placeholder: "Select action", options } : { type: discord_js.ComponentType.SelectMenu, customId: "actionDashboard.actionIdPlaceholder", placeholder: "Use /bbaction my_new_action to create an action", disabled: true, options: [{ label: "x", value: "x" }] } ] }, { type: discord_js.ComponentType.ActionRow, components: [ { type: discord_js.ComponentType.Button, customId: selected ? `actionDashboard.buttonEditSelected.${selected}` : "actionDashboard.buttonEdit", label: "Edit", style: discord_js.ButtonStyle.Primary, disabled: !selected }, { type: discord_js.ComponentType.Button, customId: selected ? `actionDashboard.buttonDeleteSelected.${selected}` : "actionDashboard.buttonDelete", label: "Delete", style: discord_js.ButtonStyle.Danger, disabled: !selected } ] } ] }; }; const createEditActionModal = ({ guildInfo, selected }) => { const action = guildInfo.actions[selected] ?? {}; const id = { type: discord_js.ComponentType.TextInput, customId: "editAction.actionId", label: "Action id", placeholder: "build_message", style: discord_js.TextInputStyle.Short, minLength: 3, maxLength: 20, required: true, value: selected }; const title = { type: discord_js.ComponentType.TextInput, customId: "editAction.actionTitle", label: "Action title", placeholder: "Build message", style: discord_js.TextInputStyle.Short, maxLength: 40, required: true, value: action.title }; const runner = { type: discord_js.ComponentType.TextInput, customId: "editAction.actionRunner", label: "Runner environment", placeholder: "default", style: discord_js.TextInputStyle.Short, minLength: 3, maxLength: 20, required: true, value: action.runner }; const config = { type: discord_js.ComponentType.TextInput, customId: "editAction.actionConfig", label: "Json config", placeholder: '{\n "pipeline": [\n "mecha"\n ]\n}', style: discord_js.TextInputStyle.Paragraph, required: true, value: JSON.stringify(action.config, void 0, 2) }; return { customId: `editAction.${selected}`, title: "Edit action", components: [ { type: discord_js.ComponentType.ActionRow, components: [id] }, { type: discord_js.ComponentType.ActionRow, components: [title] }, { type: discord_js.ComponentType.ActionRow, components: [runner] }, { type: discord_js.ComponentType.ActionRow, components: [config] } ] }; }; const formatLog = (log) => { if (!log) { return ""; } return log.map(({ level, message, annotation, details }) => { let result = level.toUpperCase().padEnd(7) + message + "\n"; if (annotation) { result += annotation + "\n"; } if (details) { for (const line of details) { result += line + "\n"; } } return result; }).join(""); }; const formatPackContents = ({ empty, text_files, binary_files }) => { const textCount = Object.keys(text_files ?? {}).length; const binaryCount = Object.keys(binary_files ?? {}).length; const images = Object.entries(binary_files ?? {}).filter(([path, _]) => path.endsWith(".png")).slice(0, 3); const sections = []; if (text_files && textCount < 16) { for (const path in text_files) { if (path !== "pack.mcmeta") { const content = text_files[path]; if (content.trim()) { sections.push("`" + path + "`\n```\n" + content + "\n```\n"); } } } } return [!empty && (binaryCount > images.length || textCount > 6), sections, images]; }; const reduceLargestSection = (sections) => { let maxLength = 0; let maxIndex = 0; for (let i = 0; i < sections.length; i++) { if (sections[i].length >= maxLength) { maxLength = sections[i].length; maxIndex = i; } } sections.splice(maxIndex, 1); return sections; }; const createReport = ({ error, log, stdout, data_pack, resource_pack }, forceZip = false) => { const formattedLog = formatLog(log); let attachStdout = ""; let attachLog = ""; let attachTraceback = ""; let stdoutSection = stdout?.trim() ? "```\n" + stdout + "\n```\n" : ""; let logSection = formattedLog ? "```\n" + formattedLog + "```\n" : ""; let errorSection = error ? "```\n" + error.message + (error?.exception ? "\n\n" + error.exception : "") + "\n```" : ""; let [shouldZipDataPack, dataPackSections, dataPackImages] = formatPackContents(!forceZip && data_pack || {}); let [shouldZipResourcePack, resourcePackSections, resourcePackImages] = formatPackContents(!forceZip && resource_pack || {}); shouldZipDataPack || (shouldZipDataPack = forceZip && !data_pack?.empty); shouldZipResourcePack || (shouldZipResourcePack = forceZip && !resource_pack?.empty); const joinContent = () => stdoutSection + logSection + errorSection + dataPackSections.join("") + resourcePackSections.join(""); let content = joinContent(); while (content.length > 2e3) { if (stdoutSection.length > content.length / 2) { stdoutSection = ""; if (stdout) { attachStdout = stdout; } } else if (logSection.length > content.length / 2) { logSection = ""; if (formattedLog) { attachLog = formattedLog; } } else if (errorSection.length > content.length / 2) { errorSection = error ? "```\n" + error.message + "\n```" : ""; if (error?.exception) { attachTraceback = error.exception; } } else if (dataPackSections.length > resourcePackSections.length) { dataPackSections = reduceLargestSection(dataPackSections); shouldZipDataPack = true; } else { resourcePackSections = reduceLargestSection(resourcePackSections); shouldZipResourcePack = true; } const previousLength = content.length; content = joinContent(); if (content.length === previousLength) { content = "```\nCould not summarize in less than 2000 characters. Check out attachments for more details.\n```"; break; } } if (content === dataPackSections[0] || content === resourcePackSections[0]) { content = content.slice(content.indexOf("\n") + 1); } const files = []; for (const [path, data] of [...dataPackImages, ...resourcePackImages]) { files.push({ attachment: Buffer.from(data, "base64"), name: path.split("/").at(-1) }); } if (attachStdout) { files.push({ attachment: Buffer.from(attachStdout, "utf-8"), name: "stdout.txt" }); } if (attachLog) { files.push({ attachment: Buffer.from(attachLog, "utf-8"), name: "log.txt" }); } if (attachTraceback) { files.push({ attachment: Buffer.from(attachTraceback, "utf-8"), name: "traceback.txt" }); } if (shouldZipDataPack && data_pack?.zip) { files.push({ attachment: Buffer.from(data_pack.zip, "base64"), name: "data_pack.zip" }); } if (shouldZipResourcePack && resource_pack?.zip) { files.push({ attachment: Buffer.from(resource_pack.zip, "base64"), name: "resource_pack.zip" }); } if (!content && files.length === 0) { content = ":thumbsup:"; } return { content: content || void 0, files, components: [] }; }; const download = (url, encoding) => { return new Promise((resolve, reject) => { https.get(url, (res) => { res.setEncoding(encoding); let body = ""; res.on("data", (data) => { body += data; }); res.on("end", () => resolve(body)); }).on("error", (err) => reject(err)); }); }; const downloadAsBase64Url = async (url, contentType) => { const content = await download(url, "base64"); return `data:${contentType};base64,${content}`; }; const packMessage = async (message) => { let input = ""; if (!message) { return input; } for (const attachment of message.attachments.values()) { if (attachment.contentType === "application/zip") { try { const base64 = await downloadAsBase64Url(attachment.url, attachment.contentType); input += "```\n@merge_zip(download)\n" + base64 + "\n```\n"; } catch (err) { console.log(`ERROR: ${err}`); } } } input += message.content; return input; }; const resolveActionOverrides = (config, guildInfo) => { if (Array.isArray(config.overrides)) { return { ...config, overrides: config.overrides.map((override) => { if (typeof override === "string" && override.startsWith("!")) { const actionId = override.substring(1); const action = guildInfo.actions[actionId]; if (action) { return JSON.stringify(resolveActionOverrides(action.config, guildInfo)); } } return override; }) }; } return config; }; const invokeBuild = async (runner, name, config, message) => { if (!Array.isArray(config.pipeline)) { config.pipeline = []; } config.pipeline.unshift("lectern.contrib.messaging"); if (typeof config.meta !== "object") { config.meta = {}; } if (typeof config.meta.messaging !== "object") { config.meta.messaging = {}; } config.meta.messaging.input = await packMessage(message); try { return await runner.build(name, config); } catch (err) { return { status: "error", error: { message: `${err}` } }; } }; const handleInteractions = ({ clientId, discordClient, discordApi, db, environments, runner }) => { const updateGuildCommands = async (guildId) => { try { await discordApi.put(discord_js.Routes.applicationGuildCommands(clientId, guildId), { body: generateGuildCommands(await db.getGuildInfo(guildId)) }); console.log(`INFO: Updated commands for guild ${guildId}`); } catch (err) { console.log(err); console.log(`ERROR: Failed to update commands for guild ${guildId}`); } }; discordClient.on("ready", async () => { console.log("INFO: Client is ready"); const allGuilds = await db.getAllGuilds(); await Promise.all(allGuilds.map(updateGuildCommands)); }); const addGuild = async (guildId) => { const allGuilds = await db.getAllGuilds(); if (!allGuilds.includes(guildId)) { await db.setAllGuilds([...allGuilds, guildId]); console.log(`INFO: Added ${guildId} to guild registry`); } }; const removeGuild = async (guildId) => { const allGuilds = await db.getAllGuilds(); if (allGuilds.includes(guildId)) { await db.setAllGuilds(allGuilds.filter((id) => id !== guildId)); console.log(`INFO: Removed ${guildId} from guild registry`); } await db.delGuildInfo(guildId); }; discordClient.on("guildCreate", async (guild) => { console.log(`INFO: Joined guild ${guild.id}`); await addGuild(guild.id); await updateGuildCommands(guild.id); }); discordClient.on("guildDelete", async (guild) => { console.log(`INFO: Left guild ${guild.id}`); await removeGuild(guild.id); }); discordClient.on("roleUpdate", async (role) => { console.log(`INFO: Role updated in guild ${role.guild.id}`); await addGuild(role.guild.id); await updateGuildCommands(role.guild.id); }); const pingRegex = new RegExp(`<@!?${clientId}>( *[a-zA-Z0-9_]{3,20})?`); discordClient.on("messageCreate", async (message) => { if (!message.author.bot && message.inGuild() && message.mentions.users.has(clientId)) { const guildInfo = await db.getGuildInfo(message.guildId); let actionId = message.content.match(pingRegex)?.[1]?.trim(); if (actionId && guildInfo.actions[actionId]) { const { runner: name2, config: config2, zip: zip2 } = guildInfo.actions[actionId]; message.channel.sendTyping(); const resolvedConfig2 = resolveActionOverrides(config2, guildInfo); const info2 = await invokeBuild(runner, name2, resolvedConfig2, message); await message.reply(createReport(info2, zip2)); return; } const reply = await message.reply(createActionChoice(guildInfo)); let interaction; try { interaction = await reply.awaitMessageComponent({ componentType: discord_js.ComponentType.StringSelect, time: 15e3, filter: (interaction2) => { if (interaction2.user.id === message.author.id) { return true; } else { interaction2.deferUpdate(); return false; } } }); } catch (err) { await reply.delete(); return; } actionId = interaction.values[0]; const { title, runner: name, config, zip } = guildInfo.actions[actionId]; await interaction.update(createActionChoiceDisabled(title)); const resolvedConfig = resolveActionOverrides(config, guildInfo); const info = await invokeBuild(runner, name, resolvedConfig, message); await interaction.editReply(createReport(info, zip)); } }); discordClient.on("interactionCreate", async (interaction) => { if (interaction.inGuild() && interaction.isCommand()) { if (interaction.commandName === "bbhelp") { const guildInfo = await db.getGuildInfo(interaction.guildId); if (guildInfo.actions.help) { const { runner: name, config, zip } = guildInfo.actions.help; const resolvedConfig = resolveActionOverrides(config, guildInfo); const info = await invokeBuild(runner, name, resolvedConfig); await interaction.reply(createReport(info, zip)); return; } } else if (interaction.commandName === "bbinfo") { let info = "beet-bot v" + version + "\nuptime: "; let uptime = process.uptime(); for (const [unit, resolution] of [["second", 60], ["minute", 60], ["hour", 24], ["day", Infinity]]) { if (uptime < resolution) { info += new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-Math.floor(uptime), unit); break; } else { uptime /= resolution; } } info += "\nenvironments: " + environments.join(", "); await interaction.reply("```\n" + info + "\n```"); } else if (interaction.commandName === "bbrefresh") { const name = interaction.options.get("environment")?.value; if (typeof name === "string") { await interaction.deferReply(); try { await runner.refresh(name); await interaction.editReply('```\nSuccessfully refreshed "' + name + '" environment\n```'); } catch (err) { await interaction.editReply("```\n" + err + "\n```"); } } else { await interaction.reply("```\nSpecify an environment name\n```"); } } else if (interaction.commandName === "bbaction") { const guildInfo = await db.getGuildInfo(interaction.guildId); const currentActions = Object.keys(guildInfo.actions); const actionId = interaction.options.get("action")?.value; if (typeof actionId === "string") { if (actionId.match(/^(?:menu:)?[a-zA-Z0-9_]{3,20}$/)) { if (currentActions.includes(actionId) || !actionId.startsWith("menu:") || currentActions.filter((id) => id.startsWith("menu:")).length < 5) { await interaction.showModal(createEditActionModal({ guildInfo, selected: actionId })); } else { await interaction.reply(createActionDashboard({ guildInfo, error: "Already reached limit of 5 context menu actions" })); } } else { await interaction.reply(createActionDashboard({ guildInfo, error: `Invalid action id \`${actionId}\`` })); } } else { await interaction.reply(createActionDashboard({ guildInfo })); } } else if (interaction.commandName === "bbexport") { const guildInfo = await db.getGuildInfo(interaction.guildId); await interaction.reply({ ephemeral: true, files: [{ attachment: Buffer.from(JSON.stringify(guildInfo, void 0, 2), "utf-8"), name: "actionDatabase.json" }] }); } else if (interaction.commandName === "bbimport") { const attachment = interaction.options.getAttachment("database"); if (attachment) { try { const content = await download(attachment.url, "utf-8"); const actionDatabase = JSON.parse(content); const actionCount = Object.keys(actionDatabase.actions).length; await db.setGuildInfo(interaction.guildId, actionDatabase); await updateGuildCommands(interaction.guildId); await interaction.reply({ ephemeral: true, embeds: [{ description: `Successfully imported ${actionCount} action(s) from attached json`, color: 65280 }] }); } catch (err) { console.log(`ERROR: ${err}`); await interaction.reply({ ephemeral: true, embeds: [{ description: "Failed to import actions from attached json", color: 16711680 }] }); } } else { await interaction.reply({ ephemeral: true, embeds: [{ description: "Missing json attachment for action database", color: 16711680 }] }); } } else if (interaction.commandName === "bbresolve") { const guildInfo = await db.getGuildInfo(interaction.guildId); const actionId = interaction.options.get("action")?.value; if (typeof actionId === "string" && guildInfo.actions[actionId]) { const resolvedConfig = resolveActionOverrides(guildInfo.actions[actionId].config, guildInfo); await interaction.reply({ ephemeral: true, files: [{ attachment: Buffer.from(JSON.stringify(resolvedConfig, void 0, 2), "utf-8"), name: `${actionId.replace("menu:", "")}.json` }] }); } else { await interaction.reply({ ephemeral: true, embeds: [{ description: `Could not find action \`${actionId}\``, color: 16711680 }] }); } } else if (interaction.commandName === "bbstop") { await interaction.reply("```\nShutting down...\n```"); discordClient.destroy(); process.exit(); } } if (interaction.inGuild() && interaction.isStringSelectMenu()) { const [scope, name] = interaction.customId.split("."); if (scope === "actionDashboard" && name === "actionId") { const guildInfo = await db.getGuildInfo(interaction.guildId); await interaction.update(createActionDashboard({ guildInfo, selected: interaction.values[0] })); } } if (interaction.inGuild() && interaction.isButton()) { const [scope, name, actionId] = interaction.customId.split("."); if (scope === "actionDashboard") { const guildInfo = await db.getGuildInfo(interaction.guildId); if (name === "buttonEditSelected") { await interaction.showModal(createEditActionModal({ guildInfo, selected: actionId })); } else if (name === "buttonDeleteSelected") { delete guildInfo.actions[actionId]; await db.setGuildInfo(interaction.guildId, guildInfo); await interaction.update(createActionDashboard({ guildInfo, success: `Successfully deleted action \`${actionId}\`` })); if (actionId.startsWith("menu:")) { await updateGuildCommands(interaction.guildId); } } } } if (interaction.inGuild() && interaction.isModalSubmit()) { const [scope, name] = interaction.customId.split("."); if (scope === "editAction") { const updateDashboard = (options) => interaction.isFromMessage() ? interaction.update(createActionDashboard(options)) : interaction.reply(createActionDashboard(options)); const guildInfo = await db.getGuildInfo(interaction.guildId); const actionId = name; const newActionId = interaction.fields.getTextInputValue("editAction.actionId"); const newActionTitle = interaction.fields.getTextInputValue("editAction.actionTitle"); const newActionRunner = interaction.fields.getTextInputValue("editAction.actionRunner"); let newActionConfig = interaction.fields.getTextInputValue("editAction.actionConfig"); if (!newActionId.match(/^(?:menu:)?[a-zA-Z0-9_]{3,20}$/)) { await updateDashboard({ guildInfo, selected: actionId, error: `Invalid action id \`${newActionId}\`` }); return; } if (newActionId !== actionId && newActionId.startsWith("menu:") && Object.keys(guildInfo.actions).filter((id) => id !== actionId && id.startsWith("menu:")).length >= 5) { await updateDashboard({ guildInfo, error: "Already reached limit of 5 context menu actions" }); return; } if (!environments.includes(newActionRunner)) { await updateDashboard({ guildInfo, selected: actionId, error: `Invalid runner \`${newActionRunner}\`` }); return; } try { newActionConfig = JSON.parse(newActionConfig); } catch { await updateDashboard({ guildInfo, selected: actionId, error: "Couldn't parse json config\n```\n" + newActionConfig + "\n```" }); return; } if (typeof newActionConfig !== "object") { await updateDashboard({ guildInfo, selected: actionId, error: "Action config must be a json object\n```\n" + JSON.stringify(newActionConfig, void 0, 2) + "\n```" }); return; } delete guildInfo.actions[actionId]; guildInfo.actions[newActionId] = { title: newActionTitle, runner: newActionRunner, config: newActionConfig, zip: newActionId.includes("zip") }; await db.setGuildInfo(interaction.guildId, guildInfo); updateDashboard({ guildInfo, selected: newActionId, success: `Successfully updated action \`${newActionId}\`` }); if (actionId.startsWith("menu:") || newActionId.startsWith("menu:") || actionId === "help" || newActionId === "help") { await updateGuildCommands(interaction.guildId); } } } if (interaction.inGuild() && interaction.isMessageContextMenuCommand()) { const guildInfo = await db.getGuildInfo(interaction.guildId); const actionMatch = Object.entries(guildInfo.actions).filter(([actionId, action]) => actionId.startsWith("menu:") && action.title === interaction.commandName).map(([_, action]) => action); if (actionMatch.length > 0) { const { runner: name, config, zip } = actionMatch[0]; let deferred; const tid = setTimeout(() => { deferred = interaction.deferReply(); }, 800); const resolvedConfig = resolveActionOverrides(config, guildInfo); const info = await invokeBuild(runner, name, resolvedConfig, interaction.targetMessage); if (deferred) { await deferred; await interaction.editReply(createReport(info, zip)); } else { clearTimeout(tid); await interaction.reply(createReport(info, zip)); } } } }); }; const createAdapter = async (config) => { if (!config || !config.type || config.type === "memory") { console.log("INFO: Using in-memory database adapter"); return createInMemoryAdapter(); } else if (config.type === "json") { console.log("INFO: Using json database adapter"); return await createJsonAdapter(config.path); } else if (config.type === "dynamodb") { console.log("INFO: Using dynamodb adapter"); return await createDynamoDBAdapter(config.table, config.region); } console.log("WARN: Default to in memory adapter due to invalid config"); return createInMemoryAdapter(); }; const createInMemoryAdapter = () => { const data = /* @__PURE__ */ new Map(); return { get: (key, defaultValue = null) => Promise.resolve(JSON.parse(data.get(key) ?? JSON.stringify(defaultValue))), set: (key, value) => Promise.resolve(data.set(key, JSON.stringify(value))), del: (key) => Promise.resolve(data.delete(key)) }; }; const createJsonAdapter = async (filename) => { const data = JSON.parse(await fs__namespace.readFile(filename, { encoding: "utf-8" }).catch(() => "{}")); return { get: (key, defaultValue = null) => Promise.resolve(JSON.parse(data[key] ?? JSON.stringify(defaultValue))), set: (key, value) => { data[key] = JSON.stringify(value); return fs__namespace.writeFile(filename, JSON.stringify(data, void 0, 2)); }, del: (key) => { delete data[key]; return fs__namespace.writeFile(filename, JSON.stringify(data, void 0, 2)); } }; }; const createDynamoDBAdapter = async (table, region) => { const { default: aws } = await import('aws-sdk'); const ddb = new aws.DynamoDB({ region }); return { get: async (key, defaultValue = null) => { const result = await ddb.getItem({ TableName: table, Key: { Id: { S: key } } }).promise(); return JSON.parse(result.Item?.Data.S ?? JSON.stringify(defaultValue)); }, set: async (key, value) => { await ddb.putItem({ TableName: table, Item: { Id: { S: key }, Data: { S: JSON.stringify(value) } } }).promise(); }, del: async (key) => { await ddb.deleteItem({ TableName: table, Key: { Id: { S: key } } }).promise(); } }; }; const createDatabase = (adapter) => { return { getAllGuilds: () => adapter.get("allGuilds", []), setAllGuilds: (allGuilds) => adapter.set("allGuilds", allGuilds), getGuildInfo: (guildId) => adapter.get(`guildInfo.${guildId}`, { actions: {} }), setGuildInfo: (guildId, commands) => adapter.set(`guildInfo.${guildId}`, commands), delGuildInfo: (guildId) => adapter.del(`guildInfo.${guildId}`) }; }; const runBeetBot = async ({ clientId, token, awsRegion, database, environments }) => { if (clientId.startsWith("ssm:") || token.startsWith("ssm:")) { const { default: aws } = await import('aws-sdk'); const ssm = new aws.SSM({ region: awsRegion }); if (clientId.startsWith("ssm:")) { const result = await ssm.getParameter({ Name: clientId.slice(4) }).promise(); if (!result.Parameter?.Value) { console.log(`ERROR: Failed to retrieve clientId parameter "${clientId}"`); process.exit(1); } clientId = result.Parameter.Value; } if (token.startsWith("ssm:")) { const result = await ssm.getParameter({ Name: token.slice(4), WithDecryption: true }).promise(); if (!result.Parameter?.Value) { console.log(`ERROR: Failed to retrieve token parameter "${token}"`); process.exit(1); } token = result.Parameter.Value; } } const discordClient = new discord_js.Client({ intents: [discord_js.GatewayIntentBits.Guilds, discord_js.GatewayIntentBits.GuildMessages] }); const discordApi = new discord_js.REST({ version: "10" }).setToken(token); handleInteractions({ clientId, discordClient, discordApi, db: createDatabase(await createAdapter(database)), environments: Object.keys(environments ?? {}), runner: runner.createPoolRunner(environments) }); discordClient.login(token); }; exports.runBeetBot = runBeetBot;