forked from zozs/a-wild-button-appears
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathclick.js
113 lines (97 loc) · 3.96 KB
/
click.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
const db = require('./db')
const slack = require('./slack')
const { DateTime } = require('luxon')
function formatTime (uuid, time, precision = 2) {
return DateTime.fromJSDate(time).diff(DateTime.fromISO(uuid), 'seconds').seconds.toFixed(precision)
}
function formatAllClicks (uuid, clicks) {
// Format every timestamp with both 2 and 3 decimal digits.
// If any 2 decimal digits strings are equal, we use the the more exact representation instead.
const allFormats = clicks.map(e => [
e.user,
formatTime(uuid, e.timestamp, 2),
formatTime(uuid, e.timestamp, 3)
])
return allFormats.map(([u, two, three], i) => allFormats.some(([_, two_, three_], j) => i !== j && two === two_) ? { u, t: three } : { u, t: two })
}
function wonMessageFormatter (uuid, clickData) {
// TODO: add block in case this was the 100, 200, etc. button.
const formatted = formatAllClicks(uuid, clickData.clicks)
const runnersUp = formatted.slice(1)
const runnersUpTexts = runnersUp.map(r => `<@${r.u}> (${r.t} s)`)
const blocks = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*A wild BUTTON appears!*'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:white_check_mark: <@${formatted[0].u}> won (${formatted[0].t} s)!`
}
}
]
if (runnersUp.length > 0) {
blocks.push({
type: 'context',
elements: [
{
type: 'mrkdwn',
text: runnersUpTexts.join(', ') + ' was close!'
}
]
})
}
const wonMessage = {
text: 'The button has a winner!',
blocks
}
return wonMessage
}
module.exports = {
async click (res, payload, asyncEventHandler) {
// launch async call to click recorder. this is done through a handler, since in
// some cases we need to launch an async call to another lambda function (when deploying
// e.g., on AWS). Running locally, we can just call clickRecorder in the same thread,
// but we ensure it is deferred to the next event loop so we acknowledge as soon as possible.
const timestamp = DateTime.fromMillis(Math.round(parseFloat(payload.actions[0].action_ts) * 1000))
await asyncEventHandler({
method: 'click',
instanceRef: payload.team.id, // TODO: currently assuming that instanceRef is always team id.
uuid: payload.actions[0].value,
user: payload.user.id,
responseUrl: payload.response_url,
timestamp: timestamp.toISO()
})
// immediately acknowledge click. we'll update message in the click recorder instead.
res.send('')
},
async clickRecorder ({ instanceRef, uuid, user, timestamp, responseUrl }) {
// record click, duplicate clicks by same user will be silently ignored.
// firstClick is true if we believe this was the first click of the button.
// it is up to the db layer to filter out duplicates and ensure all times are
// within the runner up window.
const timestampObj = DateTime.fromISO(timestamp)
const firstClick = await db.recordClick(instanceRef, uuid, user, timestampObj)
if (firstClick) {
// send an update response immediately, also update later in case db is inconsistent.
const firstClickData = await db.clickData(instanceRef, uuid)
if (firstClickData.clicks.length > 0) {
await slack.sendReplacingResponse(responseUrl, wonMessageFormatter(uuid, firstClickData))
}
}
// After this, we wait the runner up windows + some extra second for db to be consistent,
// then we update the message with the final results.
// Default is 2 + 1 seconds.
const runnerUpWindow = parseInt(process.env.RUNNER_UP_WINDOW) || 2000
const consistencyTime = parseInt(process.env.CONSISTENCY_TIME) || 1000
const waitTime = runnerUpWindow + consistencyTime
await new Promise(resolve => setTimeout(() => resolve(), waitTime))
const clickData = await db.clickData(instanceRef, uuid)
await slack.sendReplacingResponse(responseUrl, wonMessageFormatter(uuid, clickData))
}
}