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;