Passed
Pull Request — master (#19)
by Muhammad Dyas
01:34
created

ActionHandler   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 47
eloc 129
dl 0
loc 175
rs 8.64
c 0
b 0
f 0

9 Functions

Rating   Name   Duplication   Size   Complexity  
A process 0 22 3
C startPoll 0 34 10
C saveOption 0 26 11
A getEventPollState 0 7 2
A addOptionForm 0 10 1
C recordVote 0 27 11
A closePoll 0 16 5
A closePollForm 0 13 3
A newPollOnChange 0 5 1

How to fix   Complexity   

Complexity

Complex classes like ActionHandler 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
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
2
import BaseHandler from './BaseHandler';
3
import NewPollFormCard from '../cards/NewPollFormCard';
4
import {addOptionToState, getConfigFromInput, getStateFromCard} from '../helpers/state';
5
import {callMessageApi} from '../helpers/api';
6
import {createDialogActionResponse, createStatusActionResponse} from '../helpers/response';
7
import PollCard from '../cards/PollCard';
8
import {ClosableType, MessageDialogConfig, PollFormInputs, PollState, taskEvent, Voter} from '../helpers/interfaces';
9
import AddOptionFormCard from '../cards/AddOptionFormCard';
10
import {saveVotes} from '../helpers/vote';
11
import {PROHIBITED_ICON_URL} from '../config/default';
12
import ClosePollFormCard from '../cards/ClosePollFormCard';
13
import MessageDialogCard from '../cards/MessageDialogCard';
14
import {createTask} from '../helpers/task';
15
16
/*
17
This list methods are used in the poll chat message
18
 */
19
interface PollAction {
20
  saveOption(): Promise<chatV1.Schema$Message>;
21
22
  getEventPollState(): PollState;
23
}
24
25
export default class ActionHandler extends BaseHandler implements PollAction {
26
  async process(): Promise<chatV1.Schema$Message> {
27
    const action = this.event.common?.invokedFunction;
28
    switch (action) {
29
      case 'start_poll':
30
        return await this.startPoll();
31
      case 'vote':
32
        return this.recordVote();
33
      case 'add_option_form':
34
        return this.addOptionForm();
35
      case 'add_option':
36
        return await this.saveOption();
37
      case 'show_form':
38
        const pollForm = new NewPollFormCard({topic: '', choices: []}, this.getUserTimezone()).create();
39
        return createDialogActionResponse(pollForm);
40
      case 'new_poll_on_change':
41
        return this.newPollOnChange();
42
      case 'close_poll_form':
43
        return this.closePollForm();
44
      case 'close_poll':
45
        return await this.closePoll();
46
      default:
47
        return createStatusActionResponse('Unknown action!', 'UNKNOWN');
48
    }
49
  }
50
51
  /**
52
   * Handle the custom start_poll action.
53
   *
54
   * @returns {object} Response to send back to Chat
55
   */
56
  async startPoll() {
57
    // Get the form values
58
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
59
    const config = getConfigFromInput(formValues);
60
61
    if (!config.topic || config.choices.length === 0) {
62
      // Incomplete form submitted, rerender
63
      const dialog = new NewPollFormCard(config, this.getUserTimezone()).create();
64
      return createDialogActionResponse(dialog);
65
    }
66
    if (config.closedTime) {
67
      // because previously we marked up the time with user timezone offset
68
      config.closedTime -= this.getUserTimezone()?.offset ?? 0;
69
    }
70
    const pollCardMessage = new PollCard({author: this.event.user, ...config}, this.getUserTimezone()).createMessage();
71
    const request = {
72
      parent: this.event.space?.name,
73
      requestBody: pollCardMessage,
74
    };
75
    const apiResponse = await callMessageApi('create', request);
76
    if (apiResponse.data?.name) {
77
      if (config.autoclose && config.closedTime) {
78
        const taskPayload: taskEvent = {'id': apiResponse.data.name, 'action': 'close_poll', 'type': 'TASK'};
79
        await createTask(JSON.stringify(taskPayload), config.closedTime);
80
      }
81
      return createStatusActionResponse('Poll started.', 'OK');
82
    } else {
83
      return createStatusActionResponse('Failed to start poll.', 'UNKNOWN');
84
    }
85
  }
86
87
  /**
88
   * Handle the custom vote action. Updates the state to record
89
   * the user's vote then rerenders the card.
90
   *
91
   * @returns {object} Response to send back to Chat
92
   */
93
  recordVote() {
94
    const parameters = this.event.common?.parameters;
95
    if (!(parameters?.['index'])) {
96
      throw new Error('Index Out of Bounds');
97
    }
98
    const choice = parseInt(parameters['index']);
99
    const userId = this.event.user?.name ?? '';
100
    const userName = this.event.user?.displayName ?? '';
101
    const voter: Voter = {uid: userId, name: userName};
102
    const state = this.getEventPollState();
103
104
    // Add or update the user's selected option
105
    state.votes = saveVotes(choice, voter, state.votes!, state.anon);
106
    const card = new PollCard(state, this.getUserTimezone());
107
    return {
108
      thread: this.event.message?.thread,
109
      actionResponse: {
110
        type: 'UPDATE_MESSAGE',
111
      },
112
      cardsV2: [card.createCardWithId()],
113
    };
114
  }
115
116
  /**
117
   * Opens and starts a dialog that allows users to add details about a contact.
118
   *
119
   * @returns {object} open a dialog.
120
   */
121
  addOptionForm() {
122
    const state = this.getEventPollState();
123
    const dialog = new AddOptionFormCard(state).create();
124
    return createDialogActionResponse(dialog);
125
  };
126
127
  /**
128
   * Handle add new option input to the poll state
129
   * the user's vote then rerenders the card.
130
   *
131
   * @returns {object} Response to send back to Chat
132
   */
133
  async saveOption(): Promise<chatV1.Schema$Message> {
134
    const userName = this.event.user?.displayName ?? '';
135
    const state = this.getEventPollState();
136
    const formValues = this.event.common?.formInputs;
137
    const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
138
    addOptionToState(optionValue, state, userName);
139
140
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
141
142
    const request = {
143
      name: this.event.message!.name,
144
      requestBody: cardMessage,
145
      updateMask: 'cardsV2',
146
    };
147
    const apiResponse = await callMessageApi('update', request);
148
    if (apiResponse.status === 200) {
149
      return createStatusActionResponse('Option is added', 'OK');
150
    } else {
151
      return createStatusActionResponse('Failed to add option.', 'UNKNOWN');
152
    }
153
  }
154
155
  getEventPollState(): PollState {
156
    const stateJson = getStateFromCard(this.event);
157
    if (!stateJson) {
158
      throw new ReferenceError('no valid state in the event');
159
    }
160
    return JSON.parse(stateJson);
161
  }
162
163
  async closePoll(): Promise<chatV1.Schema$Message> {
164
    const state = this.getEventPollState();
165
    state.closedTime = Date.now();
166
    state.closedBy = this.event.user?.displayName ?? '';
167
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
168
    const request = {
169
      name: this.event.message!.name,
170
      requestBody: cardMessage,
171
      updateMask: 'cardsV2',
172
    };
173
    const apiResponse = await callMessageApi('update', request);
174
    if (apiResponse.status === 200) {
175
      return createStatusActionResponse('Poll is closed', 'OK');
176
    } else {
177
      return createStatusActionResponse('Failed to close poll.', 'UNKNOWN');
178
    }
179
  }
180
181
  closePollForm() {
182
    const state = this.getEventPollState();
183
    if (state.type === ClosableType.CLOSEABLE_BY_ANYONE || state.author!.name === this.event.user?.name) {
184
      return createDialogActionResponse(new ClosePollFormCard().create());
185
    }
186
187
    const dialogConfig: MessageDialogConfig = {
188
      title: 'Sorry, you can not close this poll',
189
      message: `The poll setting restricts the ability to close the poll to only the creator(${state.author!.displayName}).`,
190
      imageUrl: PROHIBITED_ICON_URL,
191
    };
192
    return createDialogActionResponse(new MessageDialogCard(dialogConfig).create());
193
  }
194
195
  newPollOnChange() {
196
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
197
    const config = getConfigFromInput(formValues);
198
    return createDialogActionResponse(new NewPollFormCard(config, this.getUserTimezone()).create());
199
  }
200
}
201