Developing LINE Messaging Chat Bot With Node.js

LINE is a very popular messaging platform. It has a lot of features such as voice and video call, user timeline, custom theme and news service. It also allows us to create channel, which can be added as friend by other users. If your channel has already have a lot of followers, it takes a lot of time if you've to reply every messages one by one. Therefore we need an automated process. The solution is by creating a bot that can reply events automatically. In this tutorial, I'll show you how to create a LINE chatbot using Node.js. 

Create a Channel

The first thing to do is creating a channel. In order to do so, you need to do the following steps

1. Create a LINE account

If you haven't had an account, just download the app on your mobile phone. Then you'll need to enter basic info such as name, phone number and password. In addition, you also need to set an email address because it's required for login on LINE developer console. If you haven't set your email, open Settings → Account and enter your email address.

2. Login to console

Open LINE developer console and enter email address and password you've set beforehand. At the first successful login attempt, you need to enter developer information. Enter your name and email address on that page.

LINE - Enter Developer Information

After completing that step, you'll be redirected to Provider List page. Before creating a channel, you must have at least one provider. Click on the Create New Provider button.

On the form, you only need to enter the provider name. Then click on Confirm button (and Create button on the next page) and your provider should be created. You'll get a message that your provider has been created. As we're going to create a chatbot, we need to use messaging API. On Messaging API section, click on Create Channel.

LINE - Create Provider Success

On the create new channel form, you need to select app icon, enter app name, enter app description, choose plan (developer trial or free), select category and subcateogry, as well as enter an email address where important notifications and announcements are sent. Then click on Confirm button and your channel should be successfully created. You'll be rediected to channel list. Click on the channel you've just created because you need to configure it.

LINE - Channel List

What you need to look for at that page is the value of channel secret. In addition, you also need to request LINE to issue a new channel access token. You'll get a popup asking for the time until current token becomes invalid (it's safe to use the default value of 0 hours beause it's never been issued before). As we're going to develop a chatbot, we can disable auto-reply messages and greeting messages. You can also enable allow bot to join group chats if you want. Another important setting is the webhook. For now we leave it disabled. You can configure it later after you've developed your bot.

On your .env file, add the following:

  LINE_CHANNEL_SECRET=1234abcd1234abcd1234abcd1234abcd
  LINE_CHANNEL_ACCESS_TOKEN=123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123Abc123AbcABC=

Don't forget to use require('dotenv') or any other similar modules anywhere in your project before reading those values.

Developing The Bot

This is the main part of this tutorial. Before starting to code, first you need to understand how it works. Once you've set a webhook URL, every time an event occurs, such as when a user follows your channel, sends a message, etc., LINE will send a request to the webhook URL. It contains a list of events, which should be processed one by one. To make sure that the request is coming from LINE server, you can check the x-line-signature header (will be exemplified later). For each events, after you process it, you can reply to the event by sending reply message(s) to LINE and then it will be forwarded to the user.

First, install the following dependencies

  "bluebird": "~3.5.0",
  "crypto": "~0.0.3",
  "request-promise": "~4.2.2",

When the webhook receives an incoming request, first we need to validate that the request comes from LINE. A valid request must contain x-line-signature header which value depends on the request body sent. That means the value of x-line-signature differs between requests. To validate the validity of x-line-signature, we need to compare it with the digest value of request body using HMAC-SHA256 algorithm. We use the value of process.env.LINE_CHANNEL_SECRET as the secret value. If the x-line-signature header is valid, the value must be the same as our calculation result. Otherwise, we can consider the request is invalid and we can ignore it. Having validated the signature, the next thing to do is processing each events. Afterwards, we need to send a response to LINE indicating that we've processed the events, with a status code of 200, regardless some events may not what you expect.

webhook-handler.js

  const Bluebird = require('bluebird');
  const crypto = require('crypto');
  const request = require('request-promise');

  module.exports = (req, res) => {
    try {
      const text = JSON.stringify(req.body);
      const signature = crypto.createHmac('SHA256', process.env.LINE_CHANNEL_SECRET).update(text).digest('base64').toString();
  
      if (signature !== req.headers['x-line-signature']) {
        return res.status(401).send('Unauthorized');
      }
  
      return processWebhookEvents(req.body.events)
        .then(() => res.status(200).send('OK'));
    } catch (err) {
      console.error(err);
  
      return res.status(500).send('Error');
    }
  };

