Skip to content

Commit

Permalink
Merge pull request #71 from jakethedev/feature/discordAsyncOps
Browse files Browse the repository at this point in the history
Bot deals in promises and strings, plus basic role mgmt and huge rolling improvements: Closes #1, #9, #70 and #72. Progress on #3 and #10
  • Loading branch information
jakethedev authored Aug 12, 2018
2 parents ec86c3b + c04ca97 commit 8007000
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 136 deletions.
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,31 @@ TavernBot is a generator bot for Game Masters of D&D. This project is a Discord

This is currently an 0.x release - I will document features and such here as they're finished up and as we close in on 1.0, but for now, I'm pouring molten free time into this project to try and forge something awesome. Check the code for now if you want to know more.

### Notable issues:
## Development

Fork this project, then follow these steps to get up and running! Make sure you have node 8+, with an appropriate npm, and a Discord bot token. Go to [this link](https://discordapp.com/developers/applications/me) to set up a bot account, add the token to config.json in the project root, then invite the bot to a server for testing. And don't forget npm install.

'npm run devbot' will set you up with a hot-reloading bot instance, and 'npm test' should run quietly with no issues if everything's set up correctly. 'npm run verbosetest' will show you the npm test output, which should look like Discord-formatted responses.

### Development triage:

#### ImportError: no module compiler.ast:

If you see the above issue during 'npm install', just run 'sudo apt install python-dev'. I'm as upset as you are that we need python for npm, but, c'est la vie.

Below is a sort of notepad, and generally contains nothing useful. If you have ideas or features that you think this bot should support, [let me know on Github](https://github.com/jakethedev/tavernbot/issues) and we'll get it prioritized :D

---

Autoreload complete! Putting this here to commit the fix :)
Below is a sort of notepad, and generally contains nothing useful. If you have ideas or features that you think this bot should support, [let me know on Github](https://github.com/jakethedev/tavernbot/issues) and we'll get it prioritized :D

Source for dungeon world content: https://www.npmjs.com/package/dungeonworld-data

Throttle max rolls to 100. If more, mutiply to match and throw another 100 on it.
Cap dice types at 5 instances of 'd'? "Error: Too complex"?
I want to roll this '1d20 + 5, 2d6 + 2d8 + 3'

Voice API https://discord.js.org/#/docs/main/stable/topics/voice

Character stats implementations
- PHB spread w/stat priority per class
- 4d6k3
- Colville method
- 6 + 2d6
Character stats implementation needs PHB spread w/stat priority per class

Keep last 128 macros? 256?

Find a good base of Character Sheet JSON

Possibly useful for inspiration https://github.com/opendnd



87 changes: 52 additions & 35 deletions bot.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
//All the custom stuff, separated by concerns
const dungeonary = require('./dungeonary')
const discordlib = require('./discordlib')
const gravemind = require('./gravemind')
//lolrandom
// For the occasional handy shortcut
require('./randomUtil')
//TODO: Map all the libs at boot, so there isn't a loop to find each op. Way better.
//var knownOps = []; for ( func in req('./lib') ){ knownOps[func] = lib[func] }

const Discord = require("discord.js")
const client = new Discord.Client()
const {
token,
botkey,
botRoleName,
activeChannels,
gameStatus
} = require("./config.json")
// Dynamically load all operations we care about into a single commander object
loadAllOperations = function(libNames){
let allOps = {}, meta = {}
for (lib of libNames) {
meta[lib] = []
let libOps = require(lib);
for (op in libOps) {
allOps[op] = libOps[op]
meta[lib].push(op)
}
}
return [ allOps, meta ]
}
const [ commander, metadata ] = loadAllOperations(
[ './discordlib', './dungeonary', './gravemind' ]
)

// Core bot setup
const { token, botkey, activeChannels, gameStatus } = require("./config.json")
const discord = require("discord.js")
const client = new discord.Client()

// In case something happens, we'll want to see logs
client.on("error", (e) => console.error(e))
Expand All @@ -24,42 +30,53 @@ client.on("error", (e) => console.error(e))
client.on('ready', () => {
if (process.env.NODE_ENV) {
console.log(`${process.env.NODE_ENV} mode activated!`)
} else {
console.log(`NODE_ENV not set, running in dev mode`)
}
console.log(`Tavernbot v${process.env.npm_package_version} has logged in as ${client.user.tag}!`)
client.user.setPresence({
"status": "online",
"game": { "name": gameStatus }
})
//TODO Set a botRoleName value on lookup from the string in config
})

// Command central
client.on('message', msg => {
// Let's hook it up for a default set of channels and DMs
if (activeChannels.includes(msg.channel.name.toLowerCase()) || msg.channel.recipient) {
//Make sure we care, and that we're not making ourselves care
// Contain the bot, and ensure we actually want to act on the command
let channelName = msg.channel.name ? msg.channel.name.toLowerCase() : "NOT_A_CHANNEL_NAME"
if (activeChannels.includes(channelName) || msg.channel.recipient) {
if (!msg.content.trim().startsWith(botkey) || msg.author.bot) return
//Remove botkey and break it up into clean not-mixed-cased parts.
// Normalize input
let parts = msg.content.trim().toLowerCase().substring(1).split(/\s+/)
let cmd = parts[0]
let input = parts[1] ? parts.slice(1).join(' ') : '' //Some cmds have no input, this lets us use if(input)
let execTime = new Date(Date.now()).toLocaleString() + ': ';
//TODO: Genericise this. Load all libs into a single 'operations' object with a 'source' prop so we can stop this ifelse nonsense
//From here, we check each lib until we find a match for execution, or we let the user know it's a no-go
if (cmd in dungeonary) {
console.log(execTime + 'running dungeonary.' + cmd + '(' + input + ') for ' + msg.author.username)
msg.reply(dungeonary[cmd](input))
} else if (cmd in discordlib) {
console.log(execTime + 'running discordlib.' + cmd + '(' + input + ') for ' + msg.author.username)
msg.reply(discordlib[cmd](input, msg, client)) //TODO Pass in botRoleReal?
} else if (cmd in gravemind) {
console.log(execTime + 'running gravemind.' + cmd + '(' + input + ') for ' + msg.author.username)
msg.reply(gravemind[cmd](input, msg, client))
let execTime = new Date(Date.now()).toLocaleString();
// If we have the requested op, send it - otherwise, log it quietly
if (cmd in commander) {
console.log(execTime + ': running ' + cmd + '(' + input + ') for ' + msg.author.username)
// Works for a string or a promise return. Sick. https://stackoverflow.com/a/27760489
Promise.resolve( commander[cmd](input, msg, client) )
.then(function(result) {
msg.reply(result)
})
.catch(function(err) {
msg.reply(`your command met with a terrible fate and I nearly died. Have an admin check the logs plz`)
console.log(`${execTime}: ERR: ${err}`)
})
} else if (cmd == 'help') {
let fullHelp = `\n**- Available Commands -**`
for (library in metadata){ // Already overloaded command, oops
fullHelp += `\n**${library} commands**: \n`
metadata[library].forEach((opName) => fullHelp += `${opName} `)
}
fullHelp += `\n\nFor any command, run '!command help' for detailed use info`
fullHelp += `\n\n*If you notice something weird or broken, let me know, run* !feedback *to learn how*`
msg.channel.send(fullHelp)
} else {
console.log(execTime + ' WARN: failed to run ' + cmd + '(' + input + ') for ' + msg.author.username)
console.log(`${execTime}: NOTICE: can't find ${cmd}(${input}) for ${msg.author.username}`)
}
}
});

// Turning the key and revving the bot engine
client.login(token)
client.login(token)
82 changes: 29 additions & 53 deletions discordlib/discordroles.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
// TO PROGRAMMERS: commands should have the signature:
// f(input:String, message:discord.js#message, client:discord.js#client)

getManagedRoles = function(client) {

}

exports.newrole = function(input, message, client) {
if (input.toLowerCase() == 'help') return `help for newrole`
if (!message.member) return `you don't even live here, stop messing with things (err: not a server member)`
// if (message.member.permissions.includes 'icandothings'){
// discord.addNewRole(name: input)
// discord.addNewRole(name: input).copyFrom(everyone).randomColor()
// return `mission accomplished - your role called "${input}" is ready to rock`
// }
return `put that back, you're not allowed to touch it. (err: ain't got permission pal)`
return `put that back, you're not allowed to touch it. (err: you don't have permission)`
}

//Given a rolename as input, add it to the requestor if it doesn't result in new privileges
exports.addrole = function(input, message, client) {
if (input.toLowerCase() == 'help') return `help for addrole`
//TODO Ensure requestor is admin or the user, then if the role is a managed role, add role to user
// let validRoles = message.guild.roles.filter((role) => role.id < )
// if (role.position < bot.role.position) {
// member.addRole().catch(console.error)
// console.log('Requesting ' + role + ' and it\'s lower ranked than the bot role, so we good')
// }
return `a role has no name`
exports.giverole = exports.addrole = function(input, message, client) {
if (input.toLowerCase() == 'help') return `Usage: addrole/giverole 'the role name' will try to add the role to your user. Optionally, you can tag one person (after the role name) to attempt to give them the role`
if (!input.trim()) return `you can't just add nothing as a role, that's not how any of this works!`
let expectedRoleName = input.split('<')[0].toLowerCase().trim() //Expecting only one role, before any mentions
// Allows us to add a role to someone, pinging them required
let requestorName = message.member.user.username
let optionalMention = message.mentions.members.first()
let targetMember = optionalMention ? optionalMention : message.member
let targetName = targetMember.user.username
let roleToAdd = message.guild.roles.find((role) => expectedRoleName == role.name.toLowerCase())
if (!roleToAdd){
return `that role does not exist, checkest thy typing or speaketh with thy lord moderators`
}
console.log(`Role '${roleToAdd.name}' requested by ${requestorName} for ${targetName}...`)
return targetMember.addRole(roleToAdd).then(result => {
// S'gooood. This is idempotent, adding an existing role id a-ok
return `${targetName} now has (or already had) the role ${roleToAdd.name}!`
}).catch(err => {
// Almost certainly a permission error
return `I can't add ${targetName} to ${roleToAdd.name}, probably not allowed to. Contact an admin if this is unexpected`
});
}

//List the requestor's roles.
//TODO Use this, list a target's roles; let targetToSummon = message.mentions.users.first()
exports.roles = function(input, message, client) {
if (input.toLowerCase() == 'help') return `help for roles`
if (input.toLowerCase() == 'help') return `roles will list the roles you have, if any`
const userRolesRaw = message.member.roles
let roleResults = []
// Stash the results, strip any @ symbols to avoid pinging @everyone every single time
Expand All @@ -50,14 +58,11 @@ exports.unrole = function(input, message, client) {
if (userRoles.size == 0)
return `it seems that you have no roles, and that's really funny`
let roleResult = userRoles.find(role => role.name.toLowerCase() === input.toLowerCase())
if (!roleResult)
return `we cannot remove what does not exist (role "${input}" not found on your account)`

//TODO VERIFY THIS ROLE IS LOWER THAN BOT ROLE OR IT DIES. async promise this shit?
console.log(`Verify the role is below bot role or (chuckles) I'm in danger`)

message.member.removeRole(roleResult)
return `as you wish - you have cast thyself from the family of "${input}"!`
return message.member.removeRole(roleResult).then(result => {
return `you are uninvited from ${input}`
}).catch(error => {
return `I'm afraid I can't do that, Dave. Either you don't have that role or a mod needs to handle it`
})
}

//Number of people in a given role
Expand Down Expand Up @@ -91,32 +96,3 @@ exports.rolemembers = function(input = '', message, client) {
return 'there was a temporal anomaly, I believe I need my oil changed'
}
}

samplecode = function() {
// get role by name
let myRole = message.guild.roles.find("name", "Moderators")

// assuming role.id is an actual ID of a valid role:
if (message.member.roles.has(role.id)) {
console.log(`Yay, the author of the message has the role!`)
} else {
console.log(`Nope, noppers, nadda.`)
}

//Other basics


// Let's pretend you mentioned the user you want to add a role to (!addrole @user Role Name):
let member = message.mentions.members.first()

// or the person who made the command: let member = message.member;

// Add the role!
member.addRole(role).catch(console.error)

message.guild.roles.find("name", "Moderators")
// Remove a role!
member.removeRole(role).catch(console.error)

//Docs https://anidiotsguide.gitbooks.io/discord-js-bot-guide/information/understanding-roles.html
}
75 changes: 49 additions & 26 deletions dungeonary/diceroller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//Requires that randomUtil has loaded already.
require('../randomUtil')
const mathOps = "+-"

//For those tricky decisions
exports.coin = function(input = 1) {
Expand All @@ -18,43 +19,65 @@ exports.coin = function(input = 1) {
}
}

// The crazy custom roll parser.
exports.dice = exports.d = function(rollInput = '') {
//Nice defaults
function diceRegexMatcher(rollInput) {
// Turns '1d20+5+ 1d6 - 3d4 for fighting' into
// ['1d20', '+', '5', '+', '1d6', '-', '3d4']
let diceRegex = /(\d*d?\d+)|[-\+]/g
let matches = []
while (match = diceRegex.exec(rollInput)) {
if (mathOps.includes(match[0])){
// + and - are external to capture groups but need to be matched for math stuff
matches.push(match[0])
} else {
matches.push(match[1])
}
}
return matches
}

// The crazy custom roll parser. It's a good parser, and it deserves more composition, but mehhhh
exports.dice = exports.d = exports.roll = function(rollInput = '') {
//Handy simple default
if (!rollInput) return "a d20 skitters across the table, you rolled a " + randIntMinOne(20)
rollInput = rollInput.toLowerCase()
if (rollInput == 'help') return `!dice XdY rolls a dY X times, you can sum varied dice, add constants, and comma separate rolls to have them all rolled at once!`
if (rollInput.includes('-')) return `sorry, reverse math is not supported yet`
if (rollInput == 'help') return `!dice XdY rolls a dY X times, you can segmentTotal varied dice, add constants, and comma separate rolls to have them all rolled at once!`

rollInput = rollInput.replace(/\s/g, '') //Cut space, much easier to parse
let response = 'the dice have fallen...\n';
// Split up the input, regex-capture the right pieces, math them, and report the result
let response = `here you go:\n`
for (rollSegment of rollInput.split(',')) {
//Smash response together with each result
let sum = 0
for (rollComponent of rollSegment.split('+')) {
if (rollComponent.split('d').length === 2) { //XdY or dY format
let [numRolls, diceSize] = rollComponent.split('d')
let diceMatches = diceRegexMatcher(rollSegment)
let segmentTotal = 0, subtractNextValue = false
for (rollValue of diceMatches) {
let tempSum = 0
// Can be one of '+', '-', 'XdY', or 'X'.
// If subtract, just note it for the next value.
if (rollValue == '-') {
subtractNextValue = true
continue
}
// The actual rolling of dice
if (rollValue.includes("d")) { //XdY or dY format
let [numRolls, diceSize] = rollValue.split('d')
numRolls = numRolls ? parseInt(numRolls) : 1
diceSize = parseInt(diceSize)
while (numRolls-- > 0) // Subtraction after comparison, trick from C
sum += randIntMinOne(diceSize)
} else if (!isNaN(rollComponent)) { // X format, crude yet effective
sum += parseInt(rollComponent)
} else {
return `there was a problem parsing '${rollComponent}', make sure that it's in XdY or X format`
while (numRolls-- > 0) // Subtraction happens after comparison
tempSum += randIntMinOne(diceSize)
} else if (rollValue.match(/^\d+$/)) { // A constant num
tempSum += parseInt(rollValue)
}
// Complete subtract contract
if (subtractNextValue){
tempSum *= -1
subtractNextValue = false
}
segmentTotal += tempSum
}
response += `${rollSegment}: **${sum}**\n`
response += `${diceMatches.join(' ')}: **${segmentTotal}**\n`
}
/**
* Remaining potential here:
* roll(2d20 - 5) Multiple sub const
* roll(2d20 - d6) Multiple sub dice
* roll(2d20 + 1d12 + 1d6 + 7 - 1d4) Long series
/* Remaining potential:
* roll(2d20, best) Multiple take best
* roll(2d20, worst) Multiple take worst
*/
return response; // TODO: Parse input, roll dice, return reasonable output chunk
return response
}

// Stat roller function. Uses an approved method and reports results cleanly
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tavernbot",
"version": "0.2.0",
"releasenotes": "Recently updated: new origin of code at github/jakethedev - and I committed a git travesty, so idk what other changes are here",
"version": "0.2.1",
"releasenotes": "!giverole/!addrole - give yourself or someone you tag a new role, !help - I'LL SHOW YOU WHAT I GOT, !anycomand help - for detailed usage, !d/!dice/!roll - REVERSE MATH NOW SUPPORTED and also commentary about what you're rolling",
"description": "A Dungeons and Dragons GM bot for discord, written in Node. This bot supports common tabletop RPG functionality, common Discord operations, and more",
"main": "bot.js",
"scripts": {
Expand Down
Loading

0 comments on commit 8007000

Please sign in to comment.