Skip to content

Instantly share code, notes, and snippets.

@fawwaz
Last active November 11, 2017 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fawwaz/f41ed5c0253249f6c1a93ea49a6be68b to your computer and use it in GitHub Desktop.
Save fawwaz/f41ed5c0253249f6c1a93ea49a6be68b to your computer and use it in GitHub Desktop.
Modified Line Messenger NodeJS SDK

Modified Line Messenger NodeJS SDK

This code is modified version of Line Messenger NodeJS SDK. You can use it to test your bot locally for your side project. Please use it wisely, you should use the official SDK for more serious cases. For more serious cases, please follow the official guideline on how to test your bot locally here : line/line-bot-sdk-nodejs#32 This repo will not be maintained.

TLDR;

👍 You can test your bot locally, you can test directly on your own computer without passing through long-boring build pipeline, No need to put on staging server, Heroku / Azure or other PaaS provider just to test your bot is working. 🖥️

👍 Test your bot by simulating multiple user at once without any devices 📵

How to use it

With this pull request, the developers are able to test their bots on local computer/environment. Accelerating bot development by bypassing x-line-signature verification.

1. Create your own LineMockServer.js

You can write a simple Line API Mock Server like this :

var express = require('express');
var bodyParser = require('body-parser');
var app = express();

app.use(bodyParser.json());

app.get('/', function(req, res){
    res.send('Hello world');
});

app.post('/message/reply',function(req, res){
    console.dir(req.body);
    res.json(req.body);
});

app.post('/message/push', function(req, res){
    console.dir(req.body);
    res.json(req.body);
});

app.get('/profile/:userId', function(req, res){
    var users = {
        'member_1':{
            'displayName': 'Spongebob',
            'userId': 'member_1',
            'pictureUrl': 'http://placeholder.it/256x256',
            'statusMessage': 'exampleStatusMessage',
        },
        'member_2':{
            'displayName': 'Patrick Star',
            'userId': 'member_2',
            'pictureUrl': 'http://placeholder.it/256x256',
            'statusMessage': 'exampleStatusMessage',
        },
    };

    var selectedUser = users[req.params.userId];
    console.log("Returning profile for userId = "+req.params.userId);
    console.dir(selectedUser);
    res.json(selectedUser);
});

app.post('*', function(req, res){
    showTime();
    console.dir(req.body);
    res.json(req.body);
});

app.listen(8000, function(){
    console.log("listening on port 8000");
});

then run your Line API Mock Server with :

node LineMockserver.js

2. Run your server js with TEST flag and appropriate API_BASE_URL

In this case, we are listening to port 8000, then run your bot server using this command :

API_BASE_URL=http://localhost:8000/ TEST=true node server.js

3. Create API request to your local bot server

You can use any REST client to create a Http request to your bot server. Make sure you follow the Line API payload structure as written on their docs while making your request. In this case, I use Postman Chrome Plugin since my bot server running on port 3000, I put localhost:3000 on Postman's address bar, set the Content-Type header to application/json and put Line Message Object, in the body. like this :

{
  "events": [
    {
      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "member_1"
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "help"
      }
    }
  ]
}

4. Enjoy it !

