신지원

START

Showing 77 changed files with 10175 additions and 0 deletions
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
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2017-2018 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
https://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.
# 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)
# `new Client(config)`
`Client` is a class representing an API client. It provides methods
corresponding to [messaging APIs](https://developers.line.biz/en/reference/messaging-api/).
#### Type signature
``` typescript
class Client {
public config: ClientConfig
constructor(config: ClientConfig) {}
// requestOption
setRequestOptionOnce(option: Partial<{
retryKey: string;
}>)
// Message
pushMessage(to: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
replyMessage(replyToken: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
multicast(to: string[], messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
narrowcast(
messages: Message | Message[],
recipient?: ReceieptObject,
filter?: { demographic: DemographicFilterObject },
limit?: { max?: number, upToRemainingQuota?: boolean },
notificationDisabled?: boolean,
): Promise<MessageAPIResponseBase>
broadcast(messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
getMessageContent(messageId: string): Promise<Readable>
// Profile
getProfile(userId: string): Promise<Profile>
// Group
getGroupSummary(groupId: string): Promise<GroupSummary>
getGroupMembersCount(groupId: string): Promise<MemberCountResponse>
getGroupMemberProfile(groupId: string, userId: string): Promise<Profile>
getGroupMemberIds(groupId: string): Promise<string[]>
leaveGroup(groupId: string): Promise<any>
// Room
getRoomMembersCount(roomId: string): Promise<MemberCountResponse>
getRoomMemberProfile(roomId: string, userId: string): Promise<Profile>
getRoomMemberIds(roomId: string): Promise<string[]>
leaveRoom(roomId: string): Promise<any>
// Rich menu
getRichMenu(richMenuId: string): Promise<RichMenuResponse>
createRichMenu(richMenu: RichMenu): Promise<string>
deleteRichMenu(richMenuId: string): Promise<any>
getRichMenuIdOfUser(userId: string): Promise<string>
linkRichMenuToUser(userId: string, richMenuId: string): Promise<any>
unlinkRichMenuFromUser(userId: string, richMenuId: string): Promise<any>
linkRichMenuToMultipleUsers(richMenuId: string, userIds: string[]): Promise<any>
unlinkRichMenusFromMultipleUsers(userIds: string[]): Promise<any>
getRichMenuImage(richMenuId: string): Promise<Readable>
setRichMenuImage(richMenuId: string, data: Buffer | Readable, contentType?: string): Promise<any>
getRichMenuList(): Promise<Array<RichMenuResponse>>
setDefaultRichMenu(richMenuId: string): Promise<{}>
getDefaultRichMenuId(): Promise<string>
deleteDefaultRichMenu(): Promise<{}>
// Account link
getLinkToken(userId: string): Promise<string>
// Get number of messages sent
getNumberOfSentReplyMessages(date: string): Promise<NumberOfMessagesSentResponse>
getNumberOfSentPushMessages(date: string): Promise<NumberOfMessagesSentResponse>
getNumberOfSentMulticastMessages(date: string): Promise<NumberOfMessagesSentResponse>
getTargetLimitForAdditionalMessages(): Promise<TargetLimitForAdditionalMessages>
getNumberOfMessagesSentThisMonth(): Promise<NumberOfMessagesSentThisMonth>
getNumberOfSentBroadcastMessages(date: string): Promise<NumberOfMessagesSentResponse>
getNarrowcastProgress(requestId: string): Promise<NarrowcastProgressResponse>
// Insight
getNumberOfMessageDeliveries(date: string): Promise<Types.NumberOfMessageDeliveriesResponse>
getNumberOfFollowers(date: string): Promise<Types.NumberOfFollowersResponse>
getFriendDemographics(): Promise<Types.FriendDemographics>
getUserInteractionStatistics(requestId: string): Promise<Types.UserInteractionStatistics>
// AudienceGroup
createUploadAudienceGroup(uploadAudienceGroup: {
description: string;
isIfaAudience?: boolean;
audiences?: { id: string }[];
uploadDescription?: string;
}) : Promise<{
audienceGroupId: number;
type: string;
description: string;
created: number;
requestId: string;
}>
createUploadAudienceGroupByFile(uploadAudienceGroup: {
description: string;
isIfaAudience?: boolean;
uploadDescription?: string;
file: Buffer | Readable;
}) : Promise<{
audienceGroupId: number;
type: "UPLOAD";
description: string;
created: number;
}>
updateUploadAudienceGroup(
uploadAudienceGroup: {
audienceGroupId: number;
description?: string;
uploadDescription?: string;
audiences: { id: string }[];
},
// for set request timeout
httpConfig?: Partial<AxiosRequestConfig>,
) : Promise<{}>
createUploadAudienceGroupByFile(
uploadAudienceGroup: {
audienceGroupId: number;
uploadDescription?: string;
file: Buffer | Readable;
},
// for set request timeout
httpConfig?: Partial<AxiosRequestConfig>,
}) : Promise<{}>
createClickAudienceGroup(clickAudienceGroup: {
description: string;
requestId: string;
clickUrl?: string;
}) :Promise<{
audienceGroupId: number;
type: string;
created: number;
description: string;
requestId: string;
clickUrl: string;
}>
createImpAudienceGroup(impAudienceGroup: {
requestId: string;
description: string;
}): Promise<{
audienceGroupId: number;
type: string;
description: string;
created: number;
requestId: string;
}>
setDescriptionAudienceGroup(
description: string,
audienceGroupId: string,
): Promise<{}>
deleteAudienceGroup(audienceGroupId: string): Promise<{}>
getAudienceGroup(audienceGroupId: string): Promise<AudienceGroup>
getAudienceGroups(
page: number,
description?: string,
status?: AudienceGroupStatus,
size?: number,
createRoute?: AudienceGroupCreateRoute,
includesExternalPublicGroups?: boolean,
): Promise<{
audienceGroups: AudienceGroups;
hasNextPage: boolean;
totalCount: number;
readWriteAudienceGroupTotalCount: number;
page: number;
size: number;
}>
getAudienceGroupAuthorityLevel(): Promise<{
authorityLevel: Types.AudienceGroupAuthorityLevel
}>
changeAudienceGroupAuthorityLevel(
authorityLevel: Types.AudienceGroupAuthorityLevel
): Promise<{}>
// Bot
getBotInfo(): Promise<BotInfoResponse>
// Webhook
setWebhookEndpointUrl(endpoint: string): Promise<{}>
getWebhookEndpointInfo(): Promise<{
endpoint: string;
active: boolean;
}>
testWebhookEndpoint(endpoint?: string): Promise<{
success: boolean;
timestamp: string;
statusCode: number;
reason: string;
detail: string;
}>
}
```
`Message` is a valid message object. About message object structure, please
refer to [Message and event objects](./message-and-event-objects.md) on this guide, or
[Send message object](https://developers.line.biz/en/reference/messaging-api/#message-objects)
on the official documentation.
`ClientConfig` type is like below.
``` typescript
interface ClientConfig {
channelAccessToken: string;
channelSecret?: string;
}
```
## Common Specifications
Regarding to things like [Retrying an API request](https://developers.line.biz/en/reference/messaging-api/#retry-api-request), there's an API called `setRequestOptionOnce`.
When you call this first and call the API support that request option, then it will be set to that request and will be cleared automatically.
## Methods
For a parameter `messages: messages: Message | Message[]`, you can provide a
message object or an array of message objects. Both will work, but please beware
that there can be a limit on the number of the messages to be sent
simultaneously. About the API detail, please refer to [the official documentation](https://developers.line.biz/en/reference/messaging-api/#message-objects).
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).
### Message
#### `pushMessage(to: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
It corresponds to the [Push message](https://developers.line.biz/en/reference/messaging-api/#send-push-message) API.
The first argument is an ID of a receiver, and the second is messages to be sent.
``` js
client.pushMessage('user_or_group_or_room_id', {
type: 'text',
text: 'hello, world',
})
```
#### `replyMessage(replyToken: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
It corresponds to the [Reply message](https://developers.line.biz/en/reference/messaging-api/#send-reply-message) API.
The first argument is a reply token, which is retrieved from a webhook event
object. For the list of replyable events, please refer to [Webhook event object](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
of the official documentation. The second argument is the same with one in `pushMessage()`.
``` js
client.replyMessage(event.replyToken, {
type: 'text',
text: 'hello, world',
})
```
#### `multicast(to: string[], messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
It corresponds to the [Multicast](https://developers.line.biz/en/reference/messaging-api/#send-multicast-message) API.
The first argument is a list of receiver IDs, and the second is messages to be
sent.
``` js
client.multicast(['user_id_1', 'user_id_2', 'room_id_1'], {
type: 'text',
text: 'hello, world',
})
```
#### `broadcast(messages: Message | Message[], notificationDisabled: boolean = false): Promise<any>`
Sends push messages to multiple users at any time.
Note: LINE@ accounts cannot call this API endpoint. Please migrate it to a LINE official account. For more information, see [Migration of LINE@ accounts](https://developers.line.biz/en/docs/messaging-api/migrating-line-at/).
``` js
client.broadcast({
type: 'text',
text: 'hello, world',
})
```
#### `getMessageContent(messageId: string): Promise<Readable>`
It corresponds to the [Content](https://developers.line.biz/en/reference/messaging-api/#get-content) API.
The argument is an ID of media messages, such as image, video, and audio. The ID
can be retrieved from a message object of a message event.
Please beware that what it returns is promise of [readable stream](https://nodejs.org/dist/latest/docs/api/stream.html#stream_readable_streams).
You can pipe the stream into a file, an HTTP response, etc.
``` js
client.getMessageContent('message_id')
.then((stream) => {
stream.on('data', (chunk) => {
...
})
stream.on('error', (err) => {
...
})
stream.pipe(...)
})
```
### Profile
#### `getProfile(userId: string): Promise<Profile>`
It corresponds to the [Profile](https://developers.line.biz/en/reference/messaging-api/#get-profile) API.
The argument is a user ID.
``` js
client.getProfile('user_id').then((profile) => {
console.log(profile);
});
```
### Group
#### `getGroupSummary(groupId: string): Promise<GroupSummary>`
It corresponds to the [Group Summary](https://developers.line.biz/en/reference/messaging-api/#get-group-summary) API.
The argument is a group ID.
``` js
client.getGroupSummary('group_id').then((summary) => {
console.log(summary)
})
```
#### `getGroupMembersCount(groupId: string): Promise<MemberCountResponse>`
It corresponds to the [Group Members Count](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count) API.
The argument is a group ID.
``` js
client.getGroupMembersCount('group_id').then((count) => {
console.log(count)
})
```
#### `getGroupMemberProfile(groupId: string, userId: string): Promise<Profile>`
It corresponds to the [Group Member Profile](https://developers.line.biz/en/reference/messaging-api/#get-group-member-profile) API.
The arguments are a group ID and an ID of a user in the group. Please refer to
the official documentation for the difference between this API and `getProfile()`.
``` js
client.getGroupMemberProfile('group_id', 'user_id').then((profile) => {
console.log(profile);
})
```
#### `getGroupMemberIds(groupId: string): Promise<string[]>`
It corresponds to the [Group Member IDs](https://developers.line.biz/en/reference/messaging-api/#get-group-member-user-ids) API.
*FYI: This feature is only available for LINE@ Approved accounts or official accounts.*
The argument is a group ID and the method returns a promise of an array of user IDs.
``` js
client.getGroupMemberIds('group_id').then((ids) => {
ids.forEach((id) => console.log(id));
})
```
#### `leaveGroup(groupId: string): Promise<any>`
It corresponds to the [Leave group](https://developers.line.biz/en/reference/messaging-api/#leave-group) API.
The argument is a group ID.
``` js
client.leaveGroup('group_id')
```
### Room
#### `getRoomMembersCount(roomId: string): Promise<MembersCountResponse>`
It corresponds to the [Room Members Count](https://developers.line.biz/en/reference/messaging-api/#get-members-room-count) API.
The argument is a room ID.
``` js
client.getRoomMembersCount('room_id').then((count) => {
console.log(count)
})
```
#### `getRoomMemberProfile(roomId: string, userId: string): Promise<Profile>`
It corresponds to the [Room Member Profile](https://developers.line.biz/en/reference/messaging-api/#get-room-member-profile) API.
The arguments are a room ID and an ID of a user in the room. Please refer to the
official documentation for the difference between this API and `getProfile()`.
``` js
client.getRoomMemberProfile('room_id', 'user_id').then((profile) => {
console.log(profile);
})
```
#### `getRoomMemberIds(roomId: string): Promise<string[]>`
It corresponds to the [Room Member IDs](https://developers.line.biz/en/reference/messaging-api/#get-room-member-user-ids) API.
*FYI: This feature is only available for LINE@ Approved accounts or official accounts.*
The argument is a room ID and the method returns a promise of an array of user IDs.
``` js
client.getRoomMemberIds('room_id').then((ids) => {
ids.forEach((id) => console.log(id));
})
```
#### `leaveRoom(roomId: string): Promise<any>`
It corresponds to the [Leave room](https://developers.line.biz/en/reference/messaging-api/#leave-room) API.
The argument is a room ID.
``` js
client.leaveGroup('room_id')
```
### Rich menu
#### `getRichMenu(richMenuId: string): Promise<RichMenuResponse>`
It corresponds to the [Get rich menu](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu) API.
The argument is a rich menu ID. The return type is [a rich menu response object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-response-object).
``` js
client.getRichMenu('rich_menu_id').then((richMenu) => {
console.log(richMenu.size);
console.log(richMenu.areas[0].bounds);
})
```
#### `createRichMenu(richMenu: RichMenu): Promise<string>`
It corresponds to the [Create rich menu](https://developers.line.biz/en/reference/messaging-api/#create-rich-menu) API.
The argument is [a rich menu object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-object).
For the detail of the object format, please refer to the official documentation.
It returns the result rich menu ID.
``` js
client.createRichMenu({ size: { width: 2500, height: 1686 }, ... })
.then((richMenuId) => console.log(richMenuId))
```
#### `deleteRichMenu(richMenuId: string): Promise<any>`
It corresponds to the [Delete rich menu](https://developers.line.biz/en/reference/messaging-api/#delete-rich-menu) API.
The argument is a rich menu ID.
``` js
client.deleteRichMenu('rich_menu_id')
```
#### `getRichMenuIdOfUser(userId: string): Promise<string>`
It corresponds to the [Get rich menu ID of user](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-id-of-user) API.
The argument is a user ID. It returns a rich menu ID to be used with other APIs.
``` js
client.getRichMenuIdOfUser('user_id').then((richMenuId) => {
console.log(richMenuId);
})
```
#### `linkRichMenuToUser(userId: string, richMenuId: string): Promise<any>`
It corresponds to the [Link rich menu to user](https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-user) API.
The arguments are a user ID and a rich menu ID.
``` js
client.linkRichMenuToUser('user_id', 'rich_menu_id')
```
#### `unlinkRichMenuFromUser(userId: string, richMenuId: string): Promise<any>`
It corresponds to the [Unlink rich menu from user](https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-user) API.
The arguments are a user ID and a rich menu ID.
``` js
client.unlinkRichMenuFromUser('user_id', 'rich_menu_id')
```
#### `linkRichMenuToMultipleUsers(richMenuId: string, userIds: string[]): Promise<any>`
It corresponds to the [Link rich menu to multiple users](https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users) API.
The arguments are a richMenuId and a array of userIds.
``` js
client.linkRichMenuToMultipleUsers('rich_menu_id', ['user_id'])
```
#### `unlinkRichMenusFromMultipleUsers(userIds: string[]): Promise<any>`
It corresponds to the [Unlink rich menus from multiple users](https://developers.line.biz/en/reference/messaging-api#unlink-rich-menu-from-users) API.
The argument is a array of userIds.
``` js
client.unlinkRichMenusFromMultipleUsers(['user_id'])
```
#### `getRichMenuImage(richMenuId: string): Promise<Readable>`
It corresponds to the [Download rich menu image](https://developers.line.biz/en/reference/messaging-api/#download-rich-menu-image) API.
The argument is a rich menu ID.
Please beware that what it returns is promise of [readable stream](https://nodejs.org/dist/latest/docs/api/stream.html#stream_readable_streams).
You can pipe the stream into a file, an HTTP response, etc.
``` js
client.getRichMenuImage('rich_menu_id')
.then((stream) => {
stream.on('data', (chunk) => {
...
})
stream.on('error', (err) => {
...
})
stream.pipe(...)
})
```
#### `setRichMenuImage(richMenuId: string, data: Buffer | Readable, contentType?: string): Promise<any>`
It corresponds to the [Upload rich menu image](https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image) API.
The 1st argument is a rich menu ID. For 2nd argument, a buffer or a readable
stream of an image should be provided. For the restriction of the image, please
refer to the official documentation. The last argument is optional. If it's not
provided, the mime type will be guessted from `data`. Only `image/jpeg` or
`image/png` is allowed for the content type.
``` js
client.setRichMenuImage('rich_menu_id', fs.createReadStream('./some_image.png'))
```
#### `getRichMenuList(): Promise<Array<RichMenuResponse>>`
It corresponds to the [Get rich menu list](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-list) API.
The return type is a list of [rich menu response objects](https://developers.line.biz/en/reference/messaging-api/#rich-menu-response-object).
### `setDefaultRichMenu(richMenuId: string): Promise<{}>`
It corresponds to the [Set default rich menu](https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu) API.
### `getDefaultRichMenuId(): Promise<string>`
It corresponds to the [Get default rich menu ID](https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id) API.
### `deleteDefaultRichMenu(): Promise<{}>`
It corresponds to the [Cancel default rich menu](https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu) API.
### Account link
#### `getLinkToken(userId: string): Promise<string>`
Send an HTTP POST request to the `/bot/user/{userId}/linkToken` endpoint,
and [issue a link token](https://developers.line.biz/en/reference/messaging-api/#issue-link-token) for the user you are attempting to link.
If the request succeeds, a link token will be returned.
Link tokens are valid for 10 minutes and can only be used once.
### Get number of messages sent
#### `getNumberOfSentReplyMessages(date: string): Promise<NumberOfMessagesSentResponse>`
Gets the number of messages sent with the `/bot/message/reply` endpoint.
The number of messages retrieved by this operation does not include
the number of messages sent from LINE@ Manager.
``` js
client.getNumberOfSentReplyMessages('20191231').then((response) => {
console.log(response);
})
```
#### `getNumberOfSentPushMessages(date: string): Promise<NumberOfMessagesSentResponse>`
Gets the number of messages sent with the `/bot/message/push` endpoint.
The number of messages retrieved by this operation does not include
the number of messages sent from LINE@ Manager.
``` js
client.getNumberOfSentPushMessages('20191231').then((response) => {
console.log(response);
})
```
#### `getNumberOfSentMulticastMessages(date: string): Promise<NumberOfMessagesSentResponse>`
Gets the number of messages sent with the `/bot/message/multicast` endpoint.
The number of messages retrieved by this operation does not include
the number of messages sent from LINE@ Manager.
``` js
client.getNumberOfSentMulticastMessages('20191231').then((response) => {
console.log(response);
})
```
#### `getTargetLimitForAdditionalMessages(): Promise<TargetLimitForAdditionalMessages>`
Gets the target limit for additional messages in the current month.
The number of messages retrieved by this operation includes the number of messages sent from LINE Official Account Manager.
Set a target limit with LINE Official Account Manager. For the procedures, refer to the LINE Official Account Manager manual.
Note: LINE@ accounts cannot call this API endpoint.
``` js
client.getTargetLimitForAdditionalMessages().then((response) => {
console.log(response);
})
```
#### `getNumberOfMessagesSentThisMonth(): Promise<NumberOfMessagesSentThisMonth>`
Gets the number of messages sent in the current month.
The number of messages retrieved by this operation includes the number of messages sent from LINE Official Account Manager.
The number of messages retrieved by this operation is approximate. To get the correct number of sent messages, use LINE Official Account Manager or execute API operations for getting the number of sent messages.
Note: LINE@ accounts cannot call this API endpoint.
``` js
client.getNumberOfMessagesSentThisMonth().then((response) => {
console.log(response);
})
```
#### `getNumberOfSentBroadcastMessages(date: string): Promise<NumberOfMessagesSentResponse>`
Gets the number of messages sent with the `/bot/message/broadcast` endpoint.
The number of messages retrieved by this operation does not include the number of messages sent from LINE Official Account Manager.
Note: LINE@ accounts cannot call this API endpoint. Please migrate it to a LINE official account. For more information, see [Migration of LINE@ accounts](https://developers.line.biz/en/docs/messaging-api/migrating-line-at/).
``` js
client.getNumberOfSentBroadcastMessages('20191231').then((response) => {
console.log(response);
})
```
### Insight
#### `getNumberOfMessageDeliveries(date: string): Promise<NumberOfMessageDeliveriesResponse>`
It corresponds to the [Get number of message deliveries](https://developers.line.biz/en/reference/messaging-api/#get-number-of-delivery-messages) API.
#### `getNumberOfFollowers(date: string): Promise<NumberOfFollowersResponse>`
It corresponds to the [Get number of followers](https://developers.line.biz/en/reference/messaging-api/#get-number-of-followers) API.
#### `getFriendDemographics(): Promise<Types.FriendDemographics>`
It corresponds to the [Get friend demographics](https://developers.line.biz/en/reference/messaging-api/#get-demographic) API.
### Bot
#### `getBotInfo(): Promise<BotInfoResponse>`
It corresponds to the [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info) API.
### Webhook
#### `setWebhookEndpointUrl(endpoint: string): Promise<{}}>`
It corresponds to the [Set webhook endpoint URL](https://developers.line.biz/en/reference/messaging-api/#set-webhook-endpoint-url) API.
#### `getWebhookEndpointInfo(): Promise<Types.WebhookEndpointInfoResponse>`
It corresponds to the [Get webhook endpoint information](https://developers.line.biz/en/reference/messaging-api/#get-webhook-endpoint-information) API.
#### `testWebhookEndpoint(endpoint?: string): Promise<Types.TestWebhookEndpointResponse>`
It corresponds to the [Test webhook endpoint](https://developers.line.biz/en/reference/messaging-api/#test-webhook-endpoint) API.
# 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}`);
});
{
"name": "echo-bot-ts",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@line/bot-sdk": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-7.2.0.tgz",
"integrity": "sha512-TW9BB1Om1lFWWo3sw9A4kx+9ZTHBjfqIj6NmUY9md+DDR5Z4X98wEeM3l74mkrbZ7naK6AtRgUw5UIaKQgi3nA==",
"requires": {
"@types/body-parser": "^1.19.0",
"@types/node": "^14.10.0",
"axios": "^0.20.0",
"body-parser": "^1.19.0",
"file-type": "^15.0.0",
"form-data": "^3.0.0"
}
},
"@tokenizer/token": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
"integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
"integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
"requires": {
"@types/node": "*"
}
},
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
},
"@types/express": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz",
"integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz",
"integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"@types/node": {
"version": "14.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
"integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ=="
},
"@types/qs": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
"integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
"dev": true,
"requires": {
"@types/mime": "*",
"@types/node": "*"
}
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"axios": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
"integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"file-type": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-15.0.1.tgz",
"integrity": "sha512-0LieQlSA3bWUdErNrxzxfI4rhsvNAVPBO06R8pTc1hp9SE6nhqlVyvhcaXoMmtXkBTPnQenbMPLW9X76hH76oQ==",
"requires": {
"readable-web-to-node-stream": "^2.0.0",
"strtok3": "^6.0.3",
"token-types": "^2.0.0",
"typedarray-to-buffer": "^3.1.5"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
},
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"requires": {
"mime-db": "1.44.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"peek-readable": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz",
"integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA=="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"readable-web-to-node-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz",
"integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"strtok3": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.4.tgz",
"integrity": "sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==",
"requires": {
"@tokenizer/token": "^0.1.1",
"@types/debug": "^4.1.5",
"peek-readable": "^3.1.0"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"token-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz",
"integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==",
"requires": {
"@tokenizer/token": "^0.1.0",
"ieee754": "^1.1.13"
}
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"dev": true
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}
{
"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}`);
});
{
"name": "echo-bot",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@line/bot-sdk": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-6.8.0.tgz",
"integrity": "sha512-DoUdgBZKw/hFCubRvwcUFu8eV9uBPm4izlXTqVyT7melqq8pOxdR+74qOJ4HhQnd3/RQYdhWloiqyXH0FPHePw==",
"requires": {
"@types/body-parser": "^1.16.8",
"@types/file-type": "^5.2.1",
"@types/node": "^7.0.31",
"axios": "^0.19.0",
"body-parser": "^1.18.2",
"file-type": "^7.2.0"
}
},
"@types/body-parser": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
"requires": {
"@types/node": "*"
}
},
"@types/file-type": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/file-type/-/file-type-5.2.2.tgz",
"integrity": "sha512-GWtM4fyqfb+bec4ocpo51/y4x0b83Je+iA6eV131LT9wL0//G+1UgwbkMg7w61ceOwR+KkZXK00z44jrrNljWg==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "7.10.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.6.tgz",
"integrity": "sha512-d0BOAicT0tEdbdVQlLGOVul1kvg6YvbaADRCThGCz5NJ0e9r00SofcR1x69hmlCyrHuB6jd4cKzL9bMLjPnpAA=="
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
}
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
}
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
"integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.2",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.3",
"qs": "6.5.1",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.1",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"body-parser": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
"integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.1",
"http-errors": "~1.6.2",
"iconv-lite": "0.4.19",
"on-finished": "~2.3.0",
"qs": "6.5.1",
"raw-body": "2.3.2",
"type-is": "~1.6.15"
}
},
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"raw-body": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
"integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.2",
"iconv-lite": "0.4.19",
"unpipe": "1.0.0"
},
"dependencies": {
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
},
"http-errors": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
"integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
"requires": {
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
"statuses": ">= 1.3.1 < 2"
}
},
"setprototypeof": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
}
}
}
}
},
"file-type": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-7.7.1.tgz",
"integrity": "sha512-bTrKkzzZI6wH+NXhyD3SOXtb2zXTw2SbwI2RxUlRcXVsnN7jNL5hJzVQLYv7FOQhxFkK4XWdAflEaWFpaLLWpQ=="
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"unpipe": "~1.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
"integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
},
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
},
"mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
},
"mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"requires": {
"mime-db": "~1.33.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
"integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.6.0"
}
},
"qs": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
}
}
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}
{
"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.
'use strict';
const line = require('@line/bot-sdk');
const express = require('express');
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const ngrok = require('ngrok');
// create LINE SDK config from env variables
const config = {
channelAccessToken: '6Aw67RtwWFJb+GCbr5DGhlFY5w6i0HpuKRNA1BNyGIDfXYrA2V/+S0yCgYy+jh4R2wHvg1XEZh0hhsFof81squrHYTR+5yvrPyuaNcDOZnkQLl4X+EWi6vDB5Rf6VeRUclczO0VyXd4hp9Oo4QUmZwdB04t89/1O/w1cDnyilFU=',
channelSecret: '2caa1add6c76bd51a84bd861e27c800c',
};
// base URL for webhook server
let baseURL = process.env.BASE_URL;
// create LINE SDK client
const client = new line.Client(config);
// create Express app
// about Express itself: https://expressjs.com/
const app = express();
// serve static and downloaded files
app.use('/static', express.static('static'));
app.use('/downloaded', express.static('downloaded'));
app.get('/callback', (req, res) => res.end(`I'm listening. Please access with POST.`));
// webhook callback
app.post('/callback', line.middleware(config), (req, res) => {
if (req.body.destination) {
console.log("Destination User ID: " + req.body.destination);
}
// req.body.events should be an array of events
if (!Array.isArray(req.body.events)) {
return res.status(500).end();
}
// handle events separately
Promise.all(req.body.events.map(handleEvent))
.then(() => res.end())
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
// simple reply function
const replyText = (token, texts) => {
texts = Array.isArray(texts) ? texts : [texts];
return client.replyMessage(
token,
texts.map((text) => ({ type: 'text', text }))
);
};
// callback function to handle a single event
function handleEvent(event) {
if (event.replyToken && event.replyToken.match(/^(.)\1*$/)) {
return console.log("Test hook recieved: " + JSON.stringify(event.message));
}
switch (event.type) {
case 'message':
const message = event.message;
switch (message.type) {
case 'text':
return handleText(message, event.replyToken, event.source);
case 'image':
return handleImage(message, event.replyToken);
case 'video':
return handleVideo(message, event.replyToken);
case 'audio':
return handleAudio(message, event.replyToken);
case 'location':
return handleLocation(message, event.replyToken);
case 'sticker':
return handleSticker(message, event.replyToken);
default:
throw new Error(`Unknown message: ${JSON.stringify(message)}`);
}
case 'follow':
return replyText(event.replyToken, 'Got followed event');
case 'unfollow':
return console.log(`Unfollowed this bot: ${JSON.stringify(event)}`);
case 'join':
return replyText(event.replyToken, `Joined ${event.source.type}`);
case 'leave':
return console.log(`Left: ${JSON.stringify(event)}`);
case 'postback':
let data = event.postback.data;
if (data === 'DATE' || data === 'TIME' || data === 'DATETIME') {
data += `(${JSON.stringify(event.postback.params)})`;
}
return replyText(event.replyToken, `Got postback: ${data}`);
case 'beacon':
return replyText(event.replyToken, `Got beacon: ${event.beacon.hwid}`);
default:
throw new Error(`Unknown event: ${JSON.stringify(event)}`);
}
}
function handleText(message, replyToken, source) {
const buttonsImageURL = `${baseURL}/static/buttons/1040.jpg`;
//★text에 따른 응답 변화!!★
switch (message.text) {
case '음악 추천해줘':
message.text = '아이유의 음악을 추천드립니다.';
return replyText(replyToken, message.text);
case 'profile':
if (source.userId) {
return client.getProfile(source.userId)
.then((profile) => replyText(
replyToken,
[
`Display name: ${profile.displayName}`,
`Status message: ${profile.statusMessage}`,
]
));
} else {
return replyText(replyToken, 'Bot can\'t use profile API without user ID');
}
case 'buttons':
return client.replyMessage(
replyToken,
{
type: 'template',
altText: 'Buttons alt text',
template: {
type: 'buttons',
thumbnailImageUrl: buttonsImageURL,
title: 'My button sample',
text: 'Hello, my button',
actions: [
{ label: 'Go to line.me', type: 'uri', uri: 'https://line.me' },
{ label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
{ label: '言 hello2', type: 'postback', data: 'hello こんにちは', text: 'hello こんにちは' },
{ label: 'Say message', type: 'message', text: 'Rice=米' },
],
},
}
);
case 'confirm':
return client.replyMessage(
replyToken,
{
type: 'template',
altText: 'Confirm alt text',
template: {
type: 'confirm',
text: 'Do it?',
actions: [
{ label: 'Yes', type: 'message', text: 'Yes!' },
{ label: 'No', type: 'message', text: 'No!' },
],
},
}
)
case 'carousel':
return client.replyMessage(
replyToken,
{
type: 'template',
altText: 'Carousel alt text',
template: {
type: 'carousel',
columns: [
{
thumbnailImageUrl: buttonsImageURL,
title: 'hoge',
text: 'fuga',
actions: [
{ label: 'Go to line.me', type: 'uri', uri: 'https://line.me' },
{ label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
],
},
{
thumbnailImageUrl: buttonsImageURL,
title: 'hoge',
text: 'fuga',
actions: [
{ label: '言 hello2', type: 'postback', data: 'hello こんにちは', text: 'hello こんにちは' },
{ label: 'Say message', type: 'message', text: 'Rice=米' },
],
},
],
},
}
);
case 'image carousel':
return client.replyMessage(
replyToken,
{
type: 'template',
altText: 'Image carousel alt text',
template: {
type: 'image_carousel',
columns: [
{
imageUrl: buttonsImageURL,
action: { label: 'Go to LINE', type: 'uri', uri: 'https://line.me' },
},
{
imageUrl: buttonsImageURL,
action: { label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
},
{
imageUrl: buttonsImageURL,
action: { label: 'Say message', type: 'message', text: 'Rice=米' },
},
{
imageUrl: buttonsImageURL,
action: {
label: 'datetime',
type: 'datetimepicker',
data: 'DATETIME',
mode: 'datetime',
},
},
]
},
}
);
case 'datetime':
return client.replyMessage(
replyToken,
{
type: 'template',
altText: 'Datetime pickers alt text',
template: {
type: 'buttons',
text: 'Select date / time !',
actions: [
{ type: 'datetimepicker', label: 'date', data: 'DATE', mode: 'date' },
{ type: 'datetimepicker', label: 'time', data: 'TIME', mode: 'time' },
{ type: 'datetimepicker', label: 'datetime', data: 'DATETIME', mode: 'datetime' },
],
},
}
);
case 'imagemap':
return client.replyMessage(
replyToken,
{
type: 'imagemap',
baseUrl: `${baseURL}/static/rich`,
altText: 'Imagemap alt text',
baseSize: { width: 1040, height: 1040 },
actions: [
{ area: { x: 0, y: 0, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/manga/en' },
{ area: { x: 520, y: 0, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/music/en' },
{ area: { x: 0, y: 520, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/play/en' },
{ area: { x: 520, y: 520, width: 520, height: 520 }, type: 'message', text: 'URANAI!' },
],
video: {
originalContentUrl: `${baseURL}/static/imagemap/video.mp4`,
previewImageUrl: `${baseURL}/static/imagemap/preview.jpg`,
area: {
x: 280,
y: 385,
width: 480,
height: 270,
},
externalLink: {
linkUri: 'https://line.me',
label: 'LINE'
}
},
}
);
case 'bye':
switch (source.type) {
case 'user':
return replyText(replyToken, 'Bot can\'t leave from 1:1 chat');
case 'group':
return replyText(replyToken, 'Leaving group')
.then(() => client.leaveGroup(source.groupId));
case 'room':
return replyText(replyToken, 'Leaving room')
.then(() => client.leaveRoom(source.roomId));
}
default:
console.log(`Echo message to ${replyToken}: ${message.text}`);
return replyText(replyToken, message.text);
}
}
function handleImage(message, replyToken) {
let getContent;
if (message.contentProvider.type === "line") {
const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.jpg`);
const previewPath = path.join(__dirname, 'downloaded', `${message.id}-preview.jpg`);
getContent = downloadContent(message.id, downloadPath)
.then((downloadPath) => {
// ImageMagick is needed here to run 'convert'
// Please consider about security and performance by yourself
cp.execSync(`convert -resize 240x jpeg:${downloadPath} jpeg:${previewPath}`);
return {
originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
previewImageUrl: baseURL + '/downloaded/' + path.basename(previewPath),
};
});
} else if (message.contentProvider.type === "external") {
getContent = Promise.resolve(message.contentProvider);
}
return getContent
.then(({ originalContentUrl, previewImageUrl }) => {
return client.replyMessage(
replyToken,
{
type: 'image',
originalContentUrl,
previewImageUrl,
}
);
});
}
function handleVideo(message, replyToken) {
let getContent;
if (message.contentProvider.type === "line") {
const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.mp4`);
const previewPath = path.join(__dirname, 'downloaded', `${message.id}-preview.jpg`);
getContent = downloadContent(message.id, downloadPath)
.then((downloadPath) => {
// FFmpeg and ImageMagick is needed here to run 'convert'
// Please consider about security and performance by yourself
cp.execSync(`convert mp4:${downloadPath}[0] jpeg:${previewPath}`);
return {
originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
previewImageUrl: baseURL + '/downloaded/' + path.basename(previewPath),
}
});
} else if (message.contentProvider.type === "external") {
getContent = Promise.resolve(message.contentProvider);
}
return getContent
.then(({ originalContentUrl, previewImageUrl }) => {
return client.replyMessage(
replyToken,
{
type: 'video',
originalContentUrl,
previewImageUrl,
}
);
});
}
function handleAudio(message, replyToken) {
let getContent;
if (message.contentProvider.type === "line") {
const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.m4a`);
getContent = downloadContent(message.id, downloadPath)
.then((downloadPath) => {
return {
originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
};
});
} else {
getContent = Promise.resolve(message.contentProvider);
}
return getContent
.then(({ originalContentUrl }) => {
return client.replyMessage(
replyToken,
{
type: 'audio',
originalContentUrl,
duration: message.duration,
}
);
});
}
function downloadContent(messageId, downloadPath) {
return client.getMessageContent(messageId)
.then((stream) => new Promise((resolve, reject) => {
const writable = fs.createWriteStream(downloadPath);
stream.pipe(writable);
stream.on('end', () => resolve(downloadPath));
stream.on('error', reject);
}));
}
function handleLocation(message, replyToken) {
return client.replyMessage(
replyToken,
{
type: 'location',
title: message.title,
address: message.address,
latitude: message.latitude,
longitude: message.longitude,
}
);
}
function handleSticker(message, replyToken) {
return client.replyMessage(
replyToken,
{
type: 'sticker',
packageId: message.packageId,
stickerId: message.stickerId,
}
);
}
// listen on port
const port = process.env.PORT || 3000;
app.listen(port, () => {
if (baseURL) {
console.log(`listening on ${baseURL}:${port}/callback`);
} else {
console.log("It seems that BASE_URL is not set. Connecting to ngrok...")
ngrok.connect(port).then(url => {
baseURL = url;
console.log(`listening on ${baseURL}/callback`);
}).catch(console.error);
}
});
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"
}
}
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
import { Readable } from "stream";
import HTTPClient from "./http";
import * as Types from "./types";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import { createMultipartFormData, ensureJSON, toArray } from "./utils";
type ChatType = "group" | "room";
type RequestOption = {
retryKey: string;
};
import {
MESSAGING_API_PREFIX,
DATA_API_PREFIX,
OAUTH_BASE_PREFIX,
OAUTH_BASE_PREFIX_V2_1,
} from "./endpoints";
export default class Client {
public config: Types.ClientConfig;
private http: HTTPClient;
private requestOption: Partial<RequestOption> = {};
constructor(config: Types.ClientConfig) {
if (!config.channelAccessToken) {
throw new Error("no channel access token");
}
this.config = config;
this.http = new HTTPClient({
defaultHeaders: {
Authorization: "Bearer " + this.config.channelAccessToken,
},
responseParser: this.parseHTTPResponse.bind(this),
...config.httpConfig,
});
}
public setRequestOptionOnce(option: Partial<RequestOption>) {
this.requestOption = option;
}
private generateRequestConfig(): Partial<AxiosRequestConfig> {
const config: Partial<AxiosRequestConfig> = { headers: {} };
if (this.requestOption.retryKey) {
config.headers["X-Line-Retry-Key"] = this.requestOption.retryKey;
}
// clear requestOption
this.requestOption = {};
return config;
}
private parseHTTPResponse(response: AxiosResponse) {
const { LINE_REQUEST_ID_HTTP_HEADER_NAME } = Types;
let resBody = {
...response.data,
};
if (response.headers[LINE_REQUEST_ID_HTTP_HEADER_NAME]) {
resBody[LINE_REQUEST_ID_HTTP_HEADER_NAME] =
response.headers[LINE_REQUEST_ID_HTTP_HEADER_NAME];
}
return resBody;
}
public pushMessage(
to: string,
messages: Types.Message | Types.Message[],
notificationDisabled: boolean = false,
): Promise<Types.MessageAPIResponseBase> {
return this.http.post(
`${MESSAGING_API_PREFIX}/message/push`,
{
messages: toArray(messages),
to,
notificationDisabled,
},
this.generateRequestConfig(),
);
}
public replyMessage(
replyToken: string,
messages: Types.Message | Types.Message[],
notificationDisabled: boolean = false,
): Promise<Types.MessageAPIResponseBase> {
return this.http.post(`${MESSAGING_API_PREFIX}/message/reply`, {
messages: toArray(messages),
replyToken,
notificationDisabled,
});
}
public async multicast(
to: string[],
messages: Types.Message | Types.Message[],
notificationDisabled: boolean = false,
): Promise<Types.MessageAPIResponseBase> {
return this.http.post(
`${MESSAGING_API_PREFIX}/message/multicast`,
{
messages: toArray(messages),
to,
notificationDisabled,
},
this.generateRequestConfig(),
);
}
public async narrowcast(
messages: Types.Message | Types.Message[],
recipient?: Types.ReceieptObject,
filter?: { demographic: Types.DemographicFilterObject },
limit?: { max?: number; upToRemainingQuota?: boolean },
notificationDisabled?: boolean,
): Promise<Types.MessageAPIResponseBase> {
return this.http.post(
`${MESSAGING_API_PREFIX}/message/narrowcast`,
{
messages: toArray(messages),
recipient,
filter,
limit,
notificationDisabled,
},
this.generateRequestConfig(),
);
}
public async broadcast(
messages: Types.Message | Types.Message[],
notificationDisabled: boolean = false,
): Promise<Types.MessageAPIResponseBase> {
return this.http.post(
`${MESSAGING_API_PREFIX}/message/broadcast`,
{
messages: toArray(messages),
notificationDisabled,
},
this.generateRequestConfig(),
);
}
public async getProfile(userId: string): Promise<Types.Profile> {
const profile = await this.http.get<Types.Profile>(
`${MESSAGING_API_PREFIX}/profile/${userId}`,
);
return ensureJSON(profile);
}
private async getChatMemberProfile(
chatType: ChatType,
chatId: string,
userId: string,
): Promise<Types.Profile> {
const profile = await this.http.get<Types.Profile>(
`${MESSAGING_API_PREFIX}/${chatType}/${chatId}/member/${userId}`,
);
return ensureJSON(profile);
}
public async getGroupMemberProfile(
groupId: string,
userId: string,
): Promise<Types.Profile> {
return this.getChatMemberProfile("group", groupId, userId);
}
public async getRoomMemberProfile(
roomId: string,
userId: string,
): Promise<Types.Profile> {
return this.getChatMemberProfile("room", roomId, userId);
}
private async getChatMemberIds(
chatType: ChatType,
chatId: string,
): Promise<string[]> {
let memberIds: string[] = [];
let start: string;
do {
const res = await this.http.get<{ memberIds: string[]; next?: string }>(
`${MESSAGING_API_PREFIX}/${chatType}/${chatId}/members/ids`,
start ? { start } : null,
);
ensureJSON(res);
memberIds = memberIds.concat(res.memberIds);
start = res.next;
} while (start);
return memberIds;
}
public async getGroupMemberIds(groupId: string): Promise<string[]> {
return this.getChatMemberIds("group", groupId);
}
public async getRoomMemberIds(roomId: string): Promise<string[]> {
return this.getChatMemberIds("room", roomId);
}
public async getGroupMembersCount(
groupId: string,
): Promise<Types.MembersCountResponse> {
const groupMemberCount = await this.http.get<Types.MembersCountResponse>(
`${MESSAGING_API_PREFIX}/group/${groupId}/members/count`,
);
return ensureJSON(groupMemberCount);
}
public async getRoomMembersCount(
roomId: string,
): Promise<Types.MembersCountResponse> {
const roomMemberCount = await this.http.get<Types.MembersCountResponse>(
`${MESSAGING_API_PREFIX}/room/${roomId}/members/count`,
);
return ensureJSON(roomMemberCount);
}
public async getGroupSummary(
groupId: string,
): Promise<Types.GroupSummaryResponse> {
const groupSummary = await this.http.get<Types.GroupSummaryResponse>(
`${MESSAGING_API_PREFIX}/group/${groupId}/summary`,
);
return ensureJSON(groupSummary);
}
public async getMessageContent(messageId: string): Promise<Readable> {
return this.http.getStream(
`${DATA_API_PREFIX}/message/${messageId}/content`,
);
}
private leaveChat(chatType: ChatType, chatId: string): Promise<any> {
return this.http.post(
`${MESSAGING_API_PREFIX}/${chatType}/${chatId}/leave`,
);
}
public async leaveGroup(groupId: string): Promise<any> {
return this.leaveChat("group", groupId);
}
public async leaveRoom(roomId: string): Promise<any> {
return this.leaveChat("room", roomId);
}
public async getRichMenu(
richMenuId: string,
): Promise<Types.RichMenuResponse> {
const res = await this.http.get<Types.RichMenuResponse>(
`${MESSAGING_API_PREFIX}/richmenu/${richMenuId}`,
);
return ensureJSON(res);
}
public async createRichMenu(richMenu: Types.RichMenu): Promise<string> {
const res = await this.http.post<any>(
`${MESSAGING_API_PREFIX}/richmenu`,
richMenu,
);
return ensureJSON(res).richMenuId;
}
public async deleteRichMenu(richMenuId: string): Promise<any> {
return this.http.delete(`${MESSAGING_API_PREFIX}/richmenu/${richMenuId}`);
}
public async getRichMenuIdOfUser(userId: string): Promise<string> {
const res = await this.http.get<any>(
`${MESSAGING_API_PREFIX}/user/${userId}/richmenu`,
);
return ensureJSON(res).richMenuId;
}
public async linkRichMenuToUser(
userId: string,
richMenuId: string,
): Promise<any> {
return this.http.post(
`${MESSAGING_API_PREFIX}/user/${userId}/richmenu/${richMenuId}`,
);
}
public async unlinkRichMenuFromUser(userId: string): Promise<any> {
return this.http.delete(`${MESSAGING_API_PREFIX}/user/${userId}/richmenu`);
}
public async linkRichMenuToMultipleUsers(
richMenuId: string,
userIds: string[],
): Promise<any> {
return this.http.post(`${MESSAGING_API_PREFIX}/richmenu/bulk/link`, {
richMenuId,
userIds,
});
}
public async unlinkRichMenusFromMultipleUsers(
userIds: string[],
): Promise<any> {
return this.http.post(`${MESSAGING_API_PREFIX}/richmenu/bulk/unlink`, {
userIds,
});
}
public async getRichMenuImage(richMenuId: string): Promise<Readable> {
return this.http.getStream(
`${DATA_API_PREFIX}/richmenu/${richMenuId}/content`,
);
}
public async setRichMenuImage(
richMenuId: string,
data: Buffer | Readable,
contentType?: string,
): Promise<any> {
return this.http.postBinary(
`${DATA_API_PREFIX}/richmenu/${richMenuId}/content`,
data,
contentType,
);
}
public async getRichMenuList(): Promise<Array<Types.RichMenuResponse>> {
const res = await this.http.get<any>(
`${MESSAGING_API_PREFIX}/richmenu/list`,
);
return ensureJSON(res).richmenus;
}
public async setDefaultRichMenu(richMenuId: string): Promise<{}> {
return this.http.post(
`${MESSAGING_API_PREFIX}/user/all/richmenu/${richMenuId}`,
);
}
public async getDefaultRichMenuId(): Promise<string> {
const res = await this.http.get<any>(
`${MESSAGING_API_PREFIX}/user/all/richmenu`,
);
return ensureJSON(res).richMenuId;
}
public async deleteDefaultRichMenu(): Promise<{}> {
return this.http.delete(`${MESSAGING_API_PREFIX}/user/all/richmenu`);
}
public async getLinkToken(userId: string): Promise<string> {
const res = await this.http.post<any>(
`${MESSAGING_API_PREFIX}/user/${userId}/linkToken`,
);
return ensureJSON(res).linkToken;
}
public async getNumberOfSentReplyMessages(
date: string,
): Promise<Types.NumberOfMessagesSentResponse> {
const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
`${MESSAGING_API_PREFIX}/message/delivery/reply?date=${date}`,
);
return ensureJSON(res);
}
public async getNumberOfSentPushMessages(
date: string,
): Promise<Types.NumberOfMessagesSentResponse> {
const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
`${MESSAGING_API_PREFIX}/message/delivery/push?date=${date}`,
);
return ensureJSON(res);
}
public async getNumberOfSentMulticastMessages(
date: string,
): Promise<Types.NumberOfMessagesSentResponse> {
const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
`${MESSAGING_API_PREFIX}/message/delivery/multicast?date=${date}`,
);
return ensureJSON(res);
}
public async getNarrowcastProgress(
requestId: string,
): Promise<Types.NarrowcastProgressResponse> {
const res = await this.http.get<Types.NarrowcastProgressResponse>(
`${MESSAGING_API_PREFIX}/message/progress/narrowcast?requestId=${requestId}`,
);
return ensureJSON(res);
}
public async getTargetLimitForAdditionalMessages(): Promise<Types.TargetLimitForAdditionalMessages> {
const res = await this.http.get<Types.TargetLimitForAdditionalMessages>(
`${MESSAGING_API_PREFIX}/message/quota`,
);
return ensureJSON(res);
}
public async getNumberOfMessagesSentThisMonth(): Promise<Types.NumberOfMessagesSentThisMonth> {
const res = await this.http.get<Types.NumberOfMessagesSentThisMonth>(
`${MESSAGING_API_PREFIX}/message/quota/consumption`,
);
return ensureJSON(res);
}
public async getNumberOfSentBroadcastMessages(
date: string,
): Promise<Types.NumberOfMessagesSentResponse> {
const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
`${MESSAGING_API_PREFIX}/message/delivery/broadcast?date=${date}`,
);
return ensureJSON(res);
}
public async getNumberOfMessageDeliveries(
date: string,
): Promise<Types.NumberOfMessageDeliveriesResponse> {
const res = await this.http.get<Types.NumberOfMessageDeliveriesResponse>(
`${MESSAGING_API_PREFIX}/insight/message/delivery?date=${date}`,
);
return ensureJSON(res);
}
public async getNumberOfFollowers(
date: string,
): Promise<Types.NumberOfFollowersResponse> {
const res = await this.http.get<Types.NumberOfFollowersResponse>(
`${MESSAGING_API_PREFIX}/insight/followers?date=${date}`,
);
return ensureJSON(res);
}
public async getFriendDemographics(): Promise<Types.FriendDemographics> {
const res = await this.http.get<Types.FriendDemographics>(
`${MESSAGING_API_PREFIX}/insight/demographic`,
);
return ensureJSON(res);
}
public async getUserInteractionStatistics(
requestId: string,
): Promise<Types.UserInteractionStatistics> {
const res = await this.http.get<Types.UserInteractionStatistics>(
`${MESSAGING_API_PREFIX}/insight/message/event?requestId=${requestId}`,
);
return ensureJSON(res);
}
public async createUploadAudienceGroup(uploadAudienceGroup: {
description: string;
isIfaAudience?: boolean;
audiences?: { id: string }[];
uploadDescription?: string;
}) {
const res = await this.http.post<{
audienceGroupId: number;
type: string;
description: string;
created: number;
}>(`${MESSAGING_API_PREFIX}/audienceGroup/upload`, {
...uploadAudienceGroup,
});
return ensureJSON(res);
}
public async createUploadAudienceGroupByFile(uploadAudienceGroup: {
description: string;
isIfaAudience?: boolean;
uploadDescription?: string;
file: Buffer | Readable;
}) {
const file = await this.http.toBuffer(uploadAudienceGroup.file);
const body = createMultipartFormData({ ...uploadAudienceGroup, file });
const res = await this.http.post<{
audienceGroupId: number;
type: "UPLOAD";
description: string;
created: number;
}>(`${DATA_API_PREFIX}/audienceGroup/upload/byFile`, body, {
headers: body.getHeaders(),
});
return ensureJSON(res);
}
public async updateUploadAudienceGroup(
uploadAudienceGroup: {
audienceGroupId: number;
description?: string;
uploadDescription?: string;
audiences: { id: string }[];
},
// for set request timeout
httpConfig?: Partial<AxiosRequestConfig>,
) {
const res = await this.http.put<{}>(
`${MESSAGING_API_PREFIX}/audienceGroup/upload`,
{
...uploadAudienceGroup,
},
httpConfig,
);
return ensureJSON(res);
}
public async updateUploadAudienceGroupByFile(
uploadAudienceGroup: {
audienceGroupId: number;
uploadDescription?: string;
file: Buffer | Readable;
},
// for set request timeout
httpConfig?: Partial<AxiosRequestConfig>,
) {
const file = await this.http.toBuffer(uploadAudienceGroup.file);
const body = createMultipartFormData({ ...uploadAudienceGroup, file });
const res = await this.http.put<{}>(
`${DATA_API_PREFIX}/audienceGroup/upload/byFile`,
body,
{ headers: body.getHeaders(), ...httpConfig },
);
return ensureJSON(res);
}
public async createClickAudienceGroup(clickAudienceGroup: {
description: string;
requestId: string;
clickUrl?: string;
}) {
const res = await this.http.post<
{
audienceGroupId: number;
type: string;
created: number;
} & typeof clickAudienceGroup
>(`${MESSAGING_API_PREFIX}/audienceGroup/click`, {
...clickAudienceGroup,
});
return ensureJSON(res);
}
public async createImpAudienceGroup(impAudienceGroup: {
requestId: string;
description: string;
}) {
const res = await this.http.post<
{
audienceGroupId: number;
type: string;
created: number;
} & typeof impAudienceGroup
>(`${MESSAGING_API_PREFIX}/audienceGroup/imp`, {
...impAudienceGroup,
});
return ensureJSON(res);
}
public async setDescriptionAudienceGroup(
description: string,
audienceGroupId: string,
) {
const res = await this.http.put<{}>(
`${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}/updateDescription`,
{
description,
},
);
return ensureJSON(res);
}
public async deleteAudienceGroup(audienceGroupId: string) {
const res = await this.http.delete<{}>(
`${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}`,
);
return ensureJSON(res);
}
public async getAudienceGroup(audienceGroupId: string) {
const res = await this.http.get<Types.AudienceGroup>(
`${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}`,
);
return ensureJSON(res);
}
public async getAudienceGroups(
page: number,
description?: string,
status?: Types.AudienceGroupStatus,
size?: number,
createRoute?: Types.AudienceGroupCreateRoute,
includesExternalPublicGroups?: boolean,
) {
const res = await this.http.get<{
audienceGroups: Types.AudienceGroups;
hasNextPage: boolean;
totalCount: number;
readWriteAudienceGroupTotalCount: number;
page: number;
size: number;
}>(`${MESSAGING_API_PREFIX}/audienceGroup/list`, {
page,
description,
status,
size,
createRoute,
includesExternalPublicGroups,
});
return ensureJSON(res);
}
public async getAudienceGroupAuthorityLevel() {
const res = await this.http.get<{
authorityLevel: Types.AudienceGroupAuthorityLevel;
}>(`${MESSAGING_API_PREFIX}/audienceGroup/authorityLevel`);
return ensureJSON(res);
}
public async changeAudienceGroupAuthorityLevel(
authorityLevel: Types.AudienceGroupAuthorityLevel,
) {
const res = await this.http.put<{}>(
`${MESSAGING_API_PREFIX}/audienceGroup/authorityLevel`,
{ authorityLevel },
);
return ensureJSON(res);
}
public async getBotInfo(): Promise<Types.BotInfoResponse> {
const res = await this.http.get<Types.BotInfoResponse>(
`${MESSAGING_API_PREFIX}/info`,
);
return ensureJSON(res);
}
public async setWebhookEndpointUrl(endpoint: string) {
return this.http.put<{}>(
`${MESSAGING_API_PREFIX}/channel/webhook/endpoint`,
{ endpoint },
);
}
public async getWebhookEndpointInfo() {
const res = await this.http.get<Types.WebhookEndpointInfoResponse>(
`${MESSAGING_API_PREFIX}/channel/webhook/endpoint`,
);
return ensureJSON(res);
}
public async testWebhookEndpoint(endpoint?: string) {
const res = await this.http.post<Types.TestWebhookEndpointResponse>(
`${MESSAGING_API_PREFIX}/channel/webhook/test`,
{ endpoint },
);
return ensureJSON(res);
}
}
export class OAuth {
private http: HTTPClient;
constructor() {
this.http = new HTTPClient();
}
public issueAccessToken(
client_id: string,
client_secret: string,
): Promise<Types.ChannelAccessToken> {
return this.http.postForm(`${OAUTH_BASE_PREFIX}/accessToken`, {
grant_type: "client_credentials",
client_id,
client_secret,
});
}
public revokeAccessToken(access_token: string): Promise<{}> {
return this.http.postForm(`${OAUTH_BASE_PREFIX}/revoke`, { access_token });
}
public issueChannelAccessTokenV2_1(
client_assertion: string,
): Promise<Types.ChannelAccessToken> {
return this.http.postForm(`${OAUTH_BASE_PREFIX_V2_1}/token`, {
grant_type: "client_credentials",
client_assertion_type:
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion,
});
}
public getChannelAccessTokenKeyIdsV2_1(
client_assertion: string,
): Promise<{ key_ids: string[] }> {
return this.http.get(`${OAUTH_BASE_PREFIX_V2_1}/tokens/kid`, {
client_assertion_type:
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion,
});
}
public revokeChannelAccessTokenV2_1(
client_id: string,
client_secret: string,
access_token: string,
): Promise<{}> {
return this.http.postForm(`${OAUTH_BASE_PREFIX_V2_1}/revoke`, {
client_id,
client_secret,
access_token,
});
}
}
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);
};
}
import { AxiosRequestConfig } from "axios";
export interface Config {
channelAccessToken?: string;
channelSecret?: string;
}
export interface ClientConfig extends Config {
channelAccessToken: string;
httpConfig?: Partial<AxiosRequestConfig>;
}
export interface MiddlewareConfig extends Config {
channelSecret: string;
}
export type Profile = {
displayName: string;
userId: string;
pictureUrl: string;
statusMessage: string;
language?: string;
};
/**
* Request body which is sent by webhook.
*
* @see [Request body](https://developers.line.biz/en/reference/messaging-api/#request-body)
*/
export type WebhookRequestBody = {
/**
* User ID of a bot that should receive webhook events. The user ID value is a string that matches the regular expression, U[0-9a-f]{32}.
*/
destination: string;
/**
* Information about the event
*/
events: Array<WebhookEvent>;
};
/**
* JSON objects which contain events generated on the LINE Platform.
*
* @see [Webhook event objects](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
*/
export type WebhookEvent =
| MessageEvent
| UnsendEvent
| FollowEvent
| UnfollowEvent
| JoinEvent
| LeaveEvent
| MemberJoinEvent
| MemberLeaveEvent
| PostbackEvent
| VideoPlayCompleteEvent
| BeaconEvent
| AccountLinkEvent
| DeviceLinkEvent
| DeviceUnlinkEvent
| LINEThingsScenarioExecutionEvent;
export type EventBase = {
/**
* Channel state.
*
* `active`: The channel is active. You can send a reply message or push message from the bot server that received this webhook event.
*
* `standby`: The channel is waiting. The bot server that received this webhook event shouldn't send any messages.
*/
mode: "active" | "standby";
/**
* Time of the event in milliseconds
*/
timestamp: number;
/**
* Source user, group, or room object with information about the source of the event.
*/
source: EventSource;
};
export type EventSource = User | Group | Room;
export type User = { type: "user"; userId: string };
export type Group = {
type: "group";
groupId: string;
/**
* ID of the source user.
*
* Only included in [message events](https://developers.line.biz/en/reference/messaging-api/#message-event).
* Not included if the user has not agreed to the
* [Official Accounts Terms of Use](https://developers.line.biz/en/docs/messaging-api/user-consent/).
*/
userId?: string;
};
export type Room = {
type: "room";
roomId: string;
/**
* ID of the source user.
*
* Only included in [message events](https://developers.line.biz/en/reference/messaging-api/#message-event).
* Not included if the user has not agreed to the
* [Official Accounts Terms of Use](https://developers.line.biz/en/docs/messaging-api/user-consent/).
*/
userId?: string;
};
export type ReplyableEvent = EventBase & { replyToken: string };
/**
* Webhook event object which contains the sent message.
*
* The `message` property contains a message object which corresponds with the
* message type. You can reply to message events.
*
* @see [Message event](https://developers.line.biz/en/reference/messaging-api/#message-event)
*/
export type MessageEvent = {
type: "message";
message: EventMessage;
} & ReplyableEvent;
/**
* Event object for when the user unsends a message in a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
* or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room).
* [Unsend event](https://developers.line.biz/en/reference/messaging-api/#unsend-event)
*/
export type UnsendEvent = {
type: "unsend";
/**
* The message ID of the unsent message
*/
unsend: { messageId: string };
} & EventBase;
/**
* Event object for when your account is added as a friend (or unblocked).
*/
export type FollowEvent = { type: "follow" } & ReplyableEvent;
/**
* Event object for when your account is blocked.
*/
export type UnfollowEvent = { type: "unfollow" } & EventBase;
/**
* Event object for when your bot joins a group or room. You can reply to join events.
*
* A join event is triggered at different times for groups and rooms.
*
* - For groups: A join event is sent when a user invites your bot.
* - For rooms: A join event is sent when the first event (for example when a
* user sends a message or is added to the room) occurs after your bot is
* added.
*/
export type JoinEvent = { type: "join" } & ReplyableEvent;
/**
* Event object for when a user removes your bot from a group or a room.
*/
export type LeaveEvent = { type: "leave" } & EventBase;
/**
* Event object for when a user joins a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
* or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room) that the bot is in.
*/
export type MemberJoinEvent = {
type: "memberJoined";
/**
* User ID of users who joined
* Array of [source user](https://developers.line.biz/en/reference/messaging-api/#source-user) objects
*/
joined: { members: Array<User> };
} & ReplyableEvent;
/**
* Event object for when a user leaves a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
* or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room) that the bot is in.
*/
export type MemberLeaveEvent = {
type: "memberLeft";
/**
* User ID of users who left
* Array of [source user](https://developers.line.biz/en/reference/messaging-api/#source-user) objects
*/
left: { members: Array<User> };
} & EventBase;
/**
* Event object for when a user performs an action on a
* [template message](https://developers.line.biz/en/reference/messaging-api/#template-messages).
*/
export type PostbackEvent = {
type: "postback";
postback: Postback;
} & ReplyableEvent;
/**
* Event for when a user finishes viewing a video at least once with the specified trackingId sent by the LINE Official Account.
*/
export type VideoPlayCompleteEvent = {
type: "videoPlayComplete";
/**
* ID used to identify a video. Returns the same value as the trackingId assigned to the [video message](https://developers.line.biz/en/reference/messaging-api/#video-message).
* String
*/
videoPlayComplete: { trackingId: string };
} & ReplyableEvent;
/**
* Event object for when a user enters or leaves the range of a
* [LINE Beacon](https://developers.line.biz/en/docs/messaging-api/using-beacons/).
*/
export type BeaconEvent = ReplyableEvent & {
type: "beacon";
beacon: {
/**
* `leave` will be deprecated
*/
type: "enter" | "leave" | "banner" | "stay";
/**
* Hardware ID of the beacon that was detected
*/
hwid: string;
/**
* Device message of beacon that was detected.
*
* This message consists of data generated by the beacon to send notifications to bots.
* Only included in webhooks from devices that support the "device message" property.
* For more information, see the
* [LINE Simple Beacon specification](https://github.com/line/line-simple-beacon/blob/master/README.en.md/#line-simple-beacon-frame).
*/
dm?: string;
};
};
/**
* Event object for when a user has linked his/her LINE account with a provider's service account.
*/
export type AccountLinkEvent = ReplyableEvent & {
type: "accountLink";
link: {
result: "ok" | "failed";
/**
* Specified nonce when verifying the user ID
*/
nonce: string;
};
};
/**
* Indicates that a LINE Things-compatible device has been linked with LINE by a user operation.
* For more information, see [Receiving device link events via webhook](https://developers.line.biz/en/docs/line-things/develop-bot/#link-event).
*/
export type DeviceLinkEvent = ReplyableEvent & {
type: "things";
things: {
/**
* Device ID of the LINE Things-compatible device that was linked with LINE
*/
deviceId: string;
type: "link";
};
};
/**
* Indicates that a LINE Things-compatible device has been unlinked from LINE by a user operation.
* For more information, see [Receiving device unlink events via webhook](https://developers.line.biz/en/docs/line-things/develop-bot/#unlink-event).
*/
export type DeviceUnlinkEvent = ReplyableEvent & {
type: "things";
things: {
/**
* Device ID of the LINE Things-compatible device that was unlinked with LINE
*/
deviceId: string;
type: "unlink";
};
};
export type LINEThingsScenarioExecutionEvent = ReplyableEvent & {
type: "things";
things: {
type: "scenarioResult";
/**
* Device ID of the device that executed the scenario
*/
deviceId: string;
result: {
/**
* Scenario ID executed
*/
scenarioId: string;
/**
* Revision number of the scenario set containing the executed scenario
*/
revision: number;
/**
* Timestamp for when execution of scenario action started (milliseconds, LINE app time)
*/
startTime: number;
/**
* Timestamp for when execution of scenario was completed (milliseconds, LINE app time)
*/
endtime: number;
/**
* Scenario execution completion status
* See also [things.resultCode definitions](https://developers.line.biz/en/reference/messaging-api/#things-resultcode).
*/
resultCode: "success" | "gatt_error" | "runtime_error";
/**
* Execution result of individual operations specified in action
* Note that an array of actions specified in a scenario has the following characteristics
* - The actions defined in a scenario are performed sequentially, from top to bottom.
* - Each action produces some result when executed.
* Even actions that do not generate data, such as `SLEEP`, return an execution result of type `void`.
* The number of items in an action array may be 0.
*
* Therefore, things.actionResults has the following properties:
* - The number of items in the array matches the number of actions defined in the scenario.
* - The order of execution results matches the order in which actions are performed.
* That is, in a scenario set with multiple `GATT_READ` actions,
* the results are returned in the order in which each individual `GATT_READ` action was performed.
* - If 0 actions are defined in the scenario, the number of items in things.actionResults will be 0.
*/
actionResults: Array<LINEThingsActionResult>;
/**
* Data contained in notification
* The value is Base64-encoded binary data.
* Only included for scenarios where `trigger.type = BLE_NOTIFICATION`.
*/
bleNotificationPayload?: string;
/**
* Error reason
*/
errorReason?: string;
};
};
};
export type LINEThingsActionResult = {
/**
* `void`, `binary`
* Depends on `type` of the executed action.
* This property is always included if `things.actionResults` is not empty.
*/
type: "void" | "binary";
/**
* Base64-encoded binary data
* This property is always included when `things.actionResults[].type` is `binary`.
*/
data?: string;
};
export type EventMessage =
| TextEventMessage
| ImageEventMessage
| VideoEventMessage
| AudioEventMessage
| LocationEventMessage
| FileEventMessage
| StickerEventMessage;
export type EventMessageBase = { id: string };
/**
* Message object which contains the text sent from the source.
*/
export type TextEventMessage = {
type: "text";
text: string;
/**
* Sendable LINE emojis
*/
emojis?: {
index: number;
length: number;
productId: string;
emojiId: string;
}[];
/**
* Object containing the contents of the mentioned user.
*/
mention?: {
/**
* Mentioned user information.
* Max: 20 mentions
*/
mentionees: {
/**
* Index position of the user mention for a character in `text`,
* with the first character being at position 0.
*/
index: number;
/**
* The length of the text of the mentioned user. For a mention `@example`,
* 8 is the length.
*/
length: number;
userId: string;
}[];
};
} & EventMessageBase;
export type ContentProvider<WithPreview extends boolean = true> =
| {
/**
* The content is provided by LINE.
*
* The data itself can be retrieved from the content API.
*/
type: "line";
}
| {
/**
* The content is provided by a provider other than LINE
*/
type: "external";
/**
* URL of the content. Only included when contentProvider.type is external.
*/
originalContentUrl: string;
/**
* URL of the content preview. Only included when contentProvider.type is external.
*
* For contents without preview (e.g. audio), it's undefined.
*/
previewImageUrl: WithPreview extends true ? string : undefined;
};
/**
* Message object which contains the image content sent from the source.
* The binary image data can be retrieved using Client#getMessageContent.
*/
export type ImageEventMessage = {
type: "image";
contentProvider: ContentProvider;
} & EventMessageBase;
/**
* Message object which contains the video content sent from the source.
* The binary video data can be retrieved using Client#getMessageContent.
*/
export type VideoEventMessage = {
type: "video";
contentProvider: ContentProvider;
} & EventMessageBase;
/**
* Message object which contains the audio content sent from the source.
* The binary audio data can be retrieved using Client#getMessageContent.
*/
export type AudioEventMessage = {
type: "audio";
duration: number;
contentProvider: ContentProvider<false>;
} & EventMessageBase;
/**
* Message object which contains the file sent from the source.
* The binary data can be retrieved using Client#getMessageContent.
*/
export type FileEventMessage = {
type: "file";
fileName: string;
fileSize: string;
} & EventMessageBase;
/**
* Message object which contains the location data sent from the source.
*/
export type LocationEventMessage = {
type: "location";
title: string;
address: string;
latitude: number;
longitude: number;
} & EventMessageBase;
/**
* Message object which contains the sticker data sent from the source.
* For a list of basic LINE stickers and sticker IDs, see
* [sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
*/
export type StickerEventMessage = {
type: "sticker";
packageId: string;
stickerId: string;
stickerResourceType:
| "STATIC"
| "ANIMATION"
| "SOUND"
| "ANIMATION_SOUND"
| "POPUP"
| "POPUP_SOUND"
| "NAME_TEXT"
| "PER_STICKER_TEXT";
keywords: string[];
} & EventMessageBase;
export type Postback = {
data: string;
/**
* Object with the date and time selected by a user through a
* [datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action).
* Only returned for postback actions via a
* [datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action).
* The `full-date`, `time-hour`, and `time-minute` formats follow the
* [RFC3339 protocol](https://www.ietf.org/rfc/rfc3339.txt).
*/
params?: {
/**
* Date selected by user. Only included in the `date` mode.
*/
date?: string;
/**
* Time selected by the user. Only included in the `time` mode.
*/
time?: string;
/**
* Date and time selected by the user. Only included in the `datetime` mode.
*/
datetime?: string;
};
};
/**
* JSON object which contains the contents of the message you send.
*
* @see [Message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects)
*/
export type Message =
| TextMessage
| ImageMessage
| VideoMessage
| AudioMessage
| LocationMessage
| StickerMessage
| ImageMapMessage
| TemplateMessage
| FlexMessage;
/**
* @see [Common properties for messages](https://developers.line.biz/en/reference/messaging-api/#common-properties-for-messages)
*/
export type MessageCommon = {
/**
* For the quick reply feature.
* For more information, see [Using quick replies](https://developers.line.biz/en/docs/messaging-api/using-quick-reply/).
*
* If the user receives multiple
* [message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects),
* the quickReply property of the last message object is displayed.
*/
quickReply?: QuickReply;
/**
* [Change icon and display name](https://developers.line.biz/en/docs/messaging-api/icon-nickname-switch/)
*
* When sending a message from the LINE Official Account, you can specify the `sender.name` and the `sender.iconUrl` properties in [Message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects).
*/
sender?: Sender;
};
/**
* @see [Text message](https://developers.line.biz/en/reference/messaging-api/#text-message)
*/
export type TextMessage = MessageCommon & {
type: "text";
/**
* Message text. You can include the following emoji:
*
* - Unicode emoji
* - LINE original emoji
* ([Unicode codepoint table for LINE original emoji](https://developers.line.biz/media/messaging-api/emoji-list.pdf))
*
* Max: 2000 characters
*/
text: string;
};
/**
* @see [Image message](https://developers.line.biz/en/reference/messaging-api/#image-message)
*/
export type ImageMessage = MessageCommon & {
type: "image";
/**
* Image URL (Max: 1000 characters)
*
* - **HTTPS**
* - JPEG
* - Max: 1024 x 1024
* - Max: 1 MB
*/
originalContentUrl: string;
/**
* Preview image URL (Max: 1000 characters)
*
* - **HTTPS**
* - JPEG
* - Max: 240 x 240
* - Max: 1 MB
*/
previewImageUrl: string;
};
/**
* @see [Video message](https://developers.line.biz/en/reference/messaging-api/#video-message)
*/
export type VideoMessage = MessageCommon & {
type: "video";
/**
* URL of video file (Max: 1000 characters)
*
* - **HTTPS**
* - mp4
* - Max: 1 minute
* - Max: 10 MB
*
* A very wide or tall video may be cropped when played in some environments.
*/
originalContentUrl: string;
/**
* URL of preview image (Max: 1000 characters)
*
* - **HTTPS**
* - JPEG
* - Max: 240 x 240
* - Max: 1 MB
*/
previewImageUrl: string;
};
/**
* @see [Audio message](https://developers.line.biz/en/reference/messaging-api/#audio-message)
*/
export type AudioMessage = MessageCommon & {
type: "audio";
/**
* URL of audio file (Max: 1000 characters)
*
* - **HTTPS**
* - m4a
* - Max: 1 minute
* - Max: 10 MB
*/
originalContentUrl: string;
/**
* Length of audio file (milliseconds)
*/
duration: number;
};
/**
* @see [Location message](https://developers.line.biz/en/reference/messaging-api/#location-message)
*/
export type LocationMessage = MessageCommon & {
type: "location";
/**
* Title (Max: 100 characters)
*/
title: string;
/**
* Address (Max: 100 characters)
*/
address: string;
latitude: number;
longitude: number;
};
/**
* @see [Sticker message](https://developers.line.biz/en/reference/messaging-api/#sticker-message)
*/
export type StickerMessage = MessageCommon & {
type: "sticker";
/**
* Package ID for a set of stickers.
* For information on package IDs, see the
* [Sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
*/
packageId: string;
/**
* Sticker ID.
* For a list of sticker IDs for stickers that can be sent with the Messaging
* API, see the
* [Sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
*/
stickerId: string;
};
/**
* @see [Imagemap message](https://developers.line.biz/en/reference/messaging-api/#imagemap-message)
*/
export type ImageMapMessage = MessageCommon & {
type: "imagemap";
/**
* [Base URL](https://developers.line.biz/en/reference/messaging-api/#base-url) of image
* (Max: 1000 characters, **HTTPS**)
*/
baseUrl: string;
/**
* Alternative text (Max: 400 characters)
*/
altText: string;
baseSize: Size;
/**
* Video to play inside a image map messages
*/
video?: {
/**
* URL of video file (Max: 1000 characters)
*
* - **HTTPS**
* - mp4
* - Max: 1 minute
* - Max: 10 MB
*
* A very wide or tall video may be cropped when played in some environments.
*/
originalContentUrl: string;
/**
* URL of preview image (Max: 1000 characters)
*
* - **HTTPS**
* - JPEG
* - Max: 240 x 240
* - Max: 1 MB
*/
previewImageUrl: string;
area: Area;
/**
* External link to be displayed after a video is played
* This property is required if you set a video to play and a label to display after the video on the imagemap
*/
externalLink?: {
linkUri: string;
label: string;
};
};
/**
* Action when tapped (Max: 50)
*/
actions: ImageMapAction[];
};
/**
* Template messages are messages with predefined layouts which you can
* customize. For more information, see
* [template messages](https://developers.line.biz/en/docs/messaging-api/message-types/#template-messages).
*
* The following template types are available:
*
* - [Buttons](https://developers.line.biz/en/reference/messaging-api/#buttons)
* - [Confirm](https://developers.line.biz/en/reference/messaging-api/#confirm)
* - [Carousel](https://developers.line.biz/en/reference/messaging-api/#carousel)
* - [Image carousel](https://developers.line.biz/en/reference/messaging-api/#image-carousel)
*
* @see [Template messages](https://developers.line.biz/en/reference/messaging-api/#template-messages)
*/
export type TemplateMessage = MessageCommon & {
type: "template";
/**
* Alternative text (Max: 400 characters)
*/
altText: string;
/**
* Carousel template content
*/
template: TemplateContent;
};
/**
* Flex Messages are messages with a customizable layout.
* You can customize the layout freely by combining multiple elements.
* For more information, see
* [Using Flex Messages](https://developers.line.biz/en/docs/messaging-api/using-flex-messages/).
*
* @see [Flex messages](https://developers.line.biz/en/reference/messaging-api/#flex-message)
*/
export type FlexMessage = MessageCommon & {
type: "flex";
altText: string;
contents: FlexContainer;
};
/**
* Object which specifies the actions and tappable regions of an imagemap.
*
* When a region is tapped, the user is redirected to the URI specified in
* `uri` and the message specified in `message` is sent.
*
* @see [Imagemap action objects](https://developers.line.biz/en/reference/messaging-api/#imagemap-action-objects)
*/
export type ImageMapAction = ImageMapURIAction | ImageMapMessageAction;
export type ImageMapActionBase = {
/**
* Spoken when the accessibility feature is enabled on the client device. (Max: 50 characters)
* Supported on LINE 8.2.0 and later for iOS.
*/
label?: string;
/** Defined tappable area */
area: Area;
};
export type ImageMapURIAction = {
type: "uri";
/**
* Webpage URL (Max: 1000 characters)
*/
linkUri: string;
} & ImageMapActionBase;
export type ImageMapMessageAction = {
type: "message";
/**
* Message to send (Max: 400 characters)
*/
text: string;
} & ImageMapActionBase;
export type Area = {
/**
* Horizontal position relative to the top-left corner of the area
*/
x: number;
/**
* Vertical position relative to the top-left corner of the area
*/
y: number;
/**
* Width of the tappable area
*/
width: number;
/**
* Height of the tappable area
*/
height: number;
};
/**
* A container is the top-level structure of a Flex Message. Here are the types of containers available.
*
* - [Bubble](https://developers.line.biz/en/reference/messaging-api/#bubble)
* - [Carousel](https://developers.line.biz/en/reference/messaging-api/#f-carousel)
*
* See [Flex Message elements](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/)
* for the containers' JSON data samples and usage.
*/
export type FlexContainer = FlexBubble | FlexCarousel;
/**
* This is a container that contains one message bubble. It can contain four
* blocks: header, hero, body, and footer.
*
* For more information about using each block, see
* [Block](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/#block).
*/
export type FlexBubble = {
type: "bubble";
size?: "nano" | "micro" | "kilo" | "mega" | "giga";
/**
* Text directionality and the order of components in horizontal boxes in the
* container. Specify one of the following values:
*
* - `ltr`: Left to right
* - `rtl`: Right to left
*
* The default value is `ltr`.
*/
direction?: "ltr" | "rtl";
header?: FlexBox;
hero?: FlexBox | FlexImage;
body?: FlexBox;
footer?: FlexBox;
styles?: FlexBubbleStyle;
action?: Action;
};
export type FlexBubbleStyle = {
header?: FlexBlockStyle;
hero?: FlexBlockStyle;
body?: FlexBlockStyle;
footer?: FlexBlockStyle;
};
export type FlexBlockStyle = {
/**
* Background color of the block. Use a hexadecimal color code.
*/
backgroundColor?: string;
/**
* - `true` to place a separator above the block.
* - `true` will be ignored for the first block in a container because you
* cannot place a separator above the first block.
* - The default value is `false`.
*/
separator?: boolean;
/**
* Color of the separator. Use a hexadecimal color code.
*/
separatorColor?: string;
};
export type FlexCarousel = {
type: "carousel";
/**
* (Max: 12 bubbles)
*/
contents: FlexBubble[];
};
/**
* Components are objects that compose a Flex Message container. Here are the
* types of components available:
*
* - [Box](https://developers.line.biz/en/reference/messaging-api/#box)
* - [Button](https://developers.line.biz/en/reference/messaging-api/#button)
* - [Image](https://developers.line.biz/en/reference/messaging-api/#f-image)
* - [Icon](https://developers.line.biz/en/reference/messaging-api/#icon)
* - [Text](https://developers.line.biz/en/reference/messaging-api/#f-text)
* - [Span](https://developers.line.biz/en/reference/messaging-api/#span)
* - [Separator](https://developers.line.biz/en/reference/messaging-api/#separator)
* - [Filler](https://developers.line.biz/en/reference/messaging-api/#filler)
* - [Spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
*
* See the followings for the components' JSON data samples and usage.
*
* - [Flex Message elements](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/)
* - [Flex Message layout](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/)
*/
export type FlexComponent =
| FlexBox
| FlexButton
| FlexImage
| FlexIcon
| FlexText
| FlexSpan
| FlexSeparator
| FlexFiller
| FlexSpacer;
/**
* This is a component that defines the layout of child components.
* You can also include a box in a box.
*/
export type FlexBox = {
type: "box";
/**
* The placement style of components in this box. Specify one of the following values:
*
* - `horizontal`: Components are placed horizontally. The `direction`
* property of the [bubble](https://developers.line.biz/en/reference/messaging-api/#bubble)
* container specifies the order.
* - `vertical`: Components are placed vertically from top to bottom.
* - `baseline`: Components are placed in the same way as `horizontal` is
* specified except the baselines of the components are aligned.
*
* For more information, see
* [Types of box layouts](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#box-layout-types).
*/
layout: "horizontal" | "vertical" | "baseline";
/**
* Components in this box. Here are the types of components available:
*
* - When the `layout` property is `horizontal` or `vertical`:
* + [Box](https://developers.line.biz/en/reference/messaging-api/#box)
* + [button](https://developers.line.biz/en/reference/messaging-api/#button)
* + [image](https://developers.line.biz/en/reference/messaging-api/#f-image)
* + [text](https://developers.line.biz/en/reference/messaging-api/#f-text)
* + [separator](https://developers.line.biz/en/reference/messaging-api/#separator)
* + [filler](https://developers.line.biz/en/reference/messaging-api/#filler)
* + [spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
* - When the `layout` property is `baseline`:
* + [icon](https://developers.line.biz/en/reference/messaging-api/#icon)
* + [text](https://developers.line.biz/en/reference/messaging-api/#f-text)
* + [filler](https://developers.line.biz/en/reference/messaging-api/#filler)
* + [spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
*/
contents: FlexComponent[];
/**
* Background color of the block. In addition to the RGB color, an alpha
* channel (transparency) can also be set. Use a hexadecimal color code.
* (Example:#RRGGBBAA) The default value is `#00000000`.
*/
backgroundColor?: string;
/**
* Color of box border. Use a hexadecimal color code.
*/
borderColor?: string;
/**
* Width of box border. You can specify a value in pixels or any one of none,
* light, normal, medium, semi-bold, or bold. none does not render a border
* while the others become wider in the order of listing.
*/
borderWidth?:
| string
| "none"
| "light"
| "normal"
| "medium"
| "semi-bold"
| "bold";
/**
* Radius at the time of rounding the corners of the border. You can specify a
* value in pixels or any one of `none`, `xs`, `sm`, `md`, `lg`, `xl`, or `xxl`. none does not
* round the corner while the others increase in radius in the order of listing. The default value is none.
*/
cornerRadius?: string | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Width of the box. For more information, see [Width of a box](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#box-width) in the API documentation.
*/
width?: string;
/**
* Height of the box. For more information, see [Height of a box](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#box-height) in the API documentation.
*/
height?: string;
/**
* The ratio of the width or height of this box within the parent box. The
* default value for the horizontal parent box is `1`, and the default value
* for the vertical parent box is `0`.
*
* For more information, see
* [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
*/
flex?: number;
/**
* Minimum space between components in this box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is `none`.
* - To override this setting for a specific component, set the `margin`
* property of that component.
*/
spacing?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Minimum space between this box and the previous component in the parent box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Free space between the borders of this box and the child element.
* For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
*/
paddingAll?: string;
/**
* Free space between the border at the upper end of this box and the upper end of the child element.
* For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
*/
paddingTop?: string;
/**
* Free space between the border at the lower end of this box and the lower end of the child element.
* For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
*/
paddingBottom?: string;
/**
* Free space between the border at the left end of this box and the left end of the child element.
* For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
*/
paddingStart?: string;
/**
* Free space between the border at the right end of this box and the right end of the child element.
* For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
*/
paddingEnd?: string;
/**
* Action performed when this button is tapped.
*
* Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
*/
action?: Action;
/**
* How child elements are aligned along the main axis of the parent element. If the
* parent element is a horizontal box, this only takes effect when its child elements have
* their `flex` property set equal to 0. For more information, see [Arranging a box's child elements and free space](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#justify-property)
* in the Messaging API documentation.
*/
justifyContent?:
| "flex-start"
| "center"
| "flex-end"
| "space-between"
| "space-around"
| "space-evenly";
/**
* How child elements are aligned along the cross axis of the parent element. For more
* information, see [Arranging a box's child elements and free space](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#justify-property) in the Messaging API documentation.
*/
alignItems?: "flex-start" | "center" | "flex-end";
background?: Background;
} & Offset;
export type Offset = {
/**
* Reference position for placing this box. Specify one of the following values:
* - `relative`: Use the previous box as reference.
* - `absolute`: Use the top left of parent element as reference.
*
* The default value is relative.
* For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
*/
position?: "relative" | "absolute";
/**
* The top offset.
* For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
*/
offsetTop?: string;
/**
* The bottom offset.
* For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
*/
offsetBottom?: string;
/**
* The left offset.
* For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
*/
offsetStart?: string;
/**
* The right offset.
* For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
*/
offsetEnd?: string;
};
export type Background = {
/**
* The type of background used. Specify these values:
* - `linearGradient`: Linear gradient. For more information, see [Linear gradient backgrounds](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#linear-gradient-bg) in the Messaging API documentation.
*/
type: "linearGradient";
/**
* The angle at which a linear gradient moves. Specify the angle using an integer value
* like `90deg` (90 degrees) or a decimal number like `23.5deg` (23.5 degrees) in the
* half-open interval [0, 360). The direction of the linear gradient rotates clockwise as the
* angle increases. Given a value of `0deg`, the gradient starts at the bottom and ends at
* the top; given a value of `45deg`, the gradient starts at the bottom-left corner and ends
* at the top-right corner; given a value of 90deg, the gradient starts at the left and ends
* at the right; and given a value of `180deg`, the gradient starts at the top and ends at
* the bottom. For more information, see [Direction (angle) of linear gradient backgrounds](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#linear-gradient-bg-angle) in the Messaging API documentation.
*/
angle: string;
/**
* The color at the gradient's starting point. Use a hexadecimal color code in the
* `#RRGGBB` or `#RRGGBBAA` format.
*/
startColor: string;
/**
* The color at the gradient's ending point. Use a hexadecimal color code in the
* `#RRGGBB` or `#RRGGBBAA` format.
*/
endColor: string;
/**
* The color in the middle of the gradient. Use a hexadecimal color code in the `#RRGGBB`
* or `#RRGGBBAA` format. Specify a value for the `background.centerColor` property to
* create a gradient that has three colors. For more information, see [Intermediate color stops for linear gradients](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#linear-gradient-bg-center-color) in the
* Messaging API documentation.
*/
centerColor?: string;
/**
* The position of the intermediate color stop. Specify an integer or decimal value
* between `0%` (the starting point) and `100%` (the ending point). This is `50%` by
* default. For more information, see [Intermediate color stops for linear gradients](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#linear-gradient-bg-center-color) in the
* Messaging API documentation.
*/
centerPosition?: string;
};
/**
* This component draws a button.
*
* When the user taps a button, a specified action is performed.
*/
export type FlexButton = {
type: "button";
/**
* Action performed when this button is tapped.
*
* Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
*/
action: Action;
/**
* The ratio of the width or height of this box within the parent box.
*
* The default value for the horizontal parent box is `1`, and the default
* value for the vertical parent box is `0`.
*
* For more information, see
* [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
*/
flex?: number;
/**
* Minimum space between this box and the previous component in the parent box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Height of the button. The default value is `md`.
*/
height?: "sm" | "md";
/**
* Style of the button. Specify one of the following values:
*
* - `link`: HTML link style
* - `primary`: Style for dark color buttons
* - `secondary`: Style for light color buttons
*
* The default value is `link`.
*/
style?: "link" | "primary" | "secondary";
/**
* Use a hexadecimal color code.
*
* - Character color when the `style` property is `link`.
* - Background color when the `style` property is `primary` or `secondary`.
*/
color?: string;
/**
* Vertical alignment style. Specify one of the following values:
*
* - `top`: Top-aligned
* - `bottom`: Bottom-aligned
* - `center`: Center-aligned
*
* The default value is `top`.
*
* If the `layout` property of the parent box is `baseline`, the `gravity`
* property will be ignored.
*/
gravity?: "top" | "bottom" | "center";
/**
* The method by which to adjust the text font size. Specify this value:
*
* - `shrink-to-fit`: Automatically shrink the font
* size to fit the width of the component. This
* property takes a "best-effort" approach that may
* work differently—or not at all!—on some platforms.
* For more information, see [Automatically shrink fonts to fit](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#adjusts-fontsize-to-fit)
* in the Messaging API documentation.
* - LINE 10.13.0 or later for iOS and Android
*/
adjustMode?: "shrink-to-fit";
} & Offset;
/**
* This is an invisible component to fill extra space between components.
*
* - The filler's `flex` property is fixed to 1.
* - The `spacing` property of the parent box will be ignored for fillers.
*/
export type FlexFiller = {
type: "filler";
/**
* The ratio of the width or height of this component within the parent box. For more information, see [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
*/
flex?: number;
};
/**
* This component draws an icon.
*/
export type FlexIcon = {
type: "icon";
/**
* Image URL
*
* Protocol: HTTPS
* Image format: JPEG or PNG
* Maximum image size: 240×240 pixels
* Maximum data size: 1 MB
*/
url: string;
/**
* Minimum space between this box and the previous component in the parent
* box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Maximum size of the icon width.
* The size increases in the order of listing.
* The default value is `md`.
* For more information, see [Icon, text, and span size](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#other-component-size) in the Messaging API documentation.
*/
size?:
| string
| "xxs"
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "xxl"
| "3xl"
| "4xl"
| "5xl";
/**
* Aspect ratio of the icon. `{width}:{height}` format.
* The values of `{width}` and `{height}` must be in the range 1–100000.
* `{height}` can't be more than three times the value of `{width}`.
* The default value is `1:1`.
*/
aspectRatio?: string;
} & Offset;
/**
* This component draws an image.
*/
export type FlexImage = {
type: "image";
/**
* Image URL
*
* - Protocol: HTTPS
* - Image format: JPEG or PNG
* - Maximum image size: 1024×1024 pixels
* - Maximum data size: 1 MB
*/
url: string;
/**
* The ratio of the width or height of this box within the parent box.
*
* The default value for the horizontal parent box is `1`, and the default
* value for the vertical parent box is `0`.
*
* - For more information, see
* [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
*/
flex?: number;
/**
* Minimum space between this box and the previous component in the parent
* box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Horizontal alignment style. Specify one of the following values:
*
* - `start`: Left-aligned
* - `end`: Right-aligned
* - `center`: Center-aligned
*
* The default value is `center`.
*/
align?: "start" | "end" | "center";
/**
* Vertical alignment style. Specify one of the following values:
*
* - `top`: Top-aligned
* - `bottom`: Bottom-aligned
* - `center`: Center-aligned
*
* The default value is `top`.
*
* If the `layout` property of the parent box is `baseline`, the `gravity` property will be ignored.
*/
gravity?: "top" | "bottom" | "center";
/**
* Maximum size of the image width.
* The size increases in the order of listing.
* The default value is `md`.
* For more information, see [Image size](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#image-size) in the Messaging API documentation.
*/
size?:
| string
| "xxs"
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "xxl"
| "3xl"
| "4xl"
| "5xl"
| "full";
/**
* Aspect ratio of the image. `{width}:{height}` format.
* Specify the value of `{width}` and `{height}` in the range from 1 to 100000. However,
* you cannot set `{height}` to a value that is more than three times the value of `{width}`.
* The default value is `1:1`.
*/
aspectRatio?: string;
/**
* Style of the image. Specify one of the following values:
*
* - `cover`: The image fills the entire drawing area. Parts of the image
* that do not fit in the drawing area are not displayed.
* - `fit`: The entire image is displayed in the drawing area. The background
* is displayed in the unused areas to the left and right of vertical images
* and in the areas above and below horizontal images.
*
* The default value is `fit`.
*/
aspectMode?: "cover" | "fit";
/**
* Background color of the image. Use a hexadecimal color code.
*/
backgroundColor?: string;
/**
* Action performed when this button is tapped.
* Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
*/
action?: Action;
/**
* When this is `true`, an animated image (APNG) plays.
* You can specify a value of `true` up to three times in a single message.
* You can't send messages that exceed this limit.
* This is `false` by default.
* Animated images larger than 300 KB aren't played back.
*/
animated?: Boolean;
} & Offset;
/**
* This component draws a separator between components in the parent box.
*/
export type FlexSeparator = {
type: "separator";
/**
* Minimum space between this box and the previous component in the parent
* box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Color of the separator. Use a hexadecimal color code.
*/
color?: string;
};
/**
* This is an invisible component that places a fixed-size space at the
* beginning or end of the box.
* @deprecated
*/
export type FlexSpacer = {
type: "spacer";
/**
* Size of the space.
* The size increases in the order of listing.
* The default value is `md`.
*/
size?: "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
};
export type FlexText = {
type: "text";
text: string;
/**
* Array of spans. Be sure to set either one of the `text` property or `contents` property. If you set the `contents` property, `text` is ignored.
*/
contents?: FlexSpan[];
/**
* The method by which to adjust the text font size. Specify this value:
*
* - `shrink-to-fit`: Automatically shrink the font
* size to fit the width of the component. This
* property takes a "best-effort" approach that may
* work differently—or not at all!—on some platforms.
* For more information, see [Automatically shrink fonts to fit](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#adjusts-fontsize-to-fit)
* in the Messaging API documentation.
* - LINE 10.13.0 or later for iOS and Android
*/
adjustMode?: "shrink-to-fit";
/**
* The ratio of the width or height of this box within the parent box.
*
* The default value for the horizontal parent box is `1`, and the default
* value for the vertical parent box is `0`.
*
* For more information, see
* [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
*/
flex?: number;
/**
* Minimum space between this box and the previous component in the parent
* box.
*
* - `none` does not set a space while the other values set a space whose
* size increases in the order of listing.
* - The default value is the value of the `spacing` property of the parent
* box.
* - If this box is the first component in the parent box, the `margin`
* property will be ignored.
*/
margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
/**
* Font size.
* The size increases in the order of listing.
* The default value is `md`.
* For more information, see [Icon, text, and span size](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#other-component-size) in the Messaging API documentation.
*/
size?:
| string
| "xxs"
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "xxl"
| "3xl"
| "4xl"
| "5xl";
/**
* Horizontal alignment style. Specify one of the following values:
*
* - `start`: Left-aligned
* - `end`: Right-aligned
* - `center`: Center-aligned
*
* The default value is `start`.
*/
align?: "start" | "end" | "center";
/**
* Vertical alignment style. Specify one of the following values:
*
* - `top`: Top-aligned
* - `bottom`: Bottom-aligned
* - `center`: Center-aligned
*
* The default value is `top`.
*
* If the `layout` property of the parent box is `baseline`, the `gravity`
* property will be ignored.
*/
gravity?: "top" | "bottom" | "center";
/**
* `true` to wrap text.
*
* The default value is `false`.
*
* If set to `true`, you can use a new line character (\n) to begin on a new
* line.
*/
wrap?: boolean;
/**
* Max number of lines. If the text does not fit in the specified number of
* lines, an ellipsis (…) is displayed at the end of the last line. If set to
* 0, all the text is displayed. The default value is 0.
*/
maxLines?: number;
/**
* Font weight.
* Specifying `bold`makes the font bold.
* The default value is `regular`.
*/
weight?: "regular" | "bold";
/**
* Font color. Use a hexadecimal color code.
*/
color?: string;
/**
* Action performed when this text is tapped.
* Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
*/
action?: Action;
/**
* Style of the text. Specify one of the following values:
* - `normal`: Normal
* - `italic`: Italic
*
* The default value is `normal`.
*/
style?: string;
/**
* Decoration of the text. Specify one of the following values:
* `none`: No decoration
* `underline`: Underline
* `line-through`: Strikethrough
*
* The default value is `none`.
*/
decoration?: string;
} & Offset;
/**
* This component renders multiple text strings with different designs in one row. You can specify the color, size, weight, and decoration for the font. Span is set to `contents` property in [Text](https://developers.line.biz/en/reference/messaging-api/#f-text).
*/
export type FlexSpan = {
type: "span";
/**
* Text. If the `wrap` property of the parent text is set to `true`, you can use a new line character (`\n`) to begin on a new line.
*/
text: string;
/**
* Font color. Use a hexadecimal color code.
*/
color?: string;
/**
* Font size. You can specify one of the following values: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl`, `3xl`, `4xl`, or `5xl`. The size increases in the order of listing. The default value is `md`.
* For more information, see [Icon, text, and span size](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#other-component-size) in the Messaging API documentation.
*/
size?:
| string
| "xxs"
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "xxl"
| "3xl"
| "4xl"
| "5xl";
/**
* Font weight. You can specify one of the following values: `regular` or `bold`. Specifying `bold` makes the font bold. The default value is `regular`.
*/
weight?: string;
/**
* Style of the text. Specify one of the following values:
* - `normal`: Normal
* - `italic`: Italic
*
* The default value is `normal`.
*/
style?: string;
/**
* Decoration of the text. Specify one of the following values:
* `none`: No decoration
* `underline`: Underline
* `line-through`: Strikethrough
*
* The default value is `none`.
*
* Note: The decoration set in the `decoration` property of the [text](https://developers.line.biz/en/reference/messaging-api/#f-text) cannot be overwritten by the `decoration` property of the span.
*/
decoration?: string;
};
export type TemplateContent =
| TemplateButtons
| TemplateConfirm
| TemplateCarousel
| TemplateImageCarousel;
/**
* Template with an image, title, text, and multiple action buttons.
*
* Because of the height limitation for buttons template messages, the lower
* part of the text display area will get cut off if the height limitation is
* exceeded. For this reason, depending on the character width, the message
* text may not be fully displayed even when it is within the character limits.
*/
export type TemplateButtons = {
type: "buttons";
/**
* Image URL (Max: 1000 characters)
*
* - HTTPS
* - JPEG or PNG
* - Max width: 1024px
* - Max: 1 MB
*/
thumbnailImageUrl?: string;
/**
* Aspect ratio of the image. Specify one of the following values:
*
* - `rectangle`: 1.51:1
* - `square`: 1:1
*
* The default value is `rectangle`
*/
imageAspectRatio?: "rectangle" | "square";
/**
* Size of the image. Specify one of the following values:
*
* - `cover`: The image fills the entire image area. Parts of the image that
* do not fit in the area are not displayed.
* - `contain`: The entire image is displayed in the image area. A background
* is displayed in the unused areas to the left and right of vertical images
* and in the areas above and below horizontal images.
*
* The default value is `cover`.
*/
imageSize?: "cover" | "contain";
/**
* Background color of image. Specify a RGB color value.
* The default value is `#FFFFFF` (white).
*/
imageBackgroundColor?: string;
/**
* Title (Max: 40 characters)
*/
title?: string;
/**
* Message text
*
* - Max: 160 characters (no image or title)
* - Max: 60 characters (message with an image or title)
*/
text: string;
/**
* Action when tapped (Max: 4)
*/
actions: Action[];
};
/**
* Template with two action buttons.
*
* Because of the height limitation for confirm template messages, the lower
* part of the `text` display area will get cut off if the height limitation is
* exceeded. For this reason, depending on the character width, the message
* text may not be fully displayed even when it is within the character limits.
*/
export type TemplateConfirm = {
type: "confirm";
/**
* Message text (Max: 240 characters)
*/
text: string;
/**
* Action when tapped. Set 2 actions for the 2 buttons
*/
actions: Action[];
};
/**
* Template with multiple columns which can be cycled like a carousel.
* The columns will be shown in order by scrolling horizontally.
*
* Because of the height limitation for carousel template messages, the lower
* part of the `text` display area will get cut off if the height limitation is
* exceeded. For this reason, depending on the character width, the message
* text may not be fully displayed even when it is within the character limits.
*
* Keep the number of actions consistent for all columns. If you use an image
* or title for a column, make sure to do the same for all other columns.
*/
export type TemplateCarousel = {
type: "carousel";
/**
* Array of columns (Max: 10)
*/
columns: TemplateColumn[];
/**
* Aspect ratio of the image. Specify one of the following values:
*
* - `rectangle`: 1.51:1
* - `square`: 1:1
*
* Applies to all columns. The default value is `rectangle`.
*/
imageAspectRatio?: "rectangle" | "square";
/**
* Size of the image. Specify one of the following values:
*
* - `cover`: The image fills the entire image area. Parts of the image that
* do not fit in the area are not displayed.
* - `contain`: The entire image is displayed in the image area. A background
* is displayed in the unused areas to the left and right of vertical images
* and in the areas above and below horizontal images.
*
* Applies to all columns. The default value is `cover`.
*/
imageSize?: "cover" | "contain";
};
export type TemplateColumn = {
/**
* Image URL (Max: 1000 characters)
*
* - HTTPS
* - JPEG or PNG
* - Aspect ratio: 1:1.51
* - Max width: 1024px
* - Max: 1 MB
*/
thumbnailImageUrl?: string;
/**
* Background color of image. Specify a RGB color value.
* The default value is `#FFFFFF` (white).
*/
imageBackgroundColor?: string;
/**
* Title (Max: 40 characters)
*/
title?: string;
/**
* Message text
*
* - Max: 120 characters (no image or title)
* - Max: 60 characters (message with an image or title)
*/
text: string;
/**
* Action when image is tapped; set for the entire image, title, and text area
*/
defaultAction?: Action;
/**
* Action when tapped (Max: 3)
*/
actions: Action[];
};
/**
* Template with multiple images which can be cycled like a carousel.
* The images will be shown in order by scrolling horizontally.
*/
export type TemplateImageCarousel = {
type: "image_carousel";
/**
* Array of columns (Max: 10)
*/
columns: TemplateImageColumn[];
};
export type TemplateImageColumn = {
/**
* Image URL (Max: 1000 characters)
*
* - HTTPS
* - JPEG or PNG
* - Aspect ratio: 1:1
* - Max width: 1024px
* - Max: 1 MB
*/
imageUrl: string;
/**
* Action when image is tapped
*/
action: Action<{ label?: string }>;
};
/**
* These properties are used for the quick reply.
*
* For more information, see
* [Using quick replies](https://developers.line.biz/en/docs/messaging-api/using-quick-reply/).
*/
export type QuickReply = {
/**
* This is a container that contains
* [quick reply buttons](https://developers.line.biz/en/reference/messaging-api/#quick-reply-button-object).
*
* Array of objects (Max: 13)
*/
items: QuickReplyItem[];
};
/**
* This is a quick reply option that is displayed as a button.
*
* For more information, see
* [quick reply buttons](https://developers.line.biz/en/reference/messaging-api/#quick-reply-button-object).
*/
export type QuickReplyItem = {
type: "action";
/**
* URL of the icon that is displayed at the beginning of the button (Max: 1000 characters)
*
* - URL scheme: https
* - Image format: PNG
* - Aspect ratio: 1:1
* - Data size: Up to 1 MB
*
* There is no limit on the image size. If the `action` property has the
* following actions with empty `imageUrl`:
*
* - [camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
* - [camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
* - [location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
*
* the default icon is displayed.
*/
imageUrl?: string;
/**
* Action performed when this button is tapped.
*
* Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
*
* The following is a list of the available actions:
*
* - [Postback action](https://developers.line.biz/en/reference/messaging-api/#postback-action)
* - [Message action](https://developers.line.biz/en/reference/messaging-api/#message-action)
* - [Datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action)
* - [Camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
* - [Camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
* - [Location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
*/
action: Action;
};
export type Sender = {
/**
* Display name
*
* - Max character limit: 20
* - Certain words such as `LINE` may not be used.
*/
name?: string;
/**
* Icon image URL
*
* - Max character limit: 1000
* - URL scheme: https
*/
iconUrl?: string;
};
/**
* These are types of actions for your bot to take when a user taps a button or an image in a message.
*
* - [Postback action](https://developers.line.biz/en/reference/messaging-api/#postback-action)
* - [Message action](https://developers.line.biz/en/reference/messaging-api/#message-action)
* - [URI action](https://developers.line.biz/en/reference/messaging-api/#uri-action)
* - [Datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action)
* - [Camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
* - [Camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
* - [Location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
*/
export type Action<ExtraFields = { label: string }> = (
| PostbackAction
| MessageAction
| URIAction
| DatetimePickerAction
| { type: "camera" }
| { type: "cameraRoll" }
| { type: "location" }
) &
ExtraFields;
/**
* When a control associated with this action is tapped, a postback event is
* returned via webhook with the specified string in the data property.
*/
export type PostbackAction = {
type: "postback";
/**
* String returned via webhook in the `postback.data` property of the
* postback event (Max: 300 characters)
*/
data: string;
/**
* Text displayed in the chat as a message sent by the user when the action
* is performed. Returned from the server through a webhook.
*
* - This property cannot be used with quick reply buttons. (Max: 300 characters)
* - The `displayText` and `text` properties cannot both be used at the same time.
* @deprecated
*/
text?: string;
/**
* Text displayed in the chat as a message sent by the user when the action is performed.
*
* - Required for quick reply buttons.
* - Optional for the other message types.
*
* Max: 300 characters
*
* The `displayText` and `text` properties cannot both be used at the same time.
*/
displayText?: string;
};
/**
* When a control associated with this action is tapped, the string in the text
* property is sent as a message from the user.
*/
export type MessageAction = {
type: "message";
/**
* Text sent when the action is performed (Max: 300 characters)
*/
text: string;
};
/**
* When a control associated with this action is tapped, the URI specified in
* the `uri` property is opened.
*/
export type URIAction = {
type: "uri";
/**
* URI opened when the action is performed (Max: 1000 characters).
* Must start with `http`, `https`, or `tel`.
*/
uri: string;
altUri?: AltURI;
};
/**
* URI opened on LINE for macOS and Windows when the action is performed (Max: 1000 characters)
* If the altUri.desktop property is set, the uri property is ignored on LINE for macOS and Windows.
* The available schemes are http, https, line, and tel.
* For more information about the LINE URL scheme, see Using the LINE URL scheme.
* This property is supported on the following version of LINE.
*
* LINE 5.12.0 or later for macOS and Windows
* Note: The altUri.desktop property is supported only when you set URI actions in Flex Messages.
*/
export type AltURI = {
desktop: string;
};
/**
* When a control associated with this action is tapped, a
* [postback event](https://developers.line.biz/en/reference/messaging-api/#postback-event)
* is returned via webhook with the date and time selected by the user from the
* date and time selection dialog.
*
* The datetime picker action does not support time zones.
*
* #### Date and time format
*
* The date and time formats for the `initial`, `max`, and `min` values are
* shown below. The `full-date`, `time-hour`, and `time-minute` formats follow
* the [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) protocol.
*
* | Mode | Format | Example |
* | -------- | ------------------------------------------------------------ | -------------------------------- |
* | date | `full-date` (Max: 2100-12-31; Min: 1900-01-01) | 2017-06-18 |
* | time | `time-hour`:`time-minute` (Max: 23:59; Min: 00:00) | 00:0006:1523:59 |
* | datetime | `full-date`T`time-hour`:`time-minute` or `full-date`t`time-hour`:`time-minute` (Max: 2100-12-31T23:59; Min: 1900-01-01T00:00) | 2017-06-18T06:152017-06-18t06:15 |
*/
export type DatetimePickerAction = {
type: "datetimepicker";
/**
* String returned via webhook in the `postback.data` property of the
* postback event (Max: 300 characters)
*/
data: string;
mode: "date" | "time" | "datetime";
/**
* Initial value of date or time
*/
initial?: string;
/**
* Largest date or time value that can be selected. Must be greater than the
* `min` value.
*/
max?: string;
/**
* Smallest date or time value that can be selected. Must be less than the
* `max` value.
*/
min?: string;
};
export type Size = {
width: number;
height: number;
};
/**
* Rich menus consist of either of these objects.
*
* - [Rich menu object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-object)
* without the rich menu ID. Use this object when you
* [create a rich menu](https://developers.line.biz/en/reference/messaging-api/#create-rich-menu).
* - [Rich menu response object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-response-object)
* with the rich menu ID. This object is returned when you
* [get a rich menu](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu)
* or [get a list of rich menus](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-list).
*
* [Area objects](https://developers.line.biz/en/reference/messaging-api/#area-object) and
* [action objects](https://developers.line.biz/en/reference/messaging-api/#action-objects)
* are included in these objects.
*/
export type RichMenu = {
/**
* [`size` object](https://developers.line.biz/en/reference/messaging-api/#size-object)
* which contains the width and height of the rich menu displayed in the chat.
* Rich menu images must be one of the following sizes: 2500x1686px or 2500x843px.
*/
size: Size;
/**
* `true` to display the rich menu by default. Otherwise, `false`.
*/
selected: boolean;
/**
* Name of the rich menu.
*
* This value can be used to help manage your rich menus and is not displayed
* to users.
*
* (Max: 300 characters)
*/
name: string;
/**
* Text displayed in the chat bar (Max: 14 characters)
*/
chatBarText: string;
/**
* Array of [area objects](https://developers.line.biz/en/reference/messaging-api/#area-object)
* which define the coordinates and size of tappable areas
* (Max: 20 area objects)
*/
areas: Array<{ bounds: Area; action: Action<{ label?: string }> }>;
};
export type RichMenuResponse = { richMenuId: string } & RichMenu;
export type NumberOfMessagesSentResponse = InsightStatisticsResponse & {
/**
* The number of messages sent with the Messaging API on the date specified in date.
* The response has this property only when the value of status is `ready`.
*/
success?: number;
};
export type TargetLimitForAdditionalMessages = {
/**
* One of the following values to indicate whether a target limit is set or not.
* - `none`: This indicates that a target limit is not set.
* - `limited`: This indicates that a target limit is set.
*/
type: "none" | "limited";
/**
* The target limit for additional messages in the current month.
* This property is returned when the `type` property has a value of `limited`.
*/
value?: number;
};
export type NumberOfMessagesSentThisMonth = {
/**
* The number of sent messages in the current month
*/
totalUsage: number;
};
export const LINE_REQUEST_ID_HTTP_HEADER_NAME = "x-line-request-id";
export type MessageAPIResponseBase = {
[LINE_REQUEST_ID_HTTP_HEADER_NAME]?: string;
};
export const LINE_SIGNATURE_HTTP_HEADER_NAME = "x-line-signature";
export type InsightStatisticsResponse = {
/**
* Calculation status. One of:
* - `ready`: Calculation has finished; the numbers are up-to-date.
* - `unready`: We haven't finished calculating the number of sent messages for the specified `date`. Calculation usually takes about a day. Please try again later.
* - `out_of_service`: The specified `date` is earlier than the date on which we first started calculating sent messages. Different APIs have different date. Check them at the [document](https://developers.line.biz/en/reference/messaging-api/).
*/
status: "ready" | "unready" | "out_of_service";
};
export type NumberOfMessageDeliveries = InsightStatisticsResponse & {
/**
* Number of push messages sent to **all** of this LINE official account's friends (broadcast messages).
*/
broadcast: number;
/**
* Number of push messages sent to **some** of this LINE official account's friends, based on specific attributes (targeted/segmented messages).
*/
targeting: number;
/**
* Number of auto-response messages sent.
*/
autoResponse: number;
/**
* Number of greeting messages sent.
*/
welcomeResponse: number;
/**
* Number of messages sent from LINE Official Account Manager [Chat screen](https://www.linebiz.com/jp-en/manual/OfficialAccountManager/chats/screens/).
*/
chat: number;
/**
* Number of broadcast messages sent with the [Send broadcast message](https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message) Messaging API operation.
*/
apiBroadcast: number;
/**
* Number of push messages sent with the [Send push message](https://developers.line.biz/en/reference/messaging-api/#send-push-message) Messaging API operation.
*/
apiPush: number;
/**
* Number of multicast messages sent with the [Send multicast message](https://developers.line.biz/en/reference/messaging-api/#send-multicast-message) Messaging API operation.
*/
apiMulticast: number;
/**
* Number of replies sent with the [Send reply message](https://developers.line.biz/en/reference/messaging-api/#send-reply-message) Messaging API operation.
*/
apiReply: number;
};
export type NumberOfFollowers = InsightStatisticsResponse & {
/**
* The number of times, as of the specified `date`, that a user added this LINE official account as a friend. The number doesn't decrease when a user blocks the account after adding it, or when they delete their own account.
*/
followers: Number;
/**
* The number of users, as of the specified `date`, that the official account can reach with messages targeted by gender, age, or area. This number includes users for whom we estimated demographic attributes based on their activity in LINE and LINE-connected services.
*/
targetedReaches: Number;
/**
* The number of users blocking the account as of the specified `date`. The number decreases when a user unblocks the account.
*/
blocks: Number;
};
export type NumberOfMessageDeliveriesResponse =
| InsightStatisticsResponse
| NumberOfMessageDeliveries;
export type NumberOfFollowersResponse =
| InsightStatisticsResponse
| NumberOfFollowers;
type PercentageAble = {
percentage: number;
};
export type FriendDemographics = {
/**
* `true` if friend demographic information is available.
*/
available: boolean;
/**
* Percentage per gender
*/
genders?: Array<
{
/**
* Gender
*/
gender: "unknown" | "male" | "female";
} & PercentageAble
>;
/**
* Percentage per age group
*/
ages?: Array<
{
/**
* Age group
*/
age: string;
} & PercentageAble
>;
/**
* Percentage per area
*/
areas?: Array<
{
area: string;
} & PercentageAble
>;
/**
* Percentage by OS
*/
appTypes?: Array<
{
appType: "ios" | "android" | "others";
} & PercentageAble
>;
/**
* Percentage per friendship duration
*/
subscriptionPeriods?: Array<
{
/**
* Friendship duration
*/
subscriptionPeriod:
| "over365days"
| "within365days"
| "within180days"
| "within90days"
| "within30days"
| "within7days"
// in case for some rare cases(almost no)
| "unknown";
} & PercentageAble
>;
};
type UserInteractionStatisticsOfEachMessage = {
seq: number;
impression: number;
mediaPlayed: number;
mediaPlayed25Percent: number;
mediaPlayed50Percent: number;
mediaPlayed75Percent: number;
mediaPlayed100Percent: number;
uniqueMediaPlayed: number;
uniqueMediaPlayed25Percent: number;
uniqueMediaPlayed50Percent: number;
uniqueMediaPlayed75Percent: number;
uniqueMediaPlayed100Percent: number;
};
type UserInteractionStatisticsOfEachURL = {
seq: number;
url: number;
click: number;
uniqueClick: number;
uniqueClickOfRequest: number;
};
/**
* https://developers.line.biz/en/reference/messaging-api/#get-message-event
*/
export type UserInteractionStatistics = {
overview: {
requestId: string;
timestamp: number;
delivered: number;
uniqueImpression: number;
uniqueClick: number;
uniqueMediaPlayed: number;
uniqueMediaPlayed100Percent: number;
};
messages: UserInteractionStatisticsOfEachMessage[];
clicks: UserInteractionStatisticsOfEachURL[];
};
type FilterOperatorObject<T> = {
type: "operator";
} & (
| {
and: (T | FilterOperatorObject<T>)[];
}
| {
or: (T | FilterOperatorObject<T>)[];
}
| {
not: T | (T | FilterOperatorObject<T>)[];
}
);
type AudienceObject = {
type: "audience";
audienceGroupId: number;
};
export type ReceieptObject =
| AudienceObject
| FilterOperatorObject<AudienceObject>;
type DemographicAge =
| "age_15"
| "age_20"
| "age_25"
| "age_30"
| "age_35"
| "age_40"
| "age_45"
| "age_50";
type DemographicSubscriptionPeriod =
| "day_7"
| "day_30"
| "day_90"
| "day_180"
| "day_365";
type DemographicArea =
| "jp_01"
| "jp_02"
| "jp_03"
| "jp_04"
| "jp_05"
| "jp_06"
| "jp_07"
| "jp_08"
| "jp_09"
| "jp_10"
| "jp_11"
| "jp_12"
| "jp_13"
| "jp_14"
| "jp_15"
| "jp_16"
| "jp_17"
| "jp_18"
| "jp_19"
| "jp_20"
| "jp_21"
| "jp_22"
| "jp_23"
| "jp_24"
| "jp_25"
| "jp_26"
| "jp_27"
| "jp_28"
| "jp_29"
| "jp_30"
| "jp_31"
| "jp_32"
| "jp_33"
| "jp_34"
| "jp_35"
| "jp_36"
| "jp_37"
| "jp_38"
| "jp_39"
| "jp_40"
| "jp_41"
| "jp_42"
| "jp_43"
| "jp_44"
| "jp_45"
| "jp_46"
| "jp_47"
| "tw_01"
| "tw_02"
| "tw_03"
| "tw_04"
| "tw_05"
| "tw_06"
| "tw_07"
| "tw_08"
| "tw_09"
| "tw_10"
| "tw_11"
| "tw_12"
| "tw_13"
| "tw_14"
| "tw_15"
| "tw_16"
| "tw_17"
| "tw_18"
| "tw_19"
| "tw_20"
| "tw_21"
| "tw_22"
| "th_01"
| "th_02"
| "th_03"
| "th_04"
| "th_05"
| "th_06"
| "th_07"
| "th_08"
| "id_01"
| "id_02"
| "id_03"
| "id_04"
| "id_06"
| "id_07"
| "id_08"
| "id_09"
| "id_10"
| "id_11"
| "id_12"
| "id_05";
type DemographicObject =
| {
type: "gender";
oneOf: ("male" | "female")[];
}
| {
type: "age";
gte?: DemographicAge;
lt?: DemographicAge;
}
| {
type: "appType";
oneOf: ("ios" | "android")[];
}
| {
type: "area";
oneOf: DemographicArea[];
}
| {
type: "subscriptionPeriod";
gte?: DemographicSubscriptionPeriod;
lt?: DemographicSubscriptionPeriod;
};
export type DemographicFilterObject =
| DemographicObject
| FilterOperatorObject<DemographicObject>;
export type NarrowcastProgressResponse = (
| {
phase: "waiting";
}
| ((
| {
phase: "sending" | "succeeded";
}
| {
phase: "failed";
failedDescription: string;
}
) & {
successCount: number;
failureCount: number;
targetCount: string;
acceptedTime: string;
completedTime: string;
})
) & {
errorCode?: 1 | 2;
};
type AudienceGroupJob = {
audienceGroupJobId: number;
audienceGroupId: number;
description: string;
type: "DIFF_ADD";
audienceCount: number;
created: number;
} & (
| {
jobStatus: "QUEUED" | "WORKING" | "FINISHED";
}
| {
jobStatus: "FAILED";
failedType: "INTERNAL_ERROR";
}
);
export type AudienceGroupStatus =
| "IN_PROGRESS"
| "READY"
| "EXPIRED"
| "FAILED";
export type AudienceGroupCreateRoute = "OA_MANAGER" | "MESSAGING_API";
type _AudienceGroup = {
audienceGroupId: number;
description: string;
audienceCount: number;
created: number;
isIfaAudience: boolean;
permission: "READ" | "READ_WRITE";
createRoute: AudienceGroupCreateRoute;
} & (
| {
status: Exclude<AudienceGroupStatus, "FAILED">;
}
| {
status: "FAILED";
failedType: "AUDIENCE_GROUP_AUDIENCE_INSUFFICIENT" | "INTERNAL_ERROR";
}
) &
(
| {
type: "UPLOAD";
}
| {
type: "CLICK";
clickUrl: string;
}
| {
type: "IMP";
requestId: string;
}
);
export type AudienceGroup = _AudienceGroup & {
jobs: AudienceGroupJob[];
};
export type AudienceGroups = _AudienceGroup[];
export type AudienceGroupAuthorityLevel = "PUBLIC" | "PRIVATE";
export type ChannelAccessToken = {
access_token: string;
expires_in: number;
token_type: "Bearer";
key_id?: string;
};
/**
* Response body of get group summary.
*
* @see [Get group summary](https://developers.line.biz/ja/reference/messaging-api/#get-group-summary)
*/
export type GroupSummaryResponse = {
groupId: string;
groupName: string;
pictureUrl: string;
};
/**
* Response body of get members in group count and get members in room count.
*
* @see [Get members in group count](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count)
* @see [Get members in room count](https://developers.line.biz/en/reference/messaging-api/#get-members-room-count)
*/
export type MembersCountResponse = {
count: number;
};
/**
* Response body of get bot info.
*
* @see [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info)
*/
export type BotInfoResponse = {
userId: string;
basicId: string;
premiumId?: string;
displayName: string;
pictureUrl?: string;
chatMode: "chat" | "bot";
markAsReadMode: "auto" | "manual";
};
/**
* Response body of get webhook endpoint info.
*
* @see [Get get webhook endpoint info](https://developers.line.biz/en/reference/messaging-api/#get-webhook-endpoint-information)
*/
export type WebhookEndpointInfoResponse = {
endpoint: string;
active: boolean;
};
/**
* Response body of test webhook endpoint.
*
* @see [Test webhook endpoint](https://developers.line.biz/en/reference/messaging-api/#test-webhook-endpoint)
*/
export type TestWebhookEndpointResponse = {
success: boolean;
timestamp: string;
statusCode: number;
reason: string;
detail: string;
};
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}`
);
import { readFileSync } from "fs";
import { join } from "path";
import { deepEqual, equal, ok } from "assert";
import { URL } from "url";
import Client, { OAuth } from "../lib/client";
import * as Types from "../lib/types";
import { getStreamData } from "./helpers/stream";
import * as nock from "nock";
import {
MESSAGING_API_PREFIX,
OAUTH_BASE_PREFIX,
OAUTH_BASE_PREFIX_V2_1,
DATA_API_PREFIX,
} from "../lib/endpoints";
import * as FormData from "form-data";
import { createMultipartFormData } from "../lib/utils";
const pkg = require("../package.json");
const channelAccessToken = "test_channel_access_token";
const client = new Client({
channelAccessToken,
});
const responseFn = function (
this: nock.ReplyFnContext,
uri: string,
_body: nock.Body,
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyFnResult) => void,
) {
const fullUrl =
// @ts-ignore
this.req.options.protocol +
"//" +
// @ts-ignore
this.req.options.hostname +
// @ts-ignore
this.req.options.path;
if (fullUrl.startsWith(MESSAGING_API_PREFIX + "/message/"))
cb(null, [
200,
{},
{
"X-Line-Request-Id": "X-Line-Request-Id",
},
]);
else cb(null, [200, {}]);
};
describe("client", () => {
before(() => nock.disableNetConnect());
afterEach(() => nock.cleanAll());
after(() => nock.enableNetConnect());
const testMsg: Types.TextMessage = { type: "text", text: "hello" };
const richMenu: Types.RichMenu = {
size: {
width: 2500,
height: 1686,
},
selected: false,
name: "Nice richmenu",
chatBarText: "Tap here",
areas: [
{
bounds: {
x: 0,
y: 0,
width: 2500,
height: 1686,
},
action: {
type: "postback",
data: "action=buy&itemid=123",
},
},
],
};
const interceptionOption = {
reqheaders: {
authorization: `Bearer ${channelAccessToken}`,
"User-Agent": `${pkg.name}/${pkg.version}`,
},
};
const mockGet = (
prefix: string,
path: string,
expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
) => {
let _it = nock(prefix, interceptionOption).get(path);
if (expectedQuery) {
_it = _it.query(expectedQuery);
}
return _it.reply(responseFn);
};
const mockPost = (
prefix: string,
path: string,
expectedBody?: nock.RequestBodyMatcher,
) => {
return nock(prefix, interceptionOption)
.post(path, expectedBody)
.reply(responseFn);
};
const multipartFormDataMatcher = (expectedBody: Record<string, any>) => (
body: any,
) => {
const decoded = Buffer.from(body, "hex");
const boundary = decoded.toString("utf-8").match(/^--(.+)/)[1];
const data = new FormData();
//@ts-ignore
data._boundary = boundary;
createMultipartFormData.call(data, expectedBody);
return data.getBuffer().compare(decoded) === 0;
};
const mockPut = (
prefix: string,
path: string,
expectedBody?: nock.RequestBodyMatcher,
) => {
return nock(prefix, interceptionOption)
.put(path, expectedBody)
.reply(responseFn);
};
const mockDelete = (
prefix: string,
path: string,
expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
) => {
let _it = nock(prefix, interceptionOption).delete(path);
if (expectedQuery) {
_it = _it.query(expectedQuery);
}
return _it.reply(responseFn);
};
it("reply", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, `/message/reply`, {
messages: [testMsg],
replyToken: "test_reply_token",
notificationDisabled: false,
});
const res = await client.replyMessage("test_reply_token", testMsg);
equal(scope.isDone(), true);
equal(res["x-line-request-id"], "X-Line-Request-Id");
});
it("push", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, `/message/push`, {
messages: [testMsg],
to: "test_user_id",
notificationDisabled: false,
});
const res = await client.pushMessage("test_user_id", testMsg);
equal(scope.isDone(), true);
equal(res["x-line-request-id"], "X-Line-Request-Id");
});
it("multicast", async () => {
const ids = ["test_user_id_1", "test_user_id_2", "test_user_id_3"];
const scope = mockPost(MESSAGING_API_PREFIX, `/message/multicast`, {
messages: [testMsg, testMsg],
to: ids,
notificationDisabled: false,
});
const res = await client.multicast(ids, [testMsg, testMsg]);
equal(scope.isDone(), true);
equal(res["x-line-request-id"], "X-Line-Request-Id");
});
it("narrowcast", async () => {
const recipient: Types.ReceieptObject = {
type: "operator",
and: [
{
type: "audience",
audienceGroupId: 5614991017776,
},
{
type: "operator",
not: {
type: "audience",
audienceGroupId: 4389303728991,
},
},
],
};
const filter = {
demographic: {
type: "operator",
or: [
{
type: "operator",
and: [
{
type: "gender",
oneOf: ["male", "female"],
},
{
type: "age",
gte: "age_20",
lt: "age_25",
},
{
type: "appType",
oneOf: ["android", "ios"],
},
{
type: "area",
oneOf: ["jp_23", "jp_05"],
},
{
type: "subscriptionPeriod",
gte: "day_7",
lt: "day_30",
},
],
},
{
type: "operator",
and: [
{
type: "age",
gte: "age_35",
lt: "age_40",
},
{
type: "operator",
not: {
type: "gender",
oneOf: ["male"],
},
},
],
},
],
} as Types.DemographicFilterObject,
};
const limit = {
max: 100,
};
const scope = mockPost(MESSAGING_API_PREFIX, `/message/narrowcast`, {
messages: [testMsg, testMsg],
recipient,
filter,
limit,
});
const res = await client.narrowcast(
[testMsg, testMsg],
recipient,
filter,
limit,
);
equal(scope.isDone(), true);
equal(res["x-line-request-id"], "X-Line-Request-Id");
});
it("broadcast", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, `/message/broadcast`, {
messages: [testMsg, testMsg],
notificationDisabled: false,
});
const res = await client.broadcast([testMsg, testMsg]);
equal(scope.isDone(), true);
equal(res["x-line-request-id"], "X-Line-Request-Id");
});
it("getProfile", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/profile/test_user_id");
const res = await client.getProfile("test_user_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getGroupMemberProfile", async () => {
const scope = mockGet(
MESSAGING_API_PREFIX,
"/group/test_group_id/member/test_user_id",
);
const res = await client.getGroupMemberProfile(
"test_group_id",
"test_user_id",
);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getRoomMemberProfile", async () => {
const scope = mockGet(
MESSAGING_API_PREFIX,
"/room/test_room_id/member/test_user_id",
);
const res = await client.getRoomMemberProfile(
"test_room_id",
"test_user_id",
);
equal(scope.isDone(), true);
deepEqual(res, {});
});
const mockGroupMemberAPI = () => {
const matchReg = /([A-Za-z0-9_]+)\/([A-Za-z0-9_]+)\/members\/ids/;
return nock(MESSAGING_API_PREFIX, interceptionOption)
.get(matchReg)
.times(3)
.reply(200, (uri, _requestBody) => {
const _url = new URL(MESSAGING_API_PREFIX + uri);
let [_matchPath, groupOrRoom, id] = _url.pathname.match(matchReg);
const ty: string = groupOrRoom;
const start: number = parseInt(_url.searchParams.get("start"), 10) || 0;
const result: { memberIds: string[]; next?: string } = {
memberIds: [start, start + 1, start + 2].map(i => `${ty}-${id}-${i}`),
};
if (start / 3 < 2) {
result.next = String(start + 3);
}
return result;
});
};
it("getGroupMemberIds", async () => {
const scope = mockGroupMemberAPI();
const ids = await client.getGroupMemberIds("test_group_id");
equal(scope.isDone(), true);
deepEqual(ids, [
"group-test_group_id-0",
"group-test_group_id-1",
"group-test_group_id-2",
"group-test_group_id-3",
"group-test_group_id-4",
"group-test_group_id-5",
"group-test_group_id-6",
"group-test_group_id-7",
"group-test_group_id-8",
]);
});
it("getRoomMemberIds", async () => {
const scope = mockGroupMemberAPI();
const ids = await client.getRoomMemberIds("test_room_id");
equal(scope.isDone(), true);
deepEqual(ids, [
"room-test_room_id-0",
"room-test_room_id-1",
"room-test_room_id-2",
"room-test_room_id-3",
"room-test_room_id-4",
"room-test_room_id-5",
"room-test_room_id-6",
"room-test_room_id-7",
"room-test_room_id-8",
]);
});
it("getGroupMembersCount", async () => {
const groupId = "groupId";
const scope = mockGet(
MESSAGING_API_PREFIX,
`/group/${groupId}/members/count`,
);
await client.getGroupMembersCount(groupId);
equal(scope.isDone(), true);
});
it("getRoomMembersCount", async () => {
const roomId = "roomId";
const scope = mockGet(
MESSAGING_API_PREFIX,
`/room/${roomId}/members/count`,
);
await client.getRoomMembersCount(roomId);
equal(scope.isDone(), true);
});
it("getGroupSummary", async () => {
const groupId = "groupId";
const scope = mockGet(MESSAGING_API_PREFIX, `/group/${groupId}/summary`);
await client.getGroupSummary(groupId);
equal(scope.isDone(), true);
});
it("getMessageContent", async () => {
const scope = mockGet(DATA_API_PREFIX, "/message/test_message_id/content");
const stream = await client.getMessageContent("test_message_id");
const data = await getStreamData(stream);
equal(scope.isDone(), true);
const res = JSON.parse(data);
deepEqual(res, {});
});
it("leaveGroup", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, "/group/test_group_id/leave");
const res = await client.leaveGroup("test_group_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("leaveRoom", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, "/room/test_room_id/leave");
const res = await client.leaveRoom("test_room_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getRichMenu", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/richmenu/test_rich_menu_id");
const res = await client.getRichMenu("test_rich_menu_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("createRichMenu", async () => {
const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu", richMenu);
await client.createRichMenu(richMenu);
equal(scope.isDone(), true);
});
it("deleteRichMenu", async () => {
// delete
const scope = mockDelete(
MESSAGING_API_PREFIX,
"/richmenu/test_rich_menu_id",
);
const res = await client.deleteRichMenu("test_rich_menu_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getRichMenuIdOfUser", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/user/test_user_id/richmenu");
await client.getRichMenuIdOfUser("test_user_id");
equal(scope.isDone(), true);
});
it("linkRichMenuToUser", async () => {
const scope = mockPost(
MESSAGING_API_PREFIX,
"/user/test_user_id/richmenu/test_rich_menu_id",
);
const res = await client.linkRichMenuToUser(
"test_user_id",
"test_rich_menu_id",
);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("unlinkRichMenuFromUser", async () => {
const scope = mockDelete(
MESSAGING_API_PREFIX,
"/user/test_user_id/richmenu",
);
const res = await client.unlinkRichMenuFromUser("test_user_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("linkRichMenuToMultipleUsers", async () => {
const richMenuId = "test_rich_menu_id",
userIds = ["test_user_id"];
const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu/bulk/link", {
richMenuId,
userIds,
});
const res = await client.linkRichMenuToMultipleUsers(richMenuId, userIds);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("unlinkRichMenusFromMultipleUsers", async () => {
const userIds = ["test_user_id"];
const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu/bulk/unlink", {
userIds,
});
const res = await client.unlinkRichMenusFromMultipleUsers(userIds);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("setRichMenuImage", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const buffer = readFileSync(filepath);
const scope = mockPost(
DATA_API_PREFIX,
"/richmenu/test_rich_menu_id/content",
buffer,
);
const res = await client.setRichMenuImage("test_rich_menu_id", buffer);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getRichMenuImage", async () => {
const scope = mockGet(
DATA_API_PREFIX,
"/richmenu/test_rich_menu_id/content",
);
const stream = await client.getRichMenuImage("test_rich_menu_id");
const data = await getStreamData(stream);
equal(scope.isDone(), true);
const res = JSON.parse(data);
deepEqual(res, {});
});
it("getRichMenuList", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/richmenu/list");
await client.getRichMenuList();
equal(scope.isDone(), true);
});
it("setDefaultRichMenu", async () => {
const scope = mockPost(
MESSAGING_API_PREFIX,
"/user/all/richmenu/test_rich_menu_id",
);
const res = await client.setDefaultRichMenu("test_rich_menu_id");
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getDefaultRichMenuId", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/user/all/richmenu");
await client.getDefaultRichMenuId();
equal(scope.isDone(), true);
});
it("deleteDefaultRichMenu", async () => {
const scope = mockDelete(MESSAGING_API_PREFIX, "/user/all/richmenu");
const res = await client.deleteDefaultRichMenu();
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getLinkToken", async () => {
const scope = mockPost(
MESSAGING_API_PREFIX,
"/user/test_user_id/linkToken",
);
await client.getLinkToken("test_user_id");
equal(scope.isDone(), true);
});
it("getNumberOfSentReplyMessages", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/reply", {
date,
});
await client.getNumberOfSentReplyMessages(date);
equal(scope.isDone(), true);
});
it("getNumberOfSentPushMessages", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/push", {
date,
});
await client.getNumberOfSentPushMessages(date);
equal(scope.isDone(), true);
});
it("getNumberOfSentMulticastMessages", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/multicast", {
date,
});
await client.getNumberOfSentMulticastMessages(date);
equal(scope.isDone(), true);
});
it("getNarrowcastProgress", async () => {
const requestId = "requestId";
const scope = mockGet(
MESSAGING_API_PREFIX,
"/message/progress/narrowcast",
{
requestId,
},
);
await client.getNarrowcastProgress(requestId);
equal(scope.isDone(), true);
});
it("getTargetLimitForAdditionalMessages", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/message/quota");
await client.getTargetLimitForAdditionalMessages();
equal(scope.isDone(), true);
});
it("getNumberOfMessagesSentThisMonth", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/message/quota/consumption");
await client.getNumberOfMessagesSentThisMonth();
equal(scope.isDone(), true);
});
it("getNumberOfSentBroadcastMessages", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/broadcast", {
date,
});
await client.getNumberOfSentBroadcastMessages(date);
equal(scope.isDone(), true);
});
it("getNumberOfMessageDeliveries", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/insight/message/delivery", {
date,
});
await client.getNumberOfMessageDeliveries(date);
equal(scope.isDone(), true);
});
it("getNumberOfFollowers", async () => {
const date = "20191231";
const scope = mockGet(MESSAGING_API_PREFIX, "/insight/followers", {
date,
});
await client.getNumberOfFollowers(date);
equal(scope.isDone(), true);
});
it("getFriendDemographics", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, "/insight/demographic");
await client.getFriendDemographics();
equal(scope.isDone(), true);
});
it("getUserInteractionStatistics", async () => {
const requestId = "requestId";
const scope = mockGet(MESSAGING_API_PREFIX, "/insight/message/event", {
requestId,
});
await client.getUserInteractionStatistics(requestId);
equal(scope.isDone(), true);
});
it("createUploadAudienceGroup", async () => {
const requestBody = {
description: "audienceGroupName",
isIfaAudience: false,
audiences: [
{
id: "id",
},
],
uploadDescription: "uploadDescription",
};
const scope = mockPost(
MESSAGING_API_PREFIX,
"/audienceGroup/upload",
requestBody,
);
await client.createUploadAudienceGroup(requestBody);
equal(scope.isDone(), true);
});
it("createUploadAudienceGroupByFile", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const buffer = readFileSync(filepath);
const requestBody = {
description: "audienceGroupName",
isIfaAudience: false,
uploadDescription: "uploadDescription",
file: buffer,
};
const scope = nock(DATA_API_PREFIX, {
reqheaders: {
...interceptionOption.reqheaders,
"content-type": value =>
value.startsWith(`multipart/form-data; boundary=`),
},
})
.post(
"/audienceGroup/upload/byFile",
multipartFormDataMatcher(requestBody),
)
.reply(responseFn);
await client.createUploadAudienceGroupByFile(requestBody);
equal(scope.isDone(), true);
});
it("updateUploadAudienceGroup", async () => {
const requestBody = {
audienceGroupId: 4389303728991,
description: "audienceGroupName",
uploadDescription: "fileName",
audiences: [
{
id: "u1000",
},
{
id: "u2000",
},
],
};
const scope = mockPut(
MESSAGING_API_PREFIX,
"/audienceGroup/upload",
requestBody,
);
await client.updateUploadAudienceGroup(requestBody);
equal(scope.isDone(), true);
});
it("updateUploadAudienceGroupByFile", async () => {
const filepath = join(__dirname, "/helpers/line-icon.png");
const buffer = readFileSync(filepath);
const requestBody = {
audienceGroupId: 4389303728991,
uploadDescription: "fileName",
file: buffer,
};
const scope = nock(DATA_API_PREFIX, {
reqheaders: {
...interceptionOption.reqheaders,
"content-type": value =>
value.startsWith(`multipart/form-data; boundary=`),
},
})
.put(
"/audienceGroup/upload/byFile",
multipartFormDataMatcher(requestBody),
)
.reply(responseFn);
await client.updateUploadAudienceGroupByFile(requestBody);
equal(scope.isDone(), true);
});
it("createClickAudienceGroup", async () => {
const requestBody = {
description: "audienceGroupName",
requestId: "requestId",
};
const scope = mockPost(
MESSAGING_API_PREFIX,
"/audienceGroup/click",
requestBody,
);
await client.createClickAudienceGroup(requestBody);
equal(scope.isDone(), true);
});
it("createImpAudienceGroup", async () => {
const requestBody = {
requestId: "requestId",
description: "description",
};
const scope = mockPost(
MESSAGING_API_PREFIX,
"/audienceGroup/imp",
requestBody,
);
await client.createImpAudienceGroup(requestBody);
equal(scope.isDone(), true);
});
it("setDescriptionAudienceGroup", async () => {
const { description, audienceGroupId } = {
description: "description",
audienceGroupId: "audienceGroupId",
};
const scope = mockPut(
MESSAGING_API_PREFIX,
`/audienceGroup/${audienceGroupId}/updateDescription`,
{
description,
},
);
await client.setDescriptionAudienceGroup(description, audienceGroupId);
equal(scope.isDone(), true);
});
it("deleteAudienceGroup", async () => {
const audienceGroupId = "audienceGroupId";
const scope = mockDelete(
MESSAGING_API_PREFIX,
`/audienceGroup/${audienceGroupId}`,
);
const res = await client.deleteAudienceGroup(audienceGroupId);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("getAudienceGroup", async () => {
const audienceGroupId = "audienceGroupId";
const scope = mockGet(
MESSAGING_API_PREFIX,
`/audienceGroup/${audienceGroupId}`,
);
await client.getAudienceGroup(audienceGroupId);
equal(scope.isDone(), true);
});
it("getAudienceGroups", async () => {
const page = 1;
const description = "description";
const status: Types.AudienceGroupStatus = "READY";
const size = 1;
const createRoute: Types.AudienceGroupCreateRoute = "MESSAGING_API";
const includesExternalPublicGroups = true;
const scope = mockGet(MESSAGING_API_PREFIX, `/audienceGroup/list`, {
page,
description,
status,
size,
createRoute,
includesExternalPublicGroups,
});
await client.getAudienceGroups(
page,
description,
status,
size,
createRoute,
includesExternalPublicGroups,
);
equal(scope.isDone(), true);
});
it("getAudienceGroupAuthorityLevel", async () => {
const scope = mockGet(
MESSAGING_API_PREFIX,
`/audienceGroup/authorityLevel`,
);
await client.getAudienceGroupAuthorityLevel();
equal(scope.isDone(), true);
});
it("changeAudienceGroupAuthorityLevel", async () => {
const authorityLevel: Types.AudienceGroupAuthorityLevel = "PRIVATE";
const scope = mockPut(
MESSAGING_API_PREFIX,
`/audienceGroup/authorityLevel`,
{
authorityLevel,
},
);
await client.changeAudienceGroupAuthorityLevel(authorityLevel);
equal(scope.isDone(), true);
});
it("setWebhookEndpointUrl", async () => {
const endpoint = "https://developers.line.biz/";
const scope = mockPut(MESSAGING_API_PREFIX, `/channel/webhook/endpoint`, {
endpoint,
});
await client.setWebhookEndpointUrl(endpoint);
equal(scope.isDone(), true);
});
it("getWebhookEndpointInfo", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, `/channel/webhook/endpoint`);
await client.getWebhookEndpointInfo();
equal(scope.isDone(), true);
});
it("testWebhookEndpoint", async () => {
const endpoint = "https://developers.line.biz/";
const scope = mockPost(MESSAGING_API_PREFIX, `/channel/webhook/test`, {
endpoint,
});
await client.testWebhookEndpoint(endpoint);
equal(scope.isDone(), true);
});
it("set option once and clear option", async () => {
const expectedBody = {
messages: [testMsg],
to: "test_user_id",
notificationDisabled: false,
};
const retryKey = "retryKey";
const firstRequest = nock(MESSAGING_API_PREFIX, {
reqheaders: {
...interceptionOption.reqheaders,
"X-Line-Retry-Key": retryKey,
},
})
.post(`/message/push`, expectedBody)
.reply(responseFn);
const secondRequest = mockPost(MESSAGING_API_PREFIX, `/message/push`, {
messages: [testMsg],
to: "test_user_id",
notificationDisabled: false,
});
client.setRequestOptionOnce({
retryKey,
});
const firstResPromise = client.pushMessage("test_user_id", testMsg);
const secondResPromise = client.pushMessage("test_user_id", testMsg);
const [firstRes, secondRes] = await Promise.all([
firstResPromise,
secondResPromise,
]);
equal(firstRequest.isDone(), true);
equal(secondRequest.isDone(), true);
equal(firstRes["x-line-request-id"], "X-Line-Request-Id");
equal(secondRes["x-line-request-id"], "X-Line-Request-Id");
});
it("fails on construct with no channelAccessToken", () => {
try {
new Client({ channelAccessToken: null });
ok(false);
} catch (err) {
equal(err.message, "no channel access token");
}
});
it("fails on pass non-Buffer to setRichMenu", async () => {
try {
await client.setRichMenuImage("test_rich_menu_id", null);
ok(false);
} catch (err) {
equal(err.message, "invalid data type for binary data");
}
});
it("getBotInfo", async () => {
const scope = mockGet(MESSAGING_API_PREFIX, `/info`);
await client.getBotInfo();
equal(scope.isDone(), true);
});
});
const oauth = new OAuth();
describe("oauth", () => {
before(() => nock.disableNetConnect());
afterEach(() => nock.cleanAll());
after(() => nock.enableNetConnect());
const interceptionOption = {
reqheaders: {
"content-type": "application/x-www-form-urlencoded",
"User-Agent": `${pkg.name}/${pkg.version}`,
},
};
it("issueAccessToken", async () => {
const client_id = "test_client_id";
const client_secret = "test_client_secret";
const reply = {
access_token: "access_token",
expires_in: 2592000,
token_type: "Bearer",
};
const scope = nock(OAUTH_BASE_PREFIX, interceptionOption)
.post("/accessToken", {
grant_type: "client_credentials",
client_id,
client_secret,
})
.reply(200, reply);
const res = await oauth.issueAccessToken(client_id, client_secret);
equal(scope.isDone(), true);
deepEqual(res, reply);
});
it("revokeAccessToken", async () => {
const access_token = "test_channel_access_token";
const scope = nock(OAUTH_BASE_PREFIX, interceptionOption)
.post("/revoke", { access_token })
.reply(200, {});
const res = await oauth.revokeAccessToken(access_token);
equal(scope.isDone(), true);
deepEqual(res, {});
});
it("issueChannelAccessTokenV2_1", async () => {
const client_assertion = "client_assertion";
const reply = {
access_token: "access_token",
expires_in: 2592000,
token_type: "Bearer",
key_id: "key_id",
};
const scope = nock(OAUTH_BASE_PREFIX_V2_1, interceptionOption)
.post("/token", {
grant_type: "client_credentials",
client_assertion_type:
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion,
})
.reply(200, reply);
const res = await oauth.issueChannelAccessTokenV2_1(client_assertion);
equal(scope.isDone(), true);
deepEqual(res, reply);
});
it("getChannelAccessTokenKeyIdsV2_1", async () => {
const client_assertion = "client_assertion";
const reply = {
key_ids: ["key_id"],
};
const scope = nock(OAUTH_BASE_PREFIX_V2_1)
.get("/tokens/kid")
.query({
client_assertion_type:
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion,
})
.reply(200, reply);
const res = await oauth.getChannelAccessTokenKeyIdsV2_1(client_assertion);
equal(scope.isDone(), true);
deepEqual(res, reply);
});
it("revokeChannelAccessTokenV2_1", async () => {
const client_id = "test_client_id",
client_secret = "test_client_secret",
access_token = "test_channel_access_token";
const scope = nock(OAUTH_BASE_PREFIX_V2_1, interceptionOption)
.post("/revoke", { client_id, client_secret, access_token })
.reply(200, {});
const res = await oauth.revokeChannelAccessTokenV2_1(
client_id,
client_secret,
access_token,
);
equal(scope.isDone(), true);
deepEqual(res, {});
});
});
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"
]
}