Add macro system

This commit is contained in:
dakedres 2023-05-20 03:31:10 -06:00
parent 77dd93296e
commit 49da6d8041
6 changed files with 331 additions and 19 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
*.ignore
.env
.env
db/

View File

@ -19,6 +19,7 @@
},
"type": "module",
"dependencies": {
"classic-level": "^1.3.0",
"discord.js": "^14.11.0",
"dotenv": "^16.0.3"
}

View File

@ -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: <https://github.com/Dakedres/dicedicedice/issues>\
`,
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

View File

@ -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)
})()

View File

@ -1 +1,2 @@
DISCORD_TOKEN="token_here"
DISCORD_TOKEN="token_here"
DISCORD_ID="client_id_here"

View File

@ -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"