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

cofacts / rumors-line-bot / 6029004231

30 Aug 2023 07:02PM UTC coverage: 89.838% (-1.0%) from 90.824%
6029004231

Pull #361

github

MrOrz
fix(processMedia): update snapshot after wording change
Pull Request #361: refactor: combine processMedia and processImage

468 of 552 branches covered (0.0%)

Branch coverage included in aggregate %.

56 of 56 new or added lines in 2 files covered. (100.0%)

973 of 1052 relevant lines covered (92.49%)

12.04 hits per line

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

50.0
/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 { ChatbotEvent, Context } from 'src/types/chatbotState';
24

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

27
const singleUserHandler = async (
1✔
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;
6✔
40
  const messageBotIsBusy = [
6✔
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 () {
6✔
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) {
6!
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 = {
6✔
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') {
6✔
86
    await UserSettings.setAllowNewReplyUpdate(userId, true);
4✔
87

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

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

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

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

130
    result = await processText(context, webhookEvent.type, input, userId, req);
×
131
  } else if (
4!
132
    webhookEvent.type === 'message' &&
4!
133
    webhookEvent.message.type !== 'text'
134
  ) {
135
    result = await processMedia(context, webhookEvent, userId);
×
136
  } else if (webhookEvent.type === 'message') {
4!
137
    // Track other message type send by user
138
    ga(userId)
×
139
      .event({
140
        ec: 'UserInput',
141
        ea: 'MessageType',
142
        el: webhookEvent.message.type,
143
      })
144
      .send();
145
  } else if (webhookEvent.type === 'postback') {
4!
146
    const postbackData = JSON.parse(webhookEvent.postback.data);
×
147

148
    // Handle the case when user context in redis is expired
149
    if (!context.data) {
×
150
      lineClient.post('/message/reply', {
×
151
        replyToken,
152
        messages: [
153
          {
154
            type: 'text',
155
            text: '🚧 ' + t`Sorry, the button is expired.`,
156
          },
157
        ],
158
      });
159
      clearTimeout(timerId);
×
160
      return;
×
161
    }
162

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

184
    const input = postbackData.input;
×
185
    result = await handlePostback(
×
186
      context,
187
      postbackData.state,
188
      { type: webhookEvent.type, input } as ChatbotEvent,
189
      userId
190
    );
191
  }
192

193
  if (isReplied) {
4!
194
    console.log('[LOG] reply & context setup aborted');
×
195
    return;
×
196
  }
197
  clearTimeout(timerId);
4✔
198

199
  console.log(
4✔
200
    JSON.stringify({
201
      CONTEXT: context,
202
      INPUT: { userId, ...webhookEvent },
203
      OUTPUT: result,
204
    })
205
  );
206

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

215
  // Set context
216
  //
217
  await redis.set(userId, result.context);
4✔
218
};
219

220
async function processText(
221
  context: { data: Partial<Context> },
222
  type: 'message' | 'postback',
223
  input: string,
224
  userId: string,
225
  req: Request
226
): Promise<Result> {
227
  let result: Result;
228
  try {
×
229
    result = await handleInput(
×
230
      context,
231
      { type, input } as ChatbotEvent,
232
      userId
233
    );
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 28s timeout
273
      const timeout = 28000;
6✔
274
      if (webhookEvent.source.type === 'user') {
6!
275
        singleUserHandler(
6✔
276
          ctx.request,
277
          webhookEvent.type,
278
          replyToken,
279
          timeout,
280
          webhookEvent.source.userId ?? '',
6!
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;
6✔
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