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:
parent
f054154717
commit
e971f7e7c2
|
@ -673,6 +673,16 @@
|
|||
"promise.allsettled": "^1.0.2",
|
||||
"raw-body": "^2.3.3",
|
||||
"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": {
|
||||
|
@ -1082,11 +1092,11 @@
|
|||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
|
||||
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@slack/bolt": "^3.9.0",
|
||||
"axios": "^0.26.0",
|
||||
"base-64": "^1.0.0",
|
||||
"express": "^4.17.3",
|
||||
"fs": "0.0.1-security",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
let base64 = require('base-64')
|
||||
let config = require('../config')
|
||||
const base64 = require('base-64')
|
||||
const config = require('../config')
|
||||
|
||||
const url = ''
|
||||
|
||||
|
|
|
@ -38,6 +38,42 @@ module.exports = {
|
|||
name: 'You light my fire, baby',
|
||||
description: 'And you pay attention to descriptions!',
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -64,5 +64,11 @@ module.exports = {
|
|||
earning: 65_000_000,
|
||||
emoji: 'hvacker_angery',
|
||||
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
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -6,8 +6,6 @@ const basic = ({ type, description, count, cost }) => ({
|
|||
effect: itemCps => itemCps * 2
|
||||
})
|
||||
|
||||
const nothing = itemCps => itemCps
|
||||
|
||||
module.exports = {
|
||||
doubleClick: basic({
|
||||
type: 'mouse',
|
||||
|
@ -173,6 +171,7 @@ module.exports = {
|
|||
count: 25,
|
||||
cost: 37_500_000_000_000,
|
||||
}),
|
||||
|
||||
fzero: basic({
|
||||
type: 'quade',
|
||||
description: 'Brings out his competitive spirit.',
|
||||
|
@ -186,6 +185,19 @@ module.exports = {
|
|||
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: {
|
||||
type: 'general',
|
||||
description: 'The power of original ideas increases your overall CPS by 10%',
|
||||
|
@ -194,6 +206,14 @@ module.exports = {
|
|||
cost: 10_000_000_000,
|
||||
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: {
|
||||
// type: 'general',
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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}`)
|
||||
})
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
require('./connect4')
|
||||
require('./tictactoe')
|
||||
require('./jokes')
|
||||
require('./hvacoins')
|
||||
require('./hvacoins')
|
||||
require('./trivia')
|
||||
|
|
|
@ -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
|
||||
const jokes = [
|
||||
['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.'],
|
||||
['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 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 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.'],
|
||||
// Puns:
|
||||
['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!'],
|
||||
['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."'],
|
||||
['Where do sick ducks go?', 'To the Ductor!'],
|
||||
['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 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!"'],
|
||||
['Why did the duck cross the road?', 'Because there was a quack in the pavement!'],
|
||||
['What has webbed feet and fangs?', 'Count Duckula!'],
|
||||
['What do ducks get after they eat?', 'A bill.'],
|
||||
['What do ducks eat with their soup?', 'Quackers.'],
|
||||
['What happens when you say something funny to a duck?', 'It quacks up.'],
|
||||
['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'],
|
||||
['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!'],
|
||||
['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!'],
|
||||
['What did the duck eat for a snack?', 'Salted quackers!'],
|
||||
['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!"'],
|
||||
['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!'],
|
||||
['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 check the news?', 'For the feather forecast.'],
|
||||
['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.'],
|
||||
['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 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 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.'],
|
||||
['Why do ducks never ask for directions?', 'They prefer to wing it.'],
|
||||
// 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 kind of duck plays goalie?', 'A hockey duck.'],
|
||||
['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.'],
|
||||
['What kind of drug does a duck like?', 'Quack.']
|
||||
['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.'],
|
||||
['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 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 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.'],
|
||||
// Puns:
|
||||
['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!'],
|
||||
['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."'],
|
||||
['Where do sick ducks go?', 'To the Ductor!'],
|
||||
['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 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!"'],
|
||||
['Why did the duck cross the road?', 'Because there was a quack in the pavement!'],
|
||||
['What has webbed feet and fangs?', 'Count Duckula!'],
|
||||
['What do ducks get after they eat?', 'A bill.'],
|
||||
['What do ducks eat with their soup?', 'Quackers.'],
|
||||
['What happens when you say something funny to a duck?', 'It quacks up.'],
|
||||
['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'],
|
||||
['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!'],
|
||||
['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!'],
|
||||
['What did the duck eat for a snack?', 'Salted quackers!'],
|
||||
['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!"'],
|
||||
['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!'],
|
||||
['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 check the news?', 'For the feather forecast.'],
|
||||
['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.'],
|
||||
['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 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 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.'],
|
||||
['Why do ducks never ask for directions?', 'They prefer to wing it.'],
|
||||
// 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 kind of duck plays goalie?', 'A hockey duck.'],
|
||||
['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.'],
|
||||
['What kind of drug does a duck like?', 'Quack.']
|
||||
]
|
||||
|
||||
const news = [
|
||||
'Duck criminal escapes from duck prison. Whereabouts unknown.',
|
||||
'Criminal mastermind duck believed to have taken refuge on Slack.',
|
||||
'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.',
|
||||
'Six geese arrested under suspicion of honking.',
|
||||
'Swan under investigation for illegal trumpet-smuggling ring.',
|
||||
'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.',
|
||||
'"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!',
|
||||
'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.',
|
||||
'_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?',
|
||||
'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"',
|
||||
'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.'
|
||||
'Duck criminal escapes from duck prison. Whereabouts unknown.',
|
||||
'Criminal mastermind duck believed to have taken refuge on Slack.',
|
||||
'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.',
|
||||
'Six geese arrested under suspicion of honking.',
|
||||
'Swan under investigation for illegal trumpet-smuggling ring.',
|
||||
'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.',
|
||||
'"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!',
|
||||
'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.',
|
||||
'_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?',
|
||||
'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"',
|
||||
'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.'
|
||||
]
|
||||
|
||||
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
|
||||
for (const part of joke) {
|
||||
setTimeout(async () => await say(part), timeout);
|
||||
timeout += 5000
|
||||
}
|
||||
let timeout = 0
|
||||
for (const part of joke) {
|
||||
setTimeout(async () => say(part), timeout)
|
||||
timeout += 5000
|
||||
}
|
||||
}
|
||||
|
||||
const newsAlert = async say => {
|
||||
let article = lastNews
|
||||
while(article === lastNews) {
|
||||
article = Math.floor(Math.random() * news.length)
|
||||
}
|
||||
lastNews = article
|
||||
await say(news[article])
|
||||
let article = lastNews
|
||||
while (article === lastNews) {
|
||||
article = Math.floor(Math.random() * news.length)
|
||||
}
|
||||
lastNews = article
|
||||
await say(news[article])
|
||||
}
|
||||
|
||||
let lastNews = -1
|
||||
slack.onMessage(async ({ event, say }) => {
|
||||
if (event.text?.toLowerCase() === '!joke') {
|
||||
await tellJoke(say)
|
||||
await tellJoke(say)
|
||||
} else if (event.text?.toLowerCase() === '!news') {
|
||||
await newsAlert(say)
|
||||
await newsAlert(say)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
tellJoke,
|
||||
newsAlert,
|
||||
}
|
||||
tellJoke,
|
||||
newsAlert
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@ const slack = require('../slack')
|
|||
const tie = 'TIE'
|
||||
|
||||
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) =>
|
||||
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
|
||||
'```' + textFromBoard(board) + '\n```'
|
||||
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
|
||||
'```' + textFromBoard(board) + '\n```'
|
||||
|
||||
const addChoiceEmojis = async ({ choices, channel, ts }) => {
|
||||
const addEmoji = async emojiName =>
|
||||
await slack.app.client.reactions.add({
|
||||
channel,
|
||||
timestamp: ts,
|
||||
name: emojiName
|
||||
})
|
||||
await slack.app.client.reactions.add({
|
||||
channel,
|
||||
timestamp: ts,
|
||||
name: emojiName
|
||||
})
|
||||
for (const choice of choices) {
|
||||
await addEmoji(choice)
|
||||
}
|
||||
|
@ -32,12 +32,12 @@ const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, in
|
|||
player2: opponent
|
||||
})
|
||||
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)
|
||||
|
||||
|
@ -95,33 +95,33 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
|
|||
}
|
||||
|
||||
const removeEmoji = async emojiName =>
|
||||
slack.app.client.reactions.remove({
|
||||
channel: event.item.channel,
|
||||
timestamp: message.messages[0]?.ts,
|
||||
name: emojiName
|
||||
})
|
||||
slack.app.client.reactions.remove({
|
||||
channel: event.item.channel,
|
||||
timestamp: message.messages[0]?.ts,
|
||||
name: emojiName
|
||||
})
|
||||
turnChoiceEmojis.forEach(removeEmoji)
|
||||
const sentBoard = await slack.app.client.chat.postMessage({
|
||||
channel: opponent,
|
||||
text: boardMessage + winnerMessages.opponent
|
||||
})
|
||||
if (!winner) {
|
||||
await addChoiceEmojis({...sentBoard, choices: turnChoiceEmojis})
|
||||
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
tie,
|
||||
build: ({
|
||||
startTriggers,
|
||||
initialBoard,
|
||||
turnChoiceEmojis,
|
||||
gameName,
|
||||
textFromBoard,
|
||||
checkWinner,
|
||||
makeMove,
|
||||
}) => {
|
||||
const dataName = gameName.replace(/[^0-9a-zA-Z]/gi, '')
|
||||
startTriggers,
|
||||
initialBoard,
|
||||
turnChoiceEmojis,
|
||||
gameName,
|
||||
textFromBoard,
|
||||
checkWinner,
|
||||
makeMove
|
||||
}) => {
|
||||
const dataName = gameName.replace(/[^0-9A-Z]/gi, '')
|
||||
const gameStarter = buildGameStarter({
|
||||
startTriggers,
|
||||
gameName,
|
||||
|
@ -129,7 +129,7 @@ module.exports = {
|
|||
initialBoard,
|
||||
textFromBoard,
|
||||
turnChoiceEmojis
|
||||
});
|
||||
})
|
||||
slack.onMessage(gameStarter)
|
||||
|
||||
const turnHandler = buildTurnHandler({
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
const postToHoneywell = options => {}
|
||||
|
||||
const postTemp = ({ heatSetpoint, coolSetpoint, mode}) => {
|
||||
const postTemp = ({ heatSetpoint, coolSetpoint, mode }) => {
|
||||
postToHoneywell({
|
||||
heatSetpoint,
|
||||
coolSetpoint,
|
||||
|
|
|
@ -5,7 +5,7 @@ const temperatureChannelId = 'C034156CE03'
|
|||
const hvackerBotUserId = 'U0344TFA7HQ'
|
||||
const sageUserId = 'U028BMEBWBV'
|
||||
|
||||
const pollingMinutes = 10
|
||||
const pollingMinutes = 5
|
||||
const pollingPeriod = 1000 * 60 * pollingMinutes
|
||||
|
||||
const colderEmoji = 'snowflake'
|
||||
|
@ -14,175 +14,200 @@ const goodEmoji = '+1'
|
|||
|
||||
let app
|
||||
try {
|
||||
app = new SlackApp({
|
||||
token: config.slackBotToken,
|
||||
signingSecret: config.slackSigningSecret,
|
||||
appToken: config.slackAppToken,
|
||||
socketMode: true
|
||||
})
|
||||
// app.client.conversations.list({types: 'private_channel'}).then(fetched => {
|
||||
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
|
||||
// console.log('techThermostatChannelId', temperatureChannelId)
|
||||
// })
|
||||
app = new SlackApp({
|
||||
token: config.slackBotToken,
|
||||
signingSecret: config.slackSigningSecret,
|
||||
appToken: config.slackAppToken,
|
||||
socketMode: true
|
||||
})
|
||||
// app.client.conversations.list({types: 'private_channel'}).then(fetched => {
|
||||
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
|
||||
// console.log('techThermostatChannelId', temperatureChannelId)
|
||||
// })
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize SlackApp', e)
|
||||
console.log('Failed to initialize SlackApp', e)
|
||||
}
|
||||
|
||||
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold']
|
||||
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
|
||||
|
||||
const sendHelp = async (say, prefix) => {
|
||||
if (prefix) {
|
||||
prefix = prefix + '\n'
|
||||
} else {
|
||||
prefix = ''
|
||||
}
|
||||
if (prefix) {
|
||||
prefix = prefix + '\n'
|
||||
} else {
|
||||
prefix = ''
|
||||
}
|
||||
|
||||
await say({
|
||||
text: prefix +
|
||||
`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.'
|
||||
})
|
||||
await say({
|
||||
text: prefix +
|
||||
`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.'
|
||||
})
|
||||
}
|
||||
|
||||
const getMessage = async ({channel, ts}) => app.client.conversations.history({
|
||||
channel: channel,
|
||||
latest: ts,
|
||||
inclusive: true,
|
||||
limit: 1
|
||||
const getMessage = async ({ channel, ts }) => app.client.conversations.history({
|
||||
channel: channel,
|
||||
latest: ts,
|
||||
inclusive: true,
|
||||
limit: 1
|
||||
})
|
||||
|
||||
app.event('reaction_added', async ({ event, context, client, say }) => {
|
||||
for (const listener of reactionListeners) {
|
||||
listener({ event, say })
|
||||
}
|
||||
for (const listener of reactionListeners) {
|
||||
listener({ event, say })
|
||||
}
|
||||
})
|
||||
|
||||
const ourUsers = {
|
||||
'U028BMEBWBV': 'Sage',
|
||||
'U02U15RFK4Y': 'Adam',
|
||||
'U02AAB54V34': 'Houston',
|
||||
'U02KYLVK1GV': 'Quade',
|
||||
'U017PG4EL1Y': 'Max',
|
||||
'UTDLFGZA5': 'Tyler'
|
||||
U028BMEBWBV: 'Sage',
|
||||
U02U15RFK4Y: 'Adam',
|
||||
U02AAB54V34: 'Houston',
|
||||
U02KYLVK1GV: 'Quade',
|
||||
U017PG4EL1Y: 'Max',
|
||||
UTDLFGZA5: 'Tyler',
|
||||
U017CB5L1K3: 'Andres'
|
||||
}
|
||||
|
||||
let activePolls = {}
|
||||
const activePolls = {}
|
||||
const testId = 'U028BMEBWBV_TEST'
|
||||
let testMode = false
|
||||
app.event('message', async ({ event, context, client, say }) => {
|
||||
for (const listener of messageListeners) {
|
||||
listener({ event, say })
|
||||
console.log(event)
|
||||
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.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') {
|
||||
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
|
||||
}
|
||||
if (event?.text === '!test') {
|
||||
testMode = !testMode
|
||||
await messageSage(`TestMode: ${testMode} with ID ${testId}`)
|
||||
} else if (event?.text === '!notest') {
|
||||
testMode = false
|
||||
await messageSage(`TestMode: ${testMode}`)
|
||||
}
|
||||
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')) {
|
||||
await sendHelp(say)
|
||||
return
|
||||
if (eventText.startsWith('!help')) {
|
||||
await sendHelp(say)
|
||||
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)) {
|
||||
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')
|
||||
}
|
||||
|
||||
await postToTechThermostatChannel({ text })
|
||||
delete activePolls[event.channel]
|
||||
}, pollingPeriod)
|
||||
await postToTechThermostatChannel({ text })
|
||||
delete activePolls[event.channel]
|
||||
}, pollingPeriod)
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
await app.start().catch(console.error)
|
||||
console.log('Slack Bolt has started')
|
||||
//setTimeout(async () => {
|
||||
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
|
||||
//}, 2000)
|
||||
})();
|
||||
await app.start().catch(console.error)
|
||||
console.log('Slack Bolt has started')
|
||||
// setTimeout(async () => {
|
||||
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
|
||||
// }, 2000)
|
||||
})()
|
||||
|
||||
const postToTechThermostatChannel = async optionsOrText => {
|
||||
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||
optionsOrText = {
|
||||
text: optionsOrText
|
||||
}
|
||||
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||
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 messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText)
|
||||
|
||||
const messageIn = async (channel, optionsOrText) => {
|
||||
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||
optionsOrText = {
|
||||
text: optionsOrText
|
||||
}
|
||||
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||
optionsOrText = {
|
||||
text: optionsOrText
|
||||
}
|
||||
return await app.client.chat.postMessage({...optionsOrText, channel})
|
||||
}
|
||||
return app.client.chat.postMessage({ ...optionsOrText, channel })
|
||||
}
|
||||
|
||||
const startPoll = async () => {
|
||||
const sent = await postToTechThermostatChannel({
|
||||
text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minute(s) 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.` +
|
||||
`\n(Note that I can't actually change the temperature yet. Make Quade do it!)`
|
||||
const sent = await postToTechThermostatChannel({
|
||||
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.` +
|
||||
`\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 app.client.reactions.add({
|
||||
channel: temperatureChannelId,
|
||||
timestamp: sent.ts,
|
||||
name: emojiName
|
||||
})
|
||||
await addReaction(colderEmoji)
|
||||
await addReaction(hotterEmoji)
|
||||
await addReaction(goodEmoji)
|
||||
return sent.ts
|
||||
await addReaction(colderEmoji)
|
||||
await addReaction(hotterEmoji)
|
||||
await addReaction(goodEmoji)
|
||||
return sent.ts
|
||||
}
|
||||
|
||||
const tempChangeListeners = []
|
||||
|
@ -190,45 +215,52 @@ const messageListeners = []
|
|||
const reactionListeners = []
|
||||
|
||||
const requestTempChange = change => {
|
||||
tempChangeListeners.forEach(listener => listener(change))
|
||||
tempChangeListeners.forEach(listener => listener(change))
|
||||
}
|
||||
|
||||
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 regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
||||
let match = message.match(regex)
|
||||
if (!match) {
|
||||
return match
|
||||
}
|
||||
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
||||
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
||||
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
||||
let match = message.match(regex)
|
||||
if (!match) {
|
||||
return match
|
||||
}
|
||||
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
||||
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
||||
}
|
||||
|
||||
const onReaction = listener => reactionListeners.push(listener)
|
||||
|
||||
onReaction(async ({ event, say }) => {
|
||||
if (event.user === sageUserId && event.reaction === 'x') {
|
||||
console.log(event)
|
||||
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
|
||||
onReaction(async ({ event }) => {
|
||||
if (event.user === sageUserId && event.reaction === 'x') {
|
||||
console.log(event)
|
||||
try {
|
||||
await app.client.chat.delete({channel: event.item.channel, ts: event.item.ts})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
hvackerBotUserId,
|
||||
temperatureChannelId,
|
||||
onAction: app.action,
|
||||
getMessage,
|
||||
updateMessage: app.client.chat.update,
|
||||
postToTechThermostatChannel,
|
||||
onTempChangeRequested: listener => tempChangeListeners.push(listener),
|
||||
onMessage: listener => messageListeners.push(listener),
|
||||
onReaction,
|
||||
encodeData,
|
||||
decodeData,
|
||||
sageUserId,
|
||||
messageSage,
|
||||
ourUsers
|
||||
}
|
||||
app,
|
||||
hvackerBotUserId,
|
||||
temperatureChannelId,
|
||||
onAction: app.action,
|
||||
getMessage,
|
||||
updateMessage: app.client.chat.update,
|
||||
postToTechThermostatChannel,
|
||||
onTempChangeRequested: listener => tempChangeListeners.push(listener),
|
||||
onMessage: listener => messageListeners.push(listener),
|
||||
onReaction,
|
||||
encodeData,
|
||||
decodeData,
|
||||
sageUserId,
|
||||
messageSage,
|
||||
messageIn,
|
||||
testMode,
|
||||
testId,
|
||||
ourUsers
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue