Change tab size

This commit is contained in:
Dakedres 2025-03-18 16:25:11 -06:00
parent 7912b455e9
commit ece60db183
2 changed files with 407 additions and 407 deletions

View File

@ -1,65 +1,65 @@
export default {
rollRegex: /^(\d+)?([dhlfb])(\d+)(\s*([+\-*x\/])\s*(\d+))?/,
optionRollRegex: /^(\d+)?(([dhlfb])(\d+))?(\s*([+\-*x\/])\s*(\d+))?/,
descriptionRegex: /\s*((\d*-\d*)|(\d+))?([^;\n]+)/g,
macroNameRegex: /^[a-z0-9]+$/,
rollRegex: /^(\d+)?([dhlfb])(\d+)(\s*([+\-*x\/])\s*(\d+))?/,
optionRollRegex: /^(\d+)?(([dhlfb])(\d+))?(\s*([+\-*x\/])\s*(\d+))?/,
descriptionRegex: /\s*((\d*-\d*)|(\d+))?([^;\n]+)/g,
macroNameRegex: /^[a-z0-9]+$/,
commands: {
about: {
name: 'about',
description: "Get information about dicedicedice"
},
commands: {
about: {
name: 'about',
description: "Get information about dicedicedice"
},
macro: {
name: 'macro',
description: "Manage macros",
'dm_permission': false,
options: [
{
name: 'add',
description: "Define a dice macro",
type: 1, // Sub command
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
}
]
},
{
name: 'remove',
description: "Remove a macro",
type: 1, // Sub command
options: [
{
name: "name",
description: "Name of the macro",
type: 3, // String
required: true,
autocomplete: true,
getAutocomplete: interaction => {
let macros = globalThis.macroCache.get(interaction.guild.id)
return macros ? Object.keys(macros) : []
}
}
]
}
]
}
},
macro: {
name: 'macro',
description: "Manage macros",
'dm_permission': false,
options: [
{
name: 'add',
description: "Define a dice macro",
type: 1, // Sub command
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
}
]
},
{
name: 'remove',
description: "Remove a macro",
type: 1, // Sub command
options: [
{
name: "name",
description: "Name of the macro",
type: 3, // String
required: true,
autocomplete: true,
getAutocomplete: interaction => {
let macros = globalThis.macroCache.get(interaction.guild.id)
return macros ? Object.keys(macros) : []
}
}
]
}
]
}
},
iconUrl: 'https://github.com/Dakedres/dicedicedice/raw/main/assets/eater-transparent.png',
iconUrl: 'https://github.com/Dakedres/dicedicedice/raw/main/assets/eater-transparent.png',
errorMessage: error => `\
errorMessage: error => `\
Something went wrong trying to execute that command.
\`\`\`fix
${error.toString()}
@ -67,7 +67,7 @@ ${error.toString()}
If this issue persists please report it here: <https://github.com/Dakedres/dicedicedice/issues>\
`,
aboutMessage: (guildCount) => `\
aboutMessage: (guildCount) => `\
A discord bot for metaphorically "rolling dice"/generating random values. Made for use with Weaverdice systems.
Present in ~${guildCount} guilds!

View File

