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

cofacts / rumors-line-bot / 6842018647

12 Nov 2023 05:00PM UTC coverage: 89.64% (-0.1%) from 89.786%
6842018647

Pull #371

github

MrOrz
fix(webhook): update initState test snapshots
Pull Request #371: [2] simplify text handlers as well

480 of 573 branches covered (0.0%)

Branch coverage included in aggregate %.

15 of 17 new or added lines in 3 files covered. (88.24%)

2 existing lines in 1 file now uncovered.

965 of 1039 relevant lines covered (92.88%)

24.99 hits per line

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

50.79
/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 { MessageEvent, TextEventMessage, WebhookEvent } from '@line/bot-sdk';
22
import { Result } from 'src/types/result';
23
import { Context, PostbackActionData } from 'src/types/chatbotState';
24

25
const userIdBlacklist = (process.env.USERID_BLACKLIST || '').split(',');
2✔
26

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

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

72
  // Set default result
73
  //
74
  let result: Result = {
12✔
75
    context: { data: {} },
76
    replies: [
77
      {
78
        type: 'text',
79
        text: t`I cannot understand messages other than text.`,
80
      },
81
    ],
82
  };
83

84
  // Handle follow/unfollow event
85
  if (webhookEvent.type === 'follow') {
12✔
86
    await UserSettings.setAllowNewReplyUpdate(userId, true);
8✔
87

88
    if (process.env.RUMORS_LINE_BOT_URL) {
8✔
89
      const data = { sessionId: Date.now() };
6✔
90
      result = {
6✔
91
        context: { data },
92
        replies: [
93
          createGreetingMessage(),
94
          createTutorialMessage(data.sessionId),
95
        ],
96
      };
97

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

115
  const context = (await redis.get(userId)) || {};
8✔
116
  // React to certain type of events
117
  //
118
  if (webhookEvent.type === 'message' && webhookEvent.message.type === 'text') {
8!
119
    // Debugging: type 'RESET' to reset user's context and start all over.
120
    //
NEW
121
    if (webhookEvent.message.text === 'RESET') {
×
122
      redis.del(userId);
×
123
      clearTimeout(timerId);
×
124
      return;
×
125
    }
126

127
    result = await processText(
×
128
      // Make TS happy:
129
      // Directly providing `webhookEvent` here can lead to type error
130
      // because it cannot correctly narrow down webhookEvent.message to be TextEventMessage.
131
      {
132
        ...webhookEvent,
133
        message: webhookEvent.message,
134
      },
135
      userId,
136
      req
137
    );
138
  } else if (
8!
139
    webhookEvent.type === 'message' &&
8!
140
    webhookEvent.message.type !== 'text'
141
  ) {
142
    result = await processMedia(webhookEvent, userId);
×
143
  } else if (webhookEvent.type === 'message') {
8!
144
    // Track other message type send by user
145
    ga(userId)
×
146
      .event({
147
        ec: 'UserInput',
148
        ea: 'MessageType',
149
        el: webhookEvent.message.type,
150
      })
151
      .send();
152
  } else if (webhookEvent.type === 'postback') {
8!
153
    const postbackData = JSON.parse(
×
154
      webhookEvent.postback.data
155
    ) as PostbackActionData<unknown>;
156

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

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

193
    result = await handlePostback(context.data, postbackData, userId);
×
194
  }
195

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

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

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

218
  // Set context
219
  //
220
  await redis.set(userId, result.context);
8✔
221
};
222

223
async function processText(
224
  event: MessageEvent & { message: TextEventMessage },
225
  userId: string,
226
  req: Request
227
): Promise<Result> {
228
  let result: Result;
229
  try {
×
NEW
230
    result = await handleInput(event, userId);
×
231
    if (!result.replies) {
×
232
      throw new Error(
×
233
        'Returned replies is empty, please check processMessages() implementation.'
234
      );
235
    }
236
  } catch (e) {
237
    console.error(e);
×
238
    rollbar.error(e as Error, req);
×
239
    result = {
×
240
      context: { data: {} },
241
      replies: [
242
        {
243
          type: 'text',
244
          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?`,
245
        },
246
      ],
247
    };
248
  }
249
  return result;
×
250
}
251

252
const router = new Router();
2✔
253

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

262
  (ctx.request.body as { events: WebhookEvent[] }).events.forEach(
12✔
263
    async (webhookEvent: WebhookEvent) => {
264
      let replyToken = '';
12✔
265
      if ('replyToken' in webhookEvent) {
12!
266
        replyToken = webhookEvent.replyToken;
12✔
267
      }
268

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

292
        groupHandler.addJob({
×
293
          type: webhookEvent.type,
294
          replyToken,
295
          groupId,
296
          webhookEvent,
297
        });
298
      }
299
    }
300
  );
301
  ctx.status = 200;
12✔
302
});
303

304
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