Completed
Push — master ( 57c78c...4fcad4 )
by Muhammad Dyas
15s queued 13s
created

index.ts ➔ showConfigurationForm   F

Complexity

Conditions 34

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 19
rs 0
c 0
b 0
f 0
cc 34

How to fix   Complexity   

Complexity

Complex classes like index.ts ➔ showConfigurationForm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
// @#ts-nocheck
2
import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions';
3
4
import {
5
  buildConfigurationForm,
6
  buildOptionsFromMessage,
7
} from './config-form';
8
import {buildVoteCard} from './vote-card';
9
import {saveVotes} from './helpers/vote';
10
import {buildAddOptionForm} from './add-option-form';
11
import {callMessageApi} from './helpers/api';
12
import {addOptionToState} from './helpers/option';
13
import {buildActionResponse} from './helpers/response';
14
import {MAX_NUM_OF_OPTIONS} from './config/default';
15
import {splitMessage} from './helpers/utils';
16
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
17
import {Voter, Votes} from './helpers/interfaces';
18
19
export const app: HttpFunction = async (req, res) => {
20
  if (!(req.method === 'POST' && req.body)) {
21
    res.status(400).send('');
22
  }
23
  const buttonCard: chatV1.Schema$CardWithId = {
24
    'cardId': 'welcome-card',
25
    'card': {
26
      'sections': [
27
        {
28
          'widgets': [
29
            {
30
              'buttonList': {
31
                'buttons': [
32
                  {
33
                    'text': 'Create Poll',
34
                    'onClick': {
35
                      'action': {
36
                        'function': 'show_form',
37
                        'interaction': 'OPEN_DIALOG',
38
                        'parameters': [],
39
                      },
40
                    },
41
                  },
42
                  {
43
                    'text': 'Terms and Conditions',
44
                    'onClick': {
45
                      'openLink': {
46
                        'url': 'https://absolute-poll.yaskur.com/terms-and-condition',
47
                      },
48
                    },
49
                  },
50
                  {
51
                    'text': 'Contact Us',
52
                    'onClick': {
53
                      'openLink': {
54
                        'url': 'https://absolute-poll.yaskur.com/contact-us',
55
                      },
56
                    },
57
                  },
58
                ],
59
              },
60
            },
61
          ],
62
        },
63
      ],
64
    },
65
  };
66
  const event = req.body;
67
  console.log(event.type,
68
    event.common?.invokedFunction || event.message?.slashCommand?.commandId || event.message?.argumentText,
69
    event.user.displayName, event.user.email, event.space.type, event.space.name);
70
  console.log(JSON.stringify(event.message.cardsV2));
71
  console.log(JSON.stringify(event.user));
72
  let reply: chatV1.Schema$Message = {
73
    thread: event.message.thread,
74
    actionResponse: {
75
      type: 'NEW_MESSAGE',
76
    },
77
    text: 'Hi! To create a poll, you can use the */poll* command. \n \n' +
78
      'Alternatively, you can create poll by mentioning me with question and answers. ' +
79
      'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*',
80
  };
81
  // Dispatch slash and action events
82
  if (event.type === 'MESSAGE') {
83
    const message = event.message;
84
    if (message.slashCommand?.commandId === '1') {
85
      reply = showConfigurationForm(event);
86
    } else if (message.slashCommand?.commandId === '2') {
87
      reply = {
88
        thread: event.message.thread,
89
        actionResponse: {
90
          type: 'NEW_MESSAGE',
91
        },
92
        text: 'Hi there! I can help you create polls to enhance collaboration and efficiency ' +
93
          'in decision-making using Google Chat™.\n' +
94
          '\n' +
95
          'Below is an example commands:\n' +
96
          '`/poll` - You will need to fill out the topic and answers in the form that will be displayed.\n' +
97
          '`/poll "Which is the best country to visit" "Indonesia"` - to create a poll with ' +
98
          '"Which is the best country to visit" as the topic and "Indonesia" as the answer\n' +
99
          '\n' +
100
          'We hope you find our service useful and please don\'t hesitate to contact us ' +
101
          'if you have any questions or concerns.',
102
      };
103
    } else if (message.text) {
104
      const argument = event.message?.argumentText?.trim().toLowerCase();
105
106
      reply = {
107
        thread: event.message.thread,
108
        actionResponse: {
109
          type: 'NEW_MESSAGE',
110
        },
111
        text: 'Hi! To create a poll, you can use the */poll* command. \n \n' +
112
          'Alternatively, you can create poll by mentioning me with question and answers. ' +
113
          'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*',
114
      };
115
      const choices = splitMessage(argument);
116
      if (choices.length > 2) {
117
        const pollCard = buildVoteCard({
118
          choiceCreator: undefined,
119
          topic: choices.shift() ?? '',
120
          author: event.user,
121
          choices: choices,
122
          votes: {},
123
          anon: false,
124
          optionable: true,
125
        });
126
        const message = {
127
          cardsV2: [pollCard],
128
        };
129
        reply = {
130
          thread: event.message.thread,
131
          actionResponse: {
132
            type: 'NEW_MESSAGE',
133
          },
134
          ...message,
135
        };
136
      }
137
138
      if (argument === 'help') {
139
        reply.text = 'Hi there! I can help you create polls to enhance collaboration and efficiency ' +
140
          'in decision-making using Google Chat™.\n' +
141
          '\n' +
142
          'Below is an example commands:\n' +
143
          '`/poll` - You will need to fill out the topic and answers in the form that will be displayed.\n' +
144
          '`/poll "Which is the best country to visit" "Indonesia"` - to create a poll with ' +
145
          '"Which is the best country to visit" as the topic and "Indonesia" as the answer\n' +
146
          '\n' +
147
          'We hope you find our service useful and please don\'t hesitate to contact us ' +
148
          'if you have any questions or concerns.';
149
        reply.cardsV2 = [buttonCard];
150
      } else if (argument === 'test') {
151
        reply.text = 'test search on <a href=\'http://www.google.com\'>google</a> (https://google.com)[https://google.com]';
152
      }
153
    }
154
  } else if (event.type === 'CARD_CLICKED') {
155
    const action = event.common?.invokedFunction;
156
    if (action === 'start_poll') {
157
      reply = await startPoll(event);
158
    } else if (action === 'vote') {
159
      reply = recordVote(event);
160
    } else if (action === 'add_option_form') {
161
      reply = addOptionForm(event);
162
    } else if (action === 'add_option') {
163
      reply = await saveOption(event);
164
    } else if (action === 'show_form') {
165
      reply = showConfigurationForm(event, true);
166
    }
167
  } else if (event.type === 'ADDED_TO_SPACE') {
168
    const message: chatV1.Schema$Message = {
169
      text: undefined,
170
      cardsV2: undefined,
171
    };
172
    const spaceType = event.space.type;
173
    if (spaceType === 'ROOM') {
174
      message.text = 'Hi there! I\'d be happy to assist you in creating polls to improve collaboration and ' +
175
        'decision-making efficiency on Google Chat™.\n' +
176
        '\n' +
177
        'To create a poll, simply use the */poll* command or click on the "Create Poll" button below. ' +
178
        'You can also test our app in a direct message if you prefer.\n' +
179
        '\n' +
180
        'Alternatively, you can ' +
181
        'You can also test our app in a direct message if you prefer.\n' +
182
        '\n' +
183
        'We hope you find our service useful and please don\'t hesitate to contact us ' +
184
        'if you have any questions or concerns.';
185
    } else if (spaceType === 'DM') {
186
      message.text = 'Hey there! ' +
187
        'Before creating a poll in a group space, you can test it out here in a direct message.\n' +
188
        '\n' +
189
        'To create a poll, you can use the */poll* command or click on the "Create Poll" button below.\n' +
190
        '\n' +
191
        'Thank you for using our bot. We hope that it will prove to be a valuable tool for you and your team.\n' +
192
        '\n' +
193
        'Don\'t hesitate to reach out if you have any questions or concerns in the future.' +
194
        ' We are always here to help you and your team';
195
    }
196
197
    message.cardsV2 = [buttonCard];
198
199
    reply = {
200
      actionResponse: {
201
        type: 'NEW_MESSAGE',
202
      },
203
      ...message,
204
    };
205
  }
206
  res.json(reply);
207
};
208
209
/**
210
 * Handles the slash command to display the config form.
211
 *
212
 * @param {object} event - chat event
213
 * @param {boolean} isBlank - fill with text from message or note
214
 * @returns {object} Response to send back to Chat
215
 */
