• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-line-bot / 6841960699

12 Nov 2023 04:53PM UTC coverage: 89.786% (-0.2%) from 89.964%
6841960699

Pull #372

github

MrOrz
refactor(webhook): remove unused import
Pull Request #372: Simplify postback handler signatures

488 of 582 branches covered (0.0%)

Branch coverage included in aggregate %.

55 of 57 new or added lines in 12 files covered. (96.49%)

1 existing line in 1 file now uncovered.

980 of 1053 relevant lines covered (93.07%)

12.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.39
/src/webhook/index.ts
1
import { t } from 'ttag';
2
import Router from 'koa-router';
3

4
import rollbar from 'src/lib/rollbar';
5
import ga from 'src/lib/ga';
6
import redis from 'src/lib/redisClient';
7
import { groupEventQueue, expiredGroupEventQueue } from 'src/lib/queues';
8

9
import lineClient from './lineClient';
10
import checkSignatureAndParse from './checkSignatureAndParse';
11
import handleInput from './handleInput';
12
import handlePostback from './handlePostback';
13
import GroupHandler from './handlers/groupHandler';
14
import {
15
  createGreetingMessage,
16
  createTutorialMessage,
17
} from './handlers/tutorial';
18
import processMedia from './handlers/processMedia';
19
import UserSettings from '../database/models/userSettings';
20
import { Request } from 'koa';
21
import { WebhookEvent } from '@line/bot-sdk';
22
import { Result } from 'src/types/result';
23
import {
24
  ChatbotEvent,
25
  Context,
26
  PostbackActionData,
27
} from 'src/types/chatbotState';
28

29
const userIdBlacklist = (process.env.USERID_BLACKLIST || '').split(',');
1✔
30

31
const singleUserHandler = async (
1✔
32
  req: Request,
33
  /** @deprecated: Just use webhookEvent.type, which enables narrowing */
34
  type: WebhookEventType,
35
  replyToken: string,
36
  timeout: number,
37
  userId: string,
38
  webhookEvent: WebhookEvent
39
) => {
40
  // reply before timeout
41
  // the reply token becomes invalid after a certain period of time
42
  // https://developers.line.biz/en/reference/messaging-api/#send-reply-message
43
  let isReplied = false;
6✔
44
  const messageBotIsBusy = [
6✔
45
    {
46
      type: 'text',
47
      text: t`Line bot is busy, or we cannot handle this message. Maybe you can try again a few minutes later.`,
48
    },
49
  ];
50
  const timerId = setTimeout(function () {
6✔
51
    isReplied = true;
×
52
    console.log(
×
53
      `[LOG] Timeout ${JSON.stringify({
54
        userId,
55
        ...webhookEvent,
56
      })}\n`
57
    );
58
    lineClient.post('/message/reply', {
×
59
      replyToken,
60
      messages: messageBotIsBusy,
61
    });
62
  }, timeout);
63

64
  if (userIdBlacklist.indexOf(userId) !== -1) {
6!
65
    // User blacklist
66
    console.log(
×
67
      `[LOG] Blocked user INPUT =\n${JSON.stringify({
68
        userId,
69
        ...webhookEvent,
70
      })}\n`
71
    );
72
    clearTimeout(timerId);
×
73
    return;
×
74
  }
75

76
  // Set default result
77
  //
78
  let result: Result = {
6✔
79
    context: { data: {} },
80
    replies: [
81
      {
82
        type: 'text',
83
        text: t`I cannot understand messages other than text.`,
84
      },
85
    ],
86
  };
87

88
  // Handle follow/unfollow event
89
  if (webhookEvent.type === 'follow') {
6✔
90
    await UserSettings.setAllowNewReplyUpdate(userId, true);
4✔
91

92
    if (process.env.RUMORS_LINE_BOT_URL) {
4✔
93
      const data = { sessionId: Date.now() };
3✔
94
      result = {
3✔
95
        context: { data },
96
        replies: [
97
          createGreetingMessage(),
98
          createTutorialMessage(data.sessionId),
99
        ],
100
      };
101

102
      const visitor = ga(userId, 'TUTORIAL');
3✔
103
      visitor.event({
3✔
104
        ec: 'Tutorial',
105
        ea: 'Step',
106
        el: 'ON_BOARDING',
107
      });
108
      visitor.send();
3✔
109
    } else {
110
      clearTimeout(timerId);
1✔
111
      return;
1✔
112
    }
113
  } else if (webhookEvent.type === 'unfollow') {
2✔
114
    await UserSettings.setAllowNewReplyUpdate(userId, false);
1✔
115
    clearTimeout(timerId);
1✔
116
    return;
1✔
117
  }
118

119
  const context = (await redis.get(userId)) || {};
4✔
120
  // React to certain type of events
121
  //
122
  if (webhookEvent.type === 'message' && webhookEvent.message.type === 'text') {
4!
123
    // normalized "input"
124
    const input = webhookEvent.message.text;
×
125

126
    // Debugging: type 'RESET' to reset user's context and start all over.
127
    //
128
    if (input === 'RESET') {
×
129
      redis.del(userId);
×
130
      clearTimeout(timerId);
×
131
      return;
×
132
    }
133

134
    result = await processText(
×
135
      context,
136
      { ...webhookEvent, input },
137
      userId,
138
      req
139
    );
140
  } else if (
4!
141
    webhookEvent.type === 'message' &&
4!
142
    webhookEvent.message.type !== 'text'
143
  ) {
NEW
144
    result = await processMedia(webhookEvent, userId);
×
145
  } else if (webhookEvent.type === 'message') {
4!
146
    // Track other message type send by user
147
    ga(userId)
×
148
      .event({
149
        ec: 'UserInput',
150
        ea: 'MessageType',
151
        el: webhookEvent.message.type,
152
      })
153
      .send();
154
  } else if (webhookEvent.type === 'postback') {
4!
UNCOV
155
    const postbackData = JSON.parse(
×
156
      webhookEvent.postback.data
157
    ) as PostbackActionData<unknown>;
158

159
    // Handle the case when user context in redis is expired
160
    if (!context.data) {
×
161
      lineClient.post('/message/reply', {
×
162
        replyToken,
163
        messages: [
164
          {
165
            type: 'text',
166
            text: '🚧 ' + t`Sorry, the button is expired.`,
167
          },
168
        ],
169
      });
170
      clearTimeout(timerId);
×
171
      return;
×
172
    }
173

174
    // When the postback is expired,
175
    // i.e. If other new messages have been sent before pressing buttons,
176
    // tell the user about the expiry of buttons
177
    //
178
    if (postbackData.sessionId !== context.data.sessionId) {
×
179
      console.log('Previous button pressed.');
×
180
      lineClient.post('/message/reply', {
×
181
        replyToken,
182
        messages: [
183
          {
184
            type: 'text',
185
            text:
186
              '🚧 ' +
187
              t`You are currently searching for another message, buttons from previous search sessions do not work now.`,
188
          },
189
        ],
190
      });
191
      clearTimeout(timerId);
×
192
      return;
×
193
    }
194

NEW
195
    result = await handlePostback(context.data, postbackData, userId);
×
196
  }
197

198
  if (isReplied) {
4!
199
    console.log('[LOG] reply & context setup aborted');
×
200
    return;
×
201
  }
202
  clearTimeout(timerId);
4✔
203

204
  console.log(
4✔
205
    JSON.stringify({
206
      CONTEXT: context,
207
      INPUT: { userId, ...webhookEvent },
208
      OUTPUT: result,
209
    })
210
  );
211

212
  // Send replies. Does not need to wait for lineClient's callbacks.
213
  // lineClient's callback does error handling by itself.
214
  //
215
  lineClient.post('/message/reply', {
4✔
216
    replyToken,
217
    messages: result.replies,
218
  });
219

220
  // Set context
221
  //
222
  await redis.set(userId, result.context);
4✔
223
};
224

