Hvacker/src/slack/index.js

329 lines
9.5 KiB
JavaScript
Raw Normal View History

2022-03-03 11:23:22 -05:00
const { App: SlackApp } = require('@slack/bolt')
const config = require('../config')
const { addReactions, saveGame } = require('../games/hvacoins/utils')
2022-03-03 11:23:22 -05:00
const temperatureChannelId = 'C034156CE03'
const pollingMinutes = 5
2022-03-03 11:23:22 -05:00
const pollingPeriod = 1000 * 60 * pollingMinutes
const colderEmoji = 'snowflake'
const hotterEmoji = 'fire'
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)
// })
2022-03-03 11:23:22 -05:00
} catch (e) {
console.log('Failed to initialize SlackApp', e)
2022-03-03 11:23:22 -05:00
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
2022-03-03 11:23:22 -05:00
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 = ''
}
2022-03-03 11:23:22 -05:00
await say({
text: prefix +
`Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` +
'\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' +
'At this time I am not capable of actually changing the temperature. Go bug Quade.'
})
2022-03-03 11:23:22 -05:00
}
const getMessage = async ({ channel, ts }) => app.client.conversations.history({
channel: channel,
latest: ts,
inclusive: true,
limit: 1
2022-03-03 11:23:22 -05:00
})
app.event('reaction_added', async ({ event, context, client, say }) => {
console.log('reaction_added', event)
for (const listener of reactionListeners) {
listener({ event, say })
}
2022-03-03 11:23:22 -05:00
})
const users = {
U028BMEBWBV: 'Sage',
U02U15RFK4Y: 'Adam',
U02AAB54V34: 'Houston',
U02KYLVK1GV: 'Quade',
U017PG4EL1Y: 'Max',
UTDLFGZA5: 'Tyler',
U017CB5L1K3: 'Andres',
U0344TFA7HQ: 'Hvacker',
U0X0ZQCN6: 'Caleb',
U03BBTD4CQZ: 'Fernando',
U03DF152WUV: 'Nik',
U2X0SG7BP: 'John',
UR2H5KNHY: 'Jake',
Sage: 'U028BMEBWBV',
Adam: 'U02U15RFK4Y',
Houston: 'U02AAB54V34',
Quade: 'U02KYLVK1GV',
Max: 'U017PG4EL1Y',
Tyler: 'UTDLFGZA5',
Andres: 'U017CB5L1K3',
Caleb: 'U0X0ZQCN6',
Hvacker: 'U0344TFA7HQ',
Fernando: 'U03BBTD4CQZ',
John: 'U2X0SG7BP',
Jake: 'UR2H5KNHY',
2022-03-03 11:23:22 -05:00
}
const buildSayPrepend = ({ say, prepend }) => async msg => {
if (typeof(msg) === 'string') {
return say(prepend + msg)
}
return say({
...msg,
text: prepend + msg.text
})
}
process.once('SIGINT', code => {
saveGame(true)
process.exit()
})
const activePolls = {}
const testId = 'U028BMEBWBV_TEST'
let testMode = false
2022-03-03 11:23:22 -05:00
app.event('message', async ({ event, context, client, say }) => {
if (event.subtype !== 'message_changed' && event?.text !== '!') {
console.log('message.event', {
...event,
userName: users[event.user]
})
}
if (event?.user === users.Sage) {
if (event?.text.startsWith('!')) {
if (testMode) {
await messageSage('Currently in test mode!')
}
2022-03-03 11:23:22 -05:00
}
if (event?.text === '!test') {
testMode = !testMode
await messageSage(`TestMode: ${testMode} with ID ${testId}`)
} else if (event?.text === '!notest') {
testMode = false
await messageSage(`TestMode: ${testMode}`)
2022-03-03 11:23:22 -05:00
}
if (testMode) {
event.user = testId
}
}
for (const listener of messageListeners) {
listener({ event, say })
}
if (event.user) {
console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
}
if (event.user === users.Sage && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') {
saveGame(true)
process.exit(1)
} else if (event.text === '!!restart') {
saveGame(true)
process.exit()
}
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) {
await postToTechThermostatChannel(event.text.substring(4).trim())
return
}
}
const eventText = event.text?.toLowerCase() || ''
if (eventText === '!help') {
await sendHelp(say)
return
}
2022-03-03 11:23:22 -05:00
if (!pollTriggers.includes(eventText)) {
if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.')
2022-03-03 11:23:22 -05:00
}
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 reactPosters = {}
reactions.message.reactions.forEach(r => r.users.forEach(user => {
reactPosters[user] ??= []
reactPosters[user].push(r.name)
}))
const reactCounts = {}
Object.entries(reactPosters).forEach(([id, votes]) => {
console.log(`VOTES FROM ${id}:`, votes)
votes = votes.filter(v => [goodEmoji, hotterEmoji, colderEmoji].find(emoji => v.startsWith(emoji)))
if (votes.length === 1) {
const name = votes[0].replace(/:.*/g, '')
reactCounts[name] ??= 0
reactCounts[name] += 1
}
})
console.log('REACT COUNTS', JSON.stringify(reactCounts))
2022-03-03 11:23:22 -05:00
const contentVotes = reactCounts[goodEmoji] || 0
let hotterVotes = reactCounts[hotterEmoji] || 0
let colderVotes = reactCounts[colderEmoji] || 0
console.log('before contentVotes', contentVotes)
console.log('before colderVotes', colderVotes)
console.log('before hotterVotes', hotterVotes)
if (hotterVotes > colderVotes) {
hotterVotes -= colderVotes
colderVotes = 0
} else if (colderVotes > hotterVotes) {
colderVotes -= hotterVotes
hotterVotes = 0
}
console.log('after contentVotes', contentVotes)
console.log('after colderVotes', colderVotes)
console.log('after hotterVotes', hotterVotes)
let text
if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text = `<@${users.Adam}> The people have spoken, and would like to `
text += 'raise the temperature, quack.'
requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text = `<@${users.Adam}> The people have spoken, and would like to `
text += 'lower the temperature, quack quack.'
requestTempChange('Colder')
} else {
text = `The people have spoken, and would like to `
text += 'keep the temperature as-is, quaaack.'
requestTempChange('Good')
2022-03-03 11:23:22 -05:00
}
await postToTechThermostatChannel({ text })
delete activePolls[event.channel]
}, pollingPeriod)
2022-03-03 11:23:22 -05:00
})
;(async () => {
await app.start().catch(console.error)
console.log('Slack Bolt has started')
})()
2022-03-03 11:23:22 -05:00
const postToTechThermostatChannel = async optionsOrText => {
if (optionsOrText === null || typeof optionsOrText !== 'object') {
optionsOrText = {
text: optionsOrText
2022-03-03 11:23:22 -05:00
}
}
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
2022-03-03 11:23:22 -05:00
}
const messageSage = async optionsOrText => messageIn(users.Sage, optionsOrText)
2022-03-03 11:23:22 -05:00
const messageIn = async (channel, optionsOrText) => {
if (optionsOrText === null || typeof optionsOrText !== 'object') {
optionsOrText = {
text: optionsOrText
2022-03-03 11:23:22 -05:00
}
}
return app.client.chat.postMessage({ ...optionsOrText, channel })
2022-03-03 11:23:22 -05:00
}
const startPoll = async () => {
const sent = await postToTechThermostatChannel({
text: `<!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!)'
})
await addReactions({
app,
channelId: temperatureChannelId,
timestamp: sent.ts,
reactions: [colderEmoji, hotterEmoji, goodEmoji]
})
return sent.ts
2022-03-03 11:23:22 -05:00
}
const tempChangeListeners = []
const messageListeners = []
const reactionListeners = []
const requestTempChange = change => {
tempChangeListeners.forEach(listener => listener(change))
2022-03-03 11:23:22 -05:00
}
const encodeData = (key, data) =>
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
2022-03-03 11:23:22 -05:00
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'))
2022-03-03 11:23:22 -05:00
}
const onReaction = listener => reactionListeners.push(listener)
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
onReaction(async ({ event }) => {
if (event.reaction === 'x' && (event.user === users.Sage || await channelIsIm(event.item.channel))) {
try {
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
} catch (e) {
2022-03-03 11:23:22 -05:00
}
}
2022-03-03 11:23:22 -05:00
})
module.exports = {
app,
temperatureChannelId,
onAction: app.action,
getMessage,
updateMessage: app.client.chat.update,
postToTechThermostatChannel,
onTempChangeRequested: listener => tempChangeListeners.push(listener),
onMessage: listener => messageListeners.push(listener),
onReaction,
encodeData,
decodeData,
messageSage,
messageIn,
testMode,
testId,
users,
buildSayPrepend
}