216
function showConfigurationForm(event: chatV1.Schema$DeprecatedEvent, isBlank = false) {
217
  // Seed the topic with any text after the slash command
218
  const message = isBlank ? '' : event.message?.argumentText?.trim() ?? '';
219
  const options = buildOptionsFromMessage(message);
220
  const dialog = buildConfigurationForm(options);
221
  return {
222
    actionResponse: {
223
      type: 'DIALOG',
224
      dialogAction: {
225
        dialog: {
226
          body: dialog,
227
        },
228
      },
229
    },
230
  };
231
}
232
233
/**
234
 * Handle the custom start_poll action.
235
 *
236
 * @param {object} event - chat event
237
 * @returns {object} Response to send back to Chat
238
 */
239
async function startPoll(event: chatV1.Schema$DeprecatedEvent) {
240
  // Get the form values
241
  const formValues = event.common?.formInputs;
242
  const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? '';
243
  const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1';
244
  const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1';
245
  const choices = [];
246
  const votes: Votes = {};
247
248
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
249
    const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim();
250
    if (choice) {
251
      choices.push(choice);
252
      votes[i] = [];
253
    }
254
  }
255
256
  if (!topic || choices.length === 0) {
257
    // Incomplete form submitted, rerender
258
    const dialog = buildConfigurationForm({
259
      topic,
260
      choices,
261
    });
262
    return {
263
      actionResponse: {
264
        type: 'DIALOG',
265
        dialogAction: {
266
          dialog: {
267
            body: dialog,
268
          },
269
        },
270
      },
271
    };
272
  }
