신지원

START

Showing 77 changed files with 10175 additions and 0 deletions
1 +root = true
2 +
3 +[*]
4 +indent_style = space
5 +indent_size = 2
6 +end_of_line = lf
7 +charset = utf-8
8 +trim_trailing_whitespace = true
9 +insert_final_newline = true
10 +
11 +[*.md]
12 +trim_trailing_whitespace = false
1 +# Logs
2 +logs
3 +*.log
4 +npm-debug.log*
5 +yarn-debug.log*
6 +yarn-error.log*
7 +
8 +# Runtime data
9 +pids
10 +*.pid
11 +*.seed
12 +*.pid.lock
13 +
14 +# Directory for instrumented libs generated by jscoverage/JSCover
15 +lib-cov
16 +
17 +# Coverage directory used by tools like istanbul
18 +coverage
19 +
20 +# nyc test coverage
21 +.nyc_output
22 +
23 +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 +.grunt
25 +
26 +# Bower dependency directory (https://bower.io/)
27 +bower_components
28 +
29 +# node-waf configuration
30 +.lock-wscript
31 +
32 +# Compiled binary addons (http://nodejs.org/api/addons.html)
33 +build/Release
34 +
35 +# Dependency directories
36 +node_modules/
37 +jspm_packages/
38 +
39 +# Typescript v1 declaration files
40 +typings/
41 +
42 +# Optional npm cache directory
43 +.npm
44 +
45 +# Optional eslint cache
46 +.eslintcache
47 +
48 +# Optional REPL history
49 +.node_repl_history
50 +
51 +# Output of 'npm pack'
52 +*.tgz
53 +
54 +# Yarn Integrity file
55 +.yarn-integrity
56 +
57 +# dotenv environment variables file
58 +.env
59 +
60 +# Built files
61 +dist
62 +
63 +# Test recent request
64 +test/helpers/request.json
65 +
66 +# IDE
67 +.idea/
1 +language: node_js
2 +node_js:
3 + - 10
4 + - 12
5 + - 14
6 +before_install:
7 + - npm i -g npm@latest
8 +install:
9 + - npm ci
1 +## 7.3.0 (26 Apr 2021)
2 +
3 +### Feature
4 +* Support Flex Message Update 2 (#271)
5 +* Messaging API - January 2021 update (#277)
6 +
7 +### Misc
8 +* Add TypeScript Example (#270)
9 +* Update dependencies (#270)(#272)(#283)
10 +
11 +## 7.2.0 (18 Sep 2020)
12 +
13 +### Feature
14 +* Messaging API - December 2020 update (#268)
15 +* Messaging API - October 2020 update (#261)(#264)
16 +* update some Flex Message Update 2 (#265)
17 +
18 +### Misc
19 +* Update dependencies (#267)
20 +
21 +## 7.1.0 (18 Sep 2020)
22 +
23 +### Feature
24 +* Messaging API - August 2020 update (#240)(#251)(#258)
25 +* Messaging API - September 2020 update (#248)
26 +* Add Video viewing complete event (#241)
27 +* Channel access token v2.1 support key id (#231)
28 +* OAuth API v2.1 endpoint change (#233)
29 +
30 +### Bug fix
31 +* Accept label in richmenu area actions (#246)
32 +* Update dependencies & fix format (#234)(#236)(#238)(#243)(#250)
33 +* fix: fix createUploadAudienceGroup & updateUploadAudienceGroup API doc (#249)
34 +
35 +### Misc
36 +* Add Release CI & change release flow (#256)
37 +* Add: build doc github workflow
38 +
39 +## 7.0.0 (15 June 2020)
40 +
41 +### Breaking Changes
42 +* Node.js: drop 8 & adopt 14 (#222)
43 +
44 +### Feature
45 +* Support Channel access token v2.1 (#223)
46 +* Support Messaging API update for June 2020 (#228)
47 +* add X-Line-Retry-Key support (#224)
48 +* Support emojis in text message webhook (#218)
49 +* narrowcast api & audience apis (#193)
50 +* Add support for sticon in text messages (#214)
51 +* Add language support for profile API (#215)
52 +* Support icon-nickname-switch (#207)
53 +* Define LINE_SIGNATURE_HTTP_HEADER_NAME (#200)
54 +* add docs for getUserInteractionStatistics (#195)
55 +
56 +### Bug fixs & Feature Changes
57 +* fix getUserInteractionStatistics (#194)
58 +* type fix: accept string in the aspectRatio property of flex image and flex icon (#212)
59 +
60 +### Others
61 +* update dependencies & rewrite to promise (#225 & #229)
62 +* fix vulnerabilities (#217)
63 +* add emoji test (#198)
64 +* update vuepress to 1.x (#188)
65 +
66 +## 6.8.4 (19 Dec 2019)
67 +
68 +### Bug fix
69 +
70 +* Fix typo in type of FriendDemographics (#177)
71 +* Add label property to ImageMapAction type (#187)
72 +
73 +### Feature
74 +
75 +* Change data api's domain to api-data.line.me (#178)
76 +* Add getUserInteractionStatistics API (#183)
77 +* Add new properties in webhook types (#182)
78 +
79 +### Misc
80 +
81 +* Rewrite test in nock (#179)
82 +* Update dependencies (#180)
83 +
84 +## 6.8.3 (05 Nov 2019)
85 +
86 +### Bug fix
87 +
88 +* Add exception handler in middleware (#153)
89 +
90 +### Feature
91 +
92 +* Flex Message Update 1 (#173)
93 +* Support friend statistics API (#161)
94 +
95 +### Misc
96 +
97 +* Update dependencies (#174)
98 +
99 +## 6.8.2 (08 Aug 2019)
100 +
101 +### Bug fix
102 +
103 +* Fix LINEThings Scenario Execution Event Types (#158)
104 +
105 +## 6.8.1 (29 Jul 2019)
106 +
107 +### Bug fix
108 +
109 +* Fix a type wrong in Template Message (#163)
110 +
111 +### Feature
112 +* Get `X-LINE-Request-Id` by using `responseData['x-line-request-id']` (#151 #157)
113 +
114 +## 6.8.0 (25 Jun 2019)
115 +
116 +### Feature
117 +
118 +* Add new parameter in push/reply/multicast/broadcast API to catch up the newest bot API (#147)
119 +* Add new APIs in bot API (#147)
120 + - Get the target limit for additional messages
121 + - Get number of messages sent this month
122 + - Get number of sent broadcast messages
123 + - Send broadcast message
124 +
125 +### Breaking changes
126 +* Deprecate Node 6 and start to support Node 12 (#139)
127 +* Remove polyfills for Node 6 (#149)
128 +
129 +### Type
130 +
131 +* Add LINE Things Event (#150)
132 +
133 +### Misc
134 +* Update axios and other dependencies by running `npm audit fix` to fix vulnerabilities. (#148 #154)
135 +
136 +## 6.7.0 (18 Apr 2019)
137 +
138 +### Feature
139 +
140 +* Add alt URL field to URI action (#135)
141 +* Implement (un)linkRichMenuToMultipleUsers (#135)
142 +
143 +### Type
144 +
145 +* Fix typo in a type (#124)
146 +
147 +
148 +## 6.6.0 (4 Mar 2019)
149 +
150 +### Feature
151 +
152 +* Add DeviceLinkEvent / DeviceUnlinkEvent (#123)
153 +
154 +### Type
155 +
156 +* Fix FlexSpacer to have optional 'size' property (#122)
157 +
158 +### Misc
159 +
160 +* Run `npm audit fix` to fix minor dependency vulnerability.
161 +
162 +
163 +## 6.5.0 (16 Feb 2019)
164 +
165 +### Feature
166 +
167 +* Add APIs to get number of sent messages (#116)
168 +* Add account link event (#117)
169 +
170 +### Misc
171 +
172 +* Fix a typo in doc (#119)
173 +
174 +
175 +## 6.4.0 (19 Nov 2018)
176 +
177 +### Feature
178 +
179 +* Add `getLinkToken` API (#96)
180 +* Handle `req.rawBody` in Google Cloud Functions (#101)
181 +* [Kitchensink] Add ngrok functionality (#99)
182 +
183 +### Type
184 +
185 +* Add types for video in imagemap message (#100)
186 +* Add `contentProvider` fields to content messages (#103)
187 +* Add `destination` field to webhook request body (#102)
188 +* Add `MemberJoinEvent` and `MemberLeaveEvent` types (#107)
189 +
190 +### Misc
191 +
192 +* Don't include doc in released source
193 +* Upgrade TypeScript to 3.1.6 (#94)
194 +* Refactoring (#94, #98, #99)
195 +* Remove webhook-tester tool
196 +
197 +
198 +## 6.3.0 (21 Sep 2018)
199 +
200 +### Feature
201 +
202 +* Add default rich menu APIs (#87)
203 +
204 +### Type
205 +
206 +* Add missing `defaultAction` field to `TemplateColumn`
207 +
208 +### Misc
209 +
210 +* Use VuePress as documentation engine (#85)
211 +* Upgrade minimum supported Node.js version to 6
212 +
213 +
214 +## 6.2.1 (16 Aug 2018)
215 +
216 +### Misc
217 +
218 +* Remove gitbook-cli from dev dependencies
219 +
220 +
221 +## 6.2.0 (15 Aug 2018)
222 +
223 +#### Type
224 +
225 +* Add QuickReply types (#83)
226 +* Format type comments
227 +
228 +#### Misc
229 +
230 +* Upgrade TypeScript to 3
231 +
232 +
233 +## 6.1.1 (14 Aug 2018)
234 +
235 +#### Type
236 +
237 +* Update FlexMessage types (#81)
238 +
239 +#### Misc
240 +
241 +* Add test coverage (#78)
242 +* Add JSDoc comments (#80)
243 +
244 +
245 +## 6.1.0 (19 June 2018)
246 +
247 +#### Type
248 +
249 +* Add types for flex message (#74)
250 +* Simplify type definition for `Action`
251 +
252 +
253 +## 6.0.3 (18 June 2018)
254 +
255 +#### Misc
256 +
257 +* Move get-audio-duration dep to proper package.json (#73)
258 +* Vulnerability fix with `npm audit fix`
259 +
260 +
261 +## 6.0.2 (21 May 2018)
262 +
263 +#### Type
264 +
265 +* Add missing `displayText` field to postback action (#63)
266 +* Add missing `FileEventMessage` to `EventMessage` (#71)
267 +
268 +#### Misc
269 +
270 +* Add audio duration lib to kitchensink example (#68)
271 +
272 +
273 +## 6.0.1 (13 Mar 2018)
274 +
275 +#### Type
276 +
277 +* Fix misimplemented 'AudioMessage' type (#61)
278 +
279 +
280 +## 6.0.0 (27 Feb 2018)
281 +
282 +#### Major
283 +
284 +* Fix misimplemented 'unlinkRichMenuFromUser' API
285 +
286 +#### Type
287 +
288 +* Fix TemplateColumn type definition (#48)
289 +
290 +#### Misc
291 +
292 +* Update GitHub issue template (#43)
293 +* Add Code of Conduct (#50)
294 +* Catch errors properly in examples (#52)
295 +
296 +
297 +## 5.2.0 (11 Dec 2017)
298 +
299 +#### Minor
300 +
301 +* Set Content-Length manually for postBinary (#42)
302 +
303 +
304 +## 5.1.0 (7 Dec 2017)
305 +
306 +#### Minor
307 +
308 +* Add new fields (#39)
309 +
310 +#### Misc
311 +
312 +* Fix Windows build (#38)
313 +* Add start scripts and webhook info to examples
314 +
315 +
316 +## 5.0.1 (14 Nov 2017)
317 +
318 +#### Minor
319 +
320 +* Fix typo in `ImageMapMessage` type
321 +* Add kitchensink example (#36)
322 +
323 +
324 +## 5.0.0 (2 Nov 2017)
325 +
326 +#### Major
327 +
328 +* Implement rich menu API (#34)
329 +
330 +#### Type
331 +
332 +* Rename `ImageMapArea` and `TemplateAction`s into general ones
333 +
334 +#### Misc
335 +
336 +* Do not enforce `checkJSON` for some APIs where it means nothing
337 +* Change how to check request object in test cases
338 +
339 +
340 +## 4.0.0 (25 Oct 2017)
341 +
342 +#### Major
343 +
344 +* Make index script export exceptions and types (#31)
345 +
346 +#### Type
347 +
348 +* Simplify config types for client and middleware (#31)
349 +
350 +#### Misc
351 +
352 +* Fix information and links in doc
353 +* Use Prettier instead of TSLint (#30)
354 +* Install git hooks for precommit and prepush (#30)
355 +
356 +
357 +## 3.1.1 (19 Sep 2017)
358 +
359 +#### Type
360 +
361 +* Fix type of postback.params
362 +
363 +
364 +## 3.1.0 (19 Sep 2017)
365 +
366 +#### Major
367 +
368 +* Make middleware return `SignatureValidationFailed` for no signature (#26)
369 +
370 +#### Type
371 +
372 +* Add `FileEventMessage` type
373 +
374 +
375 +## 3.0.0 (8 Sep 2017)
376 +
377 +#### Major
378 +
379 +* Implement "Get group/room member profile" API (#15)
380 +* Implement "Get group/room member IDs" API (#23)
381 +* `getMessageContent` now returns `Promise<ReadableStream>` (#20)
382 +
383 +#### Type
384 +
385 +* Add "datetimepicker" support (#21)
386 +* Fix typo in `TemplateURIAction` type (#21)
387 +
388 +#### Misc
389 +
390 +* Package updates and corresponding fixes
391 +* Use npm 5 instead of Yarn in dev
392 +* Fix `clean` script to work in Windows
393 +* Use "axios" for internal HTTP client instead of "got" (#20)
394 +
395 +
396 +## 2.0.0 (12 June 2017)
397 +
398 +#### Type
399 +
400 +* Use literal types for 'type' fields
401 +
402 +#### Misc
403 +
404 +* Update yarn.lock with the latest Yarn
405 +
406 +
407 +## 1.1.0 (31 May 2017)
408 +
409 +* Handle pre-parsed body (string and buffer only)
410 +
411 +#### Type
412 +
413 +* Separate config type into client and middleware types
414 +* Add `userId` to group and room event sources
415 +
416 +#### Misc
417 +
418 +* Create issue template (#4)
419 +
420 +
421 +## 1.0.0 (11 May 2017)
422 +
423 +* Initial release
1 +# Contributor Covenant Code of Conduct
2 +
3 +## Our Pledge
4 +
5 +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.
6 +
7 +## Our Standards
8 +
9 +Examples of behavior that contributes to creating a positive environment include:
10 +
11 +* Using welcoming and inclusive language
12 +* Being respectful of differing viewpoints and experiences
13 +* Gracefully accepting constructive criticism
14 +* Focusing on what is best for the community
15 +* Showing empathy towards other community members
16 +
17 +Examples of unacceptable behavior by participants include:
18 +
19 +* The use of sexualized language or imagery and unwelcome sexual attention or advances
20 +* Trolling, insulting/derogatory comments, and personal or political attacks
21 +* Public or private harassment
22 +* Publishing others' private information, such as a physical or electronic address, without explicit permission
23 +* Other conduct which could reasonably be considered inappropriate in a professional setting
24 +
25 +## Our Responsibilities
26 +
27 +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.
28 +
29 +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.
30 +
31 +## Scope
32 +
33 +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.
34 +
35 +## Enforcement
36 +
37 +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.
38 +
39 +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.
40 +
41 +## Attribution
42 +
43 +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 +
45 +[homepage]: http://contributor-covenant.org
46 +[version]: http://contributor-covenant.org/version/1/4/
1 +# How to contribute to LINE Bot SDK for Node.js
2 +
3 +First of all, thank you so much for taking your time to contribute! LINE Bot SDK
4 +for Node.js is not very different from any other open source projects. It will
5 +be fantastic if you help us by doing any of the following:
6 +
7 +- File an issue in [the issue tracker](https://github.com/line/line-bot-sdk-nodejs/issues)
8 + to report bugs and propose new features and improvements.
9 +- Ask a question using [the issue tracker](https://github.com/line/line-bot-sdk-nodejs/issues).
10 +- Contribute your work by sending [a pull request](https://github.com/line/line-bot-sdk-nodejs/pulls).
11 +
12 +## Development
13 +
14 +You can freely fork the project, clone the forked repository, and start editing.
15 +
16 +Here are each top-level directory explained:
17 +
18 +* `lib`: TypeScript source code. You may modify files under this directory.
19 +* `test`: Mocha test suites. Please add tests for modification if possible.
20 +* `examples`: Example projects using this SDK
21 +* `docs`: [VuePress](https://vuepress.vuejs.org) markdowns for project documentation
22 +* `tools`: Useful tools
23 +
24 +Also, you may use the following npm scripts for development:
25 +
26 +* `npm run test`: Run test suites in `test`.
27 +* `npm run format`: Format source code with [Prettier](https://github.com/prettier/prettier)
28 +* `npm run format:check`: Silently run `format` and report formatting errors
29 +* `npm run build`: Build TypeScript code into JavaScript. The built files will
30 + be placed in `dist/`.
31 +* `npm run docs`: Build and serve documentation
32 +
33 +We test, lint and build on CI, but it is always nice to check them before
34 +uploading a pull request.
35 +
36 +## Contributor license agreement
37 +
38 +When you are sending a pull request and it's a non-trivial change beyond fixing typos, please make sure to sign
39 +[the ICLA (individual contributor license agreement)](https://cla-assistant.io/line/line-bot-sdk-nodejs). Please
40 +[contact us](mailto:dl_oss_dev@linecorp.com) if you need the CCLA (corporate contributor license agreement).
...\ No newline at end of file ...\ No newline at end of file
1 + Apache License
2 + Version 2.0, January 2004
3 + https://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "{}"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright (C) 2017-2018 LINE Corp.
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + https://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
1 +# LINE Messaging API SDK for nodejs
2 +
3 +[![Travis CI](https://travis-ci.org/line/line-bot-sdk-nodejs.svg?branch=master)](https://travis-ci.org/line/line-bot-sdk-nodejs)
4 +[![npmjs](https://badge.fury.io/js/%40line%2Fbot-sdk.svg)](https://www.npmjs.com/package/@line/bot-sdk)
5 +
6 +
7 +## Introduction
8 +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.
9 +
10 +## Documentation
11 +
12 +See the official API documentation for more information
13 +
14 +- English: https://developers.line.biz/en/docs/messaging-api/overview/
15 +- Japanese: https://developers.line.biz/ja/docs/messaging-api/overview/
16 +
17 +line-bot-sdk-nodejs documentation: https://line.github.io/line-bot-sdk-nodejs/#getting-started
18 +
19 +## Requirements
20 +
21 +* **Node.js** 10 or higher
22 +
23 +## Installation
24 +
25 +Using [npm](https://www.npmjs.com/):
26 +
27 +``` bash
28 +$ npm install @line/bot-sdk --save
29 +```
30 +
31 +## Help and media
32 +FAQ: https://developers.line.biz/en/faq/
33 +
34 +Community Q&A: https://www.line-community.me/questions
35 +
36 +News: https://developers.line.biz/en/news/
37 +
38 +Twitter: @LINE_DEV
39 +
40 +## Versioning
41 +This project respects semantic versioning
42 +
43 +See http://semver.org/
44 +
45 +## Contributing
46 +
47 +Please check [CONTRIBUTING](CONTRIBUTING.md) before making a contribution.
48 +
49 +## License
50 +```
51 +Copyright (C) 2016 LINE Corp.
52 +
53 +Licensed under the Apache License, Version 2.0 (the "License");
54 +you may not use this file except in compliance with the License.
55 +You may obtain a copy of the License at
56 +
57 + http://www.apache.org/licenses/LICENSE-2.0
58 +
59 +Unless required by applicable law or agreed to in writing, software
60 +distributed under the License is distributed on an "AS IS" BASIS,
61 +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
62 +See the License for the specific language governing permissions and
63 +limitations under the License.
64 +```
1 +module.exports = {
2 + base: "/line-bot-sdk-nodejs/",
3 + head: [
4 + ["link", { rel: "icon", href: "/favicon.ico" }]
5 + ],
6 + title: "line-bot-sdk-nodejs",
7 + description: "Node.js SDK for LINE Messaging API",
8 + themeConfig: {
9 + nav: [
10 + {
11 + text: "Introduction",
12 + link: "/"
13 + },
14 + {
15 + text: "Getting Started",
16 + link: "/getting-started"
17 + },
18 + {
19 + text: "Guide",
20 + link: "/guide"
21 + },
22 + {
23 + text: "API Reference",
24 + link: "/api-reference"
25 +
26 + },
27 + {
28 + text: "Contributing",
29 + link: "/CONTRIBUTING"
30 + },
31 + {
32 + text: "LINE Developers",
33 + link: "https://developers.line.biz/en/"
34 + },
35 + {
36 + text: "GitHub",
37 + link: "https://github.com/line/line-bot-sdk-nodejs/"
38 + },
39 + ],
40 + sidebar: [
41 + {
42 + title: "Introduction",
43 + collapsable: false,
44 + children: [
45 + "",
46 + ]
47 + },
48 + {
49 + title: "Getting Started",
50 + collapsable: false,
51 + children: [
52 + "/getting-started/requirements",
53 + "/getting-started/install",
54 + "/getting-started/basic-usage",
55 + ]
56 + },
57 + {
58 + title: "Guide",
59 + collapsable: false,
60 + children: [
61 + "/guide/webhook",
62 + "/guide/client",
63 + "/guide/typescript",
64 + ]
65 + },
66 + {
67 + title: "API Reference",
68 + collapsable: false,
69 + children: [
70 + "/api-reference/client",
71 + "/api-reference/validate-signature",
72 + "/api-reference/middleware",
73 + "/api-reference/exceptions",
74 + "/api-reference/message-and-event-objects",
75 + ]
76 + },
77 + {
78 + title: "Contributing",
79 + collapsable: false,
80 + children: [
81 + "/CONTRIBUTING",
82 + ]
83 + },
84 + ]
85 + }
86 +}
No preview for this file type
1 +../CONTRIBUTING.md
...\ No newline at end of file ...\ No newline at end of file
1 +../README.md
...\ No newline at end of file ...\ No newline at end of file
1 +# API Reference
2 +
3 +Please import the library via `require` or `import`.
4 +
5 +``` js
6 +// CommonJS
7 +const line = require('@line/bot-sdk');
8 +
9 +// ES2015 modules or TypeScript
10 +import * as line from '@line/bot-sdk';
11 +```
12 +
13 +For the detailed API reference of each, please refer to their own pages.
14 +
15 +- [Client](api-reference/client.md)
16 +- [OAuth](api-reference/oauth.md)
17 +- [validateSignature](api-reference/validate-signature.md)
18 +- [middleware](api-reference/middleware.md)
19 +- [Exceptions](api-reference/exceptions.md)
20 +- [Message and event objects](api-reference/message-and-event-objects.md)
1 +# `new Client(config)`
2 +
3 +`Client` is a class representing an API client. It provides methods
4 +corresponding to [messaging APIs](https://developers.line.biz/en/reference/messaging-api/).
5 +
6 +#### Type signature
7 +
8 +``` typescript
9 +class Client {
10 + public config: ClientConfig
11 +
12 + constructor(config: ClientConfig) {}
13 +
14 + // requestOption
15 + setRequestOptionOnce(option: Partial<{
16 + retryKey: string;
17 + }>)
18 +
19 + // Message
20 + pushMessage(to: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
21 + replyMessage(replyToken: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
22 + multicast(to: string[], messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
23 + narrowcast(
24 + messages: Message | Message[],
25 + recipient?: ReceieptObject,
26 + filter?: { demographic: DemographicFilterObject },
27 + limit?: { max?: number, upToRemainingQuota?: boolean },
28 + notificationDisabled?: boolean,
29 + ): Promise<MessageAPIResponseBase>
30 + broadcast(messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
31 + getMessageContent(messageId: string): Promise<Readable>
32 +
33 + // Profile
34 + getProfile(userId: string): Promise<Profile>
35 +
36 + // Group
37 + getGroupSummary(groupId: string): Promise<GroupSummary>
38 + getGroupMembersCount(groupId: string): Promise<MemberCountResponse>
39 + getGroupMemberProfile(groupId: string, userId: string): Promise<Profile>
40 + getGroupMemberIds(groupId: string): Promise<string[]>
41 + leaveGroup(groupId: string): Promise<any>
42 +
43 + // Room
44 + getRoomMembersCount(roomId: string): Promise<MemberCountResponse>
45 + getRoomMemberProfile(roomId: string, userId: string): Promise<Profile>
46 + getRoomMemberIds(roomId: string): Promise<string[]>
47 + leaveRoom(roomId: string): Promise<any>
48 +
49 + // Rich menu
50 + getRichMenu(richMenuId: string): Promise<RichMenuResponse>
51 + createRichMenu(richMenu: RichMenu): Promise<string>
52 + deleteRichMenu(richMenuId: string): Promise<any>
53 + getRichMenuIdOfUser(userId: string): Promise<string>
54 + linkRichMenuToUser(userId: string, richMenuId: string): Promise<any>
55 + unlinkRichMenuFromUser(userId: string, richMenuId: string): Promise<any>
56 + linkRichMenuToMultipleUsers(richMenuId: string, userIds: string[]): Promise<any>
57 + unlinkRichMenusFromMultipleUsers(userIds: string[]): Promise<any>
58 + getRichMenuImage(richMenuId: string): Promise<Readable>
59 + setRichMenuImage(richMenuId: string, data: Buffer | Readable, contentType?: string): Promise<any>
60 + getRichMenuList(): Promise<Array<RichMenuResponse>>
61 + setDefaultRichMenu(richMenuId: string): Promise<{}>
62 + getDefaultRichMenuId(): Promise<string>
63 + deleteDefaultRichMenu(): Promise<{}>
64 +
65 + // Account link
66 + getLinkToken(userId: string): Promise<string>
67 +
68 + // Get number of messages sent
69 + getNumberOfSentReplyMessages(date: string): Promise<NumberOfMessagesSentResponse>
70 + getNumberOfSentPushMessages(date: string): Promise<NumberOfMessagesSentResponse>
71 + getNumberOfSentMulticastMessages(date: string): Promise<NumberOfMessagesSentResponse>
72 + getTargetLimitForAdditionalMessages(): Promise<TargetLimitForAdditionalMessages>
73 + getNumberOfMessagesSentThisMonth(): Promise<NumberOfMessagesSentThisMonth>
74 + getNumberOfSentBroadcastMessages(date: string): Promise<NumberOfMessagesSentResponse>
75 + getNarrowcastProgress(requestId: string): Promise<NarrowcastProgressResponse>
76 +
77 + // Insight
78 + getNumberOfMessageDeliveries(date: string): Promise<Types.NumberOfMessageDeliveriesResponse>
79 + getNumberOfFollowers(date: string): Promise<Types.NumberOfFollowersResponse>
80 + getFriendDemographics(): Promise<Types.FriendDemographics>
81 + getUserInteractionStatistics(requestId: string): Promise<Types.UserInteractionStatistics>
82 +
83 + // AudienceGroup
84 + createUploadAudienceGroup(uploadAudienceGroup: {
85 + description: string;
86 + isIfaAudience?: boolean;
87 + audiences?: { id: string }[];
88 + uploadDescription?: string;
89 + }) : Promise<{
90 + audienceGroupId: number;
91 + type: string;
92 + description: string;
93 + created: number;
94 + requestId: string;
95 + }>
96 + createUploadAudienceGroupByFile(uploadAudienceGroup: {
97 + description: string;
98 + isIfaAudience?: boolean;
99 + uploadDescription?: string;
100 + file: Buffer | Readable;
101 + }) : Promise<{
102 + audienceGroupId: number;
103 + type: "UPLOAD";
104 + description: string;
105 + created: number;
106 + }>
107 + updateUploadAudienceGroup(
108 + uploadAudienceGroup: {
109 + audienceGroupId: number;
110 + description?: string;
111 + uploadDescription?: string;
112 + audiences: { id: string }[];
113 + },
114 + // for set request timeout
115 + httpConfig?: Partial<AxiosRequestConfig>,
116 + ) : Promise<{}>
117 + createUploadAudienceGroupByFile(
118 + uploadAudienceGroup: {
119 + audienceGroupId: number;
120 + uploadDescription?: string;
121 + file: Buffer | Readable;
122 + },
123 + // for set request timeout
124 + httpConfig?: Partial<AxiosRequestConfig>,
125 + }) : Promise<{}>
126 + createClickAudienceGroup(clickAudienceGroup: {
127 + description: string;
128 + requestId: string;
129 + clickUrl?: string;
130 + }) :Promise<{
131 + audienceGroupId: number;
132 + type: string;
133 + created: number;
134 + description: string;
135 + requestId: string;
136 + clickUrl: string;
137 + }>
138 + createImpAudienceGroup(impAudienceGroup: {
139 + requestId: string;
140 + description: string;
141 + }): Promise<{
142 + audienceGroupId: number;
143 + type: string;
144 + description: string;
145 + created: number;
146 + requestId: string;
147 + }>
148 + setDescriptionAudienceGroup(
149 + description: string,
150 + audienceGroupId: string,
151 + ): Promise<{}>
152 + deleteAudienceGroup(audienceGroupId: string): Promise<{}>
153 + getAudienceGroup(audienceGroupId: string): Promise<AudienceGroup>
154 + getAudienceGroups(
155 + page: number,
156 + description?: string,
157 + status?: AudienceGroupStatus,
158 + size?: number,
159 + createRoute?: AudienceGroupCreateRoute,
160 + includesExternalPublicGroups?: boolean,
161 + ): Promise<{
162 + audienceGroups: AudienceGroups;
163 + hasNextPage: boolean;
164 + totalCount: number;
165 + readWriteAudienceGroupTotalCount: number;
166 + page: number;
167 + size: number;
168 + }>
169 + getAudienceGroupAuthorityLevel(): Promise<{
170 + authorityLevel: Types.AudienceGroupAuthorityLevel
171 + }>
172 + changeAudienceGroupAuthorityLevel(
173 + authorityLevel: Types.AudienceGroupAuthorityLevel
174 + ): Promise<{}>
175 +
176 + // Bot
177 + getBotInfo(): Promise<BotInfoResponse>
178 +
179 + // Webhook
180 + setWebhookEndpointUrl(endpoint: string): Promise<{}>
181 + getWebhookEndpointInfo(): Promise<{
182 + endpoint: string;
183 + active: boolean;
184 + }>
185 + testWebhookEndpoint(endpoint?: string): Promise<{
186 + success: boolean;
187 + timestamp: string;
188 + statusCode: number;
189 + reason: string;
190 + detail: string;
191 + }>
192 +}
193 +```
194 +
195 +`Message` is a valid message object. About message object structure, please
196 +refer to [Message and event objects](./message-and-event-objects.md) on this guide, or
197 +[Send message object](https://developers.line.biz/en/reference/messaging-api/#message-objects)
198 +on the official documentation.
199 +
200 +`ClientConfig` type is like below.
201 +
202 +``` typescript
203 +interface ClientConfig {
204 + channelAccessToken: string;
205 + channelSecret?: string;
206 +}
207 +```
208 +
209 +## Common Specifications
210 +
211 +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`.
212 +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.
213 +
214 +## Methods
215 +
216 +For a parameter `messages: messages: Message | Message[]`, you can provide a
217 +message object or an array of message objects. Both will work, but please beware
218 +that there can be a limit on the number of the messages to be sent
219 +simultaneously. About the API detail, please refer to [the official documentation](https://developers.line.biz/en/reference/messaging-api/#message-objects).
220 +
221 +For functions returning `Promise`, there will be errors thrown if something
222 +goes wrong, such as HTTP errors or parsing errors. You can catch them with the
223 +`.catch()` method of the promises. The detailed error handling is explained
224 +in [the Client guide](../guide/client.md).
225 +
226 +### Message
227 +
228 +#### `pushMessage(to: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
229 +
230 +It corresponds to the [Push message](https://developers.line.biz/en/reference/messaging-api/#send-push-message) API.
231 +
232 +The first argument is an ID of a receiver, and the second is messages to be sent.
233 +
234 +``` js
235 +client.pushMessage('user_or_group_or_room_id', {
236 + type: 'text',
237 + text: 'hello, world',
238 +})
239 +```
240 +
241 +#### `replyMessage(replyToken: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
242 +
243 +It corresponds to the [Reply message](https://developers.line.biz/en/reference/messaging-api/#send-reply-message) API.
244 +
245 +The first argument is a reply token, which is retrieved from a webhook event
246 +object. For the list of replyable events, please refer to [Webhook event object](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
247 +of the official documentation. The second argument is the same with one in `pushMessage()`.
248 +
249 +``` js
250 +client.replyMessage(event.replyToken, {
251 + type: 'text',
252 + text: 'hello, world',
253 +})
254 +```
255 +
256 +#### `multicast(to: string[], messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>`
257 +
258 +It corresponds to the [Multicast](https://developers.line.biz/en/reference/messaging-api/#send-multicast-message) API.
259 +
260 +The first argument is a list of receiver IDs, and the second is messages to be
261 +sent.
262 +
263 +``` js
264 +client.multicast(['user_id_1', 'user_id_2', 'room_id_1'], {
265 + type: 'text',
266 + text: 'hello, world',
267 +})
268 +```
269 +
270 +#### `broadcast(messages: Message | Message[], notificationDisabled: boolean = false): Promise<any>`
271 +
272 +Sends push messages to multiple users at any time.
273 +
274 +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/).
275 +
276 +``` js
277 +client.broadcast({
278 + type: 'text',
279 + text: 'hello, world',
280 +})
281 +```
282 +
283 +#### `getMessageContent(messageId: string): Promise<Readable>`
284 +
285 +It corresponds to the [Content](https://developers.line.biz/en/reference/messaging-api/#get-content) API.
286 +
287 +The argument is an ID of media messages, such as image, video, and audio. The ID
288 +can be retrieved from a message object of a message event.
289 +
290 +Please beware that what it returns is promise of [readable stream](https://nodejs.org/dist/latest/docs/api/stream.html#stream_readable_streams).
291 +You can pipe the stream into a file, an HTTP response, etc.
292 +
293 +``` js
294 +client.getMessageContent('message_id')
295 + .then((stream) => {
296 + stream.on('data', (chunk) => {
297 + ...
298 + })
299 + stream.on('error', (err) => {
300 + ...
301 + })
302 + stream.pipe(...)
303 + })
304 +```
305 +
306 +### Profile
307 +
308 +#### `getProfile(userId: string): Promise<Profile>`
309 +
310 +It corresponds to the [Profile](https://developers.line.biz/en/reference/messaging-api/#get-profile) API.
311 +
312 +The argument is a user ID.
313 +
314 +``` js
315 +client.getProfile('user_id').then((profile) => {
316 + console.log(profile);
317 +});
318 +```
319 +
320 +### Group
321 +#### `getGroupSummary(groupId: string): Promise<GroupSummary>`
322 +
323 +It corresponds to the [Group Summary](https://developers.line.biz/en/reference/messaging-api/#get-group-summary) API.
324 +
325 +The argument is a group ID.
326 +
327 +``` js
328 +client.getGroupSummary('group_id').then((summary) => {
329 + console.log(summary)
330 +})
331 +```
332 +
333 +#### `getGroupMembersCount(groupId: string): Promise<MemberCountResponse>`
334 +
335 +It corresponds to the [Group Members Count](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count) API.
336 +
337 +The argument is a group ID.
338 +
339 +``` js
340 +client.getGroupMembersCount('group_id').then((count) => {
341 + console.log(count)
342 +})
343 +```
344 +
345 +#### `getGroupMemberProfile(groupId: string, userId: string): Promise<Profile>`
346 +
347 +It corresponds to the [Group Member Profile](https://developers.line.biz/en/reference/messaging-api/#get-group-member-profile) API.
348 +
349 +The arguments are a group ID and an ID of a user in the group. Please refer to
350 +the official documentation for the difference between this API and `getProfile()`.
351 +
352 +``` js
353 +client.getGroupMemberProfile('group_id', 'user_id').then((profile) => {
354 + console.log(profile);
355 +})
356 +```
357 +
358 +#### `getGroupMemberIds(groupId: string): Promise<string[]>`
359 +
360 +It corresponds to the [Group Member IDs](https://developers.line.biz/en/reference/messaging-api/#get-group-member-user-ids) API.
361 +
362 +*FYI: This feature is only available for LINE@ Approved accounts or official accounts.*
363 +
364 +The argument is a group ID and the method returns a promise of an array of user IDs.
365 +
366 +``` js
367 +client.getGroupMemberIds('group_id').then((ids) => {
368 + ids.forEach((id) => console.log(id));
369 +})
370 +```
371 +
372 +#### `leaveGroup(groupId: string): Promise<any>`
373 +
374 +It corresponds to the [Leave group](https://developers.line.biz/en/reference/messaging-api/#leave-group) API.
375 +
376 +The argument is a group ID.
377 +
378 +``` js
379 +client.leaveGroup('group_id')
380 +```
381 +
382 +### Room
383 +#### `getRoomMembersCount(roomId: string): Promise<MembersCountResponse>`
384 +
385 +It corresponds to the [Room Members Count](https://developers.line.biz/en/reference/messaging-api/#get-members-room-count) API.
386 +
387 +The argument is a room ID.
388 +
389 +``` js
390 +client.getRoomMembersCount('room_id').then((count) => {
391 + console.log(count)
392 +})
393 +```
394 +
395 +#### `getRoomMemberProfile(roomId: string, userId: string): Promise<Profile>`
396 +
397 +It corresponds to the [Room Member Profile](https://developers.line.biz/en/reference/messaging-api/#get-room-member-profile) API.
398 +
399 +The arguments are a room ID and an ID of a user in the room. Please refer to the
400 +official documentation for the difference between this API and `getProfile()`.
401 +
402 +``` js
403 +client.getRoomMemberProfile('room_id', 'user_id').then((profile) => {
404 + console.log(profile);
405 +})
406 +```
407 +
408 +#### `getRoomMemberIds(roomId: string): Promise<string[]>`
409 +
410 +It corresponds to the [Room Member IDs](https://developers.line.biz/en/reference/messaging-api/#get-room-member-user-ids) API.
411 +
412 +*FYI: This feature is only available for LINE@ Approved accounts or official accounts.*
413 +
414 +The argument is a room ID and the method returns a promise of an array of user IDs.
415 +
416 +``` js
417 +client.getRoomMemberIds('room_id').then((ids) => {
418 + ids.forEach((id) => console.log(id));
419 +})
420 +```
421 +
422 +#### `leaveRoom(roomId: string): Promise<any>`
423 +
424 +It corresponds to the [Leave room](https://developers.line.biz/en/reference/messaging-api/#leave-room) API.
425 +
426 +The argument is a room ID.
427 +
428 +``` js
429 +client.leaveGroup('room_id')
430 +```
431 +
432 +### Rich menu
433 +
434 +#### `getRichMenu(richMenuId: string): Promise<RichMenuResponse>`
435 +
436 +It corresponds to the [Get rich menu](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu) API.
437 +
438 +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).
439 +
440 +``` js
441 +client.getRichMenu('rich_menu_id').then((richMenu) => {
442 + console.log(richMenu.size);
443 + console.log(richMenu.areas[0].bounds);
444 +})
445 +```
446 +
447 +#### `createRichMenu(richMenu: RichMenu): Promise<string>`
448 +
449 +It corresponds to the [Create rich menu](https://developers.line.biz/en/reference/messaging-api/#create-rich-menu) API.
450 +
451 +The argument is [a rich menu object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-object).
452 +For the detail of the object format, please refer to the official documentation.
453 +It returns the result rich menu ID.
454 +
455 +``` js
456 +client.createRichMenu({ size: { width: 2500, height: 1686 }, ... })
457 + .then((richMenuId) => console.log(richMenuId))
458 +```
459 +
460 +#### `deleteRichMenu(richMenuId: string): Promise<any>`
461 +
462 +It corresponds to the [Delete rich menu](https://developers.line.biz/en/reference/messaging-api/#delete-rich-menu) API.
463 +
464 +The argument is a rich menu ID.
465 +
466 +``` js
467 +client.deleteRichMenu('rich_menu_id')
468 +```
469 +
470 +#### `getRichMenuIdOfUser(userId: string): Promise<string>`
471 +
472 +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.
473 +
474 +The argument is a user ID. It returns a rich menu ID to be used with other APIs.
475 +
476 +``` js
477 +client.getRichMenuIdOfUser('user_id').then((richMenuId) => {
478 + console.log(richMenuId);
479 +})
480 +```
481 +
482 +#### `linkRichMenuToUser(userId: string, richMenuId: string): Promise<any>`
483 +
484 +It corresponds to the [Link rich menu to user](https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-user) API.
485 +
486 +The arguments are a user ID and a rich menu ID.
487 +
488 +``` js
489 +client.linkRichMenuToUser('user_id', 'rich_menu_id')
490 +```
491 +
492 +#### `unlinkRichMenuFromUser(userId: string, richMenuId: string): Promise<any>`
493 +
494 +It corresponds to the [Unlink rich menu from user](https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-user) API.
495 +
496 +The arguments are a user ID and a rich menu ID.
497 +
498 +``` js
499 +client.unlinkRichMenuFromUser('user_id', 'rich_menu_id')
500 +```
501 +
502 +#### `linkRichMenuToMultipleUsers(richMenuId: string, userIds: string[]): Promise<any>`
503 +
504 + It corresponds to the [Link rich menu to multiple users](https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users) API.
505 +
506 + The arguments are a richMenuId and a array of userIds.
507 +
508 + ``` js
509 + client.linkRichMenuToMultipleUsers('rich_menu_id', ['user_id'])
510 + ```
511 +
512 +#### `unlinkRichMenusFromMultipleUsers(userIds: string[]): Promise<any>`
513 +
514 + It corresponds to the [Unlink rich menus from multiple users](https://developers.line.biz/en/reference/messaging-api#unlink-rich-menu-from-users) API.
515 +
516 + The argument is a array of userIds.
517 +
518 + ``` js
519 + client.unlinkRichMenusFromMultipleUsers(['user_id'])
520 + ```
521 +
522 +#### `getRichMenuImage(richMenuId: string): Promise<Readable>`
523 +
524 +It corresponds to the [Download rich menu image](https://developers.line.biz/en/reference/messaging-api/#download-rich-menu-image) API.
525 +
526 +The argument is a rich menu ID.
527 +
528 +Please beware that what it returns is promise of [readable stream](https://nodejs.org/dist/latest/docs/api/stream.html#stream_readable_streams).
529 +You can pipe the stream into a file, an HTTP response, etc.
530 +
531 +``` js
532 +client.getRichMenuImage('rich_menu_id')
533 + .then((stream) => {
534 + stream.on('data', (chunk) => {
535 + ...
536 + })
537 + stream.on('error', (err) => {
538 + ...
539 + })
540 + stream.pipe(...)
541 + })
542 +```
543 +
544 +#### `setRichMenuImage(richMenuId: string, data: Buffer | Readable, contentType?: string): Promise<any>`
545 +
546 +It corresponds to the [Upload rich menu image](https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image) API.
547 +
548 +The 1st argument is a rich menu ID. For 2nd argument, a buffer or a readable
549 +stream of an image should be provided. For the restriction of the image, please
550 +refer to the official documentation. The last argument is optional. If it's not
551 +provided, the mime type will be guessted from `data`. Only `image/jpeg` or
552 +`image/png` is allowed for the content type.
553 +
554 +``` js
555 +client.setRichMenuImage('rich_menu_id', fs.createReadStream('./some_image.png'))
556 +```
557 +
558 +#### `getRichMenuList(): Promise<Array<RichMenuResponse>>`
559 +
560 +It corresponds to the [Get rich menu list](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-list) API.
561 +
562 +The return type is a list of [rich menu response objects](https://developers.line.biz/en/reference/messaging-api/#rich-menu-response-object).
563 +
564 +### `setDefaultRichMenu(richMenuId: string): Promise<{}>`
565 +
566 +It corresponds to the [Set default rich menu](https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu) API.
567 +
568 +### `getDefaultRichMenuId(): Promise<string>`
569 +
570 +It corresponds to the [Get default rich menu ID](https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id) API.
571 +
572 +### `deleteDefaultRichMenu(): Promise<{}>`
573 +
574 +It corresponds to the [Cancel default rich menu](https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu) API.
575 +
576 +### Account link
577 +
578 +#### `getLinkToken(userId: string): Promise<string>`
579 +
580 +Send an HTTP POST request to the `/bot/user/{userId}/linkToken` endpoint,
581 +and [issue a link token](https://developers.line.biz/en/reference/messaging-api/#issue-link-token) for the user you are attempting to link.
582 +
583 +If the request succeeds, a link token will be returned.
584 +Link tokens are valid for 10 minutes and can only be used once.
585 +
586 +### Get number of messages sent
587 +
588 +#### `getNumberOfSentReplyMessages(date: string): Promise<NumberOfMessagesSentResponse>`
589 +
590 +Gets the number of messages sent with the `/bot/message/reply` endpoint.
591 +
592 +The number of messages retrieved by this operation does not include
593 +the number of messages sent from LINE@ Manager.
594 +
595 +``` js
596 +client.getNumberOfSentReplyMessages('20191231').then((response) => {
597 + console.log(response);
598 +})
599 +```
600 +
601 +#### `getNumberOfSentPushMessages(date: string): Promise<NumberOfMessagesSentResponse>`
602 +
603 +Gets the number of messages sent with the `/bot/message/push` endpoint.
604 +
605 +The number of messages retrieved by this operation does not include
606 +the number of messages sent from LINE@ Manager.
607 +
608 +``` js
609 +client.getNumberOfSentPushMessages('20191231').then((response) => {
610 + console.log(response);
611 +})
612 +```
613 +
614 +#### `getNumberOfSentMulticastMessages(date: string): Promise<NumberOfMessagesSentResponse>`
615 +
616 +Gets the number of messages sent with the `/bot/message/multicast` endpoint.
617 +
618 +The number of messages retrieved by this operation does not include
619 +the number of messages sent from LINE@ Manager.
620 +
621 +``` js
622 +client.getNumberOfSentMulticastMessages('20191231').then((response) => {
623 + console.log(response);
624 +})
625 +```
626 +
627 +#### `getTargetLimitForAdditionalMessages(): Promise<TargetLimitForAdditionalMessages>`
628 +
629 +Gets the target limit for additional messages in the current month.
630 +
631 +The number of messages retrieved by this operation includes the number of messages sent from LINE Official Account Manager.
632 +
633 +Set a target limit with LINE Official Account Manager. For the procedures, refer to the LINE Official Account Manager manual.
634 +
635 +Note: LINE@ accounts cannot call this API endpoint.
636 +
637 +``` js
638 +client.getTargetLimitForAdditionalMessages().then((response) => {
639 + console.log(response);
640 +})
641 +```
642 +
643 +#### `getNumberOfMessagesSentThisMonth(): Promise<NumberOfMessagesSentThisMonth>`
644 +
645 +Gets the number of messages sent in the current month.
646 +
647 +The number of messages retrieved by this operation includes the number of messages sent from LINE Official Account Manager.
648 +
649 +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.
650 +
651 +Note: LINE@ accounts cannot call this API endpoint.
652 +
653 +``` js
654 +client.getNumberOfMessagesSentThisMonth().then((response) => {
655 + console.log(response);
656 +})
657 +```
658 +
659 +#### `getNumberOfSentBroadcastMessages(date: string): Promise<NumberOfMessagesSentResponse>`
660 +
661 +Gets the number of messages sent with the `/bot/message/broadcast` endpoint.
662 +
663 +The number of messages retrieved by this operation does not include the number of messages sent from LINE Official Account Manager.
664 +
665 +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/).
666 +
667 +``` js
668 +client.getNumberOfSentBroadcastMessages('20191231').then((response) => {
669 + console.log(response);
670 +})
671 +```
672 +
673 +### Insight
674 +
675 +#### `getNumberOfMessageDeliveries(date: string): Promise<NumberOfMessageDeliveriesResponse>`
676 +
677 +It corresponds to the [Get number of message deliveries](https://developers.line.biz/en/reference/messaging-api/#get-number-of-delivery-messages) API.
678 +
679 +#### `getNumberOfFollowers(date: string): Promise<NumberOfFollowersResponse>`
680 +
681 +It corresponds to the [Get number of followers](https://developers.line.biz/en/reference/messaging-api/#get-number-of-followers) API.
682 +
683 +
684 +#### `getFriendDemographics(): Promise<Types.FriendDemographics>`
685 +
686 +It corresponds to the [Get friend demographics](https://developers.line.biz/en/reference/messaging-api/#get-demographic) API.
687 +
688 +### Bot
689 +
690 +#### `getBotInfo(): Promise<BotInfoResponse>`
691 +
692 +It corresponds to the [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info) API.
693 +
694 +### Webhook
695 +
696 +#### `setWebhookEndpointUrl(endpoint: string): Promise<{}}>`
697 +
698 +It corresponds to the [Set webhook endpoint URL](https://developers.line.biz/en/reference/messaging-api/#set-webhook-endpoint-url) API.
699 +
700 +
701 +#### `getWebhookEndpointInfo(): Promise<Types.WebhookEndpointInfoResponse>`
702 +
703 +It corresponds to the [Get webhook endpoint information](https://developers.line.biz/en/reference/messaging-api/#get-webhook-endpoint-information) API.
704 +
705 +#### `testWebhookEndpoint(endpoint?: string): Promise<Types.TestWebhookEndpointResponse>`
706 +
707 +It corresponds to the [Test webhook endpoint](https://developers.line.biz/en/reference/messaging-api/#test-webhook-endpoint) API.
1 +# Exceptions
2 +
3 +Exception classes can also be imported from `@line/bot-sdk`.
4 +
5 +``` js
6 +// CommonJS (destructuring can be used for Node.js >= 6)
7 +const HTTPError = require('@line/bot-sdk').HTTPError;
8 +const JSONParseError = require('@line/bot-sdk').JSONParseError;
9 +const ReadError = require('@line/bot-sdk').ReadError;
10 +const RequestError = require('@line/bot-sdk').RequestError;
11 +const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed;
12 +
13 +// ES2015 modules or TypeScript
14 +import {
15 + HTTPError,
16 + JSONParseError,
17 + ReadError,
18 + RequestError,
19 + SignatureValidationFailed,
20 +} from '@line/bot-sdk/exceptions';
21 +```
22 +
23 +#### Type signature
24 +
25 +``` typescript
26 +class SignatureValidationFailed extends Error {
27 + public signature?: string;
28 +}
29 +
30 +class JSONParseError extends Error {
31 + public raw: any;
32 +}
33 +
34 +class RequestError extends Error {
35 + public code: string; // e.g. ECONNREFUSED
36 +}
37 +
38 +class ReadError extends Error {
39 +}
40 +
41 +class HTTPError extends Error {
42 + public statusCode: number; // e.g. 404
43 + public statusMessage: string; // e.g. Not Found
44 +}
45 +```
46 +
47 +About what causes the errors and how to handle them, please refer to each guide
48 +of [webhook](../guide/webhook.md) and [client](../guide/client.md).
1 +# Message and event objects
2 +
3 +The message objects and event objects are plain JS objects with no
4 +abstraction. This SDK provides TypeScript types for them, which can be imported
5 +from `@line/bot-sdk`.
6 +
7 +Please beware that the types only work in TypeScript, and will be removed when
8 +built into JavaScript.
9 +
10 +``` typescript
11 +import {
12 + // webhook event objects
13 + WebhookEvent,
14 + MessageEvent,
15 + EventSource,
16 + VideoEventMessage,
17 +
18 + // message event objects
19 + Message,
20 + TemplateMessage,
21 + TemplateContent,
22 +} from "@line/bot-sdk";
23 +```
24 +
25 +For the actual type definitions, please refer to [types.ts](https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts)
26 +directly.
27 +
28 +You can also refer to the official specification:
29 +
30 +- [Message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects)
31 +- [Webhook event objects](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
1 +# `middleware(config)`
2 +
3 +It returns a [connect](https://github.com/senchalabs/connect) middleware used
4 +by several Node.js web frameworks such as [Express](https://expressjs.com/).
5 +
6 +#### Type signature
7 +
8 +``` typescript
9 +function middleware(config: MiddlewareConfig): Middleware
10 +```
11 +
12 +The types of `MiddlewareConfig` and `Middleware` are like below.
13 +
14 +``` typescript
15 +interface MiddlewareConfig {
16 + channelAccessToken?: string;
17 + channelSecret: string;
18 +}
19 +
20 +type Middleware =
21 + ( req: http.IncomingMessage
22 + , res: http.ServerResponse
23 + , next: (err?: Error) => void
24 + ) => void
25 +```
26 +
27 +The `Middleware` type is defined according to the connect middleware itself. For
28 +the detail of the connect middleware, please refer to the [connect](https://github.com/senchalabs/connect) documentation.
29 +
30 +## Usage
31 +
32 +A very simple example of the middleware usage with an Express app is like below:
33 +
34 +``` js
35 +// globally
36 +app.use(middleware(config))
37 +
38 +// or directly with handler
39 +app.post('/webhook', middleware(config), (req, res) => {
40 + req.body.events // webhook event objects
41 + req.body.destination // user ID of the bot (optional)
42 + ...
43 +})
44 +```
45 +
46 +The middleware returned by `middleware()` parses body and checks signature
47 +validation, so you do not need to use [`validateSignature()`](./validate-signature.md)
48 +directly.
49 +
50 +You do not need to use [body-parser](https://github.com/expressjs/body-parser)
51 +to parse webhook events, as `middleware()` embeds body-parser and parses them to
52 +objects. Please keep in mind that it will not process requests without
53 +`X-Line-Signature` header. If you have a reason to use body-parser for other
54 +routes, *please do not use it before the LINE middleware*. body-parser parses
55 +the request body up and the LINE middleware cannot parse it afterwards.
56 +
57 +``` js
58 +// don't
59 +app.use(bodyParser.json())
60 +app.use(middleware(config))
61 +
62 +// do
63 +app.use(middleware(config))
64 +app.use(bodyParser.json())
65 +```
66 +
67 +There are environments where `req.body` is pre-parsed, such as [Firebase Cloud Functions](https://firebase.google.com/docs/functions/http-events).
68 +If it parses the body into string or buffer, do not worry as the middleware will
69 +work just fine. If the pre-parsed body is an object, please use [`validateSignature()`](../api-reference/validate-signature.md)
70 +manually with the raw body.
71 +
72 +About building webhook server, please refer to [Webhook](../guide/webhook.md).
1 +# `new OAuth()`
2 +
3 +`OAuth` is a class representing OAuth APIs. It provides methods
4 +corresponding to [messaging APIs](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token).
5 +
6 +#### Type signature
7 +
8 +``` typescript
9 +class OAuth {
10 + constructor() {}
11 +
12 + issueAccessToken(client_id: string, client_secret: string): Promise<Types.ChannelAccessToken>
13 + revokeAccessToken(access_token: string): Promise<{}>
14 + issueChannelAccessTokenV2_1(
15 + client_assertion: string,
16 + ): Promise<Types.ChannelAccessToken>
17 + getChannelAccessTokenKeyIdsV2_1(
18 + client_assertion: string,
19 + ): Promise<{ key_ids: string[] }>
20 + revokeChannelAccessTokenV2_1(
21 + client_id: string,
22 + client_secret: string,
23 + access_token: string,
24 + ): Promise<{}>
25 +}
26 +```
27 +
28 +## Create a OAuth
29 +
30 +The `OAuth` class is provided by the main module.
31 +
32 +``` js
33 +// CommonJS
34 +const { OAuth } = require('@line/bot-sdk');
35 +
36 +// ES6 modules or TypeScript
37 +import { OAuth } from '@line/bot-sdk';
38 +```
39 +
40 +To create a client instance:
41 +
42 +```js
43 +const oauth = new OAuth();
44 +```
45 +
46 +And now you can call client functions as usual:
47 +
48 +``` js
49 +const { access_token } = await oauth.issueAccessToken("client_id", "client_secret");
50 +```
51 +
52 +## Methods
53 +
54 +For functions returning `Promise`, there will be errors thrown if something
55 +goes wrong, such as HTTP errors or parsing errors. You can catch them with the
56 +`.catch()` method of the promises. The detailed error handling is explained
57 +in [the Client guide](../guide/client.md).
58 +
59 +### OAuth
60 +
61 +#### `issueAccessToken(client_id: string, client_secret: string): Promise<Types.ChannelAccessToken>`
62 +
63 +It corresponds to the [Issue channel access token](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token) API.
64 +
65 +``` js
66 +const { access_token, expires_in, token_type } = await oauth.issueAccessToken("client_id", "client_secret");
67 +```
68 +
69 +#### `revokeAccessToken(access_token: string): Promise<{}>`
70 +
71 +It corresponds to the [Revoke channel access token](https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token) API.
72 +
73 +
74 +``` js
75 +await oauth.revokeAccessToken("access_token");
76 +```
77 +
78 +#### issueChannelAccessTokenV2_1(client_assertion: string): Promise<Types.ChannelAccessToken>
79 +
80 +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.
81 +
82 +#### getChannelAccessTokenKeyIdsV2_1(client_assertion: string): Promise<{ key_ids: string[] }>
83 +
84 +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.
85 +
86 +#### revokeChannelAccessTokenV2_1(client_id: string, client_secret: string, access_token: string): Promise<{}>
87 +
88 +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.
1 +# `validateSignature(body, channelSecret, signature)`
2 +
3 +It is a function to check if a provided channel secret is valid, compared with a
4 +provided body.
5 +
6 +#### Type signature
7 +
8 +``` typescript
9 +function validateSignature(
10 + body: string | Buffer,
11 + channelSecret: string,
12 + signature: string,
13 +): boolean
14 +```
15 +
16 +`body` can be a string or buffer. When it's a string, it will be handled as if
17 +it's encoded in UTF-8.
18 +
19 +For more details about signature validation of LINE webhook, please refer
20 +to [the official documentation](https://developers.line.biz/en/reference/messaging-api/#webhooks).
1 +# Getting Started
2 +
3 +* [Requirements](getting-started/requirements.md)
4 +* [Install](getting-started/install.md)
5 +* [Basic Usage](getting-started/basic-usage.md)
1 +# Basic Usage
2 +
3 +It can be imported with [CommonJS](https://nodejs.org/docs/latest/api/modules.html),
4 +[ES2015 modules](https://babeljs.io/learn-es2015/#ecmascript-2015-features-modules),
5 +and preferably [TypeScript](https://www.typescriptlang.org/).
6 +
7 +The library is written in TypeScript and includes TypeScript definitions by
8 +default. Nevertheless, it can surely be used with plain JavaScript too.
9 +
10 +``` js
11 +// CommonJS
12 +const line = require('@line/bot-sdk');
13 +
14 +// ES2015 modules or TypeScript
15 +import * as line from '@line/bot-sdk';
16 +```
17 +
18 +## Configuration
19 +
20 +For the usage of webhook and client, LINE channel access token and secret are
21 +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/).
22 +
23 +``` js
24 +const config = {
25 + channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
26 + channelSecret: 'YOUR_CHANNEL_SECRET'
27 +};
28 +
29 +new line.Client(config);
30 +line.middleware(config);
31 +```
32 +
33 +## Synopsis
34 +
35 +Here is a synopsis of echoing webhook server with [Express](https://expressjs.com/):
36 +
37 +``` js
38 +const express = require('express');
39 +const line = require('@line/bot-sdk');
40 +
41 +const config = {
42 + channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
43 + channelSecret: 'YOUR_CHANNEL_SECRET'
44 +};
45 +
46 +const app = express();
47 +app.post('/webhook', line.middleware(config), (req, res) => {
48 + Promise
49 + .all(req.body.events.map(handleEvent))
50 + .then((result) => res.json(result));
51 +});
52 +
53 +const client = new line.Client(config);
54 +function handleEvent(event) {
55 + if (event.type !== 'message' || event.message.type !== 'text') {
56 + return Promise.resolve(null);
57 + }
58 +
59 + return client.replyMessage(event.replyToken, {
60 + type: 'text',
61 + text: event.message.text
62 + });
63 +}
64 +
65 +app.listen(3000);
66 +```
67 +
68 +The full examples with comments can be found in [examples](https://github.com/line/line-bot-sdk-nodejs/tree/master/examples/).
69 +
70 +For the specifications of API, please refer to [API Reference](../api-reference.md).
1 +# Install
2 +
3 +Please install via [npm](https://www.npmjs.com/).
4 +
5 +```bash
6 +$ npm install @line/bot-sdk
7 +```
8 +
9 +You can build from source. Please clone the repository and run the following
10 +scripts to build.
11 +
12 +``` bash
13 +$ git clone https://github.com/line/line-bot-sdk-nodejs
14 +$ cd line-bot-sdk-nodejs
15 +$ npm install
16 +$ npm run build
17 +```
18 +
19 +The built result will be placed in `dist/`.
20 +
21 +For the details of development, please refer to [Contributing](../../CONTRIBUTING.md).
1 +# Requirements
2 +
3 +* **Node.js** >= 4, preferably >=6
4 + * It uses ES2015.
5 +* [**npm**](https://www.npmjs.com/), preferably >=5
6 +
7 +Other dependencies are installed via npm, and do not need to be pre-installed.
1 +# Guide
2 +
3 +* [Webhook](guide/webhook.md)
4 +* [Client](guide/client.md)
5 +* [TypeScript](guide/typescript.md)
1 +# Client
2 +
3 +Client is to send messages, get user or content information, or leave chats.
4 +A client instance provides functions for [messaging APIs](https://developers.line.biz/en/reference/messaging-api/),
5 +so that you do not need to worry about HTTP requests and can focus on data.
6 +For type signatures of the methods, please refer to [its API reference](../api-reference/client.md).
7 +
8 +## Create a client
9 +
10 +The `Client` class is provided by the main module.
11 +
12 +``` js
13 +// CommonJS
14 +const Client = require('@line/bot-sdk').Client;
15 +
16 +// ES6 modules or TypeScript
17 +import { Client } from '@line/bot-sdk';
18 +```
19 +
20 +To create a client instance:
21 +
22 +```js
23 +const client = new Client({
24 + channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
25 + channelSecret: 'YOUR_CHANNEL_SECRET'
26 +});
27 +```
28 +
29 +And now you can call client functions as usual:
30 +
31 +``` js
32 +client.pushMessage(userId, { type: 'text', text: 'hello, world' });
33 +```
34 +
35 +## Retrieving parameters from webhook
36 +
37 +Many of data used in the client functions, such as user IDs or reply tokens, can
38 +be obtained from nowhere but webhook.
39 +
40 +Webhook event objects are just plain JSON objects, sent as request body, so you
41 +can easily access and use it.
42 +
43 +``` js
44 +const event = req.body.events[0];
45 +
46 +if (event.type === 'message') {
47 + const message = event.message;
48 +
49 + if (message.type === 'text' && message.text === 'bye') {
50 + if (event.source.type === 'room') {
51 + client.leaveRoom(event.source.roomId);
52 + } else if (event.source.type === 'group') {
53 + client.leaveGroup(event.source.groupId);
54 + } else {
55 + client.replyMessage(event.replyToken, {
56 + type: 'text',
57 + text: 'I cannot leave a 1-on-1 chat!',
58 + });
59 + }
60 + }
61 +}
62 +```
63 +
64 +For more detail of building webhook and retrieve event objects, please refer to
65 +its [guide](./webhook.html).
66 +
67 +## Error handling
68 +
69 +There are 4 types of errors caused by client usage.
70 +
71 +- `RequestError`: A request fails by, for example, wrong domain or server
72 + refusal.
73 +- `ReadError`: Reading from a response pipe fails.
74 +- `HTTPError`: Server returns a non-2xx response.
75 +- `JSONParseError`: JSON parsing fails for response body.
76 +
77 +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)
78 +method. For others returning `ReadableStream`, you can observe the `'error'`
79 +event for the stream.
80 +
81 +``` js
82 +client
83 + .replyMessage(replyToken, message)
84 + .catch((err) => {
85 + if (err instanceof HTTPError) {
86 + console.error(err.statusCode);
87 + }
88 + });
89 +
90 +const stream = client.getMessageContent(messageId);
91 +stream.on('error', (err) => {
92 + console.log(err.message);
93 +});
94 +```
95 +
96 +You can check which method returns `Promise` or `ReadableStream` in the API
97 +reference of [`Client`](../api-reference/client.md). For type signatures of the
98 +errors above, please refer to [Exceptions](../api-reference/exceptions.md).
1 +# TypeScript
2 +
3 +[TypeScript](https://www.typescriptlang.org/) is a statically typed language
4 +that compiled to plain JavaScript. As you may already have found, This library
5 +is written in TypeScript.
6 +
7 +When installed via npm, the built JavaScript files are already included and
8 +you do not need to worry about TypeScript, but it may be nice to consider
9 +using TypeScript for implement what you need.
10 +
11 +## What's good about using TypeScript
12 +
13 +It provides a default type set for mostly used objects in webhook and client
14 +and prevent possible typo and mistakes.
15 +
16 +``` typescript
17 +const config = {
18 + channelAccessToken: "", // typo Token
19 + channelSecret: "",
20 +}
21 +
22 +const c = new Client(config) // will throw a compile error
23 +```
24 +
25 +Also, when building a complex message object, you can make use of types for
26 +its fields.
27 +
28 +``` typescript
29 +const message: TemplateMessage = {
30 + type: "template",
31 + altText: "cannot display template message",
32 + template: {
33 + type: "carousel",
34 + columns: [ {
35 + text: "col1",
36 + title: "Column 1",
37 + actions: [ {
38 + type: "message",
39 + label: "send message",
40 + text: "hi, there",
41 + } ],
42 + } ],
43 + },
44 +}
45 +```
46 +
47 +The object above will be type-checked to have the type of
48 +`TemplateMessage`, and thus ensured not to miss any required field.
49 +
50 +Also, [literal type](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
51 +is used for `type` fields, which means the compiler will complain if a wrong
52 +type string is used, and also inference the type of objects by its `type` field.
53 +
54 +## How to use
55 +
56 +The library is built to just-work with TypeScript too, so import the library and
57 +there you go.
58 +
59 +``` typescript
60 +import {
61 + // main APIs
62 + Client,
63 + middleware,
64 +
65 + // exceptions
66 + JSONParseError,
67 + SignatureValidationFailed,
68 +
69 + // types
70 + TemplateMessage,
71 + WebhookEvent,
72 +} from "@line/bot-sdk";
73 +```
74 +
75 +Message object and webhook event types can be also imported from `@line/bot-sdk`,
76 +e.g. `TemplateMessage` or `WebhookEvent`. For declarations of the types, please
77 +refer to [types.ts](https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts).
1 +# Webhook
2 +
3 +A webhook server for LINE messaging API is just a plain HTTP(S) server. When
4 +there is a observable user event, an HTTP request will be sent to a
5 +pre-configured webhook server.
6 +
7 +About configuration of webhook itself, please refer to [Webhook](https://developers.line.biz/en/reference/messaging-api/#webhooks)
8 +of the official document.
9 +
10 +## What a webhook server should do
11 +
12 +- [Signature validation](https://developers.line.biz/en/reference/messaging-api/#signature-validation)
13 +- [Webhook event object parsing](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
14 +
15 +**Signature validation** is checking if a request is actually sent from real
16 +LINE servers, not a fraud. The validation is conducted by checking
17 +the [X-Line-Signature](https://developers.line.biz/en/reference/messaging-api/#signature-validation) header
18 +and request body. There is a [`validateSignature()`](../api-reference/validate-signature.md)
19 +function to do this.
20 +
21 +**Webhook event object parsing** is literally parsing webhook event objects,
22 +which contains information of each webhook event. The objects are provided as
23 +request body in JSON format, so any body parser will work here. For interal
24 +object types in this SDK, please refer to [Message and event objects](../api-reference/message-and-event-objects.md).
25 +
26 +There is a function to generate a [connect](https://github.com/senchalabs/connect) middleware,
27 +[`middleware()`](../api-reference/middleware.md), to conduct both of them. If
28 +your server can make use of connect middlewares, such as [Express](https://expressjs.com/),
29 +using the middleware is a recommended way to build a webhook server.
30 +
31 +## Build a webhook server with Express
32 +
33 +[Express](https://expressjs.com/) is a minimal web framework for Node.js, which
34 +is widely used in Node.js communities. You can surely build a webhook server
35 +with any web framework, but we use Express as an example here for its
36 +popularity.
37 +
38 +We skip the detailed guide for Express. If more information is needed about
39 +Express, please refer to its documentation.
40 +
41 +Here is an example of an HTTP server built with Express.
42 +
43 +``` js
44 +const express = require('express')
45 +
46 +const app = express()
47 +
48 +app.post('/webhook', (req, res) => {
49 + res.json({})
50 +})
51 +
52 +app.listen(8080)
53 +```
54 +
55 +The server above listens to 8080 and will response with an empty object for
56 +`POST /webhook`. We will add webhook functionality to this server.
57 +
58 +``` js
59 +const express = require('express')
60 +const middleware = require('@line/bot-sdk').middleware
61 +
62 +const app = express()
63 +
64 +const config = {
65 + channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
66 + channelSecret: 'YOUR_CHANNEL_SECRET'
67 +}
68 +
69 +app.post('/webhook', middleware(config), (req, res) => {
70 + req.body.events // webhook event objects
71 + req.body.destination // user ID of the bot (optional)
72 + ...
73 +})
74 +
75 +app.listen(8080)
76 +```
77 +
78 +We have imported `middleware` from the package and make the Express app to use
79 +the middleware. The middlware validates the request and parses webhook event
80 +object. It embeds body-parser and parses them to objects. If you have a reason
81 +to use another body-parser separately for other routes, please keep in mind the
82 +followings.
83 +
84 +### Do not use the webhook `middleware()` for other usual routes
85 +
86 +``` js
87 +// don't
88 +app.use(middleware(config))
89 +
90 +// do
91 +app.use('/webhook', middleware(config))
92 +```
93 +
94 +The middleware will throw an exception when the [X-Line-Signature](https://developers.line.biz/en/reference/messaging-api/#signature-validation)
95 +header is not set. If you want to handle usual user requests, the middleware
96 +shouldn't be used for them.
97 +
98 +### Do not use another body-parser before the webhook `middleware()`
99 +
100 +``` js
101 +// don't
102 +app.use(bodyParser.json())
103 +app.use('/webhook', middleware(config))
104 +
105 +// do
106 +app.use('/webhook', middleware(config))
107 +app.use(bodyParser.json())
108 +```
109 +
110 +If another body parser already parsed a request's body, the webhook middleware
111 +cannot access to the raw body of the request. The raw body should be retrieved
112 +for signature validation.
113 +
114 +However, there are environments where `req.body` is pre-parsed, such as
115 +[Firebase Cloud Functions](https://firebase.google.com/docs/functions/http-events).
116 +If it parses the body into string or buffer, the middleware will use the body
117 +as it is and work just fine. If the pre-parsed body is an object, the webhook
118 +middleware will fail to work. In the case, please use [`validateSignature()`](../api-reference/validate-signature.md)
119 +manually with raw body.
120 +
121 +## Error handling
122 +
123 +There are two types of errors thrown by the middleware, one is `SignatureValidationFailed`
124 +and the other is `JSONParseError`.
125 +
126 +- `SignatureValidationFailed` is thrown when a request doesn't have a signature.
127 +- `SignatureValidationFailed` is thrown when a request has a wrong signature.
128 +- `JSONParseError` occurs when a request body cannot be parsed as JSON.
129 +
130 +For type references of the errors, please refer to [the API reference](../api-reference/exceptions.md).
131 +
132 +The errors can be handled with [error middleware](https://github.com/senchalabs/connect#error-middleware).
133 +
134 +``` js
135 +const express = require('express')
136 +const middleware = require('@line/bot-sdk').middleware
137 +const JSONParseError = require('@line/bot-sdk').JSONParseError
138 +const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed
139 +
140 +const app = express()
141 +
142 +const config = {
143 + channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
144 + channelSecret: 'YOUR_CHANNEL_SECRET'
145 +}
146 +
147 +app.use(middleware(config))
148 +
149 +app.post('/webhook', (req, res) => {
150 + res.json(req.body.events) // req.body will be webhook event object
151 +})
152 +
153 +app.use((err, req, res, next) => {
154 + if (err instanceof SignatureValidationFailed) {
155 + res.status(401).send(err.signature)
156 + return
157 + } else if (err instanceof JSONParseError) {
158 + res.status(400).send(err.raw)
159 + return
160 + }
161 + next(err) // will throw default 500
162 +})
163 +
164 +app.listen(8080)
165 +```
166 +
167 +## HTTPS
168 +
169 +The webhook URL should have HTTPS protocol. There are several ways to build an
170 +HTTPS server. For example, here is a [documentation](https://expressjs.com/en/api.html#app.listen)
171 +of making Express work with HTTPS. You can also set HTTPS in web servers like
172 +[NGINX](https://www.nginx.com/). This guide will not cover HTTPS configuration,
173 +but do not forget to set HTTPS beforehand.
174 +
175 +For development and test usages, [ngrok](https://ngrok.com/) works perfectly.
1 +# Dependencies
2 +node_modules/
3 +
4 +# Built files.
5 +dist/
1 +# LINE Echo Bot with TypeScript
2 +
3 +An example LINE bot to echo message with TypeScript. The bot is coded according to TypeScript's best practices.
4 +
5 +## Prerequisite
6 +
7 +- Git
8 +- Node.js version 10 and up
9 +- Heroku CLI (optional)
10 +- LINE Developers Account for the bot
11 +
12 +## Installation
13 +
14 +- Clone the repository.
15 +
16 +```bash
17 +git clone https://github.com/line/line-bot-sdk-nodejs.git
18 +```
19 +
20 +- Change directory to the example.
21 +
22 +```bash
23 +cd line-bot-sdk-nodejs/examples/echo-bot-ts
24 +```
25 +
26 +- Install all dependencies.
27 +
28 +```bash
29 +npm install
30 +```
31 +
32 +- Configure all of the environment variables.
33 +
34 +```bash
35 +export CHANNEL_ACCESS_TOKEN=<YOUR_CHANNEL_ACCESS_TOKEN>
36 +export CHANNEL_SECRET=<YOUR_CHANNEL_SECRET>
37 +export PORT=<YOUR_PORT>
38 +```
39 +
40 +- 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.
41 +
42 +```bash
43 +https://example-url.com/webhook
44 +```
45 +
46 +- Compile the TypeScript files.
47 +
48 +```bash
49 +npm run build
50 +```
51 +
52 +- Run the application.
53 +
54 +```bash
55 +npm start
56 +```
57 +
58 +## Alternative Installation
59 +
60 +If you want to deploy it via Heroku, it is also possible and is even easier for testing purposes.
61 +
62 +- Clone the repository.
63 +
64 +```bash
65 +git clone https://github.com/line/line-bot-sdk-nodejs.git
66 +```
67 +
68 +- Change directory to the example.
69 +
70 +```bash
71 +cd line-bot-sdk-nodejs/examples/echo-bot-ts
72 +```
73 +
74 +- Create a Heroku application.
75 +
76 +```bash
77 +git init
78 +heroku create <YOUR_APP_NAME> # Do not specify for a random name
79 +```
80 +
81 +- 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`.
82 +
83 +```bash
84 +heroku config:set CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
85 +heroku config:set CHANNEL_SECRET=YOUR_CHANNEL_SECRET
86 +```
87 +
88 +- Push the application to the server.
89 +
90 +```bash
91 +git add .
92 +git commit -m "Initial commit for Heroku testing"
93 +git push heroku master
94 +```
95 +
96 +- Open your application.
97 +
98 +```bash
99 +heroku open
100 +```
101 +
102 +- Done!
1 +declare global {
2 + namespace NodeJS {
3 + interface ProcessEnv {
4 + CHANNEL_ACCESS_TOKEN: string;
5 + CHANNEL_SECRET: string;
6 + PORT: string;
7 + }
8 + }
9 +}
10 +
11 +export {};
1 +// Import all dependencies, mostly using destructuring for better view.
2 +import { ClientConfig, Client, middleware, MiddlewareConfig, WebhookEvent, TextMessage, MessageAPIResponseBase } from '@line/bot-sdk';
3 +import express, { Application, Request, Response } from 'express';
4 +
5 +// Setup all LINE client and Express configurations.
6 +const clientConfig: ClientConfig = {
7 + channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
8 + channelSecret: process.env.CHANNEL_SECRET,
9 +};
10 +
11 +const middlewareConfig: MiddlewareConfig = {
12 + channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
13 + channelSecret: process.env.CHANNEL_SECRET || '',
14 +};
15 +
16 +const PORT = process.env.PORT || 3000;
17 +
18 +// Create a new LINE SDK client.
19 +const client = new Client(clientConfig);
20 +
21 +// Create a new Express application.
22 +const app: Application = express();
23 +
24 +// Function handler to receive the text.
25 +const textEventHandler = async (event: WebhookEvent): Promise<MessageAPIResponseBase | undefined> => {
26 + // Process all variables here.
27 + if (event.type !== 'message' || event.message.type !== 'text') {
28 + return;
29 + }
30 +
31 + // Process all message related variables here.
32 + const { replyToken } = event;
33 + const { text } = event.message;
34 +
35 + // Create a new message.
36 + const response: TextMessage = {
37 + type: 'text',
38 + text,
39 + };
40 +
41 + // Reply to the user.
42 + await client.replyMessage(replyToken, response);
43 +};
44 +
45 +// Register the LINE middleware.
46 +// As an alternative, you could also pass the middleware in the route handler, which is what is used here.
47 +// app.use(middleware(middlewareConfig));
48 +
49 +// Route handler to receive webhook events.
50 +// This route is used to receive connection tests.
51 +app.get(
52 + '/',
53 + async (_: Request, res: Response): Promise<Response> => {
54 + return res.status(200).json({
55 + status: 'success',
56 + message: 'Connected successfully!',
57 + });
58 + }
59 +);
60 +
61 +// This route is used for the Webhook.
62 +app.post(
63 + '/webhook',
64 + middleware(middlewareConfig),
65 + async (req: Request, res: Response): Promise<Response> => {
66 + const events: WebhookEvent[] = req.body.events;
67 +
68 + // Process all of the received events asynchronously.
69 + const results = await Promise.all(
70 + events.map(async (event: WebhookEvent) => {
71 + try {
72 + await textEventHandler(event);
73 + } catch (err: unknown) {
74 + if (err instanceof Error) {
75 + console.error(err);
76 + }
77 +
78 + // Return an error message.
79 + return res.status(500).json({
80 + status: 'error',
81 + });
82 + }
83 + })
84 + );
85 +
86 + // Return a successfull message.
87 + return res.status(200).json({
88 + status: 'success',
89 + results,
90 + });
91 + }
92 +);
93 +
94 +// Create a server and listen to it.
95 +app.listen(PORT, () => {
96 + console.log(`Application is live and listening on port ${PORT}`);
97 +});
1 +{
2 + "name": "echo-bot-ts",
3 + "version": "0.0.0",
4 + "lockfileVersion": 1,
5 + "requires": true,
6 + "dependencies": {
7 + "@line/bot-sdk": {
8 + "version": "7.2.0",
9 + "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-7.2.0.tgz",
10 + "integrity": "sha512-TW9BB1Om1lFWWo3sw9A4kx+9ZTHBjfqIj6NmUY9md+DDR5Z4X98wEeM3l74mkrbZ7naK6AtRgUw5UIaKQgi3nA==",
11 + "requires": {
12 + "@types/body-parser": "^1.19.0",
13 + "@types/node": "^14.10.0",
14 + "axios": "^0.20.0",
15 + "body-parser": "^1.19.0",
16 + "file-type": "^15.0.0",
17 + "form-data": "^3.0.0"
18 + }
19 + },
20 + "@tokenizer/token": {
21 + "version": "0.1.1",
22 + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
23 + "integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
24 + },
25 + "@types/body-parser": {
26 + "version": "1.19.0",
27 + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
28 + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
29 + "requires": {
30 + "@types/connect": "*",
31 + "@types/node": "*"
32 + }
33 + },
34 + "@types/connect": {
35 + "version": "3.4.34",
36 + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
37 + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
38 + "requires": {
39 + "@types/node": "*"
40 + }
41 + },
42 + "@types/debug": {
43 + "version": "4.1.5",
44 + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
45 + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
46 + },
47 + "@types/express": {
48 + "version": "4.17.9",
49 + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz",
50 + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==",
51 + "dev": true,
52 + "requires": {
53 + "@types/body-parser": "*",
54 + "@types/express-serve-static-core": "*",
55 + "@types/qs": "*",
56 + "@types/serve-static": "*"
57 + }
58 + },
59 + "@types/express-serve-static-core": {
60 + "version": "4.17.17",
61 + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz",
62 + "integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==",
63 + "dev": true,
64 + "requires": {
65 + "@types/node": "*",
66 + "@types/qs": "*",
67 + "@types/range-parser": "*"
68 + }
69 + },
70 + "@types/mime": {
71 + "version": "2.0.3",
72 + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
73 + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
74 + "dev": true
75 + },
76 + "@types/node": {
77 + "version": "14.14.14",
78 + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
79 + "integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ=="
80 + },
81 + "@types/qs": {
82 + "version": "6.9.5",
83 + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
84 + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
85 + "dev": true
86 + },
87 + "@types/range-parser": {
88 + "version": "1.2.3",
89 + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
90 + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
91 + "dev": true
92 + },
93 + "@types/serve-static": {
94 + "version": "1.13.8",
95 + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
96 + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
97 + "dev": true,
98 + "requires": {
99 + "@types/mime": "*",
100 + "@types/node": "*"
101 + }
102 + },
103 + "accepts": {
104 + "version": "1.3.7",
105 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
106 + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
107 + "requires": {
108 + "mime-types": "~2.1.24",
109 + "negotiator": "0.6.2"
110 + }
111 + },
112 + "array-flatten": {
113 + "version": "1.1.1",
114 + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
115 + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
116 + },
117 + "asynckit": {
118 + "version": "0.4.0",
119 + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
120 + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
121 + },
122 + "axios": {
123 + "version": "0.20.0",
124 + "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
125 + "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
126 + "requires": {
127 + "follow-redirects": "^1.10.0"
128 + }
129 + },
130 + "balanced-match": {
131 + "version": "1.0.0",
132 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
133 + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
134 + "dev": true
135 + },
136 + "body-parser": {
137 + "version": "1.19.0",
138 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
139 + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
140 + "requires": {
141 + "bytes": "3.1.0",
142 + "content-type": "~1.0.4",
143 + "debug": "2.6.9",
144 + "depd": "~1.1.2",
145 + "http-errors": "1.7.2",
146 + "iconv-lite": "0.4.24",
147 + "on-finished": "~2.3.0",
148 + "qs": "6.7.0",
149 + "raw-body": "2.4.0",
150 + "type-is": "~1.6.17"
151 + }
152 + },
153 + "brace-expansion": {
154 + "version": "1.1.11",
155 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
156 + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
157 + "dev": true,
158 + "requires": {
159 + "balanced-match": "^1.0.0",
160 + "concat-map": "0.0.1"
161 + }
162 + },
163 + "bytes": {
164 + "version": "3.1.0",
165 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
166 + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
167 + },
168 + "combined-stream": {
169 + "version": "1.0.8",
170 + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
171 + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
172 + "requires": {
173 + "delayed-stream": "~1.0.0"
174 + }
175 + },
176 + "concat-map": {
177 + "version": "0.0.1",
178 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
179 + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
180 + "dev": true
181 + },
182 + "content-disposition": {
183 + "version": "0.5.3",
184 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
185 + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
186 + "requires": {
187 + "safe-buffer": "5.1.2"
188 + }
189 + },
190 + "content-type": {
191 + "version": "1.0.4",
192 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
193 + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
194 + },
195 + "cookie": {
196 + "version": "0.4.0",
197 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
198 + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
199 + },
200 + "cookie-signature": {
201 + "version": "1.0.6",
202 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
203 + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
204 + },
205 + "debug": {
206 + "version": "2.6.9",
207 + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
208 + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
209 + "requires": {
210 + "ms": "2.0.0"
211 + }
212 + },
213 + "delayed-stream": {
214 + "version": "1.0.0",
215 + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
216 + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
217 + },
218 + "depd": {
219 + "version": "1.1.2",
220 + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
221 + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
222 + },
223 + "destroy": {
224 + "version": "1.0.4",
225 + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
226 + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
227 + },
228 + "ee-first": {
229 + "version": "1.1.1",
230 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
231 + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
232 + },
233 + "encodeurl": {
234 + "version": "1.0.2",
235 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
236 + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
237 + },
238 + "escape-html": {
239 + "version": "1.0.3",
240 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
241 + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
242 + },
243 + "etag": {
244 + "version": "1.8.1",
245 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
246 + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
247 + },
248 + "express": {
249 + "version": "4.17.1",
250 + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
251 + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
252 + "requires": {
253 + "accepts": "~1.3.7",
254 + "array-flatten": "1.1.1",
255 + "body-parser": "1.19.0",
256 + "content-disposition": "0.5.3",
257 + "content-type": "~1.0.4",
258 + "cookie": "0.4.0",
259 + "cookie-signature": "1.0.6",
260 + "debug": "2.6.9",
261 + "depd": "~1.1.2",
262 + "encodeurl": "~1.0.2",
263 + "escape-html": "~1.0.3",
264 + "etag": "~1.8.1",
265 + "finalhandler": "~1.1.2",
266 + "fresh": "0.5.2",
267 + "merge-descriptors": "1.0.1",
268 + "methods": "~1.1.2",
269 + "on-finished": "~2.3.0",
270 + "parseurl": "~1.3.3",
271 + "path-to-regexp": "0.1.7",
272 + "proxy-addr": "~2.0.5",
273 + "qs": "6.7.0",
274 + "range-parser": "~1.2.1",
275 + "safe-buffer": "5.1.2",
276 + "send": "0.17.1",
277 + "serve-static": "1.14.1",
278 + "setprototypeof": "1.1.1",
279 + "statuses": "~1.5.0",
280 + "type-is": "~1.6.18",
281 + "utils-merge": "1.0.1",
282 + "vary": "~1.1.2"
283 + }
284 + },
285 + "file-type": {
286 + "version": "15.0.1",
287 + "resolved": "https://registry.npmjs.org/file-type/-/file-type-15.0.1.tgz",
288 + "integrity": "sha512-0LieQlSA3bWUdErNrxzxfI4rhsvNAVPBO06R8pTc1hp9SE6nhqlVyvhcaXoMmtXkBTPnQenbMPLW9X76hH76oQ==",
289 + "requires": {
290 + "readable-web-to-node-stream": "^2.0.0",
291 + "strtok3": "^6.0.3",
292 + "token-types": "^2.0.0",
293 + "typedarray-to-buffer": "^3.1.5"
294 + }
295 + },
296 + "finalhandler": {
297 + "version": "1.1.2",
298 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
299 + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
300 + "requires": {
301 + "debug": "2.6.9",
302 + "encodeurl": "~1.0.2",
303 + "escape-html": "~1.0.3",
304 + "on-finished": "~2.3.0",
305 + "parseurl": "~1.3.3",
306 + "statuses": "~1.5.0",
307 + "unpipe": "~1.0.0"
308 + }
309 + },
310 + "follow-redirects": {
311 + "version": "1.13.1",
312 + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
313 + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
314 + },
315 + "form-data": {
316 + "version": "3.0.0",
317 + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
318 + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
319 + "requires": {
320 + "asynckit": "^0.4.0",
321 + "combined-stream": "^1.0.8",
322 + "mime-types": "^2.1.12"
323 + }
324 + },
325 + "forwarded": {
326 + "version": "0.1.2",
327 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
328 + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
329 + },
330 + "fresh": {
331 + "version": "0.5.2",
332 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
333 + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
334 + },
335 + "fs.realpath": {
336 + "version": "1.0.0",
337 + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
338 + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
339 + "dev": true
340 + },
341 + "glob": {
342 + "version": "7.1.6",
343 + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
344 + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
345 + "dev": true,
346 + "requires": {
347 + "fs.realpath": "^1.0.0",
348 + "inflight": "^1.0.4",
349 + "inherits": "2",
350 + "minimatch": "^3.0.4",
351 + "once": "^1.3.0",
352 + "path-is-absolute": "^1.0.0"
353 + }
354 + },
355 + "http-errors": {
356 + "version": "1.7.2",
357 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
358 + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
359 + "requires": {
360 + "depd": "~1.1.2",
361 + "inherits": "2.0.3",
362 + "setprototypeof": "1.1.1",
363 + "statuses": ">= 1.5.0 < 2",
364 + "toidentifier": "1.0.0"
365 + }
366 + },
367 + "iconv-lite": {
368 + "version": "0.4.24",
369 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
370 + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
371 + "requires": {
372 + "safer-buffer": ">= 2.1.2 < 3"
373 + }
374 + },
375 + "ieee754": {
376 + "version": "1.2.1",
377 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
378 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
379 + },
380 + "inflight": {
381 + "version": "1.0.6",
382 + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
383 + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
384 + "dev": true,
385 + "requires": {
386 + "once": "^1.3.0",
387 + "wrappy": "1"
388 + }
389 + },
390 + "inherits": {
391 + "version": "2.0.3",
392 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
393 + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
394 + },
395 + "ipaddr.js": {
396 + "version": "1.9.1",
397 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
398 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
399 + },
400 + "is-typedarray": {
401 + "version": "1.0.0",
402 + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
403 + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
404 + },
405 + "media-typer": {
406 + "version": "0.3.0",
407 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
408 + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
409 + },
410 + "merge-descriptors": {
411 + "version": "1.0.1",
412 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
413 + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
414 + },
415 + "methods": {
416 + "version": "1.1.2",
417 + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
418 + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
419 + },
420 + "mime": {
421 + "version": "1.6.0",
422 + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
423 + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
424 + },
425 + "mime-db": {
426 + "version": "1.44.0",
427 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
428 + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
429 + },
430 + "mime-types": {
431 + "version": "2.1.27",
432 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
433 + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
434 + "requires": {
435 + "mime-db": "1.44.0"
436 + }
437 + },
438 + "minimatch": {
439 + "version": "3.0.4",
440 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
441 + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
442 + "dev": true,
443 + "requires": {
444 + "brace-expansion": "^1.1.7"
445 + }
446 + },
447 + "ms": {
448 + "version": "2.0.0",
449 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
450 + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
451 + },
452 + "negotiator": {
453 + "version": "0.6.2",
454 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
455 + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
456 + },
457 + "on-finished": {
458 + "version": "2.3.0",
459 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
460 + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
461 + "requires": {
462 + "ee-first": "1.1.1"
463 + }
464 + },
465 + "once": {
466 + "version": "1.4.0",
467 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
468 + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
469 + "dev": true,
470 + "requires": {
471 + "wrappy": "1"
472 + }
473 + },
474 + "parseurl": {
475 + "version": "1.3.3",
476 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
477 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
478 + },
479 + "path-is-absolute": {
480 + "version": "1.0.1",
481 + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
482 + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
483 + "dev": true
484 + },
485 + "path-to-regexp": {
486 + "version": "0.1.7",
487 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
488 + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
489 + },
490 + "peek-readable": {
491 + "version": "3.1.0",
492 + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz",
493 + "integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA=="
494 + },
495 + "proxy-addr": {
496 + "version": "2.0.6",
497 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
498 + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
499 + "requires": {
500 + "forwarded": "~0.1.2",
501 + "ipaddr.js": "1.9.1"
502 + }
503 + },
504 + "qs": {
505 + "version": "6.7.0",
506 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
507 + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
508 + },
509 + "range-parser": {
510 + "version": "1.2.1",
511 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
512 + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
513 + },
514 + "raw-body": {
515 + "version": "2.4.0",
516 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
517 + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
518 + "requires": {
519 + "bytes": "3.1.0",
520 + "http-errors": "1.7.2",
521 + "iconv-lite": "0.4.24",
522 + "unpipe": "1.0.0"
523 + }
524 + },
525 + "readable-web-to-node-stream": {
526 + "version": "2.0.0",
527 + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz",
528 + "integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA=="
529 + },
530 + "rimraf": {
531 + "version": "3.0.2",
532 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
533 + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
534 + "dev": true,
535 + "requires": {
536 + "glob": "^7.1.3"
537 + }
538 + },
539 + "safe-buffer": {
540 + "version": "5.1.2",
541 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
542 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
543 + },
544 + "safer-buffer": {
545 + "version": "2.1.2",
546 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
547 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
548 + },
549 + "send": {
550 + "version": "0.17.1",
551 + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
552 + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
553 + "requires": {
554 + "debug": "2.6.9",
555 + "depd": "~1.1.2",
556 + "destroy": "~1.0.4",
557 + "encodeurl": "~1.0.2",
558 + "escape-html": "~1.0.3",
559 + "etag": "~1.8.1",
560 + "fresh": "0.5.2",
561 + "http-errors": "~1.7.2",
562 + "mime": "1.6.0",
563 + "ms": "2.1.1",
564 + "on-finished": "~2.3.0",
565 + "range-parser": "~1.2.1",
566 + "statuses": "~1.5.0"
567 + },
568 + "dependencies": {
569 + "ms": {
570 + "version": "2.1.1",
571 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
572 + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
573 + }
574 + }
575 + },
576 + "serve-static": {
577 + "version": "1.14.1",
578 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
579 + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
580 + "requires": {
581 + "encodeurl": "~1.0.2",
582 + "escape-html": "~1.0.3",
583 + "parseurl": "~1.3.3",
584 + "send": "0.17.1"
585 + }
586 + },
587 + "setprototypeof": {
588 + "version": "1.1.1",
589 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
590 + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
591 + },
592 + "statuses": {
593 + "version": "1.5.0",
594 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
595 + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
596 + },
597 + "strtok3": {
598 + "version": "6.0.4",
599 + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.4.tgz",
600 + "integrity": "sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==",
601 + "requires": {
602 + "@tokenizer/token": "^0.1.1",
603 + "@types/debug": "^4.1.5",
604 + "peek-readable": "^3.1.0"
605 + }
606 + },
607 + "toidentifier": {
608 + "version": "1.0.0",
609 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
610 + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
611 + },
612 + "token-types": {
613 + "version": "2.0.0",
614 + "resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz",
615 + "integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==",
616 + "requires": {
617 + "@tokenizer/token": "^0.1.0",
618 + "ieee754": "^1.1.13"
619 + }
620 + },
621 + "type-is": {
622 + "version": "1.6.18",
623 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
624 + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
625 + "requires": {
626 + "media-typer": "0.3.0",
627 + "mime-types": "~2.1.24"
628 + }
629 + },
630 + "typedarray-to-buffer": {
631 + "version": "3.1.5",
632 + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
633 + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
634 + "requires": {
635 + "is-typedarray": "^1.0.0"
636 + }
637 + },
638 + "typescript": {
639 + "version": "4.1.3",
640 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
641 + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
642 + "dev": true
643 + },
644 + "unpipe": {
645 + "version": "1.0.0",
646 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
647 + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
648 + },
649 + "utils-merge": {
650 + "version": "1.0.1",
651 + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
652 + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
653 + },
654 + "vary": {
655 + "version": "1.1.2",
656 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
657 + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
658 + },
659 + "wrappy": {
660 + "version": "1.0.2",
661 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
662 + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
663 + "dev": true
664 + }
665 + }
666 +}
1 +{
2 + "name": "echo-bot-ts",
3 + "version": "0.0.0",
4 + "description": "An example LINE bot with TypeScript made to echo messages",
5 + "main": "./dist/index.js",
6 + "scripts": {
7 + "clean": "rimraf ./dist",
8 + "build": "npm run clean && tsc",
9 + "start": "node dist/index.js"
10 + },
11 + "author": "Nicholas Dwiarto <nicholasdwiarto@yahoo.com> (https://nicholasdw.com)",
12 + "dependencies": {
13 + "@line/bot-sdk": "^7.2.0",
14 + "express": "^4.17.1"
15 + },
16 + "devDependencies": {
17 + "@types/express": "^4.17.9",
18 + "@types/node": "^14.14.14",
19 + "rimraf": "^3.0.2",
20 + "typescript": "^4.1.3"
21 + }
22 +}
1 +{
2 + "compilerOptions": {
3 + /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 +
5 + /* Basic Options */
6 + // "incremental": true, /* Enable incremental compilation */
7 + "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
8 + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 + // "lib": [], /* Specify library files to be included in the compilation. */
10 + // "allowJs": true, /* Allow javascript files to be compiled. */
11 + // "checkJs": true, /* Report errors in .js files. */
12 + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 + // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 + // "sourceMap": true, /* Generates corresponding '.map' file. */
16 + // "outFile": "./", /* Concatenate and emit output to single file. */
17 + "outDir": "./dist", /* Redirect output structure to the directory. */
18 + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 + // "composite": true, /* Enable project compilation */
20 + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 + // "removeComments": true, /* Do not emit comments to output. */
22 + // "noEmit": true, /* Do not emit outputs. */
23 + // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 +
27 + /* Strict Type-Checking Options */
28 + "strict": true, /* Enable all strict type-checking options. */
29 + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 + // "strictNullChecks": true, /* Enable strict null checks. */
31 + // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 +
37 + /* Additional Checks */
38 + // "noUnusedLocals": true, /* Report errors on unused locals. */
39 + // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 +
44 + /* Module Resolution Options */
45 + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
46 + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
47 + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
48 + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
49 + // "typeRoots": [], /* List of folders to include type definitions from. */
50 + // "types": [], /* Type declaration files to be included in compilation. */
51 + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
52 + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
53 + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
54 + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
55 +
56 + /* Source Map Options */
57 + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
58 + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
59 + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
60 + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
61 +
62 + /* Experimental Options */
63 + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
64 + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
65 +
66 + /* Advanced Options */
67 + "skipLibCheck": true, /* Skip type checking of declaration files. */
68 + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
69 + },
70 + "exclude": ["node_modules"]
71 +}
1 +# Echo Bot
2 +
3 +An example LINE bot just to echo messages
4 +
5 +## How to use
6 +
7 +### Install deps
8 +
9 +``` shell
10 +$ npm install
11 +```
12 +
13 +### Configuration
14 +
15 +``` shell
16 +$ export CHANNEL_SECRET=YOUR_CHANNEL_SECRET
17 +$ export CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
18 +$ export PORT=1234
19 +```
20 +
21 +### Run
22 +
23 +``` shell
24 +$ node .
25 +```
26 +
27 +## Webhook URL
28 +
29 +```
30 +https://your.base.url/callback
31 +```
1 +'use strict';
2 +
3 +const line = require('@line/bot-sdk');
4 +const express = require('express');
5 +
6 +// create LINE SDK config from env variables
7 +const config = {
8 + channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
9 + channelSecret: process.env.CHANNEL_SECRET,
10 +};
11 +
12 +// create LINE SDK client
13 +const client = new line.Client(config);
14 +
15 +// create Express app
16 +// about Express itself: https://expressjs.com/
17 +const app = express();
18 +
19 +// register a webhook handler with middleware
20 +// about the middleware, please refer to doc
21 +app.post('/callback', line.middleware(config), (req, res) => {
22 + Promise
23 + .all(req.body.events.map(handleEvent))
24 + .then((result) => res.json(result))
25 + .catch((err) => {
26 + console.error(err);
27 + res.status(500).end();
28 + });
29 +});
30 +
31 +// event handler
32 +function handleEvent(event) {
33 + if (event.type !== 'message' || event.message.type !== 'text') {
34 + // ignore non-text-message event
35 + return Promise.resolve(null);
36 + }
37 +
38 + // create a echoing text message
39 + const echo = { type: 'text', text: event.message.text };
40 +
41 + // use reply API
42 + return client.replyMessage(event.replyToken, echo);
43 +}
44 +
45 +// listen on port
46 +const port = process.env.PORT || 3000;
47 +app.listen(port, () => {
48 + console.log(`listening on ${port}`);
49 +});
1 +{
2 + "name": "echo-bot",
3 + "version": "0.0.0",
4 + "lockfileVersion": 1,
5 + "requires": true,
6 + "dependencies": {
7 + "@line/bot-sdk": {
8 + "version": "6.8.0",
9 + "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-6.8.0.tgz",
10 + "integrity": "sha512-DoUdgBZKw/hFCubRvwcUFu8eV9uBPm4izlXTqVyT7melqq8pOxdR+74qOJ4HhQnd3/RQYdhWloiqyXH0FPHePw==",
11 + "requires": {
12 + "@types/body-parser": "^1.16.8",
13 + "@types/file-type": "^5.2.1",
14 + "@types/node": "^7.0.31",
15 + "axios": "^0.19.0",
16 + "body-parser": "^1.18.2",
17 + "file-type": "^7.2.0"
18 + }
19 + },
20 + "@types/body-parser": {
21 + "version": "1.17.0",
22 + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
23 + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
24 + "requires": {
25 + "@types/connect": "*",
26 + "@types/node": "*"
27 + }
28 + },
29 + "@types/connect": {
30 + "version": "3.4.32",
31 + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
32 + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
33 + "requires": {
34 + "@types/node": "*"
35 + }
36 + },
37 + "@types/file-type": {
38 + "version": "5.2.2",
39 + "resolved": "https://registry.npmjs.org/@types/file-type/-/file-type-5.2.2.tgz",
40 + "integrity": "sha512-GWtM4fyqfb+bec4ocpo51/y4x0b83Je+iA6eV131LT9wL0//G+1UgwbkMg7w61ceOwR+KkZXK00z44jrrNljWg==",
41 + "requires": {
42 + "@types/node": "*"
43 + }
44 + },
45 + "@types/node": {
46 + "version": "7.10.6",
47 + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.6.tgz",
48 + "integrity": "sha512-d0BOAicT0tEdbdVQlLGOVul1kvg6YvbaADRCThGCz5NJ0e9r00SofcR1x69hmlCyrHuB6jd4cKzL9bMLjPnpAA=="
49 + },
50 + "accepts": {
51 + "version": "1.3.5",
52 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
53 + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
54 + "requires": {
55 + "mime-types": "~2.1.18",
56 + "negotiator": "0.6.1"
57 + }
58 + },
59 + "array-flatten": {
60 + "version": "1.1.1",
61 + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
62 + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
63 + },
64 + "axios": {
65 + "version": "0.19.0",
66 + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
67 + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
68 + "requires": {
69 + "follow-redirects": "1.5.10",
70 + "is-buffer": "^2.0.2"
71 + }
72 + },
73 + "body-parser": {
74 + "version": "1.19.0",
75 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
76 + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
77 + "requires": {
78 + "bytes": "3.1.0",
79 + "content-type": "~1.0.4",
80 + "debug": "2.6.9",
81 + "depd": "~1.1.2",
82 + "http-errors": "1.7.2",
83 + "iconv-lite": "0.4.24",
84 + "on-finished": "~2.3.0",
85 + "qs": "6.7.0",
86 + "raw-body": "2.4.0",
87 + "type-is": "~1.6.17"
88 + },
89 + "dependencies": {
90 + "bytes": {
91 + "version": "3.1.0",
92 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
93 + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
94 + },
95 + "http-errors": {
96 + "version": "1.7.2",
97 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
98 + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
99 + "requires": {
100 + "depd": "~1.1.2",
101 + "inherits": "2.0.3",
102 + "setprototypeof": "1.1.1",
103 + "statuses": ">= 1.5.0 < 2",
104 + "toidentifier": "1.0.0"
105 + }
106 + },
107 + "mime-db": {
108 + "version": "1.40.0",
109 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
110 + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
111 + },
112 + "mime-types": {
113 + "version": "2.1.24",
114 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
115 + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
116 + "requires": {
117 + "mime-db": "1.40.0"
118 + }
119 + },
120 + "qs": {
121 + "version": "6.7.0",
122 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
123 + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
124 + },
125 + "setprototypeof": {
126 + "version": "1.1.1",
127 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
128 + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
129 + },
130 + "statuses": {
131 + "version": "1.5.0",
132 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
133 + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
134 + },
135 + "type-is": {
136 + "version": "1.6.18",
137 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
138 + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
139 + "requires": {
140 + "media-typer": "0.3.0",
141 + "mime-types": "~2.1.24"
142 + }
143 + }
144 + }
145 + },
146 + "bytes": {
147 + "version": "3.0.0",
148 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
149 + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
150 + },
151 + "content-disposition": {
152 + "version": "0.5.2",
153 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
154 + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
155 + },
156 + "content-type": {
157 + "version": "1.0.4",
158 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
159 + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
160 + },
161 + "cookie": {
162 + "version": "0.3.1",
163 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
164 + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
165 + },
166 + "cookie-signature": {
167 + "version": "1.0.6",
168 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
169 + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
170 + },
171 + "debug": {
172 + "version": "2.6.9",
173 + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
174 + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
175 + "requires": {
176 + "ms": "2.0.0"
177 + }
178 + },
179 + "depd": {
180 + "version": "1.1.2",
181 + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
182 + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
183 + },
184 + "destroy": {
185 + "version": "1.0.4",
186 + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
187 + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
188 + },
189 + "ee-first": {
190 + "version": "1.1.1",
191 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
192 + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
193 + },
194 + "encodeurl": {
195 + "version": "1.0.2",
196 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
197 + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
198 + },
199 + "escape-html": {
200 + "version": "1.0.3",
201 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
202 + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
203 + },
204 + "etag": {
205 + "version": "1.8.1",
206 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
207 + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
208 + },
209 + "express": {
210 + "version": "4.16.3",
211 + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
212 + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
213 + "requires": {
214 + "accepts": "~1.3.5",
215 + "array-flatten": "1.1.1",
216 + "body-parser": "1.18.2",
217 + "content-disposition": "0.5.2",
218 + "content-type": "~1.0.4",
219 + "cookie": "0.3.1",
220 + "cookie-signature": "1.0.6",
221 + "debug": "2.6.9",
222 + "depd": "~1.1.2",
223 + "encodeurl": "~1.0.2",
224 + "escape-html": "~1.0.3",
225 + "etag": "~1.8.1",
226 + "finalhandler": "1.1.1",
227 + "fresh": "0.5.2",
228 + "merge-descriptors": "1.0.1",
229 + "methods": "~1.1.2",
230 + "on-finished": "~2.3.0",
231 + "parseurl": "~1.3.2",
232 + "path-to-regexp": "0.1.7",
233 + "proxy-addr": "~2.0.3",
234 + "qs": "6.5.1",
235 + "range-parser": "~1.2.0",
236 + "safe-buffer": "5.1.1",
237 + "send": "0.16.2",
238 + "serve-static": "1.13.2",
239 + "setprototypeof": "1.1.0",
240 + "statuses": "~1.4.0",
241 + "type-is": "~1.6.16",
242 + "utils-merge": "1.0.1",
243 + "vary": "~1.1.2"
244 + },
245 + "dependencies": {
246 + "body-parser": {
247 + "version": "1.18.2",
248 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
249 + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
250 + "requires": {
251 + "bytes": "3.0.0",
252 + "content-type": "~1.0.4",
253 + "debug": "2.6.9",
254 + "depd": "~1.1.1",
255 + "http-errors": "~1.6.2",
256 + "iconv-lite": "0.4.19",
257 + "on-finished": "~2.3.0",
258 + "qs": "6.5.1",
259 + "raw-body": "2.3.2",
260 + "type-is": "~1.6.15"
261 + }
262 + },
263 + "iconv-lite": {
264 + "version": "0.4.19",
265 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
266 + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
267 + },
268 + "raw-body": {
269 + "version": "2.3.2",
270 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
271 + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
272 + "requires": {
273 + "bytes": "3.0.0",
274 + "http-errors": "1.6.2",
275 + "iconv-lite": "0.4.19",
276 + "unpipe": "1.0.0"
277 + },
278 + "dependencies": {
279 + "depd": {
280 + "version": "1.1.1",
281 + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
282 + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
283 + },
284 + "http-errors": {
285 + "version": "1.6.2",
286 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
287 + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
288 + "requires": {
289 + "depd": "1.1.1",
290 + "inherits": "2.0.3",
291 + "setprototypeof": "1.0.3",
292 + "statuses": ">= 1.3.1 < 2"
293 + }
294 + },
295 + "setprototypeof": {
296 + "version": "1.0.3",
297 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
298 + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
299 + }
300 + }
301 + }
302 + }
303 + },
304 + "file-type": {
305 + "version": "7.7.1",
306 + "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.7.1.tgz",
307 + "integrity": "sha512-bTrKkzzZI6wH+NXhyD3SOXtb2zXTw2SbwI2RxUlRcXVsnN7jNL5hJzVQLYv7FOQhxFkK4XWdAflEaWFpaLLWpQ=="
308 + },
309 + "finalhandler": {
310 + "version": "1.1.1",
311 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
312 + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
313 + "requires": {
314 + "debug": "2.6.9",
315 + "encodeurl": "~1.0.2",
316 + "escape-html": "~1.0.3",
317 + "on-finished": "~2.3.0",
318 + "parseurl": "~1.3.2",
319 + "statuses": "~1.4.0",
320 + "unpipe": "~1.0.0"
321 + }
322 + },
323 + "follow-redirects": {
324 + "version": "1.5.10",
325 + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
326 + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
327 + "requires": {
328 + "debug": "=3.1.0"
329 + },
330 + "dependencies": {
331 + "debug": {
332 + "version": "3.1.0",
333 + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
334 + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
335 + "requires": {
336 + "ms": "2.0.0"
337 + }
338 + }
339 + }
340 + },
341 + "forwarded": {
342 + "version": "0.1.2",
343 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
344 + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
345 + },
346 + "fresh": {
347 + "version": "0.5.2",
348 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
349 + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
350 + },
351 + "http-errors": {
352 + "version": "1.6.3",
353 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
354 + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
355 + "requires": {
356 + "depd": "~1.1.2",
357 + "inherits": "2.0.3",
358 + "setprototypeof": "1.1.0",
359 + "statuses": ">= 1.4.0 < 2"
360 + }
361 + },
362 + "iconv-lite": {
363 + "version": "0.4.24",
364 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
365 + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
366 + "requires": {
367 + "safer-buffer": ">= 2.1.2 < 3"
368 + }
369 + },
370 + "inherits": {
371 + "version": "2.0.3",
372 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
373 + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
374 + },
375 + "ipaddr.js": {
376 + "version": "1.6.0",
377 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
378 + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
379 + },
380 + "is-buffer": {
381 + "version": "2.0.3",
382 + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
383 + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
384 + },
385 + "media-typer": {
386 + "version": "0.3.0",
387 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
388 + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
389 + },
390 + "merge-descriptors": {
391 + "version": "1.0.1",
392 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
393 + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
394 + },
395 + "methods": {
396 + "version": "1.1.2",
397 + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
398 + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
399 + },
400 + "mime": {
401 + "version": "1.4.1",
402 + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
403 + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
404 + },
405 + "mime-db": {
406 + "version": "1.33.0",
407 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
408 + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
409 + },
410 + "mime-types": {
411 + "version": "2.1.18",
412 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
413 + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
414 + "requires": {
415 + "mime-db": "~1.33.0"
416 + }
417 + },
418 + "ms": {
419 + "version": "2.0.0",
420 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
421 + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
422 + },
423 + "negotiator": {
424 + "version": "0.6.1",
425 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
426 + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
427 + },
428 + "on-finished": {
429 + "version": "2.3.0",
430 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
431 + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
432 + "requires": {
433 + "ee-first": "1.1.1"
434 + }
435 + },
436 + "parseurl": {
437 + "version": "1.3.2",
438 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
439 + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
440 + },
441 + "path-to-regexp": {
442 + "version": "0.1.7",
443 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
444 + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
445 + },
446 + "proxy-addr": {
447 + "version": "2.0.3",
448 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
449 + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
450 + "requires": {
451 + "forwarded": "~0.1.2",
452 + "ipaddr.js": "1.6.0"
453 + }
454 + },
455 + "qs": {
456 + "version": "6.5.1",
457 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
458 + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
459 + },
460 + "range-parser": {
461 + "version": "1.2.0",
462 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
463 + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
464 + },
465 + "raw-body": {
466 + "version": "2.4.0",
467 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
468 + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
469 + "requires": {
470 + "bytes": "3.1.0",
471 + "http-errors": "1.7.2",
472 + "iconv-lite": "0.4.24",
473 + "unpipe": "1.0.0"
474 + },
475 + "dependencies": {
476 + "bytes": {
477 + "version": "3.1.0",
478 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
479 + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
480 + },
481 + "http-errors": {
482 + "version": "1.7.2",
483 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
484 + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
485 + "requires": {
486 + "depd": "~1.1.2",
487 + "inherits": "2.0.3",
488 + "setprototypeof": "1.1.1",
489 + "statuses": ">= 1.5.0 < 2",
490 + "toidentifier": "1.0.0"
491 + }
492 + },
493 + "setprototypeof": {
494 + "version": "1.1.1",
495 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
496 + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
497 + },
498 + "statuses": {
499 + "version": "1.5.0",
500 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
501 + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
502 + }
503 + }
504 + },
505 + "safe-buffer": {
506 + "version": "5.1.1",
507 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
508 + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
509 + },
510 + "safer-buffer": {
511 + "version": "2.1.2",
512 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
513 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
514 + },
515 + "send": {
516 + "version": "0.16.2",
517 + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
518 + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
519 + "requires": {
520 + "debug": "2.6.9",
521 + "depd": "~1.1.2",
522 + "destroy": "~1.0.4",
523 + "encodeurl": "~1.0.2",
524 + "escape-html": "~1.0.3",
525 + "etag": "~1.8.1",
526 + "fresh": "0.5.2",
527 + "http-errors": "~1.6.2",
528 + "mime": "1.4.1",
529 + "ms": "2.0.0",
530 + "on-finished": "~2.3.0",
531 + "range-parser": "~1.2.0",
532 + "statuses": "~1.4.0"
533 + }
534 + },
535 + "serve-static": {
536 + "version": "1.13.2",
537 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
538 + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
539 + "requires": {
540 + "encodeurl": "~1.0.2",
541 + "escape-html": "~1.0.3",
542 + "parseurl": "~1.3.2",
543 + "send": "0.16.2"
544 + }
545 + },
546 + "setprototypeof": {
547 + "version": "1.1.0",
548 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
549 + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
550 + },
551 + "statuses": {
552 + "version": "1.4.0",
553 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
554 + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
555 + },
556 + "toidentifier": {
557 + "version": "1.0.0",
558 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
559 + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
560 + },
561 + "type-is": {
562 + "version": "1.6.16",
563 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
564 + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
565 + "requires": {
566 + "media-typer": "0.3.0",
567 + "mime-types": "~2.1.18"
568 + }
569 + },
570 + "unpipe": {
571 + "version": "1.0.0",
572 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
573 + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
574 + },
575 + "utils-merge": {
576 + "version": "1.0.1",
577 + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
578 + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
579 + },
580 + "vary": {
581 + "version": "1.1.2",
582 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
583 + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
584 + }
585 + }
586 +}
1 +{
2 + "name": "echo-bot",
3 + "version": "0.0.0",
4 + "description": "An example LINE bot just to echo messages",
5 + "main": "index.js",
6 + "scripts": {
7 + "start": "node ."
8 + },
9 + "dependencies": {
10 + "@line/bot-sdk": "^6.8.0",
11 + "express": "^4.16.3"
12 + }
13 +}
No preview for this file type
1 +/downloaded/*
2 +!/downloaded/.gitkeep
1 +# Kitchen Sink Bot
2 +
3 +A kitchen-sink LINE bot example
4 +
5 +## Requirements
6 +
7 +Install npm dependencies:
8 +
9 +```bash
10 +npm run build-sdk # build SDK installed from local directory
11 +npm install
12 +```
13 +
14 +Also, FFmpeg and ImageMagick should be installed to test image and video
15 +echoing.
16 +
17 +### About local dependencies
18 +
19 +Currently, [`@line/bot-sdk`](package.json) is installed from local directory.
20 +
21 +```json
22 +{
23 + "@line/bot-sdk": "../../"
24 +}
25 +```
26 +
27 +To install `@line/bot-sdk` from npm, please update the line with the following:
28 +
29 +```json
30 +{
31 + "@line/bot-sdk": "*"
32 +}
33 +```
34 +
35 +In the case, `npm run build-sdk` needn't be run before `npm install`.
36 +
37 +## Configuration
38 +
39 +Configuration can be done via environment variables.
40 +
41 +```bash
42 +export CHANNEL_SECRET=YOUR_CHANNEL_SECRET
43 +export CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
44 +export BASE_URL=https://your.base.url # for static file serving
45 +export PORT=1234
46 +```
47 +
48 +The code above is an example of Bash. It may differ in other shells.
49 +
50 +## Run webhook server
51 +
52 +```bash
53 +npm start
54 +```
55 +
56 +With the configuration above, the webhook listens on `https://your.base.url:1234/callback`.
57 +
58 +## ngrok usage
59 +
60 +[ngrok](https://ngrok.com/) tunnels extenral requests to localhost, helps
61 +debugging local webhooks.
62 +
63 +This example includes ngrok inside, and it just works if no `BASE_URL` is
64 +set. Make sure that other configurations are set correctly.
65 +
66 +```
67 +❯ npm start
68 +
69 +...
70 +
71 +It seems that BASE_URL is not set. Connecting to ngrok...
72 +listening on https://ffffffff.ngrok.io/callback
73 +```
74 +
75 +The URL can be directly registered as the webhook URL in LINE Developers
76 +console.
1 +'use strict';
2 +
3 +const line = require('@line/bot-sdk');
4 +const express = require('express');
5 +const fs = require('fs');
6 +const path = require('path');
7 +const cp = require('child_process');
8 +const ngrok = require('ngrok');
9 +
10 +
11 +
12 +// create LINE SDK config from env variables
13 +const config = {
14 + channelAccessToken: '6Aw67RtwWFJb+GCbr5DGhlFY5w6i0HpuKRNA1BNyGIDfXYrA2V/+S0yCgYy+jh4R2wHvg1XEZh0hhsFof81squrHYTR+5yvrPyuaNcDOZnkQLl4X+EWi6vDB5Rf6VeRUclczO0VyXd4hp9Oo4QUmZwdB04t89/1O/w1cDnyilFU=',
15 + channelSecret: '2caa1add6c76bd51a84bd861e27c800c',
16 +};
17 +
18 +// base URL for webhook server
19 +let baseURL = process.env.BASE_URL;
20 +
21 +// create LINE SDK client
22 +const client = new line.Client(config);
23 +
24 +// create Express app
25 +// about Express itself: https://expressjs.com/
26 +const app = express();
27 +
28 +// serve static and downloaded files
29 +app.use('/static', express.static('static'));
30 +app.use('/downloaded', express.static('downloaded'));
31 +
32 +app.get('/callback', (req, res) => res.end(`I'm listening. Please access with POST.`));
33 +
34 +// webhook callback
35 +app.post('/callback', line.middleware(config), (req, res) => {
36 + if (req.body.destination) {
37 + console.log("Destination User ID: " + req.body.destination);
38 + }
39 +
40 + // req.body.events should be an array of events
41 + if (!Array.isArray(req.body.events)) {
42 + return res.status(500).end();
43 + }
44 +
45 + // handle events separately
46 + Promise.all(req.body.events.map(handleEvent))
47 + .then(() => res.end())
48 + .catch((err) => {
49 + console.error(err);
50 + res.status(500).end();
51 + });
52 +});
53 +
54 +// simple reply function
55 +const replyText = (token, texts) => {
56 + texts = Array.isArray(texts) ? texts : [texts];
57 + return client.replyMessage(
58 + token,
59 + texts.map((text) => ({ type: 'text', text }))
60 + );
61 +};
62 +
63 +// callback function to handle a single event
64 +function handleEvent(event) {
65 + if (event.replyToken && event.replyToken.match(/^(.)\1*$/)) {
66 + return console.log("Test hook recieved: " + JSON.stringify(event.message));
67 + }
68 +
69 + switch (event.type) {
70 + case 'message':
71 + const message = event.message;
72 + switch (message.type) {
73 + case 'text':
74 + return handleText(message, event.replyToken, event.source);
75 + case 'image':
76 + return handleImage(message, event.replyToken);
77 + case 'video':
78 + return handleVideo(message, event.replyToken);
79 + case 'audio':
80 + return handleAudio(message, event.replyToken);
81 + case 'location':
82 + return handleLocation(message, event.replyToken);
83 + case 'sticker':
84 + return handleSticker(message, event.replyToken);
85 + default:
86 + throw new Error(`Unknown message: ${JSON.stringify(message)}`);
87 + }
88 +
89 + case 'follow':
90 + return replyText(event.replyToken, 'Got followed event');
91 +
92 + case 'unfollow':
93 + return console.log(`Unfollowed this bot: ${JSON.stringify(event)}`);
94 +
95 + case 'join':
96 + return replyText(event.replyToken, `Joined ${event.source.type}`);
97 +
98 + case 'leave':
99 + return console.log(`Left: ${JSON.stringify(event)}`);
100 +
101 + case 'postback':
102 + let data = event.postback.data;
103 + if (data === 'DATE' || data === 'TIME' || data === 'DATETIME') {
104 + data += `(${JSON.stringify(event.postback.params)})`;
105 + }
106 + return replyText(event.replyToken, `Got postback: ${data}`);
107 +
108 + case 'beacon':
109 + return replyText(event.replyToken, `Got beacon: ${event.beacon.hwid}`);
110 +
111 + default:
112 + throw new Error(`Unknown event: ${JSON.stringify(event)}`);
113 + }
114 +}
115 +
116 +function handleText(message, replyToken, source) {
117 + const buttonsImageURL = `${baseURL}/static/buttons/1040.jpg`;
118 + //★text에 따른 응답 변화!!★
119 + switch (message.text) {
120 + case '음악 추천해줘':
121 + message.text = '아이유의 음악을 추천드립니다.';
122 + return replyText(replyToken, message.text);
123 + case 'profile':
124 + if (source.userId) {
125 + return client.getProfile(source.userId)
126 + .then((profile) => replyText(
127 + replyToken,
128 + [
129 + `Display name: ${profile.displayName}`,
130 + `Status message: ${profile.statusMessage}`,
131 + ]
132 + ));
133 + } else {
134 + return replyText(replyToken, 'Bot can\'t use profile API without user ID');
135 + }
136 + case 'buttons':
137 + return client.replyMessage(
138 + replyToken,
139 + {
140 + type: 'template',
141 + altText: 'Buttons alt text',
142 + template: {
143 + type: 'buttons',
144 + thumbnailImageUrl: buttonsImageURL,
145 + title: 'My button sample',
146 + text: 'Hello, my button',
147 + actions: [
148 + { label: 'Go to line.me', type: 'uri', uri: 'https://line.me' },
149 + { label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
150 + { label: '言 hello2', type: 'postback', data: 'hello こんにちは', text: 'hello こんにちは' },
151 + { label: 'Say message', type: 'message', text: 'Rice=米' },
152 + ],
153 + },
154 + }
155 + );
156 + case 'confirm':
157 + return client.replyMessage(
158 + replyToken,
159 + {
160 + type: 'template',
161 + altText: 'Confirm alt text',
162 + template: {
163 + type: 'confirm',
164 + text: 'Do it?',
165 + actions: [
166 + { label: 'Yes', type: 'message', text: 'Yes!' },
167 + { label: 'No', type: 'message', text: 'No!' },
168 + ],
169 + },
170 + }
171 + )
172 + case 'carousel':
173 + return client.replyMessage(
174 + replyToken,
175 + {
176 + type: 'template',
177 + altText: 'Carousel alt text',
178 + template: {
179 + type: 'carousel',
180 + columns: [
181 + {
182 + thumbnailImageUrl: buttonsImageURL,
183 + title: 'hoge',
184 + text: 'fuga',
185 + actions: [
186 + { label: 'Go to line.me', type: 'uri', uri: 'https://line.me' },
187 + { label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
188 + ],
189 + },
190 + {
191 + thumbnailImageUrl: buttonsImageURL,
192 + title: 'hoge',
193 + text: 'fuga',
194 + actions: [
195 + { label: '言 hello2', type: 'postback', data: 'hello こんにちは', text: 'hello こんにちは' },
196 + { label: 'Say message', type: 'message', text: 'Rice=米' },
197 + ],
198 + },
199 + ],
200 + },
201 + }
202 + );
203 + case 'image carousel':
204 + return client.replyMessage(
205 + replyToken,
206 + {
207 + type: 'template',
208 + altText: 'Image carousel alt text',
209 + template: {
210 + type: 'image_carousel',
211 + columns: [
212 + {
213 + imageUrl: buttonsImageURL,
214 + action: { label: 'Go to LINE', type: 'uri', uri: 'https://line.me' },
215 + },
216 + {
217 + imageUrl: buttonsImageURL,
218 + action: { label: 'Say hello1', type: 'postback', data: 'hello こんにちは' },
219 + },
220 + {
221 + imageUrl: buttonsImageURL,
222 + action: { label: 'Say message', type: 'message', text: 'Rice=米' },
223 + },
224 + {
225 + imageUrl: buttonsImageURL,
226 + action: {
227 + label: 'datetime',
228 + type: 'datetimepicker',
229 + data: 'DATETIME',
230 + mode: 'datetime',
231 + },
232 + },
233 + ]
234 + },
235 + }
236 + );
237 + case 'datetime':
238 + return client.replyMessage(
239 + replyToken,
240 + {
241 + type: 'template',
242 + altText: 'Datetime pickers alt text',
243 + template: {
244 + type: 'buttons',
245 + text: 'Select date / time !',
246 + actions: [
247 + { type: 'datetimepicker', label: 'date', data: 'DATE', mode: 'date' },
248 + { type: 'datetimepicker', label: 'time', data: 'TIME', mode: 'time' },
249 + { type: 'datetimepicker', label: 'datetime', data: 'DATETIME', mode: 'datetime' },
250 + ],
251 + },
252 + }
253 + );
254 + case 'imagemap':
255 + return client.replyMessage(
256 + replyToken,
257 + {
258 + type: 'imagemap',
259 + baseUrl: `${baseURL}/static/rich`,
260 + altText: 'Imagemap alt text',
261 + baseSize: { width: 1040, height: 1040 },
262 + actions: [
263 + { area: { x: 0, y: 0, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/manga/en' },
264 + { area: { x: 520, y: 0, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/music/en' },
265 + { area: { x: 0, y: 520, width: 520, height: 520 }, type: 'uri', linkUri: 'https://store.line.me/family/play/en' },
266 + { area: { x: 520, y: 520, width: 520, height: 520 }, type: 'message', text: 'URANAI!' },
267 + ],
268 + video: {
269 + originalContentUrl: `${baseURL}/static/imagemap/video.mp4`,
270 + previewImageUrl: `${baseURL}/static/imagemap/preview.jpg`,
271 + area: {
272 + x: 280,
273 + y: 385,
274 + width: 480,
275 + height: 270,
276 + },
277 + externalLink: {
278 + linkUri: 'https://line.me',
279 + label: 'LINE'
280 + }
281 + },
282 + }
283 + );
284 + case 'bye':
285 + switch (source.type) {
286 + case 'user':
287 + return replyText(replyToken, 'Bot can\'t leave from 1:1 chat');
288 + case 'group':
289 + return replyText(replyToken, 'Leaving group')
290 + .then(() => client.leaveGroup(source.groupId));
291 + case 'room':
292 + return replyText(replyToken, 'Leaving room')
293 + .then(() => client.leaveRoom(source.roomId));
294 + }
295 + default:
296 + console.log(`Echo message to ${replyToken}: ${message.text}`);
297 + return replyText(replyToken, message.text);
298 + }
299 +}
300 +
301 +function handleImage(message, replyToken) {
302 + let getContent;
303 + if (message.contentProvider.type === "line") {
304 + const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.jpg`);
305 + const previewPath = path.join(__dirname, 'downloaded', `${message.id}-preview.jpg`);
306 +
307 + getContent = downloadContent(message.id, downloadPath)
308 + .then((downloadPath) => {
309 + // ImageMagick is needed here to run 'convert'
310 + // Please consider about security and performance by yourself
311 + cp.execSync(`convert -resize 240x jpeg:${downloadPath} jpeg:${previewPath}`);
312 +
313 + return {
314 + originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
315 + previewImageUrl: baseURL + '/downloaded/' + path.basename(previewPath),
316 + };
317 + });
318 + } else if (message.contentProvider.type === "external") {
319 + getContent = Promise.resolve(message.contentProvider);
320 + }
321 +
322 + return getContent
323 + .then(({ originalContentUrl, previewImageUrl }) => {
324 + return client.replyMessage(
325 + replyToken,
326 + {
327 + type: 'image',
328 + originalContentUrl,
329 + previewImageUrl,
330 + }
331 + );
332 + });
333 +}
334 +
335 +function handleVideo(message, replyToken) {
336 + let getContent;
337 + if (message.contentProvider.type === "line") {
338 + const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.mp4`);
339 + const previewPath = path.join(__dirname, 'downloaded', `${message.id}-preview.jpg`);
340 +
341 + getContent = downloadContent(message.id, downloadPath)
342 + .then((downloadPath) => {
343 + // FFmpeg and ImageMagick is needed here to run 'convert'
344 + // Please consider about security and performance by yourself
345 + cp.execSync(`convert mp4:${downloadPath}[0] jpeg:${previewPath}`);
346 +
347 + return {
348 + originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
349 + previewImageUrl: baseURL + '/downloaded/' + path.basename(previewPath),
350 + }
351 + });
352 + } else if (message.contentProvider.type === "external") {
353 + getContent = Promise.resolve(message.contentProvider);
354 + }
355 +
356 + return getContent
357 + .then(({ originalContentUrl, previewImageUrl }) => {
358 + return client.replyMessage(
359 + replyToken,
360 + {
361 + type: 'video',
362 + originalContentUrl,
363 + previewImageUrl,
364 + }
365 + );
366 + });
367 +}
368 +
369 +function handleAudio(message, replyToken) {
370 + let getContent;
371 + if (message.contentProvider.type === "line") {
372 + const downloadPath = path.join(__dirname, 'downloaded', `${message.id}.m4a`);
373 +
374 + getContent = downloadContent(message.id, downloadPath)
375 + .then((downloadPath) => {
376 + return {
377 + originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
378 + };
379 + });
380 + } else {
381 + getContent = Promise.resolve(message.contentProvider);
382 + }
383 +
384 + return getContent
385 + .then(({ originalContentUrl }) => {
386 + return client.replyMessage(
387 + replyToken,
388 + {
389 + type: 'audio',
390 + originalContentUrl,
391 + duration: message.duration,
392 + }
393 + );
394 + });
395 +}
396 +
397 +function downloadContent(messageId, downloadPath) {
398 + return client.getMessageContent(messageId)
399 + .then((stream) => new Promise((resolve, reject) => {
400 + const writable = fs.createWriteStream(downloadPath);
401 + stream.pipe(writable);
402 + stream.on('end', () => resolve(downloadPath));
403 + stream.on('error', reject);
404 + }));
405 +}
406 +
407 +function handleLocation(message, replyToken) {
408 + return client.replyMessage(
409 + replyToken,
410 + {
411 + type: 'location',
412 + title: message.title,
413 + address: message.address,
414 + latitude: message.latitude,
415 + longitude: message.longitude,
416 + }
417 + );
418 +}
419 +
420 +function handleSticker(message, replyToken) {
421 + return client.replyMessage(
422 + replyToken,
423 + {
424 + type: 'sticker',
425 + packageId: message.packageId,
426 + stickerId: message.stickerId,
427 + }
428 + );
429 +}
430 +
431 +// listen on port
432 +const port = process.env.PORT || 3000;
433 +app.listen(port, () => {
434 + if (baseURL) {
435 + console.log(`listening on ${baseURL}:${port}/callback`);
436 + } else {
437 + console.log("It seems that BASE_URL is not set. Connecting to ngrok...")
438 + ngrok.connect(port).then(url => {
439 + baseURL = url;
440 + console.log(`listening on ${baseURL}/callback`);
441 + }).catch(console.error);
442 + }
443 +});
This diff could not be displayed because it is too large.
1 +{
2 + "name": "kitchensink",
3 + "version": "0.0.0",
4 + "description": "A kitchen-sink LINE bot example",
5 + "main": "index.js",
6 + "scripts": {
7 + "build-sdk": "cd ../../; npm i; npm run build",
8 + "start": "node ."
9 + },
10 + "dependencies": {
11 + "@line/bot-sdk": "../../",
12 + "express": "^4.17.1",
13 + "ngrok": "^3.2.7"
14 + }
15 +}
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
1 +import { Readable } from "stream";
2 +import HTTPClient from "./http";
3 +import * as Types from "./types";
4 +import { AxiosResponse, AxiosRequestConfig } from "axios";
5 +import { createMultipartFormData, ensureJSON, toArray } from "./utils";
6 +
7 +type ChatType = "group" | "room";
8 +type RequestOption = {
9 + retryKey: string;
10 +};
11 +import {
12 + MESSAGING_API_PREFIX,
13 + DATA_API_PREFIX,
14 + OAUTH_BASE_PREFIX,
15 + OAUTH_BASE_PREFIX_V2_1,
16 +} from "./endpoints";
17 +
18 +export default class Client {
19 + public config: Types.ClientConfig;
20 + private http: HTTPClient;
21 +
22 + private requestOption: Partial<RequestOption> = {};
23 +
24 + constructor(config: Types.ClientConfig) {
25 + if (!config.channelAccessToken) {
26 + throw new Error("no channel access token");
27 + }
28 +
29 + this.config = config;
30 + this.http = new HTTPClient({
31 + defaultHeaders: {
32 + Authorization: "Bearer " + this.config.channelAccessToken,
33 + },
34 + responseParser: this.parseHTTPResponse.bind(this),
35 + ...config.httpConfig,
36 + });
37 + }
38 + public setRequestOptionOnce(option: Partial<RequestOption>) {
39 + this.requestOption = option;
40 + }
41 +
42 + private generateRequestConfig(): Partial<AxiosRequestConfig> {
43 + const config: Partial<AxiosRequestConfig> = { headers: {} };
44 + if (this.requestOption.retryKey) {
45 + config.headers["X-Line-Retry-Key"] = this.requestOption.retryKey;
46 + }
47 +
48 + // clear requestOption
49 + this.requestOption = {};
50 + return config;
51 + }
52 +
53 + private parseHTTPResponse(response: AxiosResponse) {
54 + const { LINE_REQUEST_ID_HTTP_HEADER_NAME } = Types;
55 + let resBody = {
56 + ...response.data,
57 + };
58 + if (response.headers[LINE_REQUEST_ID_HTTP_HEADER_NAME]) {
59 + resBody[LINE_REQUEST_ID_HTTP_HEADER_NAME] =
60 + response.headers[LINE_REQUEST_ID_HTTP_HEADER_NAME];
61 + }
62 + return resBody;
63 + }
64 +
65 + public pushMessage(
66 + to: string,
67 + messages: Types.Message | Types.Message[],
68 + notificationDisabled: boolean = false,
69 + ): Promise<Types.MessageAPIResponseBase> {
70 + return this.http.post(
71 + `${MESSAGING_API_PREFIX}/message/push`,
72 + {
73 + messages: toArray(messages),
74 + to,
75 + notificationDisabled,
76 + },
77 + this.generateRequestConfig(),
78 + );
79 + }
80 +
81 + public replyMessage(
82 + replyToken: string,
83 + messages: Types.Message | Types.Message[],
84 + notificationDisabled: boolean = false,
85 + ): Promise<Types.MessageAPIResponseBase> {
86 + return this.http.post(`${MESSAGING_API_PREFIX}/message/reply`, {
87 + messages: toArray(messages),
88 + replyToken,
89 + notificationDisabled,
90 + });
91 + }
92 +
93 + public async multicast(
94 + to: string[],
95 + messages: Types.Message | Types.Message[],
96 + notificationDisabled: boolean = false,
97 + ): Promise<Types.MessageAPIResponseBase> {
98 + return this.http.post(
99 + `${MESSAGING_API_PREFIX}/message/multicast`,
100 + {
101 + messages: toArray(messages),
102 + to,
103 + notificationDisabled,
104 + },
105 + this.generateRequestConfig(),
106 + );
107 + }
108 +
109 + public async narrowcast(
110 + messages: Types.Message | Types.Message[],
111 + recipient?: Types.ReceieptObject,
112 + filter?: { demographic: Types.DemographicFilterObject },
113 + limit?: { max?: number; upToRemainingQuota?: boolean },
114 + notificationDisabled?: boolean,
115 + ): Promise<Types.MessageAPIResponseBase> {
116 + return this.http.post(
117 + `${MESSAGING_API_PREFIX}/message/narrowcast`,
118 + {
119 + messages: toArray(messages),
120 + recipient,
121 + filter,
122 + limit,
123 + notificationDisabled,
124 + },
125 + this.generateRequestConfig(),
126 + );
127 + }
128 +
129 + public async broadcast(
130 + messages: Types.Message | Types.Message[],
131 + notificationDisabled: boolean = false,
132 + ): Promise<Types.MessageAPIResponseBase> {
133 + return this.http.post(
134 + `${MESSAGING_API_PREFIX}/message/broadcast`,
135 + {
136 + messages: toArray(messages),
137 + notificationDisabled,
138 + },
139 + this.generateRequestConfig(),
140 + );
141 + }
142 +
143 + public async getProfile(userId: string): Promise<Types.Profile> {
144 + const profile = await this.http.get<Types.Profile>(
145 + `${MESSAGING_API_PREFIX}/profile/${userId}`,
146 + );
147 + return ensureJSON(profile);
148 + }
149 +
150 + private async getChatMemberProfile(
151 + chatType: ChatType,
152 + chatId: string,
153 + userId: string,
154 + ): Promise<Types.Profile> {
155 + const profile = await this.http.get<Types.Profile>(
156 + `${MESSAGING_API_PREFIX}/${chatType}/${chatId}/member/${userId}`,
157 + );
158 + return ensureJSON(profile);
159 + }
160 +
161 + public async getGroupMemberProfile(
162 + groupId: string,
163 + userId: string,
164 + ): Promise<Types.Profile> {
165 + return this.getChatMemberProfile("group", groupId, userId);
166 + }
167 +
168 + public async getRoomMemberProfile(
169 + roomId: string,
170 + userId: string,
171 + ): Promise<Types.Profile> {
172 + return this.getChatMemberProfile("room", roomId, userId);
173 + }
174 +
175 + private async getChatMemberIds(
176 + chatType: ChatType,
177 + chatId: string,
178 + ): Promise<string[]> {
179 + let memberIds: string[] = [];
180 +
181 + let start: string;
182 + do {
183 + const res = await this.http.get<{ memberIds: string[]; next?: string }>(
184 + `${MESSAGING_API_PREFIX}/${chatType}/${chatId}/members/ids`,
185 + start ? { start } : null,
186 + );
187 + ensureJSON(res);
188 + memberIds = memberIds.concat(res.memberIds);
189 + start = res.next;
190 + } while (start);
191 +
192 + return memberIds;
193 + }
194 +
195 + public async getGroupMemberIds(groupId: string): Promise<string[]> {
196 + return this.getChatMemberIds("group", groupId);
197 + }
198 +
199 + public async getRoomMemberIds(roomId: string): Promise<string[]> {
200 + return this.getChatMemberIds("room", roomId);
201 + }
202 +
203 + public async getGroupMembersCount(
204 + groupId: string,
205 + ): Promise<Types.MembersCountResponse> {
206 + const groupMemberCount = await this.http.get<Types.MembersCountResponse>(
207 + `${MESSAGING_API_PREFIX}/group/${groupId}/members/count`,
208 + );
209 + return ensureJSON(groupMemberCount);
210 + }
211 +
212 + public async getRoomMembersCount(
213 + roomId: string,
214 + ): Promise<Types.MembersCountResponse> {
215 + const roomMemberCount = await this.http.get<Types.MembersCountResponse>(
216 + `${MESSAGING_API_PREFIX}/room/${roomId}/members/count`,
217 + );
218 + return ensureJSON(roomMemberCount);
219 + }
220 +
221 + public async getGroupSummary(
222 + groupId: string,
223 + ): Promise<Types.GroupSummaryResponse> {
224 + const groupSummary = await this.http.get<Types.GroupSummaryResponse>(
225 + `${MESSAGING_API_PREFIX}/group/${groupId}/summary`,
226 + );
227 + return ensureJSON(groupSummary);
228 + }
229 +
230 + public async getMessageContent(messageId: string): Promise<Readable> {
231 + return this.http.getStream(
232 + `${DATA_API_PREFIX}/message/${messageId}/content`,
233 + );
234 + }
235 +
236 + private leaveChat(chatType: ChatType, chatId: string): Promise<any> {
237 + return this.http.post(
238 + `${MESSAGING_API_PREFIX}/${chatType}/${chatId}/leave`,
239 + );
240 + }
241 +
242 + public async leaveGroup(groupId: string): Promise<any> {
243 + return this.leaveChat("group", groupId);
244 + }
245 +
246 + public async leaveRoom(roomId: string): Promise<any> {
247 + return this.leaveChat("room", roomId);
248 + }
249 +
250 + public async getRichMenu(
251 + richMenuId: string,
252 + ): Promise<Types.RichMenuResponse> {
253 + const res = await this.http.get<Types.RichMenuResponse>(
254 + `${MESSAGING_API_PREFIX}/richmenu/${richMenuId}`,
255 + );
256 + return ensureJSON(res);
257 + }
258 +
259 + public async createRichMenu(richMenu: Types.RichMenu): Promise<string> {
260 + const res = await this.http.post<any>(
261 + `${MESSAGING_API_PREFIX}/richmenu`,
262 + richMenu,
263 + );
264 + return ensureJSON(res).richMenuId;
265 + }
266 +
267 + public async deleteRichMenu(richMenuId: string): Promise<any> {
268 + return this.http.delete(`${MESSAGING_API_PREFIX}/richmenu/${richMenuId}`);
269 + }
270 +
271 + public async getRichMenuIdOfUser(userId: string): Promise<string> {
272 + const res = await this.http.get<any>(
273 + `${MESSAGING_API_PREFIX}/user/${userId}/richmenu`,
274 + );
275 + return ensureJSON(res).richMenuId;
276 + }
277 +
278 + public async linkRichMenuToUser(
279 + userId: string,
280 + richMenuId: string,
281 + ): Promise<any> {
282 + return this.http.post(
283 + `${MESSAGING_API_PREFIX}/user/${userId}/richmenu/${richMenuId}`,
284 + );
285 + }
286 +
287 + public async unlinkRichMenuFromUser(userId: string): Promise<any> {
288 + return this.http.delete(`${MESSAGING_API_PREFIX}/user/${userId}/richmenu`);
289 + }
290 +
291 + public async linkRichMenuToMultipleUsers(
292 + richMenuId: string,
293 + userIds: string[],
294 + ): Promise<any> {
295 + return this.http.post(`${MESSAGING_API_PREFIX}/richmenu/bulk/link`, {
296 + richMenuId,
297 + userIds,
298 + });
299 + }
300 +
301 + public async unlinkRichMenusFromMultipleUsers(
302 + userIds: string[],
303 + ): Promise<any> {
304 + return this.http.post(`${MESSAGING_API_PREFIX}/richmenu/bulk/unlink`, {
305 + userIds,
306 + });
307 + }
308 +
309 + public async getRichMenuImage(richMenuId: string): Promise<Readable> {
310 + return this.http.getStream(
311 + `${DATA_API_PREFIX}/richmenu/${richMenuId}/content`,
312 + );
313 + }
314 +
315 + public async setRichMenuImage(
316 + richMenuId: string,
317 + data: Buffer | Readable,
318 + contentType?: string,
319 + ): Promise<any> {
320 + return this.http.postBinary(
321 + `${DATA_API_PREFIX}/richmenu/${richMenuId}/content`,
322 + data,
323 + contentType,
324 + );
325 + }
326 +
327 + public async getRichMenuList(): Promise<Array<Types.RichMenuResponse>> {
328 + const res = await this.http.get<any>(
329 + `${MESSAGING_API_PREFIX}/richmenu/list`,
330 + );
331 + return ensureJSON(res).richmenus;
332 + }
333 +
334 + public async setDefaultRichMenu(richMenuId: string): Promise<{}> {
335 + return this.http.post(
336 + `${MESSAGING_API_PREFIX}/user/all/richmenu/${richMenuId}`,
337 + );
338 + }
339 +
340 + public async getDefaultRichMenuId(): Promise<string> {
341 + const res = await this.http.get<any>(
342 + `${MESSAGING_API_PREFIX}/user/all/richmenu`,
343 + );
344 + return ensureJSON(res).richMenuId;
345 + }
346 +
347 + public async deleteDefaultRichMenu(): Promise<{}> {
348 + return this.http.delete(`${MESSAGING_API_PREFIX}/user/all/richmenu`);
349 + }
350 +
351 + public async getLinkToken(userId: string): Promise<string> {
352 + const res = await this.http.post<any>(
353 + `${MESSAGING_API_PREFIX}/user/${userId}/linkToken`,
354 + );
355 + return ensureJSON(res).linkToken;
356 + }
357 +
358 + public async getNumberOfSentReplyMessages(
359 + date: string,
360 + ): Promise<Types.NumberOfMessagesSentResponse> {
361 + const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
362 + `${MESSAGING_API_PREFIX}/message/delivery/reply?date=${date}`,
363 + );
364 + return ensureJSON(res);
365 + }
366 +
367 + public async getNumberOfSentPushMessages(
368 + date: string,
369 + ): Promise<Types.NumberOfMessagesSentResponse> {
370 + const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
371 + `${MESSAGING_API_PREFIX}/message/delivery/push?date=${date}`,
372 + );
373 + return ensureJSON(res);
374 + }
375 +
376 + public async getNumberOfSentMulticastMessages(
377 + date: string,
378 + ): Promise<Types.NumberOfMessagesSentResponse> {
379 + const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
380 + `${MESSAGING_API_PREFIX}/message/delivery/multicast?date=${date}`,
381 + );
382 + return ensureJSON(res);
383 + }
384 +
385 + public async getNarrowcastProgress(
386 + requestId: string,
387 + ): Promise<Types.NarrowcastProgressResponse> {
388 + const res = await this.http.get<Types.NarrowcastProgressResponse>(
389 + `${MESSAGING_API_PREFIX}/message/progress/narrowcast?requestId=${requestId}`,
390 + );
391 + return ensureJSON(res);
392 + }
393 +
394 + public async getTargetLimitForAdditionalMessages(): Promise<Types.TargetLimitForAdditionalMessages> {
395 + const res = await this.http.get<Types.TargetLimitForAdditionalMessages>(
396 + `${MESSAGING_API_PREFIX}/message/quota`,
397 + );
398 + return ensureJSON(res);
399 + }
400 +
401 + public async getNumberOfMessagesSentThisMonth(): Promise<Types.NumberOfMessagesSentThisMonth> {
402 + const res = await this.http.get<Types.NumberOfMessagesSentThisMonth>(
403 + `${MESSAGING_API_PREFIX}/message/quota/consumption`,
404 + );
405 + return ensureJSON(res);
406 + }
407 +
408 + public async getNumberOfSentBroadcastMessages(
409 + date: string,
410 + ): Promise<Types.NumberOfMessagesSentResponse> {
411 + const res = await this.http.get<Types.NumberOfMessagesSentResponse>(
412 + `${MESSAGING_API_PREFIX}/message/delivery/broadcast?date=${date}`,
413 + );
414 + return ensureJSON(res);
415 + }
416 +
417 + public async getNumberOfMessageDeliveries(
418 + date: string,
419 + ): Promise<Types.NumberOfMessageDeliveriesResponse> {
420 + const res = await this.http.get<Types.NumberOfMessageDeliveriesResponse>(
421 + `${MESSAGING_API_PREFIX}/insight/message/delivery?date=${date}`,
422 + );
423 + return ensureJSON(res);
424 + }
425 +
426 + public async getNumberOfFollowers(
427 + date: string,
428 + ): Promise<Types.NumberOfFollowersResponse> {
429 + const res = await this.http.get<Types.NumberOfFollowersResponse>(
430 + `${MESSAGING_API_PREFIX}/insight/followers?date=${date}`,
431 + );
432 + return ensureJSON(res);
433 + }
434 +
435 + public async getFriendDemographics(): Promise<Types.FriendDemographics> {
436 + const res = await this.http.get<Types.FriendDemographics>(
437 + `${MESSAGING_API_PREFIX}/insight/demographic`,
438 + );
439 + return ensureJSON(res);
440 + }
441 +
442 + public async getUserInteractionStatistics(
443 + requestId: string,
444 + ): Promise<Types.UserInteractionStatistics> {
445 + const res = await this.http.get<Types.UserInteractionStatistics>(
446 + `${MESSAGING_API_PREFIX}/insight/message/event?requestId=${requestId}`,
447 + );
448 + return ensureJSON(res);
449 + }
450 +
451 + public async createUploadAudienceGroup(uploadAudienceGroup: {
452 + description: string;
453 + isIfaAudience?: boolean;
454 + audiences?: { id: string }[];
455 + uploadDescription?: string;
456 + }) {
457 + const res = await this.http.post<{
458 + audienceGroupId: number;
459 + type: string;
460 + description: string;
461 + created: number;
462 + }>(`${MESSAGING_API_PREFIX}/audienceGroup/upload`, {
463 + ...uploadAudienceGroup,
464 + });
465 + return ensureJSON(res);
466 + }
467 +
468 + public async createUploadAudienceGroupByFile(uploadAudienceGroup: {
469 + description: string;
470 + isIfaAudience?: boolean;
471 + uploadDescription?: string;
472 + file: Buffer | Readable;
473 + }) {
474 + const file = await this.http.toBuffer(uploadAudienceGroup.file);
475 + const body = createMultipartFormData({ ...uploadAudienceGroup, file });
476 + const res = await this.http.post<{
477 + audienceGroupId: number;
478 + type: "UPLOAD";
479 + description: string;
480 + created: number;
481 + }>(`${DATA_API_PREFIX}/audienceGroup/upload/byFile`, body, {
482 + headers: body.getHeaders(),
483 + });
484 + return ensureJSON(res);
485 + }
486 +
487 + public async updateUploadAudienceGroup(
488 + uploadAudienceGroup: {
489 + audienceGroupId: number;
490 + description?: string;
491 + uploadDescription?: string;
492 + audiences: { id: string }[];
493 + },
494 + // for set request timeout
495 + httpConfig?: Partial<AxiosRequestConfig>,
496 + ) {
497 + const res = await this.http.put<{}>(
498 + `${MESSAGING_API_PREFIX}/audienceGroup/upload`,
499 + {
500 + ...uploadAudienceGroup,
501 + },
502 + httpConfig,
503 + );
504 + return ensureJSON(res);
505 + }
506 +
507 + public async updateUploadAudienceGroupByFile(
508 + uploadAudienceGroup: {
509 + audienceGroupId: number;
510 + uploadDescription?: string;
511 + file: Buffer | Readable;
512 + },
513 + // for set request timeout
514 + httpConfig?: Partial<AxiosRequestConfig>,
515 + ) {
516 + const file = await this.http.toBuffer(uploadAudienceGroup.file);
517 + const body = createMultipartFormData({ ...uploadAudienceGroup, file });
518 +
519 + const res = await this.http.put<{}>(
520 + `${DATA_API_PREFIX}/audienceGroup/upload/byFile`,
521 + body,
522 + { headers: body.getHeaders(), ...httpConfig },
523 + );
524 + return ensureJSON(res);
525 + }
526 +
527 + public async createClickAudienceGroup(clickAudienceGroup: {
528 + description: string;
529 + requestId: string;
530 + clickUrl?: string;
531 + }) {
532 + const res = await this.http.post<
533 + {
534 + audienceGroupId: number;
535 + type: string;
536 + created: number;
537 + } & typeof clickAudienceGroup
538 + >(`${MESSAGING_API_PREFIX}/audienceGroup/click`, {
539 + ...clickAudienceGroup,
540 + });
541 + return ensureJSON(res);
542 + }
543 +
544 + public async createImpAudienceGroup(impAudienceGroup: {
545 + requestId: string;
546 + description: string;
547 + }) {
548 + const res = await this.http.post<
549 + {
550 + audienceGroupId: number;
551 + type: string;
552 + created: number;
553 + } & typeof impAudienceGroup
554 + >(`${MESSAGING_API_PREFIX}/audienceGroup/imp`, {
555 + ...impAudienceGroup,
556 + });
557 + return ensureJSON(res);
558 + }
559 +
560 + public async setDescriptionAudienceGroup(
561 + description: string,
562 + audienceGroupId: string,
563 + ) {
564 + const res = await this.http.put<{}>(
565 + `${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}/updateDescription`,
566 + {
567 + description,
568 + },
569 + );
570 + return ensureJSON(res);
571 + }
572 +
573 + public async deleteAudienceGroup(audienceGroupId: string) {
574 + const res = await this.http.delete<{}>(
575 + `${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}`,
576 + );
577 + return ensureJSON(res);
578 + }
579 +
580 + public async getAudienceGroup(audienceGroupId: string) {
581 + const res = await this.http.get<Types.AudienceGroup>(
582 + `${MESSAGING_API_PREFIX}/audienceGroup/${audienceGroupId}`,
583 + );
584 + return ensureJSON(res);
585 + }
586 +
587 + public async getAudienceGroups(
588 + page: number,
589 + description?: string,
590 + status?: Types.AudienceGroupStatus,
591 + size?: number,
592 + createRoute?: Types.AudienceGroupCreateRoute,
593 + includesExternalPublicGroups?: boolean,
594 + ) {
595 + const res = await this.http.get<{
596 + audienceGroups: Types.AudienceGroups;
597 + hasNextPage: boolean;
598 + totalCount: number;
599 + readWriteAudienceGroupTotalCount: number;
600 + page: number;
601 + size: number;
602 + }>(`${MESSAGING_API_PREFIX}/audienceGroup/list`, {
603 + page,
604 + description,
605 + status,
606 + size,
607 + createRoute,
608 + includesExternalPublicGroups,
609 + });
610 + return ensureJSON(res);
611 + }
612 +
613 + public async getAudienceGroupAuthorityLevel() {
614 + const res = await this.http.get<{
615 + authorityLevel: Types.AudienceGroupAuthorityLevel;
616 + }>(`${MESSAGING_API_PREFIX}/audienceGroup/authorityLevel`);
617 + return ensureJSON(res);
618 + }
619 +
620 + public async changeAudienceGroupAuthorityLevel(
621 + authorityLevel: Types.AudienceGroupAuthorityLevel,
622 + ) {
623 + const res = await this.http.put<{}>(
624 + `${MESSAGING_API_PREFIX}/audienceGroup/authorityLevel`,
625 + { authorityLevel },
626 + );
627 + return ensureJSON(res);
628 + }
629 +
630 + public async getBotInfo(): Promise<Types.BotInfoResponse> {
631 + const res = await this.http.get<Types.BotInfoResponse>(
632 + `${MESSAGING_API_PREFIX}/info`,
633 + );
634 + return ensureJSON(res);
635 + }
636 +
637 + public async setWebhookEndpointUrl(endpoint: string) {
638 + return this.http.put<{}>(
639 + `${MESSAGING_API_PREFIX}/channel/webhook/endpoint`,
640 + { endpoint },
641 + );
642 + }
643 +
644 + public async getWebhookEndpointInfo() {
645 + const res = await this.http.get<Types.WebhookEndpointInfoResponse>(
646 + `${MESSAGING_API_PREFIX}/channel/webhook/endpoint`,
647 + );
648 + return ensureJSON(res);
649 + }
650 +
651 + public async testWebhookEndpoint(endpoint?: string) {
652 + const res = await this.http.post<Types.TestWebhookEndpointResponse>(
653 + `${MESSAGING_API_PREFIX}/channel/webhook/test`,
654 + { endpoint },
655 + );
656 + return ensureJSON(res);
657 + }
658 +}
659 +
660 +export class OAuth {
661 + private http: HTTPClient;
662 +
663 + constructor() {
664 + this.http = new HTTPClient();
665 + }
666 +
667 + public issueAccessToken(
668 + client_id: string,
669 + client_secret: string,
670 + ): Promise<Types.ChannelAccessToken> {
671 + return this.http.postForm(`${OAUTH_BASE_PREFIX}/accessToken`, {
672 + grant_type: "client_credentials",
673 + client_id,
674 + client_secret,
675 + });
676 + }
677 +
678 + public revokeAccessToken(access_token: string): Promise<{}> {
679 + return this.http.postForm(`${OAUTH_BASE_PREFIX}/revoke`, { access_token });
680 + }
681 +
682 + public issueChannelAccessTokenV2_1(
683 + client_assertion: string,
684 + ): Promise<Types.ChannelAccessToken> {
685 + return this.http.postForm(`${OAUTH_BASE_PREFIX_V2_1}/token`, {
686 + grant_type: "client_credentials",
687 + client_assertion_type:
688 + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
689 + client_assertion,
690 + });
691 + }
692 +
693 + public getChannelAccessTokenKeyIdsV2_1(
694 + client_assertion: string,
695 + ): Promise<{ key_ids: string[] }> {
696 + return this.http.get(`${OAUTH_BASE_PREFIX_V2_1}/tokens/kid`, {
697 + client_assertion_type:
698 + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
699 + client_assertion,
700 + });
701 + }
702 +
703 + public revokeChannelAccessTokenV2_1(
704 + client_id: string,
705 + client_secret: string,
706 + access_token: string,
707 + ): Promise<{}> {
708 + return this.http.postForm(`${OAUTH_BASE_PREFIX_V2_1}/revoke`, {
709 + client_id,
710 + client_secret,
711 + access_token,
712 + });
713 + }
714 +}
1 +export const MESSAGING_API_PREFIX = `https://api.line.me/v2/bot`;
2 +export const DATA_API_PREFIX = `https://api-data.line.me/v2/bot`;
3 +export const OAUTH_BASE_PREFIX = `https://api.line.me/v2/oauth`;
4 +export const OAUTH_BASE_PREFIX_V2_1 = `https://api.line.me/oauth2/v2.1`;
1 +export class SignatureValidationFailed extends Error {
2 + constructor(message: string, public signature?: string) {
3 + super(message);
4 + }
5 +}
6 +
7 +export class JSONParseError extends Error {
8 + constructor(message: string, public raw: any) {
9 + super(message);
10 + }
11 +}
12 +
13 +export class RequestError extends Error {
14 + constructor(
15 + message: string,
16 + public code: string,
17 + private originalError: Error,
18 + ) {
19 + super(message);
20 + }
21 +}
22 +
23 +export class ReadError extends Error {
24 + constructor(private originalError: Error) {
25 + super(originalError.message);
26 + }
27 +}
28 +
29 +export class HTTPError extends Error {
30 + constructor(
31 + message: string,
32 + public statusCode: number,
33 + public statusMessage: string,
34 + public originalError: any,
35 + ) {
36 + super(message);
37 + }
38 +}
1 +import axios, {
2 + AxiosInstance,
3 + AxiosError,
4 + AxiosResponse,
5 + AxiosRequestConfig,
6 +} from "axios";
7 +import { Readable } from "stream";
8 +import { HTTPError, ReadError, RequestError } from "./exceptions";
9 +import * as fileType from "file-type";
10 +import * as qs from "querystring";
11 +
12 +const pkg = require("../package.json");
13 +
14 +interface httpClientConfig extends Partial<AxiosRequestConfig> {
15 + baseURL?: string;
16 + defaultHeaders?: any;
17 + responseParser?: <T>(res: AxiosResponse) => T;
18 +}
19 +
20 +export default class HTTPClient {
21 + private instance: AxiosInstance;
22 + private config: httpClientConfig;
23 +
24 + constructor(config: httpClientConfig = {}) {
25 + this.config = config;
26 + const { baseURL, defaultHeaders } = config;
27 + this.instance = axios.create({
28 + baseURL,
29 + headers: Object.assign({}, defaultHeaders, {
30 + "User-Agent": `${pkg.name}/${pkg.version}`,
31 + }),
32 + });
33 +
34 + this.instance.interceptors.response.use(
35 + res => res,
36 + err => Promise.reject(this.wrapError(err)),
37 + );
38 + }
39 +
40 + public async get<T>(url: string, params?: any): Promise<T> {
41 + const res = await this.instance.get(url, { params });
42 + return res.data;
43 + }
44 +
45 + public async getStream(url: string, params?: any): Promise<Readable> {
46 + const res = await this.instance.get(url, {
47 + params,
48 + responseType: "stream",
49 + });
50 + return res.data as Readable;
51 + }
52 +
53 + public async post<T>(
54 + url: string,
55 + body?: any,
56 + config?: Partial<AxiosRequestConfig>,
57 + ): Promise<T> {
58 + const res = await this.instance.post(url, body, {
59 + headers: {
60 + "Content-Type": "application/json",
61 + ...(config && config.headers),
62 + },
63 + ...config,
64 + });
65 +
66 + return this.responseParse(res);
67 + }
68 +
69 + private responseParse(res: AxiosResponse) {
70 + const { responseParser } = this.config;
71 + if (responseParser) return responseParser(res);
72 + else return res.data;
73 + }
74 +
75 + public async put<T>(
76 + url: string,
77 + body?: any,
78 + config?: Partial<AxiosRequestConfig>,
79 + ): Promise<T> {
80 + const res = await this.instance.put(url, body, {
81 + headers: {
82 + "Content-Type": "application/json",
83 + ...(config && config.headers),
84 + },
85 + ...config,
86 + });
87 +
88 + return this.responseParse(res);
89 + }
90 +
91 + public async postForm<T>(url: string, body?: any): Promise<T> {
92 + const res = await this.instance.post(url, qs.stringify(body), {
93 + headers: { "Content-Type": "application/x-www-form-urlencoded" },
94 + });
95 +
96 + return res.data;
97 + }
98 +
99 + public async toBuffer(data: Buffer | Readable) {
100 + if (Buffer.isBuffer(data)) {
101 + return data;
102 + } else if (data instanceof Readable) {
103 + return await new Promise<Buffer>((resolve, reject) => {
104 + const buffers: Buffer[] = [];
105 + let size = 0;
106 + data.on("data", (chunk: Buffer) => {
107 + buffers.push(chunk);
108 + size += chunk.length;
109 + });
110 + data.on("end", () => resolve(Buffer.concat(buffers, size)));
111 + data.on("error", reject);
112 + });
113 + } else {
114 + throw new Error("invalid data type for binary data");
115 + }
116 + }
117 +
118 + public async postBinary<T>(
119 + url: string,
120 + data: Buffer | Readable,
121 + contentType?: string,
122 + ): Promise<T> {
123 + const buffer = await this.toBuffer(data);
124 +
125 + const res = await this.instance.post(url, buffer, {
126 + headers: {
127 + "Content-Type": contentType || (await fileType.fromBuffer(buffer)).mime,
128 + "Content-Length": buffer.length,
129 + },
130 + });
131 +
132 + return res.data;
133 + }
134 +
135 + public async delete<T>(url: string, params?: any): Promise<T> {
136 + const res = await this.instance.delete(url, { params });
137 + return res.data;
138 + }
139 +
140 + private wrapError(err: AxiosError): Error {
141 + if (err.response) {
142 + return new HTTPError(
143 + err.message,
144 + err.response.status,
145 + err.response.statusText,
146 + err,
147 + );
148 + } else if (err.code) {
149 + return new RequestError(err.message, err.code, err);
150 + } else if (err.config) {
151 + // unknown, but from axios
152 + return new ReadError(err);
153 + }
154 +
155 + // otherwise, just rethrow
156 + return err;
157 + }
158 +}
1 +import Client, { OAuth } from "./client";
2 +import middleware from "./middleware";
3 +import validateSignature from "./validate-signature";
4 +
5 +export { Client, middleware, validateSignature, OAuth };
6 +
7 +// re-export exceptions and types
8 +export * from "./exceptions";
9 +export * from "./types";
1 +import { raw } from "body-parser";
2 +import * as http from "http";
3 +import { JSONParseError, SignatureValidationFailed } from "./exceptions";
4 +import * as Types from "./types";
5 +import validateSignature from "./validate-signature";
6 +
7 +export type Request = http.IncomingMessage & { body: any };
8 +export type Response = http.ServerResponse;
9 +export type NextCallback = (err?: Error) => void;
10 +
11 +export type Middleware = (
12 + req: Request,
13 + res: Response,
14 + next: NextCallback,
15 +) => void | Promise<void>;
16 +
17 +function isValidBody(body?: any): body is string | Buffer {
18 + return (body && typeof body === "string") || Buffer.isBuffer(body);
19 +}
20 +
21 +export default function middleware(config: Types.MiddlewareConfig): Middleware {
22 + if (!config.channelSecret) {
23 + throw new Error("no channel secret");
24 + }
25 +
26 + const secret = config.channelSecret;
27 +
28 + const _middleware: Middleware = async (req, res, next) => {
29 + // header names are lower-cased
30 + // https://nodejs.org/api/http.html#http_message_headers
31 + const signature = req.headers[
32 + Types.LINE_SIGNATURE_HTTP_HEADER_NAME
33 + ] as string;
34 +
35 + if (!signature) {
36 + next(new SignatureValidationFailed("no signature"));
37 + return;
38 + }
39 +
40 + const body = await (async (): Promise<string | Buffer> => {
41 + if (isValidBody((req as any).rawBody)) {
42 + // rawBody is provided in Google Cloud Functions and others
43 + return (req as any).rawBody;
44 + } else if (isValidBody(req.body)) {
45 + return req.body;
46 + } else {
47 + // body may not be parsed yet, parse it to a buffer
48 + return new Promise<Buffer>((resolve, reject) =>
49 + raw({ type: "*/*" })(req as any, res as any, (error: Error) =>
50 + error ? reject(error) : resolve(req.body),
51 + ),
52 + );
53 + }
54 + })();
55 +
56 + if (!validateSignature(body, secret, signature)) {
57 + next(
58 + new SignatureValidationFailed("signature validation failed", signature),
59 + );
60 + return;
61 + }
62 +
63 + const strBody = Buffer.isBuffer(body) ? body.toString() : body;
64 +
65 + try {
66 + req.body = JSON.parse(strBody);
67 + next();
68 + } catch (err) {
69 + next(new JSONParseError(err.message, strBody));
70 + }
71 + };
72 + return (req, res, next): void => {
73 + (<Promise<void>>_middleware(req, res, next)).catch(next);
74 + };
75 +}
1 +import { AxiosRequestConfig } from "axios";
2 +
3 +export interface Config {
4 + channelAccessToken?: string;
5 + channelSecret?: string;
6 +}
7 +
8 +export interface ClientConfig extends Config {
9 + channelAccessToken: string;
10 + httpConfig?: Partial<AxiosRequestConfig>;
11 +}
12 +
13 +export interface MiddlewareConfig extends Config {
14 + channelSecret: string;
15 +}
16 +
17 +export type Profile = {
18 + displayName: string;
19 + userId: string;
20 + pictureUrl: string;
21 + statusMessage: string;
22 + language?: string;
23 +};
24 +
25 +/**
26 + * Request body which is sent by webhook.
27 + *
28 + * @see [Request body](https://developers.line.biz/en/reference/messaging-api/#request-body)
29 + */
30 +export type WebhookRequestBody = {
31 + /**
32 + * 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}.
33 + */
34 + destination: string;
35 +
36 + /**
37 + * Information about the event
38 + */
39 + events: Array<WebhookEvent>;
40 +};
41 +
42 +/**
43 + * JSON objects which contain events generated on the LINE Platform.
44 + *
45 + * @see [Webhook event objects](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects)
46 + */
47 +export type WebhookEvent =
48 + | MessageEvent
49 + | UnsendEvent
50 + | FollowEvent
51 + | UnfollowEvent
52 + | JoinEvent
53 + | LeaveEvent
54 + | MemberJoinEvent
55 + | MemberLeaveEvent
56 + | PostbackEvent
57 + | VideoPlayCompleteEvent
58 + | BeaconEvent
59 + | AccountLinkEvent
60 + | DeviceLinkEvent
61 + | DeviceUnlinkEvent
62 + | LINEThingsScenarioExecutionEvent;
63 +
64 +export type EventBase = {
65 + /**
66 + * Channel state.
67 + *
68 + * `active`: The channel is active. You can send a reply message or push message from the bot server that received this webhook event.
69 + *
70 + * `standby`: The channel is waiting. The bot server that received this webhook event shouldn't send any messages.
71 + */
72 + mode: "active" | "standby";
73 + /**
74 + * Time of the event in milliseconds
75 + */
76 + timestamp: number;
77 + /**
78 + * Source user, group, or room object with information about the source of the event.
79 + */
80 + source: EventSource;
81 +};
82 +
83 +export type EventSource = User | Group | Room;
84 +
85 +export type User = { type: "user"; userId: string };
86 +
87 +export type Group = {
88 + type: "group";
89 + groupId: string;
90 + /**
91 + * ID of the source user.
92 + *
93 + * Only included in [message events](https://developers.line.biz/en/reference/messaging-api/#message-event).
94 + * Not included if the user has not agreed to the
95 + * [Official Accounts Terms of Use](https://developers.line.biz/en/docs/messaging-api/user-consent/).
96 + */
97 + userId?: string;
98 +};
99 +
100 +export type Room = {
101 + type: "room";
102 + roomId: string;
103 + /**
104 + * ID of the source user.
105 + *
106 + * Only included in [message events](https://developers.line.biz/en/reference/messaging-api/#message-event).
107 + * Not included if the user has not agreed to the
108 + * [Official Accounts Terms of Use](https://developers.line.biz/en/docs/messaging-api/user-consent/).
109 + */
110 + userId?: string;
111 +};
112 +
113 +export type ReplyableEvent = EventBase & { replyToken: string };
114 +
115 +/**
116 + * Webhook event object which contains the sent message.
117 + *
118 + * The `message` property contains a message object which corresponds with the
119 + * message type. You can reply to message events.
120 + *
121 + * @see [Message event](https://developers.line.biz/en/reference/messaging-api/#message-event)
122 + */
123 +export type MessageEvent = {
124 + type: "message";
125 + message: EventMessage;
126 +} & ReplyableEvent;
127 +
128 +/**
129 + * Event object for when the user unsends a message in a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
130 + * or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room).
131 + * [Unsend event](https://developers.line.biz/en/reference/messaging-api/#unsend-event)
132 + */
133 +export type UnsendEvent = {
134 + type: "unsend";
135 + /**
136 + * The message ID of the unsent message
137 + */
138 + unsend: { messageId: string };
139 +} & EventBase;
140 +
141 +/**
142 + * Event object for when your account is added as a friend (or unblocked).
143 + */
144 +export type FollowEvent = { type: "follow" } & ReplyableEvent;
145 +
146 +/**
147 + * Event object for when your account is blocked.
148 + */
149 +export type UnfollowEvent = { type: "unfollow" } & EventBase;
150 +
151 +/**
152 + * Event object for when your bot joins a group or room. You can reply to join events.
153 + *
154 + * A join event is triggered at different times for groups and rooms.
155 + *
156 + * - For groups: A join event is sent when a user invites your bot.
157 + * - For rooms: A join event is sent when the first event (for example when a
158 + * user sends a message or is added to the room) occurs after your bot is
159 + * added.
160 + */
161 +export type JoinEvent = { type: "join" } & ReplyableEvent;
162 +
163 +/**
164 + * Event object for when a user removes your bot from a group or a room.
165 + */
166 +export type LeaveEvent = { type: "leave" } & EventBase;
167 +
168 +/**
169 + * Event object for when a user joins a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
170 + * or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room) that the bot is in.
171 + */
172 +export type MemberJoinEvent = {
173 + type: "memberJoined";
174 + /**
175 + * User ID of users who joined
176 + * Array of [source user](https://developers.line.biz/en/reference/messaging-api/#source-user) objects
177 + */
178 + joined: { members: Array<User> };
179 +} & ReplyableEvent;
180 +
181 +/**
182 + * Event object for when a user leaves a [group](https://developers.line.biz/en/docs/messaging-api/group-chats/#group)
183 + * or [room](https://developers.line.biz/en/docs/messaging-api/group-chats/#room) that the bot is in.
184 + */
185 +export type MemberLeaveEvent = {
186 + type: "memberLeft";
187 + /**
188 + * User ID of users who left
189 + * Array of [source user](https://developers.line.biz/en/reference/messaging-api/#source-user) objects
190 + */
191 + left: { members: Array<User> };
192 +} & EventBase;
193 +
194 +/**
195 + * Event object for when a user performs an action on a
196 + * [template message](https://developers.line.biz/en/reference/messaging-api/#template-messages).
197 + */
198 +export type PostbackEvent = {
199 + type: "postback";
200 + postback: Postback;
201 +} & ReplyableEvent;
202 +
203 +/**
204 + * Event for when a user finishes viewing a video at least once with the specified trackingId sent by the LINE Official Account.
205 + */
206 +export type VideoPlayCompleteEvent = {
207 + type: "videoPlayComplete";
208 + /**
209 + * 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).
210 + * String
211 + */
212 + videoPlayComplete: { trackingId: string };
213 +} & ReplyableEvent;
214 +
215 +/**
216 + * Event object for when a user enters or leaves the range of a
217 + * [LINE Beacon](https://developers.line.biz/en/docs/messaging-api/using-beacons/).
218 + */
219 +export type BeaconEvent = ReplyableEvent & {
220 + type: "beacon";
221 + beacon: {
222 + /**
223 + * `leave` will be deprecated
224 + */
225 + type: "enter" | "leave" | "banner" | "stay";
226 +
227 + /**
228 + * Hardware ID of the beacon that was detected
229 + */
230 + hwid: string;
231 +
232 + /**
233 + * Device message of beacon that was detected.
234 + *
235 + * This message consists of data generated by the beacon to send notifications to bots.
236 + * Only included in webhooks from devices that support the "device message" property.
237 + * For more information, see the
238 + * [LINE Simple Beacon specification](https://github.com/line/line-simple-beacon/blob/master/README.en.md/#line-simple-beacon-frame).
239 + */
240 + dm?: string;
241 + };
242 +};
243 +
244 +/**
245 + * Event object for when a user has linked his/her LINE account with a provider's service account.
246 + */
247 +export type AccountLinkEvent = ReplyableEvent & {
248 + type: "accountLink";
249 + link: {
250 + result: "ok" | "failed";
251 +
252 + /**
253 + * Specified nonce when verifying the user ID
254 + */
255 + nonce: string;
256 + };
257 +};
258 +
259 +/**
260 + * Indicates that a LINE Things-compatible device has been linked with LINE by a user operation.
261 + * For more information, see [Receiving device link events via webhook](https://developers.line.biz/en/docs/line-things/develop-bot/#link-event).
262 + */
263 +export type DeviceLinkEvent = ReplyableEvent & {
264 + type: "things";
265 + things: {
266 + /**
267 + * Device ID of the LINE Things-compatible device that was linked with LINE
268 + */
269 + deviceId: string;
270 + type: "link";
271 + };
272 +};
273 +
274 +/**
275 + * Indicates that a LINE Things-compatible device has been unlinked from LINE by a user operation.
276 + * For more information, see [Receiving device unlink events via webhook](https://developers.line.biz/en/docs/line-things/develop-bot/#unlink-event).
277 + */
278 +export type DeviceUnlinkEvent = ReplyableEvent & {
279 + type: "things";
280 + things: {
281 + /**
282 + * Device ID of the LINE Things-compatible device that was unlinked with LINE
283 + */
284 + deviceId: string;
285 + type: "unlink";
286 + };
287 +};
288 +
289 +export type LINEThingsScenarioExecutionEvent = ReplyableEvent & {
290 + type: "things";
291 + things: {
292 + type: "scenarioResult";
293 + /**
294 + * Device ID of the device that executed the scenario
295 + */
296 + deviceId: string;
297 + result: {
298 + /**
299 + * Scenario ID executed
300 + */
301 + scenarioId: string;
302 + /**
303 + * Revision number of the scenario set containing the executed scenario
304 + */
305 + revision: number;
306 + /**
307 + * Timestamp for when execution of scenario action started (milliseconds, LINE app time)
308 + */
309 + startTime: number;
310 + /**
311 + * Timestamp for when execution of scenario was completed (milliseconds, LINE app time)
312 + */
313 + endtime: number;
314 + /**
315 + * Scenario execution completion status
316 + * See also [things.resultCode definitions](https://developers.line.biz/en/reference/messaging-api/#things-resultcode).
317 + */
318 + resultCode: "success" | "gatt_error" | "runtime_error";
319 + /**
320 + * Execution result of individual operations specified in action
321 + * Note that an array of actions specified in a scenario has the following characteristics
322 + * - The actions defined in a scenario are performed sequentially, from top to bottom.
323 + * - Each action produces some result when executed.
324 + * Even actions that do not generate data, such as `SLEEP`, return an execution result of type `void`.
325 + * The number of items in an action array may be 0.
326 + *
327 + * Therefore, things.actionResults has the following properties:
328 + * - The number of items in the array matches the number of actions defined in the scenario.
329 + * - The order of execution results matches the order in which actions are performed.
330 + * That is, in a scenario set with multiple `GATT_READ` actions,
331 + * the results are returned in the order in which each individual `GATT_READ` action was performed.
332 + * - If 0 actions are defined in the scenario, the number of items in things.actionResults will be 0.
333 + */
334 + actionResults: Array<LINEThingsActionResult>;
335 + /**
336 + * Data contained in notification
337 + * The value is Base64-encoded binary data.
338 + * Only included for scenarios where `trigger.type = BLE_NOTIFICATION`.
339 + */
340 + bleNotificationPayload?: string;
341 + /**
342 + * Error reason
343 + */
344 + errorReason?: string;
345 + };
346 + };
347 +};
348 +
349 +export type LINEThingsActionResult = {
350 + /**
351 + * `void`, `binary`
352 + * Depends on `type` of the executed action.
353 + * This property is always included if `things.actionResults` is not empty.
354 + */
355 + type: "void" | "binary";
356 + /**
357 + * Base64-encoded binary data
358 + * This property is always included when `things.actionResults[].type` is `binary`.
359 + */
360 + data?: string;
361 +};
362 +
363 +export type EventMessage =
364 + | TextEventMessage
365 + | ImageEventMessage
366 + | VideoEventMessage
367 + | AudioEventMessage
368 + | LocationEventMessage
369 + | FileEventMessage
370 + | StickerEventMessage;
371 +
372 +export type EventMessageBase = { id: string };
373 +
374 +/**
375 + * Message object which contains the text sent from the source.
376 + */
377 +export type TextEventMessage = {
378 + type: "text";
379 + text: string;
380 + /**
381 + * Sendable LINE emojis
382 + */
383 + emojis?: {
384 + index: number;
385 + length: number;
386 + productId: string;
387 + emojiId: string;
388 + }[];
389 + /**
390 + * Object containing the contents of the mentioned user.
391 + */
392 + mention?: {
393 + /**
394 + * Mentioned user information.
395 + * Max: 20 mentions
396 + */
397 + mentionees: {
398 + /**
399 + * Index position of the user mention for a character in `text`,
400 + * with the first character being at position 0.
401 + */
402 + index: number;
403 + /**
404 + * The length of the text of the mentioned user. For a mention `@example`,
405 + * 8 is the length.
406 + */
407 + length: number;
408 + userId: string;
409 + }[];
410 + };
411 +} & EventMessageBase;
412 +
413 +export type ContentProvider<WithPreview extends boolean = true> =
414 + | {
415 + /**
416 + * The content is provided by LINE.
417 + *
418 + * The data itself can be retrieved from the content API.
419 + */
420 + type: "line";
421 + }
422 + | {
423 + /**
424 + * The content is provided by a provider other than LINE
425 + */
426 + type: "external";
427 + /**
428 + * URL of the content. Only included when contentProvider.type is external.
429 + */
430 + originalContentUrl: string;
431 + /**
432 + * URL of the content preview. Only included when contentProvider.type is external.
433 + *
434 + * For contents without preview (e.g. audio), it's undefined.
435 + */
436 + previewImageUrl: WithPreview extends true ? string : undefined;
437 + };
438 +
439 +/**
440 + * Message object which contains the image content sent from the source.
441 + * The binary image data can be retrieved using Client#getMessageContent.
442 + */
443 +export type ImageEventMessage = {
444 + type: "image";
445 + contentProvider: ContentProvider;
446 +} & EventMessageBase;
447 +
448 +/**
449 + * Message object which contains the video content sent from the source.
450 + * The binary video data can be retrieved using Client#getMessageContent.
451 + */
452 +export type VideoEventMessage = {
453 + type: "video";
454 + contentProvider: ContentProvider;
455 +} & EventMessageBase;
456 +
457 +/**
458 + * Message object which contains the audio content sent from the source.
459 + * The binary audio data can be retrieved using Client#getMessageContent.
460 + */
461 +export type AudioEventMessage = {
462 + type: "audio";
463 + duration: number;
464 + contentProvider: ContentProvider<false>;
465 +} & EventMessageBase;
466 +
467 +/**
468 + * Message object which contains the file sent from the source.
469 + * The binary data can be retrieved using Client#getMessageContent.
470 + */
471 +export type FileEventMessage = {
472 + type: "file";
473 + fileName: string;
474 + fileSize: string;
475 +} & EventMessageBase;
476 +
477 +/**
478 + * Message object which contains the location data sent from the source.
479 + */
480 +export type LocationEventMessage = {
481 + type: "location";
482 + title: string;
483 + address: string;
484 + latitude: number;
485 + longitude: number;
486 +} & EventMessageBase;
487 +
488 +/**
489 + * Message object which contains the sticker data sent from the source.
490 + * For a list of basic LINE stickers and sticker IDs, see
491 + * [sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
492 + */
493 +export type StickerEventMessage = {
494 + type: "sticker";
495 + packageId: string;
496 + stickerId: string;
497 + stickerResourceType:
498 + | "STATIC"
499 + | "ANIMATION"
500 + | "SOUND"
501 + | "ANIMATION_SOUND"
502 + | "POPUP"
503 + | "POPUP_SOUND"
504 + | "NAME_TEXT"
505 + | "PER_STICKER_TEXT";
506 + keywords: string[];
507 +} & EventMessageBase;
508 +
509 +export type Postback = {
510 + data: string;
511 + /**
512 + * Object with the date and time selected by a user through a
513 + * [datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action).
514 + * Only returned for postback actions via a
515 + * [datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action).
516 + * The `full-date`, `time-hour`, and `time-minute` formats follow the
517 + * [RFC3339 protocol](https://www.ietf.org/rfc/rfc3339.txt).
518 + */
519 + params?: {
520 + /**
521 + * Date selected by user. Only included in the `date` mode.
522 + */
523 + date?: string;
524 + /**
525 + * Time selected by the user. Only included in the `time` mode.
526 + */
527 + time?: string;
528 + /**
529 + * Date and time selected by the user. Only included in the `datetime` mode.
530 + */
531 + datetime?: string;
532 + };
533 +};
534 +
535 +/**
536 + * JSON object which contains the contents of the message you send.
537 + *
538 + * @see [Message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects)
539 + */
540 +export type Message =
541 + | TextMessage
542 + | ImageMessage
543 + | VideoMessage
544 + | AudioMessage
545 + | LocationMessage
546 + | StickerMessage
547 + | ImageMapMessage
548 + | TemplateMessage
549 + | FlexMessage;
550 +
551 +/**
552 + * @see [Common properties for messages](https://developers.line.biz/en/reference/messaging-api/#common-properties-for-messages)
553 + */
554 +export type MessageCommon = {
555 + /**
556 + * For the quick reply feature.
557 + * For more information, see [Using quick replies](https://developers.line.biz/en/docs/messaging-api/using-quick-reply/).
558 + *
559 + * If the user receives multiple
560 + * [message objects](https://developers.line.biz/en/reference/messaging-api/#message-objects),
561 + * the quickReply property of the last message object is displayed.
562 + */
563 + quickReply?: QuickReply;
564 + /**
565 + * [Change icon and display name](https://developers.line.biz/en/docs/messaging-api/icon-nickname-switch/)
566 + *
567 + * 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).
568 + */
569 + sender?: Sender;
570 +};
571 +
572 +/**
573 + * @see [Text message](https://developers.line.biz/en/reference/messaging-api/#text-message)
574 + */
575 +export type TextMessage = MessageCommon & {
576 + type: "text";
577 + /**
578 + * Message text. You can include the following emoji:
579 + *
580 + * - Unicode emoji
581 + * - LINE original emoji
582 + * ([Unicode codepoint table for LINE original emoji](https://developers.line.biz/media/messaging-api/emoji-list.pdf))
583 + *
584 + * Max: 2000 characters
585 + */
586 + text: string;
587 +};
588 +
589 +/**
590 + * @see [Image message](https://developers.line.biz/en/reference/messaging-api/#image-message)
591 + */
592 +export type ImageMessage = MessageCommon & {
593 + type: "image";
594 + /**
595 + * Image URL (Max: 1000 characters)
596 + *
597 + * - **HTTPS**
598 + * - JPEG
599 + * - Max: 1024 x 1024
600 + * - Max: 1 MB
601 + */
602 + originalContentUrl: string;
603 + /**
604 + * Preview image URL (Max: 1000 characters)
605 + *
606 + * - **HTTPS**
607 + * - JPEG
608 + * - Max: 240 x 240
609 + * - Max: 1 MB
610 + */
611 + previewImageUrl: string;
612 +};
613 +
614 +/**
615 + * @see [Video message](https://developers.line.biz/en/reference/messaging-api/#video-message)
616 + */
617 +export type VideoMessage = MessageCommon & {
618 + type: "video";
619 + /**
620 + * URL of video file (Max: 1000 characters)
621 + *
622 + * - **HTTPS**
623 + * - mp4
624 + * - Max: 1 minute
625 + * - Max: 10 MB
626 + *
627 + * A very wide or tall video may be cropped when played in some environments.
628 + */
629 + originalContentUrl: string;
630 + /**
631 + * URL of preview image (Max: 1000 characters)
632 + *
633 + * - **HTTPS**
634 + * - JPEG
635 + * - Max: 240 x 240
636 + * - Max: 1 MB
637 + */
638 + previewImageUrl: string;
639 +};
640 +
641 +/**
642 + * @see [Audio message](https://developers.line.biz/en/reference/messaging-api/#audio-message)
643 + */
644 +export type AudioMessage = MessageCommon & {
645 + type: "audio";
646 + /**
647 + * URL of audio file (Max: 1000 characters)
648 + *
649 + * - **HTTPS**
650 + * - m4a
651 + * - Max: 1 minute
652 + * - Max: 10 MB
653 + */
654 + originalContentUrl: string;
655 + /**
656 + * Length of audio file (milliseconds)
657 + */
658 + duration: number;
659 +};
660 +
661 +/**
662 + * @see [Location message](https://developers.line.biz/en/reference/messaging-api/#location-message)
663 + */
664 +export type LocationMessage = MessageCommon & {
665 + type: "location";
666 + /**
667 + * Title (Max: 100 characters)
668 + */
669 + title: string;
670 + /**
671 + * Address (Max: 100 characters)
672 + */
673 + address: string;
674 + latitude: number;
675 + longitude: number;
676 +};
677 +
678 +/**
679 + * @see [Sticker message](https://developers.line.biz/en/reference/messaging-api/#sticker-message)
680 + */
681 +export type StickerMessage = MessageCommon & {
682 + type: "sticker";
683 + /**
684 + * Package ID for a set of stickers.
685 + * For information on package IDs, see the
686 + * [Sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
687 + */
688 + packageId: string;
689 + /**
690 + * Sticker ID.
691 + * For a list of sticker IDs for stickers that can be sent with the Messaging
692 + * API, see the
693 + * [Sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf).
694 + */
695 + stickerId: string;
696 +};
697 +
698 +/**
699 + * @see [Imagemap message](https://developers.line.biz/en/reference/messaging-api/#imagemap-message)
700 + */
701 +export type ImageMapMessage = MessageCommon & {
702 + type: "imagemap";
703 + /**
704 + * [Base URL](https://developers.line.biz/en/reference/messaging-api/#base-url) of image
705 + * (Max: 1000 characters, **HTTPS**)
706 + */
707 + baseUrl: string;
708 + /**
709 + * Alternative text (Max: 400 characters)
710 + */
711 + altText: string;
712 + baseSize: Size;
713 + /**
714 + * Video to play inside a image map messages
715 + */
716 + video?: {
717 + /**
718 + * URL of video file (Max: 1000 characters)
719 + *
720 + * - **HTTPS**
721 + * - mp4
722 + * - Max: 1 minute
723 + * - Max: 10 MB
724 + *
725 + * A very wide or tall video may be cropped when played in some environments.
726 + */
727 + originalContentUrl: string;
728 + /**
729 + * URL of preview image (Max: 1000 characters)
730 + *
731 + * - **HTTPS**
732 + * - JPEG
733 + * - Max: 240 x 240
734 + * - Max: 1 MB
735 + */
736 + previewImageUrl: string;
737 + area: Area;
738 + /**
739 + * External link to be displayed after a video is played
740 + * This property is required if you set a video to play and a label to display after the video on the imagemap
741 + */
742 + externalLink?: {
743 + linkUri: string;
744 + label: string;
745 + };
746 + };
747 + /**
748 + * Action when tapped (Max: 50)
749 + */
750 + actions: ImageMapAction[];
751 +};
752 +
753 +/**
754 + * Template messages are messages with predefined layouts which you can
755 + * customize. For more information, see
756 + * [template messages](https://developers.line.biz/en/docs/messaging-api/message-types/#template-messages).
757 + *
758 + * The following template types are available:
759 + *
760 + * - [Buttons](https://developers.line.biz/en/reference/messaging-api/#buttons)
761 + * - [Confirm](https://developers.line.biz/en/reference/messaging-api/#confirm)
762 + * - [Carousel](https://developers.line.biz/en/reference/messaging-api/#carousel)
763 + * - [Image carousel](https://developers.line.biz/en/reference/messaging-api/#image-carousel)
764 + *
765 + * @see [Template messages](https://developers.line.biz/en/reference/messaging-api/#template-messages)
766 + */
767 +export type TemplateMessage = MessageCommon & {
768 + type: "template";
769 + /**
770 + * Alternative text (Max: 400 characters)
771 + */
772 + altText: string;
773 + /**
774 + * Carousel template content
775 + */
776 + template: TemplateContent;
777 +};
778 +
779 +/**
780 + * Flex Messages are messages with a customizable layout.
781 + * You can customize the layout freely by combining multiple elements.
782 + * For more information, see
783 + * [Using Flex Messages](https://developers.line.biz/en/docs/messaging-api/using-flex-messages/).
784 + *
785 + * @see [Flex messages](https://developers.line.biz/en/reference/messaging-api/#flex-message)
786 + */
787 +export type FlexMessage = MessageCommon & {
788 + type: "flex";
789 + altText: string;
790 + contents: FlexContainer;
791 +};
792 +
793 +/**
794 + * Object which specifies the actions and tappable regions of an imagemap.
795 + *
796 + * When a region is tapped, the user is redirected to the URI specified in
797 + * `uri` and the message specified in `message` is sent.
798 + *
799 + * @see [Imagemap action objects](https://developers.line.biz/en/reference/messaging-api/#imagemap-action-objects)
800 + */
801 +export type ImageMapAction = ImageMapURIAction | ImageMapMessageAction;
802 +
803 +export type ImageMapActionBase = {
804 + /**
805 + * Spoken when the accessibility feature is enabled on the client device. (Max: 50 characters)
806 + * Supported on LINE 8.2.0 and later for iOS.
807 + */
808 + label?: string;
809 + /** Defined tappable area */
810 + area: Area;
811 +};
812 +
813 +export type ImageMapURIAction = {
814 + type: "uri";
815 + /**
816 + * Webpage URL (Max: 1000 characters)
817 + */
818 + linkUri: string;
819 +} & ImageMapActionBase;
820 +
821 +export type ImageMapMessageAction = {
822 + type: "message";
823 + /**
824 + * Message to send (Max: 400 characters)
825 + */
826 + text: string;
827 +} & ImageMapActionBase;
828 +
829 +export type Area = {
830 + /**
831 + * Horizontal position relative to the top-left corner of the area
832 + */
833 + x: number;
834 + /**
835 + * Vertical position relative to the top-left corner of the area
836 + */
837 + y: number;
838 + /**
839 + * Width of the tappable area
840 + */
841 + width: number;
842 + /**
843 + * Height of the tappable area
844 + */
845 + height: number;
846 +};
847 +
848 +/**
849 + * A container is the top-level structure of a Flex Message. Here are the types of containers available.
850 + *
851 + * - [Bubble](https://developers.line.biz/en/reference/messaging-api/#bubble)
852 + * - [Carousel](https://developers.line.biz/en/reference/messaging-api/#f-carousel)
853 + *
854 + * See [Flex Message elements](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/)
855 + * for the containers' JSON data samples and usage.
856 + */
857 +export type FlexContainer = FlexBubble | FlexCarousel;
858 +
859 +/**
860 + * This is a container that contains one message bubble. It can contain four
861 + * blocks: header, hero, body, and footer.
862 + *
863 + * For more information about using each block, see
864 + * [Block](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/#block).
865 + */
866 +export type FlexBubble = {
867 + type: "bubble";
868 + size?: "nano" | "micro" | "kilo" | "mega" | "giga";
869 + /**
870 + * Text directionality and the order of components in horizontal boxes in the
871 + * container. Specify one of the following values:
872 + *
873 + * - `ltr`: Left to right
874 + * - `rtl`: Right to left
875 + *
876 + * The default value is `ltr`.
877 + */
878 + direction?: "ltr" | "rtl";
879 + header?: FlexBox;
880 + hero?: FlexBox | FlexImage;
881 + body?: FlexBox;
882 + footer?: FlexBox;
883 + styles?: FlexBubbleStyle;
884 + action?: Action;
885 +};
886 +
887 +export type FlexBubbleStyle = {
888 + header?: FlexBlockStyle;
889 + hero?: FlexBlockStyle;
890 + body?: FlexBlockStyle;
891 + footer?: FlexBlockStyle;
892 +};
893 +
894 +export type FlexBlockStyle = {
895 + /**
896 + * Background color of the block. Use a hexadecimal color code.
897 + */
898 + backgroundColor?: string;
899 + /**
900 + * - `true` to place a separator above the block.
901 + * - `true` will be ignored for the first block in a container because you
902 + * cannot place a separator above the first block.
903 + * - The default value is `false`.
904 + */
905 + separator?: boolean;
906 + /**
907 + * Color of the separator. Use a hexadecimal color code.
908 + */
909 + separatorColor?: string;
910 +};
911 +
912 +export type FlexCarousel = {
913 + type: "carousel";
914 + /**
915 + * (Max: 12 bubbles)
916 + */
917 + contents: FlexBubble[];
918 +};
919 +
920 +/**
921 + * Components are objects that compose a Flex Message container. Here are the
922 + * types of components available:
923 + *
924 + * - [Box](https://developers.line.biz/en/reference/messaging-api/#box)
925 + * - [Button](https://developers.line.biz/en/reference/messaging-api/#button)
926 + * - [Image](https://developers.line.biz/en/reference/messaging-api/#f-image)
927 + * - [Icon](https://developers.line.biz/en/reference/messaging-api/#icon)
928 + * - [Text](https://developers.line.biz/en/reference/messaging-api/#f-text)
929 + * - [Span](https://developers.line.biz/en/reference/messaging-api/#span)
930 + * - [Separator](https://developers.line.biz/en/reference/messaging-api/#separator)
931 + * - [Filler](https://developers.line.biz/en/reference/messaging-api/#filler)
932 + * - [Spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
933 + *
934 + * See the followings for the components' JSON data samples and usage.
935 + *
936 + * - [Flex Message elements](https://developers.line.biz/en/docs/messaging-api/flex-message-elements/)
937 + * - [Flex Message layout](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/)
938 + */
939 +export type FlexComponent =
940 + | FlexBox
941 + | FlexButton
942 + | FlexImage
943 + | FlexIcon
944 + | FlexText
945 + | FlexSpan
946 + | FlexSeparator
947 + | FlexFiller
948 + | FlexSpacer;
949 +
950 +/**
951 + * This is a component that defines the layout of child components.
952 + * You can also include a box in a box.
953 + */
954 +export type FlexBox = {
955 + type: "box";
956 + /**
957 + * The placement style of components in this box. Specify one of the following values:
958 + *
959 + * - `horizontal`: Components are placed horizontally. The `direction`
960 + * property of the [bubble](https://developers.line.biz/en/reference/messaging-api/#bubble)
961 + * container specifies the order.
962 + * - `vertical`: Components are placed vertically from top to bottom.
963 + * - `baseline`: Components are placed in the same way as `horizontal` is
964 + * specified except the baselines of the components are aligned.
965 + *
966 + * For more information, see
967 + * [Types of box layouts](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#box-layout-types).
968 + */
969 + layout: "horizontal" | "vertical" | "baseline";
970 + /**
971 + * Components in this box. Here are the types of components available:
972 + *
973 + * - When the `layout` property is `horizontal` or `vertical`:
974 + * + [Box](https://developers.line.biz/en/reference/messaging-api/#box)
975 + * + [button](https://developers.line.biz/en/reference/messaging-api/#button)
976 + * + [image](https://developers.line.biz/en/reference/messaging-api/#f-image)
977 + * + [text](https://developers.line.biz/en/reference/messaging-api/#f-text)
978 + * + [separator](https://developers.line.biz/en/reference/messaging-api/#separator)
979 + * + [filler](https://developers.line.biz/en/reference/messaging-api/#filler)
980 + * + [spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
981 + * - When the `layout` property is `baseline`:
982 + * + [icon](https://developers.line.biz/en/reference/messaging-api/#icon)
983 + * + [text](https://developers.line.biz/en/reference/messaging-api/#f-text)
984 + * + [filler](https://developers.line.biz/en/reference/messaging-api/#filler)
985 + * + [spacer (not recommended)](https://developers.line.biz/en/reference/messaging-api/#spacer)
986 + */
987 + contents: FlexComponent[];
988 + /**
989 + * Background color of the block. In addition to the RGB color, an alpha
990 + * channel (transparency) can also be set. Use a hexadecimal color code.
991 + * (Example:#RRGGBBAA) The default value is `#00000000`.
992 + */
993 + backgroundColor?: string;
994 + /**
995 + * Color of box border. Use a hexadecimal color code.
996 + */
997 + borderColor?: string;
998 + /**
999 + * Width of box border. You can specify a value in pixels or any one of none,
1000 + * light, normal, medium, semi-bold, or bold. none does not render a border
1001 + * while the others become wider in the order of listing.
1002 + */
1003 + borderWidth?:
1004 + | string
1005 + | "none"
1006 + | "light"
1007 + | "normal"
1008 + | "medium"
1009 + | "semi-bold"
1010 + | "bold";
1011 + /**
1012 + * Radius at the time of rounding the corners of the border. You can specify a
1013 + * value in pixels or any one of `none`, `xs`, `sm`, `md`, `lg`, `xl`, or `xxl`. none does not
1014 + * round the corner while the others increase in radius in the order of listing. The default value is none.
1015 + */
1016 + cornerRadius?: string | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1017 + /**
1018 + * 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.
1019 + */
1020 + width?: string;
1021 + /**
1022 + * 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.
1023 + */
1024 + height?: string;
1025 + /**
1026 + * The ratio of the width or height of this box within the parent box. The
1027 + * default value for the horizontal parent box is `1`, and the default value
1028 + * for the vertical parent box is `0`.
1029 + *
1030 + * For more information, see
1031 + * [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
1032 + */
1033 + flex?: number;
1034 + /**
1035 + * Minimum space between components in this box.
1036 + *
1037 + * - `none` does not set a space while the other values set a space whose
1038 + * size increases in the order of listing.
1039 + * - The default value is `none`.
1040 + * - To override this setting for a specific component, set the `margin`
1041 + * property of that component.
1042 + */
1043 + spacing?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1044 + /**
1045 + * Minimum space between this box and the previous component in the parent box.
1046 + *
1047 + * - `none` does not set a space while the other values set a space whose
1048 + * size increases in the order of listing.
1049 + * - The default value is the value of the `spacing` property of the parent
1050 + * box.
1051 + * - If this box is the first component in the parent box, the `margin`
1052 + * property will be ignored.
1053 + */
1054 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1055 + /**
1056 + * Free space between the borders of this box and the child element.
1057 + * For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
1058 + */
1059 + paddingAll?: string;
1060 + /**
1061 + * Free space between the border at the upper end of this box and the upper end of the child element.
1062 + * For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
1063 + */
1064 + paddingTop?: string;
1065 + /**
1066 + * Free space between the border at the lower end of this box and the lower end of the child element.
1067 + * For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
1068 + */
1069 + paddingBottom?: string;
1070 + /**
1071 + * Free space between the border at the left end of this box and the left end of the child element.
1072 + * For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
1073 + */
1074 + paddingStart?: string;
1075 + /**
1076 + * Free space between the border at the right end of this box and the right end of the child element.
1077 + * For more information, see [Box padding](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#padding-property) in the API documentation.
1078 + */
1079 + paddingEnd?: string;
1080 + /**
1081 + * Action performed when this button is tapped.
1082 + *
1083 + * Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
1084 + */
1085 + action?: Action;
1086 + /**
1087 + * How child elements are aligned along the main axis of the parent element. If the
1088 + * parent element is a horizontal box, this only takes effect when its child elements have
1089 + * 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)
1090 + * in the Messaging API documentation.
1091 + */
1092 + justifyContent?:
1093 + | "flex-start"
1094 + | "center"
1095 + | "flex-end"
1096 + | "space-between"
1097 + | "space-around"
1098 + | "space-evenly";
1099 + /**
1100 + * How child elements are aligned along the cross axis of the parent element. For more
1101 + * 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.
1102 + */
1103 + alignItems?: "flex-start" | "center" | "flex-end";
1104 + background?: Background;
1105 +} & Offset;
1106 +
1107 +export type Offset = {
1108 + /**
1109 + * Reference position for placing this box. Specify one of the following values:
1110 + * - `relative`: Use the previous box as reference.
1111 + * - `absolute`: Use the top left of parent element as reference.
1112 + *
1113 + * The default value is relative.
1114 + * For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
1115 + */
1116 + position?: "relative" | "absolute";
1117 + /**
1118 + * The top offset.
1119 + * For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
1120 + */
1121 + offsetTop?: string;
1122 + /**
1123 + * The bottom offset.
1124 + * For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
1125 + */
1126 + offsetBottom?: string;
1127 + /**
1128 + * The left offset.
1129 + * For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
1130 + */
1131 + offsetStart?: string;
1132 + /**
1133 + * The right offset.
1134 + * For more information, see [Offset](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-offset) in the API documentation.
1135 + */
1136 + offsetEnd?: string;
1137 +};
1138 +
1139 +export type Background = {
1140 + /**
1141 + * The type of background used. Specify these values:
1142 + * - `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.
1143 + */
1144 + type: "linearGradient";
1145 + /**
1146 + * The angle at which a linear gradient moves. Specify the angle using an integer value
1147 + * like `90deg` (90 degrees) or a decimal number like `23.5deg` (23.5 degrees) in the
1148 + * half-open interval [0, 360). The direction of the linear gradient rotates clockwise as the
1149 + * angle increases. Given a value of `0deg`, the gradient starts at the bottom and ends at
1150 + * the top; given a value of `45deg`, the gradient starts at the bottom-left corner and ends
1151 + * at the top-right corner; given a value of 90deg, the gradient starts at the left and ends
1152 + * at the right; and given a value of `180deg`, the gradient starts at the top and ends at
1153 + * 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.
1154 + */
1155 + angle: string;
1156 + /**
1157 + * The color at the gradient's starting point. Use a hexadecimal color code in the
1158 + * `#RRGGBB` or `#RRGGBBAA` format.
1159 + */
1160 + startColor: string;
1161 + /**
1162 + * The color at the gradient's ending point. Use a hexadecimal color code in the
1163 + * `#RRGGBB` or `#RRGGBBAA` format.
1164 + */
1165 + endColor: string;
1166 + /**
1167 + * The color in the middle of the gradient. Use a hexadecimal color code in the `#RRGGBB`
1168 + * or `#RRGGBBAA` format. Specify a value for the `background.centerColor` property to
1169 + * 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
1170 + * Messaging API documentation.
1171 + */
1172 + centerColor?: string;
1173 + /**
1174 + * The position of the intermediate color stop. Specify an integer or decimal value
1175 + * between `0%` (the starting point) and `100%` (the ending point). This is `50%` by
1176 + * 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
1177 + * Messaging API documentation.
1178 + */
1179 + centerPosition?: string;
1180 +};
1181 +
1182 +/**
1183 + * This component draws a button.
1184 + *
1185 + * When the user taps a button, a specified action is performed.
1186 + */
1187 +export type FlexButton = {
1188 + type: "button";
1189 + /**
1190 + * Action performed when this button is tapped.
1191 + *
1192 + * Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
1193 + */
1194 + action: Action;
1195 + /**
1196 + * The ratio of the width or height of this box within the parent box.
1197 + *
1198 + * The default value for the horizontal parent box is `1`, and the default
1199 + * value for the vertical parent box is `0`.
1200 + *
1201 + * For more information, see
1202 + * [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
1203 + */
1204 + flex?: number;
1205 + /**
1206 + * Minimum space between this box and the previous component in the parent box.
1207 + *
1208 + * - `none` does not set a space while the other values set a space whose
1209 + * size increases in the order of listing.
1210 + * - The default value is the value of the `spacing` property of the parent
1211 + * box.
1212 + * - If this box is the first component in the parent box, the `margin`
1213 + * property will be ignored.
1214 + */
1215 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1216 + /**
1217 + * Height of the button. The default value is `md`.
1218 + */
1219 + height?: "sm" | "md";
1220 + /**
1221 + * Style of the button. Specify one of the following values:
1222 + *
1223 + * - `link`: HTML link style
1224 + * - `primary`: Style for dark color buttons
1225 + * - `secondary`: Style for light color buttons
1226 + *
1227 + * The default value is `link`.
1228 + */
1229 + style?: "link" | "primary" | "secondary";
1230 + /**
1231 + * Use a hexadecimal color code.
1232 + *
1233 + * - Character color when the `style` property is `link`.
1234 + * - Background color when the `style` property is `primary` or `secondary`.
1235 + */
1236 + color?: string;
1237 + /**
1238 + * Vertical alignment style. Specify one of the following values:
1239 + *
1240 + * - `top`: Top-aligned
1241 + * - `bottom`: Bottom-aligned
1242 + * - `center`: Center-aligned
1243 + *
1244 + * The default value is `top`.
1245 + *
1246 + * If the `layout` property of the parent box is `baseline`, the `gravity`
1247 + * property will be ignored.
1248 + */
1249 + gravity?: "top" | "bottom" | "center";
1250 + /**
1251 + * The method by which to adjust the text font size. Specify this value:
1252 + *
1253 + * - `shrink-to-fit`: Automatically shrink the font
1254 + * size to fit the width of the component. This
1255 + * property takes a "best-effort" approach that may
1256 + * work differently—or not at all!—on some platforms.
1257 + * For more information, see [Automatically shrink fonts to fit](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#adjusts-fontsize-to-fit)
1258 + * in the Messaging API documentation.
1259 + * - LINE 10.13.0 or later for iOS and Android
1260 + */
1261 + adjustMode?: "shrink-to-fit";
1262 +} & Offset;
1263 +
1264 +/**
1265 + * This is an invisible component to fill extra space between components.
1266 + *
1267 + * - The filler's `flex` property is fixed to 1.
1268 + * - The `spacing` property of the parent box will be ignored for fillers.
1269 + */
1270 +export type FlexFiller = {
1271 + type: "filler";
1272 + /**
1273 + * 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).
1274 + */
1275 + flex?: number;
1276 +};
1277 +
1278 +/**
1279 + * This component draws an icon.
1280 + */
1281 +export type FlexIcon = {
1282 + type: "icon";
1283 + /**
1284 + * Image URL
1285 + *
1286 + * Protocol: HTTPS
1287 + * Image format: JPEG or PNG
1288 + * Maximum image size: 240×240 pixels
1289 + * Maximum data size: 1 MB
1290 + */
1291 + url: string;
1292 + /**
1293 + * Minimum space between this box and the previous component in the parent
1294 + * box.
1295 + *
1296 + * - `none` does not set a space while the other values set a space whose
1297 + * size increases in the order of listing.
1298 + * - The default value is the value of the `spacing` property of the parent
1299 + * box.
1300 + * - If this box is the first component in the parent box, the `margin`
1301 + * property will be ignored.
1302 + */
1303 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1304 + /**
1305 + * Maximum size of the icon width.
1306 + * The size increases in the order of listing.
1307 + * The default value is `md`.
1308 + * 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.
1309 + */
1310 + size?:
1311 + | string
1312 + | "xxs"
1313 + | "xs"
1314 + | "sm"
1315 + | "md"
1316 + | "lg"
1317 + | "xl"
1318 + | "xxl"
1319 + | "3xl"
1320 + | "4xl"
1321 + | "5xl";
1322 + /**
1323 + * Aspect ratio of the icon. `{width}:{height}` format.
1324 + * The values of `{width}` and `{height}` must be in the range 1–100000.
1325 + * `{height}` can't be more than three times the value of `{width}`.
1326 + * The default value is `1:1`.
1327 + */
1328 + aspectRatio?: string;
1329 +} & Offset;
1330 +
1331 +/**
1332 + * This component draws an image.
1333 + */
1334 +export type FlexImage = {
1335 + type: "image";
1336 + /**
1337 + * Image URL
1338 + *
1339 + * - Protocol: HTTPS
1340 + * - Image format: JPEG or PNG
1341 + * - Maximum image size: 1024×1024 pixels
1342 + * - Maximum data size: 1 MB
1343 + */
1344 + url: string;
1345 + /**
1346 + * The ratio of the width or height of this box within the parent box.
1347 + *
1348 + * The default value for the horizontal parent box is `1`, and the default
1349 + * value for the vertical parent box is `0`.
1350 + *
1351 + * - For more information, see
1352 + * [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
1353 + */
1354 + flex?: number;
1355 + /**
1356 + * Minimum space between this box and the previous component in the parent
1357 + * box.
1358 + *
1359 + * - `none` does not set a space while the other values set a space whose
1360 + * size increases in the order of listing.
1361 + * - The default value is the value of the `spacing` property of the parent
1362 + * box.
1363 + * - If this box is the first component in the parent box, the `margin`
1364 + * property will be ignored.
1365 + */
1366 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1367 + /**
1368 + * Horizontal alignment style. Specify one of the following values:
1369 + *
1370 + * - `start`: Left-aligned
1371 + * - `end`: Right-aligned
1372 + * - `center`: Center-aligned
1373 + *
1374 + * The default value is `center`.
1375 + */
1376 + align?: "start" | "end" | "center";
1377 + /**
1378 + * Vertical alignment style. Specify one of the following values:
1379 + *
1380 + * - `top`: Top-aligned
1381 + * - `bottom`: Bottom-aligned
1382 + * - `center`: Center-aligned
1383 + *
1384 + * The default value is `top`.
1385 + *
1386 + * If the `layout` property of the parent box is `baseline`, the `gravity` property will be ignored.
1387 + */
1388 + gravity?: "top" | "bottom" | "center";
1389 + /**
1390 + * Maximum size of the image width.
1391 + * The size increases in the order of listing.
1392 + * The default value is `md`.
1393 + * For more information, see [Image size](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#image-size) in the Messaging API documentation.
1394 + */
1395 + size?:
1396 + | string
1397 + | "xxs"
1398 + | "xs"
1399 + | "sm"
1400 + | "md"
1401 + | "lg"
1402 + | "xl"
1403 + | "xxl"
1404 + | "3xl"
1405 + | "4xl"
1406 + | "5xl"
1407 + | "full";
1408 + /**
1409 + * Aspect ratio of the image. `{width}:{height}` format.
1410 + * Specify the value of `{width}` and `{height}` in the range from 1 to 100000. However,
1411 + * you cannot set `{height}` to a value that is more than three times the value of `{width}`.
1412 + * The default value is `1:1`.
1413 + */
1414 + aspectRatio?: string;
1415 + /**
1416 + * Style of the image. Specify one of the following values:
1417 + *
1418 + * - `cover`: The image fills the entire drawing area. Parts of the image
1419 + * that do not fit in the drawing area are not displayed.
1420 + * - `fit`: The entire image is displayed in the drawing area. The background
1421 + * is displayed in the unused areas to the left and right of vertical images
1422 + * and in the areas above and below horizontal images.
1423 + *
1424 + * The default value is `fit`.
1425 + */
1426 + aspectMode?: "cover" | "fit";
1427 + /**
1428 + * Background color of the image. Use a hexadecimal color code.
1429 + */
1430 + backgroundColor?: string;
1431 + /**
1432 + * Action performed when this button is tapped.
1433 + * Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
1434 + */
1435 + action?: Action;
1436 + /**
1437 + * When this is `true`, an animated image (APNG) plays.
1438 + * You can specify a value of `true` up to three times in a single message.
1439 + * You can't send messages that exceed this limit.
1440 + * This is `false` by default.
1441 + * Animated images larger than 300 KB aren't played back.
1442 + */
1443 + animated?: Boolean;
1444 +} & Offset;
1445 +
1446 +/**
1447 + * This component draws a separator between components in the parent box.
1448 + */
1449 +export type FlexSeparator = {
1450 + type: "separator";
1451 + /**
1452 + * Minimum space between this box and the previous component in the parent
1453 + * box.
1454 + *
1455 + * - `none` does not set a space while the other values set a space whose
1456 + * size increases in the order of listing.
1457 + * - The default value is the value of the `spacing` property of the parent
1458 + * box.
1459 + * - If this box is the first component in the parent box, the `margin`
1460 + * property will be ignored.
1461 + */
1462 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1463 + /**
1464 + * Color of the separator. Use a hexadecimal color code.
1465 + */
1466 + color?: string;
1467 +};
1468 +
1469 +/**
1470 + * This is an invisible component that places a fixed-size space at the
1471 + * beginning or end of the box.
1472 + * @deprecated
1473 + */
1474 +export type FlexSpacer = {
1475 + type: "spacer";
1476 + /**
1477 + * Size of the space.
1478 + * The size increases in the order of listing.
1479 + * The default value is `md`.
1480 + */
1481 + size?: "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1482 +};
1483 +
1484 +export type FlexText = {
1485 + type: "text";
1486 + text: string;
1487 + /**
1488 + * 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.
1489 + */
1490 + contents?: FlexSpan[];
1491 + /**
1492 + * The method by which to adjust the text font size. Specify this value:
1493 + *
1494 + * - `shrink-to-fit`: Automatically shrink the font
1495 + * size to fit the width of the component. This
1496 + * property takes a "best-effort" approach that may
1497 + * work differently—or not at all!—on some platforms.
1498 + * For more information, see [Automatically shrink fonts to fit](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#adjusts-fontsize-to-fit)
1499 + * in the Messaging API documentation.
1500 + * - LINE 10.13.0 or later for iOS and Android
1501 + */
1502 + adjustMode?: "shrink-to-fit";
1503 + /**
1504 + * The ratio of the width or height of this box within the parent box.
1505 + *
1506 + * The default value for the horizontal parent box is `1`, and the default
1507 + * value for the vertical parent box is `0`.
1508 + *
1509 + * For more information, see
1510 + * [Width and height of components](https://developers.line.biz/en/docs/messaging-api/flex-message-layout/#component-width-and-height).
1511 + */
1512 + flex?: number;
1513 + /**
1514 + * Minimum space between this box and the previous component in the parent
1515 + * box.
1516 + *
1517 + * - `none` does not set a space while the other values set a space whose
1518 + * size increases in the order of listing.
1519 + * - The default value is the value of the `spacing` property of the parent
1520 + * box.
1521 + * - If this box is the first component in the parent box, the `margin`
1522 + * property will be ignored.
1523 + */
1524 + margin?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
1525 + /**
1526 + * Font size.
1527 + * The size increases in the order of listing.
1528 + * The default value is `md`.
1529 + * 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.
1530 + */
1531 + size?:
1532 + | string
1533 + | "xxs"
1534 + | "xs"
1535 + | "sm"
1536 + | "md"
1537 + | "lg"
1538 + | "xl"
1539 + | "xxl"
1540 + | "3xl"
1541 + | "4xl"
1542 + | "5xl";
1543 + /**
1544 + * Horizontal alignment style. Specify one of the following values:
1545 + *
1546 + * - `start`: Left-aligned
1547 + * - `end`: Right-aligned
1548 + * - `center`: Center-aligned
1549 + *
1550 + * The default value is `start`.
1551 + */
1552 + align?: "start" | "end" | "center";
1553 + /**
1554 + * Vertical alignment style. Specify one of the following values:
1555 + *
1556 + * - `top`: Top-aligned
1557 + * - `bottom`: Bottom-aligned
1558 + * - `center`: Center-aligned
1559 + *
1560 + * The default value is `top`.
1561 + *
1562 + * If the `layout` property of the parent box is `baseline`, the `gravity`
1563 + * property will be ignored.
1564 + */
1565 + gravity?: "top" | "bottom" | "center";
1566 + /**
1567 + * `true` to wrap text.
1568 + *
1569 + * The default value is `false`.
1570 + *
1571 + * If set to `true`, you can use a new line character (\n) to begin on a new
1572 + * line.
1573 + */
1574 + wrap?: boolean;
1575 + /**
1576 + * Max number of lines. If the text does not fit in the specified number of
1577 + * lines, an ellipsis (…) is displayed at the end of the last line. If set to
1578 + * 0, all the text is displayed. The default value is 0.
1579 + */
1580 + maxLines?: number;
1581 + /**
1582 + * Font weight.
1583 + * Specifying `bold`makes the font bold.
1584 + * The default value is `regular`.
1585 + */
1586 + weight?: "regular" | "bold";
1587 + /**
1588 + * Font color. Use a hexadecimal color code.
1589 + */
1590 + color?: string;
1591 + /**
1592 + * Action performed when this text is tapped.
1593 + * Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
1594 + */
1595 + action?: Action;
1596 + /**
1597 + * Style of the text. Specify one of the following values:
1598 + * - `normal`: Normal
1599 + * - `italic`: Italic
1600 + *
1601 + * The default value is `normal`.
1602 + */
1603 + style?: string;
1604 + /**
1605 + * Decoration of the text. Specify one of the following values:
1606 + * `none`: No decoration
1607 + * `underline`: Underline
1608 + * `line-through`: Strikethrough
1609 + *
1610 + * The default value is `none`.
1611 + */
1612 + decoration?: string;
1613 +} & Offset;
1614 +
1615 +/**
1616 + * 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).
1617 + */
1618 +export type FlexSpan = {
1619 + type: "span";
1620 + /**
1621 + * 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.
1622 + */
1623 + text: string;
1624 + /**
1625 + * Font color. Use a hexadecimal color code.
1626 + */
1627 + color?: string;
1628 + /**
1629 + * 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`.
1630 + * 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.
1631 + */
1632 + size?:
1633 + | string
1634 + | "xxs"
1635 + | "xs"
1636 + | "sm"
1637 + | "md"
1638 + | "lg"
1639 + | "xl"
1640 + | "xxl"
1641 + | "3xl"
1642 + | "4xl"
1643 + | "5xl";
1644 + /**
1645 + * Font weight. You can specify one of the following values: `regular` or `bold`. Specifying `bold` makes the font bold. The default value is `regular`.
1646 + */
1647 + weight?: string;
1648 + /**
1649 + * Style of the text. Specify one of the following values:
1650 + * - `normal`: Normal
1651 + * - `italic`: Italic
1652 + *
1653 + * The default value is `normal`.
1654 + */
1655 + style?: string;
1656 + /**
1657 + * Decoration of the text. Specify one of the following values:
1658 + * `none`: No decoration
1659 + * `underline`: Underline
1660 + * `line-through`: Strikethrough
1661 + *
1662 + * The default value is `none`.
1663 + *
1664 + * 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.
1665 + */
1666 + decoration?: string;
1667 +};
1668 +
1669 +export type TemplateContent =
1670 + | TemplateButtons
1671 + | TemplateConfirm
1672 + | TemplateCarousel
1673 + | TemplateImageCarousel;
1674 +
1675 +/**
1676 + * Template with an image, title, text, and multiple action buttons.
1677 + *
1678 + * Because of the height limitation for buttons template messages, the lower
1679 + * part of the text display area will get cut off if the height limitation is
1680 + * exceeded. For this reason, depending on the character width, the message
1681 + * text may not be fully displayed even when it is within the character limits.
1682 + */
1683 +export type TemplateButtons = {
1684 + type: "buttons";
1685 + /**
1686 + * Image URL (Max: 1000 characters)
1687 + *
1688 + * - HTTPS
1689 + * - JPEG or PNG
1690 + * - Max width: 1024px
1691 + * - Max: 1 MB
1692 + */
1693 + thumbnailImageUrl?: string;
1694 + /**
1695 + * Aspect ratio of the image. Specify one of the following values:
1696 + *
1697 + * - `rectangle`: 1.51:1
1698 + * - `square`: 1:1
1699 + *
1700 + * The default value is `rectangle`
1701 + */
1702 + imageAspectRatio?: "rectangle" | "square";
1703 + /**
1704 + * Size of the image. Specify one of the following values:
1705 + *
1706 + * - `cover`: The image fills the entire image area. Parts of the image that
1707 + * do not fit in the area are not displayed.
1708 + * - `contain`: The entire image is displayed in the image area. A background
1709 + * is displayed in the unused areas to the left and right of vertical images
1710 + * and in the areas above and below horizontal images.
1711 + *
1712 + * The default value is `cover`.
1713 + */
1714 + imageSize?: "cover" | "contain";
1715 + /**
1716 + * Background color of image. Specify a RGB color value.
1717 + * The default value is `#FFFFFF` (white).
1718 + */
1719 + imageBackgroundColor?: string;
1720 + /**
1721 + * Title (Max: 40 characters)
1722 + */
1723 + title?: string;
1724 + /**
1725 + * Message text
1726 + *
1727 + * - Max: 160 characters (no image or title)
1728 + * - Max: 60 characters (message with an image or title)
1729 + */
1730 + text: string;
1731 + /**
1732 + * Action when tapped (Max: 4)
1733 + */
1734 + actions: Action[];
1735 +};
1736 +
1737 +/**
1738 + * Template with two action buttons.
1739 + *
1740 + * Because of the height limitation for confirm template messages, the lower
1741 + * part of the `text` display area will get cut off if the height limitation is
1742 + * exceeded. For this reason, depending on the character width, the message
1743 + * text may not be fully displayed even when it is within the character limits.
1744 + */
1745 +export type TemplateConfirm = {
1746 + type: "confirm";
1747 + /**
1748 + * Message text (Max: 240 characters)
1749 + */
1750 + text: string;
1751 + /**
1752 + * Action when tapped. Set 2 actions for the 2 buttons
1753 + */
1754 + actions: Action[];
1755 +};
1756 +
1757 +/**
1758 + * Template with multiple columns which can be cycled like a carousel.
1759 + * The columns will be shown in order by scrolling horizontally.
1760 + *
1761 + * Because of the height limitation for carousel template messages, the lower
1762 + * part of the `text` display area will get cut off if the height limitation is
1763 + * exceeded. For this reason, depending on the character width, the message
1764 + * text may not be fully displayed even when it is within the character limits.
1765 + *
1766 + * Keep the number of actions consistent for all columns. If you use an image
1767 + * or title for a column, make sure to do the same for all other columns.
1768 + */
1769 +export type TemplateCarousel = {
1770 + type: "carousel";
1771 + /**
1772 + * Array of columns (Max: 10)
1773 + */
1774 + columns: TemplateColumn[];
1775 + /**
1776 + * Aspect ratio of the image. Specify one of the following values:
1777 + *
1778 + * - `rectangle`: 1.51:1
1779 + * - `square`: 1:1
1780 + *
1781 + * Applies to all columns. The default value is `rectangle`.
1782 + */
1783 + imageAspectRatio?: "rectangle" | "square";
1784 + /**
1785 + * Size of the image. Specify one of the following values:
1786 + *
1787 + * - `cover`: The image fills the entire image area. Parts of the image that
1788 + * do not fit in the area are not displayed.
1789 + * - `contain`: The entire image is displayed in the image area. A background
1790 + * is displayed in the unused areas to the left and right of vertical images
1791 + * and in the areas above and below horizontal images.
1792 + *
1793 + * Applies to all columns. The default value is `cover`.
1794 + */
1795 + imageSize?: "cover" | "contain";
1796 +};
1797 +
1798 +export type TemplateColumn = {
1799 + /**
1800 + * Image URL (Max: 1000 characters)
1801 + *
1802 + * - HTTPS
1803 + * - JPEG or PNG
1804 + * - Aspect ratio: 1:1.51
1805 + * - Max width: 1024px
1806 + * - Max: 1 MB
1807 + */
1808 + thumbnailImageUrl?: string;
1809 + /**
1810 + * Background color of image. Specify a RGB color value.
1811 + * The default value is `#FFFFFF` (white).
1812 + */
1813 + imageBackgroundColor?: string;
1814 + /**
1815 + * Title (Max: 40 characters)
1816 + */
1817 + title?: string;
1818 + /**
1819 + * Message text
1820 + *
1821 + * - Max: 120 characters (no image or title)
1822 + * - Max: 60 characters (message with an image or title)
1823 + */
1824 + text: string;
1825 + /**
1826 + * Action when image is tapped; set for the entire image, title, and text area
1827 + */
1828 + defaultAction?: Action;
1829 + /**
1830 + * Action when tapped (Max: 3)
1831 + */
1832 + actions: Action[];
1833 +};
1834 +
1835 +/**
1836 + * Template with multiple images which can be cycled like a carousel.
1837 + * The images will be shown in order by scrolling horizontally.
1838 + */
1839 +export type TemplateImageCarousel = {
1840 + type: "image_carousel";
1841 + /**
1842 + * Array of columns (Max: 10)
1843 + */
1844 + columns: TemplateImageColumn[];
1845 +};
1846 +
1847 +export type TemplateImageColumn = {
1848 + /**
1849 + * Image URL (Max: 1000 characters)
1850 + *
1851 + * - HTTPS
1852 + * - JPEG or PNG
1853 + * - Aspect ratio: 1:1
1854 + * - Max width: 1024px
1855 + * - Max: 1 MB
1856 + */
1857 + imageUrl: string;
1858 + /**
1859 + * Action when image is tapped
1860 + */
1861 + action: Action<{ label?: string }>;
1862 +};
1863 +
1864 +/**
1865 + * These properties are used for the quick reply.
1866 + *
1867 + * For more information, see
1868 + * [Using quick replies](https://developers.line.biz/en/docs/messaging-api/using-quick-reply/).
1869 + */
1870 +export type QuickReply = {
1871 + /**
1872 + * This is a container that contains
1873 + * [quick reply buttons](https://developers.line.biz/en/reference/messaging-api/#quick-reply-button-object).
1874 + *
1875 + * Array of objects (Max: 13)
1876 + */
1877 + items: QuickReplyItem[];
1878 +};
1879 +
1880 +/**
1881 + * This is a quick reply option that is displayed as a button.
1882 + *
1883 + * For more information, see
1884 + * [quick reply buttons](https://developers.line.biz/en/reference/messaging-api/#quick-reply-button-object).
1885 + */
1886 +export type QuickReplyItem = {
1887 + type: "action";
1888 + /**
1889 + * URL of the icon that is displayed at the beginning of the button (Max: 1000 characters)
1890 + *
1891 + * - URL scheme: https
1892 + * - Image format: PNG
1893 + * - Aspect ratio: 1:1
1894 + * - Data size: Up to 1 MB
1895 + *
1896 + * There is no limit on the image size. If the `action` property has the
1897 + * following actions with empty `imageUrl`:
1898 + *
1899 + * - [camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
1900 + * - [camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
1901 + * - [location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
1902 + *
1903 + * the default icon is displayed.
1904 + */
1905 + imageUrl?: string;
1906 + /**
1907 + * Action performed when this button is tapped.
1908 + *
1909 + * Specify an [action object](https://developers.line.biz/en/reference/messaging-api/#action-objects).
1910 + *
1911 + * The following is a list of the available actions:
1912 + *
1913 + * - [Postback action](https://developers.line.biz/en/reference/messaging-api/#postback-action)
1914 + * - [Message action](https://developers.line.biz/en/reference/messaging-api/#message-action)
1915 + * - [Datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action)
1916 + * - [Camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
1917 + * - [Camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
1918 + * - [Location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
1919 + */
1920 + action: Action;
1921 +};
1922 +
1923 +export type Sender = {
1924 + /**
1925 + * Display name
1926 + *
1927 + * - Max character limit: 20
1928 + * - Certain words such as `LINE` may not be used.
1929 + */
1930 + name?: string;
1931 + /**
1932 + * Icon image URL
1933 + *
1934 + * - Max character limit: 1000
1935 + * - URL scheme: https
1936 + */
1937 + iconUrl?: string;
1938 +};
1939 +
1940 +/**
1941 + * These are types of actions for your bot to take when a user taps a button or an image in a message.
1942 + *
1943 + * - [Postback action](https://developers.line.biz/en/reference/messaging-api/#postback-action)
1944 + * - [Message action](https://developers.line.biz/en/reference/messaging-api/#message-action)
1945 + * - [URI action](https://developers.line.biz/en/reference/messaging-api/#uri-action)
1946 + * - [Datetime picker action](https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action)
1947 + * - [Camera action](https://developers.line.biz/en/reference/messaging-api/#camera-action)
1948 + * - [Camera roll action](https://developers.line.biz/en/reference/messaging-api/#camera-roll-action)
1949 + * - [Location action](https://developers.line.biz/en/reference/messaging-api/#location-action)
1950 + */
1951 +export type Action<ExtraFields = { label: string }> = (
1952 + | PostbackAction
1953 + | MessageAction
1954 + | URIAction
1955 + | DatetimePickerAction
1956 + | { type: "camera" }
1957 + | { type: "cameraRoll" }
1958 + | { type: "location" }
1959 +) &
1960 + ExtraFields;
1961 +
1962 +/**
1963 + * When a control associated with this action is tapped, a postback event is
1964 + * returned via webhook with the specified string in the data property.
1965 + */
1966 +export type PostbackAction = {
1967 + type: "postback";
1968 + /**
1969 + * String returned via webhook in the `postback.data` property of the
1970 + * postback event (Max: 300 characters)
1971 + */
1972 + data: string;
1973 + /**
1974 + * Text displayed in the chat as a message sent by the user when the action
1975 + * is performed. Returned from the server through a webhook.
1976 + *
1977 + * - This property cannot be used with quick reply buttons. (Max: 300 characters)
1978 + * - The `displayText` and `text` properties cannot both be used at the same time.
1979 + * @deprecated
1980 + */
1981 + text?: string;
1982 + /**
1983 + * Text displayed in the chat as a message sent by the user when the action is performed.
1984 + *
1985 + * - Required for quick reply buttons.
1986 + * - Optional for the other message types.
1987 + *
1988 + * Max: 300 characters
1989 + *
1990 + * The `displayText` and `text` properties cannot both be used at the same time.
1991 + */
1992 + displayText?: string;
1993 +};
1994 +
1995 +/**
1996 + * When a control associated with this action is tapped, the string in the text
1997 + * property is sent as a message from the user.
1998 + */
1999 +export type MessageAction = {
2000 + type: "message";
2001 + /**
2002 + * Text sent when the action is performed (Max: 300 characters)
2003 + */
2004 + text: string;
2005 +};
2006 +
2007 +/**
2008 + * When a control associated with this action is tapped, the URI specified in
2009 + * the `uri` property is opened.
2010 + */
2011 +export type URIAction = {
2012 + type: "uri";
2013 + /**
2014 + * URI opened when the action is performed (Max: 1000 characters).
2015 + * Must start with `http`, `https`, or `tel`.
2016 + */
2017 + uri: string;
2018 + altUri?: AltURI;
2019 +};
2020 +
2021 +/**
2022 + * URI opened on LINE for macOS and Windows when the action is performed (Max: 1000 characters)
2023 + * If the altUri.desktop property is set, the uri property is ignored on LINE for macOS and Windows.
2024 + * The available schemes are http, https, line, and tel.
2025 + * For more information about the LINE URL scheme, see Using the LINE URL scheme.
2026 + * This property is supported on the following version of LINE.
2027 + *
2028 + * LINE 5.12.0 or later for macOS and Windows
2029 + * Note: The altUri.desktop property is supported only when you set URI actions in Flex Messages.
2030 + */
2031 +export type AltURI = {
2032 + desktop: string;
2033 +};
2034 +
2035 +/**
2036 + * When a control associated with this action is tapped, a
2037 + * [postback event](https://developers.line.biz/en/reference/messaging-api/#postback-event)
2038 + * is returned via webhook with the date and time selected by the user from the
2039 + * date and time selection dialog.
2040 + *
2041 + * The datetime picker action does not support time zones.
2042 + *
2043 + * #### Date and time format
2044 + *
2045 + * The date and time formats for the `initial`, `max`, and `min` values are
2046 + * shown below. The `full-date`, `time-hour`, and `time-minute` formats follow
2047 + * the [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) protocol.
2048 + *
2049 + * | Mode | Format | Example |
2050 + * | -------- | ------------------------------------------------------------ | -------------------------------- |
2051 + * | date | `full-date` (Max: 2100-12-31; Min: 1900-01-01) | 2017-06-18 |
2052 + * | time | `time-hour`:`time-minute` (Max: 23:59; Min: 00:00) | 00:0006:1523:59 |
2053 + * | 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 |
2054 + */
2055 +export type DatetimePickerAction = {
2056 + type: "datetimepicker";
2057 + /**
2058 + * String returned via webhook in the `postback.data` property of the
2059 + * postback event (Max: 300 characters)
2060 + */
2061 + data: string;
2062 + mode: "date" | "time" | "datetime";
2063 + /**
2064 + * Initial value of date or time
2065 + */
2066 + initial?: string;
2067 + /**
2068 + * Largest date or time value that can be selected. Must be greater than the
2069 + * `min` value.
2070 + */
2071 + max?: string;
2072 + /**
2073 + * Smallest date or time value that can be selected. Must be less than the
2074 + * `max` value.
2075 + */
2076 + min?: string;
2077 +};
2078 +
2079 +export type Size = {
2080 + width: number;
2081 + height: number;
2082 +};
2083 +
2084 +/**
2085 + * Rich menus consist of either of these objects.
2086 + *
2087 + * - [Rich menu object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-object)
2088 + * without the rich menu ID. Use this object when you
2089 + * [create a rich menu](https://developers.line.biz/en/reference/messaging-api/#create-rich-menu).
2090 + * - [Rich menu response object](https://developers.line.biz/en/reference/messaging-api/#rich-menu-response-object)
2091 + * with the rich menu ID. This object is returned when you
2092 + * [get a rich menu](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu)
2093 + * or [get a list of rich menus](https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-list).
2094 + *
2095 + * [Area objects](https://developers.line.biz/en/reference/messaging-api/#area-object) and
2096 + * [action objects](https://developers.line.biz/en/reference/messaging-api/#action-objects)
2097 + * are included in these objects.
2098 + */
2099 +export type RichMenu = {
2100 + /**
2101 + * [`size` object](https://developers.line.biz/en/reference/messaging-api/#size-object)
2102 + * which contains the width and height of the rich menu displayed in the chat.
2103 + * Rich menu images must be one of the following sizes: 2500x1686px or 2500x843px.
2104 + */
2105 + size: Size;
2106 + /**
2107 + * `true` to display the rich menu by default. Otherwise, `false`.
2108 + */
2109 + selected: boolean;
2110 + /**
2111 + * Name of the rich menu.
2112 + *
2113 + * This value can be used to help manage your rich menus and is not displayed
2114 + * to users.
2115 + *
2116 + * (Max: 300 characters)
2117 + */
2118 + name: string;
2119 + /**
2120 + * Text displayed in the chat bar (Max: 14 characters)
2121 + */
2122 + chatBarText: string;
2123 + /**
2124 + * Array of [area objects](https://developers.line.biz/en/reference/messaging-api/#area-object)
2125 + * which define the coordinates and size of tappable areas
2126 + * (Max: 20 area objects)
2127 + */
2128 + areas: Array<{ bounds: Area; action: Action<{ label?: string }> }>;
2129 +};
2130 +
2131 +export type RichMenuResponse = { richMenuId: string } & RichMenu;
2132 +
2133 +export type NumberOfMessagesSentResponse = InsightStatisticsResponse & {
2134 + /**
2135 + * The number of messages sent with the Messaging API on the date specified in date.
2136 + * The response has this property only when the value of status is `ready`.
2137 + */
2138 + success?: number;
2139 +};
2140 +
2141 +export type TargetLimitForAdditionalMessages = {
2142 + /**
2143 + * One of the following values to indicate whether a target limit is set or not.
2144 + * - `none`: This indicates that a target limit is not set.
2145 + * - `limited`: This indicates that a target limit is set.
2146 + */
2147 + type: "none" | "limited";
2148 + /**
2149 + * The target limit for additional messages in the current month.
2150 + * This property is returned when the `type` property has a value of `limited`.
2151 + */
2152 + value?: number;
2153 +};
2154 +
2155 +export type NumberOfMessagesSentThisMonth = {
2156 + /**
2157 + * The number of sent messages in the current month
2158 + */
2159 + totalUsage: number;
2160 +};
2161 +
2162 +export const LINE_REQUEST_ID_HTTP_HEADER_NAME = "x-line-request-id";
2163 +export type MessageAPIResponseBase = {
2164 + [LINE_REQUEST_ID_HTTP_HEADER_NAME]?: string;
2165 +};
2166 +
2167 +export const LINE_SIGNATURE_HTTP_HEADER_NAME = "x-line-signature";
2168 +
2169 +export type InsightStatisticsResponse = {
2170 + /**
2171 + * Calculation status. One of:
2172 + * - `ready`: Calculation has finished; the numbers are up-to-date.
2173 + * - `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.
2174 + * - `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/).
2175 + */
2176 + status: "ready" | "unready" | "out_of_service";
2177 +};
2178 +
2179 +export type NumberOfMessageDeliveries = InsightStatisticsResponse & {
2180 + /**
2181 + * Number of push messages sent to **all** of this LINE official account's friends (broadcast messages).
2182 + */
2183 + broadcast: number;
2184 + /**
2185 + * Number of push messages sent to **some** of this LINE official account's friends, based on specific attributes (targeted/segmented messages).
2186 + */
2187 + targeting: number;
2188 + /**
2189 + * Number of auto-response messages sent.
2190 + */
2191 + autoResponse: number;
2192 + /**
2193 + * Number of greeting messages sent.
2194 + */
2195 + welcomeResponse: number;
2196 + /**
2197 + * Number of messages sent from LINE Official Account Manager [Chat screen](https://www.linebiz.com/jp-en/manual/OfficialAccountManager/chats/screens/).
2198 + */
2199 + chat: number;
2200 + /**
2201 + * Number of broadcast messages sent with the [Send broadcast message](https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message) Messaging API operation.
2202 + */
2203 + apiBroadcast: number;
2204 + /**
2205 + * Number of push messages sent with the [Send push message](https://developers.line.biz/en/reference/messaging-api/#send-push-message) Messaging API operation.
2206 + */
2207 + apiPush: number;
2208 + /**
2209 + * Number of multicast messages sent with the [Send multicast message](https://developers.line.biz/en/reference/messaging-api/#send-multicast-message) Messaging API operation.
2210 + */
2211 + apiMulticast: number;
2212 + /**
2213 + * Number of replies sent with the [Send reply message](https://developers.line.biz/en/reference/messaging-api/#send-reply-message) Messaging API operation.
2214 + */
2215 + apiReply: number;
2216 +};
2217 +
2218 +export type NumberOfFollowers = InsightStatisticsResponse & {
2219 + /**
2220 + * 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.
2221 + */
2222 + followers: Number;
2223 + /**
2224 + * 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.
2225 + */
2226 + targetedReaches: Number;
2227 + /**
2228 + * The number of users blocking the account as of the specified `date`. The number decreases when a user unblocks the account.
2229 + */
2230 + blocks: Number;
2231 +};
2232 +
2233 +export type NumberOfMessageDeliveriesResponse =
2234 + | InsightStatisticsResponse
2235 + | NumberOfMessageDeliveries;
2236 +
2237 +export type NumberOfFollowersResponse =
2238 + | InsightStatisticsResponse
2239 + | NumberOfFollowers;
2240 +
2241 +type PercentageAble = {
2242 + percentage: number;
2243 +};
2244 +
2245 +export type FriendDemographics = {
2246 + /**
2247 + * `true` if friend demographic information is available.
2248 + */
2249 + available: boolean;
2250 + /**
2251 + * Percentage per gender
2252 + */
2253 + genders?: Array<
2254 + {
2255 + /**
2256 + * Gender
2257 + */
2258 + gender: "unknown" | "male" | "female";
2259 + } & PercentageAble
2260 + >;
2261 + /**
2262 + * Percentage per age group
2263 + */
2264 + ages?: Array<
2265 + {
2266 + /**
2267 + * Age group
2268 + */
2269 + age: string;
2270 + } & PercentageAble
2271 + >;
2272 + /**
2273 + * Percentage per area
2274 + */
2275 + areas?: Array<
2276 + {
2277 + area: string;
2278 + } & PercentageAble
2279 + >;
2280 + /**
2281 + * Percentage by OS
2282 + */
2283 + appTypes?: Array<
2284 + {
2285 + appType: "ios" | "android" | "others";
2286 + } & PercentageAble
2287 + >;
2288 + /**
2289 + * Percentage per friendship duration
2290 + */
2291 + subscriptionPeriods?: Array<
2292 + {
2293 + /**
2294 + * Friendship duration
2295 + */
2296 + subscriptionPeriod:
2297 + | "over365days"
2298 + | "within365days"
2299 + | "within180days"
2300 + | "within90days"
2301 + | "within30days"
2302 + | "within7days"
2303 + // in case for some rare cases(almost no)
2304 + | "unknown";
2305 + } & PercentageAble
2306 + >;
2307 +};
2308 +
2309 +type UserInteractionStatisticsOfEachMessage = {
2310 + seq: number;
2311 + impression: number;
2312 + mediaPlayed: number;
2313 + mediaPlayed25Percent: number;
2314 + mediaPlayed50Percent: number;
2315 + mediaPlayed75Percent: number;
2316 + mediaPlayed100Percent: number;
2317 + uniqueMediaPlayed: number;
2318 + uniqueMediaPlayed25Percent: number;
2319 + uniqueMediaPlayed50Percent: number;
2320 + uniqueMediaPlayed75Percent: number;
2321 + uniqueMediaPlayed100Percent: number;
2322 +};
2323 +
2324 +type UserInteractionStatisticsOfEachURL = {
2325 + seq: number;
2326 + url: number;
2327 + click: number;
2328 + uniqueClick: number;
2329 + uniqueClickOfRequest: number;
2330 +};
2331 +
2332 +/**
2333 + * https://developers.line.biz/en/reference/messaging-api/#get-message-event
2334 + */
2335 +export type UserInteractionStatistics = {
2336 + overview: {
2337 + requestId: string;
2338 + timestamp: number;
2339 + delivered: number;
2340 + uniqueImpression: number;
2341 + uniqueClick: number;
2342 + uniqueMediaPlayed: number;
2343 + uniqueMediaPlayed100Percent: number;
2344 + };
2345 + messages: UserInteractionStatisticsOfEachMessage[];
2346 + clicks: UserInteractionStatisticsOfEachURL[];
2347 +};
2348 +
2349 +type FilterOperatorObject<T> = {
2350 + type: "operator";
2351 +} & (
2352 + | {
2353 + and: (T | FilterOperatorObject<T>)[];
2354 + }
2355 + | {
2356 + or: (T | FilterOperatorObject<T>)[];
2357 + }
2358 + | {
2359 + not: T | (T | FilterOperatorObject<T>)[];
2360 + }
2361 +);
2362 +
2363 +type AudienceObject = {
2364 + type: "audience";
2365 + audienceGroupId: number;
2366 +};
2367 +
2368 +export type ReceieptObject =
2369 + | AudienceObject
2370 + | FilterOperatorObject<AudienceObject>;
2371 +
2372 +type DemographicAge =
2373 + | "age_15"
2374 + | "age_20"
2375 + | "age_25"
2376 + | "age_30"
2377 + | "age_35"
2378 + | "age_40"
2379 + | "age_45"
2380 + | "age_50";
2381 +
2382 +type DemographicSubscriptionPeriod =
2383 + | "day_7"
2384 + | "day_30"
2385 + | "day_90"
2386 + | "day_180"
2387 + | "day_365";
2388 +
2389 +type DemographicArea =
2390 + | "jp_01"
2391 + | "jp_02"
2392 + | "jp_03"
2393 + | "jp_04"
2394 + | "jp_05"
2395 + | "jp_06"
2396 + | "jp_07"
2397 + | "jp_08"
2398 + | "jp_09"
2399 + | "jp_10"
2400 + | "jp_11"
2401 + | "jp_12"
2402 + | "jp_13"
2403 + | "jp_14"
2404 + | "jp_15"
2405 + | "jp_16"
2406 + | "jp_17"
2407 + | "jp_18"
2408 + | "jp_19"
2409 + | "jp_20"
2410 + | "jp_21"
2411 + | "jp_22"
2412 + | "jp_23"
2413 + | "jp_24"
2414 + | "jp_25"
2415 + | "jp_26"
2416 + | "jp_27"
2417 + | "jp_28"
2418 + | "jp_29"
2419 + | "jp_30"
2420 + | "jp_31"
2421 + | "jp_32"
2422 + | "jp_33"
2423 + | "jp_34"
2424 + | "jp_35"
2425 + | "jp_36"
2426 + | "jp_37"
2427 + | "jp_38"
2428 + | "jp_39"
2429 + | "jp_40"
2430 + | "jp_41"
2431 + | "jp_42"
2432 + | "jp_43"
2433 + | "jp_44"
2434 + | "jp_45"
2435 + | "jp_46"
2436 + | "jp_47"
2437 + | "tw_01"
2438 + | "tw_02"
2439 + | "tw_03"
2440 + | "tw_04"
2441 + | "tw_05"
2442 + | "tw_06"
2443 + | "tw_07"
2444 + | "tw_08"
2445 + | "tw_09"
2446 + | "tw_10"
2447 + | "tw_11"
2448 + | "tw_12"
2449 + | "tw_13"
2450 + | "tw_14"
2451 + | "tw_15"
2452 + | "tw_16"
2453 + | "tw_17"
2454 + | "tw_18"
2455 + | "tw_19"
2456 + | "tw_20"
2457 + | "tw_21"
2458 + | "tw_22"
2459 + | "th_01"
2460 + | "th_02"
2461 + | "th_03"
2462 + | "th_04"
2463 + | "th_05"
2464 + | "th_06"
2465 + | "th_07"
2466 + | "th_08"
2467 + | "id_01"
2468 + | "id_02"
2469 + | "id_03"
2470 + | "id_04"
2471 + | "id_06"
2472 + | "id_07"
2473 + | "id_08"
2474 + | "id_09"
2475 + | "id_10"
2476 + | "id_11"
2477 + | "id_12"
2478 + | "id_05";
2479 +
2480 +type DemographicObject =
2481 + | {
2482 + type: "gender";
2483 + oneOf: ("male" | "female")[];
2484 + }
2485 + | {
2486 + type: "age";
2487 + gte?: DemographicAge;
2488 + lt?: DemographicAge;
2489 + }
2490 + | {
2491 + type: "appType";
2492 + oneOf: ("ios" | "android")[];
2493 + }
2494 + | {
2495 + type: "area";
2496 + oneOf: DemographicArea[];
2497 + }
2498 + | {
2499 + type: "subscriptionPeriod";
2500 + gte?: DemographicSubscriptionPeriod;
2501 + lt?: DemographicSubscriptionPeriod;
2502 + };
2503 +
2504 +export type DemographicFilterObject =
2505 + | DemographicObject
2506 + | FilterOperatorObject<DemographicObject>;
2507 +
2508 +export type NarrowcastProgressResponse = (
2509 + | {
2510 + phase: "waiting";
2511 + }
2512 + | ((
2513 + | {
2514 + phase: "sending" | "succeeded";
2515 + }
2516 + | {
2517 + phase: "failed";
2518 + failedDescription: string;
2519 + }
2520 + ) & {
2521 + successCount: number;
2522 + failureCount: number;
2523 + targetCount: string;
2524 + acceptedTime: string;
2525 + completedTime: string;
2526 + })
2527 +) & {
2528 + errorCode?: 1 | 2;
2529 +};
2530 +
2531 +type AudienceGroupJob = {
2532 + audienceGroupJobId: number;
2533 + audienceGroupId: number;
2534 + description: string;
2535 + type: "DIFF_ADD";
2536 + audienceCount: number;
2537 + created: number;
2538 +} & (
2539 + | {
2540 + jobStatus: "QUEUED" | "WORKING" | "FINISHED";
2541 + }
2542 + | {
2543 + jobStatus: "FAILED";
2544 + failedType: "INTERNAL_ERROR";
2545 + }
2546 +);
2547 +
2548 +export type AudienceGroupStatus =
2549 + | "IN_PROGRESS"
2550 + | "READY"
2551 + | "EXPIRED"
2552 + | "FAILED";
2553 +
2554 +export type AudienceGroupCreateRoute = "OA_MANAGER" | "MESSAGING_API";
2555 +
2556 +type _AudienceGroup = {
2557 + audienceGroupId: number;
2558 + description: string;
2559 + audienceCount: number;
2560 + created: number;
2561 + isIfaAudience: boolean;
2562 + permission: "READ" | "READ_WRITE";
2563 + createRoute: AudienceGroupCreateRoute;
2564 +} & (
2565 + | {
2566 + status: Exclude<AudienceGroupStatus, "FAILED">;
2567 + }
2568 + | {
2569 + status: "FAILED";
2570 + failedType: "AUDIENCE_GROUP_AUDIENCE_INSUFFICIENT" | "INTERNAL_ERROR";
2571 + }
2572 +) &
2573 + (
2574 + | {
2575 + type: "UPLOAD";
2576 + }
2577 + | {
2578 + type: "CLICK";
2579 + clickUrl: string;
2580 + }
2581 + | {
2582 + type: "IMP";
2583 + requestId: string;
2584 + }
2585 + );
2586 +
2587 +export type AudienceGroup = _AudienceGroup & {
2588 + jobs: AudienceGroupJob[];
2589 +};
2590 +
2591 +export type AudienceGroups = _AudienceGroup[];
2592 +
2593 +export type AudienceGroupAuthorityLevel = "PUBLIC" | "PRIVATE";
2594 +
2595 +export type ChannelAccessToken = {
2596 + access_token: string;
2597 + expires_in: number;
2598 + token_type: "Bearer";
2599 + key_id?: string;
2600 +};
2601 +
2602 +/**
2603 + * Response body of get group summary.
2604 + *
2605 + * @see [Get group summary](https://developers.line.biz/ja/reference/messaging-api/#get-group-summary)
2606 + */
2607 +export type GroupSummaryResponse = {
2608 + groupId: string;
2609 + groupName: string;
2610 + pictureUrl: string;
2611 +};
2612 +
2613 +/**
2614 + * Response body of get members in group count and get members in room count.
2615 + *
2616 + * @see [Get members in group count](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count)
2617 + * @see [Get members in room count](https://developers.line.biz/en/reference/messaging-api/#get-members-room-count)
2618 + */
2619 +export type MembersCountResponse = {
2620 + count: number;
2621 +};
2622 +
2623 +/**
2624 + * Response body of get bot info.
2625 + *
2626 + * @see [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info)
2627 + */
2628 +export type BotInfoResponse = {
2629 + userId: string;
2630 + basicId: string;
2631 + premiumId?: string;
2632 + displayName: string;
2633 + pictureUrl?: string;
2634 + chatMode: "chat" | "bot";
2635 + markAsReadMode: "auto" | "manual";
2636 +};
2637 +
2638 +/**
2639 + * Response body of get webhook endpoint info.
2640 + *
2641 + * @see [Get get webhook endpoint info](https://developers.line.biz/en/reference/messaging-api/#get-webhook-endpoint-information)
2642 + */
2643 +export type WebhookEndpointInfoResponse = {
2644 + endpoint: string;
2645 + active: boolean;
2646 +};
2647 +
2648 +/**
2649 + * Response body of test webhook endpoint.
2650 + *
2651 + * @see [Test webhook endpoint](https://developers.line.biz/en/reference/messaging-api/#test-webhook-endpoint)
2652 + */
2653 +export type TestWebhookEndpointResponse = {
2654 + success: boolean;
2655 + timestamp: string;
2656 + statusCode: number;
2657 + reason: string;
2658 + detail: string;
2659 +};
1 +import { JSONParseError } from "./exceptions";
2 +import * as FormData from "form-data";
3 +
4 +export function toArray<T>(maybeArr: T | T[]): T[] {
5 + return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
6 +}
7 +
8 +export function ensureJSON<T>(raw: T): T {
9 + if (typeof raw === "object") {
10 + return raw;
11 + } else {
12 + throw new JSONParseError("Failed to parse response body as JSON", raw);
13 + }
14 +}
15 +
16 +export function createMultipartFormData(
17 + this: FormData | void,
18 + formBody: Record<string, any>,
19 +): FormData {
20 + const formData = this instanceof FormData ? this : new FormData();
21 + Object.entries(formBody).forEach(([key, value]) => {
22 + if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
23 + formData.append(key, value);
24 + } else {
25 + formData.append(key, String(value));
26 + }
27 + });
28 + return formData;
29 +}
1 +import { createHmac, timingSafeEqual } from "crypto";
2 +
3 +function s2b(str: string, encoding: BufferEncoding): Buffer {
4 + return Buffer.from(str, encoding);
5 +}
6 +
7 +function safeCompare(a: Buffer, b: Buffer): boolean {
8 + if (a.length !== b.length) {
9 + return false;
10 + }
11 + return timingSafeEqual(a, b);
12 +}
13 +
14 +export default function validateSignature(
15 + body: string | Buffer,
16 + channelSecret: string,
17 + signature: string,
18 +): boolean {
19 + return safeCompare(
20 + createHmac("SHA256", channelSecret).update(body).digest(),
21 + s2b(signature, "base64"),
22 + );
23 +}
This diff could not be displayed because it is too large.
1 +{
2 + "name": "@line/bot-sdk",
3 + "version": "7.3.0",
4 + "description": "Node.js SDK for LINE Messaging API",
5 + "engines": {
6 + "node": ">=10"
7 + },
8 + "main": "dist/index.js",
9 + "types": "dist/index.d.ts",
10 + "files": [
11 + "dist",
12 + "lib"
13 + ],
14 + "scripts": {
15 + "pretest": "npm run format && npm run build",
16 + "test": "TEST_PORT=1234 TS_NODE_CACHE=0 nyc mocha",
17 + "prettier": "prettier --parser typescript --trailing-comma all --arrow-parens avoid \"{lib,test}/**/*.ts\"",
18 + "format": "npm run prettier -- --write",
19 + "format:check": "npm run prettier -- -l",
20 + "clean": "rm -rf dist/*",
21 + "prebuild": "npm run format:check && npm run clean",
22 + "build": "tsc",
23 + "docs": "vuepress dev docs",
24 + "docs:build": "vuepress build docs",
25 + "docs:deploy": "./scripts/deploy-docs.sh",
26 + "generate-changelog": "ts-node ./scripts/generate-changelog.ts",
27 + "release": "npm run build && npm publish --access public"
28 + },
29 + "repository": {
30 + "type": "git",
31 + "url": "git@github.com:line/line-bot-sdk-nodejs.git"
32 + },
33 + "keywords": [
34 + "node",
35 + "line",
36 + "sdk"
37 + ],
38 + "dependencies": {
39 + "@types/body-parser": "^1.19.0",
40 + "@types/node": "^14.10.0",
41 + "axios": "^0.21.1",
42 + "body-parser": "^1.19.0",
43 + "file-type": "^15.0.0",
44 + "form-data": "^3.0.0"
45 + },
46 + "devDependencies": {
47 + "@types/express": "^4.17.8",
48 + "@types/finalhandler": "^1.1.0",
49 + "@types/mocha": "^8.0.3",
50 + "express": "^4.17.1",
51 + "finalhandler": "^1.1.2",
52 + "husky": "^4.3.0",
53 + "mocha": "^8.1.3",
54 + "nock": "^13.0.4",
55 + "nyc": "^15.1.0",
56 + "prettier": "^2.1.1",
57 + "ts-node": "^9.0.0",
58 + "typescript": "^3.9.7",
59 + "vuepress": "^1.5.4"
60 + },
61 + "husky": {
62 + "hooks": {
63 + "pre-commit": "npm run format:check",
64 + "pre-push": "npm run format:check && npm run build && npm run test"
65 + }
66 + },
67 + "nyc": {
68 + "require": [
69 + "ts-node/register"
70 + ],
71 + "extension": [
72 + ".ts"
73 + ],
74 + "reporter": [
75 + "lcov",
76 + "text"
77 + ],
78 + "sourceMap": true,
79 + "instrument": true
80 + },
81 + "mocha": {
82 + "require": "ts-node/register",
83 + "spec": "test/*.spec.ts"
84 + },
85 + "license": "Apache-2.0"
86 +}
1 +#!/usr/bin/env sh
2 +
3 +# abort on errors
4 +set -e
5 +
6 +# build
7 +npm run docs:build
8 +
9 +# navigate into the build output directory
10 +cd docs/.vuepress/dist
11 +
12 +git init
13 +git add -A
14 +git commit -m 'Deploy docs'
15 +
16 +git push -f git@github.com:line/line-bot-sdk-nodejs.git master:gh-pages
17 +
18 +cd -
1 +#!/bin/bash
2 +
3 +git checkout ${GITHUB_HEAD_REF}
4 +
5 +git config --global user.email "action@github.com"
6 +git config --global user.name "GitHub Action"
7 +
8 +npm run generate-changelog
9 +
10 +git add -A
11 +git commit -m "(Changelog CI) Added Changelog"
12 +git push -u origin ${GITHUB_HEAD_REF}
1 +import { execSync } from "child_process";
2 +import { readFileSync, writeFileSync } from "fs";
3 +import { resolve } from "path";
4 +const { version: lastVersion } = require("../package.json");
5 +
6 +const changeLogPath = resolve(__dirname, "../CHANGELOG.md");
7 +
8 +let newVersion = lastVersion;
9 +
10 +console.log("Gets Release Version from GITHUB_EVENT_PATH");
11 +if (process.env.GITHUB_EVENT_PATH) {
12 + const {
13 + pull_request: { title },
14 + } = require(process.env.GITHUB_EVENT_PATH);
15 +
16 + if (/^release/i.test(title))
17 + newVersion = (title as string).match(/release ([\d\.]+)/i)[1];
18 + else {
19 + console.log("Not target pull request, exiting");
20 + process.exit(0);
21 + }
22 +}
23 +console.log(`New Version: ${newVersion}`);
24 +
25 +console.log("Bump Version");
26 +execSync(`npm version ${newVersion}`);
27 +
28 +const gitLogOutput = execSync(
29 + `git log v${lastVersion}... --format=%s`
30 +).toString("utf-8");
31 +
32 +const commitsArray = gitLogOutput
33 + .split("\n")
34 + .filter((message) => message && message !== "");
35 +
36 +const category = {
37 + miscs: [] as string[],
38 + features: [] as string[],
39 + bugFixes: [] as string[],
40 +};
41 +
42 +commitsArray.forEach((message) => {
43 + let cat: keyof typeof category;
44 + if (/^([\d\.]+)$/.test(message)) {
45 + return;
46 + } else if (message.includes("test")) {
47 + cat = "miscs";
48 + } else if (/(add)|(support)/i.test(message)) {
49 + cat = "features";
50 + } else if (/fix/i.test(message)) {
51 + cat = "bugFixes";
52 + } else {
53 + cat = "miscs";
54 + }
55 + category[cat].push(`* ${message}`);
56 +});
57 +
58 +const now = new Date();
59 +const MonthText = [
60 + "Jan",
61 + "Feb",
62 + "Mar",
63 + "Apr",
64 + "May",
65 + "Jun",
66 + "Jul",
67 + "Aug",
68 + "Sep",
69 + "Oct",
70 + "Nov",
71 + "Dec",
72 +];
73 +let newChangelog = `## ${newVersion} (${now.getDate()} ${
74 + MonthText[now.getMonth()]
75 +} ${now.getFullYear()})
76 +`;
77 +
78 +if (category.features.length > 0) {
79 + newChangelog += `
80 +### Feature
81 +${category.features.join("\n")}
82 +`;
83 +}
84 +
85 +if (category.bugFixes.length > 0) {
86 + newChangelog += `
87 +### Bug fix
88 +${category.bugFixes.join("\n")}
89 +`;
90 +}
91 +
92 +if (category.miscs.length > 0) {
93 + newChangelog += `
94 +### Misc
95 +${category.miscs.join("\n")}
96 +`;
97 +}
98 +
99 +const currentChangelog = readFileSync(changeLogPath, "utf-8");
100 +
101 +writeFileSync(
102 + changeLogPath,
103 + `${newChangelog}
104 +${currentChangelog}`
105 +);
1 +import { readFileSync } from "fs";
2 +import { join } from "path";
3 +import { deepEqual, equal, ok } from "assert";
4 +import { URL } from "url";
5 +import Client, { OAuth } from "../lib/client";
6 +import * as Types from "../lib/types";
7 +import { getStreamData } from "./helpers/stream";
8 +import * as nock from "nock";
9 +import {
10 + MESSAGING_API_PREFIX,
11 + OAUTH_BASE_PREFIX,
12 + OAUTH_BASE_PREFIX_V2_1,
13 + DATA_API_PREFIX,
14 +} from "../lib/endpoints";
15 +import * as FormData from "form-data";
16 +import { createMultipartFormData } from "../lib/utils";
17 +
18 +const pkg = require("../package.json");
19 +
20 +const channelAccessToken = "test_channel_access_token";
21 +
22 +const client = new Client({
23 + channelAccessToken,
24 +});
25 +
26 +const responseFn = function (
27 + this: nock.ReplyFnContext,
28 + uri: string,
29 + _body: nock.Body,
30 + cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyFnResult) => void,
31 +) {
32 + const fullUrl =
33 + // @ts-ignore
34 + this.req.options.protocol +
35 + "//" +
36 + // @ts-ignore
37 + this.req.options.hostname +
38 + // @ts-ignore
39 + this.req.options.path;
40 +
41 + if (fullUrl.startsWith(MESSAGING_API_PREFIX + "/message/"))
42 + cb(null, [
43 + 200,
44 + {},
45 + {
46 + "X-Line-Request-Id": "X-Line-Request-Id",
47 + },
48 + ]);
49 + else cb(null, [200, {}]);
50 +};
51 +
52 +describe("client", () => {
53 + before(() => nock.disableNetConnect());
54 + afterEach(() => nock.cleanAll());
55 + after(() => nock.enableNetConnect());
56 +
57 + const testMsg: Types.TextMessage = { type: "text", text: "hello" };
58 + const richMenu: Types.RichMenu = {
59 + size: {
60 + width: 2500,
61 + height: 1686,
62 + },
63 + selected: false,
64 + name: "Nice richmenu",
65 + chatBarText: "Tap here",
66 + areas: [
67 + {
68 + bounds: {
69 + x: 0,
70 + y: 0,
71 + width: 2500,
72 + height: 1686,
73 + },
74 + action: {
75 + type: "postback",
76 + data: "action=buy&itemid=123",
77 + },
78 + },
79 + ],
80 + };
81 +
82 + const interceptionOption = {
83 + reqheaders: {
84 + authorization: `Bearer ${channelAccessToken}`,
85 + "User-Agent": `${pkg.name}/${pkg.version}`,
86 + },
87 + };
88 +
89 + const mockGet = (
90 + prefix: string,
91 + path: string,
92 + expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
93 + ) => {
94 + let _it = nock(prefix, interceptionOption).get(path);
95 + if (expectedQuery) {
96 + _it = _it.query(expectedQuery);
97 + }
98 + return _it.reply(responseFn);
99 + };
100 +
101 + const mockPost = (
102 + prefix: string,
103 + path: string,
104 + expectedBody?: nock.RequestBodyMatcher,
105 + ) => {
106 + return nock(prefix, interceptionOption)
107 + .post(path, expectedBody)
108 + .reply(responseFn);
109 + };
110 +
111 + const multipartFormDataMatcher = (expectedBody: Record<string, any>) => (
112 + body: any,
113 + ) => {
114 + const decoded = Buffer.from(body, "hex");
115 + const boundary = decoded.toString("utf-8").match(/^--(.+)/)[1];
116 + const data = new FormData();
117 + //@ts-ignore
118 + data._boundary = boundary;
119 + createMultipartFormData.call(data, expectedBody);
120 + return data.getBuffer().compare(decoded) === 0;
121 + };
122 +
123 + const mockPut = (
124 + prefix: string,
125 + path: string,
126 + expectedBody?: nock.RequestBodyMatcher,
127 + ) => {
128 + return nock(prefix, interceptionOption)
129 + .put(path, expectedBody)
130 + .reply(responseFn);
131 + };
132 +
133 + const mockDelete = (
134 + prefix: string,
135 + path: string,
136 + expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
137 + ) => {
138 + let _it = nock(prefix, interceptionOption).delete(path);
139 + if (expectedQuery) {
140 + _it = _it.query(expectedQuery);
141 + }
142 + return _it.reply(responseFn);
143 + };
144 +
145 + it("reply", async () => {
146 + const scope = mockPost(MESSAGING_API_PREFIX, `/message/reply`, {
147 + messages: [testMsg],
148 + replyToken: "test_reply_token",
149 + notificationDisabled: false,
150 + });
151 +
152 + const res = await client.replyMessage("test_reply_token", testMsg);
153 + equal(scope.isDone(), true);
154 + equal(res["x-line-request-id"], "X-Line-Request-Id");
155 + });
156 +
157 + it("push", async () => {
158 + const scope = mockPost(MESSAGING_API_PREFIX, `/message/push`, {
159 + messages: [testMsg],
160 + to: "test_user_id",
161 + notificationDisabled: false,
162 + });
163 +
164 + const res = await client.pushMessage("test_user_id", testMsg);
165 + equal(scope.isDone(), true);
166 + equal(res["x-line-request-id"], "X-Line-Request-Id");
167 + });
168 +
169 + it("multicast", async () => {
170 + const ids = ["test_user_id_1", "test_user_id_2", "test_user_id_3"];
171 + const scope = mockPost(MESSAGING_API_PREFIX, `/message/multicast`, {
172 + messages: [testMsg, testMsg],
173 + to: ids,
174 + notificationDisabled: false,
175 + });
176 +
177 + const res = await client.multicast(ids, [testMsg, testMsg]);
178 + equal(scope.isDone(), true);
179 + equal(res["x-line-request-id"], "X-Line-Request-Id");
180 + });
181 +
182 + it("narrowcast", async () => {
183 + const recipient: Types.ReceieptObject = {
184 + type: "operator",
185 + and: [
186 + {
187 + type: "audience",
188 + audienceGroupId: 5614991017776,
189 + },
190 + {
191 + type: "operator",
192 + not: {
193 + type: "audience",
194 + audienceGroupId: 4389303728991,
195 + },
196 + },
197 + ],
198 + };
199 + const filter = {
200 + demographic: {
201 + type: "operator",
202 + or: [
203 + {
204 + type: "operator",
205 + and: [
206 + {
207 + type: "gender",
208 + oneOf: ["male", "female"],
209 + },
210 + {
211 + type: "age",
212 + gte: "age_20",
213 + lt: "age_25",
214 + },
215 + {
216 + type: "appType",
217 + oneOf: ["android", "ios"],
218 + },
219 + {
220 + type: "area",
221 + oneOf: ["jp_23", "jp_05"],
222 + },
223 + {
224 + type: "subscriptionPeriod",
225 + gte: "day_7",
226 + lt: "day_30",
227 + },
228 + ],
229 + },
230 + {
231 + type: "operator",
232 + and: [
233 + {
234 + type: "age",
235 + gte: "age_35",
236 + lt: "age_40",
237 + },
238 + {
239 + type: "operator",
240 + not: {
241 + type: "gender",
242 + oneOf: ["male"],
243 + },
244 + },
245 + ],
246 + },
247 + ],
248 + } as Types.DemographicFilterObject,
249 + };
250 +
251 + const limit = {
252 + max: 100,
253 + };
254 + const scope = mockPost(MESSAGING_API_PREFIX, `/message/narrowcast`, {
255 + messages: [testMsg, testMsg],
256 + recipient,
257 + filter,
258 + limit,
259 + });
260 +
261 + const res = await client.narrowcast(
262 + [testMsg, testMsg],
263 + recipient,
264 + filter,
265 + limit,
266 + );
267 + equal(scope.isDone(), true);
268 + equal(res["x-line-request-id"], "X-Line-Request-Id");
269 + });
270 +
271 + it("broadcast", async () => {
272 + const scope = mockPost(MESSAGING_API_PREFIX, `/message/broadcast`, {
273 + messages: [testMsg, testMsg],
274 + notificationDisabled: false,
275 + });
276 +
277 + const res = await client.broadcast([testMsg, testMsg]);
278 + equal(scope.isDone(), true);
279 + equal(res["x-line-request-id"], "X-Line-Request-Id");
280 + });
281 +
282 + it("getProfile", async () => {
283 + const scope = mockGet(MESSAGING_API_PREFIX, "/profile/test_user_id");
284 +
285 + const res = await client.getProfile("test_user_id");
286 + equal(scope.isDone(), true);
287 + deepEqual(res, {});
288 + });
289 +
290 + it("getGroupMemberProfile", async () => {
291 + const scope = mockGet(
292 + MESSAGING_API_PREFIX,
293 + "/group/test_group_id/member/test_user_id",
294 + );
295 +
296 + const res = await client.getGroupMemberProfile(
297 + "test_group_id",
298 + "test_user_id",
299 + );
300 + equal(scope.isDone(), true);
301 + deepEqual(res, {});
302 + });
303 +
304 + it("getRoomMemberProfile", async () => {
305 + const scope = mockGet(
306 + MESSAGING_API_PREFIX,
307 + "/room/test_room_id/member/test_user_id",
308 + );
309 +
310 + const res = await client.getRoomMemberProfile(
311 + "test_room_id",
312 + "test_user_id",
313 + );
314 + equal(scope.isDone(), true);
315 + deepEqual(res, {});
316 + });
317 +
318 + const mockGroupMemberAPI = () => {
319 + const matchReg = /([A-Za-z0-9_]+)\/([A-Za-z0-9_]+)\/members\/ids/;
320 +
321 + return nock(MESSAGING_API_PREFIX, interceptionOption)
322 + .get(matchReg)
323 + .times(3)
324 + .reply(200, (uri, _requestBody) => {
325 + const _url = new URL(MESSAGING_API_PREFIX + uri);
326 + let [_matchPath, groupOrRoom, id] = _url.pathname.match(matchReg);
327 +
328 + const ty: string = groupOrRoom;
329 + const start: number = parseInt(_url.searchParams.get("start"), 10) || 0;
330 +
331 + const result: { memberIds: string[]; next?: string } = {
332 + memberIds: [start, start + 1, start + 2].map(i => `${ty}-${id}-${i}`),
333 + };
334 +
335 + if (start / 3 < 2) {
336 + result.next = String(start + 3);
337 + }
338 + return result;
339 + });
340 + };
341 +
342 + it("getGroupMemberIds", async () => {
343 + const scope = mockGroupMemberAPI();
344 +
345 + const ids = await client.getGroupMemberIds("test_group_id");
346 + equal(scope.isDone(), true);
347 + deepEqual(ids, [
348 + "group-test_group_id-0",
349 + "group-test_group_id-1",
350 + "group-test_group_id-2",
351 + "group-test_group_id-3",
352 + "group-test_group_id-4",
353 + "group-test_group_id-5",
354 + "group-test_group_id-6",
355 + "group-test_group_id-7",
356 + "group-test_group_id-8",
357 + ]);
358 + });
359 +
360 + it("getRoomMemberIds", async () => {
361 + const scope = mockGroupMemberAPI();
362 +
363 + const ids = await client.getRoomMemberIds("test_room_id");
364 + equal(scope.isDone(), true);
365 + deepEqual(ids, [
366 + "room-test_room_id-0",
367 + "room-test_room_id-1",
368 + "room-test_room_id-2",
369 + "room-test_room_id-3",
370 + "room-test_room_id-4",
371 + "room-test_room_id-5",
372 + "room-test_room_id-6",
373 + "room-test_room_id-7",
374 + "room-test_room_id-8",
375 + ]);
376 + });
377 +
378 + it("getGroupMembersCount", async () => {
379 + const groupId = "groupId";
380 + const scope = mockGet(
381 + MESSAGING_API_PREFIX,
382 + `/group/${groupId}/members/count`,
383 + );
384 +
385 + await client.getGroupMembersCount(groupId);
386 + equal(scope.isDone(), true);
387 + });
388 +
389 + it("getRoomMembersCount", async () => {
390 + const roomId = "roomId";
391 + const scope = mockGet(
392 + MESSAGING_API_PREFIX,
393 + `/room/${roomId}/members/count`,
394 + );
395 +
396 + await client.getRoomMembersCount(roomId);
397 + equal(scope.isDone(), true);
398 + });
399 +
400 + it("getGroupSummary", async () => {
401 + const groupId = "groupId";
402 + const scope = mockGet(MESSAGING_API_PREFIX, `/group/${groupId}/summary`);
403 +
404 + await client.getGroupSummary(groupId);
405 + equal(scope.isDone(), true);
406 + });
407 +
408 + it("getMessageContent", async () => {
409 + const scope = mockGet(DATA_API_PREFIX, "/message/test_message_id/content");
410 +
411 + const stream = await client.getMessageContent("test_message_id");
412 + const data = await getStreamData(stream);
413 + equal(scope.isDone(), true);
414 + const res = JSON.parse(data);
415 + deepEqual(res, {});
416 + });
417 +
418 + it("leaveGroup", async () => {
419 + const scope = mockPost(MESSAGING_API_PREFIX, "/group/test_group_id/leave");
420 +
421 + const res = await client.leaveGroup("test_group_id");
422 + equal(scope.isDone(), true);
423 + deepEqual(res, {});
424 + });
425 +
426 + it("leaveRoom", async () => {
427 + const scope = mockPost(MESSAGING_API_PREFIX, "/room/test_room_id/leave");
428 + const res = await client.leaveRoom("test_room_id");
429 + equal(scope.isDone(), true);
430 + deepEqual(res, {});
431 + });
432 +
433 + it("getRichMenu", async () => {
434 + const scope = mockGet(MESSAGING_API_PREFIX, "/richmenu/test_rich_menu_id");
435 + const res = await client.getRichMenu("test_rich_menu_id");
436 + equal(scope.isDone(), true);
437 + deepEqual(res, {});
438 + });
439 +
440 + it("createRichMenu", async () => {
441 + const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu", richMenu);
442 + await client.createRichMenu(richMenu);
443 +
444 + equal(scope.isDone(), true);
445 + });
446 +
447 + it("deleteRichMenu", async () => {
448 + // delete
449 + const scope = mockDelete(
450 + MESSAGING_API_PREFIX,
451 + "/richmenu/test_rich_menu_id",
452 + );
453 + const res = await client.deleteRichMenu("test_rich_menu_id");
454 + equal(scope.isDone(), true);
455 + deepEqual(res, {});
456 + });
457 +
458 + it("getRichMenuIdOfUser", async () => {
459 + const scope = mockGet(MESSAGING_API_PREFIX, "/user/test_user_id/richmenu");
460 + await client.getRichMenuIdOfUser("test_user_id");
461 + equal(scope.isDone(), true);
462 + });
463 +
464 + it("linkRichMenuToUser", async () => {
465 + const scope = mockPost(
466 + MESSAGING_API_PREFIX,
467 + "/user/test_user_id/richmenu/test_rich_menu_id",
468 + );
469 +
470 + const res = await client.linkRichMenuToUser(
471 + "test_user_id",
472 + "test_rich_menu_id",
473 + );
474 + equal(scope.isDone(), true);
475 + deepEqual(res, {});
476 + });
477 +
478 + it("unlinkRichMenuFromUser", async () => {
479 + const scope = mockDelete(
480 + MESSAGING_API_PREFIX,
481 + "/user/test_user_id/richmenu",
482 + );
483 +
484 + const res = await client.unlinkRichMenuFromUser("test_user_id");
485 + equal(scope.isDone(), true);
486 + deepEqual(res, {});
487 + });
488 +
489 + it("linkRichMenuToMultipleUsers", async () => {
490 + const richMenuId = "test_rich_menu_id",
491 + userIds = ["test_user_id"];
492 + const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu/bulk/link", {
493 + richMenuId,
494 + userIds,
495 + });
496 +
497 + const res = await client.linkRichMenuToMultipleUsers(richMenuId, userIds);
498 + equal(scope.isDone(), true);
499 + deepEqual(res, {});
500 + });
501 +
502 + it("unlinkRichMenusFromMultipleUsers", async () => {
503 + const userIds = ["test_user_id"];
504 + const scope = mockPost(MESSAGING_API_PREFIX, "/richmenu/bulk/unlink", {
505 + userIds,
506 + });
507 +
508 + const res = await client.unlinkRichMenusFromMultipleUsers(userIds);
509 + equal(scope.isDone(), true);
510 + deepEqual(res, {});
511 + });
512 +
513 + it("setRichMenuImage", async () => {
514 + const filepath = join(__dirname, "/helpers/line-icon.png");
515 + const buffer = readFileSync(filepath);
516 + const scope = mockPost(
517 + DATA_API_PREFIX,
518 + "/richmenu/test_rich_menu_id/content",
519 + buffer,
520 + );
521 +
522 + const res = await client.setRichMenuImage("test_rich_menu_id", buffer);
523 + equal(scope.isDone(), true);
524 + deepEqual(res, {});
525 + });
526 +
527 + it("getRichMenuImage", async () => {
528 + const scope = mockGet(
529 + DATA_API_PREFIX,
530 + "/richmenu/test_rich_menu_id/content",
531 + );
532 +
533 + const stream = await client.getRichMenuImage("test_rich_menu_id");
534 + const data = await getStreamData(stream);
535 + equal(scope.isDone(), true);
536 + const res = JSON.parse(data);
537 + deepEqual(res, {});
538 + });
539 +
540 + it("getRichMenuList", async () => {
541 + const scope = mockGet(MESSAGING_API_PREFIX, "/richmenu/list");
542 +
543 + await client.getRichMenuList();
544 + equal(scope.isDone(), true);
545 + });
546 +
547 + it("setDefaultRichMenu", async () => {
548 + const scope = mockPost(
549 + MESSAGING_API_PREFIX,
550 + "/user/all/richmenu/test_rich_menu_id",
551 + );
552 +
553 + const res = await client.setDefaultRichMenu("test_rich_menu_id");
554 + equal(scope.isDone(), true);
555 + deepEqual(res, {});
556 + });
557 +
558 + it("getDefaultRichMenuId", async () => {
559 + const scope = mockGet(MESSAGING_API_PREFIX, "/user/all/richmenu");
560 +
561 + await client.getDefaultRichMenuId();
562 + equal(scope.isDone(), true);
563 + });
564 +
565 + it("deleteDefaultRichMenu", async () => {
566 + const scope = mockDelete(MESSAGING_API_PREFIX, "/user/all/richmenu");
567 +
568 + const res = await client.deleteDefaultRichMenu();
569 + equal(scope.isDone(), true);
570 + deepEqual(res, {});
571 + });
572 +
573 + it("getLinkToken", async () => {
574 + const scope = mockPost(
575 + MESSAGING_API_PREFIX,
576 + "/user/test_user_id/linkToken",
577 + );
578 +
579 + await client.getLinkToken("test_user_id");
580 + equal(scope.isDone(), true);
581 + });
582 +
583 + it("getNumberOfSentReplyMessages", async () => {
584 + const date = "20191231";
585 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/reply", {
586 + date,
587 + });
588 +
589 + await client.getNumberOfSentReplyMessages(date);
590 + equal(scope.isDone(), true);
591 + });
592 +
593 + it("getNumberOfSentPushMessages", async () => {
594 + const date = "20191231";
595 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/push", {
596 + date,
597 + });
598 +
599 + await client.getNumberOfSentPushMessages(date);
600 + equal(scope.isDone(), true);
601 + });
602 +
603 + it("getNumberOfSentMulticastMessages", async () => {
604 + const date = "20191231";
605 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/multicast", {
606 + date,
607 + });
608 +
609 + await client.getNumberOfSentMulticastMessages(date);
610 + equal(scope.isDone(), true);
611 + });
612 +
613 + it("getNarrowcastProgress", async () => {
614 + const requestId = "requestId";
615 + const scope = mockGet(
616 + MESSAGING_API_PREFIX,
617 + "/message/progress/narrowcast",
618 + {
619 + requestId,
620 + },
621 + );
622 +
623 + await client.getNarrowcastProgress(requestId);
624 + equal(scope.isDone(), true);
625 + });
626 +
627 + it("getTargetLimitForAdditionalMessages", async () => {
628 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/quota");
629 +
630 + await client.getTargetLimitForAdditionalMessages();
631 + equal(scope.isDone(), true);
632 + });
633 +
634 + it("getNumberOfMessagesSentThisMonth", async () => {
635 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/quota/consumption");
636 +
637 + await client.getNumberOfMessagesSentThisMonth();
638 + equal(scope.isDone(), true);
639 + });
640 +
641 + it("getNumberOfSentBroadcastMessages", async () => {
642 + const date = "20191231";
643 + const scope = mockGet(MESSAGING_API_PREFIX, "/message/delivery/broadcast", {
644 + date,
645 + });
646 +
647 + await client.getNumberOfSentBroadcastMessages(date);
648 + equal(scope.isDone(), true);
649 + });
650 +
651 + it("getNumberOfMessageDeliveries", async () => {
652 + const date = "20191231";
653 + const scope = mockGet(MESSAGING_API_PREFIX, "/insight/message/delivery", {
654 + date,
655 + });
656 +
657 + await client.getNumberOfMessageDeliveries(date);
658 + equal(scope.isDone(), true);
659 + });
660 +
661 + it("getNumberOfFollowers", async () => {
662 + const date = "20191231";
663 + const scope = mockGet(MESSAGING_API_PREFIX, "/insight/followers", {
664 + date,
665 + });
666 +
667 + await client.getNumberOfFollowers(date);
668 + equal(scope.isDone(), true);
669 + });
670 +
671 + it("getFriendDemographics", async () => {
672 + const scope = mockGet(MESSAGING_API_PREFIX, "/insight/demographic");
673 +
674 + await client.getFriendDemographics();
675 + equal(scope.isDone(), true);
676 + });
677 +
678 + it("getUserInteractionStatistics", async () => {
679 + const requestId = "requestId";
680 + const scope = mockGet(MESSAGING_API_PREFIX, "/insight/message/event", {
681 + requestId,
682 + });
683 +
684 + await client.getUserInteractionStatistics(requestId);
685 + equal(scope.isDone(), true);
686 + });
687 +
688 + it("createUploadAudienceGroup", async () => {
689 + const requestBody = {
690 + description: "audienceGroupName",
691 + isIfaAudience: false,
692 + audiences: [
693 + {
694 + id: "id",
695 + },
696 + ],
697 + uploadDescription: "uploadDescription",
698 + };
699 + const scope = mockPost(
700 + MESSAGING_API_PREFIX,
701 + "/audienceGroup/upload",
702 + requestBody,
703 + );
704 +
705 + await client.createUploadAudienceGroup(requestBody);
706 + equal(scope.isDone(), true);
707 + });
708 +
709 + it("createUploadAudienceGroupByFile", async () => {
710 + const filepath = join(__dirname, "/helpers/line-icon.png");
711 + const buffer = readFileSync(filepath);
712 +
713 + const requestBody = {
714 + description: "audienceGroupName",
715 + isIfaAudience: false,
716 + uploadDescription: "uploadDescription",
717 + file: buffer,
718 + };
719 +
720 + const scope = nock(DATA_API_PREFIX, {
721 + reqheaders: {
722 + ...interceptionOption.reqheaders,
723 + "content-type": value =>
724 + value.startsWith(`multipart/form-data; boundary=`),
725 + },
726 + })
727 + .post(
728 + "/audienceGroup/upload/byFile",
729 + multipartFormDataMatcher(requestBody),
730 + )
731 + .reply(responseFn);
732 +
733 + await client.createUploadAudienceGroupByFile(requestBody);
734 + equal(scope.isDone(), true);
735 + });
736 +
737 + it("updateUploadAudienceGroup", async () => {
738 + const requestBody = {
739 + audienceGroupId: 4389303728991,
740 + description: "audienceGroupName",
741 + uploadDescription: "fileName",
742 + audiences: [
743 + {
744 + id: "u1000",
745 + },
746 + {
747 + id: "u2000",
748 + },
749 + ],
750 + };
751 + const scope = mockPut(
752 + MESSAGING_API_PREFIX,
753 + "/audienceGroup/upload",
754 + requestBody,
755 + );
756 +
757 + await client.updateUploadAudienceGroup(requestBody);
758 + equal(scope.isDone(), true);
759 + });
760 +
761 + it("updateUploadAudienceGroupByFile", async () => {
762 + const filepath = join(__dirname, "/helpers/line-icon.png");
763 + const buffer = readFileSync(filepath);
764 + const requestBody = {
765 + audienceGroupId: 4389303728991,
766 + uploadDescription: "fileName",
767 + file: buffer,
768 + };
769 + const scope = nock(DATA_API_PREFIX, {
770 + reqheaders: {
771 + ...interceptionOption.reqheaders,
772 + "content-type": value =>
773 + value.startsWith(`multipart/form-data; boundary=`),
774 + },
775 + })
776 + .put(
777 + "/audienceGroup/upload/byFile",
778 + multipartFormDataMatcher(requestBody),
779 + )
780 + .reply(responseFn);
781 +
782 + await client.updateUploadAudienceGroupByFile(requestBody);
783 + equal(scope.isDone(), true);
784 + });
785 +
786 + it("createClickAudienceGroup", async () => {
787 + const requestBody = {
788 + description: "audienceGroupName",
789 + requestId: "requestId",
790 + };
791 + const scope = mockPost(
792 + MESSAGING_API_PREFIX,
793 + "/audienceGroup/click",
794 + requestBody,
795 + );
796 +
797 + await client.createClickAudienceGroup(requestBody);
798 + equal(scope.isDone(), true);
799 + });
800 +
801 + it("createImpAudienceGroup", async () => {
802 + const requestBody = {
803 + requestId: "requestId",
804 + description: "description",
805 + };
806 + const scope = mockPost(
807 + MESSAGING_API_PREFIX,
808 + "/audienceGroup/imp",
809 + requestBody,
810 + );
811 +
812 + await client.createImpAudienceGroup(requestBody);
813 + equal(scope.isDone(), true);
814 + });
815 +
816 + it("setDescriptionAudienceGroup", async () => {
817 + const { description, audienceGroupId } = {
818 + description: "description",
819 + audienceGroupId: "audienceGroupId",
820 + };
821 + const scope = mockPut(
822 + MESSAGING_API_PREFIX,
823 + `/audienceGroup/${audienceGroupId}/updateDescription`,
824 + {
825 + description,
826 + },
827 + );
828 +
829 + await client.setDescriptionAudienceGroup(description, audienceGroupId);
830 + equal(scope.isDone(), true);
831 + });
832 +
833 + it("deleteAudienceGroup", async () => {
834 + const audienceGroupId = "audienceGroupId";
835 + const scope = mockDelete(
836 + MESSAGING_API_PREFIX,
837 + `/audienceGroup/${audienceGroupId}`,
838 + );
839 + const res = await client.deleteAudienceGroup(audienceGroupId);
840 + equal(scope.isDone(), true);
841 + deepEqual(res, {});
842 + });
843 +
844 + it("getAudienceGroup", async () => {
845 + const audienceGroupId = "audienceGroupId";
846 + const scope = mockGet(
847 + MESSAGING_API_PREFIX,
848 + `/audienceGroup/${audienceGroupId}`,
849 + );
850 +
851 + await client.getAudienceGroup(audienceGroupId);
852 + equal(scope.isDone(), true);
853 + });
854 +
855 + it("getAudienceGroups", async () => {
856 + const page = 1;
857 + const description = "description";
858 + const status: Types.AudienceGroupStatus = "READY";
859 + const size = 1;
860 + const createRoute: Types.AudienceGroupCreateRoute = "MESSAGING_API";
861 + const includesExternalPublicGroups = true;
862 +
863 + const scope = mockGet(MESSAGING_API_PREFIX, `/audienceGroup/list`, {
864 + page,
865 + description,
866 + status,
867 + size,
868 + createRoute,
869 + includesExternalPublicGroups,
870 + });
871 +
872 + await client.getAudienceGroups(
873 + page,
874 + description,
875 + status,
876 + size,
877 + createRoute,
878 + includesExternalPublicGroups,
879 + );
880 + equal(scope.isDone(), true);
881 + });
882 +
883 + it("getAudienceGroupAuthorityLevel", async () => {
884 + const scope = mockGet(
885 + MESSAGING_API_PREFIX,
886 + `/audienceGroup/authorityLevel`,
887 + );
888 +
889 + await client.getAudienceGroupAuthorityLevel();
890 + equal(scope.isDone(), true);
891 + });
892 +
893 + it("changeAudienceGroupAuthorityLevel", async () => {
894 + const authorityLevel: Types.AudienceGroupAuthorityLevel = "PRIVATE";
895 + const scope = mockPut(
896 + MESSAGING_API_PREFIX,
897 + `/audienceGroup/authorityLevel`,
898 + {
899 + authorityLevel,
900 + },
901 + );
902 +
903 + await client.changeAudienceGroupAuthorityLevel(authorityLevel);
904 + equal(scope.isDone(), true);
905 + });
906 +
907 + it("setWebhookEndpointUrl", async () => {
908 + const endpoint = "https://developers.line.biz/";
909 + const scope = mockPut(MESSAGING_API_PREFIX, `/channel/webhook/endpoint`, {
910 + endpoint,
911 + });
912 +
913 + await client.setWebhookEndpointUrl(endpoint);
914 + equal(scope.isDone(), true);
915 + });
916 +
917 + it("getWebhookEndpointInfo", async () => {
918 + const scope = mockGet(MESSAGING_API_PREFIX, `/channel/webhook/endpoint`);
919 +
920 + await client.getWebhookEndpointInfo();
921 + equal(scope.isDone(), true);
922 + });
923 +
924 + it("testWebhookEndpoint", async () => {
925 + const endpoint = "https://developers.line.biz/";
926 + const scope = mockPost(MESSAGING_API_PREFIX, `/channel/webhook/test`, {
927 + endpoint,
928 + });
929 +
930 + await client.testWebhookEndpoint(endpoint);
931 + equal(scope.isDone(), true);
932 + });
933 +
934 + it("set option once and clear option", async () => {
935 + const expectedBody = {
936 + messages: [testMsg],
937 + to: "test_user_id",
938 + notificationDisabled: false,
939 + };
940 + const retryKey = "retryKey";
941 +
942 + const firstRequest = nock(MESSAGING_API_PREFIX, {
943 + reqheaders: {
944 + ...interceptionOption.reqheaders,
945 + "X-Line-Retry-Key": retryKey,
946 + },
947 + })
948 + .post(`/message/push`, expectedBody)
949 + .reply(responseFn);
950 + const secondRequest = mockPost(MESSAGING_API_PREFIX, `/message/push`, {
951 + messages: [testMsg],
952 + to: "test_user_id",
953 + notificationDisabled: false,
954 + });
955 +
956 + client.setRequestOptionOnce({
957 + retryKey,
958 + });
959 +
960 + const firstResPromise = client.pushMessage("test_user_id", testMsg);
961 + const secondResPromise = client.pushMessage("test_user_id", testMsg);
962 +
963 + const [firstRes, secondRes] = await Promise.all([
964 + firstResPromise,
965 + secondResPromise,
966 + ]);
967 + equal(firstRequest.isDone(), true);
968 + equal(secondRequest.isDone(), true);
969 + equal(firstRes["x-line-request-id"], "X-Line-Request-Id");
970 + equal(secondRes["x-line-request-id"], "X-Line-Request-Id");
971 + });
972 +
973 + it("fails on construct with no channelAccessToken", () => {
974 + try {
975 + new Client({ channelAccessToken: null });
976 + ok(false);
977 + } catch (err) {
978 + equal(err.message, "no channel access token");
979 + }
980 + });
981 +
982 + it("fails on pass non-Buffer to setRichMenu", async () => {
983 + try {
984 + await client.setRichMenuImage("test_rich_menu_id", null);
985 + ok(false);
986 + } catch (err) {
987 + equal(err.message, "invalid data type for binary data");
988 + }
989 + });
990 +
991 + it("getBotInfo", async () => {
992 + const scope = mockGet(MESSAGING_API_PREFIX, `/info`);
993 +
994 + await client.getBotInfo();
995 + equal(scope.isDone(), true);
996 + });
997 +});
998 +
999 +const oauth = new OAuth();
1000 +describe("oauth", () => {
1001 + before(() => nock.disableNetConnect());
1002 + afterEach(() => nock.cleanAll());
1003 + after(() => nock.enableNetConnect());
1004 +
1005 + const interceptionOption = {
1006 + reqheaders: {
1007 + "content-type": "application/x-www-form-urlencoded",
1008 + "User-Agent": `${pkg.name}/${pkg.version}`,
1009 + },
1010 + };
1011 +
1012 + it("issueAccessToken", async () => {
1013 + const client_id = "test_client_id";
1014 + const client_secret = "test_client_secret";
1015 + const reply = {
1016 + access_token: "access_token",
1017 + expires_in: 2592000,
1018 + token_type: "Bearer",
1019 + };
1020 +
1021 + const scope = nock(OAUTH_BASE_PREFIX, interceptionOption)
1022 + .post("/accessToken", {
1023 + grant_type: "client_credentials",
1024 + client_id,
1025 + client_secret,
1026 + })
1027 + .reply(200, reply);
1028 +
1029 + const res = await oauth.issueAccessToken(client_id, client_secret);
1030 + equal(scope.isDone(), true);
1031 + deepEqual(res, reply);
1032 + });
1033 +
1034 + it("revokeAccessToken", async () => {
1035 + const access_token = "test_channel_access_token";
1036 + const scope = nock(OAUTH_BASE_PREFIX, interceptionOption)
1037 + .post("/revoke", { access_token })
1038 + .reply(200, {});
1039 +
1040 + const res = await oauth.revokeAccessToken(access_token);
1041 + equal(scope.isDone(), true);
1042 + deepEqual(res, {});
1043 + });
1044 +
1045 + it("issueChannelAccessTokenV2_1", async () => {
1046 + const client_assertion = "client_assertion";
1047 + const reply = {
1048 + access_token: "access_token",
1049 + expires_in: 2592000,
1050 + token_type: "Bearer",
1051 + key_id: "key_id",
1052 + };
1053 +
1054 + const scope = nock(OAUTH_BASE_PREFIX_V2_1, interceptionOption)
1055 + .post("/token", {
1056 + grant_type: "client_credentials",
1057 + client_assertion_type:
1058 + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
1059 + client_assertion,
1060 + })
1061 + .reply(200, reply);
1062 +
1063 + const res = await oauth.issueChannelAccessTokenV2_1(client_assertion);
1064 + equal(scope.isDone(), true);
1065 + deepEqual(res, reply);
1066 + });
1067 +
1068 + it("getChannelAccessTokenKeyIdsV2_1", async () => {
1069 + const client_assertion = "client_assertion";
1070 + const reply = {
1071 + key_ids: ["key_id"],
1072 + };
1073 +
1074 + const scope = nock(OAUTH_BASE_PREFIX_V2_1)
1075 + .get("/tokens/kid")
1076 + .query({
1077 + client_assertion_type:
1078 + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
1079 + client_assertion,
1080 + })
1081 + .reply(200, reply);
1082 +
1083 + const res = await oauth.getChannelAccessTokenKeyIdsV2_1(client_assertion);
1084 + equal(scope.isDone(), true);
1085 + deepEqual(res, reply);
1086 + });
1087 +
1088 + it("revokeChannelAccessTokenV2_1", async () => {
1089 + const client_id = "test_client_id",
1090 + client_secret = "test_client_secret",
1091 + access_token = "test_channel_access_token";
1092 + const scope = nock(OAUTH_BASE_PREFIX_V2_1, interceptionOption)
1093 + .post("/revoke", { client_id, client_secret, access_token })
1094 + .reply(200, {});
1095 +
1096 + const res = await oauth.revokeChannelAccessTokenV2_1(
1097 + client_id,
1098 + client_secret,
1099 + access_token,
1100 + );
1101 + equal(scope.isDone(), true);
1102 + deepEqual(res, {});
1103 + });
1104 +});
1 +import { Readable } from "stream";
2 +
3 +export function getStreamData(stream: Readable): Promise<string> {
4 + return new Promise(resolve => {
5 + let result: string = "";
6 + stream.on("data", (chunk: Buffer) => {
7 + result += chunk.toString();
8 + });
9 + stream.on("end", () => {
10 + resolve(result);
11 + });
12 + });
13 +}
1 +import * as bodyParser from "body-parser";
2 +import * as express from "express";
3 +import { Server } from "http";
4 +import { join } from "path";
5 +import { writeFileSync } from "fs";
6 +import {
7 + JSONParseError,
8 + SignatureValidationFailed,
9 +} from "../../lib/exceptions";
10 +import * as finalhandler from "finalhandler";
11 +
12 +let server: Server = null;
13 +
14 +function listen(port: number, middleware?: express.RequestHandler) {
15 + const app = express();
16 +
17 + if (middleware) {
18 + app.use((req: express.Request, res, next) => {
19 + if (req.path === "/mid-text") {
20 + bodyParser.text({ type: "*/*" })(req, res, next);
21 + } else if (req.path === "/mid-buffer") {
22 + bodyParser.raw({ type: "*/*" })(req, res, next);
23 + } else if (req.path === "/mid-rawbody") {
24 + bodyParser.raw({ type: "*/*" })(req, res, err => {
25 + if (err) return next(err);
26 + (req as any).rawBody = req.body;
27 + delete req.body;
28 + next();
29 + });
30 + } else if (req.path === "/mid-json") {
31 + bodyParser.json({ type: "*/*" })(req, res, next);
32 + } else {
33 + next();
34 + }
35 + });
36 +
37 + app.use(middleware);
38 + }
39 +
40 + // write request info
41 + app.use((req: express.Request, res, next) => {
42 + const request: any = ["headers", "method", "path", "query"].reduce(
43 + (r, k) => Object.assign(r, { [k]: (req as any)[k] }),
44 + {},
45 + );
46 + if (Buffer.isBuffer(req.body)) {
47 + request.body = req.body.toString("base64");
48 + } else {
49 + request.body = req.body;
50 + }
51 + writeFileSync(
52 + join(__dirname, "request.json"),
53 + JSON.stringify(request, null, 2),
54 + );
55 + next();
56 + });
57 +
58 + // return an empty object for others
59 + app.use((req, res) => res.json({}));
60 +
61 + app.use(
62 + (err: Error, req: express.Request, res: express.Response, next: any) => {
63 + if (err instanceof SignatureValidationFailed) {
64 + res.status(401).send(err.signature);
65 + return;
66 + } else if (err instanceof JSONParseError) {
67 + res.status(400).send(err.raw);
68 + return;
69 + }
70 + // https://github.com/expressjs/express/blob/2df1ad26a58bf51228d7600df0d62ed17a90ff71/lib/application.js#L162
71 + // express will record error in console when
72 + // there is no other handler to handle error & it is in test environment
73 + // use final handler the same as in express application.js
74 + finalhandler(req, res)(err);
75 + },
76 + );
77 +
78 + return new Promise(resolve => {
79 + server = app.listen(port, () => resolve());
80 + });
81 +}
82 +
83 +function close() {
84 + return new Promise(resolve => {
85 + if (!server) {
86 + resolve();
87 + }
88 + server.close(() => resolve());
89 + });
90 +}
91 +
92 +export { listen, close };
1 +import { deepEqual, equal, ok } from "assert";
2 +import { HTTPError, RequestError } from "../lib/exceptions";
3 +import HTTPClient from "../lib/http";
4 +import { getStreamData } from "./helpers/stream";
5 +import * as nock from "nock";
6 +import { readFileSync, createReadStream } from "fs";
7 +import { join } from "path";
8 +
9 +const pkg = require("../package.json");
10 +const baseURL = "https://line.me";
11 +const defaultHeaders = {
12 + "test-header-key": "Test-Header-Value",
13 +};
14 +
15 +describe("http", () => {
16 + const http = new HTTPClient({
17 + baseURL,
18 + defaultHeaders,
19 + });
20 +
21 + before(() => nock.disableNetConnect());
22 + afterEach(() => nock.cleanAll());
23 + after(() => nock.enableNetConnect());
24 +
25 + const interceptionOption = {
26 + reqheaders: {
27 + ...defaultHeaders,
28 + "User-Agent": `${pkg.name}/${pkg.version}`,
29 + },
30 + };
31 +
32 + const mockGet = (
33 + path: string,
34 + expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
35 + ) => {
36 + let _it = nock(baseURL, interceptionOption).get(path);
37 + if (expectedQuery) {
38 + _it = _it.query(expectedQuery);
39 + }
40 + return _it.reply(200, {});
41 + };
42 +
43 + const mockPost = (path: string, expectedBody?: nock.RequestBodyMatcher) => {
44 + return nock(baseURL, interceptionOption)
45 + .post(path, expectedBody)
46 + .reply(200, {});
47 + };
48 +
49 + const mockDelete = (
50 + path: string,
51 + expectedQuery?: boolean | string | nock.DataMatcherMap | URLSearchParams,
52 + ) => {
53 + let _it = nock(baseURL, interceptionOption).delete(path);
54 + if (expectedQuery) {
55 + _it = _it.query(expectedQuery);
56 + }
57 + return _it.reply(200, {});
58 + };
59 +
60 + it("get", async () => {
61 + const scope = mockGet("/get");
62 + const res = await http.get<any>(`/get`);
63 + equal(scope.isDone(), true);
64 + deepEqual(res, {});
65 + });
66 +
67 + it("get with query", async () => {
68 + const scope = mockGet("/get", { x: 10 });
69 + const res = await http.get<any>(`/get`, { x: 10 });
70 + equal(scope.isDone(), true);
71 + deepEqual(res, {});
72 + });
73 +
74 + it("post without body", async () => {
75 + const scope = mockPost("/post");
76 + const res = await http.post<any>(`/post`);
77 + equal(scope.isDone(), true);
78 +
79 + deepEqual(res, {});
80 + });
81 +
82 + it("post with body", async () => {
83 + const testBody = {
84 + id: 12345,
85 + message: "hello, body!",
86 + };
87 +
88 + const scope = mockPost("/post/body", testBody);
89 + const res = await http.post<any>(`/post/body`, testBody);
90 + equal(scope.isDone(), true);
91 +
92 + deepEqual(res, {});
93 + });
94 +
95 + it("getStream", async () => {
96 + const scope = nock(baseURL, interceptionOption)
97 + .get("/stream.txt")
98 + .reply(200, () =>
99 + createReadStream(join(__dirname, "./helpers/stream.txt")),
100 + );
101 + const stream = await http.getStream(`/stream.txt`);
102 + const data = await getStreamData(stream);
103 +
104 + equal(scope.isDone(), true);
105 + equal(data, "hello, stream!\n");
106 + });
107 +
108 + it("delete", async () => {
109 + const scope = mockDelete("/delete");
110 + await http.delete(`/delete`);
111 + equal(scope.isDone(), true);
112 + });
113 +
114 + it("delete with query", async () => {
115 + const scope = mockDelete("/delete", { x: 10 });
116 + await http.delete(`/delete`, { x: 10 });
117 + equal(scope.isDone(), true);
118 + });
119 +
120 + const mockPostBinary = (
121 + buffer: Buffer,
122 + reqheaders: Record<string, nock.RequestHeaderMatcher>,
123 + ) => {
124 + return nock(baseURL, {
125 + reqheaders: {
126 + ...interceptionOption.reqheaders,
127 + ...reqheaders,
128 + "content-length": buffer.length + "",
129 + },
130 + })
131 + .post("/post/binary", buffer)
132 + .reply(200, {});
133 + };
134 +
135 + it("postBinary", async () => {
136 + const filepath = join(__dirname, "/helpers/line-icon.png");
137 + const buffer = readFileSync(filepath);
138 + const scope = mockPostBinary(buffer, {
139 + "content-type": "image/png",
140 + });
141 +
142 + await http.postBinary(`/post/binary`, buffer);
143 + equal(scope.isDone(), true);
144 + });
145 +
146 + it("postBinary with specific content type", async () => {
147 + const filepath = join(__dirname, "/helpers/line-icon.png");
148 + const buffer = readFileSync(filepath);
149 + const scope = mockPostBinary(buffer, {
150 + "content-type": "image/jpeg",
151 + });
152 +
153 + await http.postBinary(`/post/binary`, buffer, "image/jpeg");
154 + equal(scope.isDone(), true);
155 + });
156 +
157 + it("postBinary with stream", async () => {
158 + const filepath = join(__dirname, "/helpers/line-icon.png");
159 + const stream = createReadStream(filepath);
160 + const buffer = readFileSync(filepath);
161 + const scope = mockPostBinary(buffer, {
162 + "content-type": "image/png",
163 + });
164 +
165 + await http.postBinary(`/post/binary`, stream);
166 + equal(scope.isDone(), true);
167 + });
168 +
169 + it("fail with 404", async () => {
170 + const scope = nock(baseURL, interceptionOption).get("/404").reply(404, {});
171 + try {
172 + await http.get(`/404`);
173 + ok(false);
174 + } catch (err) {
175 + ok(err instanceof HTTPError);
176 + equal(scope.isDone(), true);
177 + equal(err.statusCode, 404);
178 + }
179 + });
180 +
181 + it("fail with wrong addr", async () => {
182 + nock.enableNetConnect();
183 + try {
184 + await http.get("http://domain.invalid");
185 + ok(false);
186 + } catch (err) {
187 + ok(err instanceof RequestError);
188 + equal(err.code, "ENOTFOUND");
189 + nock.disableNetConnect();
190 + }
191 + });
192 +
193 + it("will generate default params", async () => {
194 + const scope = nock(baseURL, {
195 + reqheaders: {
196 + "User-Agent": `${pkg.name}/${pkg.version}`,
197 + },
198 + })
199 + .get("/get")
200 + .reply(200, {});
201 +
202 + const http = new HTTPClient();
203 + const res = await http.get<any>(`${baseURL}/get`);
204 + equal(scope.isDone(), true);
205 + deepEqual(res, {});
206 + });
207 +});
1 +import { deepEqual, equal, ok } from "assert";
2 +import { readFileSync } from "fs";
3 +import { join } from "path";
4 +import { HTTPError } from "../lib/exceptions";
5 +import HTTPClient from "../lib/http";
6 +import middleware from "../lib/middleware";
7 +import * as Types from "../lib/types";
8 +import { close, listen } from "./helpers/test-server";
9 +
10 +const TEST_PORT = parseInt(process.env.TEST_PORT, 10);
11 +
12 +const m = middleware({ channelSecret: "test_channel_secret" });
13 +
14 +const getRecentReq = (): { body: Types.WebhookRequestBody } =>
15 + JSON.parse(readFileSync(join(__dirname, "helpers/request.json")).toString());
16 +
17 +describe("middleware", () => {
18 + const webhook: Types.MessageEvent = {
19 + message: {
20 + id: "test_event_message_id",
21 + text: "this is test message.😄😅😢😞😄😅😢😞",
22 + type: "text",
23 + },
24 + replyToken: "test_reply_token",
25 + source: {
26 + groupId: "test_group_id",
27 + type: "group",
28 + },
29 + timestamp: 0,
30 + mode: "active",
31 + type: "message",
32 + };
33 + const webhookSignature = {
34 + "X-Line-Signature": "GzU7H3qOXDzDD6cNcS/9otLzlLFxnYYriz62rNu5BDE=",
35 + };
36 +
37 + const http = (headers: any = { ...webhookSignature }) =>
38 + new HTTPClient({
39 + baseURL: `http://localhost:${TEST_PORT}`,
40 + defaultHeaders: headers,
41 + });
42 +
43 + before(() => listen(TEST_PORT, m));
44 + after(() => close());
45 +
46 + it("succeed", async () => {
47 + await http().post(`/webhook`, {
48 + events: [webhook],
49 + destination: "Uaaaabbbbccccddddeeeeffff",
50 + });
51 + const req = getRecentReq();
52 + deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
53 + deepEqual(req.body.events, [webhook]);
54 + });
55 +
56 + it("succeed with pre-parsed string", async () => {
57 + await http().post(`/mid-text`, {
58 + events: [webhook],
59 + destination: "Uaaaabbbbccccddddeeeeffff",
60 + });
61 + const req = getRecentReq();
62 + deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
63 + deepEqual(req.body.events, [webhook]);
64 + });
65 +
66 + it("succeed with pre-parsed buffer", async () => {
67 + await http().post(`/mid-buffer`, {
68 + events: [webhook],
69 + destination: "Uaaaabbbbccccddddeeeeffff",
70 + });
71 + const req = getRecentReq();
72 + deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
73 + deepEqual(req.body.events, [webhook]);
74 + });
75 +
76 + it("succeed with pre-parsed buffer in rawBody", async () => {
77 + await http().post(`/mid-rawbody`, {
78 + events: [webhook],
79 + destination: "Uaaaabbbbccccddddeeeeffff",
80 + });
81 + const req = getRecentReq();
82 + deepEqual(req.body.destination, "Uaaaabbbbccccddddeeeeffff");
83 + deepEqual(req.body.events, [webhook]);
84 + });
85 +
86 + it("fails on parsing raw as it's a not valid request and should be catched", async () => {
87 + try {
88 + await http({
89 + "X-Line-Signature": "wqJD7WAIZhWcXThMCf8jZnwG3Hmn7EF36plkQGkj48w=",
90 + "Content-Encoding": 1,
91 + }).post(`/webhook`, {
92 + events: [webhook],
93 + destination: "Uaaaabbbbccccddddeeeeffff",
94 + });
95 + ok(false);
96 + } catch (err) {
97 + if (err instanceof HTTPError) {
98 + equal(err.statusCode, 415);
99 + } else {
100 + throw err;
101 + }
102 + }
103 + });
104 +
105 + it("fails on pre-parsed json", async () => {
106 + try {
107 + await http().post(`/mid-json`, {
108 + events: [webhook],
109 + destination: "Uaaaabbbbccccddddeeeeffff",
110 + });
111 + ok(false);
112 + } catch (err) {
113 + if (err instanceof HTTPError) {
114 + equal(err.statusCode, 500);
115 + } else {
116 + throw err;
117 + }
118 + }
119 + });
120 + it("fails on construct with no channelSecret", () => {
121 + try {
122 + middleware({ channelSecret: null });
123 + ok(false);
124 + } catch (err) {
125 + equal(err.message, "no channel secret");
126 + }
127 + });
128 +
129 + it("fails on wrong signature", async () => {
130 + try {
131 + await http({
132 + "X-Line-Signature": "WqJD7WAIZhWcXThMCf8jZnwG3Hmn7EF36plkQGkj48w=",
133 + }).post(`/webhook`, {
134 + events: [webhook],
135 + destination: "Uaaaabbbbccccddddeeeeffff",
136 + });
137 + ok(false);
138 + } catch (err) {
139 + if (err instanceof HTTPError) {
140 + equal(err.statusCode, 401);
141 + } else {
142 + throw err;
143 + }
144 + }
145 + });
146 +
147 + it("fails on wrong signature (length)", async () => {
148 + try {
149 + await http({
150 + "X-Line-Signature": "WqJD7WAIZ6plkQGkj48w=",
151 + }).post(`/webhook`, {
152 + events: [webhook],
153 + destination: "Uaaaabbbbccccddddeeeeffff",
154 + });
155 + ok(false);
156 + } catch (err) {
157 + if (err instanceof HTTPError) {
158 + equal(err.statusCode, 401);
159 + } else {
160 + throw err;
161 + }
162 + }
163 + });
164 +
165 + it("fails on invalid JSON", async () => {
166 + try {
167 + await http({
168 + "X-Line-Signature": "Z8YlPpm0lQOqPipiCHVbiuwIDIzRzD7w5hvHgmwEuEs=",
169 + }).post(`/webhook`, "i am not jason");
170 + ok(false);
171 + } catch (err) {
172 + if (err instanceof HTTPError) {
173 + equal(err.statusCode, 400);
174 + } else {
175 + throw err;
176 + }
177 + }
178 + });
179 +
180 + it("fails on empty signature", async () => {
181 + try {
182 + await http({}).post(`/webhook`, {
183 + events: [webhook],
184 + destination: "Uaaaabbbbccccddddeeeeffff",
185 + });
186 + ok(false);
187 + } catch (err) {
188 + if (err instanceof HTTPError) {
189 + equal(err.statusCode, 401);
190 + } else {
191 + throw err;
192 + }
193 + }
194 + });
195 +});
1 +import { ensureJSON } from "../lib/utils";
2 +import { JSONParseError } from "../lib/exceptions";
3 +import { equal, ok } from "assert";
4 +
5 +describe("utils", () => {
6 + describe("ensureJSON", () => {
7 + it("fails when input isn't an object", () => {
8 + let input = "not Object";
9 + try {
10 + ensureJSON(input);
11 + ok(false);
12 + } catch (err) {
13 + equal(
14 + (err as JSONParseError).message,
15 + "Failed to parse response body as JSON",
16 + );
17 + }
18 + });
19 + });
20 +});
1 +import { ok } from "assert";
2 +import validateSignature from "../lib/validate-signature";
3 +
4 +const body = { hello: "world" };
5 +const secret = "test_secret";
6 +
7 +describe("validateSignature", () => {
8 + it("success", () => {
9 + const validSignature = "t7Hn4ZDHqs6e+wdvI5TyQIvzie0DmMUmuXEBqyyE/tM=";
10 + ok(validateSignature(JSON.stringify(body), secret, validSignature));
11 + });
12 +
13 + it("failure", () => {
14 + const invalidSignature = "t7Hn4ZDHqs6e+wdvi5TyQivzie0DmMUmuXEBqyyE/tM=";
15 + ok(!validateSignature(JSON.stringify(body), secret, invalidSignature));
16 + });
17 +});
1 +{
2 + "compilerOptions": {
3 + "module": "commonjs",
4 + "target": "es2017",
5 + "noImplicitAny": true,
6 + "outDir": "dist",
7 + "rootDirs": ["lib", "test"],
8 + "declaration": true
9 + },
10 + "include": [
11 + "lib/**/*.ts"
12 + ]
13 +}