forked from romseymutualaid/helpRequests
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathslack_wrapper.gs
282 lines (222 loc) · 10.7 KB
/
slack_wrapper.gs
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
class SlackEventWrapper {
// Wrapper for slack doPost events
constructor(e){
// declaire the entire structure (for readability)
this.token=null; // slack app verification token string
this.teamid=null; // slack workspace id
this.type=null; // describes the high level type of event (slash command, interactive message, ...)
this.subtype=null; // describes the lower level type of event (slash command name, interactive message subtype, ...)
this.argsString=null; // temporary var for backwards compatibility
this.args={
channelid:null, // channel_id that event originates from
userid:null, // user_id from whom the event originates
username:null, // (optional) user_name associated to this.userid
response_url:null, // POST url to provide delayed response to user
trigger_id:null, // needed to generate interactive messages in response to event
uniqueid:null, // (optional) help request number
mention:{str:null, userid:null, username:null}, // (optional) markdown-formatted mention name
more:null // (optional) space for extra arguments
};
}
parseEvent(e){
if (typeof e !== 'undefined'){
// extract message body
var par = e.parameter;
// parse according to the nature of the doPost event
var payload = par.payload;
if (payload){ // if payload exists, this is a doPost event from a slack interactive component
this.parseEventInteractiveMessage(JSON.parse(payload));
} else{ // else, this is a doPost event from a slack slash command
this.parseEventSlashCommand(par);
}
// quick check event authenticity
var authenticityCheck = this.checkAuthenticity();
if (!authenticityCheck.code){
return authenticityCheck;
}
// quick check event syntax
var syntaxCheck = this.checkSyntax();
if (!syntaxCheck.code){
return syntaxCheck;
}
return {code:true, msg:''}; // if all good, return success object
}
}
parseEventInteractiveMessage(par){
this.token = par.token;
this.teamid = par.team.id;
this.type = par.type;
this.subtype = par.view.callback_id;
var metadata_parsed = JSON.parse(par.view.private_metadata);
this.args.channelid = metadata_parsed.channelid;
this.args.userid = par.user.id;
this.args.response_url = metadata_parsed.response_url;
this.args.uniqueid = metadata_parsed.uniqueid;
this.args.more = par.view.state.values;
}
parseEventSlashCommand(par){
this.token = par.token;
this.teamid = par.team_id;
this.type = 'command';
this.subtype = par.command;
this.args.channelid = par.channel_id;
this.args.userid = par.user_id;
this.args.username = par.user_name;
this.args.response_url = par.response_url;
this.args.trigger_id = par.trigger_id;
this.argsString=par.text;
this.parseEventSlashCommandTxt(par.text); // populates this.args
}
parseEventSlashCommandTxt(txt){
// parse txt string and store parsed values in this.args
if(txt) {
var args = txt.split(' ');
this.args.uniqueid = args[0]; // if uniqueid is specified, it is always the first argument
this.args.mention.str = args[1]; // if user mention is specified, it is always the second argument
}
}
checkAuthenticity(){
// fetch validation variables
var globvar = globalVariables();
var teamid_true = globvar['TEAM_ID'];
var token_true = PropertiesService.getScriptProperties().getProperty('VERIFICATION_TOKEN'); // expected verification token that accompanies slack API request
// initialise return message
var output = {code:false, msg:''};
// check token
if(!token_true){ // check that token_true has been set in script properties
output.msg = 'error: VERIFICATION_TOKEN is not set in script properties. The command will not run. Please contact the web app developer.';
return output;
}
if (this.token !== token_true) {
output.msg = 'error: Invalid token ' + this.token + ' . The command will not run. Please contact the web app developer.';
return output;
}
// check request originates from our slack workspace
if (this.teamid != teamid_true){
output.msg = 'error: You are sending your command from an unauthorised slack workspace.';
return output;
}
output.code=true;
return output;
}
slackCmd2FctName(){
// match this.subtype with the function it is meant to call
var slackCmd2FctName = globalVariables()['SLACKCMD_TO_FUNCTIONNAME']; // this is where all the key-value pairs (event.subtype - functionName) are stored
if (!slackCmd2FctName.hasOwnProperty(this.subtype)){ // if no key is found for this.subtype, return error
return false;
}
return slackCmd2FctName[this.subtype];
}
checkSyntax(){
// check syntax of this.type, this.subtype, and this.args depending on function to be called
// initialise output variable
var output = {code:false, msg:''};
// check this.type
var accepted_types = ['view_submission', 'command'];
if(accepted_types.indexOf(this.type) < 0){ // if this.type does not match any accepted_types, return error
output.msg = 'error: I can\'t handle the event type "'+this.type+'".';
return output;
}
// match doPost this.subtype with the function it is meant to call
var fctName = this.slackCmd2FctName();
if (!fctName){
output.msg = 'error: Sorry, the `' + this.subtype + '` command is not currently supported.';
return output;
}
// check that function associated to this.subtype exists in global scope
if (!GlobalFuncHandle[fctName]){
output.msg = 'error: Sorry, the `' + this.subtype + '` command is not properly connected on the server. Please contact the web app developer.';
return output;
}
// check argument syntax for fctName
return this.checkArgSyntaxRegexp(fctName);
}
checkArgSyntaxRegexp(fctname){
// checkArgSyntaxRegexp: check that a particular function fctname has all the correct args by regexp matching
// load global variables
var globvar = globalVariables();
var mod_userid = globvar['MOD_USERID'];
var mention_mod = '<@'+mod_userid+'>';
// define all args to check, the functions where they are expected, the regexp they should match and all possible error messages
var syntax_object={
"uniqueid":{
arg:this.args.uniqueid,
regexp:"^[0-9]{4}$",
fcts:['assign', 'volunteer', 'cancel', 'done_send_modal','done_process_modal'], // functions this argument is expected in
fail_msg_empty:'error: You must provide the request number present in the help request message (example: `/volunteer 9999`). '+
'You appear to have not typed any number. If the issue persists, contact ' + mention_mod + '.',
fail_msg_nomatch:'error: The request number `'+this.args.uniqueid+'` does not appear to be a 4-digit number as expected. '+
'Please specify a correct request number. Example: `/volunteer 9999`.'
},
"mention":{
arg:this.args.mention.str,
regexp:"<@(U[A-Z0-9]+)\\|?(.*)>",
fcts:['assign'],
fail_msg_empty:'error: You must mention a user that the command applies to. Example: `/assign 9999 ' + mention_mod + '`.'+
'You appear to have not mentioned anyone. If the issue persists, please contact ' + mention_mod + '.',
fail_msg_nomatch:'error: I did not recognise the user `'+this.args.mention.str+'` you specified. Please specify the user by their mention name. Example: `/assign 9999 ' + mention_mod + '`.'
}
};
// initialise output object
var output = {code:false, msg:''};
// iterate check over all potential arguments in syntax_object
Object.keys(syntax_object).forEach(function(key,index) { // iterate over the object properties of cmd_state_machine.command[cmd].status
// key: the name of the object property
// move to next iteration (i.e. next arg to check) if fctname does not expect the argument syntax_object[key]
if(syntax_object[key].fcts.indexOf(fctname) < 0){
return;
}
// if argument is expected, check syntax. If wrong syntax, append appropriate error message. If correct syntax and if relevant, do some parsing.
if (!syntax_object[key].arg || syntax_object[key].arg == ''){ // personalise error message if arg was not specified at all
output.msg += '\n' + syntax_object[key].fail_msg_empty;
} else{
// regexp match arg
var re = new RegExp(syntax_object[key].regexp);
var re_match = re.exec(syntax_object[key].arg); // RegExp.exec returns array if match (null if not). First element is matched string, following elements are matched groupings.
if (!re_match){ // if arg did not match syntax, add to error message
output.msg += '\n' + syntax_object[key].fail_msg_nomatch;
} else { // or do some optional parsing if successful
if (key === 'mention'){ // parse userid and username from user mention string
this.args.mention.userid = re_match[1];
this.args.mention.username = re_match[2];
}
}
}
}, this); // make sure to pass this in forEach to maintain scope
// format output variable
if (output.msg !== ''){ // if any error was picked up, wrap
output.msg = 'I wasn\'t able to process your command for the following reasons:' + output.msg;
} else{
output.code = true;
}
return output;
}
handleEvent (){
// handle slack doPost events
// match doPost event.subtype with the function it is meant to call
var fctName = this.slackCmd2FctName();
if (!fctName){ // this check is already done in parsing. added here in case corruption occurs during event queuing
return contentServerJsonReply('error: Sorry, the `' + this.subtype + '` command is not currently supported.');
}
// check command validity
this.checkCmdValidity();
// Process Command
if (globalVariables()["ASYNC_FUNCTIONS"].indexOf(fctName) != -1){
// Handle Asyc
if(this.subtype==='done_modal'){
var immediateReturnMessage = null; // modal requires a blank HTTP 200 OK immediate response to close
} else{
var immediateReturnMessage = "Thank you for your message. I\'m a poor bot so please be patient... it should take me up to a few minutes to get back to you...";
}
var reply_url = this.args.response_url;
return processFunctionAsync(
fctName, this.args, reply_url, immediateReturnMessage);
} else {
// Handle Sync
return processFunctionSync(fctName, this.args);
}
}
checkCmdValidity(){
//**** todo ****//
}
}