273
  const pollCard = buildVoteCard({
274
    topic: topic, choiceCreator: undefined,
275
    author: event.user,
276
    choices: choices,
277
    votes: votes,
278
    anon: isAnonymous,
279
    optionable: allowAddOption,
280
  });
281
  // Valid configuration, build the voting card to display in the space
282
  const message = {
283
    cardsV2: [pollCard],
284
  };
285
  const request = {
286
    parent: event.space?.name,
287
    requestBody: message,
288
  };
289
  const apiResponse = await callMessageApi('create', request);
290
  if (apiResponse) {
291
    return buildActionResponse('Poll started.', 'OK');
292
  } else {
293
    return buildActionResponse('Failed to start poll.', 'UNKNOWN');
294
  }
295
}
296
297
/**
298
 * Handle the custom vote action. Updates the state to record
299
 * the user's vote then rerenders the card.
300
 *
301
 * @param {object} event - chat event
302
 * @returns {object} Response to send back to Chat
303
 */
304
function recordVote(event: chatV1.Schema$DeprecatedEvent) {
305
  const parameters = event.common?.parameters;
306
  if (!(parameters?.['index'])) {
307
    throw new Error('Index Out of Bounds');
308
  }
309
  const choice = parseInt(parameters['index']);
310
  const userId = event.user?.name ?? '';
311
  const userName = event.user?.displayName ?? '';
312
  const voter: Voter = {uid: userId, name: userName};
313
  const state = JSON.parse(parameters['state']);
314
315
  // Add or update the user's selected option
316
  state.votes = saveVotes(choice, voter, state.votes, state.anon);
317
318
  const card = buildVoteCard(state);
319
  return {
320
    thread: event.message?.thread,
321
    actionResponse: {
322
      type: 'UPDATE_MESSAGE',
323
    },
324
    cardsV2: [card],
325
  };
326
}
327
328
/**
329
 * Opens and starts a dialog that allows users to add details about a contact.
330
 *
331
 * @param {object} event the event object from Google Chat.
332
 *
333
 * @returns {object} open a dialog.
334
 */
335
function addOptionForm(event: chatV1.Schema$DeprecatedEvent) {
336
  const card = event.message!.cardsV2?.[0]?.card;
337
  // @ts-ignore: because too long
338
  const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? '';
339
  const state = JSON.parse(stateJson);
340
  const dialog = buildAddOptionForm(state);
341
  return {
342
    actionResponse: {
343
      type: 'DIALOG',
344
      dialogAction: {
345
        dialog: {
346
          body: dialog,
347
        },
348
      },
349
    },
350
  };
351
}
352
;
353
354
/**
355
 * Handle the custom vote action. Updates the state to record
356
 * the user's vote then rerenders the card.
357
 *
358
 * @param {chatV1.Schema$DeprecatedEvent} event - chat event
359
 * @returns {object} Response to send back to Chat
360
 */
361
async function saveOption(event: chatV1.Schema$DeprecatedEvent) {
362
  const userName = event.user?.displayName ?? '';
363
  const state = getEventPollState(event);
364
  const formValues = event.common?.formInputs;
365
  const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
366
  addOptionToState(optionValue, state, userName);
367
368
  const card = buildVoteCard(state);
369
  const message = {
370
    cardsV2: [card],
371
  };
372
  const request = {
373
    name: event.message!.name,
374
    requestBody: message,
375
    updateMask: 'cardsV2',
376
  };
377
  const apiResponse = await callMessageApi('update', request);
378
  if (apiResponse) {
379
    return buildActionResponse('Option is added', 'OK');
380
  } else {
381
    return buildActionResponse('Failed to add option.', 'UNKNOWN');
382
  }
383
}
384
385
function getEventPollState(event: chatV1.Schema$DeprecatedEvent) {
386
  const parameters = event.common?.parameters;
387
  const state = parameters?.['state'];
388
  if (!state) {
389
    throw new ReferenceError('no valid state in the event');
390
  }
391
  return JSON.parse(state);
392
}
393