working perfectly

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const http_1 = require("./http");
const URL = require("./urls");
const util_1 = require("./util");
class Client {
constructor(config) {
if (!config.channelAccessToken) {
throw new Error("no channel access token");
}
this.config = config;
}
pushMessage(to, messages) {
return this.post(URL.push, {
messages: util_1.toArray(messages),
to,
});
}
replyMessage(replyToken, messages) {
return this.post(URL.reply, {
messages: util_1.toArray(messages),
replyToken,
});
}
multicast(to, messages) {
return this.post(URL.multicast, {
messages: util_1.toArray(messages),
to,
});
}
getProfile(userId) {
return this.get(URL.profile(userId));
}
getGroupMemberProfile(groupId, userId) {
return this.get(URL.groupMemberProfile(groupId, userId));
}
getRoomMemberProfile(roomId, userId) {
return this.get(URL.roomMemberProfile(roomId, userId));
}
getGroupMemberIds(groupId) {
const load = (start) => this.get(URL.groupMemberIds(groupId, start))
.then((res) => {
if (!res.next) {
return res.memberIds;
}
return load(res.next).then((extraIds) => res.memberIds.concat(extraIds));
});
return load();
}
getRoomMemberIds(roomId) {
const load = (start) => this.get(URL.roomMemberIds(roomId, start))
.then((res) => {
if (!res.next) {
return res.memberIds;
}
return load(res.next).then((extraIds) => res.memberIds.concat(extraIds));
});
return load();
}
getMessageContent(messageId) {
return this.stream(URL.content(messageId));
}
leaveGroup(groupId) {
return this.post(URL.leaveGroup(groupId));
}
leaveRoom(roomId) {
return this.post(URL.leaveRoom(roomId));
}
authHeader() {
return { Authorization: "Bearer " + this.config.channelAccessToken };
}
get(url) {
return http_1.get(url, this.authHeader());
}
post(url, body) {
return http_1.post(url, this.authHeader(), body);
}
stream(url) {
return http_1.stream(url, this.authHeader());
}
}
exports.default = Client;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class SignatureValidationFailed extends Error {
constructor(message, signature) {
super(message);
this.signature = signature;
}
}
exports.SignatureValidationFailed = SignatureValidationFailed;
class JSONParseError extends Error {
constructor(message, raw) {
super(message);
this.raw = raw;
}
}
exports.JSONParseError = JSONParseError;
class RequestError extends Error {
constructor(message, code, originalError) {
super(message);
this.code = code;
this.originalError = originalError;
}
}
exports.RequestError = RequestError;
class ReadError extends Error {
constructor(originalError) {
super(originalError.message);
this.originalError = originalError;
}
}
exports.ReadError = ReadError;
class HTTPError extends Error {
constructor(message, statusCode, statusMessage, originalError) {
super(message);
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.originalError = originalError;
}
}
exports.HTTPError = HTTPError;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = require("axios");
const exceptions_1 = require("./exceptions");
const pkg = require("./package.json"); // tslint:disable-line no-var-requires
function checkJSON(raw) {
if (typeof raw === "object") {
return raw;
}
else {
throw new exceptions_1.JSONParseError("Failed to parse response body as JSON", raw);
}
}
function wrapError(err) {
if (err.response) {
throw new exceptions_1.HTTPError(err.message, err.response.status, err.response.statusText, err);
}
else if (err.code) {
throw new exceptions_1.RequestError(err.message, err.code, err);
}
else if (err.config) {
// unknown, but from axios
throw new exceptions_1.ReadError(err);
}
// otherwise, just rethrow
throw err;
}
const userAgent = `${pkg.name}/${pkg.version}`;
function stream(url, headers) {
headers["User-Agent"] = userAgent;
return axios_1.default
.get(url, { headers, responseType: "stream" })
.then((res) => res.data);
}
exports.stream = stream;
function get(url, headers) {
headers["User-Agent"] = userAgent;
return axios_1.default
.get(url, { headers })
.then((res) => checkJSON(res.data))
.catch(wrapError);
}
exports.get = get;
function post(url, headers, data) {
headers["Content-Type"] = "application/json";
headers["User-Agent"] = userAgent;
return axios_1.default
.post(url, data, { headers })
.then((res) => checkJSON(res.data))
.catch(wrapError);
}
exports.post = post;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("./client");
exports.Client = client_1.default;
const middleware_1 = require("./middleware");
exports.middleware = middleware_1.default;
const validate_signature_1 = require("./validate-signature");
exports.validateSignature = validate_signature_1.default;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const body_parser_1 = require("body-parser");
const exceptions_1 = require("./exceptions");
const validate_signature_1 = require("./validate-signature");
function middleware(config) {
if (!config.channelSecret) {
throw new Error("no channel secret");
}
const secret = config.channelSecret;
return (req, res, next) => {
// header names are lower-cased
// https://nodejs.org/api/http.html#http_message_headers
let signature;
if (!process.env.DEV_ENV) {
signature = req.headers["x-line-signature"];
if (!signature) {
next(new exceptions_1.SignatureValidationFailed("no signature"));
return;
}
}
const validate = (body) => {
if (!process.env.DEV_ENV) {
if (!validate_signature_1.default(body, secret, signature)) {
next(new exceptions_1.SignatureValidationFailed("signature validation failed", signature));
return;
}
}
const strBody = Buffer.isBuffer(body) ? body.toString() : body;
try {
req.body = JSON.parse(strBody);
next();
}
catch (err) {
next(new exceptions_1.JSONParseError(err.message, strBody));
}
};
if (typeof req.body === "string" || Buffer.isBuffer(req.body)) {
return validate(req.body);
}
// if body is not parsed yet, parse it to a buffer
body_parser_1.raw({ type: "*/*" })(req, res, () => validate(req.body));
};
}
exports.default = middleware;
{
"name": "modified-line-nodejs-sdk",
"version": "0.0.1"
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const qs = require("querystring");
const baseURL = process.env.API_BASE_URL || "https://api.line.me/v2/bot/";
const apiURL = (path, query) => baseURL + path + (query ? `?${qs.stringify(query)}` : "");
exports.reply = apiURL("message/reply");
exports.push = apiURL("message/push");
exports.multicast = apiURL("message/multicast");
exports.content = (messageId) => apiURL(`message/${messageId}/content`);
exports.profile = (userId) => apiURL(`profile/${userId}`);
exports.groupMemberProfile = (groupId, userId) => apiURL(`group/${groupId}/member/${userId}`);
exports.roomMemberProfile = (roomId, userId) => apiURL(`room/${roomId}/member/${userId}`);
exports.groupMemberIds = (groupId, start) => apiURL(`group/${groupId}/members/ids`, start ? { start } : null);
exports.roomMemberIds = (roomId, start) => apiURL(`room/${roomId}/members/ids`, start ? { start } : null);
exports.leaveGroup = (groupId) => apiURL(`group/${groupId}/leave`);
exports.leaveRoom = (roomId) => apiURL(`room/${roomId}/leave`);
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function toArray(maybeArr) {
return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
}
exports.toArray = toArray;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = require("crypto");
function s2b(str, encoding) {
if (Buffer.from) {
try {
return Buffer.from(str, encoding);
}
catch (err) {
if (err.name === "TypeError") {
return new Buffer(str, encoding);
}
throw err;
}
}
else {
return new Buffer(str, encoding);
}
}
function safeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
if (crypto_1.timingSafeEqual) {
return crypto_1.timingSafeEqual(a, b);
}
else {
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i]; // tslint:disable-line no-bitwise
}
return result === 0;
}
}
function validateSignature(body, channelSecret, signature) {
return safeCompare(crypto_1.createHmac("SHA256", channelSecret).update(body).digest(), s2b(signature, "base64"));
}
exports.default = validateSignature;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment