Home Manual Reference Source Repository

es2015/LineBot/index.js

import { createHmac, createHash } from 'crypto';
import { EventEmitter2 } from 'eventemitter2';
import axios from 'axios';
import express from 'express';
import bodyParser from 'body-parser';
import { LineEvent } from '../LineEvents';
import { GroupSource, RoomSource } from '../LineSources';
/* eslint-enable no-unused-vars */
/**
 * LINE Bot API wrapper.
 *
 * @example <caption>EventEmitter</caption>
 * import { LineBot } from '@3846masa/linebot';
 * const bot = new LineBot(configs);
 *
 * bot.on('message', (result) => {
 *   console.log('You got a message!', result);
 * });
 *
 * bot.listen(3000);
 *
 * @extends {EventEmitter2}
 * @see https://devdocs.line.me/ja/
 */
export class LineBot extends EventEmitter2 {
    /* eslint-enable no-undef */
    /**
     * Constructor
     * @param  {LineBotConfig} config
     */
    constructor({ channelSecret, channelToken }) {
        super({
            wildcard: true,
            delimiter: ':',
            maxListeners: 20,
        });
        /**
         * Configuration.
         * @type {LineBotConfig}
         */
        this.config = {
            channelSecret,
            channelToken,
        };
        Object.freeze(this.config);
        this._initAxios();
        this._initExpress();
    }
    /** @private */
    _initAxios() {
        /** @private */
        this._axios = axios.create({
            baseURL: 'https://api.line.me/v2/bot',
            headers: {
                Authorization: `Bearer ${this.config.channelToken}`,
            },
            validateStatus: () => true,
        });
    }
    /** @private */
    _initExpress() {
        /**
         * Configuration.
         * @type {Express.Application}
         */
        this.express = express();
        this.express.use(bodyParser.raw({ type: () => true }));
        this.express.use((req, res, next) => {
            const isValid = this._checkSignature({
                signature: req.header('X-Line-Signature'),
                body: req.body,
            });
            if (!isValid) {
                return next(new Error('Invalid request.'));
            }
            return next();
        });
        this.express.use((req, res, next) => {
            Promise.resolve(req.body.toString('utf8'))
                .then(JSON.parse)
                .then(json => Object.assign(req, { body: json }))
                .then(() => next())
                .catch(next);
        });
        this.express.all('*', (req, res, next) => {
            if (!req.body || !req.body.events) {
                return next(new Error('Invalid JSON.'));
            }
            for (const event of req.body.events) {
                const eventObj = LineEvent.createFromObject(event, this);
                this.emit(`webhook:${event.type}`, eventObj);
            }
            res.status(200).send();
            return next();
        });
        this.express.use((err, req, res, next) => {
            console.error(err);
            res.status(400).send({ error: err.message });
            next();
        });
    }
    /** @private */
    _checkSignature({ signature, body }) {
        const hmac = createHmac('sha256', this.config.channelSecret)
            .update(body).digest('base64');
        const hmacHashed = createHash('sha1')
            .update(hmac).digest();
        const signatureHashed = createHash('sha1')
            .update(signature).digest();
        return hmacHashed.equals(signatureHashed);
    }
    /**
     * Send messages to users, groups, and rooms at any time.
     * @see https://devdocs.line.me/en/#push-message
     * @param  {string}                            to   ID of the receiver
     * @param  {LineMessage[] | LineMessage | any} msg  Messages (Max: 5)
     * @return {Promise<void,Error>}
     */
    push(to, msg) {
        let messages;
        if (!Array.isArray(msg)) {
            messages = [msg];
        }
        else {
            messages = msg;
        }
        const promise = this._post('/message/push', { to, messages });
        return promise.then(({ status, data }) => {
            if (status !== 200) {
                return Promise.reject(new Error(`${status}: ${data.message}`));
            }
            return Promise.resolve();
        });
    }
    /**
     * Retrieve image, video, and audio data sent by users.
     * @see https://devdocs.line.me/en/#get-content
     * @param  {LineMessage} message Message
     * @return {Promise<stream.Readable,Error>}
     */
    getContentFromMessage(message) {
        return this.getContent(message.id);
    }
    /**
     * Retrieve image, video, and audio data sent by users.
     * @see https://devdocs.line.me/en/#get-content
     * @param  {string} messageId Message ID.
     * @return {Promise<stream.Readable,Error>}
     */
    getContent(messageId) {
        const promise = this._get(`/message/${messageId}/content`, {
            responseType: 'stream',
        });
        return promise.then(({ status, statusText, data }) => {
            if (status !== 200) {
                throw new Error(`${status}: ${statusText}`);
            }
            return Promise.resolve(data);
        });
    }
    /**
     * Send messages to users, groups, and rooms at any time.
     * @see https://devdocs.line.me/en/#bot-api-get-profile
     * @param  {UserSource} user  User source
     * @return {Promise<LineProfile,Error>}
     */
    getProfileFromUserSource(user) {
        return this.getProfile(user.userId);
    }
    /**
     * Send messages to users, groups, and rooms at any time.
     * @see https://devdocs.line.me/en/#bot-api-get-profile
     * @param  {string} userId  User ID
     * @return {Promise<LineProfile,Error>}
     */
    getProfile(userId) {
        const promise = this._get(`/profile/${userId}`);
        return promise.then(({ status, data }) => {
            if (status !== 200) {
                throw new Error(`${status}: ${data.message}`);
            }
            return Promise.resolve(data);
        });
    }
    /**
     * Leave a group or room.
     * @see https://devdocs.line.me/en/#leave
     * @param  {GroupSource | RoomSource}  source
     * @return {Promise<void,Error>}
     */
    leaveFromSource(source) {
        if (source instanceof GroupSource) {
            return this.leave('group', source.groupId);
        }
        else if (source instanceof RoomSource) {
            return this.leave('room', source.roomId);
        }
        return Promise.resolve();
    }
    /**
     * Leave a group or room.
     * @see https://devdocs.line.me/en/#leave
     * @param  {string} type
     * @param  {string} id
     * @return {Promise<void,Error>}
     */
    leave(type, id) {
        const promise = this._post(`/${type}/${id}/leave`);
        return promise.then(({ status, data }) => {
            if (status !== 200) {
                return Promise.reject(new Error(`${status}: ${data.message}`));
            }
            return Promise.resolve();
        });
    }
    /** @private */
    _get(endpoint, options) {
        return this._axios.get(endpoint, options || {});
    }
    /** @private */
    _post(endpoint, data, options) {
        return this._axios.post(endpoint, data, options || {});
    }
    /**
     * Binds and listens for connections on the specified host and port.
     * @see http://expressjs.com/en/4x/api.html#app.listen
     * @param  { ...any }    args http://expressjs.com/en/4x/api.html#app.listen
     * @return {http.Server}
     */
    listen(...args) {
        return this.express.listen.call(this.express, ...args);
    }
    /**
     * Adds a listener to the end of the listeners array for the specified event.
     * @see https://github.com/asyncly/EventEmitter2#emitteronevent-listener
     * @param   {string | string[]} event    Event name
     * @param   {Function}          listener Listener function
     * @return  {LineBot}
     * @listens {webhook:{eventType}}  Listen message event. https://developers.line.me/bot-api/api-reference#sending_message
     */
    on(event, listener) {
        super.on(event, listener);
        return this;
    }
}
export default LineBot;