-
Notifications
You must be signed in to change notification settings - Fork 0
/
Code.js
515 lines (389 loc) · 13.1 KB
/
Code.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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
// GoalKeeper app for slack...
// 2018-03-04 - Shaun L. Cloherty <[email protected]>
function doPost(e) {
// HTTP POST endpoint
if (e.parameter.hasOwnProperty("payload")) {
// this is an interactive message... payload is json
var response = msgHandler(JSON.parse(e.parameter.payload));
} else {
// this is a slash command
var response = cmdHandler(e.parameter);
}
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
function msgHandler(payload) {
// handler for slack interactive messages
//
// POST payloads from slack interactive messages contain the following:
//
// payload = {
// type: "interactive_message",
// actions: [{name: "~", type: "~", value: "~"}],
// callback_id: "~",
// team: {id: "Txxxxxxxx",domain: "~"},
// channel: {id: "Cxxxxxxxx", name: "~"},
// user: {id: "Uxxxxxxxx", name: "~"},
// action_ts: "~",
// message_ts: "~",
// attachment_id: "~",
// token: "~",
// is_app_unfurl: true/false,
// response_url: "~",
// trigger_id: "~"
// }
if (payload.token != slackToken()) {
return mkErrorMsg("Verification failed.");
}
var msg = {
response_type: "ephemeral",
text: "Ok, got it!"
};
// adduser() can take a while, longer than the 3s slack allows for
// a response. To avoid the user getting a timeout/failure message,
// here we respond straight away, and *then* call addUser().
//
// I don't think this is a sanctioned use of the response_url... we
// should be responding with an empty HTTP 200, but Google's Apps
// Script is synchronous so I'm fudging it
var response = postToUrl(payload.response_url,msg);
addUser(payload.user.id,payload.user.name);
// msg = {
// response_type: "ephemeral",
// text: "",
// attachment: {
// fallback: "Connected!",
// color: "good",
// text: "Connected!",
// mrkdn_in: [ "text" ]
// }
// };
//
// response = postToUrl(payload.response_url,msg);
// return mkGeneralMsg("Ok, got it!");
}
function cmdHandler(payload) {
// handler for slack slash commands
//
// POST payloads from slack slash commands contain the following:
//
// payload = {
// user_name: "~"
// trigger_id: "~"
// user_id: "Uxxxxxxxx"
// team_id: "Txxxxxxxx"
// response_url: "~"
// channel_name: "~"
// token: "~"
// team_domain: "~"
// command: "~"
// channel_id: "Cxxxxxxxx"
// text: ""
// }
if (payload.token != slackToken()) {
return mkErrorMsg("Verification failed.");
}
var uid = payload.user_id;
var uname = payload.user_name;
var args = payload.text;
switch (payload.command) {
case "/goal":
return goalHandler(uid,uname,args);
case "/score":
return scoreHandler(uid,uname,args);
}
// shouldn't be possible to end up here...
return mkGeneralMsg("Got POST on "+ new Date() + " for " + payload.command + ".");
}
function goalHandler(uid,uname,args) {
// handler for /goal commands
// possible variants:
//
// /goal <-- return current goal (user only, not displayed in channel)
// /goal @user <-- return current goal for @user (user only, not in channel)
// /goal new goal <-- set current goal to 'new goal' (posts to channel/webhook)
// /goal @user new goal <-- set current goal for @user (shows in channel)? [NOT SUPPORTED]
//
// /goal help
// /goal connect
//
// so, args should be a string like: "<@Uxxxxxxxx|name> Some new goal."
args = parseArgs(args); // uid, uname, body (either an action or a new goal)
if (args.body) {
// check for supported actions...
var action = [/help/,/connect/]; // regexp for supported actions
var idx = action.reduce(function(prev,re,i) {
if (re.test(args.body)) {
return i;
}
return prev;
},null);
switch(idx) {
case 0: // help
// return help msg
return mkHelpMsg();
case 1: // connect
// return connect msg/prompt?
if (!getUser(uid)) {
return mkConnectMsg();
} else {
return mkErrorMsg("You're already connected... try `/goal help` to get started.");
}
}
}
// check user is known to us...
if (!getUser(uid)) {
return mkUserErrorMsg();
}
// if we end up here, args.body is either empty or contains a new goal...
if (args.body) {
// setting goal
if (args.uid && args.uid != uid) {
return mkErrorMsg("You can't set goals for <@" + args.uid + ">.");
}
// set goal in Google sheet
return setCurrentGoal(uid,args.body);
} else {
// querying goal
if (args.uid) {
uid = args.uid;
}
// get goal from Google sheet
return getCurrentGoal(uid);
}
}
function testGoalHandler() {
// test goalHandler()
var uid = "Uxxxxxxxx";
var uname = "nobody";
// usage: /goal help
var args = "help";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
var result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// usage: /goal connect
args = "connect";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// usage: /goal
args = "";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// usage; /goal @uname
args = "<@" + uid + "|" + uname + "> ";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// usage: /goal lorem ipsum
args = "lorem ipsum";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// usage: /goal @uname lorem ipsum
args = "<@" + uid + "|" + uname + "> lorem ipsum";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
// test unknown user...
uid = "UUnknownUser";
uname = "noname";
Logger.log("Testing goalHandler(%s,%s,%s)",uid,uname,args);
result = goalHandler(uid,uname,args);
Logger.log("- %s.",result);
}
function scoreHandler(uid,uname,args) {
// handler for /score commands
// possible variants:
//
// /score <-- return current users score (user only, not in channel)
// /score @user <-- return the score for @user (user only, not in channel)
// /score @user score <-- set the score for @user (note: cannot set your own score) (shows in channel)
//
// /score help
return mkGeneralMsg("Who's keeping score?");
}
function parseArgs(args) {
// parse slash command arguments
// args is likely a string like: "<@Uxxxxxxxx|name> Some new goal."
//defaults
var uid = null;
var uname = null;
var body = null;
var re = /.*(<@U.*>).*/; // regexp, match <@Uxxxxxxxx|xxxxxxxx>
if (re.test(args)) {
re = /.*<@(U\w+)\|(\w+)>\s*(.*)?/; // regexp
var tokens = re.exec(args);
uid = tokens[1];
uname = tokens[2];
if (typeof(tokens[3]) != "undefined") {
body = tokens[3];
}
} else {
re = /\s*(.*)?/; // regexp
var tokens = re.exec(args);
if (typeof(tokens[1]) != "undefined") {
body = tokens[1];
}
}
return ({uid: uid, uname: uname, body: body});
}
function getUser(uid) {
// check that uid is known to us
var ss = SpreadsheetApp.openById(sheetId());
var s = ss.getSheetByName("Sheet1");
var row = getRowByColumn(s,["Slack UID"],[uid])[0]; // first matching entry
return (typeof(row) != "undefined"); // return true for know users
}
function testGetUser() {
// test getUser()
// known user
uid = "Uxxxxxxxx";
Logger.log("Testing getUser(%s)",uid);
var result = getUser(uid);
Logger.log("- %s.",result);
// unknown user
uid = "UUnknownUser";
Logger.log("Testing getUser(%s)",uid);
var result = getUser(uid);
Logger.log("- %s.",result);
}
function addUser(uid,uname) { // OK
// add a new user and return backend settings
var ss = SpreadsheetApp.openById(sheetId());
// 1. find candidate entry on the score sheet
var s = ss.getSheetByName("Sheet1");
var row = getRowByColumn(s,["Slack UID"],[uid])[0]; // first matching entry
if (row != null) { // FIXME: row is actually 'undefined'
// user exists...?
return row;
}
// 2. user is unknown... but use some huristics here to see if we have any close matches
Logger.log("User %s (%s) is unknown.",uid,uname);
// loop over known writers and see if any "match" the supplied slack user name
var col = getColumnByName(s,["Writer"]);
var range = s.getDataRange();
var nRows = range.getHeight();
writerloop:
for (row = 1; row <= nRows; row++) { // getHeight()
var writer = range.getCell(row,col).getDisplayValue(); // col is the "Writer" column
if (writer.length === 0) {
// empty cell...
continue
}
var re = new RegExp(".*"+writer+".*","i"); // case insensitive
if (re.test(uname)) {
// writer might be our new/unknown user...
//
// 1. check that this writers uid field is empty
// 2. if so, appropriate the existing record by assigning the current uid to this writer,
var uid_ = getRow(s,row,["Slack UID"])[0];
Logger.log("User %s (%s) might be %s (uid:%s).",uid,uname,writer,uid_);
if (uid_.length === 0) {
Logger.log("Ok. Appropriating %s to be %s (%s).",writer,uid,uname);
setRow(s,row,["Slack UID"],[uid]);
break writerloop;
}
}
}
row = getRowByColumn(s,["Slack UID"],[uid])[0]; // first matching entry
if (row == null) { // FIXME: row is 'undefined'
// no entry found... create a new row
s.insertRowAfter(nRows); // append row at the bottom (inherits formatting)
row = nRows + 1;
setRow(s,row,["Slack UID","Writer"],[uid,uname]);
}
// 3. find/create the users history sheet
var h = ss.getSheetByName(uid);
if (h == null) {
// create the users history sheet...
// var template = ss.getSheetByName('Template');
// ss.insertSheet(uid, {template: template});
ss.insertSheet(uid);
h = ss.getSheetByName(uid);
h.appendRow(["Date","Goal","Score"]); // column headings
}
// copy any existing goal to the history sheet
h.insertRowAfter(h.getLastRow()); // append row at the bottom (inherits formatting)
setRow(h,h.getLastRow()+1,["Date","Goal"],getRow(s,row,["Date","Goal"]));
return row;
}
function testAddUser() {
// test addUser()
var ss = SpreadsheetApp.openById(sheetId());
uid = "Uxxxxxxxx";
uname = "nobody";
Logger.log("Testing addUser(%s,%s,%s)",ss.getName(),uid,uname);
var result = addUser(ss,uid,uname);
Logger.log("- %s.",result);
}
function setCurrentGoal(uid,goal) {
var ss = SpreadsheetApp.openById(sheetId());
SpreadsheetApp.setActiveSpreadsheet(ss); // handy...?
var s = ss.getSheetByName("Sheet1"); // FIXME
var row = getRowByColumn(s,["Slack UID"],[uid])[0];
if (row == null) {
return mkUserErrorMsg(); // shouldn't ever end up here!
}
setRow(s,row,["Date","Goal"],[new Date(),goal]);
// update the history sheet
var h = ss.getSheetByName(uid);
h.insertRowAfter(h.getLastRow()); // append row at the bottom (inherits formatting)
setRow(h,h.getLastRow()+1,["Date","Goal"],[new Date(),goal]);
// post msg to webhook... in_channel - goes to everyone
var url = webhookUrl();
if (url) {
var msg = Utilities.formatString("<@%s> set a new goal: %s",uid,goal);
postToUrl(url,mkGeneralMsg(msg));
}
return mkGeneralMsg("Ok, got it!"); // ephemeral - goes to the user only
}
function testSetCurrentGoal() {
// test setCurrentGoal()
var ss = SpreadsheetApp.openById(sheetId());
s = ss.getSheetByName("Sheet1");
var row = getRowByColumn(s,["Writer"],["nobody"]);
var uid = getRow(s,row,["Slack UID"]);
var goal = "lorem ipsum";
// known user
Logger.log("Testing setCurrentGoal(%s,%s)",uid,goal);
var result = setCurrentGoal(uid,goal);
Logger.log("- %s",result);
// unknown user
uid = "UUnknownUser";
Logger.log("Testing setCurrentGoal(%s,%s)",uid,goal);
var result = setCurrentGoal(uid,goal);
Logger.log("- %s",result);
}
function getCurrentGoal(uid) {
var ss = SpreadsheetApp.openById(sheetId());
SpreadsheetApp.setActiveSpreadsheet(ss); // handy...?
var s = ss.getSheetByName("Sheet1"); // FIXME
var row = getRowByColumn(s,["Slack UID"],[uid])[0];
if (row == null) {
return mkGeneralMsg("I don't know <@" + uid + ">."); // uid isn't known to us...
}
var goal = getRow(s,row,["Goal","Date"]);
var msg = "Goal for <@" + uid + ">: " + goal[0];
if (goal[1]) {
var days = Math.floor((new Date() - goal[1])/(24*60*60*1000)); // converts milliseconds to days
msg = msg + " (set " + days + " days ago)";
}
// msg = msg + ".";
return mkGeneralMsg(msg);
}
function testGetCurrentGoal() {
// test getCurrentGoal()
var ss = SpreadsheetApp.openById(sheetId());
s = ss.getSheetByName("Sheet1");
// known user
var uid = "Uxxxxxxxx";
Logger.log("Testing getCurrentGoal(%s)",uid);
var result = getCurrentGoal(uid);
Logger.log("- %s",result);
// unknown user
uid = "UUnknownUser";
Logger.log("Testing getCurrentGoal(%s)",uid);
result = getCurrentGoal(uid);
Logger.log("- %s",result);
}