@ -11,508 +11,508 @@ globalThis.macroCache = new Map()
const db = new ClassicLevel('./db')
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages
],
partials: [
Partials.Channel
]
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages
],
partials: [
Partials.Channel
]
})
const rest = new REST().setToken(process.env.DISCORD_TOKEN)
const ParseRollInt = (value, defaultValue) =>
value ? parseInt(value) : defaultValue
value ? parseInt(value) : defaultValue
const ParseOptionRoll = expression => {
let match = constants.optionRollRegex.exec(expression.trim())
let match = constants.optionRollRegex.exec(expression.trim())
let [
count,
modeSize,
mode,
size,
operationModifier,
operation,
modifier
] = match
.slice(1)
return {
count: ParseRollInt(count),
mode,
size: ParseRollInt(size),
operation,
modifier: ParseRollInt(modifier),
descriptionConditions: PullDescription(expression, match)
}
let [
count,
modeSize,
mode,
size,
operationModifier,
operation,
modifier
] = match
.slice(1)
return {
count: ParseRollInt(count),
mode,
size: ParseRollInt(size),
operation,
modifier: ParseRollInt(modifier),
descriptionConditions: PullDescription(expression, match)
}
}
const ParseRoll = expression => {
let match = constants.rollRegex.exec(expression.trim())
let match = constants.rollRegex.exec(expression.trim())
if(match == null)
return
if(match == null)
return
let [
count,
mode,
size,
modifierString,
operation,
modifier
] = match.slice(1)
let [
count,
mode,
size,
modifierString,
operation,
modifier
] = match.slice(1)
return {
count: ParseRollInt(count, 1),
mode,
size: ParseRollInt(size),
operation,
modifier: ParseRollInt(modifier),
descriptionConditions: PullDescription(expression, match)
}
return {
count: ParseRollInt(count, 1),
mode,
size: ParseRollInt(size),
operation,
modifier: ParseRollInt(modifier),
descriptionConditions: PullDescription(expression, match)
}
}
const PullDescription = (expression, match) => {
if(match[0].length == expression.length)
return
if(match[0].length == expression.length)
return
return ParseDescription(expression.slice(match[0].length))
return ParseDescription(expression.slice(match[0].length))
}
const ParseDescription = description => {
let conditions = []
let match
let conditions = []
let match
while((match = constants.descriptionRegex.exec(description)) !== null) {
let range
let [
rangeExp,
valueExp,
content
] = match.slice(2)
while((match = constants.descriptionRegex.exec(description)) !== null) {
let range
let [
rangeExp,
valueExp,
content
] = match.slice(2)
if(rangeExp) {
let split = rangeExp.split('-')
if(rangeExp) {
let split = rangeExp.split('-')
range = {
lower: ParseRollInt(split[0], -Infinity),
upper: ParseRollInt(split[1], Infinity)
}
} else if(valueExp) {
range = {
upper: valueExp,
lower: valueExp
}
}
range = {
lower: ParseRollInt(split[0], -Infinity),
upper: ParseRollInt(split[1], Infinity)
}
} else if(valueExp) {
range = {
upper: valueExp,
lower: valueExp
}
}
conditions.push({
range,
content: content.trim()
})
}
conditions.push({
range,
content: content.trim()
})
}
return conditions
return conditions
}
const OnMessage = (message, respond) => {
let dice = ParseRoll(message.content)
let dice = ParseRoll(message.content)
if(dice == undefined)
return // No dice
if(dice == undefined)
return // No dice
RollDice(dice, respond)
RollDice(dice, respond)
}
const RollDice = (dice, respond) => {
if(dice.size > 255) {
respond('That die is way too big... .-.')
return
} else if(dice.size < 2) {
respond('I cannot even fathom a die with that geometry ;-;')
return
}
if(dice.size > 255) {
respond('That die is way too big... .-.')
return
} else if(dice.size < 2) {
respond('I cannot even fathom a die with that geometry ;-;')
return
}
if(dice.count > 100) {
respond('I don\'t have that many dice O_O')
return
}
if(dice.count > 100) {
respond('I don\'t have that many dice O_O')
return
}
let rolls = [ ...crypto.getRandomValues(new Uint8Array(dice.count) ) ]
.map(n => Math.ceil((n / 256) * dice.size))
let result = 0
let operationSymbol = dice.operation
let response = ''
let rolls = [ ...crypto.getRandomValues(new Uint8Array(dice.count) ) ]
.map(n => Math.ceil((n / 256) * dice.size))
let result = 0
let operationSymbol = dice.operation
let response = ''
switch(dice.mode.toLowerCase()) {
case 'd':
result = rolls.reduce((a, v) => a + v, 0)
break
switch(dice.mode.toLowerCase()) {
case 'd':
result = rolls.reduce((a, v) => a + v, 0)
break
case 'h':
result = rolls.reduce((a, v) => v > a ? v : a, 0)
break
case 'h':
result = rolls.reduce((a, v) => v > a ? v : a, 0)
break
case 'l':
result = rolls.reduce((a, v) => v < a ? v : a, Infinity)
break
case 'l':
result = rolls.reduce((a, v) => v < a ? v : a, Infinity)
break
case 'f':
let pseudoMedian = Math.floor(dice.size / 2)
let resultDistance = -1
case 'f':
let pseudoMedian = Math.floor(dice.size / 2)
let resultDistance = -1
for(let roll of rolls) {
let distance = Math.abs(roll - pseudoMedian)
for(let roll of rolls) {
let distance = Math.abs(roll - pseudoMedian)
if(distance > resultDistance) {
result = roll
resultDistance = distance
}
}
break
}
if(distance > resultDistance) {
result = roll
resultDistance = distance
}
}
break
}
switch(dice.operation) {
case '+':
result += dice.modifier
break
switch(dice.operation) {
case '+':
result += dice.modifier
break
case '-':
result -= dice.modifier
break
case '-':
result -= dice.modifier
break
case 'x':
operationSymbol = '*'
case '*':
result = result * dice.modifier
break
case 'x':
operationSymbol = '*'
case '*':
result = result * dice.modifier
break
case '/':
result = result / dice.modifier
break
}
case '/':
result = result / dice.modifier
break
}
if(dice.descriptionConditions) {
for(let { range, content } of dice.descriptionConditions) {
if(!range || result >= range.lower && result <= range.upper)
response += `'${content}', `
}
}
if(dice.descriptionConditions) {
for(let { range, content } of dice.descriptionConditions) {
if(!range || result >= range.lower && result <= range.upper)
response += `'${content}', `
}
}
response += `\` ${result} \` \u27F5 [${rolls.join(', ')}] ${dice.count + dice.mode + dice.size}`
response += `\` ${result} \` \u27F5 [${rolls.join(', ')}] ${dice.count + dice.mode + dice.size}`
if(dice.operation) {
response += ' ' + operationSymbol + ' ' + dice.modifier
}
if(dice.operation) {
response += ' ' + operationSymbol + ' ' + dice.modifier
}
respond(response)
respond(response)
}
const SaveReply = (message, reply) => {
globalThis.replies.set(message.id, {
id: reply.id,
timestamp: Date.now()
})
globalThis.replies.set(message.id, {
id: reply.id,
timestamp: Date.now()
})
}
const MessageCycle = async message => {
OnMessage(message, async content => {
SaveReply(message, await message.reply(content) )
})
OnMessage(message, async content => {
SaveReply(message, await message.reply(content) )
})
}
const RehandleMessage = async (message, reply) => {
OnMessage(message, async content => {
SaveReply(message, await reply.edit(content) )
})
OnMessage(message, async content => {
SaveReply(message, await reply.edit(content) )
})
}
const PruneReplies = () => {
for(let [ id, entry ] of globalThis.replies.entries()) {
let age = Date.now() - entry.timestamp
for(let [ id, entry ] of globalThis.replies.entries()) {
let age = Date.now() - entry.timestamp
if(age > 1000 * 60 * 3) {
globalThis.replies.delete(id)
}
}
if(age > 1000 * 60 * 3) {
globalThis.replies.delete(id)
}
}
}
const InteractionRespond = (interaction, content) => {
let reply = { content, ephemeral: true }
let reply = { content, ephemeral: true }
if(interaction.replied || interaction.deferred) {
return interaction.followUp(reply)
} else {
return interaction.reply(reply)
}
if(interaction.replied || interaction.deferred) {
return interaction.followUp(reply)
} else {
return interaction.reply(reply)
}
}
const ErrorHandler = (interaction) => (error) =>
InteractionRespond(interaction, constants.errorMessage(error) )
.catch(reportingError => console.error('Could not display error message:\n ', reportingError) )
InteractionRespond(interaction, constants.errorMessage(error) )
.catch(reportingError => console.error('Could not display error message:\n ', reportingError) )
const Command = (data, callback) => {
globalThis.commands.set(data.name, {
data,
execute: callback
})
globalThis.commands.set(data.name, {
data,
execute: callback
})
}
const Subcommands = (data, subcommandCallbacks) =>
Command(data, interaction => {
return subcommandCallbacks[interaction.options.getSubcommand()](interaction)
})
Command(data, interaction => {
return subcommandCallbacks[interaction.options.getSubcommand()](interaction)
})
const OpenMacros = guildId =>
db.sublevel(guildId).sublevel('macros')
db.sublevel(guildId).sublevel('macros')
const ReloadMacros = async guildId => {
let commands = []
let macros = OpenMacros(guildId)
let cacheEntry = {}
let commands = []
let macros = OpenMacros(guildId)
let cacheEntry = {}
for await (let [ name, dice ] of macros.iterator() ) {
cacheEntry[name] = dice
for await (let [ name, dice ] of macros.iterator() ) {
cacheEntry[name] = dice
commands.push({
name,
description: Elipsify("Roll " + dice.replaceAll('\n', ';'), 100),
options: [
{
name: "options",
description: "Dice, modifiers, or descriptions to apply over the macro",
type: 3
}
]
})
}
commands.push({
name,
description: Elipsify("Roll " + dice.replaceAll('\n', ';'), 100),
options: [
{
name: "options",
description: "Dice, modifiers, or descriptions to apply over the macro",
type: 3
}
]
})
}
globalThis.macroCache.set(guildId, cacheEntry)
globalThis.macroCache.set(guildId, cacheEntry)
await rest.put(
Routes.applicationGuildCommands(process.env.DISCORD_ID, guildId),
{ body: globalThis.commands }
)
.catch(err => console.error('Failed to reload macros:', err) )
await rest.put(
Routes.applicationGuildCommands(process.env.DISCORD_ID, guildId),
{ body: globalThis.commands }
)
.catch(err => console.error('Failed to reload macros:', err) )
}
const Elipsify = (string, maxLength) =>
string.length > maxLength ? string.slice(0, maxLength - 3) + '...' : string
string.length > maxLength ? string.slice(0, maxLength - 3) + '...' : string
const PruneDB = async () => {
let validIds = []
let validIds = []
for await(let key of db.keys()) {
let [ guildId ] = key.split('!').slice(1)
for await(let key of db.keys()) {
let [ guildId ] = key.split('!').slice(1)
if(validIds.includes(guildId))
continue
if(validIds.includes(guildId))
continue
if(client.guilds.cache.has(guildId)) {
validIds.push(guildId)
} else {
await db.del(key)
}
}
if(client.guilds.cache.has(guildId)) {
validIds.push(guildId)
} else {
await db.del(key)
}
}
return validIds
return validIds
}
const Responses = (interaction, ephemeral) => async content =>
interaction.reply({ content, ephemeral })
interaction.reply({ content, ephemeral })
const Subscribe = (event, callback) => {
client.on(event, (...args) => {
return callback(...args)
.catch(err => console.error(err))
})
client.on(event, (...args) => {
return callback(...args)
.catch(err => console.error(err))
})
}
const HandleCommand = async interaction => {
if(globalThis.commands.has(interaction.commandName) ) {
globalThis.commands.get(interaction.commandName).execute(interaction)
.catch(ErrorHandler(interaction))
return
}
if(globalThis.commands.has(interaction.commandName) ) {
globalThis.commands.get(interaction.commandName).execute(interaction)
.catch(ErrorHandler(interaction))
return
}
await interaction.deferReply()
let roll = globalThis.macroCache.get(interaction.guild.id)[interaction.commandName]
await interaction.deferReply()
let roll = globalThis.macroCache.get(interaction.guild.id)[interaction.commandName]
if(roll) {
let dice = ParseRoll(roll)
let options = interaction.options.get('options')
if(options) {
let optionDice = ParseOptionRoll(options.value)
if(roll) {
let dice = ParseRoll(roll)
let options = interaction.options.get('options')
if(options) {
let optionDice = ParseOptionRoll(options.value)
for(let [ key, value ] of Object.entries(optionDice)) {
if(value)
dice[key] = Array.isArray(value) ? value.concat(dice[key]) : value
}
}
for(let [ key, value ] of Object.entries(optionDice)) {
if(value)
dice[key] = Array.isArray(value) ? value.concat(dice[key]) : value
}
}
RollDice(dice, content => interaction.followUp(content) )
}
RollDice(dice, content => interaction.followUp(content) )
}
}
const FindOption = (options, name) =>
options.find(option => option.name == name)
options.find(option => option.name == name)
const HandleAutocomplete = async interaction => {
if(globalThis.commands.has(interaction.commandName) ) {
let { data } = globalThis.commands.get(interaction.commandName)
let subcommand = interaction.options.getSubcommand()
let focusedOption = interaction.options.getFocused(true)
if(globalThis.commands.has(interaction.commandName) ) {
let { data } = globalThis.commands.get(interaction.commandName)
let subcommand = interaction.options.getSubcommand()
let focusedOption = interaction.options.getFocused(true)
if(subcommand !== undefined) {
data = FindOption(data.options, subcommand)
}
if(subcommand !== undefined) {
data = FindOption(data.options, subcommand)
}
let option = FindOption(data.options, focusedOption.name)
let option = FindOption(data.options, focusedOption.name)
if(!option) {
console.error('Could not find option: ' + focusedOption)
return
}
if(!option) {
console.error('Could not find option: ' + focusedOption)
return
}
let filtered = option
.getAutocomplete(interaction)
.filter(choice => choice.startsWith(focusedOption.value) )
.map(choice => ({ name: choice, value: choice }) )
let filtered = option
.getAutocomplete(interaction)
.filter(choice => choice.startsWith(focusedOption.value) )
.map(choice => ({ name: choice, value: choice }) )
await interaction.respond(filtered)
}
await interaction.respond(filtered)
}
}
const Interaction = interaction => {
if(interaction.isChatInputCommand()) {
return HandleCommand(interaction)
} else if(interaction.isAutocomplete()) {
return HandleAutocomplete(interaction)
.catch(console.error)
}
if(interaction.isChatInputCommand()) {
return HandleCommand(interaction)
} else if(interaction.isAutocomplete()) {
return HandleAutocomplete(interaction)
.catch(console.error)
}
}
const MessageUpdate = async (oldMessage, newMessage) => {
if(globalThis.replies.has(newMessage.id) ) {
let { id } = globalThis.replies.get(newMessage.id)
if(globalThis.replies.has(newMessage.id) ) {
let { id } = globalThis.replies.get(newMessage.id)
newMessage.channel.messages.fetch(id)
.then(reply => RehandleMessage(newMessage, reply) )
.catch(err => MessageCycle(newMessage) )
} else {
MessageCycle(newMessage)
}
newMessage.channel.messages.fetch(id)
.then(reply => RehandleMessage(newMessage, reply) )
.catch(err => MessageCycle(newMessage) )
} else {
MessageCycle(newMessage)
}
}
const About = async (interaction) => {
let embed = {
title: 'dicedicedice',
thumbnail: {
url: constants.iconUrl
},
description: constants.aboutMessage(client.guilds.cache.size)
}
let embed = {
title: 'dicedicedice',
thumbnail: {
url: constants.iconUrl
},
description: constants.aboutMessage(client.guilds.cache.size)
}
await interaction.reply({
embeds: [ embed ],
ephemeral: true
})
await interaction.reply({
embeds: [ embed ],
ephemeral: true
})
}
const AddMacro = async (interaction) => {
let respond = Responses(interaction, true)
let name = interaction.options.get('name').value.toLowerCase()
let respond = Responses(interaction, true)
let name = interaction.options.get('name').value.toLowerCase()
if(!constants.macroNameRegex.test(name))
return respond("Please provide a macro name that consists of only alphanumeric characters.")
if(!constants.macroNameRegex.test(name))
return respond("Please provide a macro name that consists of only alphanumeric characters.")
if(commands.has(name))
return respond("Uhh,, I think that macro name is already taken by my own commands, sorry.")
if(commands.has(name))
return respond("Uhh,, I think that macro name is already taken by my own commands, sorry.")
let macros = globalThis.macroCache.get(interaction.guild.id)
let macros = globalThis.macroCache.get(interaction.guild.id)
if(macros && !macros[name] && Object.keys(macros).length >= 100)
return respond("I can't keep track of that many macros,, ;-;")
if(macros && !macros[name] && Object.keys(macros).length >= 100)
return respond("I can't keep track of that many macros,, ;-;")
let dice = interaction.options.get('dice').value
let dice = interaction.options.get('dice').value
if(!constants.rollRegex.test(dice) )
return respond("Please provide a valid roll expression.")
if(!constants.rollRegex.test(dice) )
return respond("Please provide a valid roll expression.")
await interaction.deferReply({ ephemeral: true })
await interaction.deferReply({ ephemeral: true })
await Promise.all([
OpenMacros(interaction.guild.id).put(name, dice),
ReloadMacros(interaction.guild.id)
])
interaction.followUp(`Macro added! Try \`/${name}\`! You might need to switch to a different server and back or reopen Discord in order for it to recognize the new command.`)
await Promise.all([
OpenMacros(interaction.guild.id).put(name, dice),
ReloadMacros(interaction.guild.id)
])
interaction.followUp(`Macro added! Try \`/${name}\`! You might need to switch to a different server and back or reopen Discord in order for it to recognize the new command.`)
}
const RemoveMacro = async (interaction) => {
let name = interaction.options.get('name').value.toLowerCase()
let macros = macroCache.get(interaction.guild.id)
let respond = openResponses(interaction, true)
if(!macros)
return respond('There aren\'t even any macros in this guild!')
let name = interaction.options.get('name').value.toLowerCase()
let macros = macroCache.get(interaction.guild.id)
let respond = openResponses(interaction, true)
if(!macros)
return respond('There aren\'t even any macros in this guild!')
let dice = macros && macroCache.get(interaction.guild.id)[name]
let dice = macros && macroCache.get(interaction.guild.id)[name]
if(!dice)
return respond("There isn't a macro with that name .-.")
if(!dice)
return respond("There isn't a macro with that name .-.")
await interaction.deferReply({ ephemeral: true })
await Promise.all([
openMacros(interaction.guild.id).del(name),
reloadMacros(interaction.guild.id)
])
await interaction.deferReply({ ephemeral: true })
await Promise.all([
openMacros(interaction.guild.id).del(name),
reloadMacros(interaction.guild.id)
])
await interaction.followUp(`Removed \`${name}\`, its dice expression was: \`\`\`${dice}\`\`\``)
await interaction.followUp(`Removed \`${name}\`, its dice expression was: \`\`\`${dice}\`\`\``)
}
const Start = async () => {
Command(
constants.commands.about,
About
)
Command(
constants.commands.about,
About
)
Subcommands(constants.commands.macro, {
add: AddMacro,
remove: RemoveMacro
})
Subcommands(constants.commands.macro, {
add: AddMacro,
remove: RemoveMacro
})
Subscribe('interactionCreate', Interaction)
Subscribe('messageCreate', MessageCycle)
Subscribe('messageUpdate', MessageUpdate)
Subscribe('ready', async () => {
console.log("Logged in!")
Subscribe('interactionCreate', Interaction)
Subscribe('messageCreate', MessageCycle)
Subscribe('messageUpdate', MessageUpdate)
Subscribe('ready', async () => {
console.log("Logged in!")
let guildIds = await PruneDB()
let guildIds = await PruneDB()
for(let guildId of guildIds)
await ReloadMacros(guildId)
for(let guildId of guildIds)
await ReloadMacros(guildId)
console.log("Ready")
})
await rest.put(
Routes.applicationCommands(process.env.DISCORD_ID),
{
body: [ ...globalThis.commands.values() ]
.map(command => command.data )
}
)
.catch(err => console.error('Command registration failed: ', err) )
console.log("Ready")
})
await rest.put(
Routes.applicationCommands(process.env.DISCORD_ID),
{
body: [ ...globalThis.commands.values() ]
.map(command => command.data )
}
)
.catch(err => console.error('Command registration failed: ', err) )
console.log(process.env)
console.log(process.env)
await client.login(process.env.DISCORD_TOKEN)
.catch(err => console.error('Login failed: ', err) )
await client.login(process.env.DISCORD_TOKEN)
.catch(err => console.error('Login failed: ', err) )
setInterval(PruneReplies, 1000 * 60)
setInterval(PruneReplies, 1000 * 60)
}
Start()