Many improvements and additions.

Several new achievements.
Separate files for buy route, webapi, and shared utils.
Add Creator buyable item.
Access control now object-based.
Toying with Trivia and a lottery system.
!cleanusers, !setpw, !rach, !myupgrades, !squad, !gimme, !prestige,
!quack, !whois, !ngift, !message, !!kill,
Add simple test user system.
Add several oneShot commands.
This commit is contained in:
Sage Vaillancourt 2022-03-14 08:37:38 -04:00
parent f054154717
commit e971f7e7c2
19 changed files with 1590 additions and 797 deletions

18
package-lock.json generated
View File

@ -673,6 +673,16 @@
"promise.allsettled": "^1.0.2", "promise.allsettled": "^1.0.2",
"raw-body": "^2.3.3", "raw-body": "^2.3.3",
"tsscmp": "^1.0.6" "tsscmp": "^1.0.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
} }
}, },
"@slack/logger": { "@slack/logger": {
@ -1082,11 +1092,11 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"axios": { "axios": {
"version": "0.21.4", "version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"requires": { "requires": {
"follow-redirects": "^1.14.0" "follow-redirects": "^1.14.8"
} }
}, },
"babel-jest": { "babel-jest": {

View File

@ -17,6 +17,7 @@
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@slack/bolt": "^3.9.0", "@slack/bolt": "^3.9.0",
"axios": "^0.26.0",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"fs": "0.0.1-security", "fs": "0.0.1-security",

View File

@ -1,5 +1,5 @@
let base64 = require('base-64') const base64 = require('base-64')
let config = require('../config') const config = require('../config')
const url = '' const url = ''

View File

@ -38,6 +38,42 @@ module.exports = {
name: 'You light my fire, baby', name: 'You light my fire, baby',
description: 'And you pay attention to descriptions!', description: 'And you pay attention to descriptions!',
emoji: 'fire' emoji: 'fire'
},
ratGod: {
name: 'Own 100 Mice',
description: 'I\'m beginning to feel like a rat god, rat god.',
emoji: 'mouse2'
},
weAllNeedHelp: {
name: `View the '!coin' help`,
description: 'We all need a little help sometimes',
emoji: 'grey_question'
},
showReverence: { // Not implemented
name: 'Show your reverence in the chat',
description: 'What a good little worshipper.',
emoji: 'blush'
},
walmartGiftCard: {
name: 'Walmart Gift Card',
description: 'May or may not be expired',
emoji: 'credit_card'
},
hvackerAfterDark: {
name: 'Hvacker after dark',
description: 'You might be taking this a little far.',
emoji: 'night_with_stars'
},
certifiedCoolGuy: {
name: 'Certified Cool Guy',
description: 'You absolutely know how to party.',
emoji: 'sunglasses'
},
youDisgustMe: {
name: 'You disgust me',
description: 'Like, wow.',
emoji: 'nauseated_face'
} }
} }

132
src/games/hvacoins/buy.js Normal file
View File