Now, let's implement the processWebhookEvents function. For a better performance, we can process the events concurrently, of course with concurrency limitation. Before module.exports add the following:

webhook-handler.js

  const MAX_CONCURRENCY = 10;

  const processWebhookEvents = events => Bluebird.map(events, event => processEvent(event), { concurrency: MAX_CONCURRENCY });

Next, we're going to implement processEvent. For each events, we need to process it based on the type (such as message, follow, unfollow, etc.). Then we send a reply to the user by sending a request to an API provided by LINE. The value of process.env.LINE_CHANNEL_ACCESS_TOKEN is used for authentication.

webhook-handler.js

  const REPLY = {
    URL: 'https://api.line.me/v2/bot/message/reply',
    TIMEOUT: 60000, // 60 seconds
  };

  const processEvent = event => processEventByType(event)
    .catch((err) => {
      console.error(err);
  
      // In case something error on our side,
      // we should tell the user that we're unable to process the request
      const messages = [{
        type: 'text',
        text: 'Something error',
      }];
  
      return messages;
    })
    .then((messages) => {
      // Some events don't have replyToken
      if (!event.replyToken) {
        return Bluebird.resolve();
      }
  
      const requestBody = {
        replyToken: event.replyToken,
        messages,
      };
  
      const requestOptions = {
        uri: REPLY.URL,
        method: 'POST',
        timeout: REPLY.TIMEOUT,
        headers: {
          Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
        resolveWithFullResponse: true,
      };
  
      return request(requestOptions)
        .then((response) => {
          if (response.statusCode === 200) {
            console.log('Reply sent successfully');
          } else {
            console.log(`Error sending reply to LINE server with status ${response.statusCode}:\n ${response.body}`);
          }
        });
    })
    .catch((err) => {
      // Error sending HTTP request
      console.error(err);
    });

In processEventByType, we determine which handler should be used for each event types.

webhook-handler.js

  const followEventProcessor = require('./event-processors/follow');
  const invalidEventProcessor = require('./event-processors/invalid');
  const joinEventProcessor = require('./event-processors/join');
  const leaveEventProcessor = require('./event-processors/leave');
  const messageEventProcessor = require('./event-processors/message');
  const unfollowEventProcessor = require('./event-processors/unfollow');

  const processEventByType = (event) => {
    switch (event.type) {
      case 'follow':
        return followEventProcessor(event);
  
      case 'join':
        return joinEventProcessor(event);
  
      case 'leave':
        return leaveEventProcessor(event);
  
      case 'message':
        return messageEventProcessor(event);
  
      case 'unfollow':
        return unfollowEventProcessor(event);
  
      default:
        return invalidEventProcessor();
    }
  };

Below is the full code of webhook-handler.js

webhook-handler.js

  const Bluebird = require('bluebird');
  const crypto = require('crypto');
  const request = require('request-promise');
  
  const followEventProcessor = require('./event-processors/follow');
  const invalidEventProcessor = require('./event-processors/invalid');
  const joinEventProcessor = require('./event-processors/join');
  const leaveEventProcessor = require('./event-processors/leave');
  const messageEventProcessor = require('./event-processors/message');
  const unfollowEventProcessor = require('./event-processors/unfollow');
  
  const MAX_CONCURRENCY = 5;
  
  const REPLY = {
    URL: 'https://api.line.me/v2/bot/message/reply',
    TIMEOUT: 60000, // 60 seconds
  };
  
  const processEventByType = (event) => {
    switch (event.type) {
      case 'follow':
        return followEventProcessor(event);
  
      case 'join':
        return joinEventProcessor(event);
  
      case 'leave':
        return leaveEventProcessor(event);
  
      case 'message':
        return messageEventProcessor(event);
  
      case 'unfollow':
        return unfollowEventProcessor(event);
  
      default:
        return invalidEventProcessor();
    }
  };
  
  const processEvent = event => processEventByType(event)
    .catch((err) => {
      console.error(err);
  
      // In case something error on our side,
      // we should tell the user that we're unable to process the request
      const messages = [{
        type: 'text',
        text: 'Something error',
      }];
  
      return messages;
    })
    .then((messages) => {
      // Some events don't have replyToken
      if (!event.replyToken) {
        return Bluebird.resolve();
      }
  
      const requestBody = {
        replyToken: event.replyToken,
        messages,
      };
  
      const requestOptions = {
        uri: REPLY.URL,
        method: 'POST',
        timeout: REPLY.TIMEOUT,
        headers: {
          Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
        resolveWithFullResponse: true,
      };
  
      return request(requestOptions)
        .then((response) => {
          if (response.statusCode === 200) {
            console.log('Reply sent successfully');
          } else {
            console.log(`Error sending reply to LINE server with status ${response.statusCode}:\n ${response.body}`);
          }
        });
    })
    .catch((err) => {
      // Error sending HTTP request
      console.error(err);
    });
  
  const processWebhookEvents = events => Bluebird.map(events, event => processEvent(event), { concurrency: MAX_CONCURRENCY });
  
  module.exports = (req, res) => {
    try {
      const text = JSON.stringify(req.body);
      // const hmac = crypto.createHmac('SHA256', process.env.LINE_CHANNEL_SECRET).update(text).digest('raw');
      // const signature = Buffer.from(hmac).toString('base64');
      const signature = crypto.createHmac('SHA256', process.env.LINE_CHANNEL_SECRET).update(text).digest('base64').toString();
  
      if (signature !== req.headers['x-line-signature']) {
        return res.status(401).send('Unauthorized');
      }
  
      return processWebhookEvents(req.body.events)
        .then(() => res.status(200).send('OK'));
    } catch (err) {
      console.error(err);
  
      return res.status(500).send('Error');
    }
  };

Here are the examples of handling several event types:

event-processors/message.js

  const request = require('request-promise');
  
  const processImageMessage = (event) => {
    const messageId = event.message.id;
  
    const requestOptions = {
      uri: `https://api.line.me/v2/bot/message/${messageId}/content`,
      encoding: 'binary',
      method: 'GET',
      timeout: 60000, // 60 seconds
      headers: {
        Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
        'Content-Type': 'application/json',
      },
      resolveWithFullResponse: true,
    };
  
    return request(requestOptions)
      .then((response) => {
        const imageBinaryData = response.body;
        const base64EncodedImage = Buffer.from(imageBinaryData, 'binary').toString('base64');
  
        // Here you may need to do something with the image, such as storing the image somewhere.
  
        // Actually you can reply image message with text and vice versa
        // But in this example we will reply image message with another image
        const messages = [
          {
            type: 'image',
            originalContentUrl: 'https://www.woolha.com/favicon/android-icon-192x192.png',
            previewImageUrl: 'https://www.woolha.com/favicon/android-icon-192x192.png',
          },
        ];
  
        return messages;
      });
  };
  
  const processTextMessage = (event) => {
    const { text } = event.message;
    console.log(`The message is ${text}`);
  
    // Here you may need to process the event based on the text content
  
    const replyForTextMessages = [
      {
        type: 'text',
        text: 'Here is the reply if you sent text',
      },
    ];
  
    return Promise.resolve(replyForTextMessages);
  };
  
  module.exports = (event) => {
    const messageType = event.message.type;
  
    if (messageType === 'image') {
      return processImageMessage(event);
    }
  
    return processTextMessage(event);
  };

event-processors/follow.js

  module.exports = (event) => {
    const messages = [
      {
        type: 'text',
        text: 'Thank you for following this channel',
      },
    ];
  
    return Promise.resolve(messages);
  };

event-processors/unfollow.js

  module.exports = (event) => {
    // No need to reply
    return Promise.resolve();
  };

event-processors/join.js

  module.exports = (event) => {
    const messages = [
      {
        type: 'text',
        text: 'Hi, my name is chatbot. Finally I can join this channel',
      },
    ];
  
    return Promise.resolve(messages);
  };

event-processors/leave.js

  module.exports = (event) => {
    const messages = [
      {
        type: 'text',
        text: 'Chatbot has decided to leave this chat',
      },
    ];
  
    return Promise.resolve(messages);
  };

event-processors/invalid.js

  module.exports = () => {
    const messages = [
      {
        type: 'text',
        text: 'Unable to process event',
      },
    ];
  
    return Promise.resolve(messages);
  };

This tutorial doesn't cover all event types. For the list of supported events including the message format, you can read LINE messaging API docmentation.

Enable Webhook

After the webhook is ready, we need to update the webhook settings of the channel. Go back to your channel settings page. On Message Settings section enable the webhook and enter your webhook URL. Now you can try to test your webhook by triggering some events, such as adding your channel (the easiest way is using QR code on the settings page) or sending messages.