UNPKG

linebot

Version:

Node.js SDK for the LINE Messaging API

384 lines (349 loc) 10.8 kB
'use strict'; // Required to use class in node v4 const EventEmitter = require('events'); const fetch = require('node-fetch'); const crypto = require('crypto'); const http = require('http'); const bodyParser = require('body-parser'); const debug = require('debug')('linebot'); class LineBot extends EventEmitter { constructor(options) { super(); this.options = options || {}; this.options.channelId = options.channelId || ''; this.options.channelSecret = options.channelSecret || ''; this.options.channelAccessToken = options.channelAccessToken || ''; if (this.options.verify === undefined) { this.options.verify = true; } this.headers = { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + this.options.channelAccessToken }; this.endpoint = 'https://api.line.me/v2/bot'; this.dataEndpoint = 'https://api-data.line.me/v2/bot'; } verify(rawBody, signature) { const hash = crypto.createHmac('sha256', this.options.channelSecret) .update(rawBody, 'utf8') .digest('base64'); // Constant-time comparison to prevent timing attack. if (hash.length !== signature.length) { return false; } let res = 0; for (let i = 0; i < hash.length; i++) { res |= (hash.charCodeAt(i) ^ signature.charCodeAt(i)); } return res === 0; } parse(body) { const that = this; if (!body || !body.events) { return; } body.events.forEach(function(event) { debug('%O', event); event.reply = function (message) { return that.reply(event.replyToken, message); }; if (event.source) { event.source.profile = function() { if (event.source.type === 'group') { return that.getGroupMemberProfile(event.source.groupId, event.source.userId); } if (event.source.type === 'room') { return that.getRoomMemberProfile(event.source.roomId, event.source.userId); } return that.getUserProfile(event.source.userId); }; event.source.member = function() { if (event.source.type === 'group') { return that.getGroupMember(event.source.groupId); } if (event.source.type === 'room') { return that.getRoomMember(event.source.roomId); } }; } if (event.message) { event.message.content = function() { return that.getMessageContent(event.message.id); }; } process.nextTick(function() { that.emit(event.type, event); }); }); } static createMessages(message) { if (typeof message === 'string') { return [{ type: 'text', text: message }]; } if (Array.isArray(message)) { return message.map(function(m) { if (typeof m === 'string') { return { type: 'text', text: m }; } return m; }); } return [message]; } reply(replyToken, message) { const url = '/message/reply'; const body = { replyToken: replyToken, messages: LineBot.createMessages(message) }; debug('POST %s', url); debug('%O', body); return this.post(url, body).then(res => res.json()).then((result) => { debug(result); return result; }); } push(to, message) { const url = '/message/push'; if (Array.isArray(to)) { return Promise.all(to.map(recipient => this.push(recipient, message))); } const body = { to: to, messages: LineBot.createMessages(message) }; debug('POST %s', url); debug('%O', body); return this.post(url, body).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } multicast(to, message) { const url = '/message/multicast'; const body = { to: to, messages: LineBot.createMessages(message) }; debug('POST %s', url); debug('%O', body); return this.post(url, body).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } broadcast(message){ const url = '/message/broadcast'; const body = { messages: LineBot.createMessages(message) }; debug('POST %s', url); debug('%O', body); return this.post(url, body).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } getMessageContent(messageId) { const url = `/message/${messageId}/content`; debug('GET %s', url); return this.getData(url).then(res => res.buffer()).then((buffer) => { debug(buffer.toString('hex')); return buffer; }); } getUserProfile(userId) { const url = `/profile/${userId}`; debug('GET %s', url); return this.get(url).then(res => res.json()).then((profile) => { debug('%O', profile); return profile; }); } getGroupMemberProfile(groupId, userId) { const url = `/group/${groupId}/member/${userId}`; debug('GET %s', url); return this.get(url).then(res => res.json()).then((profile) => { debug('%O', profile); profile.groupId = groupId; return profile; }); } getGroupMember(groupId, next) { const url = `/group/${groupId}/members/ids` + (next ? `?start=${next}` : ''); debug('GET %s', url); return this.get(url).then(res => res.json()).then((groupMember) => { debug('%O', groupMember); if (groupMember.next) { return this.getGroupMember(groupId, groupMember.next).then((nextGroupMember) => { groupMember.memberIds = groupMember.memberIds.concat(nextGroupMember.memberIds); delete groupMember.next; return groupMember; }); } delete groupMember.next; return groupMember; }); } leaveGroup(groupId) { const url = `/group/${groupId}/leave`; debug('POST %s', url); return this.post(url).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } getRoomMemberProfile(roomId, userId) { const url = `/room/${roomId}/member/${userId}`; debug('GET %s', url); return this.get(url).then(res => res.json()).then((profile) => { debug('%O', profile); profile.roomId = roomId; return profile; }); } getRoomMember(roomId, next) { const url = `/room/${roomId}/members/ids` + (next ? `?start=${next}` : ''); debug('GET %s', url); return this.get(url).then(res => res.json()).then((roomMember) => { debug('%O', roomMember); if (roomMember.next) { return this.getRoomMember(roomId, roomMember.next).then((nextRoomMember) => { roomMember.memberIds = roomMember.memberIds.concat(nextRoomMember.memberIds); delete roomMember.next; return roomMember; }); } delete roomMember.next; return roomMember; }); } leaveRoom(roomId) { const url = `/room/${roomId}/leave`; debug('POST %s', url); return this.post(url).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } getTotalFollowers(date) { if (date == null) { date = yesterday(); } const url = `/insight/followers?date=${date}`; debug('GET %s', url); return this.get(url).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } getQuota() { const url = '/message/quota'; debug('GET %s', url); return this.get(url).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } getTotalReplyMessages(date) { return this.getTotalMessages(date, 'reply'); } getTotalPushMessages(date) { return this.getTotalMessages(date, 'push'); } getTotalBroadcastMessages(date) { return this.getTotalMessages(date, 'broadcast'); } getTotalMulticastMessages(date) { return this.getTotalMessages(date, 'multicast'); } getTotalMessages(date, type) { if (date == null) { date = yesterday(); } const url = `/message/delivery/${type}?date=${date}`; debug('GET %s', url); return this.get(url).then(res => res.json()).then((result) => { debug('%O', result); return result; }); } get(path) { const url = this.endpoint + path; const options = { method: 'GET', headers: this.headers }; return fetch(url, options); } getData(path) { const url = this.dataEndpoint + path; const options = { method: 'GET', headers: this.headers }; return fetch(url, options); } post(path, body) { const url = this.endpoint + path; const options = { method: 'POST', headers: this.headers, body: JSON.stringify(body) }; return fetch(url, options); } // Optional Express.js middleware parser() { const parser = bodyParser.json({ verify: function (req, res, buf, encoding) { req.rawBody = buf.toString(encoding); } }); return (req, res) => { parser(req, res, () => { if (this.options.verify && !this.verify(req.rawBody, req.get('X-Line-Signature'))) { return res.sendStatus(400); } this.parse(req.body); return res.json({}); }); }; } // Optional built-in http server listen(path, port, callback) { const parser = bodyParser.json({ verify: function (req, res, buf, encoding) { req.rawBody = buf.toString(encoding); } }); const server = http.createServer((req, res) => { const signature = req.headers['x-line-signature']; // Must be lowercase res.setHeader('X-Powered-By', 'linebot'); if (req.method === 'POST' && req.url === path) { parser(req, res, () => { if (this.options.verify && !this.verify(req.rawBody, signature)) { res.statusCode = 400; res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.end('Bad request'); } this.parse(req.body); res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); return res.end('{}'); }); } else { res.statusCode = 404; res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.end('Not found'); } }); return server.listen(port, callback); } } // class LineBot function createBot(options) { return new LineBot(options); } function yesterday() { const tempDate = new Date(); tempDate.setDate(tempDate.getDate() - 1); const yesterday = tempDate.toLocaleString('en-US', { timeZone: 'Asia/Tokyo', day: '2-digit', month: '2-digit', year: 'numeric' }); return yesterday.substr(6, 4) + yesterday.substr(0, 2) + yesterday.substr(3, 2); } module.exports = createBot; module.exports.LineBot = LineBot;