@ -0,0 +1,132 @@
const buyableItems = require('./buyableItems');
const { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils');
const slack = require('../../slack')
const calculateCost = ({ itemName, user, quantity = 1 }) => {
let currentlyOwned = user.items[itemName] || 0
let realCost = 0
for (let i = 0; i < quantity; i++) {
realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0))
currentlyOwned += 1
}
return realCost
}
const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost)
const itemCps = Math.round(singleItemCps(user, itemName))
return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_`
}
const canView = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101
const buyableText = (highestCoins, user) => Object.entries(buyableItems)
.filter(canView(highestCoins))
.map(getItemHeader(user))
.join('\n\n') +
'\n\n:grey_question::grey_question::grey_question:' +
'\n\nJust type \'!buy item_name\' to purchase'
const buildBlock = ({ user, itemName, cost, cps }) => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - 𝕳${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_`
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Buy 1',
emoji: true
},
value: 'buy_' + itemName,
action_id: 'buy_' + itemName
},
})
const buyText2 = (highestCoins, user) => {
return ({
text: buyableText(highestCoins, user),
blocks: Object.entries(buyableItems)
.filter(canView(highestCoins))
.map(([itemName, item]) => {
const cost = calculateCost({ itemName, user, quantity: 1 })
const cps = Math.round(singleItemCps(user, itemName))
return ({ user, itemName, cost, cps })
}).map(buildBlock)
})
}
const buyRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
const buying = words[1]
setHighestCoins(event.user)
if (!buying) {
const highestCoins = user.highestEver || user.coins || 1
if (buyableItems.quade.baseCost < highestCoins * 100) {
addAchievement(user, 'seeTheQuade', say)
}
await say(buyText2(highestCoins, user))
return
}
const buyable = buyableItems[buying]
if (!buyable) {
await say('That item does not exist!')
return
}
let quantity = 1
const currentCoins = getCoins(event.user)
if (words[2] === 'max') {
while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) {
quantity++
}
} else {
quantity = parseInt(words[2] || '1')
}
if (!quantity || quantity < 1) {
await say('Quantity must be a positive integer')
return
}
const realCost = calculateCost({ itemName: buying, user, quantity })
if (currentCoins < realCost) {
await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`)
return
}
user.coins -= realCost
user.items[buying] = user.items[buying] || 0
user.items[buying] += quantity
console.log(buying, user.items.mouse)
if (buying === 'mouse' && user.items.mouse >= 100) {
addAchievement(user, 'ratGod', say)
}
if (quantity === 1) {
await say(`You bought one :${buyable.emoji}:`)
} else {
await say(`You bought ${quantity} :${buyable.emoji}:`)
}
saveGame()
}
const buyButton = async ({ body, ack, say, payload }) => {
await ack()
const buying = payload.action_id.substring(4)
console.log(`buyButton ${buying} clicked`)
const event = {
user: body.user.id,
}
const user = getUser(event.user)
const words = ['', buying, '1']
await buyRoute({ event, say, words })
const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({
channel: body.channel.id,
ts: body.message.ts,
...buyText2(highestCoins, user)
})
}
Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton))
module.exports = buyRoute

View File

@ -64,5 +64,11 @@ module.exports = {
earning: 65_000_000, earning: 65_000_000,
emoji: 'hvacker_angery', emoji: 'hvacker_angery',
description: 'Harness the power of the mad god himself.' description: 'Harness the power of the mad god himself.'
} },
creator: {
baseCost: 170_000_000_000_000,
earning: 430_000_000,
emoji: 'question',
description: 'The elusive creator of Hvacker takes a favorable look at your CPS.'
},
} }

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,125 @@
const { getUser, getCoins, commas, saveGame } = require('./utils');
const quackStore = require('./quackstore');
const possiblePrestige = coins => {
let p = 0
while (tpcRec(p + 1) <= coins) {
p += 1
}
return p
}
const totalCostForPrestige = prestigeLevel => {
let cost = 0
while (prestigeLevel) {
cost += 1_000_000_000_000 * Math.pow(prestigeLevel, 3)
prestigeLevel -= 1
}
return cost
}
const tpcRecMemo = []
const tpcRec = prestigeLevel => {
if (prestigeLevel === 0) {
return 0
}
return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + tpcRec(prestigeLevel - 1))
}
// TODO
const prestigeRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
getCoins(event.user)
const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige ??= 0
if (words[1] === 'me') {
await say(
'This will permanently remove all of your items, upgrades, and coins!\n\n' +
'Say \'!!prestige me\' to confirm.'
)
} else {
await say(
`Current Prestige: ${commas(current)}\n\n` +
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
`HVAC until next quack: ${commas(Math.round(tpcRec(possible + 1) - user.coinsAllTime))}\n\n` +
'Say \'!prestige me\' to start the prestige process.'
)
}
}//, true, adminOnly)
// TODO
const prestigeConfirmRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
getCoins(event.user)
const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige
if (possible <= current) {
await say('You don\'t have enough HVAC to prestige right now!')
return
}
if (event?.text !== '!!prestige me') {
await say('Say exactly \'!!prestige me\' to confirm')
return
}
console.log('possible', possible)
console.log('user.prestige', user.prestige)
user.quacks ??= 0
user.quacks += (possible - user.prestige)
console.log('user.quacks', user.quacks)
user.prestige = possible
user.coins = 0
user.items = {}
user.upgrades = {}
saveGame()
await say('You prestiged! Check out !quackstore to see what you can buy!')
}
const quackStoreListing = ([name, upgrade]) =>
`:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
const allUserQuackUpgrades = user =>
Object.entries(user.quackUpgrades || {})
.map(([type, upgrades]) => upgrades)
const hasPreReqs = user => ([name, upgrade]) => {
if (!upgrade.preReqs) {
return true
}
const allUserUpgrades = allUserQuackUpgrades(user)
console.log(allUserUpgrades)
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
}
const quackStoreText = user =>
Object.entries(quackStore)
.filter(hasPreReqs(user))
.map(quackStoreListing)
.join('\n\n')
const quackStoreRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
user.quackUpgrades ??= {}
const quacks = user.quacks ??= 0
if (!words[1]) {
await say(quackStoreText(user))
return
}
const quackItem = quackStore[words[1]]
if (!quackItem) {
await say(`'${words[1]}' is not available in the quack store!`)
return
}
if (quackItem.cost > quacks) {
await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
return
}
user.quackUpgrades[quackItem.type] ??= []
user.quackUpgrades[quackItem.type].push(words[1])
saveGame()
}
module.exports = {
quackStoreRoute,
prestigeRoute,
prestigeConfirmRoute
}

View File

@ -0,0 +1,20 @@
const quackStore = {
ascent: {
name: 'Ascent',
type: 'cps',
emoji: 'rocket',
description: 'Welcome to level 2. Boosts all CPS by 20%',
effect: cps => cps * 1.2,
cost: 1
},
nuclearFuel: {
name: 'Nuclear Fuel',
type: 'cps',
emoji: 'atom_symbol',
description: 'The future is now. Boosts all CPS by 20%.',
preReqs: ['ascent'],
effect: cps => cps * 1.2,
cost: 5
},
}
module.exports = quackStore

View File