225
async function processText(
226
  context: { data: Partial<Context> },
227
  event: ChatbotEvent,
228
  userId: string,
229
  req: Request
230
): Promise<Result> {
231
  let result: Result;
232
  try {
×
233
    result = await handleInput(context, event, userId);
×
234
    if (!result.replies) {
×
235
      throw new Error(
×
236
        'Returned replies is empty, please check processMessages() implementation.'
237
      );
238
    }
239
  } catch (e) {
240
    console.error(e);
×
241
    rollbar.error(e as Error, req);
×
242
    result = {
×
243
      context: { data: {} },
244
      replies: [
245
        {
246
          type: 'text',
247
          text: t`Oops, something is not working. We have cleared your search data, hopefully the error will go away. Would you please send us the message from the start?`,
248
        },
249
      ],
250
    };
251
  }
252
  return result;
×
253
}
254

255
const router = new Router();
1✔
256

257
const groupHandler = new GroupHandler(groupEventQueue, expiredGroupEventQueue);
1✔
258
// Routes that is after protection of checkSignature
259
//
260
router.use('/', checkSignatureAndParse);
1✔
261
router.post('/', (ctx) => {
1✔
262
  // Allow free-form request handling.
263
  // Don't wait for anything before returning 200.
264

265
  (ctx.request.body as { events: WebhookEvent[] }).events.forEach(
6✔
266
    async (webhookEvent: WebhookEvent) => {
267
      let replyToken = '';
6✔
268
      if ('replyToken' in webhookEvent) {
6!
269
        replyToken = webhookEvent.replyToken;
6✔
270
      }
271

272
      // Set 58s timeout.
273
      // Reply tokens must be used within one minute after receiving the webhook.
274
      // Ref: https://developers.line.biz/en/reference/messaging-api/#send-reply-message
275
      //
276
      const timeout = 58000;
6✔
277
      if (webhookEvent.source.type === 'user') {
6!
278
        singleUserHandler(
6✔
279
          ctx.request,
280
          webhookEvent.type,
281
          replyToken,
282
          timeout,
283
          webhookEvent.source.userId ?? '',
6!
284
          webhookEvent
285
        );
286
      } else if (
×
287
        webhookEvent.source.type === 'group' ||
×
288
        webhookEvent.source.type === 'room'
289
      ) {
290
        const groupId =
291
          webhookEvent.source.type === 'group'
×
292
            ? webhookEvent.source.groupId
293
            : webhookEvent.source.roomId;
294

295
        groupHandler.addJob({
×
296
          type: webhookEvent.type,
297
          replyToken,
298
          groupId,
299
          webhookEvent,
300
        });
301
      }
302
    }
303
  );
304
  ctx.status = 200;
6✔
305
});
306

307
export default router;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc