From 49da6d8041af6cb3c6a8b34ea7fc59ec4cc3f48e Mon Sep 17 00:00:00 2001 From: dakedres Date: Sat, 20 May 2023 03:31:10 -0600 Subject: [PATCH] Add macro system --- .gitignore | 3 +- package.json | 1 + src/constants.js | 47 ++++++++++- src/index.js | 202 +++++++++++++++++++++++++++++++++++++++++++++-- template.env | 3 +- yarn.lock | 94 ++++++++++++++++++++-- 6 files changed, 331 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 1c1dd40..446a0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ *.ignore -.env \ No newline at end of file +.env +db/ \ No newline at end of file diff --git a/package.json b/package.json index 37a8cb7..b83c605 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "type": "module", "dependencies": { + "classic-level": "^1.3.0", "discord.js": "^14.11.0", "dotenv": "^16.0.3" } diff --git a/src/constants.js b/src/constants.js index 7a69aa1..7d0668f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,6 +1,49 @@ const constants = { -rollRegex: /^(\d+)?([dhl])(\d+)(\s*([+\-*x\/])\s*(\d+))?/, -descriptionRegex: /\s*(\d+(-\d+)?)?([^;\n]+)/g + rollRegex: /^(\d+)?([dhl])(\d+)(\s*([+\-*x\/])\s*(\d+))?/, + descriptionRegex: /\s*(\d+(-\d+)?)?([^;\n]+)/g, + macroNameRegex: /^[a-z0-9]+$/, + + commands: { + about: { + name: 'about', + description: "Get information about dicedicedice" + }, + + macro: { + name: 'macro', + type: 1, + description: "Define a dice macro", + options: [ + { + name: "name", + description: "Name of the macro", + type: 3, // String + required: true + }, + { + name: "dice", + description: "The dice expression to save as a macro", + type: 3, // String + required: true + } + ] + } + }, + + iconUrl: 'https://github.com/Dakedres/dicedicedice/raw/main/assets/eater-transparent.png', + + errorMessage: error => `\ +Something went wrong trying to execute that command. +\`\`\`fix +${error.toString()} +\`\`\` +If this issue persists please report it here: \ +`, + aboutMessage: (guildCount) => `\ +A discord bot for metaphorically "rolling dice"/generating random values. Made for use with Weaverdice systems. + +Present in ~${guildCount} guilds! +` } export default constants \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0e6504a..1a10bfd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,13 @@ -import { Client, GatewayIntentBits } from 'discord.js'; +import { Client, GatewayIntentBits, REST, Routes } from 'discord.js'; import * as dotenv from 'dotenv' import constants from './constants.js'; +import { ClassicLevel } from 'classic-level'; + dotenv.config() const replies = new Map() +const commands = new Map() +const db = new ClassicLevel('./db') const parseRoll = content => { let match = constants.rollRegex.exec(content.trim()) @@ -65,6 +69,10 @@ const handleMessage = (message, respond) => { if(dice == undefined) return // No dice + handleDice(dice, respond) +} + +const handleDice = (dice, respond) => { if(dice.size > 255) { respond('That die is way too big... .-.') return @@ -79,10 +87,10 @@ const handleMessage = (message, respond) => { } let rolls = [ ...crypto.getRandomValues(new Uint8Array(dice.count) ) ] - .map(n => Math.ceil((n / 256) * dice.size)), - result = 0, - operationSymbol = dice.operation, - response = '' + .map(n => Math.ceil((n / 256) * dice.size)) + let result = 0 + let operationSymbol = dice.operation + let response = '' switch(dice.mode) { case 'd': @@ -163,6 +171,141 @@ const pruneReplies = () => { } } +const interactionRespond = (interaction, content) => { + let reply = { content, ephemeral: true } + + if(interaction.replied || interaction.deferred) { + return interaction.followUp(reply) + } else { + return interaction.reply(reply) + } +} + +const handleError = (error, interaction) => + interactionRespond(interaction, constants.errorMessage(error) ) + .catch(reportingError => console.error('Could not display error message:\n ', reportingError) ) + + +const addCommand = (data, callback) => { + commands.set(data.name, { + data, + execute: callback + }) +} + +const openMacros = guildId => + db.sublevel(guildId).sublevel('macros') + +const registerMacroCommands = async guildId => { + let commands = [] + let macros = openMacros(guildId) + + for await (let [ name, dice ] of macros.iterator() ) + commands.push({ + name, + description: "Roll " + dice.replaceAll('\n', ';') + }) + + await rest.put( + Routes.applicationGuildCommands(process.env.DISCORD_ID, guildId), + { body: commands } + ) +} + +const pruneDB = async () => { + let validIds = [] + + for await(let key of db.keys()) { + let [ guildId ] = key.split('!').slice(1) + + console.log(guildId) + + if(validIds.includes(guildId)) + continue + + if(client.guilds.cache.has(guildId)) { + validIds.push(guildId) + } else { + console.log('Pruning key: ' + key) + await db.del(key) + } + } + + return validIds +} + + +addCommand( + constants.commands.about, + async interaction => { + let embed = { + title: 'dicedicedice', + thumbnail: { + url: constants.iconUrl + }, + description: constants.aboutMessage(client.guilds.cache.size) + } + + await interaction.reply({ + embeds: [ embed ], + ephemeral: true + }) + } +) + +addCommand( + constants.commands.macro, + async interaction => { + let name = interaction.options.get('name').value.toLowerCase() + + if(!constants.macroNameRegex.test(name) ) { + interaction.reply("Please provide a macro name that consists of only alphanumeric characters.") + return + } + + if(commands.has(name)) { + interaction.reply("Uhh... I think that macro name is already taken by my own commands, sorry.") + return + } + + // let dice = parseRoll(interaction.options.get('dice').value) + let dice = interaction.options.get('dice').value + + if(!constants.rollRegex.test(dice) ) { + interaction.reply("Please provide a valid roll expression.") + return + } + + // let exists = true + // let macros = openTable(interaction.guild, 'macros') + // let macro = await macros.get(name) + // .catch(err => { + // if(err.code == 'LEVEL_NOT_FOUND') + // exists = false + // else + // handleError(err, interaction) + // }) + + // if(exists) { + // interaction.followUp('A macro with this name already exists in this guild.') + // return + // } + + + await interaction.deferReply() + + let macros = openMacros(interaction.guild.id) + + await Promise.all([ + macros.put(name, dice), + registerMacroCommands(interaction.guild.id) + ]) + interaction.followUp(`Macro added! Try \`/${name}\``) + } +) + + + const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -172,8 +315,17 @@ const client = new Client({ ] }) -client.on('ready', () => { +const rest = new REST().setToken(process.env.DISCORD_TOKEN) + +client.on('ready', async () => { console.log("Logged in!") + + let guildIds = await pruneDB() + + for(let guildId of guildIds) + await registerMacroCommands(guildId) + + console.log("Ready") }) client.on('messageCreate', messageCycle) @@ -190,5 +342,39 @@ client.on('messageUpdate', async (oldMessage, newMessage) => { } }) -client.login(process.env.DISCORD_TOKEN); -setInterval(pruneReplies, 1000 * 60) +client.on('interactionCreate', async interaction => { + if(!interaction.isChatInputCommand()) + return + + if(commands.has(interaction.commandName) ) { + commands.get(interaction.commandName).execute(interaction) + .catch(err => handleError(err, interaction) ) + return + } + + let roll = await openMacros(interaction.guild.id).get(interaction.commandName) + + if(roll) { + let dice = parseRoll(roll) + + handleDice(dice, content => interaction.reply(content) ) + } +}) + + + +;(async () => { + await rest.put( + Routes.applicationCommands(process.env.DISCORD_ID), + { + body: [ ...commands.values() ] + .map(command => command.data ) + } + ) + .catch(err => console.error('Command registration failed: ', err) ) + + await client.login(process.env.DISCORD_TOKEN) + .catch(err => console.error('Login failed: ', err) ) + + setInterval(pruneReplies, 1000 * 60) +})() diff --git a/template.env b/template.env index 9cc6489..860fdbd 100644 --- a/template.env +++ b/template.env @@ -1 +1,2 @@ -DISCORD_TOKEN="token_here" \ No newline at end of file +DISCORD_TOKEN="token_here" +DISCORD_ID="client_id_here" \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 05f2e09..9654b39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -85,9 +85,9 @@ integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== "@types/node@*": - version "20.2.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.0.tgz#e33da33171ac4eba79b9cfe30b68a4f1561e74ec" - integrity sha512-3iD2jaCCziTx04uudpJKwe39QxXgSUnpxXSvRQjRvHPxFQfmfP4NXIm/NURVeNlTCc+ru4WqjYGTmpXrW9uMlw== + version "20.2.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.1.tgz#de559d4b33be9a808fd43372ccee822c70f39704" + integrity sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg== "@types/ws@^8.5.4": version "8.5.4" @@ -101,6 +101,32 @@ resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz#84c5a3f8d648842cec5cc649b88df599af32ed88" integrity sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ== +abstract-level@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741" + integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA== + dependencies: + buffer "^6.0.3" + catering "^2.1.0" + is-buffer "^2.0.5" + level-supports "^4.0.0" + level-transcoder "^1.0.1" + module-error "^1.0.1" + queue-microtask "^1.2.3" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -108,6 +134,22 @@ busboy@^1.6.0: dependencies: streamsearch "^1.1.0" +catering@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" + integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== + +classic-level@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-1.3.0.tgz#5e36680e01dc6b271775c093f2150844c5edd5c8" + integrity sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg== + dependencies: + abstract-level "^1.0.2" + catering "^2.1.0" + module-error "^1.0.1" + napi-macros "^2.2.2" + node-gyp-build "^4.3.0" + discord-api-types@^0.37.41: version "0.37.42" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.42.tgz#3c196267ed31e9ea249e6880c15e2af1c6428629" @@ -154,7 +196,7 @@ file-type@^18.3.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== inherits@^2.0.3: @@ -162,6 +204,24 @@ inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +level-supports@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-4.0.1.tgz#431546f9d81f10ff0fea0e74533a0e875c08c66a" + integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA== + +level-transcoder@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" + integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== + dependencies: + buffer "^6.0.3" + module-error "^1.0.1" + lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -172,11 +232,31 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +module-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" + integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== + +napi-macros@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.2.2.tgz#817fef20c3e0e40a963fbf7b37d1600bd0201044" + integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== + +node-gyp-build@^4.3.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + peek-readable@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== +queue-microtask@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -232,9 +312,9 @@ ts-mixer@^6.0.3: integrity sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ== tslib@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.1.tgz#f2ad78c367857d54e49a0ef9def68737e1a67b21" - integrity sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw== + version "2.5.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" + integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== undici@^5.22.0: version "5.22.1"