@ -6,8 +6,6 @@ const basic = ({ type, description, count, cost }) => ({
effect: itemCps => itemCps * 2 effect: itemCps => itemCps * 2
}) })
const nothing = itemCps => itemCps
module.exports = { module.exports = {
doubleClick: basic({ doubleClick: basic({
type: 'mouse', type: 'mouse',
@ -173,6 +171,7 @@ module.exports = {
count: 25, count: 25,
cost: 37_500_000_000_000, cost: 37_500_000_000_000,
}), }),
fzero: basic({ fzero: basic({
type: 'quade', type: 'quade',
description: 'Brings out his competitive spirit.', description: 'Brings out his competitive spirit.',
@ -186,6 +185,19 @@ module.exports = {
cost: 500_000_000_000_000, cost: 500_000_000_000_000,
}), }),
latestNode: basic({
type: 'hvacker',
description: 'The old one has terrible ergonomics, tsk tsk.',
count: 1,
cost: 140_000_000_000_000,
}),
gitCommits: basic({
type: 'hvacker',
description: 'The heads of multiple people in a company are better than, for example, merely one head.',
count: 25,
cost: 7_000_000_000_000_000,
}),
homage: { homage: {
type: 'general', type: 'general',
description: 'The power of original ideas increases your overall CPS by 10%', description: 'The power of original ideas increases your overall CPS by 10%',
@ -194,6 +206,14 @@ module.exports = {
cost: 10_000_000_000, cost: 10_000_000_000,
effect: (itemCps, user) => Math.ceil(itemCps * 1.1) effect: (itemCps, user) => Math.ceil(itemCps * 1.1)
}, },
iLoveHvac: {
type: 'general',
description: 'The power of love increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
emoji: 'heart',
cost: 100_000_000_000_000,
effect: (itemCps, user) => Math.ceil(itemCps * 1.1)
}
// moreUpgrades: { // moreUpgrades: {
// type: 'general', // type: 'general',

194
src/games/hvacoins/utils.js Normal file
View File

@ -0,0 +1,194 @@
const fs = require('fs')
const jokes = require('../jokes')
const achievements = require('./achievements');
const buyableItems = require("./buyableItems");
const upgrades = require("./upgrades");
const quackStore = require("./quackstore");
const saveFile = 'hvacoins.json'
const logError = msg => msg ? console.error(msg) : () => { /* Don't log empty message */ }
const loadGame = () => parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
() => ({
users: {},
nfts: [],
squad: {}
}))
const parseOr = (parseable, orFunc) => {
try {
return JSON.parse(parseable)
} catch (e) {
logError(e)
return orFunc()
}
}
let saves = 0
const saveGame = () => {
if (saves % 100 === 0) {
fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game))
}
saves += 1
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
}
const maybeNews = say => {
const random = Math.random()
if (random > 0.98) {
const prefixedSay = msg => console.log(`Sent news update: '${msg}'`) || say('_Breaking news:_\n' + msg)
setTimeout(() => jokes.newsAlert(prefixedSay).catch(logError), 3000)
} else if (random > 0.96) {
setTimeout(async () => say('_Say have you heard this one?_'), 3000)
setTimeout(() => jokes.tellJoke(say).catch(logError), 4000)
}
}
const idFromWord = word => {
if (!word?.startsWith('<@') || !word.endsWith('>')) {
return null
}
return word.substring(2, word.length - 1)
}
const getSeconds = () => new Date().getTime() / 1000
const commas = num => num.toLocaleString()
const game = loadGame()
const { users, nfts, squad } = game
const setHighestCoins = userId => {
const prevMax = users[userId].highestEver || 0
if (prevMax < users[userId].coins) {
users[userId].highestEver = users[userId].coins
}
}
const addAchievement = (user, achievementName, say) => {
if (!achievements[achievementName]) {
logError(`Achievement ${achievementName} does not exist!`)
return
}
if (user.achievements[achievementName]) {
return
}
setTimeout(async () => {
user.achievements[achievementName] = true
saveGame()
await say(`You earned the achievement ${achievements[achievementName].name}!`)
}, 500)
}
const getUser = userId => {
if (!users[userId]) {
users[userId] = {
coins: 0,
items: {},
upgrades: {},
achievements: {},
coinsAllTime: 0,
prestige: 0
}
} else {
users[userId].items ??= {}
users[userId].upgrades ??= {}
users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0
}
return users[userId]
}
const getCoins = userId => {
const user = getUser(userId)
const currentTime = getSeconds()
const lastCheck = user.lastCheck || currentTime
const secondsPassed = currentTime - lastCheck
const increase = getCPS(userId) * secondsPassed
user.coins += increase
user.coinsAllTime += increase
user.coins = Math.floor(user.coins)
user.lastCheck = currentTime
setHighestCoins(userId)
saveGame()
return user.coins
}
const getCPS = userId => {
const user = getUser(userId)
const userItems = user?.items || {}
return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0))
}
const getItemCps = (user, itemName) => (user.items[itemName] || 0) * singleItemCps(user, itemName)
const squadUpgrades = {
tastyKeyboards: {
name: 'Tasty Keyboards',
description: 'Delicious and sticky. Boosts CPS by 20% for everyone.',
effect: cps => Math.ceil(cps * 1.2),
cost: 10_000_000_000_000,
emoji: 'keyboard'
},
copyPasteMacro: {
name: 'Copy-Paste Macro.',
description: 'Don\'t actually use this. Boosts CPS by 20% for everyone.',
effect: cps => Math.ceil(cps * 1.2),
cost: 100_000_000_000_000,
emoji: 'printer'
}
}
const squadHas = ([name]) => squad.upgrades[name] === true
const squadIsMissing = name => !squadHas(name)
const getCompletedSquadgrades = () =>
Object.entries(squadUpgrades)
.filter(squadHas)
.map(([, upgrade]) => upgrade)
const singleItemCps = (user, itemName) => {
const baseCps = buyableItems[itemName].earning
const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps)
const userGeneralUpgrades = user.upgrades.general || []
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps)
const achievementCount = Object.keys(user.achievements || {}).length
const achievementMultiplier = Math.pow(1.01, achievementCount)
const userQuackgrades = user.quackUpgrades?.cps || []
const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01)
return achievementMultiplier *
quackMultiplier *
prestigeMultiplier *
getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps)
}
module.exports = {
saveGame,
logError,
parseOr,
maybeNews,
idFromWord,
commas,
setHighestCoins,
addAchievement,
getCoins,
getUser,
singleItemCps,
getCPS,
getItemCps,
squadUpgrades,
squadIsMissing,
game
}

View File

