엄성진

Develop Initialization

Showing 512 changed files with 0 additions and 4558 deletions
File mode changed
No preview for this file type
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# Built files
dist
# Test recent request
test/helpers/request.json
# IDE
.idea/
language: node_js
node_js:
- 10
- 12
- 14
before_install:
- npm i -g npm@latest
install:
- npm ci
## 7.3.0 (26 Apr 2021)
### Feature
* Support Flex Message Update 2 (#271)
* Messaging API - January 2021 update (#277)
### Misc
* Add TypeScript Example (#270)
* Update dependencies (#270)(#272)(#283)
## 7.2.0 (18 Sep 2020)
### Feature
* Messaging API - December 2020 update (#268)
* Messaging API - October 2020 update (#261)(#264)
* update some Flex Message Update 2 (#265)
### Misc
* Update dependencies (#267)
## 7.1.0 (18 Sep 2020)
### Feature
* Messaging API - August 2020 update (#240)(#251)(#258)
* Messaging API - September 2020 update (#248)
* Add Video viewing complete event (#241)
* Channel access token v2.1 support key id (#231)
* OAuth API v2.1 endpoint change (#233)
### Bug fix
* Accept label in richmenu area actions (#246)
* Update dependencies & fix format (#234)(#236)(#238)(#243)(#250)
* fix: fix createUploadAudienceGroup & updateUploadAudienceGroup API doc (#249)
### Misc
* Add Release CI & change release flow (#256)
* Add: build doc github workflow
## 7.0.0 (15 June 2020)
### Breaking Changes
* Node.js: drop 8 & adopt 14 (#222)
### Feature
* Support Channel access token v2.1 (#223)
* Support Messaging API update for June 2020 (#228)
* add X-Line-Retry-Key support (#224)
* Support emojis in text message webhook (#218)
* narrowcast api & audience apis (#193)
* Add support for sticon in text messages (#214)
* Add language support for profile API (#215)
* Support icon-nickname-switch (#207)
* Define LINE_SIGNATURE_HTTP_HEADER_NAME (#200)
* add docs for getUserInteractionStatistics (#195)
### Bug fixs & Feature Changes
* fix getUserInteractionStatistics (#194)
* type fix: accept string in the aspectRatio property of flex image and flex icon (#212)
### Others
* update dependencies & rewrite to promise (#225 & #229)
* fix vulnerabilities (#217)
* add emoji test (#198)
* update vuepress to 1.x (#188)
## 6.8.4 (19 Dec 2019)
### Bug fix
* Fix typo in type of FriendDemographics (#177)
* Add label property to ImageMapAction type (#187)
### Feature
* Change data api's domain to api-data.line.me (#178)
* Add getUserInteractionStatistics API (#183)
* Add new properties in webhook types (#182)
### Misc
* Rewrite test in nock (#179)
* Update dependencies (#180)
## 6.8.3 (05 Nov 2019)
### Bug fix
* Add exception handler in middleware (#153)
### Feature
* Flex Message Update 1 (#173)
* Support friend statistics API (#161)
### Misc
* Update dependencies (#174)
## 6.8.2 (08 Aug 2019)
### Bug fix
* Fix LINEThings Scenario Execution Event Types (#158)
## 6.8.1 (29 Jul 2019)
### Bug fix
* Fix a type wrong in Template Message (#163)
### Feature
* Get `X-LINE-Request-Id` by using `responseData['x-line-request-id']` (#151 #157)
## 6.8.0 (25 Jun 2019)
### Feature
* Add new parameter in push/reply/multicast/broadcast API to catch up the newest bot API (#147)
* Add new APIs in bot API (#147)
- Get the target limit for additional messages
- Get number of messages sent this month
- Get number of sent broadcast messages
- Send broadcast message
### Breaking changes
* Deprecate Node 6 and start to support Node 12 (#139)
* Remove polyfills for Node 6 (#149)
### Type
* Add LINE Things Event (#150)
### Misc
* Update axios and other dependencies by running `npm audit fix` to fix vulnerabilities. (#148 #154)
## 6.7.0 (18 Apr 2019)
### Feature
* Add alt URL field to URI action (#135)
* Implement (un)linkRichMenuToMultipleUsers (#135)
### Type
* Fix typo in a type (#124)
## 6.6.0 (4 Mar 2019)
### Feature
* Add DeviceLinkEvent / DeviceUnlinkEvent (#123)
### Type
* Fix FlexSpacer to have optional 'size' property (#122)
### Misc
* Run `npm audit fix` to fix minor dependency vulnerability.
## 6.5.0 (16 Feb 2019)
### Feature
* Add APIs to get number of sent messages (#116)
* Add account link event (#117)
### Misc
* Fix a typo in doc (#119)
## 6.4.0 (19 Nov 2018)
### Feature
* Add `getLinkToken` API (#96)
* Handle `req.rawBody` in Google Cloud Functions (#101)
* [Kitchensink] Add ngrok functionality (#99)
### Type
* Add types for video in imagemap message (#100)
* Add `contentProvider` fields to content messages (#103)
* Add `destination` field to webhook request body (#102)
* Add `MemberJoinEvent` and `MemberLeaveEvent` types (#107)
### Misc
* Don't include doc in released source
* Upgrade TypeScript to 3.1.6 (#94)
* Refactoring (#94, #98, #99)
* Remove webhook-tester tool
## 6.3.0 (21 Sep 2018)
### Feature
* Add default rich menu APIs (#87)
### Type
* Add missing `defaultAction` field to `TemplateColumn`
### Misc
* Use VuePress as documentation engine (#85)
* Upgrade minimum supported Node.js version to 6
## 6.2.1 (16 Aug 2018)
### Misc
* Remove gitbook-cli from dev dependencies
## 6.2.0 (15 Aug 2018)
#### Type
* Add QuickReply types (#83)
* Format type comments
#### Misc
* Upgrade TypeScript to 3
## 6.1.1 (14 Aug 2018)
#### Type
* Update FlexMessage types (#81)
#### Misc
* Add test coverage (#78)
* Add JSDoc comments (#80)
## 6.1.0 (19 June 2018)
#### Type
* Add types for flex message (#74)
* Simplify type definition for `Action`
## 6.0.3 (18 June 2018)
#### Misc
* Move get-audio-duration dep to proper package.json (#73)
* Vulnerability fix with `npm audit fix`
## 6.0.2 (21 May 2018)
#### Type
* Add missing `displayText` field to postback action (#63)
* Add missing `FileEventMessage` to `EventMessage` (#71)
#### Misc
* Add audio duration lib to kitchensink example (#68)
## 6.0.1 (13 Mar 2018)
#### Type
* Fix misimplemented 'AudioMessage' type (#61)
## 6.0.0 (27 Feb 2018)
#### Major
* Fix misimplemented 'unlinkRichMenuFromUser' API
#### Type
* Fix TemplateColumn type definition (#48)
#### Misc
* Update GitHub issue template (#43)
* Add Code of Conduct (#50)
* Catch errors properly in examples (#52)
## 5.2.0 (11 Dec 2017)
#### Minor
* Set Content-Length manually for postBinary (#42)
## 5.1.0 (7 Dec 2017)
#### Minor
* Add new fields (#39)
#### Misc
* Fix Windows build (#38)
* Add start scripts and webhook info to examples
## 5.0.1 (14 Nov 2017)
#### Minor
* Fix typo in `ImageMapMessage` type
* Add kitchensink example (#36)
## 5.0.0 (2 Nov 2017)
#### Major
* Implement rich menu API (#34)
#### Type
* Rename `ImageMapArea` and `TemplateAction`s into general ones
#### Misc
* Do not enforce `checkJSON` for some APIs where it means nothing
* Change how to check request object in test cases
## 4.0.0 (25 Oct 2017)
#### Major
* Make index script export exceptions and types (#31)
#### Type
* Simplify config types for client and middleware (#31)
#### Misc
* Fix information and links in doc
* Use Prettier instead of TSLint (#30)
* Install git hooks for precommit and prepush (#30)
## 3.1.1 (19 Sep 2017)
#### Type
* Fix type of postback.params
## 3.1.0 (19 Sep 2017)
#### Major
* Make middleware return `SignatureValidationFailed` for no signature (#26)
#### Type
* Add `FileEventMessage` type
## 3.0.0 (8 Sep 2017)
#### Major
* Implement "Get group/room member profile" API (#15)
* Implement "Get group/room member IDs" API (#23)
* `getMessageContent` now returns `Promise<ReadableStream>` (#20)
#### Type
* Add "datetimepicker" support (#21)
* Fix typo in `TemplateURIAction` type (#21)
#### Misc
* Package updates and corresponding fixes
* Use npm 5 instead of Yarn in dev
* Fix `clean` script to work in Windows
* Use "axios" for internal HTTP client instead of "got" (#20)
## 2.0.0 (12 June 2017)
#### Type
* Use literal types for 'type' fields
#### Misc
* Update yarn.lock with the latest Yarn
## 1.1.0 (31 May 2017)
* Handle pre-parsed body (string and buffer only)
#### Type
* Separate config type into client and middleware types
* Add `userId` to group and room event sources
#### Misc
* Create issue template (#4)
## 1.0.0 (11 May 2017)
* Initial release
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dl_oss_dev@linecorp.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
# How to contribute to LINE Bot SDK for Node.js
First of all, thank you so much for taking your time to contribute! LINE Bot SDK
for Node.js is not very different from any other open source projects. It will
be fantastic if you help us by doing any of the following:
- File an issue in [the issue tracker](https://github.com/line/line-bot-sdk-nodejs/issues)
to report bugs and propose new features and improvements.
- Ask a question using [the issue tracker](https://github.com/line/line-bot-sdk-nodejs/issues).
- Contribute your work by sending [a pull request](https://github.com/line/line-bot-sdk-nodejs/pulls).
## Development
You can freely fork the project, clone the forked repository, and start editing.
Here are each top-level directory explained:
* `lib`: TypeScript source code. You may modify files under this directory.
* `test`: Mocha test suites. Please add tests for modification if possible.
* `examples`: Example projects using this SDK
* `docs`: [VuePress](https://vuepress.vuejs.org) markdowns for project documentation
* `tools`: Useful tools
Also, you may use the following npm scripts for development:
* `npm run test`: Run test suites in `test`.
* `npm run format`: Format source code with [Prettier](https://github.com/prettier/prettier)
* `npm run format:check`: Silently run `format` and report formatting errors
* `npm run build`: Build TypeScript code into JavaScript. The built files will
be placed in `dist/`.
* `npm run docs`: Build and serve documentation
We test, lint and build on CI, but it is always nice to check them before
uploading a pull request.
## Contributor license agreement
When you are sending a pull request and it's a non-trivial change beyond fixing typos, please make sure to sign
[the ICLA (individual contributor license agreement)](https://cla-assistant.io/line/line-bot-sdk-nodejs). Please
[contact us](mailto:dl_oss_dev@linecorp.com) if you need the CCLA (corporate contributor license agreement).
\ No newline at end of file
This diff is collapsed. Click to expand it.
# LINE Messaging API SDK for nodejs
[![Travis CI](https://travis-ci.org/line/line-bot-sdk-nodejs.svg?branch=master)](https://travis-ci.org/line/line-bot-sdk-nodejs)
[![npmjs](https://badge.fury.io/js/%40line%2Fbot-sdk.svg)](https://www.npmjs.com/package/@line/bot-sdk)
## Introduction
The LINE Messaging API SDK for nodejs makes it easy to develop bots using LINE Messaging API, and you can create a sample bot within minutes.
## Documentation
See the official API documentation for more information
- English: https://developers.line.biz/en/docs/messaging-api/overview/
- Japanese: https://developers.line.biz/ja/docs/messaging-api/overview/
line-bot-sdk-nodejs documentation: https://line.github.io/line-bot-sdk-nodejs/#getting-started
## Requirements
* **Node.js** 10 or higher
## Installation
Using [npm](https://www.npmjs.com/):
``` bash
$ npm install @line/bot-sdk --save
```
## Help and media
FAQ: https://developers.line.biz/en/faq/
Community Q&A: https://www.line-community.me/questions
News: https://developers.line.biz/en/news/
Twitter: @LINE_DEV
## Versioning
This project respects semantic versioning
See http://semver.org/
## Contributing
Please check [CONTRIBUTING](CONTRIBUTING.md) before making a contribution.
## License
```
Copyright (C) 2016 LINE Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
module.exports = {
base: "/line-bot-sdk-nodejs/",
head: [
["link", { rel: "icon", href: "/favicon.ico" }]
],
title: "line-bot-sdk-nodejs",
description: "Node.js SDK for LINE Messaging API",
themeConfig: {
nav: [
{
text: "Introduction",
link: "/"
},
{
text: "Getting Started",
link: "/getting-started"
},
{
text: "Guide",
link: "/guide"
},
{
text: "API Reference",
link: "/api-reference"
},
{
text: "Contributing",
link: "/CONTRIBUTING"
},
{
text: "LINE Developers",
link: "https://developers.line.biz/en/"
},
{
text: "GitHub",
link: "https://github.com/line/line-bot-sdk-nodejs/"
},
],
sidebar: [
{
title: "Introduction",
collapsable: false,
children: [
"",
]
},
{
title: "Getting Started",
collapsable: false,
children: [
"/getting-started/requirements",
"/getting-started/install",
"/getting-started/basic-usage",
]
},
{
title: "Guide",
collapsable: false,
children: [
"/guide/webhook",
"/guide/client",
"/guide/typescript",
]
},
{
title: "API Reference",
collapsable: false,
children: [
"/api-reference/client",
"/api-reference/validate-signature",
"/api-reference/middleware",
"/api-reference/exceptions",
"/api-reference/message-and-event-objects",
]
},
{
title: "Contributing",
collapsable: false,
children: [
"/CONTRIBUTING",
]
},
]
}
}
No preview for this file type
../CONTRIBUTING.md
\ No newline at end of file
../README.md
\ No newline at end of file
# API Reference
Please import the library via `require` or `import`.
``` js
// CommonJS
const line = require('@line/bot-sdk');
// ES2015 modules or TypeScript
import * as line from '@line/bot-sdk';
```
For the detailed API reference of each, please refer to their own pages.
- [Client](api-reference/client.md)
- [OAuth](api-reference/oauth.md)
- [validateSignature](api-reference/validate-signature.md)
- [middleware](api-reference/middleware.md)
- [Exceptions](api-reference/exceptions.md)
- [Message and event objects](api-reference/message-and-event-objects.md)
This diff is collapsed. Click to expand it.
# Exceptions
Exception classes can also be imported from `@line/bot-sdk`.
``` js
// CommonJS (destructuring can be used for Node.js >= 6)
const HTTPError = require('@line/bot-sdk').HTTPError;
const JSONParseError = require('@line/bot-sdk').JSONParseError;
const ReadError = require('@line/bot-sdk').ReadError;
const RequestError = require('@line/bot-sdk').RequestError;
const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed;
// ES2015 modules or TypeScript
import {
HTTPError,
JSONParseError,
ReadError,
RequestError,
SignatureValidationFailed,
} from '@line/bot-sdk/exceptions';
```
#### Type signature
``` typescript
class SignatureValidationFailed extends Error {
public signature?: string;
}
class JSONParseError extends Error {
public raw: any;
}
class RequestError extends Error {
public code: string; // e.g. ECONNREFUSED
}
class ReadError extends Error {
}
class HTTPError extends Error {
public statusCode: number; // e.g. 404
public statusMessage: string; // e.g. Not Found
}
```
About what causes the errors and how to handle them, please refer to each guide
of [webhook](../guide/webhook.md) and [client](../guide/client.md).
# Message and event objects
The message objects and event objects are plain JS objects with no
abstraction. This SDK provides TypeScript types for them, which can be imported
from `@line/bot-sdk`.
Please beware that the types only work in TypeScript, and will be removed when
built into JavaScript.
``` typescript
import {
// webhook event objects
WebhookEvent,
MessageEvent,
EventSource,
VideoEventMessage,
// message event objects
Message,
TemplateMessage,
TemplateContent,
} from "@line/bot-sdk";
```
For the actual type definitions, please refer to [types.ts](https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts)
directly.
You can also refer to the official specification:
- [Message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects)
- [Webhook event objects](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
# `middleware(config)`
It returns a [connect](https://github.com/senchalabs/connect) middleware used
by several Node.js web frameworks such as [Express](https://expressjs.com/).
#### Type signature
``` typescript
function middleware(config: MiddlewareConfig): Middleware
```
The types of `MiddlewareConfig` and `Middleware` are like below.
``` typescript
interface MiddlewareConfig {
channelAccessToken?: string;
channelSecret: string;
}
type Middleware =
( req: http.IncomingMessage
, res: http.ServerResponse
, next: (err?: Error) => void
) => void
```
The `Middleware` type is defined according to the connect middleware itself. For
the detail of the connect middleware, please refer to the [connect](https://github.com/senchalabs/connect) documentation.
## Usage
A very simple example of the middleware usage with an Express app is like below:
``` js
// globally
app.use(middleware(config))
// or directly with handler
app.post('/webhook', middleware(config), (req, res) => {
req.body.events // webhook event objects
req.body.destination // user ID of the bot (optional)
...
})
```
The middleware returned by `middleware()` parses body and checks signature
validation, so you do not need to use [`validateSignature()`](./validate-signature.md)
directly.
You do not need to use [body-parser](https://github.com/expressjs/body-parser)
to parse webhook events, as `middleware()` embeds body-parser and parses them to
objects. Please keep in mind that it will not process requests without
`X-Line-Signature` header. If you have a reason to use body-parser for other
routes, *please do not use it before the LINE middleware*. body-parser parses
the request body up and the LINE middleware cannot parse it afterwards.
``` js
// don't
app.use(bodyParser.json())
app.use(middleware(config))
// do
app.use(middleware(config))
app.use(bodyParser.json())
```
There are environments where `req.body` is pre-parsed, such as [Firebase Cloud Functions](https://firebase.google.com/docs/functions/http-events).
If it parses the body into string or buffer, do not worry as the middleware will
work just fine. If the pre-parsed body is an object, please use [`validateSignature()`](../api-reference/validate-signature.md)
manually with the raw body.
About building webhook server, please refer to [Webhook](../guide/webhook.md).
# `new OAuth()`
`OAuth` is a class representing OAuth APIs. It provides methods
corresponding to [messaging APIs](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token).
#### Type signature
``` typescript
class OAuth {
constructor() {}
issueAccessToken(client_id: string, client_secret: string): Promise<Types.ChannelAccessToken>
revokeAccessToken(access_token: string): Promise<{}>
issueChannelAccessTokenV2_1(
client_assertion: string,
): Promise<Types.ChannelAccessToken>
getChannelAccessTokenKeyIdsV2_1(
client_assertion: string,
): Promise<{ key_ids: string[] }>
revokeChannelAccessTokenV2_1(
client_id: string,
client_secret: string,
access_token: string,
): Promise<{}>
}
```
## Create a OAuth
The `OAuth` class is provided by the main module.
``` js
// CommonJS
const { OAuth } = require('@line/bot-sdk');
// ES6 modules or TypeScript
import { OAuth } from '@line/bot-sdk';
```
To create a client instance:
```js
const oauth = new OAuth();
```
And now you can call client functions as usual:
``` js
const { access_token } = await oauth.issueAccessToken("client_id", "client_secret");
```
## Methods
For functions returning `Promise`, there will be errors thrown if something
goes wrong, such as HTTP errors or parsing errors. You can catch them with the
`.catch()` method of the promises. The detailed error handling is explained
in [the Client guide](../guide/client.md).
### OAuth
#### `issueAccessToken(client_id: string, client_secret: string): Promise<Types.ChannelAccessToken>`
It corresponds to the [Issue channel access token](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token) API.
``` js
const { access_token, expires_in, token_type } = await oauth.issueAccessToken("client_id", "client_secret");
```
#### `revokeAccessToken(access_token: string): Promise<{}>`
It corresponds to the [Revoke channel access token](https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token) API.
``` js
await oauth.revokeAccessToken("access_token");
```
#### issueChannelAccessTokenV2_1(client_assertion: string): Promise<Types.ChannelAccessToken>
It corresponds to the [Issue channel access token v2.1](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token-v2-1) API.
#### getChannelAccessTokenKeyIdsV2_1(client_assertion: string): Promise<{ key_ids: string[] }>
It corresponds to the [Get all valid channel access token key IDs v2.1](https://developers.line.biz/en/reference/messaging-api/#get-all-issued-channel-access-token-key-ids-v2-1) API.
#### revokeChannelAccessTokenV2_1(client_id: string, client_secret: string, access_token: string): Promise<{}>
It corresponds to the [Revoke channel access token v2.1](https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token-v2-1) API.
# `validateSignature(body, channelSecret, signature)`
It is a function to check if a provided channel secret is valid, compared with a
provided body.
#### Type signature
``` typescript
function validateSignature(
body: string | Buffer,
channelSecret: string,
signature: string,
): boolean
```
`body` can be a string or buffer. When it's a string, it will be handled as if
it's encoded in UTF-8.
For more details about signature validation of LINE webhook, please refer
to [the official documentation](https://developers.line.biz/en/reference/messaging-api/#webhooks).
# Getting Started
* [Requirements](getting-started/requirements.md)
* [Install](getting-started/install.md)
* [Basic Usage](getting-started/basic-usage.md)
# Basic Usage
It can be imported with [CommonJS](https://nodejs.org/docs/latest/api/modules.html),
[ES2015 modules](https://babeljs.io/learn-es2015/#ecmascript-2015-features-modules),
and preferably [TypeScript](https://www.typescriptlang.org/).
The library is written in TypeScript and includes TypeScript definitions by
default. Nevertheless, it can surely be used with plain JavaScript too.
``` js
// CommonJS
const line = require('@line/bot-sdk');
// ES2015 modules or TypeScript
import * as line from '@line/bot-sdk';
```
## Configuration
For the usage of webhook and client, LINE channel access token and secret are
needed. About issuing the token and secret, please refer to [Getting started with the Messaging API](https://developers.line.biz/en/docs/messaging-api/getting-started/).
``` js
const config = {
channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
channelSecret: 'YOUR_CHANNEL_SECRET'
};
new line.Client(config);
line.middleware(config);
```
## Synopsis
Here is a synopsis of echoing webhook server with [Express](https://expressjs.com/):
``` js
const express = require('express');
const line = require('@line/bot-sdk');
const config = {
channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
channelSecret: 'YOUR_CHANNEL_SECRET'
};
const app = express();
app.post('/webhook', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result));
});
const client = new line.Client(config);
function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null);
}
return client.replyMessage(event.replyToken, {
type: 'text',
text: event.message.text
});
}
app.listen(3000);
```
The full examples with comments can be found in [examples](https://github.com/line/line-bot-sdk-nodejs/tree/master/examples/).
For the specifications of API, please refer to [API Reference](../api-reference.md).
# Install
Please install via [npm](https://www.npmjs.com/).
```bash
$ npm install @line/bot-sdk
```
You can build from source. Please clone the repository and run the following
scripts to build.
``` bash
$ git clone https://github.com/line/line-bot-sdk-nodejs
$ cd line-bot-sdk-nodejs
$ npm install
$ npm run build
```
The built result will be placed in `dist/`.
For the details of development, please refer to [Contributing](../../CONTRIBUTING.md).
# Requirements
* **Node.js** >= 4, preferably >=6
* It uses ES2015.
* [**npm**](https://www.npmjs.com/), preferably >=5
Other dependencies are installed via npm, and do not need to be pre-installed.
# Guide
* [Webhook](guide/webhook.md)
* [Client](guide/client.md)
* [TypeScript](guide/typescript.md)
# Client
Client is to send messages, get user or content information, or leave chats.
A client instance provides functions for [messaging APIs](https://developers.line.biz/en/reference/messaging-api/),
so that you do not need to worry about HTTP requests and can focus on data.
For type signatures of the methods, please refer to [its API reference](../api-reference/client.md).
## Create a client
The `Client` class is provided by the main module.
``` js
// CommonJS
const Client = require('@line/bot-sdk').Client;
// ES6 modules or TypeScript
import { Client } from '@line/bot-sdk';
```
To create a client instance:
```js
const client = new Client({
channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
channelSecret: 'YOUR_CHANNEL_SECRET'
});
```
And now you can call client functions as usual:
``` js
client.pushMessage(userId, { type: 'text', text: 'hello, world' });
```
## Retrieving parameters from webhook
Many of data used in the client functions, such as user IDs or reply tokens, can
be obtained from nowhere but webhook.
Webhook event objects are just plain JSON objects, sent as request body, so you
can easily access and use it.
``` js
const event = req.body.events[0];
if (event.type === 'message') {
const message = event.message;
if (message.type === 'text' && message.text === 'bye') {
if (event.source.type === 'room') {
client.leaveRoom(event.source.roomId);
} else if (event.source.type === 'group') {
client.leaveGroup(event.source.groupId);
} else {
client.replyMessage(event.replyToken, {
type: 'text',
text: 'I cannot leave a 1-on-1 chat!',
});
}
}
}
```
For more detail of building webhook and retrieve event objects, please refer to
its [guide](./webhook.html).
## Error handling
There are 4 types of errors caused by client usage.
- `RequestError`: A request fails by, for example, wrong domain or server
refusal.
- `ReadError`: Reading from a response pipe fails.
- `HTTPError`: Server returns a non-2xx response.
- `JSONParseError`: JSON parsing fails for response body.
For methods returning `Promise`, you can handle the errors with [`catch()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)
method. For others returning `ReadableStream`, you can observe the `'error'`
event for the stream.
``` js
client
.replyMessage(replyToken, message)
.catch((err) => {
if (err instanceof HTTPError) {
console.error(err.statusCode);
}
});
const stream = client.getMessageContent(messageId);
stream.on('error', (err) => {
console.log(err.message);
});
```
You can check which method returns `Promise` or `ReadableStream` in the API
reference of [`Client`](../api-reference/client.md). For type signatures of the
errors above, please refer to [Exceptions](../api-reference/exceptions.md).
# TypeScript
[TypeScript](https://www.typescriptlang.org/) is a statically typed language
that compiled to plain JavaScript. As you may already have found, This library
is written in TypeScript.
When installed via npm, the built JavaScript files are already included and
you do not need to worry about TypeScript, but it may be nice to consider
using TypeScript for implement what you need.
## What's good about using TypeScript
It provides a default type set for mostly used objects in webhook and client
and prevent possible typo and mistakes.
``` typescript
const config = {
channelAccessToken: "", // typo Token
channelSecret: "",
}
const c = new Client(config) // will throw a compile error
```
Also, when building a complex message object, you can make use of types for
its fields.
``` typescript
const message: TemplateMessage = {
type: "template",
altText: "cannot display template message",
template: {
type: "carousel",
columns: [ {
text: "col1",
title: "Column 1",
actions: [ {
type: "message",
label: "send message",
text: "hi, there",
} ],
} ],
},
}
```
The object above will be type-checked to have the type of
`TemplateMessage`, and thus ensured not to miss any required field.
Also, [literal type](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
is used for `type` fields, which means the compiler will complain if a wrong
type string is used, and also inference the type of objects by its `type` field.
## How to use
The library is built to just-work with TypeScript too, so import the library and
there you go.
``` typescript
import {
// main APIs
Client,
middleware,
// exceptions
JSONParseError,
SignatureValidationFailed,
// types
TemplateMessage,
WebhookEvent,
} from "@line/bot-sdk";
```
Message object and webhook event types can be also imported from `@line/bot-sdk`,
e.g. `TemplateMessage` or `WebhookEvent`. For declarations of the types, please
refer to [types.ts](https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts).
# Webhook
A webhook server for LINE messaging API is just a plain HTTP(S) server. When
there is a observable user event, an HTTP request will be sent to a
pre-configured webhook server.
About configuration of webhook itself, please refer to [Webhook](https://developers.line.biz/en/reference/messaging-api/#webhooks)
of the official document.
## What a webhook server should do
- [Signature validation](https://developers.line.biz/en/reference/messaging-api/#signature-validation)
- [Webhook event object parsing](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
**Signature validation** is checking if a request is actually sent from real
LINE servers, not a fraud. The validation is conducted by checking
the [X-Line-Signature](https://developers.line.biz/en/reference/messaging-api/#signature-validation) header
and request body. There is a [`validateSignature()`](../api-reference/validate-signature.md)
function to do this.
**Webhook event object parsing** is literally parsing webhook event objects,
which contains information of each webhook event. The objects are provided as
request body in JSON format, so any body parser will work here. For interal
object types in this SDK, please refer to [Message and event objects](../api-reference/message-and-event-objects.md).
There is a function to generate a [connect](https://github.com/senchalabs/connect) middleware,
[`middleware()`](../api-reference/middleware.md), to conduct both of them. If
your server can make use of connect middlewares, such as [Express](https://expressjs.com/),
using the middleware is a recommended way to build a webhook server.
## Build a webhook server with Express
[Express](https://expressjs.com/) is a minimal web framework for Node.js, which
is widely used in Node.js communities. You can surely build a webhook server
with any web framework, but we use Express as an example here for its
popularity.
We skip the detailed guide for Express. If more information is needed about
Express, please refer to its documentation.
Here is an example of an HTTP server built with Express.
``` js
const express = require('express')
const app = express()
app.post('/webhook', (req, res) => {
res.json({})
})
app.listen(8080)
```
The server above listens to 8080 and will response with an empty object for
`POST /webhook`. We will add webhook functionality to this server.
``` js
const express = require('express')
const middleware = require('@line/bot-sdk').middleware
const app = express()
const config = {
channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
channelSecret: 'YOUR_CHANNEL_SECRET'
}
app.post('/webhook', middleware(config), (req, res) => {
req.body.events // webhook event objects
req.body.destination // user ID of the bot (optional)
...
})
app.listen(8080)
```
We have imported `middleware` from the package and make the Express app to use
the middleware. The middlware validates the request and parses webhook event
object. It embeds body-parser and parses them to objects. If you have a reason
to use another body-parser separately for other routes, please keep in mind the
followings.
### Do not use the webhook `middleware()` for other usual routes
``` js
// don't
app.use(middleware(config))
// do
app.use('/webhook', middleware(config))
```
The middleware will throw an exception when the [X-Line-Signature](https://developers.line.biz/en/reference/messaging-api/#signature-validation)
header is not set. If you want to handle usual user requests, the middleware
shouldn't be used for them.
### Do not use another body-parser before the webhook `middleware()`
``` js
// don't
app.use(bodyParser.json())
app.use('/webhook', middleware(config))
// do
app.use('/webhook', middleware(config))
app.use(bodyParser.json())
```
If another body parser already parsed a request's body, the webhook middleware
cannot access to the raw body of the request. The raw body should be retrieved
for signature validation.
However, there are environments where `req.body` is pre-parsed, such as
[Firebase Cloud Functions](https://firebase.google.com/docs/functions/http-events).
If it parses the body into string or buffer, the middleware will use the body
as it is and work just fine. If the pre-parsed body is an object, the webhook
middleware will fail to work. In the case, please use [`validateSignature()`](../api-reference/validate-signature.md)
manually with raw body.
## Error handling
There are two types of errors thrown by the middleware, one is `SignatureValidationFailed`
and the other is `JSONParseError`.
- `SignatureValidationFailed` is thrown when a request doesn't have a signature.
- `SignatureValidationFailed` is thrown when a request has a wrong signature.
- `JSONParseError` occurs when a request body cannot be parsed as JSON.
For type references of the errors, please refer to [the API reference](../api-reference/exceptions.md).
The errors can be handled with [error middleware](https://github.com/senchalabs/connect#error-middleware).
``` js
const express = require('express')
const middleware = require('@line/bot-sdk').middleware
const JSONParseError = require('@line/bot-sdk').JSONParseError
const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed
const app = express()
const config = {
channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
channelSecret: 'YOUR_CHANNEL_SECRET'
}
app.use(middleware(config))
app.post('/webhook', (req, res) => {
res.json(req.body.events) // req.body will be webhook event object
})
app.use((err, req, res, next) => {
if (err instanceof SignatureValidationFailed) {
res.status(401).send(err.signature)
return
} else if (err instanceof JSONParseError) {
res.status(400).send(err.raw)
return
}
next(err) // will throw default 500
})
app.listen(8080)
```
## HTTPS
The webhook URL should have HTTPS protocol. There are several ways to build an
HTTPS server. For example, here is a [documentation](https://expressjs.com/en/api.html#app.listen)
of making Express work with HTTPS. You can also set HTTPS in web servers like
[NGINX](https://www.nginx.com/). This guide will not cover HTTPS configuration,
but do not forget to set HTTPS beforehand.
For development and test usages, [ngrok](https://ngrok.com/) works perfectly.
# Dependencies
node_modules/
# Built files.
dist/
# LINE Echo Bot with TypeScript
An example LINE bot to echo message with TypeScript. The bot is coded according to TypeScript's best practices.
## Prerequisite
- Git
- Node.js version 10 and up
- Heroku CLI (optional)
- LINE Developers Account for the bot
## Installation
- Clone the repository.
```bash
git clone https://github.com/line/line-bot-sdk-nodejs.git
```
- Change directory to the example.
```bash
cd line-bot-sdk-nodejs/examples/echo-bot-ts
```
- Install all dependencies.
```bash
npm install
```
- Configure all of the environment variables.
```bash
export CHANNEL_ACCESS_TOKEN=<YOUR_CHANNEL_ACCESS_TOKEN>
export CHANNEL_SECRET=<YOUR_CHANNEL_SECRET>
export PORT=<YOUR_PORT>
```
- Setup your webhook URL in your LINE Official Account to be in the following format. Don't forget to disable the greeting messages and auto-response messages for convenience.
```bash
https://example-url.com/webhook
```
- Compile the TypeScript files.
```bash
npm run build
```
- Run the application.
```bash
npm start
```
## Alternative Installation
If you want to deploy it via Heroku, it is also possible and is even easier for testing purposes.
- Clone the repository.
```bash
git clone https://github.com/line/line-bot-sdk-nodejs.git
```
- Change directory to the example.
```bash
cd line-bot-sdk-nodejs/examples/echo-bot-ts
```
- Create a Heroku application.
```bash
git init
heroku create <YOUR_APP_NAME> # Do not specify for a random name
```
- Setup the environment variables, and don't forget to setup your webhook URL (from the Heroku application that you have just created) in your LINE Offical Account. The webhook URL will still accept the following format: `https://example-url.com.herokuapp.com/webhook`.
```bash
heroku config:set CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
heroku config:set CHANNEL_SECRET=YOUR_CHANNEL_SECRET
```
- Push the application to the server.
```bash
git add .
git commit -m "Initial commit for Heroku testing"
git push heroku master
```
- Open your application.
```bash
heroku open
```
- Done!
declare global {
namespace NodeJS {
interface ProcessEnv {
CHANNEL_ACCESS_TOKEN: string;
CHANNEL_SECRET: string;
PORT: string;
}
}
}
export {};
// Import all dependencies, mostly using destructuring for better view.
import { ClientConfig, Client, middleware, MiddlewareConfig, WebhookEvent, TextMessage, MessageAPIResponseBase } from '@line/bot-sdk';
import express, { Application, Request, Response } from 'express';
// Setup all LINE client and Express configurations.
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET,
};
const middlewareConfig: MiddlewareConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET || '',
};
const PORT = process.env.PORT || 3000;
// Create a new LINE SDK client.
const client = new Client(clientConfig);
// Create a new Express application.
const app: Application = express();
// Function handler to receive the text.
const textEventHandler = async (event: WebhookEvent): Promise<MessageAPIResponseBase | undefined> => {
// Process all variables here.
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
// Process all message related variables here.
const { replyToken } = event;
const { text } = event.message;
// Create a new message.
const response: TextMessage = {
type: 'text',
text,
};
// Reply to the user.
await client.replyMessage(replyToken, response);
};
// Register the LINE middleware.
// As an alternative, you could also pass the middleware in the route handler, which is what is used here.
// app.use(middleware(middlewareConfig));
// Route handler to receive webhook events.
// This route is used to receive connection tests.
app.get(
'/',
async (_: Request, res: Response): Promise<Response> => {
return res.status(200).json({
status: 'success',
message: 'Connected successfully!',
});
}
);
// This route is used for the Webhook.
app.post(
'/webhook',
middleware(middlewareConfig),
async (req: Request, res: Response): Promise<Response> => {
const events: WebhookEvent[] = req.body.events;
// Process all of the received events asynchronously.
const results = await Promise.all(
events.map(async (event: WebhookEvent) => {
try {
await textEventHandler(event);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err);
}
// Return an error message.
return res.status(500).json({
status: 'error',
});
}
})
);
// Return a successfull message.
return res.status(200).json({
status: 'success',
results,
});
}
);
// Create a server and listen to it.
app.listen(PORT, () => {
console.log(`Application is live and listening on port ${PORT}`);
});
This diff is collapsed. Click to expand it.
{
"name": "echo-bot-ts",
"version": "0.0.0",
"description": "An example LINE bot with TypeScript made to echo messages",
"main": "./dist/index.js",
"scripts": {
"clean": "rimraf ./dist",
"build": "npm run clean && tsc",
"start": "node dist/index.js"
},
"author": "Nicholas Dwiarto <nicholasdwiarto@yahoo.com> (https://nicholasdw.com)",
"dependencies": {
"@line/bot-sdk": "^7.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"@types/node": "^14.14.14",
"rimraf": "^3.0.2",
"typescript": "^4.1.3"
}
}
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": ["node_modules"]
}
# Echo Bot
An example LINE bot just to echo messages
## How to use
### Install deps
``` shell
$ npm install
```
### Configuration
``` shell
$ export CHANNEL_SECRET=YOUR_CHANNEL_SECRET
$ export CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
$ export PORT=1234
```
### Run
``` shell
$ node .
```
## Webhook URL
```
https://your.base.url/callback
```
'use strict';
const line = require('@line/bot-sdk');
const express = require('express');
// create LINE SDK config from env variables
const config = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET,
};
// create LINE SDK client
const client = new line.Client(config);
// create Express app
// about Express itself: https://expressjs.com/
const app = express();
// register a webhook handler with middleware
// about the middleware, please refer to doc
app.post('/callback', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result))
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
// event handler
function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
// ignore non-text-message event
return Promise.resolve(null);
}
// create a echoing text message
const echo = { type: 'text', text: event.message.text };
// use reply API
return client.replyMessage(event.replyToken, echo);
}
// listen on port
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`listening on ${port}`);
});
This diff is collapsed. Click to expand it.
{
"name": "echo-bot",
"version": "0.0.0",
"description": "An example LINE bot just to echo messages",
"main": "index.js",
"scripts": {
"start": "node ."
},
"dependencies": {
"@line/bot-sdk": "^6.8.0",
"express": "^4.16.3"
}
}
No preview for this file type
/downloaded/*
!/downloaded/.gitkeep
# Kitchen Sink Bot
A kitchen-sink LINE bot example
## Requirements
Install npm dependencies:
```bash
npm run build-sdk # build SDK installed from local directory
npm install
```
Also, FFmpeg and ImageMagick should be installed to test image and video
echoing.
### About local dependencies
Currently, [`@line/bot-sdk`](package.json) is installed from local directory.
```json
{
"@line/bot-sdk": "../../"
}
```
To install `@line/bot-sdk` from npm, please update the line with the following:
```json
{
"@line/bot-sdk": "*"
}
```
In the case, `npm run build-sdk` needn't be run before `npm install`.
## Configuration
Configuration can be done via environment variables.
```bash
export CHANNEL_SECRET=YOUR_CHANNEL_SECRET
export CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
export BASE_URL=https://your.base.url # for static file serving
export PORT=1234
```
The code above is an example of Bash. It may differ in other shells.
## Run webhook server
```bash
npm start
```
With the configuration above, the webhook listens on `https://your.base.url:1234/callback`.
## ngrok usage
[ngrok](https://ngrok.com/) tunnels extenral requests to localhost, helps
debugging local webhooks.
This example includes ngrok inside, and it just works if no `BASE_URL` is
set. Make sure that other configurations are set correctly.
```
❯ npm start
...
It seems that BASE_URL is not set. Connecting to ngrok...
listening on https://ffffffff.ngrok.io/callback
```
The URL can be directly registered as the webhook URL in LINE Developers
console.
This diff could not be displayed because it is too large.
{
"name": "kitchensink",
"version": "0.0.0",
"description": "A kitchen-sink LINE bot example",
"main": "index.js",
"scripts": {
"build-sdk": "cd ../../; npm i; npm run build",
"start": "node ."
},
"dependencies": {
"@line/bot-sdk": "../../",
"express": "^4.17.1",
"ngrok": "^3.2.7"
}
}
This diff is collapsed. Click to expand it.
export const MESSAGING_API_PREFIX = `https://api.line.me/v2/bot`;
export const DATA_API_PREFIX = `https://api-data.line.me/v2/bot`;
export const OAUTH_BASE_PREFIX = `https://api.line.me/v2/oauth`;
export const OAUTH_BASE_PREFIX_V2_1 = `https://api.line.me/oauth2/v2.1`;
export class SignatureValidationFailed extends Error {
constructor(message: string, public signature?: string) {
super(message);
}
}
export class JSONParseError extends Error {
constructor(message: string, public raw: any) {
super(message);
}
}
export class RequestError extends Error {
constructor(
message: string,
public code: string,
private originalError: Error,
) {
super(message);
}
}
export class ReadError extends Error {
constructor(private originalError: Error) {
super(originalError.message);
}
}
export class HTTPError extends Error {
constructor(
message: string,
public statusCode: number,
public statusMessage: string,
public originalError: any,
) {
super(message);
}
}
import axios, {
AxiosInstance,
AxiosError,
AxiosResponse,
AxiosRequestConfig,
} from "axios";
import { Readable } from "stream";
import { HTTPError, ReadError, RequestError } from "./exceptions";
import * as fileType from "file-type";
import * as qs from "querystring";
const pkg = require("../package.json");
interface httpClientConfig extends Partial<AxiosRequestConfig> {
baseURL?: string;
defaultHeaders?: any;
responseParser?: <T>(res: AxiosResponse) => T;
}
export default class HTTPClient {
private instance: AxiosInstance;
private config: httpClientConfig;
constructor(config: httpClientConfig = {}) {
this.config = config;
const { baseURL, defaultHeaders } = config;
this.instance = axios.create({
baseURL,
headers: Object.assign({}, defaultHeaders, {
"User-Agent": `${pkg.name}/${pkg.version}`,
}),
});
this.instance.interceptors.response.use(
res => res,
err => Promise.reject(this.wrapError(err)),
);
}
public async get<T>(url: string, params?: any): Promise<T> {
const res = await this.instance.get(url, { params });
return res.data;
}
public async getStream(url: string, params?: any): Promise<Readable> {
const res = await this.instance.get(url, {
params,
responseType: "stream",
});
return res.data as Readable;
}
public async post<T>(
url: string,
body?: any,
config?: Partial<AxiosRequestConfig>,
): Promise<T> {
const res = await this.instance.post(url, body, {
headers: {
"Content-Type": "application/json",
...(config && config.headers),
},
...config,
});
return this.responseParse(res);
}
private responseParse(res: AxiosResponse) {
const { responseParser } = this.config;
if (responseParser) return responseParser(res);
else return res.data;
}
public async put<T>(
url: string,
body?: any,
config?: Partial<AxiosRequestConfig>,
): Promise<T> {
const res = await this.instance.put(url, body, {
headers: {
"Content-Type": "application/json",
...(config && config.headers),
},
...config,
});
return this.responseParse(res);
}
public async postForm<T>(url: string, body?: any): Promise<T> {
const res = await this.instance.post(url, qs.stringify(body), {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
return res.data;
}
public async toBuffer(data: Buffer | Readable) {
if (Buffer.isBuffer(data)) {
return data;
} else if (data instanceof Readable) {
return await new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
let size = 0;
data.on("data", (chunk: Buffer) => {
buffers.push(chunk);
size += chunk.length;
});
data.on("end", () => resolve(Buffer.concat(buffers, size)));
data.on("error", reject);
});
} else {
throw new Error("invalid data type for binary data");
}
}
public async postBinary<T>(
url: string,
data: Buffer | Readable,
contentType?: string,
): Promise<T> {
const buffer = await this.toBuffer(data);
const res = await this.instance.post(url, buffer, {
headers: {
"Content-Type": contentType || (await fileType.fromBuffer(buffer)).mime,
"Content-Length": buffer.length,
},
});
return res.data;
}
public async delete<T>(url: string, params?: any): Promise<T> {
const res = await this.instance.delete(url, { params });
return res.data;
}
private wrapError(err: AxiosError): Error {
if (err.response) {
return new HTTPError(
err.message,
err.response.status,
err.response.statusText,
err,
);
} else if (err.code) {
return new RequestError(err.message, err.code, err);
} else if (err.config) {
// unknown, but from axios
return new ReadError(err);
}
// otherwise, just rethrow
return err;
}
}
import Client, { OAuth } from "./client";
import middleware from "./middleware";
import validateSignature from "./validate-signature";
export { Client, middleware, validateSignature, OAuth };
// re-export exceptions and types
export * from "./exceptions";
export * from "./types";
import { raw } from "body-parser";
import * as http from "http";
import { JSONParseError, SignatureValidationFailed } from "./exceptions";
import * as Types from "./types";
import validateSignature from "./validate-signature";
export type Request = http.IncomingMessage & { body: any };
export type Response = http.ServerResponse;
export type NextCallback = (err?: Error) => void;
export type Middleware = (
req: Request,
res: Response,
next: NextCallback,
) => void | Promise<void>;
function isValidBody(body?: any): body is string | Buffer {
return (body && typeof body === "string") || Buffer.isBuffer(body);
}
export default function middleware(config: Types.MiddlewareConfig): Middleware {
if (!config.channelSecret) {
throw new Error("no channel secret");
}
const secret = config.channelSecret;
const _middleware: Middleware = async (req, res, next) => {
// header names are lower-cased
// https://nodejs.org/api/http.html#http_message_headers
const signature = req.headers[
Types.LINE_SIGNATURE_HTTP_HEADER_NAME
] as string;
if (!signature) {
next(new SignatureValidationFailed("no signature"));
return;
}
const body = await (async (): Promise<string | Buffer> => {
if (isValidBody((req as any).rawBody)) {
// rawBody is provided in Google Cloud Functions and others
return (req as any).rawBody;
} else if (isValidBody(req.body)) {
return req.body;
} else {
// body may not be parsed yet, parse it to a buffer
return new Promise<Buffer>((resolve, reject) =>
raw({ type: "*/*" })(req as any, res as any, (error: Error) =>
error ? reject(error) : resolve(req.body),
),
);
}
})();
if (!validateSignature(body, secret, signature)) {
next(
new 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 JSONParseError(err.message, strBody));
}
};
return (req, res, next): void => {
(<Promise<void>>_middleware(req, res, next)).catch(next);
};
}
This diff is collapsed. Click to expand it.
import { JSONParseError } from "./exceptions";
import * as FormData from "form-data";
export function toArray<T>(maybeArr: T | T[]): T[] {
return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
}
export function ensureJSON<T>(raw: T): T {
if (typeof raw === "object") {
return raw;
} else {
throw new JSONParseError("Failed to parse response body as JSON", raw);
}
}
export function createMultipartFormData(
this: FormData | void,
formBody: Record<string, any>,
): FormData {
const formData = this instanceof FormData ? this : new FormData();
Object.entries(formBody).forEach(([key, value]) => {
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
formData.append(key, value);
} else {
formData.append(key, String(value));
}
});
return formData;
}
import { createHmac, timingSafeEqual } from "crypto";
function s2b(str: string, encoding: BufferEncoding): Buffer {
return Buffer.from(str, encoding);
}
function safeCompare(a: Buffer, b: Buffer): boolean {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(a, b);
}
export default function validateSignature(
body: string | Buffer,
channelSecret: string,
signature: string,
): boolean {
return safeCompare(
createHmac("SHA256", channelSecret).update(body).digest(),
s2b(signature, "base64"),
);
}
This diff could not be displayed because it is too large.
{
"name": "@line/bot-sdk",
"version": "7.3.0",
"description": "Node.js SDK for LINE Messaging API",
"engines": {
"node": ">=10"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"lib"
],
"scripts": {
"pretest": "npm run format && npm run build",
"test": "TEST_PORT=1234 TS_NODE_CACHE=0 nyc mocha",
"prettier": "prettier --parser typescript --trailing-comma all --arrow-parens avoid \"{lib,test}/**/*.ts\"",
"format": "npm run prettier -- --write",
"format:check": "npm run prettier -- -l",
"clean": "rm -rf dist/*",
"prebuild": "npm run format:check && npm run clean",
"build": "tsc",
"docs": "vuepress dev docs",
"docs:build": "vuepress build docs",
"docs:deploy": "./scripts/deploy-docs.sh",
"generate-changelog": "ts-node ./scripts/generate-changelog.ts",
"release": "npm run build && npm publish --access public"
},
"repository": {
"type": "git",
"url": "git@github.com:line/line-bot-sdk-nodejs.git"
},
"keywords": [
"node",
"line",
"sdk"
],
"dependencies": {
"@types/body-parser": "^1.19.0",
"@types/node": "^14.10.0",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"file-type": "^15.0.0",
"form-data": "^3.0.0"
},
"devDependencies": {
"@types/express": "^4.17.8",
"@types/finalhandler": "^1.1.0",
"@types/mocha": "^8.0.3",
"express": "^4.17.1",
"finalhandler": "^1.1.2",
"husky": "^4.3.0",
"mocha": "^8.1.3",
"nock": "^13.0.4",
"nyc": "^15.1.0",
"prettier": "^2.1.1",
"ts-node": "^9.0.0",
"typescript": "^3.9.7",
"vuepress": "^1.5.4"
},
"husky": {
"hooks": {
"pre-commit": "npm run format:check",
"pre-push": "npm run format:check && npm run build && npm run test"
}
},
"nyc": {
"require": [
"ts-node/register"
],
"extension": [
".ts"
],
"reporter": [
"lcov",
"text"
],
"sourceMap": true,
"instrument": true
},
"mocha": {
"require": "ts-node/register",
"spec": "test/*.spec.ts"
},
"license": "Apache-2.0"
}
#!/usr/bin/env sh
# abort on errors
set -e
# build
npm run docs:build
# navigate into the build output directory
cd docs/.vuepress/dist
git init
git add -A
git commit -m 'Deploy docs'
git push -f git@github.com:line/line-bot-sdk-nodejs.git master:gh-pages
cd -
#!/bin/bash
git checkout ${GITHUB_HEAD_REF}
git config --global user.email "action@github.com"
git config --global user.name "GitHub Action"
npm run generate-changelog
git add -A
git commit -m "(Changelog CI) Added Changelog"
git push -u origin ${GITHUB_HEAD_REF}
import { execSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
const { version: lastVersion } = require("../package.json");
const changeLogPath = resolve(__dirname, "../CHANGELOG.md");
let newVersion = lastVersion;
console.log("Gets Release Version from GITHUB_EVENT_PATH");
if (process.env.GITHUB_EVENT_PATH) {
const {
pull_request: { title },
} = require(process.env.GITHUB_EVENT_PATH);
if (/^release/i.test(title))
newVersion = (title as string).match(/release ([\d\.]+)/i)[1];
else {
console.log("Not target pull request, exiting");
process.exit(0);
}
}
console.log(`New Version: ${newVersion}`);
console.log("Bump Version");
execSync(`npm version ${newVersion}`);
const gitLogOutput = execSync(
`git log v${lastVersion}... --format=%s`
).toString("utf-8");
const commitsArray = gitLogOutput
.split("\n")
.filter((message) => message && message !== "");
const category = {
miscs: [] as string[],
features: [] as string[],
bugFixes: [] as string[],
};
commitsArray.forEach((message) => {
let cat: keyof typeof category;
if (/^([\d\.]+)$/.test(message)) {
return;
} else if (message.includes("test")) {
cat = "miscs";
} else if (/(add)|(support)/i.test(message)) {
cat = "features";
} else if (/fix/i.test(message)) {
cat = "bugFixes";
} else {
cat = "miscs";
}
category[cat].push(`* ${message}`);
});
const now = new Date();
const MonthText = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
let newChangelog = `## ${newVersion} (${now.getDate()} ${
MonthText[now.getMonth()]
} ${now.getFullYear()})
`;
if (category.features.length > 0) {
newChangelog += `
### Feature
${category.features.join("\n")}
`;
}
if (category.bugFixes.length > 0) {
newChangelog += `
### Bug fix
${category.bugFixes.join("\n")}
`;
}
if (category.miscs.length > 0) {
newChangelog += `
### Misc
${category.miscs.join("\n")}
`;
}
const currentChangelog = readFileSync(changeLogPath, "utf-8");
writeFileSync(
changeLogPath,
`${newChangelog}
${currentChangelog}`
);
This diff is collapsed. Click to expand it.
import { Readable } from "stream";
export function getStreamData(stream: Readable): Promise<string> {
return new Promise(resolve => {
let result: string = "";
stream.on("data", (chunk: Buffer) => {
result += chunk.toString();
});
stream.on("end", () => {
resolve(result);
});
});
}
import * as bodyParser from "body-parser";
import * as express from "express";
import { Server } from "http";
import { join } from "path";
import { writeFileSync } from "fs";
import {
JSONParseError,
SignatureValidationFailed,
} from "../../lib/exceptions";
import * as finalhandler from "finalhandler";
let server: Server = null;
function listen(port: number, middleware?: express.RequestHandler) {
const app = express();
if (middleware) {
app.use((req: express.Request, res, next) => {
if (req.path === "/mid-text") {
bodyParser.text({ type: "*/*" })(req, res, next);
} else if (req.path === "/mid-buffer") {
bodyParser.raw({ type: "*/*" })(req, res, next);
} else if (req.path === "/mid-rawbody") {
bodyParser.raw({ type: "*/*" })(req, res, err => {
if (err) return next(err);
(req as any).rawBody = req.body;
delete req.body;
next();
});
} else if (req.path === "/mid-json") {
bodyParser.json({ type: "*/*" })(req, res, next);
} else {
next();
}
});
app.use(middleware);
}
// write request info
app.use((req: express.Request, res, next) => {
const request: any = ["headers", "method", "path", "query"].reduce(
(r, k) => Object.assign(r, { [k]: (req as any)[k] }),
{},
);
if (Buffer.isBuffer(req.body)) {
request.body = req.body.toString("base64");
} else {
request.body = req.body;
}
writeFileSync(
join(__dirname, "request.json"),
JSON.stringify(request, null, 2),
);
next();
});
// return an empty object for others
app.use((req, res) => res.json({}));
app.use(
(err: Error, req: express.Request, res: express.Response, next: any) => {
if (err instanceof SignatureValidationFailed) {
res.status(401).send(err.signature);
return;
} else if (err instanceof JSONParseError) {
res.status(400).send(err.raw);
return;
}
// https://github.com/expressjs/express/blob/2df1ad26a58bf51228d7600df0d62ed17a90ff71/lib/application.js#L162
// express will record error in console when
// there is no other handler to handle error & it is in test environment
// use final handler the same as in express application.js
finalhandler(req, res)(err);
},
);
return new Promise(resolve => {
server = app.listen(port, () => resolve());
});
}
function close() {
return new Promise(resolve => {
if (!server) {
resolve();
}
server.close(() => resolve());
});
}
export { listen, close };
import { deepEqual, equal, ok } from "assert";
import { HTTPError, RequestError } from "../lib/exceptions";
import HTTPClient from "../lib/http";
import { getStreamData } from "./helpers/stream";
import * as nock from "nock";
import { readFileSync, createReadStream } from "fs";
import { join } from "path";
const pkg = require("../package.json");
const baseURL = "https://line.me";
const defaultHeaders = {
"test-header-key": "Test-Header-Value",
};
describe("http", () => {
const http = new HTTPClient({
baseURL,
defaultHeaders,
});
before(() => nock.disableNetConnect());
afterEach(() => nock.cleanAll());
after(() => nock.enableNetConnect());
const interceptionOption = {
reqheaders: {
...defaultHeaders,
"User-Agent": `${pkg.name}/${pkg.version}`,
},
};
const mockGet = (
path: string,
expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
) => {
let _it = nock(baseURL, interceptionOption).get(path);
if (expectedQuery) {
_it = _it.query(expectedQuery);
}
return _it.reply(200, {});
};
const mockPost = (path: string, expectedBody?: nock.RequestBodyMatcher) => {
return nock(baseURL, interceptionOption)
.post(path, expectedBody)
.reply(200, {});
};
const mockDelete = (
path: string,
expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
) => {
let _it = nock(baseURL, interceptionOption).delete(path);
if (expectedQuery) {
_it = _it.query(expectedQuery);
}
return _it.reply(200, {});
};
it("get", async () => {
const scope = mockGet("/get");
const res = await http.get<any>(`/get`);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("get with query", async () => {
const scope = mockGet("/get", { x: 10 });
const res = await http.get<any>(`/get`, { x: 10 });
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("post without body", async () => {
const scope = mockPost("/post");
const res = await http.post<any>(`/post`);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("post with body", async () => {
const testBody = {
id: 12345,
message: "hello, body!",
};
const scope = mockPost("/post/body", testBody);
const res = await http.post<any>(`/post/body`, testBody);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getStream", async () => {
const scope = nock(baseURL, interceptionOption)
.get("/stream.txt")
.reply(200, () =>
createReadStream(join(__dirname, "./helpers/stream.txt")),
);
const stream = await http.getStream(`/stream.txt`);
const data = await getStreamData(stream);
equal(scope.isDone(), true);
equal(data, "hello, stream!\n");
});
it("delete", async () => {
const scope = mockDelete("/delete");
await http.delete(`/delete`);
equal(scope.isDone(), true);
});
it("delete with query", async () => {
const scope = mockDelete("/delete", { x: 10 });
await http.delete(`/delete`, { x: 10 });
equal(scope.isDone(), true);
});
const mockPostBinary = (
buffer: Buffer,
reqheaders: Record<string, nock.RequestHeaderMatcher>,
) => {
return nock(baseURL, {
reqheaders: {
...interceptionOption.reqheaders,
...reqheaders,
"content-length": buffer.length + "",
},
})
.post("/post/binary", buffer)
.reply(200, {});
};
it("postBinary", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const buffer = readFileSync(filepath);
const scope = mockPostBinary(buffer, {
"content-type": "image/png",
});
await http.postBinary(`/post/binary`, buffer);
equal(scope.isDone(), true);
});
it("postBinary with specific content type", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const buffer = readFileSync(filepath);
const scope = mockPostBinary(buffer, {
"content-type": "image/jpeg",
});
await http.postBinary(`/post/binary`, buffer, "image/jpeg");
equal(scope.isDone(), true);
});
it("postBinary with stream", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const stream = createReadStream(filepath);
const buffer = readFileSync(filepath);
const scope = mockPostBinary(buffer, {
"content-type": "image/png",
});
await http.postBinary(`/post/binary`, stream);
equal(scope.isDone(), true);
});
it("fail with 404", async () => {
const scope = nock(baseURL, interceptionOption).get("/404").reply(404, {});
try {
await http.get(`/404`);
ok(false);
} catch (err) {
ok(err instanceof HTTPError);
equal(scope.isDone(), true);
equal(err.statusCode, 404);
}
});
it("fail with wrong addr", async () => {
nock.enableNetConnect();
try {
await http.get("http://domain.invalid");
ok(false);
} catch (err) {
ok(err instanceof RequestError);
equal(err.code, "ENOTFOUND");
nock.disableNetConnect();
}
});
it("will generate default params", async () => {
const scope = nock(baseURL, {
reqheaders: {
"User-Agent": `${pkg.name}/${pkg.version}`,
},
})
.get("/get")
.reply(200, {});
const http = new HTTPClient();
const res = await http.get<any>(`${baseURL}/get`);
equal(scope.isDone(), true);
deepEqual(res, {});
});
});
import { deepEqual, equal, ok } from "assert";
import { readFileSync } from "fs";
import { join } from "path";
import { HTTPError } from "../lib/exceptions";
import HTTPClient from "../lib/http";
import middleware from "../lib/middleware";
import * as Types from "../lib/types";
import { close, listen } from "./helpers/test-server";
const TEST_PORT = parseInt(process.env.TEST_PORT, 10);
const m = middleware({ channelSecret: "test_channel_secret" });
const getRecentReq = (): { body: Types.WebhookRequestBody } =>
JSON.parse(readFileSync(join(__dirname, "helpers/request.json")).toString());
describe("middleware", () => {
const webhook: Types.MessageEvent = {
message: {
id: "test_event_message_id",
text: "this is test message.😄😅😢😞😄😅😢😞",
type: "text",
},
replyToken: "test_reply_token",
source: {
groupId: "test_group_id",
type: "group",
},
timestamp: 0,
mode: "active",
type: "message",
};
const webhookSignature = {
"X-Line-Signature": "GzU7H3qOXDzDD6cNcS/9otLzlLFxnYYriz62rNu5BDE=",
};
const http = (headers: any = { ...webhookSignature }) =>
new HTTPClient({
baseURL: `http://localhost:${TEST_PORT}`,
defaultHeaders: headers,
});
before(() => listen(TEST_PORT, m));
after(() => close());
it("succeed", async () => {
await http().post(`/webhook`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
const req = getRecentReq();
deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
deepEqual(req.body.events, [webhook]);
});
it("succeed with pre-parsed string", async () => {
await http().post(`/mid-text`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
const req = getRecentReq();
deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
deepEqual(req.body.events, [webhook]);
});
it("succeed with pre-parsed buffer", async () => {
await http().post(`/mid-buffer`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
const req = getRecentReq();
deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
deepEqual(req.body.events, [webhook]);
});
it("succeed with pre-parsed buffer in rawBody", async () => {
await http().post(`/mid-rawbody`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
const req = getRecentReq();
deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
deepEqual(req.body.events, [webhook]);
});
it("fails on parsing raw as it's a not valid request and should be catched", async () => {
try {
await http({
"X-Line-Signature": "wqJD7WAIZhWcXThMCf8jZnwG3Hmn7EF36plkQGkj48w=",
"Content-Encoding": 1,
}).post(`/webhook`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 415);
} else {
throw err;
}
}
});
it("fails on pre-parsed json", async () => {
try {
await http().post(`/mid-json`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 500);
} else {
throw err;
}
}
});
it("fails on construct with no channelSecret", () => {
try {
middleware({ channelSecret: null });
ok(false);
} catch (err) {
equal(err.message, "no channel secret");
}
});
it("fails on wrong signature", async () => {
try {
await http({
"X-Line-Signature": "WqJD7WAIZhWcXThMCf8jZnwG3Hmn7EF36plkQGkj48w=",
}).post(`/webhook`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 401);
} else {
throw err;
}
}
});
it("fails on wrong signature (length)", async () => {
try {
await http({
"X-Line-Signature": "WqJD7WAIZ6plkQGkj48w=",
}).post(`/webhook`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 401);
} else {
throw err;
}
}
});
it("fails on invalid JSON", async () => {
try {
await http({
"X-Line-Signature": "Z8YlPpm0lQOqPipiCHVbiuwIDIzRzD7w5hvHgmwEuEs=",
}).post(`/webhook`, "i am not jason");
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 400);
} else {
throw err;
}
}
});
it("fails on empty signature", async () => {
try {
await http({}).post(`/webhook`, {
events: [webhook],
destination: "Uaaaabbbbccccddddeeeeffff",
});
ok(false);
} catch (err) {
if (err instanceof HTTPError) {
equal(err.statusCode, 401);
} else {
throw err;
}
}
});
});
import { ensureJSON } from "../lib/utils";
import { JSONParseError } from "../lib/exceptions";
import { equal, ok } from "assert";
describe("utils", () => {
describe("ensureJSON", () => {
it("fails when input isn't an object", () => {
let input = "not Object";
try {
ensureJSON(input);
ok(false);
} catch (err) {
equal(
(err as JSONParseError).message,
"Failed to parse response body as JSON",
);
}
});
});
});
import { ok } from "assert";
import validateSignature from "../lib/validate-signature";
const body = { hello: "world" };
const secret = "test_secret";
describe("validateSignature", () => {
it("success", () => {
const validSignature = "t7Hn4ZDHqs6e+wdvI5TyQIvzie0DmMUmuXEBqyyE/tM=";
ok(validateSignature(JSON.stringify(body), secret, validSignature));
});
it("failure", () => {
const invalidSignature = "t7Hn4ZDHqs6e+wdvi5TyQivzie0DmMUmuXEBqyyE/tM=";
ok(!validateSignature(JSON.stringify(body), secret, invalidSignature));
});
});
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"noImplicitAny": true,
"outDir": "dist",
"rootDirs": ["lib", "test"],
"declaration": true
},
"include": [
"lib/**/*.ts"
]
}
var express = require('express');
var app = express();
const line = require('@line/bot-sdk');
//papago api
var request = require('request');
//번역 api_url
var translate_api_url = 'https://openapi.naver.com/v1/papago/n2mt';
//언어감지 api_url
var languagedetect_api_url = 'https://openapi.naver.com/v1/papago/detectLangs'
// Naver Auth Key
//새로 발급받은 naver papago api id, pw 입력
// var client_id = 'xZMx34y7uru1v8lywZ2d';
var client_id = 'aQTbsS2NCxUuRaAhk6IL';
// var client_secret = 'p6L7M7WsH9';
var client_secret = 'iu9_X3PJQ7';
const config = {
channelAccessToken: 'mnny0MJSezgBXzR9C3Ddcc1Csdb7Y9jkvy2nqV5saOmvR2YOJ1/kj/2M0CNsLA+57B2qDpdUQ7WbCTtIKx/LAJ6Kwfop4tX3up7EM8H9EZK1td6GMbhhCb6wvUFVdb1PcTO4joCv8mspd3ubo8a+gAdB04t89/1O/w1cDnyilFU=',
channelSecret: 'bde77633a16fc5bfbd532d5990c6170e',
};
// create LINE SDK client
const client = new line.Client(config);
// create Express app
// about Express itself: https://expressjs.com/
// register a webhook handler with middleware
// about the middleware, please refer to doc
app.post('/webhook', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result))
.catch((err) => {
console.error(err);
res.status(200).end();
});
});
// event handler
function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
// ignore non-text-message event
return Promise.resolve(null);
}
return new Promise(function(resolve, reject) {
//언어 감지 option
var detect_options = {
url : languagedetect_api_url,
form : {'query': event.message.text},
headers: {'X-Naver-Client-Id': client_id, 'X-Naver-Client-Secret': client_secret}
};
//papago 언어 감지
request.post(detect_options,function(error,response,body){
console.log(response.statusCode);
if(!error && response.statusCode == 200){
var detect_body = JSON.parse(response.body);
var source = '';
var target = '';
var result = { type: 'text', text:''};
//언어 감지가 제대로 됐는지 확인
console.log(detect_body.langCode);
//번역은 한국어->영어 / 영어->한국어만 지원
if(detect_body.langCode == 'ko'||detect_body.langCode == 'en'){
source = detect_body.langCode == 'ko' ? 'ko':'en';
target = source == 'ko' ? 'en':'ko';
//papago 번역 option
var options = {
url: translate_api_url,
// 한국어(source : ko), 영어(target: en), 카톡에서 받는 메시지(text)
form: {'source':source, 'target':target, 'text':event.message.text},
headers: {'X-Naver-Client-Id': client_id, 'X-Naver-Client-Secret': client_secret}
};
// Naver Post API
request.post(options, function(error, response, body){
// Translate API Sucess
if(!error && response.statusCode == 200){
// JSON
var objBody = JSON.parse(response.body);
// Message 잘 찍히는지 확인
result.text = objBody.message.result.translatedText;
console.log(result.text);
//번역된 문장 보내기
client.replyMessage(event.replyToken,result).then(resolve).catch(reject);
}
});
}
// 메시지의 언어가 영어 또는 한국어가 아닐 경우
else{
result.text = '언어를 감지할 수 없습니다. \n 번역 언어는 한글 또는 영어만 가능합니다.';
client.replyMessage(event.replyToken,result).then(resolve).catch(reject);
}
}
});
});
}
app.listen(3000, function () {
console.log('Linebot listening on port 3000!');
});
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('project:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../mime/cli.js" "$@"
ret=$?
else
node "$basedir/../mime/cli.js" "$@"
ret=$?
fi
exit $ret
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\mime\cli.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\mime\cli.js" %*
)
\ No newline at end of file
## 6.4.0 (19 Nov 2018)
### Feature
* Add `getLinkToken` API (#96)
* Handle `req.rawBody` in Google Cloud Functions (#101)
* [Kitchensink] Add ngrok functionality (#99)
### Type
* Add types for video in imagemap message (#100)
* Add `contentProvider` fields to content messages (#103)
* Add `destination` field to webhook request body (#102)
* Add `MemberJoinEvent` and `MemberLeaveEvent` types (#107)
### Misc
* Don't include doc in released source
* Upgrade TypeScript to 3.1.6 (#94)
* Refactoring (#94, #98, #99)
* Remove webhook-tester tool
## 6.3.0 (21 Sep 2018)
### Feature
* Add default rich menu APIs (#87)
### Type
* Add missing `defaultAction` field to `TemplateColumn`
### Misc
* Use VuePress as documentation engine (#85)
* Upgrade minimum supported Node.js version to 6
## 6.2.1 (16 Aug 2018)
### Misc
* Remove gitbook-cli from dev dependencies
## 6.2.0 (15 Aug 2018)
#### Type
* Add QuickReply types (#83)
* Format type comments
#### Misc
* Upgrade TypeScript to 3
## 6.1.1 (14 Aug 2018)
#### Type
* Update FlexMessage types (#81)
#### Misc
* Add test coverage (#78)
* Add JSDoc comments (#80)
## 6.1.0 (19 June 2018)
#### Type
* Add types for flex message (#74)
* Simplify type definition for `Action`
## 6.0.3 (18 June 2018)
#### Misc
* Move get-audio-duration dep to proper package.json (#73)
* Vulnerability fix with `npm audit fix`
## 6.0.2 (21 May 2018)
#### Type
* Add missing `displayText` field to postback action (#63)
* Add missing `FileEventMessage` to `EventMessage` (#71)
#### Misc
* Add audio duration lib to kitchensink example (#68)
## 6.0.1 (13 Mar 2018)
#### Type
* Fix misimplemented 'AudioMessage' type (#61)
## 6.0.0 (27 Feb 2018)
#### Major
* Fix misimplemented 'unlinkRichMenuFromUser' API
#### Type
* Fix TemplateColumn type definition (#48)
#### Misc
* Update GitHub issue template (#43)
* Add Code of Conduct (#50)
* Catch errors properly in examples (#52)
## 5.2.0 (11 Dec 2017)
#### Minor
* Set Content-Length manually for postBinary (#42)
## 5.1.0 (7 Dec 2017)
#### Minor
* Add new fields (#39)
#### Misc
* Fix Windows build (#38)
* Add start scripts and webhook info to examples
## 5.0.1 (14 Nov 2017)
#### Minor
* Fix typo in `ImageMapMessage` type
* Add kitchensink example (#36)
## 5.0.0 (2 Nov 2017)
#### Major
* Implement rich menu API (#34)
#### Type
* Rename `ImageMapArea` and `TemplateAction`s into general ones
#### Misc
* Do not enforce `checkJSON` for some APIs where it means nothing
* Change how to check request object in test cases
## 4.0.0 (25 Oct 2017)
#### Major
* Make index script export exceptions and types (#31)
#### Type
* Simplify config types for client and middleware (#31)
#### Misc
* Fix information and links in doc
* Use Prettier instead of TSLint (#30)
* Install git hooks for precommit and prepush (#30)
## 3.1.1 (19 Sep 2017)
#### Type
* Fix type of postback.params
## 3.1.0 (19 Sep 2017)
#### Major
* Make middleware return `SignatureValidationFailed` for no signature (#26)
#### Type
* Add `FileEventMessage` type
## 3.0.0 (8 Sep 2017)
#### Major
* Implement "Get group/room member profile" API (#15)
* Implement "Get group/room member IDs" API (#23)
* `getMessageContent` now returns `Promise<ReadableStream>` (#20)
#### Type
* Add "datetimepicker" support (#21)
* Fix typo in `TemplateURIAction` type (#21)
#### Misc
* Package updates and corresponding fixes
* Use npm 5 instead of Yarn in dev
* Fix `clean` script to work in Windows
* Use "axios" for internal HTTP client instead of "got" (#20)
## 2.0.0 (12 June 2017)
#### Type
* Use literal types for 'type' fields
#### Misc
* Update yarn.lock with the latest Yarn
## 1.1.0 (31 May 2017)
* Handle pre-parsed body (string and buffer only)
#### Type
* Separate config type into client and middleware types
* Add `userId` to group and room event sources
#### Misc
* Create issue template (#4)
## 1.0.0 (11 May 2017)
* Initial release
This diff is collapsed. Click to expand it.
# line-bot-sdk-nodejs
[![Travis CI](https://travis-ci.org/line/line-bot-sdk-nodejs.svg?branch=master)](https://travis-ci.org/line/line-bot-sdk-nodejs)
[![npmjs](https://badge.fury.io/js/%40line%2Fbot-sdk.svg)](https://www.npmjs.com/package/@line/bot-sdk)
Node.js SDK for LINE Messaging API
## Getting Started
### Install
Using [npm](https://www.npmjs.com/):
``` bash
$ npm install @line/bot-sdk --save
```
### Documentation
For guide, API reference, and other information, please refer to
the [documentation](https://line.github.io/line-bot-sdk-nodejs/).
### LINE Messaging API References
Here are links to official references for LINE Messaging API. It is recommended
reading them beforehand.
* LINE API Reference [EN](https://developers.line.me/en/docs/messaging-api/reference/) [JA](https://developers.line.me/ja/docs/messaging-api/reference/)
* LINE Developers - Messaging API
* [Overview](https://developers.line.me/messaging-api/overview)
* [Getting started](https://developers.line.me/messaging-api/getting-started)
* [Joining groups and rooms](https://developers.line.me/messaging-api/joining-groups-and-rooms)
## Requirements
* **Node.js** 6 or higher
## Contributing
Please check [CONTRIBUTING](CONTRIBUTING.md) before making a contribution.
## License
[Apache License Version 2.0](LICENSE)
/// <reference types="node" />
import { Readable } from "stream";
import * as Types from "./types";
export default class Client {
config: Types.ClientConfig;
private http;
constructor(config: Types.ClientConfig);
pushMessage(to: string, messages: Types.Message | Types.Message[]): Promise<any>;
replyMessage(replyToken: string, messages: Types.Message | Types.Message[]): Promise<any>;
multicast(to: string[], messages: Types.Message | Types.Message[]): Promise<any>;
getProfile(userId: string): Promise<Types.Profile>;
private getChatMemberProfile;
getGroupMemberProfile(groupId: string, userId: string): Promise<Types.Profile>;
getRoomMemberProfile(roomId: string, userId: string): Promise<Types.Profile>;
private getChatMemberIds;
getGroupMemberIds(groupId: string): Promise<string[]>;
getRoomMemberIds(roomId: string): Promise<string[]>;
getMessageContent(messageId: string): Promise<Readable>;
private leaveChat;
leaveGroup(groupId: string): Promise<any>;
leaveRoom(roomId: string): Promise<any>;
getRichMenu(richMenuId: string): Promise<Types.RichMenuResponse>;
createRichMenu(richMenu: Types.RichMenu): Promise<string>;
deleteRichMenu(richMenuId: string): Promise<any>;
getRichMenuIdOfUser(userId: string): Promise<string>;
linkRichMenuToUser(userId: string, richMenuId: string): Promise<any>;
unlinkRichMenuFromUser(userId: string): Promise<any>;
getRichMenuImage(richMenuId: string): Promise<Readable>;
setRichMenuImage(richMenuId: string, data: Buffer | Readable, contentType?: string): Promise<any>;
getRichMenuList(): Promise<Array<Types.RichMenuResponse>>;
setDefaultRichMenu(richMenuId: string): Promise<{}>;
getDefaultRichMenuId(): Promise<string>;
deleteDefaultRichMenu(): Promise<{}>;
getLinkToken(userId: string): Promise<string>;
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const http_1 = require("./http");
const exceptions_1 = require("./exceptions");
function toArray(maybeArr) {
return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
}
function checkJSON(raw) {
if (typeof raw === "object") {
return raw;
}
else {
throw new exceptions_1.JSONParseError("Failed to parse response body as JSON", raw);
}
}
class Client {
constructor(config) {
if (!config.channelAccessToken) {
throw new Error("no channel access token");
}
this.config = config;
this.http = new http_1.default(process.env.API_BASE_URL || "https://api.line.me/v2/bot/", {
Authorization: "Bearer " + this.config.channelAccessToken,
});
}
pushMessage(to, messages) {
return this.http.post("/message/push", {
messages: toArray(messages),
to,
});
}
replyMessage(replyToken, messages) {
return this.http.post("/message/reply", {
messages: toArray(messages),
replyToken,
});
}
multicast(to, messages) {
return this.http.post("/message/multicast", {
messages: toArray(messages),
to,
});
}
getProfile(userId) {
return this.http.get(`/profile/${userId}`).then(checkJSON);
}
getChatMemberProfile(chatType, chatId, userId) {
return this.http
.get(`/${chatType}/${chatId}/member/${userId}`)
.then(checkJSON);
}
getGroupMemberProfile(groupId, userId) {
return this.getChatMemberProfile("group", groupId, userId);
}
getRoomMemberProfile(roomId, userId) {
return this.getChatMemberProfile("room", roomId, userId);
}
getChatMemberIds(chatType, chatId) {
const load = (start) => this.http
.get(`/${chatType}/${chatId}/members/ids`, start ? { start } : null)
.then(checkJSON)
.then((res) => {
if (!res.next) {
return res.memberIds;
}
return load(res.next).then(extraIds => res.memberIds.concat(extraIds));
});
return load();
}
getGroupMemberIds(groupId) {
return this.getChatMemberIds("group", groupId);
}
getRoomMemberIds(roomId) {
return this.getChatMemberIds("room", roomId);
}
getMessageContent(messageId) {
return this.http.getStream(`/message/${messageId}/content`);
}
leaveChat(chatType, chatId) {
return this.http.post(`/${chatType}/${chatId}/leave`);
}
leaveGroup(groupId) {
return this.leaveChat("group", groupId);
}
leaveRoom(roomId) {
return this.leaveChat("room", roomId);
}
getRichMenu(richMenuId) {
return this.http
.get(`/richmenu/${richMenuId}`)
.then(checkJSON);
}
createRichMenu(richMenu) {
return this.http
.post("/richmenu", richMenu)
.then(checkJSON)
.then(res => res.richMenuId);
}
deleteRichMenu(richMenuId) {
return this.http.delete(`/richmenu/${richMenuId}`);
}
getRichMenuIdOfUser(userId) {
return this.http
.get(`/user/${userId}/richmenu`)
.then(checkJSON)
.then(res => res.richMenuId);
}
linkRichMenuToUser(userId, richMenuId) {
return this.http.post(`/user/${userId}/richmenu/${richMenuId}`);
}
unlinkRichMenuFromUser(userId) {
return this.http.delete(`/user/${userId}/richmenu`);
}
getRichMenuImage(richMenuId) {
return this.http.getStream(`/richmenu/${richMenuId}/content`);
}
setRichMenuImage(richMenuId, data, contentType) {
return this.http.postBinary(`/richmenu/${richMenuId}/content`, data, contentType);
}
getRichMenuList() {
return this.http
.get(`/richmenu/list`)
.then(checkJSON)
.then(res => res.richmenus);
}
setDefaultRichMenu(richMenuId) {
return this.http.post(`/user/all/richmenu/${richMenuId}`);
}
getDefaultRichMenuId() {
return this.http
.get("/user/all/richmenu")
.then(checkJSON)
.then(res => res.richMenuId);
}
deleteDefaultRichMenu() {
return this.http.delete("/user/all/richmenu");
}
getLinkToken(userId) {
return this.http
.post(`/user/${userId}/linkToken`)
.then(checkJSON)
.then(res => res.linkToken);
}
}
exports.default = Client;
export declare class SignatureValidationFailed extends Error {
signature?: string;
constructor(message: string, signature?: string);
}
export declare class JSONParseError extends Error {
raw: any;
constructor(message: string, raw: any);
}
export declare class RequestError extends Error {
code: string;
private originalError;
constructor(message: string, code: string, originalError: Error);
}
export declare class ReadError extends Error {
private originalError;
constructor(originalError: Error);
}
export declare class HTTPError extends Error {
statusCode: number;
statusMessage: string;
originalError: any;
constructor(message: string, statusCode: number, statusMessage: string, originalError: any);
}
"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;
/// <reference types="node" />
import { Readable } from "stream";
export default class HTTPClient {
private instance;
constructor(baseURL?: string, defaultHeaders?: any);
get<T>(url: string, params?: any): Promise<T>;
getStream(url: string, params?: any): Promise<Readable>;
post<T>(url: string, data?: any): Promise<T>;
postBinary<T>(url: string, data: Buffer | Readable, contentType?: string): Promise<T>;
delete<T>(url: string, params?: any): Promise<T>;
private wrapError;
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = require("axios");
const stream_1 = require("stream");
const exceptions_1 = require("./exceptions");
const fileType = require("file-type");
const pkg = require("../package.json");
class HTTPClient {
constructor(baseURL, defaultHeaders) {
this.instance = axios_1.default.create({
baseURL,
headers: Object.assign({}, defaultHeaders, {
"User-Agent": `${pkg.name}/${pkg.version}`,
}),
});
this.instance.interceptors.response.use(res => res, err => Promise.reject(this.wrapError(err)));
}
get(url, params) {
return this.instance.get(url, { params }).then(res => res.data);
}
getStream(url, params) {
return this.instance
.get(url, { params, responseType: "stream" })
.then(res => res.data);
}
post(url, data) {
return this.instance
.post(url, data, { headers: { "Content-Type": "application/json" } })
.then(res => res.data);
}
postBinary(url, data, contentType) {
let getBuffer;
if (Buffer.isBuffer(data)) {
getBuffer = Promise.resolve(data);
}
else {
getBuffer = new Promise((resolve, reject) => {
if (data instanceof stream_1.Readable) {
const buffers = [];
let size = 0;
data.on("data", (chunk) => {
buffers.push(chunk);
size += chunk.length;
});
data.on("end", () => resolve(Buffer.concat(buffers, size)));
data.on("error", reject);
}
else {
reject(new Error("invalid data type for postBinary"));
}
});
}
return getBuffer.then(data => {
return this.instance
.post(url, data, {
headers: {
"Content-Type": contentType || fileType(data).mime,
"Content-Length": data.length,
},
})
.then(res => res.data);
});
}
delete(url, params) {
return this.instance.delete(url, { params }).then(res => res.data);
}
wrapError(err) {
if (err.response) {
return new exceptions_1.HTTPError(err.message, err.response.status, err.response.statusText, err);
}
else if (err.code) {
return new exceptions_1.RequestError(err.message, err.code, err);
}
else if (err.config) {
// unknown, but from axios
return new exceptions_1.ReadError(err);
}
// otherwise, just rethrow
return err;
}
}
exports.default = HTTPClient;
import Client from "./client";
import middleware from "./middleware";
import validateSignature from "./validate-signature";
export { Client, middleware, validateSignature };
export * from "./exceptions";
export * from "./types";
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
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;
// re-export exceptions and types
__export(require("./exceptions"));
/// <reference types="node" />
import * as http from "http";
import * as Types from "./types";
export declare type Request = http.IncomingMessage & {
body: any;
};
export declare type Response = http.ServerResponse;
export declare type NextCallback = (err?: Error) => void;
export declare type Middleware = (req: Request, res: Response, next: NextCallback) => void;
export default function middleware(config: Types.MiddlewareConfig): Middleware;
"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 isValidBody(body) {
return (body && typeof body === "string") || Buffer.isBuffer(body);
}
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
const signature = req.headers["x-line-signature"];
if (!signature) {
next(new exceptions_1.SignatureValidationFailed("no signature"));
return;
}
let getBody;
if (isValidBody(req.rawBody)) {
// rawBody is provided in Google Cloud Functions and others
getBody = Promise.resolve(req.rawBody);
}
else if (isValidBody(req.body)) {
getBody = Promise.resolve(req.body);
}
else {
// body may not be parsed yet, parse it to a buffer
getBody = new Promise(resolve => {
body_parser_1.raw({ type: "*/*" })(req, res, () => resolve(req.body));
});
}
getBody.then(body => {
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));
}
});
};
}
exports.default = middleware;
This diff is collapsed. Click to expand it.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/// <reference types="node" />
export default function validateSignature(body: string | Buffer, channelSecret: string, signature: string): boolean;
"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];
}
return result === 0;
}
}
function validateSignature(body, channelSecret, signature) {
return safeCompare(crypto_1.createHmac("SHA256", channelSecret)
.update(body)
.digest(), s2b(signature, "base64"));
}
exports.default = validateSignature;
import { Readable } from "stream";
import HTTPClient from "./http";
import * as Types from "./types";
import { JSONParseError } from "./exceptions";
function toArray<T>(maybeArr: T | T[]): T[] {
return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
}
function checkJSON<T>(raw: T): T {
if (typeof raw === "object") {
return raw;
} else {
throw new JSONParseError("Failed to parse response body as JSON", raw);
}
}
type ChatType = "group" | "room";
export default class Client {
public config: Types.ClientConfig;
private http: HTTPClient;
constructor(config: Types.ClientConfig) {
if (!config.channelAccessToken) {
throw new Error("no channel access token");
}
this.config = config;
this.http = new HTTPClient(
process.env.API_BASE_URL || "https://api.line.me/v2/bot/",
{
Authorization: "Bearer " + this.config.channelAccessToken,
},
);
}
public pushMessage(
to: string,
messages: Types.Message | Types.Message[],
): Promise<any> {
return this.http.post("/message/push", {
messages: toArray(messages),
to,
});
}
public replyMessage(
replyToken: string,
messages: Types.Message | Types.Message[],
): Promise<any> {
return this.http.post("/message/reply", {
messages: toArray(messages),
replyToken,
});
}
public multicast(
to: string[],
messages: Types.Message | Types.Message[],
): Promise<any> {
return this.http.post("/message/multicast", {
messages: toArray(messages),
to,
});
}
public getProfile(userId: string): Promise<Types.Profile> {
return this.http.get<Types.Profile>(`/profile/${userId}`).then(checkJSON);
}
private getChatMemberProfile(
chatType: ChatType,
chatId: string,
userId: string,
): Promise<Types.Profile> {
return this.http
.get<Types.Profile>(`/${chatType}/${chatId}/member/${userId}`)
.then(checkJSON);
}
public getGroupMemberProfile(
groupId: string,
userId: string,
): Promise<Types.Profile> {
return this.getChatMemberProfile("group", groupId, userId);
}
public getRoomMemberProfile(
roomId: string,
userId: string,
): Promise<Types.Profile> {
return this.getChatMemberProfile("room", roomId, userId);
}
private getChatMemberIds(
chatType: ChatType,
chatId: string,
): Promise<string[]> {
const load = (start?: string): Promise<string[]> =>
this.http
.get(`/${chatType}/${chatId}/members/ids`, start ? { start } : null)
.then(checkJSON)
.then((res: { memberIds: string[]; next?: string }) => {
if (!res.next) {
return res.memberIds;
}
return load(res.next).then(extraIds =>
res.memberIds.concat(extraIds),
);
});
return load();
}
public getGroupMemberIds(groupId: string): Promise<string[]> {
return this.getChatMemberIds("group", groupId);
}
public getRoomMemberIds(roomId: string): Promise<string[]> {
return this.getChatMemberIds("room", roomId);
}
public getMessageContent(messageId: string): Promise<Readable> {
return this.http.getStream(`/message/${messageId}/content`);
}
private leaveChat(chatType: ChatType, chatId: string): Promise<any> {
return this.http.post(`/${chatType}/${chatId}/leave`);
}
public leaveGroup(groupId: string): Promise<any> {
return this.leaveChat("group", groupId);
}
public leaveRoom(roomId: string): Promise<any> {
return this.leaveChat("room", roomId);
}
public getRichMenu(richMenuId: string): Promise<Types.RichMenuResponse> {
return this.http
.get<Types.RichMenuResponse>(`/richmenu/${richMenuId}`)
.then(checkJSON);
}
public createRichMenu(richMenu: Types.RichMenu): Promise<string> {
return this.http
.post<any>("/richmenu", richMenu)
.then(checkJSON)
.then(res => res.richMenuId);
}
public deleteRichMenu(richMenuId: string): Promise<any> {
return this.http.delete(`/richmenu/${richMenuId}`);
}
public getRichMenuIdOfUser(userId: string): Promise<string> {
return this.http
.get<any>(`/user/${userId}/richmenu`)
.then(checkJSON)
.then(res => res.richMenuId);
}
public linkRichMenuToUser(userId: string, richMenuId: string): Promise<any> {
return this.http.post(`/user/${userId}/richmenu/${richMenuId}`);
}
public unlinkRichMenuFromUser(userId: string): Promise<any> {
return this.http.delete(`/user/${userId}/richmenu`);
}
public getRichMenuImage(richMenuId: string): Promise<Readable> {
return this.http.getStream(`/richmenu/${richMenuId}/content`);
}
public setRichMenuImage(
richMenuId: string,
data: Buffer | Readable,
contentType?: string,
): Promise<any> {
return this.http.postBinary(
`/richmenu/${richMenuId}/content`,
data,
contentType,
);
}
public getRichMenuList(): Promise<Array<Types.RichMenuResponse>> {
return this.http
.get<any>(`/richmenu/list`)
.then(checkJSON)
.then(res => res.richmenus);
}
public setDefaultRichMenu(richMenuId: string): Promise<{}> {
return this.http.post(`/user/all/richmenu/${richMenuId}`);
}
public getDefaultRichMenuId(): Promise<string> {
return this.http
.get<any>("/user/all/richmenu")
.then(checkJSON)
.then(res => res.richMenuId);
}
public deleteDefaultRichMenu(): Promise<{}> {
return this.http.delete("/user/all/richmenu");
}
public getLinkToken(userId: string): Promise<string> {
return this.http
.post<any>(`/user/${userId}/linkToken`)
.then(checkJSON)
.then(res => res.linkToken);
}
}
export class SignatureValidationFailed extends Error {
constructor(message: string, public signature?: string) {
super(message);
}
}
export class JSONParseError extends Error {
constructor(message: string, public raw: any) {
super(message);
}
}
export class RequestError extends Error {
constructor(
message: string,
public code: string,
private originalError: Error,
) {
super(message);
}
}
export class ReadError extends Error {
constructor(private originalError: Error) {
super(originalError.message);
}
}
export class HTTPError extends Error {
constructor(
message: string,
public statusCode: number,
public statusMessage: string,
public originalError: any,
) {
super(message);
}
}
import axios, { AxiosInstance, AxiosError } from "axios";
import { Readable } from "stream";
import { HTTPError, ReadError, RequestError } from "./exceptions";
import * as fileType from "file-type";
const pkg = require("../package.json");
export default class HTTPClient {
private instance: AxiosInstance;
constructor(baseURL?: string, defaultHeaders?: any) {
this.instance = axios.create({
baseURL,
headers: Object.assign({}, defaultHeaders, {
"User-Agent": `${pkg.name}/${pkg.version}`,
}),
});
this.instance.interceptors.response.use(
res => res,
err => Promise.reject(this.wrapError(err)),
);
}
public get<T>(url: string, params?: any): Promise<T> {
return this.instance.get(url, { params }).then(res => res.data);
}
public getStream(url: string, params?: any): Promise<Readable> {
return this.instance
.get(url, { params, responseType: "stream" })
.then(res => res.data as Readable);
}
public post<T>(url: string, data?: any): Promise<T> {
return this.instance
.post(url, data, { headers: { "Content-Type": "application/json" } })
.then(res => res.data);
}
public postBinary<T>(
url: string,
data: Buffer | Readable,
contentType?: string,
): Promise<T> {
let getBuffer: Promise<Buffer>;
if (Buffer.isBuffer(data)) {
getBuffer = Promise.resolve(data);
} else {
getBuffer = new Promise((resolve, reject) => {
if (data instanceof Readable) {
const buffers: Buffer[] = [];
let size = 0;
data.on("data", (chunk: Buffer) => {
buffers.push(chunk);
size += chunk.length;
});
data.on("end", () => resolve(Buffer.concat(buffers, size)));
data.on("error", reject);
} else {
reject(new Error("invalid data type for postBinary"));
}
});
}
return getBuffer.then(data => {
return this.instance
.post(url, data, {
headers: {
"Content-Type": contentType || fileType(data).mime,
"Content-Length": data.length,
},
})
.then(res => res.data);
});
}
public delete<T>(url: string, params?: any): Promise<T> {
return this.instance.delete(url, { params }).then(res => res.data);
}
private wrapError(err: AxiosError): Error {
if (err.response) {
return new HTTPError(
err.message,
err.response.status,
err.response.statusText,
err,
);
} else if (err.code) {
return new RequestError(err.message, err.code, err);
} else if (err.config) {
// unknown, but from axios
return new ReadError(err);
}
// otherwise, just rethrow
return err;
}
}
import Client from "./client";
import middleware from "./middleware";
import validateSignature from "./validate-signature";
export { Client, middleware, validateSignature };
// re-export exceptions and types
export * from "./exceptions";
export * from "./types";
import { raw } from "body-parser";
import * as http from "http";
import { JSONParseError, SignatureValidationFailed } from "./exceptions";
import * as Types from "./types";
import validateSignature from "./validate-signature";
export type Request = http.IncomingMessage & { body: any };
export type Response = http.ServerResponse;
export type NextCallback = (err?: Error) => void;
export type Middleware = (
req: Request,
res: Response,
next: NextCallback,
) => void;
function isValidBody(body?: any): body is string | Buffer {
return (body && typeof body === "string") || Buffer.isBuffer(body);
}
export default function middleware(config: Types.MiddlewareConfig): Middleware {
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
const signature = req.headers["x-line-signature"] as string;
if (!signature) {
next(new SignatureValidationFailed("no signature"));
return;
}
let getBody: Promise<string | Buffer>;
if (isValidBody((req as any).rawBody)) {
// rawBody is provided in Google Cloud Functions and others
getBody = Promise.resolve((req as any).rawBody);
} else if (isValidBody(req.body)) {
getBody = Promise.resolve(req.body);
} else {
// body may not be parsed yet, parse it to a buffer
getBody = new Promise(resolve => {
raw({ type: "*/*" })(req as any, res as any, () => resolve(req.body));
});
}
getBody.then(body => {
if (!validateSignature(body, secret, signature)) {
next(
new 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 JSONParseError(err.message, strBody));
}
});
};
}
This diff is collapsed. Click to expand it.
import { createHmac, timingSafeEqual } from "crypto";
function s2b(str: string, encoding: string): Buffer {
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: Buffer, b: Buffer): boolean {
if (a.length !== b.length) {
return false;
}
if (timingSafeEqual) {
return timingSafeEqual(a, b);
} else {
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
}
export default function validateSignature(
body: string | Buffer,
channelSecret: string,
signature: string,
): boolean {
return safeCompare(
createHmac("SHA256", channelSecret)
.update(body)
.digest(),
s2b(signature, "base64"),
);
}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This file is too large to display.