@ -0,0 +1,69 @@
const express = require('express')
const app = express()
const port = 3001
const crypto = require('crypto')
const base64 = require('base-64')
const slack = require('../../slack')
const { game: { users } } = require('./utils')
const apiGetUserId = hash => {
return Object.entries(userGetter.users)
.filter(([id, user]) => user.pwHash === hash)
.map(([id, user]) => id)[0]
}
const makeHash = pw =>
crypto.createHash('md5')
.update(pw)
.digest('hex')
const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
const route = async (req, res) => {
const say = async msg => res.send(msg)
try {
const words = ['', ...Object.keys(req.query)]
console.log('INCOMING API CALL:', name, words)
const encoded = req.header('Authorization').substring(5)
const decoded = base64.decode(encoded).substring(1)
const event = {
user: apiGetUserId(makeHash(decoded))
}
if (!event.user) {
res.status(400)
res.send(
'User does not exist, or does not have a password.\n' +
'See \'!setpw help\' for assistance.'
)
console.log(' bad password')
return
}
const lastCall = userGetter.users[event.user].lastApiCall || 0
const secondsBetweenCalls = 5
const currentTime = Math.floor(new Date().getTime() / 1000)
if (lastCall + secondsBetweenCalls > currentTime) {
res.status(400)
res.send(`Must have at least ${secondsBetweenCalls}s between api calls`)
console.log(' rate limited')
return
}
console.log(` went through for ${slack.ourUsers[event.user]}`)
userGetter.users[event.user].lastApiCall = currentTime
await action({event, say, words})
} catch (e) {
console.error(e)
await say(e.stack)
}
}
commandNames.forEach(name =>
app.get('/' + name.replace(/!/gi, ''), route)
)
}
module.exports = {
addCommand,
makeHash,
launch: () => app.listen(port, () => {
console.log(`Express listening on port ${port}`)
})
}

View File

@ -1,4 +1,5 @@
require('./connect4') require('./connect4')
require('./tictactoe') require('./tictactoe')
require('./jokes') require('./jokes')
require('./hvacoins') require('./hvacoins')
require('./trivia')

View File

@ -2,107 +2,107 @@ const slack = require('../slack')
// TODO: Move jokes/news into their own files, and let hvacker edit them when !addjoke or !addnews are used // TODO: Move jokes/news into their own files, and let hvacker edit them when !addjoke or !addnews are used
const jokes = [ const jokes = [
['What do you call a duck that steals things from the bathroom?', 'A robber ducky.'], ['What do you call a duck that steals things from the bathroom?', 'A robber ducky.'],
['On what side does a duck have the most feathers?', 'The outside.'], ['On what side does a duck have the most feathers?', 'The outside.'],
['Why did the duck cross the playground?', 'To get to the other slide.'], ['Why did the duck cross the playground?', 'To get to the other slide.'],
['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'],
['Why do ducks lay eggs?', 'They would break if they dropped them.'], ['Why do ducks lay eggs?', 'They would break if they dropped them.'],
['Why do ducks quack?', 'Well, because they can\'t oink, or moo, or bark.'], ['Why do ducks quack?', 'Well, because they can\'t oink, or moo, or bark.'],
['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'],
['Why did the duck cross the road?', 'To show the chicken how to do it.'], ['Why did the duck cross the road?', 'To show the chicken how to do it.'],
// Puns: // Puns:
['Why do ducks make good detectives?', 'Because they always quack the case!'], ['Why do ducks make good detectives?', 'Because they always quack the case!'],
['When does a duck get up in the morning?', 'At the quack of dawn!'], ['When does a duck get up in the morning?', 'At the quack of dawn!'],
['What do you call a duck that loves fireworks?', 'A fire-quacker.'], ['What do you call a duck that loves fireworks?', 'A fire-quacker.'],
['What did the duck say to the waiter?', '"Put it on my bill."'], ['What did the duck say to the waiter?', '"Put it on my bill."'],
['Where do sick ducks go?', 'To the Ductor!'], ['Where do sick ducks go?', 'To the Ductor!'],
['What kind of TV shows do ducks watch?', 'Duckumenteries!'], ['What kind of TV shows do ducks watch?', 'Duckumenteries!'],
['What type of food do you get when you cross a duck with a mole?', 'Duckamole!'], ['What type of food do you get when you cross a duck with a mole?', 'Duckamole!'],
['What did the duck say when he dropped the dishes?', '"I hope I didn\'t quack any!"'], ['What did the duck say when he dropped the dishes?', '"I hope I didn\'t quack any!"'],
['What is a duck\'s favourite game?', '"Beak-a-boo!"'], ['What is a duck\'s favourite game?', '"Beak-a-boo!"'],
['Why did the duck cross the road?', 'Because there was a quack in the pavement!'], ['Why did the duck cross the road?', 'Because there was a quack in the pavement!'],
['What has webbed feet and fangs?', 'Count Duckula!'], ['What has webbed feet and fangs?', 'Count Duckula!'],
['What do ducks get after they eat?', 'A bill.'], ['What do ducks get after they eat?', 'A bill.'],
['What do ducks eat with their soup?', 'Quackers.'], ['What do ducks eat with their soup?', 'Quackers.'],
['What happens when you say something funny to a duck?', 'It quacks up.'], ['What happens when you say something funny to a duck?', 'It quacks up.'],
['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'], ['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'],
['What do ducks say when people throw things at them?', '"Time to duck!"'], ['What do ducks say when people throw things at them?', '"Time to duck!"'],
['Why are ducks so good at fixing things?', 'Because they\'re great at using duck-tape!'], ['Why are ducks so good at fixing things?', 'Because they\'re great at using duck-tape!'],
['What do you get when you put a bunch of rubber ducks in a box?', 'A box of quackers.'], ['What do you get when you put a bunch of rubber ducks in a box?', 'A box of quackers.'],
['Why was the teacher annoyed with the duck?', 'Because it wouldn\'t stop quacking jokes!'], ['Why was the teacher annoyed with the duck?', 'Because it wouldn\'t stop quacking jokes!'],
['What did the duck eat for a snack?', 'Salted quackers!'], ['What did the duck eat for a snack?', 'Salted quackers!'],
['What do you call a rude duck?', 'A duck with a quackitude.'], ['What do you call a rude duck?', 'A duck with a quackitude.'],
['What did the lawyer say to the duck in court?', '"I demand an egg-splanation!"'], ['What did the lawyer say to the duck in court?', '"I demand an egg-splanation!"'],
['How can you tell rubber ducks apart?', 'You can\'t, they look egg-xactly the same!'], ['How can you tell rubber ducks apart?', 'You can\'t, they look egg-xactly the same!'],
['Why are ducks good at budgeting?', 'They know how to handle the bills!'], ['Why are ducks good at budgeting?', 'They know how to handle the bills!'],
['Where do tough ducks come from?', 'Hard-boiled eggs.'], ['Where do tough ducks come from?', 'Hard-boiled eggs.'],
['Why do ducks have webbed feet?', 'To stomp out fires.', 'Why do elephants have big feet?', 'To stomp out burning ducks.'], ['Why do ducks have webbed feet?', 'To stomp out fires.', 'Why do elephants have big feet?', 'To stomp out burning ducks.'],
['Why do ducks check the news?', 'For the feather forecast.'], ['Why do ducks check the news?', 'For the feather forecast.'],
['What did the ducks carry their schoolbooks in?', 'Their quack-packs.'], ['What did the ducks carry their schoolbooks in?', 'Their quack-packs.'],
['What do you call it when it\'s raining ducks and chickens?', 'Fowl weather.'], ['What do you call it when it\'s raining ducks and chickens?', 'Fowl weather.'],
['Why did the duck get a red card in the football game?', 'For Fowl-play.'], ['Why did the duck get a red card in the football game?', 'For Fowl-play.'],
['What did the duck say to the spider?', '"Why don\'t you have webbed feet?"'], ['What did the duck say to the spider?', '"Why don\'t you have webbed feet?"'],
['What do you get if you cross a duck with an accountant?', 'A bill with a bill.'], ['What do you get if you cross a duck with an accountant?', 'A bill with a bill.'],
['What do you call a duck\'s burp?', 'A fowl smell!'], ['What do you call a duck\'s burp?', 'A fowl smell!'],
['What do you get if you cross a vampire, duck and a sheep?', 'Count Duck-ewe-la.'], ['What do you get if you cross a vampire, duck and a sheep?', 'Count Duck-ewe-la.'],
['What do you call a duck that\'s very clever?', 'A wise-quacker.'], ['What do you call a duck that\'s very clever?', 'A wise-quacker.'],
['Why do ducks never ask for directions?', 'They prefer to wing it.'], ['Why do ducks never ask for directions?', 'They prefer to wing it.'],
// I wrote dis // I wrote dis
['What did the man say to his wife when a duck flew at her head?', '"Look out! There\'s a duck flying at your head!"'], ['What did the man say to his wife when a duck flew at her head?', '"Look out! There\'s a duck flying at your head!"'],
['What kind of duck plays goalie?', 'A hockey duck.'], ['What kind of duck plays goalie?', 'A hockey duck.'],
['What kind of bird gets a job here?', 'A software duckveloper!'], ['What kind of bird gets a job here?', 'A software duckveloper!'],
['How many ducks does it take to screw in a light bulb?', 'Ducks do not live indoors.'], ['How many ducks does it take to screw in a light bulb?', 'Ducks do not live indoors.'],
['What kind of drug does a duck like?', 'Quack.'] ['What kind of drug does a duck like?', 'Quack.']
] ]
const news = [ const news = [
'Duck criminal escapes from duck prison. Whereabouts unknown.', 'Duck criminal escapes from duck prison. Whereabouts unknown.',
'Criminal mastermind duck believed to have taken refuge on Slack.', 'Criminal mastermind duck believed to have taken refuge on Slack.',
'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.', 'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.',
'Six geese arrested under suspicion of honking.', 'Six geese arrested under suspicion of honking.',
'Swan under investigation for illegal trumpet-smuggling ring.', 'Swan under investigation for illegal trumpet-smuggling ring.',
'Local rooster the subject of serious egg-stealing allegations.', 'Local rooster the subject of serious egg-stealing allegations.',
'Inducknesian court rules that the news is certainly _not_ controlled by an all-powerful duck lord, and that everyone should please stop asking about it.', 'Inducknesian court rules that the news is certainly _not_ controlled by an all-powerful duck lord, and that everyone should please stop asking about it.',
'"Spending 6,000,000,000 HVAC on a space yacht was the best decision of my life", reveals local billionaire/jerk.', '"Spending 6,000,000,000 HVAC on a space yacht was the best decision of my life", reveals local billionaire/jerk.',
'16 Unethical Egg Hacks You Won\'t Learn In School. You can\'t believe number 5!', '16 Unethical Egg Hacks You Won\'t Learn In School. You can\'t believe number 5!',
'The word "Tuesday" declared illegal. Refer to it as "Quackday" from now on.', 'The word "Tuesday" declared illegal. Refer to it as "Quackday" from now on.',
'Has anyone been killed/maimed/publicly humiliated for not following the duck lord\'s orders? The answer might surprise you.', 'Has anyone been killed/maimed/publicly humiliated for not following the duck lord\'s orders? The answer might surprise you.',
'_They called him crazy -_ Local duck bores to the center of the earth and lives with the mole people.', '_They called him crazy -_ Local duck bores to the center of the earth and lives with the mole people.',
'Jerry Seinfeld - Love affair with a hen! Is this the new Hollywood power couple?', 'Jerry Seinfeld - Love affair with a hen! Is this the new Hollywood power couple?',
'Is your uncle secretly a goose? There\'s simply no way of knowing.', 'Is your uncle secretly a goose? There\'s simply no way of knowing.',
'Danny Duckvito considers opening his own chain of supermarkets. He is quoted as saying "Heyyyy, I\'m Danny Duckvito"', 'Danny Duckvito considers opening his own chain of supermarkets. He is quoted as saying "Heyyyy, I\'m Danny Duckvito"',
'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.' 'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.'
] ]
const tellJoke = async say => { const tellJoke = async say => {
const joke = jokes[Math.floor(Math.random() * jokes.length)] const joke = jokes[Math.floor(Math.random() * jokes.length)]
let timeout = 0 let timeout = 0
for (const part of joke) { for (const part of joke) {
setTimeout(async () => await say(part), timeout); setTimeout(async () => say(part), timeout)
timeout += 5000 timeout += 5000
} }
} }
const newsAlert = async say => { const newsAlert = async say => {
let article = lastNews let article = lastNews
while(article === lastNews) { while (article === lastNews) {
article = Math.floor(Math.random() * news.length) article = Math.floor(Math.random() * news.length)
} }
lastNews = article lastNews = article
await say(news[article]) await say(news[article])
} }
let lastNews = -1 let lastNews = -1
slack.onMessage(async ({ event, say }) => { slack.onMessage(async ({ event, say }) => {
if (event.text?.toLowerCase() === '!joke') { if (event.text?.toLowerCase() === '!joke') {
await tellJoke(say) await tellJoke(say)
} else if (event.text?.toLowerCase() === '!news') { } else if (event.text?.toLowerCase() === '!news') {
await newsAlert(say) await newsAlert(say)
} }
}) })
module.exports = { module.exports = {
tellJoke, tellJoke,
newsAlert, newsAlert
} }

View File

@ -3,16 +3,16 @@ const slack = require('../slack')
const tie = 'TIE' const tie = 'TIE'
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) => const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) =>
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' + gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
'```' + textFromBoard(board) + '\n```' '```' + textFromBoard(board) + '\n```'
const addChoiceEmojis = async ({ choices, channel, ts }) => { const addChoiceEmojis = async ({ choices, channel, ts }) => {
const addEmoji = async emojiName => const addEmoji = async emojiName =>
await slack.app.client.reactions.add({ await slack.app.client.reactions.add({
channel, channel,
timestamp: ts, timestamp: ts,
name: emojiName name: emojiName
}) })
for (const choice of choices) { for (const choice of choices) {
await addEmoji(choice) await addEmoji(choice)
} }
@ -32,12 +32,12 @@ const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, in
player2: opponent player2: opponent
}) })
const sent = await say(msg) const sent = await say(msg)
await addChoiceEmojis({...sent, choices: turnChoiceEmojis}) await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis })
} }
} }
} }
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, {board, players}) const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players })
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message) const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
@ -95,33 +95,33 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
} }
const removeEmoji = async emojiName => const removeEmoji = async emojiName =>
slack.app.client.reactions.remove({ slack.app.client.reactions.remove({
channel: event.item.channel, channel: event.item.channel,
timestamp: message.messages[0]?.ts, timestamp: message.messages[0]?.ts,
name: emojiName name: emojiName
}) })
turnChoiceEmojis.forEach(removeEmoji) turnChoiceEmojis.forEach(removeEmoji)
const sentBoard = await slack.app.client.chat.postMessage({ const sentBoard = await slack.app.client.chat.postMessage({
channel: opponent, channel: opponent,
text: boardMessage + winnerMessages.opponent text: boardMessage + winnerMessages.opponent
}) })
if (!winner) { if (!winner) {
await addChoiceEmojis({...sentBoard, choices: turnChoiceEmojis}) await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
} }
} }
module.exports = { module.exports = {
tie, tie,
build: ({ build: ({
startTriggers, startTriggers,
initialBoard, initialBoard,
turnChoiceEmojis, turnChoiceEmojis,
gameName, gameName,
textFromBoard, textFromBoard,
checkWinner, checkWinner,
makeMove, makeMove
}) => { }) => {
const dataName = gameName.replace(/[^0-9a-zA-Z]/gi, '') const dataName = gameName.replace(/[^0-9A-Z]/gi, '')
const gameStarter = buildGameStarter({ const gameStarter = buildGameStarter({
startTriggers, startTriggers,
gameName, gameName,
@ -129,7 +129,7 @@ module.exports = {
initialBoard, initialBoard,
textFromBoard, textFromBoard,
turnChoiceEmojis turnChoiceEmojis
}); })
slack.onMessage(gameStarter) slack.onMessage(gameStarter)
const turnHandler = buildTurnHandler({ const turnHandler = buildTurnHandler({

13
src/games/trivia.js Normal file
View File

@ -0,0 +1,13 @@
const axios = require('axios')
const getTrivia = async () => axios.get('https://opentdb.com/api.php?amount=10&category=9&difficulty=medium&type=multiple', {
headers: {
Accept: 'application/json, text/plain, */*'
}
})
.then(res => res.data.results)
.catch(console.error)
module.exports = {
getTrivia
}

View File

@ -1,6 +1,6 @@
const postToHoneywell = options => {} const postToHoneywell = options => {}
const postTemp = ({ heatSetpoint, coolSetpoint, mode}) => { const postTemp = ({ heatSetpoint, coolSetpoint, mode }) => {
postToHoneywell({ postToHoneywell({
heatSetpoint, heatSetpoint,
coolSetpoint, coolSetpoint,

View File

@ -5,7 +5,7 @@ const temperatureChannelId = 'C034156CE03'
const hvackerBotUserId = 'U0344TFA7HQ' const hvackerBotUserId = 'U0344TFA7HQ'
const sageUserId = 'U028BMEBWBV' const sageUserId = 'U028BMEBWBV'
const pollingMinutes = 10 const pollingMinutes = 5
const pollingPeriod = 1000 * 60 * pollingMinutes const pollingPeriod = 1000 * 60 * pollingMinutes
const colderEmoji = 'snowflake' const colderEmoji = 'snowflake'
@ -14,175 +14,200 @@ const goodEmoji = '+1'
let app let app
try { try {
app = new SlackApp({ app = new SlackApp({
token: config.slackBotToken, token: config.slackBotToken,
signingSecret: config.slackSigningSecret, signingSecret: config.slackSigningSecret,
appToken: config.slackAppToken, appToken: config.slackAppToken,
socketMode: true socketMode: true
}) })
// app.client.conversations.list({types: 'private_channel'}).then(fetched => { // app.client.conversations.list({types: 'private_channel'}).then(fetched => {
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
// console.log('techThermostatChannelId', temperatureChannelId) // console.log('techThermostatChannelId', temperatureChannelId)
// }) // })
} catch (e) { } catch (e) {
console.log('Failed to initialize SlackApp', e) console.log('Failed to initialize SlackApp', e)
} }
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold'] const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>'] const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
const sendHelp = async (say, prefix) => { const sendHelp = async (say, prefix) => {
if (prefix) { if (prefix) {
prefix = prefix + '\n' prefix = prefix + '\n'
} else { } else {
prefix = '' prefix = ''
} }
await say({ await say({
text: prefix + text: prefix +
`Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` +
'At this time I am not capable of actually changing the temperature. Go bug Quade.' 'At this time I am not capable of actually changing the temperature. Go bug Quade.'
}) })
} }
const getMessage = async ({channel, ts}) => app.client.conversations.history({ const getMessage = async ({ channel, ts }) => app.client.conversations.history({
channel: channel, channel: channel,
latest: ts, latest: ts,
inclusive: true, inclusive: true,
limit: 1 limit: 1
}) })
app.event('reaction_added', async ({ event, context, client, say }) => { app.event('reaction_added', async ({ event, context, client, say }) => {
for (const listener of reactionListeners) { for (const listener of reactionListeners) {
listener({ event, say }) listener({ event, say })
} }
}) })
const ourUsers = { const ourUsers = {
'U028BMEBWBV': 'Sage', U028BMEBWBV: 'Sage',
'U02U15RFK4Y': 'Adam', U02U15RFK4Y: 'Adam',
'U02AAB54V34': 'Houston', U02AAB54V34: 'Houston',
'U02KYLVK1GV': 'Quade', U02KYLVK1GV: 'Quade',
'U017PG4EL1Y': 'Max', U017PG4EL1Y: 'Max',
'UTDLFGZA5': 'Tyler' UTDLFGZA5: 'Tyler',
U017CB5L1K3: 'Andres'
} }
let activePolls = {} const activePolls = {}
const testId = 'U028BMEBWBV_TEST'
let testMode = false
app.event('message', async ({ event, context, client, say }) => { app.event('message', async ({ event, context, client, say }) => {
for (const listener of messageListeners) { console.log(event)
listener({ event, say }) if (event.user === sageUserId) {
if (event?.text.startsWith('!')) {
if (testMode) {
await messageSage('Currently in test mode!')
}
} }
console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) if (event?.text === '!test') {
if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') { testMode = !testMode
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) { await messageSage(`TestMode: ${testMode} with ID ${testId}`)
await postToTechThermostatChannel(event.text.substring(4).trim()) } else if (event?.text === '!notest') {
return testMode = false
} await messageSage(`TestMode: ${testMode}`)
if (event.text?.startsWith('!saytoq ')) {
await messageQuade(event.text.substring(7).trim())
return
}
if (event.text?.startsWith('!saytos')) {
await messageSage(event.text.substring(7).trim())
return
}
} }
let eventText = event.text?.toLowerCase() || '' if (testMode) {
event.user = testId
}
// console.log(event.blocks[0].elements[0])
}
for (const listener of messageListeners) {
listener({ event, say })
}
console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') {
process.exit()
}
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) {
await postToTechThermostatChannel(event.text.substring(4).trim())
return
}
if (event.text?.startsWith('!saytoq ')) {
await messageQuade(event.text.substring(7).trim())
return
}
if (event.text?.startsWith('!saytos')) {
await messageSage(event.text.substring(7).trim())
return
}
}
const eventText = event.text?.toLowerCase() || ''
if (eventText.startsWith('!help')) { if (eventText.startsWith('!help')) {
await sendHelp(say) await sendHelp(say)
return return
}
if (!pollTriggers.includes(eventText)) {
if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.')
}
return
}
if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
return
}
activePolls[event.channel] = true
const pollTs = await startPoll()
setTimeout(async () => {
const reactions = await app.client.reactions.get({
channel: temperatureChannelId,
timestamp: pollTs,
full: true
})
const reactCounts = {}
reactions.message.reactions.forEach(reaction => { reactCounts[reaction.name] = reaction.count })
const contentVotes = reactCounts[goodEmoji]
const hotterVotes = reactCounts[hotterEmoji]
const colderVotes = reactCounts[colderEmoji]
let text = 'The people have spoken, and would like to '
if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text += 'raise the temperature, quack.'
requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text += 'lower the temperature, quack quack.'
requestTempChange('Colder')
} else {
text += 'keep the temperature as-is, quaaack.'
requestTempChange('Good')
} }
if (!pollTriggers.includes(eventText)) { await postToTechThermostatChannel({ text })
if (halfTriggers.includes(eventText)) { delete activePolls[event.channel]
await sendHelp(say, 'It looks like you might want to change the temperature.') }, pollingPeriod)
}
return
}
if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
return
}
activePolls[event.channel] = true
const pollTs = await startPoll()
setTimeout(async () => {
const reactions = await app.client.reactions.get({
channel: temperatureChannelId,
timestamp: pollTs,
full: true
})
const reactCounts = {}
reactions.message.reactions.forEach(reaction => reactCounts[reaction.name] = reaction.count)
const contentVotes = reactCounts[goodEmoji]
const hotterVotes = reactCounts[hotterEmoji]
const colderVotes = reactCounts[colderEmoji]
let text = 'The people have spoken, and would like to '
if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text += 'raise the temperature, quack.'
requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text += 'lower the temperature, quack quack.'
requestTempChange('Colder')
} else {
text += 'keep the temperature as-is, quaaack.'
requestTempChange('Good')
}
await postToTechThermostatChannel({ text })
delete activePolls[event.channel]
}, pollingPeriod)
}) })
;(async () => { ;(async () => {
await app.start().catch(console.error) await app.start().catch(console.error)
console.log('Slack Bolt has started') console.log('Slack Bolt has started')
//setTimeout(async () => { // setTimeout(async () => {
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>') // await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
//}, 2000) // }, 2000)
})(); })()
const postToTechThermostatChannel = async optionsOrText => { const postToTechThermostatChannel = async optionsOrText => {
if (optionsOrText === null || typeof optionsOrText !== 'object') { if (optionsOrText === null || typeof optionsOrText !== 'object') {
optionsOrText = { optionsOrText = {
text: optionsOrText text: optionsOrText
}
} }
return await app.client.chat.postMessage({...optionsOrText, channel: temperatureChannelId}) }
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
} }
const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText) const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText)
const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText) const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText)
const messageIn = async (channel, optionsOrText) => { const messageIn = async (channel, optionsOrText) => {
if (optionsOrText === null || typeof optionsOrText !== 'object') { if (optionsOrText === null || typeof optionsOrText !== 'object') {
optionsOrText = { optionsOrText = {
text: optionsOrText text: optionsOrText
}
} }
return await app.client.chat.postMessage({...optionsOrText, channel}) }
return app.client.chat.postMessage({ ...optionsOrText, channel })
} }
const startPoll = async () => { const startPoll = async () => {
const sent = await postToTechThermostatChannel({ const sent = await postToTechThermostatChannel({
text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minute(s) the temperature will be adjusted.\n` + text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` +
`Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` + `Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` +
`\n(Note that I can't actually change the temperature yet. Make Quade do it!)` `\n(Note that I can't actually change the temperature yet. Make Quade do it!)`
})
const addReaction = async emojiName =>
app.client.reactions.add({
channel: temperatureChannelId,
timestamp: sent.ts,
name: emojiName
}) })
const addReaction = async emojiName => await addReaction(colderEmoji)
await app.client.reactions.add({ await addReaction(hotterEmoji)
channel: temperatureChannelId, await addReaction(goodEmoji)
timestamp: sent.ts, return sent.ts
name: emojiName
})
await addReaction(colderEmoji)
await addReaction(hotterEmoji)
await addReaction(goodEmoji)
return sent.ts
} }
const tempChangeListeners = [] const tempChangeListeners = []
@ -190,45 +215,52 @@ const messageListeners = []
const reactionListeners = [] const reactionListeners = []
const requestTempChange = change => { const requestTempChange = change => {
tempChangeListeners.forEach(listener => listener(change)) tempChangeListeners.forEach(listener => listener(change))
} }
const encodeData = (key, data) => const encodeData = (key, data) =>
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >` `<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
const decodeData = (key, message) => { const decodeData = (key, message) => {
const regex = new RegExp(`http://${key}ZZZ[^|]*`) const regex = new RegExp(`http://${key}ZZZ[^|]*`)
let match = message.match(regex) let match = message.match(regex)
if (!match) { if (!match) {
return match return match
} }
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8')) return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} }
const onReaction = listener => reactionListeners.push(listener) const onReaction = listener => reactionListeners.push(listener)
onReaction(async ({ event, say }) => { onReaction(async ({ event }) => {
if (event.user === sageUserId && event.reaction === 'x') { if (event.user === sageUserId && event.reaction === 'x') {
console.log(event) console.log(event)
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts }) try {
await app.client.chat.delete({channel: event.item.channel, ts: event.item.ts})
} catch (e) {
console.error(e)
} }
}
}) })
module.exports = { module.exports = {
app, app,
hvackerBotUserId, hvackerBotUserId,
temperatureChannelId, temperatureChannelId,
onAction: app.action, onAction: app.action,
getMessage, getMessage,
updateMessage: app.client.chat.update, updateMessage: app.client.chat.update,
postToTechThermostatChannel, postToTechThermostatChannel,
onTempChangeRequested: listener => tempChangeListeners.push(listener), onTempChangeRequested: listener => tempChangeListeners.push(listener),
onMessage: listener => messageListeners.push(listener), onMessage: listener => messageListeners.push(listener),
onReaction, onReaction,
encodeData, encodeData,
decodeData, decodeData,
sageUserId, sageUserId,
messageSage, messageSage,
ourUsers messageIn,
} testMode,
testId,
ourUsers
}