diff --git a/.dockerignore b/.dockerignore deleted file mode 100755 index 27d2dae2b..000000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -*/node_modules -*.log diff --git a/.eslintrc.js b/.eslintrc.js index aeac31be8..a4fe19f36 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,18 +1,13 @@ module.exports = { parser: '@typescript-eslint/parser', - extends: [ - 'yoctol-base', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'prettier/@typescript-eslint', - ], + extends: ['yoctol-base', 'plugin:@typescript-eslint/recommended', 'prettier'], env: { browser: true, node: true, jest: true, jasmine: true, }, - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc'], rules: { 'class-methods-use-this': 'off', 'consistent-return': 'off', @@ -20,7 +15,11 @@ module.exports = { 'no-param-reassign': 'off', 'no-undef': 'off', 'prefer-destructuring': 'off', + + camelcase: 'warn', + 'import/extensions': 'off', + 'prettier/prettier': [ 'error', { @@ -28,9 +27,19 @@ module.exports = { singleQuote: true, }, ], + '@typescript-eslint/camelcase': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-var-requires': 'warn', + '@typescript-eslint/ban-types': 'warn', + '@typescript-eslint/no-non-null-assertion': 'off', + + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', }, overrides: [ { @@ -41,6 +50,9 @@ module.exports = { rules: { '@typescript-eslint/no-var-requires': 'off', }, + globals: { + MessengerExtensions: true, + }, }, { files: ['packages/**/__tests__/**/*.ts'], @@ -48,15 +60,19 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', }, }, + { + files: ['packages/**/*.ts'], + rules: { + 'tsdoc/syntax': 'warn', + }, + }, ], settings: { 'import/resolver': { node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, + typescript: {}, }, }, - globals: { - MessengerExtensions: true, - }, }; diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..36af21989 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbca3521..8e858574e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,275 @@ +# 1.5.1 / 2020-09-xx + +- [new] Server: support experimental custom connectors (#781): + +```js +// bottender.config.js + +module.exports = { + channels: { + mychannel: { + enabled: true, + path: '/webhooks/mychannel', + connector: new MyConnector(/* ... */), + }, + }, +}; +``` + +- [new]: export clients, factories from `messaging-apis` (#806): + +```js +const { + // clients + MessengerClient, + LineClient, + TelegramClient, + SlackOAuthClient, + ViberClient, + TwilioClient, + + // factories + Messenger, + Line, +} = require('bottender'); +``` + +- [new] Bot: implement the `onRequest` option (#773): + +```js +// bottender.config.js + +function onRequest(body, requestContext) { + console.log({ + body, + requestContext, + }); +} + +module.exports = { + channels: { + messenger: { + // ... + onRequest, + }, + whatsapp: { + // ... + onRequest, + }, + line: { + // ... + onRequest, + }, + telegram: { + // ... + onRequest, + }, + slack: { + // ... + onRequest, + }, + viber: { + // ... + onRequest, + }, + }, +}; +``` + +- [new] RequestContext: add `id` to `RequestContext` (#774) +- [fix] Server: should await for `connector.preprocess` (#771) +- [deps] upgrade `messaging-apis` to v1.0.0 + +## messenger + +- [new] get/set/delete user level persistent menu for context user (#790): + +```js +await context.getUserPersistentMenu(); +// [ +// { +// locale: 'default', +// composerInputDisabled: false, +// callToActions: [ +// { +// type: 'postback', +// title: 'Restart Conversation', +// payload: 'RESTART', +// }, +// { +// type: 'web_url', +// title: 'Powered by ALOHA.AI, Yoctol', +// url: 'https://www.yoctol.com/', +// }, +// ], +// }, +// ] + +await context.setUserPersistentMenu([ + { + locale: 'default', + composerInputDisabled: false, + callToActions: [ + { + type: 'postback', + title: 'Restart Conversation', + payload: 'RESTART', + }, + { + type: 'web_url', + title: 'Powered by ALOHA.AI, Yoctol', + url: 'https://www.yoctol.com/', + }, + ], + }, +]); + +await context.deleteUserPersistentMenu(); +``` + +## line + +- [new] support line multi-channel using `getConfig` (#770): + +```js +// bottender.config.js +module.exports = { + channels: { + line: { + enabled: true, + path: '/webhooks/line/:channelId', + async getConfig({ params }) { + console.log(params.channelId); + // ...get the config from the API, database or wherever you like when every time receiving a new event + return { + accessToken, + channelSecret, + }; + }, + }, + }, +}; +``` + +- [new] add `emojis` on LINE text message event (#793): + +```js +if (context.event.isMessage) { + context.event.message.emojis; + // [ + // { + // index: 14, + // length: 6, + // productId: '5ac1bfd5040ab15980c9b435', + // emojiId: '001', + // }, + // ] +} +``` + +- [new] add `LineContext.getMembersCount` method (#824): + +```js +await context.getMembersCount(); +// 10 +``` + +## telegram + +- [new] add `TelegramEvent.isPollAnswer` and `TelegramEvent.pollAnswer` (#745): + +```js +if (context.event.isPollAnswer) { + console.log(context.event.pollAnswer); +} +``` + +- [new] add `pollAnswer` to telegram routes: + +```js +const { router, telegram } = require('bottender/router'); + +async function HandlePollAnswer(context) { + // ... +} + +function App() { + return router([telegram.pollAnswer(HandlePollAnswer)]); +} +``` + +- [new] add `TelegramContext.answerCallbackQuery` (#750): + +```js +await context.answerCallbackQuery({ + url: 'https://example.com/', +}); +``` + +## slack + +- [new] slack route accept any requests by passing `*` (#758): + +```js +const { router, slack } = require('bottender/router'); + +async function HandleAllEvent(context) { + // ... +} + +function App() { + return router([slack.event('*', HandleAllEvent)]); +} +``` + +- [fix] fix `context.views.open` in slack home tab (#809) +- [fix] fix route slack event (#841) +- [fix] fix slack session when channel id is null (#802) +- [docs] update slack routes improvement (#759) +- [example] example: slack update and delete (#769) +- [example] slack home tab (#829) +- [example] slack modal on home (#827) +- [example] slack modal update (#825) +- [example] slack modal form (#828) + +## dialogflow + +- [deps] use `@google-cloud/dialogflow` v4 + +## create-bottender-app + +- [fix] fix context concat and env name (#859) + +## bottender-facebook + +- [new] add new connector - `FacebookConnector` to experiment using same connector for Messenger and Facebook. + +```js +// bottender.config.js +const { FacebookConnector } = require('@bottender/facebook'); + +module.exports = { + channels: { + facebook: { + enabled: true, + path: '/webhooks/facebook', + connector: new FacebookConnector({ + // The top level access token should be provided for the batch requests. + accessToken: process.env.FACEBOOK_ACCESS_TOKEN, + appSecret: process.env.FACEBOOK_APP_SECRET, + verifyToken: process.env.FACEBOOK_VERIFY_TOKEN, + origin: process.env.FACEBOOK_ORIGIN, + async mapPageToAccessToken(pageId) { + console.log(pageId); + return accessToken; + }, + }), + onRequest: onFacebookRequest, + }, + }, +}; +``` + # 1.4.12 / 2020-08-25 ## create-bottender-app @@ -1242,7 +1514,7 @@ const bot = new ConsoleBot({ }); bot.connector.platform; // 'messenger' -bot.onEvent(context => { +bot.onEvent((context) => { context.platform; // 'messenger' }); ``` @@ -1525,7 +1797,7 @@ Aliases: * [`context.getUserProfilePhotos`](https://bottender.js.org/docs/APIReference-TelegramContext#code-classlanguage-textgetuserprofilephotosoptionscode---official-docs) ```js -context.getUserProfilePhotos({ limit: 1 }).then(result => { +context.getUserProfilePhotos({ limit: 1 }).then((result) => { console.log(result); // { // total_count: 3, @@ -1558,7 +1830,7 @@ context.getUserProfilePhotos({ limit: 1 }).then(result => { - [`context.getChat`](https://bottender.js.org/docs/APIReference-TelegramContext#code-classlanguage-textgetchatcode---official-docs) ```js -context.getChat().then(result => { +context.getChat().then((result) => { console.log(result); // { // id: 313534466, @@ -1573,7 +1845,7 @@ context.getChat().then(result => { - [`context.getChatAdministrators`](https://bottender.js.org/docs/APIReference-TelegramContext#code-classlanguage-textgetchatadministratorscode---official-docs) ```js -context.getChatAdministrators().then(result => { +context.getChatAdministrators().then((result) => { console.log(result); // [ // { @@ -1593,7 +1865,7 @@ context.getChatAdministrators().then(result => { - [`context.getChatMembersCount`](https://bottender.js.org/docs/APIReference-TelegramContext#code-classlanguage-textgetchatmemberscountcode---official-docs) ```js -context.getChatMembersCount().then(result => { +context.getChatMembersCount().then((result) => { console.log(result); // '6' }); @@ -1602,7 +1874,7 @@ context.getChatMembersCount().then(result => { - [`context.getChatMember`](https://bottender.js.org/docs/APIReference-TelegramContext#code-classlanguage-textgetchatmemberuseridcode---official-docs) ```js -context.getChatMember().then(result => { +context.getChatMember().then((result) => { console.log(result); // { // user: { @@ -1652,7 +1924,7 @@ context.getChatMember().then(result => { * [`context.getUserProfile`](https://bottender.js.org/docs/APIReference-LineContext#getuserprofile): ```js -context.getUserProfile().then(profile => { +context.getUserProfile().then((profile) => { console.log(profile); // { // displayName: 'LINE taro', @@ -1666,7 +1938,7 @@ context.getUserProfile().then(profile => { - [`context.getMemberProfile`](https://bottender.js.org/docs/APIReference-LineContext#getmemberprofileuserid): ```js -context.getMemberProfile(USER_ID).then(member => { +context.getMemberProfile(USER_ID).then((member) => { console.log(member); // { // "displayName":"LINE taro", @@ -1679,7 +1951,7 @@ context.getMemberProfile(USER_ID).then(member => { - [`context.getMemberIds`](https://bottender.js.org/docs/APIReference-LineContext#getmemberidsstart): ```js -context.getMemberIds(CURSOR).then(res => { +context.getMemberIds(CURSOR).then((res) => { console.log(res); // { // memberIds: [ @@ -1695,7 +1967,7 @@ context.getMemberIds(CURSOR).then(res => { - [`context.getAllMemberIds`](https://bottender.js.org/docs/APIReference-LineContext#getallmemberids): ```js -context.getAllMemberIds().then(ids => { +context.getAllMemberIds().then((ids) => { console.log(ids); // [ // 'Uxxxxxxxxxxxxxx..1', @@ -1864,7 +2136,7 @@ context.event.payload; // PAYLOAD ```js const bot = new ConsoleBot({ fallbackMethods: true }); -bot.onEvent(async context => { +bot.onEvent(async (context) => { await context.sendText('Hello World'); await context.sendImage('https://example.com/vr.jpg'); await context.sendButtonTemplate('What do you want to do next?', [ @@ -1958,7 +2230,7 @@ const bot = new SlackBot({ accessToken: '__FILL_YOUR_TOKEN_HERE__', }); -bot.onEvent(async context => { +bot.onEvent(async (context) => { await context.sendText('Hello World'); }); @@ -2050,7 +2322,7 @@ const bot = new TelegramBot({ accessToken: '__FILL_YOUR_TOKEN_HERE__', }); -bot.onEvent(async context => { +bot.onEvent(async (context) => { await context.sendText('Hello World'); }); @@ -2113,7 +2385,7 @@ Game API: ```js context.sendGame('Mario Bros.'); context.setGameScore(999); -context.getGameHighScores().then(result => { +context.getGameHighScores().then((result) => { console.log(result); /* { @@ -2148,7 +2420,7 @@ const bot = new ViberBot({ accessToken: '__FILL_YOUR_TOKEN_HERE__', }); -bot.onEvent(async context => { +bot.onEvent(async (context) => { if (context.event.isMessage) { await context.sendText('Hello World'); } @@ -2218,7 +2490,7 @@ event.ref; // 'my_ref' ```js new MessengerBot({ appSecret: '__FILL_YOUR_SECRET_HERE__', - mapPageToAccessToken: pageId => accessToken, + mapPageToAccessToken: (pageId) => accessToken, }); ``` diff --git a/Dockerfile b/Dockerfile deleted file mode 100755 index d369844d5..000000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM node:8.11.4 - -WORKDIR /app/website - -EXPOSE 3000 35729 -COPY ./docs /app/docs -COPY ./website /app/website -RUN yarn install - -CMD ["yarn", "start"] diff --git a/__mocks__/messaging-api-line.ts b/__mocks__/messaging-api-line.ts index c0df3eea6..f57a596cb 100644 --- a/__mocks__/messaging-api-line.ts +++ b/__mocks__/messaging-api-line.ts @@ -1,12 +1,6 @@ -import createMockInstance from 'jest-create-mock-instance'; - +// eslint-disable-next-line @typescript-eslint/no-explicit-any const MessagingAPILine = jest.genMockFromModule('messaging-api-line') as any; -const { Line, LineClient } = require.requireActual('messaging-api-line'); - -MessagingAPILine.Line = Line; -MessagingAPILine.LineClient.connect = jest.fn(() => - createMockInstance(LineClient) -); +MessagingAPILine.Line = jest.requireActual('messaging-api-line').Line; module.exports = MessagingAPILine; diff --git a/__mocks__/messaging-api-messenger.ts b/__mocks__/messaging-api-messenger.ts deleted file mode 100644 index ed7c46f43..000000000 --- a/__mocks__/messaging-api-messenger.ts +++ /dev/null @@ -1,12 +0,0 @@ -import createMockInstance from 'jest-create-mock-instance'; - -const MessagingAPIMessenger = jest.genMockFromModule( - 'messaging-api-messenger' -) as any; -const { MessengerClient } = require.requireActual('messaging-api-messenger'); - -MessagingAPIMessenger.MessengerClient.connect = jest.fn(() => - createMockInstance(MessengerClient) -); - -module.exports = MessagingAPIMessenger; diff --git a/__mocks__/messaging-api-slack.ts b/__mocks__/messaging-api-slack.ts deleted file mode 100644 index 39cafb5f5..000000000 --- a/__mocks__/messaging-api-slack.ts +++ /dev/null @@ -1,10 +0,0 @@ -import createMockInstance from 'jest-create-mock-instance'; - -const MessagingAPISlack = jest.genMockFromModule('messaging-api-slack') as any; -const { SlackOAuthClient } = require.requireActual('messaging-api-slack'); - -MessagingAPISlack.SlackOAuthClient.connect = jest.fn(() => - createMockInstance(SlackOAuthClient) -); - -module.exports = MessagingAPISlack; diff --git a/__mocks__/messaging-api-telegram.ts b/__mocks__/messaging-api-telegram.ts deleted file mode 100644 index dcfe377a4..000000000 --- a/__mocks__/messaging-api-telegram.ts +++ /dev/null @@ -1,12 +0,0 @@ -import createMockInstance from 'jest-create-mock-instance'; - -const MessagingAPITelegram = jest.genMockFromModule( - 'messaging-api-telegram' -) as any; -const { TelegramClient } = require.requireActual('messaging-api-telegram'); - -MessagingAPITelegram.TelegramClient.connect = jest.fn(() => - createMockInstance(TelegramClient) -); - -module.exports = MessagingAPITelegram; diff --git a/__mocks__/messaging-api-viber.ts b/__mocks__/messaging-api-viber.ts deleted file mode 100644 index 2108b3712..000000000 --- a/__mocks__/messaging-api-viber.ts +++ /dev/null @@ -1,10 +0,0 @@ -import createMockInstance from 'jest-create-mock-instance'; - -const MessagingAPIViber = jest.genMockFromModule('messaging-api-viber') as any; -const { ViberClient } = require.requireActual('messaging-api-viber'); - -MessagingAPIViber.ViberClient.connect = jest.fn(() => - createMockInstance(ViberClient) -); - -module.exports = MessagingAPIViber; diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100755 index 6711192ae..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - docusaurus: - build: . - ports: - - 3000:3000 - - 35729:35729 - volumes: - - ./docs:/app/docs - - ./website/blog:/app/website/blog - - ./website/core:/app/website/core - - ./website/i18n:/app/website/i18n - - ./website/pages:/app/website/pages - - ./website/static:/app/website/static - - ./website/sidebars.json:/app/website/sidebars.json - - ./website/siteConfig.js:/app/website/siteConfig.js - working_dir: /app/website diff --git a/docs/advanced-guides-custom-server.md b/docs/advanced-guides-custom-server.md index bfb694252..c2d3a1b3d 100644 --- a/docs/advanced-guides-custom-server.md +++ b/docs/advanced-guides-custom-server.md @@ -74,7 +74,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); @@ -139,18 +139,18 @@ app.prepare().then(() => { const router = new Router(); - router.get('/api', ctx => { + router.get('/api', (ctx) => { ctx.response.body = { ok: true }; }); - router.all('*', async ctx => { + router.all('*', async (ctx) => { await handle(ctx.req, ctx.res); ctx.respond = false; }); server.use(router.routes()); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); @@ -218,7 +218,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/docs/advanced-guides-deployment.md b/docs/advanced-guides-deployment.md index 11973e27b..a886f0c0f 100644 --- a/docs/advanced-guides-deployment.md +++ b/docs/advanced-guides-deployment.md @@ -186,7 +186,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/docs/api-line-client.md b/docs/api-line-client.md index 025fff0cc..3d6f26610 100644 --- a/docs/api-line-client.md +++ b/docs/api-line-client.md @@ -58,7 +58,7 @@ async function MyAction(context) { `LineClient` uses [axios](https://github.com/axios/axios) as HTTP client. We use [axios-error](https://github.com/Yoctol/messaging-apis/tree/master/packages/axios-error) package to wrap API error instances for better formatting error messages. Calling `console.log` with the error instance returns the formatted message. If you'd like to get the axios `request`, `response`, or `config`, you can still get them via those keys on the error instance. ```js -client.replyText(token, text).catch(error => { +client.replyText(token, text).catch((error) => { console.log(error); // the formatted error message console.log(error.stack); // stack trace of the error console.log(error.config); // axios request config @@ -1915,7 +1915,7 @@ Retrieves image, video, and audio data sent in specified message. Example: ```js -client.getMessageContent(MESSAGE_ID).then(buffer => { +client.getMessageContent(MESSAGE_ID).then((buffer) => { console.log(buffer); // }); @@ -1938,7 +1938,7 @@ Gets user profile information. Example: ```js -client.getUserProfile(USER_ID).then(profile => { +client.getUserProfile(USER_ID).then((profile) => { console.log(profile); // { // displayName: 'LINE taro', @@ -1967,7 +1967,7 @@ Gets the user profile of a member of a group that the bot is in. This includes t Example: ```js -client.getGroupMemberProfile(GROUP_ID, USER_ID).then(member => { +client.getGroupMemberProfile(GROUP_ID, USER_ID).then((member) => { console.log(member); // { // "displayName":"LINE taro", @@ -1991,7 +1991,7 @@ Gets the user profile of a member of a room that the bot is in. This includes th Example: ```js -client.getRoomMemberProfile(ROOM_ID, USER_ID).then(member => { +client.getRoomMemberProfile(ROOM_ID, USER_ID).then((member) => { console.log(member); // { // "displayName":"LINE taro", @@ -2021,7 +2021,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -client.getGroupMemberIds(GROUP_ID, CURSOR).then(res => { +client.getGroupMemberIds(GROUP_ID, CURSOR).then((res) => { console.log(res); // { // memberIds: [ @@ -2049,7 +2049,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -client.getAllGroupMemberIds(GROUP_ID).then(ids => { +client.getAllGroupMemberIds(GROUP_ID).then((ids) => { console.log(ids); // [ // 'Uxxxxxxxxxxxxxx..1', @@ -2078,7 +2078,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -client.getRoomMemberIds(ROOM_ID, CURSOR).then(res => { +client.getRoomMemberIds(ROOM_ID, CURSOR).then((res) => { console.log(res); // { // memberIds: [ @@ -2106,7 +2106,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -client.getAllRoomMemberIds(ROOM_ID).then(ids => { +client.getAllRoomMemberIds(ROOM_ID).then((ids) => { console.log(ids); // [ // 'Uxxxxxxxxxxxxxx..1', @@ -2168,7 +2168,7 @@ Gets a list of all uploaded rich menus. Example: ```js -client.getRichMenuList().then(richMenus => { +client.getRichMenuList().then((richMenus) => { console.log(richMenus); // [ // { @@ -2212,7 +2212,7 @@ Gets a rich menu via a rich menu ID. Example: ```js -client.getRichMenu(RICH_MENU_ID).then(richMenu => { +client.getRichMenu(RICH_MENU_ID).then((richMenu) => { console.log(richMenu); // { // richMenuId: 'RICH_MENU_ID', @@ -2278,7 +2278,7 @@ client }, ], }) - .then(richMenu => { + .then((richMenu) => { console.log(richMenu); // { // richMenuId: "{richMenuId}" @@ -2315,7 +2315,7 @@ Gets the ID of the rich menu linked to a user. Example: ```js -client.getLinkedRichMenu(USER_ID).then(richMenu => { +client.getLinkedRichMenu(USER_ID).then((richMenu) => { console.log(richMenu); // { // richMenuId: "{richMenuId}" @@ -2369,7 +2369,7 @@ Downloads an image associated with a rich menu. Example: ```js -client.downloadRichMenuImage(RICH_MENU_ID).then(imageBuffer => { +client.downloadRichMenuImage(RICH_MENU_ID).then((imageBuffer) => { console.log(imageBuffer); // }); @@ -2403,7 +2403,7 @@ Gets the ID of the default rich menu set with the Messaging API. Example: ```js -client.getDefaultRichMenu().then(richMenu => { +client.getDefaultRichMenu().then((richMenu) => { console.log(richMenu); // { // "richMenuId": "{richMenuId}" @@ -2458,7 +2458,7 @@ Issues a link token used for the [account link](https://developers.line.me/en/me Example: ```js -client.issueLinkToken(USER_ID).then(result => { +client.issueLinkToken(USER_ID).then((result) => { console.log(result); // { // linkToken: 'NMZTNuVrPTqlr2IF8Bnymkb7rXfYv5EY', @@ -2524,7 +2524,7 @@ Gets information on all the LIFF apps registered in the channel. Example: ```js -client.getLiffApps().then(apps => { +client.getLiffApps().then((apps) => { console.log(apps); // [ // { diff --git a/docs/api-line-context.md b/docs/api-line-context.md index 787abf911..b952385e7 100644 --- a/docs/api-line-context.md +++ b/docs/api-line-context.md @@ -1143,7 +1143,7 @@ Gets user profile information. Example: ```js -context.getUserProfile().then(profile => { +context.getUserProfile().then((profile) => { console.log(profile); // { // displayName: 'LINE taro', @@ -1171,7 +1171,7 @@ Gets the user profile of a member of the group/room that the bot is in. This inc Example: ```js -context.getMemberProfile(USER_ID).then(member => { +context.getMemberProfile(USER_ID).then((member) => { console.log(member); // { // "displayName":"LINE taro", @@ -1200,7 +1200,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -context.getMemberIds(CURSOR).then(res => { +context.getMemberIds(CURSOR).then((res) => { console.log(res); // { // memberIds: [ @@ -1224,7 +1224,7 @@ This feature is only available for LINE@ Approved accounts or official accounts. Example: ```js -context.getAllMemberIds().then(ids => { +context.getAllMemberIds().then((ids) => { console.log(ids); // [ // 'Uxxxxxxxxxxxxxx..1', @@ -1268,7 +1268,7 @@ Gets the ID of the rich menu linked to the user. Example: ```js -context.getLinkedRichMenu().then(richMenu => { +context.getLinkedRichMenu().then((richMenu) => { console.log(richMenu); // { // richMenuId: "{richMenuId}" @@ -1317,7 +1317,7 @@ Issues a link token used for the [account link](https://developers.line.me/en/do Example: ```js -context.issueLinkToken().then(result => { +context.issueLinkToken().then((result) => { console.log(result); // { // linkToken: 'NMZTNuVrPTqlr2IF8Bnymkb7rXfYv5EY', diff --git a/docs/api-messenger-client.md b/docs/api-messenger-client.md index e77981e8f..003a269af 100644 --- a/docs/api-messenger-client.md +++ b/docs/api-messenger-client.md @@ -60,7 +60,7 @@ async function MyAction(context) { `MessengerClient` uses [axios](https://github.com/axios/axios) as HTTP client. We use [axios-error](https://github.com/Yoctol/messaging-apis/tree/master/packages/axios-error) package to wrap API error instances for better formatting error messages. Calling `console.log` with the error instance returns the formatted message. If you'd like to get the axios `request`, `response`, or `config`, you can still get them via those keys on the error instance. ```js -client.sendRawBody(body).catch(error => { +client.sendRawBody(body).catch((error) => { console.log(error); // the formatted error message console.log(error.stack); // stack trace of the error console.log(error.config); // axios request config @@ -1327,7 +1327,7 @@ Creating a Label. Example: ```js -client.createLabel('awesome').then(label => { +client.createLabel('awesome').then((label) => { console.log(label); // { // id: 1712444532121303 @@ -1382,7 +1382,7 @@ Retrieving Labels Associated with a PSID. Example: ```js -client.getAssociatedLabels(USER_ID).then(result => { +client.getAssociatedLabels(USER_ID).then((result) => { console.log(result); // { // data: [ @@ -1421,7 +1421,7 @@ Retrieving Label Details. Example: ```js -client.getLabelDetails(LABEL_ID, { fields: ['name'] }).then(result => { +client.getLabelDetails(LABEL_ID, { fields: ['name'] }).then((result) => { console.log(result); // { // id: "1001200005002", @@ -1439,7 +1439,7 @@ Retrieving a List of All Labels. Example: ```js -client.getLabelList().then(result => { +client.getLabelList().then((result) => { console.log(result); // { // data: [ @@ -1498,7 +1498,7 @@ Retrieving a Person's Profile. Example: ```js -client.getUserProfile(USER_ID).then(user => { +client.getUserProfile(USER_ID).then((user) => { console.log(user); // { // id: '5566' @@ -1526,31 +1526,33 @@ Retrieves the current value of one or more Messenger Profile properties by name. Example: ```js -client.getMessengerProfile(['get_started', 'persistent_menu']).then(profile => { - console.log(profile); - // [ - // { - // getStarted: { - // payload: 'GET_STARTED', - // }, - // }, - // { - // persistentMenu: [ - // { - // locale: 'default', - // composerInputDisabled: true, - // callToActions: [ - // { - // type: 'postback', - // title: 'Restart Conversation', - // payload: 'RESTART', - // }, - // ], - // }, - // ], - // }, - // ] -}); +client + .getMessengerProfile(['get_started', 'persistent_menu']) + .then((profile) => { + console.log(profile); + // [ + // { + // getStarted: { + // payload: 'GET_STARTED', + // }, + // }, + // { + // persistentMenu: [ + // { + // locale: 'default', + // composerInputDisabled: true, + // callToActions: [ + // { + // type: 'postback', + // title: 'Restart Conversation', + // payload: 'RESTART', + // }, + // ], + // }, + // ], + // }, + // ] + }); ```
@@ -1617,7 +1619,7 @@ Retrieves the current value of persistent menu. Example: ```js -client.getPersistentMenu().then(menu => { +client.getPersistentMenu().then((menu) => { console.log(menu); // [ // { @@ -1734,7 +1736,7 @@ Retrieves the current value of get started button. Example: ```js -client.getGetStarted().then(getStarted => { +client.getGetStarted().then((getStarted) => { console.log(getStarted); // { // payload: 'GET_STARTED', @@ -1783,7 +1785,7 @@ Retrieves the current value of greeting text. Example: ```js -client.getGreeting().then(greeting => { +client.getGreeting().then((greeting) => { console.log(greeting); // [ // { @@ -1838,7 +1840,7 @@ Retrieves the current value of whitelisted domains. Example: ```js -client.getWhitelistedDomains().then(domains => { +client.getWhitelistedDomains().then((domains) => { console.log(domains); // ['http://www.example.com/'] }); @@ -1883,7 +1885,7 @@ Retrieves the current value of account linking URL. Example: ```js -client.getAccountLinkingURL().then(accountLinking => { +client.getAccountLinkingURL().then((accountLinking) => { console.log(accountLinking); // { // accountLinkingUrl: @@ -1935,7 +1937,7 @@ Retrieves the current value of target audience. Example: ```js -client.getTargetAudience().then(targetAudience => { +client.getTargetAudience().then((targetAudience) => { console.log(targetAudience); // { // audienceType: 'custom', @@ -1989,7 +1991,7 @@ Retrieves the current value of chat extension home URL. Example: ```js -client.getHomeURL().then(chatExtension => { +client.getHomeURL().then((chatExtension) => { console.log(chatExtension); // { // url: 'http://petershats.com/send-a-hat', @@ -2118,7 +2120,7 @@ Get the current thread owner. Example: ```js -client.getThreadOwner(USER_ID).then(threadOwner => { +client.getThreadOwner(USER_ID).then((threadOwner) => { console.log(threadOwner); // { // appId: '12345678910' @@ -2135,7 +2137,7 @@ Retrieves the list of apps that are Secondary Receivers for a page. Example: ```js -client.getSecondaryReceivers().then(receivers => { +client.getSecondaryReceivers().then((receivers) => { console.log(receivers); // [ // { @@ -2177,7 +2179,7 @@ Example: ```js client .getInsights(['page_messages_reported_conversations_unique']) - .then(counts => { + .then((counts) => { console.log(counts); // [ // { @@ -2213,7 +2215,7 @@ Retrieves the number of conversations with the Page that have been blocked. Example: ```js -client.getBlockedConversations().then(counts => { +client.getBlockedConversations().then((counts) => { console.log(counts); // { // "name": "page_messages_blocked_conversations_unique", @@ -2247,7 +2249,7 @@ Retrieves the number of conversations from your Page that have been reported by Example: ```js -client.getReportedConversations().then(counts => { +client.getReportedConversations().then((counts) => { console.log(counts); // { // "name": "page_messages_reported_conversations_unique", @@ -2286,7 +2288,7 @@ Retrieves the total number of open conversations between your Page and people in Example: ```js -client.getOpenConversations().then(result => { +client.getOpenConversations().then((result) => { console.log(result); // { // name: 'page_messages_total_messaging_connections', @@ -2319,7 +2321,7 @@ Retrieves the number of people who have sent a message to your business, not inc Example: ```js -client.getTotalMessagingConnections().then(result => { +client.getTotalMessagingConnections().then((result) => { console.log(result); // { // name: 'page_messages_total_messaging_connections', @@ -2352,7 +2354,7 @@ Retrieves the number of messaging conversations on Facebook Messenger that began Example: ```js -client.getNewConversations().then(result => { +client.getNewConversations().then((result) => { console.log(result); // { // name: 'page_messages_new_conversations_unique', @@ -2479,7 +2481,7 @@ client userId: USER_ID, appSecret: APP_SECRET, }) - .then(result => { + .then((result) => { console.log(result); // { // data: [ @@ -2533,7 +2535,7 @@ client userId: USER_ID, appSecret: APP_SECRET, }) - .then(result => { + .then((result) => { console.log(result); // { // data: [ @@ -2577,7 +2579,7 @@ Creates a persona. createPersona({ name: 'John Mathew', profilePictureUrl: 'https://facebook.com/john_image.jpg', -}).then(persona => { +}).then((persona) => { console.log(persona); // { // "id": "" @@ -2596,7 +2598,7 @@ Retrieves the name and profile picture of a persona. | personaId | String | ID of the persona. | ```js -getPersona(personaId).then(persona => { +getPersona(personaId).then((persona) => { console.log(persona); // { // "name": "John Mathew", @@ -2617,7 +2619,7 @@ Retrieves personas associated with a Page using the cursor. | cursor | String | pagination cursor. | ```js -getPersonas(cursor).then(personas => { +getPersonas(cursor).then((personas) => { console.log(personas); // { // "data": [ @@ -2649,7 +2651,7 @@ getPersonas(cursor).then(personas => { Retrieves all the personas associated with a Page. ```js -getAllPersonas().then(personas => { +getAllPersonas().then((personas) => { console.log(personas); // [ // { @@ -2691,7 +2693,7 @@ Gets token information. Example: ```js -client.debugToken().then(pageInfo => { +client.debugToken().then((pageInfo) => { console.log(pageInfo); // { // appId: '000000000000000', @@ -2806,7 +2808,7 @@ Get page name and page id using Graph API. Example: ```js -client.getPageInfo().then(page => { +client.getPageInfo().then((page) => { console.log(page); // { // name: 'Bot Demo', @@ -2822,7 +2824,7 @@ Programmatically check the feature submission status of Page-level Platform feat Example: ```js -client.getMessagingFeatureReview().then(data => { +client.getMessagingFeatureReview().then((data) => { console.log(data); // [ // { diff --git a/docs/api-messenger-context.md b/docs/api-messenger-context.md index 7e491a6c2..e34c68784 100644 --- a/docs/api-messenger-context.md +++ b/docs/api-messenger-context.md @@ -962,7 +962,7 @@ Retrieving Labels Associated with the user. Example: ```js -context.getAssociatedLabels().then(result => { +context.getAssociatedLabels().then((result) => { console.log(result); // { // data: [ @@ -1000,7 +1000,7 @@ Retrieving profile of the user. Example: ```js -context.getUserProfile().then(user => { +context.getUserProfile().then((user) => { console.log(user); // { // firstName: 'Johnathan', diff --git a/docs/api-slack-client.md b/docs/api-slack-client.md index f2b496247..695673678 100644 --- a/docs/api-slack-client.md +++ b/docs/api-slack-client.md @@ -37,7 +37,7 @@ async function MyAction(context) { `SlackOAuthClient` uses [axios](https://github.com/axios/axios) as HTTP client. We use [axios-error](https://github.com/Yoctol/messaging-apis/tree/master/packages/axios-error) package to wrap API error instances for better formatting error messages. Calling `console.log` with the error instance returns the formatted message. If you'd like to get the axios `request`, `response`, or `config`, you can still get them via those keys on the error instance. ```js -client.callMethod(method, body).catch(error => { +client.callMethod(method, body).catch((error) => { console.log(error); // the formatted error message console.log(error.stack); // stack trace of the error console.log(error.config); // axios request config @@ -166,7 +166,7 @@ Lists all users in a Slack team. Example: ```js -client.getUserList({ cursor }).then(res => { +client.getUserList({ cursor }).then((res) => { console.log(res); // { // members: [ @@ -192,7 +192,7 @@ Recursively lists all users in a Slack team using cursor. Example: ```js -client.getAllUserList().then(res => { +client.getAllUserList().then((res) => { console.log(res); // [ // { ... }, @@ -216,7 +216,7 @@ Gets information about an user. Example: ```js -client.getUserInfo(userId).then(res => { +client.getUserInfo(userId).then((res) => { console.log(res); // { // id: 'U123456', @@ -242,7 +242,7 @@ Lists all channels in a Slack team. Example: ```js -client.getChannelList().then(res => { +client.getChannelList().then((res) => { console.log(res); // [ // { ... }, @@ -266,7 +266,7 @@ Gets information about a channel. Example: ```js -client.getChannelInfo(channelId).then(res => { +client.getChannelInfo(channelId).then((res) => { console.log(res); // { // id: 'C8763', @@ -293,7 +293,7 @@ Retrieve information about a conversation. Example: ```js -client.getConversationInfo(channelId).then(res => { +client.getConversationInfo(channelId).then((res) => { console.log(res); // { // id: 'C8763', @@ -319,7 +319,7 @@ Example: ```js client.getConversationMembers(channelId, { cursor: 'xxx' }); -client.getConversationMembers(channelId).then(res => { +client.getConversationMembers(channelId).then((res) => { console.log(res); // { // members: ['U061F7AUR', 'U0C0NS9HN'], @@ -343,7 +343,7 @@ Recursively retrieve members of a conversation using cursor. Example: ```js -client.getAllConversationMembers(channelId).then(res => { +client.getAllConversationMembers(channelId).then((res) => { console.log(res); // ['U061F7AUR', 'U0C0NS9HN', ...] }); @@ -364,7 +364,7 @@ Example: ```js client.getConversationList({ cursor: 'xxx' }); -client.getConversationList().then(res => { +client.getConversationList().then((res) => { console.log(res); // { // channels: [ @@ -398,7 +398,7 @@ Recursively lists all channels in a Slack team using cursor. Example: ```js -client.getAllConversationList().then(res => { +client.getAllConversationList().then((res) => { console.log(res); // [ // { diff --git a/docs/api-telegram-client.md b/docs/api-telegram-client.md index bf286601e..d5e75b11c 100644 --- a/docs/api-telegram-client.md +++ b/docs/api-telegram-client.md @@ -46,7 +46,7 @@ async function MyAction(context) { `TelegramClient` uses [axios](https://github.com/axios/axios) as HTTP client. We use [axios-error](https://github.com/Yoctol/messaging-apis/tree/master/packages/axios-error) package to wrap API error instances for better formatting error messages. Calling `console.log` with the error instance returns the formatted message. If you'd like to get the axios `request`, `response`, or `config`, you can still get them via those keys on the error instance. ```js -client.getWebhookInfo().catch(error => { +client.getWebhookInfo().catch((error) => { console.log(error); // the formatted error message console.log(error.stack); // stack trace of the error console.log(error.config); // axios request config @@ -72,7 +72,7 @@ Gets current webhook status. Example: ```js -client.getWebhookInfo().then(info => { +client.getWebhookInfo().then((info) => { console.log(info); // { // url: 'https://4a16faff.ngrok.io/', @@ -100,7 +100,7 @@ client .getUpdates({ limit: 10, }) - .then(updates => { + .then((updates) => { console.log(updates); /* [ @@ -467,7 +467,7 @@ Gets bot's information. Example: ```js -client.getMe().then(result => { +client.getMe().then((result) => { console.log(result); // { // id: 313534466, @@ -491,7 +491,7 @@ Gets a list of profile pictures for a user. Example: ```js -client.getUserProfilePhotos(USER_ID, { limit: 1 }).then(result => { +client.getUserProfilePhotos(USER_ID, { limit: 1 }).then((result) => { console.log(result); // { // totalCount: 3, @@ -539,7 +539,7 @@ Example: ```js client .getFile('UtAqweADGTo4Gz8cZAeR-ouu4XBx78EeqRkABPL_pM4A1UpI0koD65K2') - .then(file => { + .then((file) => { console.log(file); // { // fileId: 'UtAqweADGTo4Gz8cZAeR-ouu4XBx78EeqRkABPL_pM4A1UpI0koD65K2', @@ -564,7 +564,7 @@ Example: ```js client .getFileLink('UtAqweADGTo4Gz8cZAeR-ouu4XBx78EeqRkABPL_pM4A1UpI0koD65K2') - .then(link => { + .then((link) => { console.log(link); // 'https://api.telegram.org/file/bot/photos/1068230105874016297.jpg' }); @@ -583,7 +583,7 @@ Gets up to date information about the chat (current name of the user for one-on- Example: ```js -client.getChat(CHAT_ID).then(chat => { +client.getChat(CHAT_ID).then((chat) => { console.log(chat); // { // id: 313534466, @@ -608,7 +608,7 @@ Gets a list of administrators in a chat. Example: ```js -client.getChatAdministrators(CHAT_ID).then(admins => { +client.getChatAdministrators(CHAT_ID).then((admins) => { console.log(admins); // [ // { @@ -638,7 +638,7 @@ Gets the number of members in a chat. Example: ```js -client.getChatMembersCount(CHAT_ID).then(count => { +client.getChatMembersCount(CHAT_ID).then((count) => { console.log(count); // '6' }); ``` @@ -657,7 +657,7 @@ Gets information about a member of a chat. Example: ```js -client.getChatMember(CHAT_ID, USER_ID).then(member => { +client.getChatMember(CHAT_ID, USER_ID).then((member) => { console.log(member); // { // user: { @@ -1214,7 +1214,7 @@ Gets data for high score tables. Example: ```js -client.getGameHighScores(USER_ID).then(scores => { +client.getGameHighScores(USER_ID).then((scores) => { console.log(scores); // [ // { diff --git a/docs/api-telegram-context.md b/docs/api-telegram-context.md index 53525a7fd..482b4f4f8 100644 --- a/docs/api-telegram-context.md +++ b/docs/api-telegram-context.md @@ -306,7 +306,7 @@ Gets a list of profile pictures for a user. Example: ```js -context.getUserProfilePhotos().then(result => { +context.getUserProfilePhotos().then((result) => { console.log(result); // { // totalCount: 3, @@ -343,7 +343,7 @@ Gets up to date information about the chat (current name of the user for one-on- Example: ```js -context.getChat().then(result => { +context.getChat().then((result) => { console.log(result); // { // id: 313534466, @@ -364,7 +364,7 @@ Gets a list of administrators in the chat. Example: ```js -context.getChatAdministrators().then(result => { +context.getChatAdministrators().then((result) => { console.log(result); // [ // { @@ -390,7 +390,7 @@ Gets the number of members in the chat. Example: ```js -context.getChatMembersCount().then(result => { +context.getChatMembersCount().then((result) => { console.log(result); // '6' }); @@ -409,7 +409,7 @@ Gets information about a member of the chat. Example: ```js -context.getChatMember(USER_ID).then(result => { +context.getChatMember(USER_ID).then((result) => { console.log(result); // { // user: { diff --git a/docs/api-viber-client.md b/docs/api-viber-client.md index 063b0ec27..76f1992de 100644 --- a/docs/api-viber-client.md +++ b/docs/api-viber-client.md @@ -44,7 +44,7 @@ async function MyAction(context) { `ViberClient` uses [axios](https://github.com/axios/axios) as HTTP client. We use [axios-error](https://github.com/Yoctol/messaging-apis/tree/master/packages/axios-error) package to wrap API error instances for better formatting error messages. Calling `console.log` with the error instance returns the formatted message. If you'd like to get the axios `request`, `response`, or `config`, you can still get them via those keys on the error instance. ```js -client.setWebhook(url).catch(error => { +client.setWebhook(url).catch((error) => { console.log(error); // the formatted error message console.log(error.stack); // stack trace of the error console.log(error.config); // axios request config @@ -499,7 +499,7 @@ client ], 'a broadcast to everybody' ) - .then(result => { + .then((result) => { console.log(result); // { // messageToken: 40808912438712, @@ -532,7 +532,7 @@ It will fetch the account’s details as registered in Viber. Example: ```js -client.getAccountInfo().then(info => { +client.getAccountInfo().then((info) => { console.log(info); // { // status: 0, @@ -579,7 +579,7 @@ It will fetch the details of a specific Viber user based on his unique user ID. Example: ```js -client.getUserDetails('01234567890A=').then(user => { +client.getUserDetails('01234567890A=').then((user) => { console.log(user); // { // id: '01234567890A=', @@ -614,7 +614,7 @@ Example: ```js client .getOnlineStatus(['01234567890=', '01234567891=', '01234567893=']) - .then(status => { + .then((status) => { console.log(status); // [ // { diff --git a/docs/api-viber-context.md b/docs/api-viber-context.md index cc93a485d..8a4746b7f 100644 --- a/docs/api-viber-context.md +++ b/docs/api-viber-context.md @@ -370,7 +370,7 @@ It will fetch the details of the user. Example: ```js -context.getUserDetails().then(user => { +context.getUserDetails().then((user) => { console.log(user); // { // id: '01234567890A=', @@ -401,7 +401,7 @@ It will fetch the online status of the user. Example: ```js -context.getOnlineStatus().then(status => { +context.getOnlineStatus().then((status) => { console.log(status); // { // id: '01234567891=', diff --git a/docs/channel-line-liff.md b/docs/channel-line-liff.md index 424662014..47cef39c3 100644 --- a/docs/channel-line-liff.md +++ b/docs/channel-line-liff.md @@ -114,19 +114,19 @@ Before starting using any feature provided by LIFF, you need to create a `liff.h .then(() => { alert('LIFF init success!'); }) - .catch(err => { + .catch((err) => { alert(`error: ${JSON.stringify(err)}`); }); } document.addEventListener('DOMContentLoaded', () => { fetch(`/send-id`) - .then(reqResponse => reqResponse.json()) - .then(jsonResponse => { + .then((reqResponse) => reqResponse.json()) + .then((jsonResponse) => { let myLiffId = jsonResponse.id; initializeLiff(myLiffId); }) - .catch(err => { + .catch((err) => { alert(`error: ${JSON.stringify(err)}`); }); }); @@ -167,7 +167,7 @@ Let's add a click event listener to send messages on click. You could replace th .then(() => { setButtonHandler(); }) - .catch(err => { + .catch((err) => { alert(`error: ${JSON.stringify(err)}`); }); } @@ -187,7 +187,7 @@ Let's add a click event listener to send messages on click. You could replace th alert('message sent'); liff.closeWindow(); }) - .catch(err => { + .catch((err) => { window.alert('Error sending message: ' + err); }); }); @@ -195,12 +195,12 @@ Let's add a click event listener to send messages on click. You could replace th document.addEventListener('DOMContentLoaded', () => { fetch(`/send-id`) - .then(reqResponse => reqResponse.json()) - .then(jsonResponse => { + .then((reqResponse) => reqResponse.json()) + .then((jsonResponse) => { let myLiffId = jsonResponse.id; initializeLiff(myLiffId); }) - .catch(err => { + .catch((err) => { alert(`error: ${JSON.stringify(err)}`); }); }); diff --git a/docs/channel-line-migrating-from-sdk.md b/docs/channel-line-migrating-from-sdk.md index e3d10f575..154411649 100644 --- a/docs/channel-line-migrating-from-sdk.md +++ b/docs/channel-line-migrating-from-sdk.md @@ -26,7 +26,7 @@ const config = { const app = express(); app.post('/webhooks/line', line.middleware(config), (req, res) => { - Promise.all(req.body.events.map(handleEvent)).then(result => + Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result) ); }); diff --git a/docs/channel-line-notify.md b/docs/channel-line-notify.md index 842a8dc6d..154c8551e 100644 --- a/docs/channel-line-notify.md +++ b/docs/channel-line-notify.md @@ -96,7 +96,7 @@ const lineNotify = require('../lineNotify'); app.prepare().then(() => { //... - server.get('/notify/redirect', async function(req, res) { + server.get('/notify/redirect', async function (req, res) { const code = req.query.code; const token = await lineNotify.getToken(code); await lineNotify.sendNotify(token, 'Hello bottender!'); diff --git a/docs/channel-messenger-multi-page.md b/docs/channel-messenger-multi-page.md index fcb6cd80f..51235002a 100644 --- a/docs/channel-messenger-multi-page.md +++ b/docs/channel-messenger-multi-page.md @@ -35,7 +35,7 @@ module.exports = { appId: process.env.MESSENGER_APP_ID, appSecret: process.env.MESSENGER_APP_SECRET, verifyToken: process.env.MESSENGER_VERIFY_TOKEN, - mapPageToAccessToken: pageId => { + mapPageToAccessToken: (pageId) => { // resolve corresponding access token }, }, diff --git a/docs/channel-slack-routing.md b/docs/channel-slack-routing.md index 2ca03a4d3..c7bc37fe8 100644 --- a/docs/channel-slack-routing.md +++ b/docs/channel-slack-routing.md @@ -13,6 +13,9 @@ function App() { slack.message(HandleMessage), slack.event('pin_added', HandlePinAdded), slack.event('star_added', HandleStarAdded), + slack.event('*', HandleAnyEvent), + slack.command('/price', HandlePriceCommand), + slack.command('*', HandleAnySlashCommand), slack.any(HandleSlack), ]); } @@ -21,6 +24,9 @@ function App() { async function HandleMessage(context) {} async function HandlePinAdded(context) {} async function HandleStarAdded(context) {} +async function HandleAnyEvent(context) {} +async function HandlePriceCommand(context) {} +async function HandleAnySlashCommand(context) {} async function HandleSlack(context) {} ``` @@ -29,3 +35,4 @@ All available routes in `slack` that recognize different kind of events: - `slack.any` - triggers the action when receiving any Slack events. - `slack.message` - triggers the action when receiving Slack message events. - `slack.event` - triggers the action when receiving particular Slack events. See all event types in [Slack docs](https://api.slack.com/events). +- `slack.command` - triggers the action when receiving Slack slash command events. diff --git a/docs/migrating-v1.md b/docs/migrating-v1.md index 954cce427..3f5904778 100644 --- a/docs/migrating-v1.md +++ b/docs/migrating-v1.md @@ -48,7 +48,7 @@ const bot = new MessengerBot({ sessionStore: new MemorySessionStore(maxSize), }); -bot.onEvent(async context => { +bot.onEvent(async (context) => { await context.sendText('Hello World'); }); diff --git a/docs/the-basics-routing.md b/docs/the-basics-routing.md index 347968a7c..f1c43e802 100644 --- a/docs/the-basics-routing.md +++ b/docs/the-basics-routing.md @@ -147,7 +147,7 @@ If you prefer to use your route predicate, you may use the `route` function to c const { router, route } = require('bottender/router'); function sayHiTo(name, Action) { - return route(context => context.event.text === `Hi ${name}`, Action); + return route((context) => context.event.text === `Hi ${name}`, Action); } async function App(context) { diff --git a/examples/custom-connector-facebook/.env b/examples/custom-connector-facebook/.env new file mode 100644 index 000000000..92fc7058e --- /dev/null +++ b/examples/custom-connector-facebook/.env @@ -0,0 +1,5 @@ +FACEBOOK_PAGE_ID= +FACEBOOK_ACCESS_TOKEN= +FACEBOOK_APP_ID= +FACEBOOK_APP_SECRET= +FACEBOOK_VERIFY_TOKEN= diff --git a/examples/custom-connector-facebook/bottender.config.js b/examples/custom-connector-facebook/bottender.config.js new file mode 100644 index 000000000..ee064cb2f --- /dev/null +++ b/examples/custom-connector-facebook/bottender.config.js @@ -0,0 +1,17 @@ +const { FacebookConnector } = require('@bottender/facebook'); + +module.exports = { + channels: { + facebook: { + enabled: true, + path: '/webhooks/facebook', + connector: new FacebookConnector({ + pageId: process.env.FACEBOOK_PAGE_ID, + accessToken: process.env.FACEBOOK_ACCESS_TOKEN, + appId: process.env.FACEBOOK_APP_ID, + appSecret: process.env.FACEBOOK_APP_SECRET, + verifyToken: process.env.FACEBOOK_VERIFY_TOKEN, + }), + }, + }, +}; diff --git a/examples/custom-connector-facebook/index.js b/examples/custom-connector-facebook/index.js new file mode 100644 index 000000000..db892355a --- /dev/null +++ b/examples/custom-connector-facebook/index.js @@ -0,0 +1,3 @@ +module.exports = async function App(context) { + await context.sendText('Hello World'); +}; diff --git a/examples/custom-connector-facebook/package.json b/examples/custom-connector-facebook/package.json new file mode 100644 index 000000000..d240a20b7 --- /dev/null +++ b/examples/custom-connector-facebook/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "@bottender/facebook": "next", + "bottender": "next" + } +} diff --git a/examples/custom-server-express-typescript/src/server.ts b/examples/custom-server-express-typescript/src/server.ts index 6050c8ca7..44fcb1fc3 100644 --- a/examples/custom-server-express-typescript/src/server.ts +++ b/examples/custom-server-express-typescript/src/server.ts @@ -29,7 +29,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/custom-server-express/server.js b/examples/custom-server-express/server.js index 88dffbc70..cc8a65415 100644 --- a/examples/custom-server-express/server.js +++ b/examples/custom-server-express/server.js @@ -31,7 +31,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/custom-server-koa/server.js b/examples/custom-server-koa/server.js index bff637ff3..9b0611c6d 100644 --- a/examples/custom-server-koa/server.js +++ b/examples/custom-server-koa/server.js @@ -23,18 +23,18 @@ app.prepare().then(() => { const router = new Router(); - router.get('/api', ctx => { + router.get('/api', (ctx) => { ctx.response.body = { ok: true }; }); - router.all('*', async ctx => { + router.all('*', async (ctx) => { await handle(ctx.req, ctx.res); ctx.respond = false; }); server.use(router.routes()); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/custom-server-restify/server.js b/examples/custom-server-restify/server.js index eb95662fe..86b88a754 100644 --- a/examples/custom-server-restify/server.js +++ b/examples/custom-server-restify/server.js @@ -26,7 +26,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/line-liff-v1/server.js b/examples/line-liff-v1/server.js index 8f0add50b..73609d777 100644 --- a/examples/line-liff-v1/server.js +++ b/examples/line-liff-v1/server.js @@ -33,7 +33,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/line-liff-v2/server.js b/examples/line-liff-v2/server.js index eb8b90170..e70ae8665 100644 --- a/examples/line-liff-v2/server.js +++ b/examples/line-liff-v2/server.js @@ -37,7 +37,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/line-multiple-channels/.env b/examples/line-multiple-channels/.env new file mode 100644 index 000000000..5d9ea7002 --- /dev/null +++ b/examples/line-multiple-channels/.env @@ -0,0 +1,7 @@ +CHANNEL_1_CHANNEL_ID= +CHANNEL_1_ACCESS_TOKEN= +CHANNEL_1_CHANNEL_SECRET= + +CHANNEL_2_CHANNEL_ID= +CHANNEL_2_ACCESS_TOKEN= +CHANNEL_2_CHANNEL_SECRET= diff --git a/examples/line-multiple-channels/bottender.config.js b/examples/line-multiple-channels/bottender.config.js new file mode 100644 index 000000000..bb2719c79 --- /dev/null +++ b/examples/line-multiple-channels/bottender.config.js @@ -0,0 +1,32 @@ +module.exports = { + channels: { + line: { + enabled: true, + path: '/webhooks/line/:channelId', + + // [Optional] If you want to avoid user id conflict between LINE channels in the same provider, + // you must add prefix to the session keys using the parameters from the URL + getSessionKeyPrefix(event, { params }) { + return `${params.channelId}:`; + // or you can use the destination to avoid the conflict + // return `${event.destination}:`; + }, + + async getConfig({ params }) { + switch (params.channelId) { + case process.env.CHANNEL_2_CHANNEL_ID: + return { + accessToken: process.env.CHANNEL_2_ACCESS_TOKEN, + channelSecret: process.env.CHANNEL_2_CHANNEL_SECRET, + }; + case process.env.CHANNEL_1_CHANNEL_ID: + default: + return { + accessToken: process.env.CHANNEL_1_ACCESS_TOKEN, + channelSecret: process.env.CHANNEL_1_CHANNEL_SECRET, + }; + } + }, + }, + }, +}; diff --git a/examples/line-multiple-channels/index.js b/examples/line-multiple-channels/index.js new file mode 100644 index 000000000..db892355a --- /dev/null +++ b/examples/line-multiple-channels/index.js @@ -0,0 +1,3 @@ +module.exports = async function App(context) { + await context.sendText('Hello World'); +}; diff --git a/examples/line-multiple-channels/package.json b/examples/line-multiple-channels/package.json new file mode 100644 index 000000000..8642479aa --- /dev/null +++ b/examples/line-multiple-channels/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "bottender": "next" + } +} diff --git a/examples/line-notify/server.js b/examples/line-notify/server.js index 965a42c8b..67f05479b 100644 --- a/examples/line-notify/server.js +++ b/examples/line-notify/server.js @@ -34,7 +34,7 @@ app.prepare().then(() => { server.get('/notify/new', (req, res) => { const filename = path.join(`${__dirname}/notify.html`); const url = lineNotify.getAuthLink('test'); - ejs.renderFile(filename, { url }, {}, function(err, str) { + ejs.renderFile(filename, { url }, {}, function (err, str) { if (err) { console.log('err:'); console.log(err); @@ -43,7 +43,7 @@ app.prepare().then(() => { }); }); - server.get('/notify/redirect', async function(req, res) { + server.get('/notify/redirect', async function (req, res) { const code = req.query.code; const token = await lineNotify.getToken(code); await lineNotify.sendNotify(token, 'Hello bottender!'); @@ -55,7 +55,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/messenger-account-linking/public/scripts/disable-submit.js b/examples/messenger-account-linking/public/scripts/disable-submit.js index 8e809f3b2..81e21c9de 100644 --- a/examples/messenger-account-linking/public/scripts/disable-submit.js +++ b/examples/messenger-account-linking/public/scripts/disable-submit.js @@ -79,7 +79,7 @@ const passwordsMatch = () => { * @param {Object} field Fieldset item to apply check icon to * @returns {undefined} */ -const applyCheckToField = field => { +const applyCheckToField = (field) => { const inputStatus = document.querySelector( `#${field.name}Field > div.inputStatus` ); @@ -98,7 +98,7 @@ const applyCheckToField = field => { * @param {Object} field Fieldset item to remove icon from * @returns {undefined} */ -const removeStatusFromField = field => { +const removeStatusFromField = (field) => { const inputStatus = document.querySelector( `#${field.name}Field > div.inputStatus` ); @@ -117,7 +117,7 @@ const applyCheckToFilledFields = () => { null, document.getElementsByClassName('required') ); - requiredFields.forEach(field => { + requiredFields.forEach((field) => { if (field.value) { applyCheckToField(field); } else { @@ -137,7 +137,7 @@ const requiredFieldsFilled = () => { null, document.getElementsByClassName('required') ); - const unfilledFields = requiredFields.filter(field => field.value === ''); + const unfilledFields = requiredFields.filter((field) => field.value === ''); return unfilledFields.length === 0; }; diff --git a/examples/messenger-account-linking/routes/index.js b/examples/messenger-account-linking/routes/index.js index 3158ed037..7a1d1d020 100644 --- a/examples/messenger-account-linking/routes/index.js +++ b/examples/messenger-account-linking/routes/index.js @@ -11,7 +11,7 @@ const { Router } = require('express'); const router = Router(); /* GET home page. */ -router.get('/', function(req, res) { +router.get('/', function (req, res) { res.render('index'); }); diff --git a/examples/messenger-account-linking/routes/users.js b/examples/messenger-account-linking/routes/users.js index 65b6f6cbf..fac51fb4c 100644 --- a/examples/messenger-account-linking/routes/users.js +++ b/examples/messenger-account-linking/routes/users.js @@ -40,7 +40,7 @@ const linkAccountToMessenger = (res, username, redirectURI) => { /** * GET Create user account view */ -router.get('/create', function(req, res) { +router.get('/create', function (req, res) { const accountLinkingToken = req.query.account_linking_token; const redirectURI = req.query.redirect_uri; @@ -50,7 +50,7 @@ router.get('/create', function(req, res) { /** * Create user account and link to messenger */ -router.post('/create', function(req, res) { +router.post('/create', function (req, res) { const { username, password, password2, redirectURI } = req.body; if (UserStore.has(username)) { res.render('create-account', { @@ -77,7 +77,7 @@ router.post('/create', function(req, res) { * (sendAccountLinking) is pointed to this URL. * */ -router.get('/login', function(req, res) { +router.get('/login', function (req, res) { /* Account Linking Token is never used in this demo, however it is useful to know about this token in the context of account linking. @@ -96,7 +96,7 @@ router.get('/login', function(req, res) { /** * User login route is used to authorize account_link actions */ -router.post('/login', function(req, res) { +router.post('/login', function (req, res) { const { username, password, redirectURI } = req.body; const userLogin = UserStore.get(username); if (!userLogin || userLogin.password !== password) { diff --git a/examples/messenger-account-linking/server.js b/examples/messenger-account-linking/server.js index 6e29e5bef..045ea2ff0 100644 --- a/examples/messenger-account-linking/server.js +++ b/examples/messenger-account-linking/server.js @@ -47,7 +47,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/messenger-account-linking/stores/UserStore.js b/examples/messenger-account-linking/stores/UserStore.js index 305a1fdb4..18801b838 100644 --- a/examples/messenger-account-linking/stores/UserStore.js +++ b/examples/messenger-account-linking/stores/UserStore.js @@ -51,7 +51,7 @@ class UserStore extends Store { */ getByMessengerId(messengerId) { let currentUser = {}; - this.data.forEach(userData => { + this.data.forEach((userData) => { if (userData.messengerId === messengerId) { currentUser = userData; } diff --git a/examples/messenger-batch-multi-pages/bottender.config.js b/examples/messenger-batch-multi-pages/bottender.config.js index 53e7c5dfe..3291941b9 100644 --- a/examples/messenger-batch-multi-pages/bottender.config.js +++ b/examples/messenger-batch-multi-pages/bottender.config.js @@ -12,7 +12,7 @@ const { * but you can dynamically load tokens from SQL database, mongodb, redis, REST API ... * or whatever you want. */ -const mapPageToAccessToken = pageId => { +const mapPageToAccessToken = (pageId) => { switch (pageId) { case PAGE_1_PAGE_ID: return PAGE_1_ACCESS_TOKEN; diff --git a/examples/messenger-multi-pages/bottender.config.js b/examples/messenger-multi-pages/bottender.config.js index d2805158d..99a686bfc 100644 --- a/examples/messenger-multi-pages/bottender.config.js +++ b/examples/messenger-multi-pages/bottender.config.js @@ -10,7 +10,7 @@ const { * but you can dynamically load tokens from SQL database, mongodb, redis, REST API ... * or whatever you want. */ -const mapPageToAccessToken = pageId => { +const mapPageToAccessToken = (pageId) => { switch (pageId) { case PAGE_1_PAGE_ID: return PAGE_1_ACCESS_TOKEN; diff --git a/examples/slack-home-tab/.env b/examples/slack-home-tab/.env new file mode 100644 index 000000000..c31340e02 --- /dev/null +++ b/examples/slack-home-tab/.env @@ -0,0 +1,2 @@ +SLACK_ACCESS_TOKEN= +SLACK_SIGNING_SECRET= diff --git a/examples/slack-home-tab/README.md b/examples/slack-home-tab/README.md new file mode 100644 index 000000000..85e17456d --- /dev/null +++ b/examples/slack-home-tab/README.md @@ -0,0 +1,58 @@ +# Slack Home Tab + +## Install and Run + +Download this example or clone [bottender](https://github.com/Yoctol/bottender). + +```sh +curl https://codeload.github.com/Yoctol/bottender/tar.gz/master | tar -xz --strip=2 bottender-master/examples/slack-home-tab +cd slack-home-tab +``` + +Install dependencies: + +```sh +npm install +``` + +You must fill `SLACK_ACCESS_TOKEN` and `SLACK_SIGNING_SECRET` in your `.env` file. + +If you are not familiar with Slack Bot, you may refer to Bottender's doc, [Slack Setup](https://bottender.js.org/docs/channel-slack-setup), to find detailed instructions. + +After that, you can run the bot with this npm script: + +```sh +npm run dev +``` + +This command starts a server listening at `http://localhost:5000` for bot development. + +If you successfully start the server, you get a webhook URL in the format of `https://xxxxxxxx.ngrok.io/webhooks/slack` from your terminal. + +## Slack App Settings + +To enable the home tab, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / App Home / Show Tabs, and enable the home tab. + +## Set Webhook + +To set the webhook, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Event Subscriptions and [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Interactivity & Shortcuts, and use the webhook URL you got from running `npm run dev` to edit Request URLs for your bot. + +You must subscribed the `app_home_opened` event. + +## Idea of This Example + +This example is a bot running on [Slack](https://slack.com/). + +This example contains the following topics: + +- How to use home tab. +- How to update home tab view. + +For more information, check our [Slack guides](https://bottender.js.org/docs/en/channel-slack-block-kit). + +## Related Examples + +- [slack-modal-update](../slack-modal-update) +- [slack-modal-push](../slack-modal-push) +- [slack-modal-form](../slack-modal-form) +- [slack-modal-on-home](../slack-modal-on-home) diff --git a/examples/slack-home-tab/bottender.config.js b/examples/slack-home-tab/bottender.config.js new file mode 100644 index 000000000..9578fa52b --- /dev/null +++ b/examples/slack-home-tab/bottender.config.js @@ -0,0 +1,10 @@ +module.exports = { + channels: { + slack: { + enabled: true, + path: '/webhooks/slack', + accessToken: process.env.SLACK_ACCESS_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + }, + }, +}; diff --git a/examples/slack-home-tab/index.js b/examples/slack-home-tab/index.js new file mode 100644 index 000000000..82945be0a --- /dev/null +++ b/examples/slack-home-tab/index.js @@ -0,0 +1,58 @@ +const { router, route, slack } = require('bottender/router'); + +let counter = 0; + +function getBlocks(text) { + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'add count', + emoji: true, + }, + value: 'add count', + }, + ], + }, + ]; +} + +async function Home(context) { + await context.views.publish({ + userId: context.session.user.id, + view: { + type: 'home', + blocks: getBlocks(`click count: ${counter}`), + }, + }); +} + +async function OnBlockActions(context) { + if (context.event.action.value === 'add count') { + counter += 1; + return Home; + } +} + +async function Default(context) { + await context.sendText('Hello World'); +} + +module.exports = async function App(context) { + return router([ + slack.event('app_home_opened', Home), + slack.event('block_actions', OnBlockActions), + route('*', Default), + ]); +}; diff --git a/examples/slack-home-tab/package.json b/examples/slack-home-tab/package.json new file mode 100644 index 000000000..d39ea1d45 --- /dev/null +++ b/examples/slack-home-tab/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "bottender": "1.5.1-alpha.4" + } +} diff --git a/examples/slack-modal-form/.env b/examples/slack-modal-form/.env new file mode 100644 index 000000000..c31340e02 --- /dev/null +++ b/examples/slack-modal-form/.env @@ -0,0 +1,2 @@ +SLACK_ACCESS_TOKEN= +SLACK_SIGNING_SECRET= diff --git a/examples/slack-modal-form/README.md b/examples/slack-modal-form/README.md new file mode 100644 index 000000000..89bea9c5e --- /dev/null +++ b/examples/slack-modal-form/README.md @@ -0,0 +1,51 @@ +# Slack Modal Form + +## Install and Run + +Download this example or clone [bottender](https://github.com/Yoctol/bottender). + +```sh +curl https://codeload.github.com/Yoctol/bottender/tar.gz/master | tar -xz --strip=2 bottender-master/examples/slack-modal-form +cd slack-modal-form +``` + +Install dependencies: + +```sh +npm install +``` + +You must fill `SLACK_ACCESS_TOKEN` and `SLACK_SIGNING_SECRET` in your `.env` file. + +If you are not familiar with Slack Bot, you may refer to Bottender's doc, [Slack Setup](https://bottender.js.org/docs/channel-slack-setup), to find detailed instructions. + +After that, you can run the bot with this npm script: + +```sh +npm run dev +``` + +This command starts a server listening at `http://localhost:5000` for bot development. + +If you successfully start the server, you get a webhook URL in the format of `https://xxxxxxxx.ngrok.io/webhooks/slack` from your terminal. + +## Set Webhook + +To set the webhook, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Event Subscriptions and [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Interactivity & Shortcuts, and use the webhook URL you got from running `npm run dev` to edit Request URLs for your bot. + +## Idea of This Example + +This example is a bot running on [Slack](https://slack.com/). + +This example contains the following topics: + +- How to use modal as a form. + +For more information, check our [Slack guides](https://bottender.js.org/docs/en/channel-slack-block-kit). + +## Related Examples + +- [slack-home-tab](../slack-home-tab) +- [slack-modal-update](../slack-modal-update) +- [slack-modal-push](../slack-modal-push) +- [slack-modal-on-home](../slack-modal-on-home) diff --git a/examples/slack-modal-form/bottender.config.js b/examples/slack-modal-form/bottender.config.js new file mode 100644 index 000000000..9578fa52b --- /dev/null +++ b/examples/slack-modal-form/bottender.config.js @@ -0,0 +1,10 @@ +module.exports = { + channels: { + slack: { + enabled: true, + path: '/webhooks/slack', + accessToken: process.env.SLACK_ACCESS_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + }, + }, +}; diff --git a/examples/slack-modal-form/index.js b/examples/slack-modal-form/index.js new file mode 100644 index 000000000..fe6da3234 --- /dev/null +++ b/examples/slack-modal-form/index.js @@ -0,0 +1,221 @@ +const { router, route, slack } = require('bottender/router'); + +function getBlocks(text, buttonValue) { + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: buttonValue, + emoji: true, + }, + value: buttonValue, + }, + ], + }, + ]; +} + +async function ShowModal(context) { + const { triggerId } = context.event.rawEvent; + await context.views.open({ + triggerId, + view: { + type: 'modal', + title: { + type: 'plain_text', + text: 'Form', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'Submit', + emoji: true, + }, + close: { + type: 'plain_text', + text: 'Cancel', + emoji: true, + }, + notifyOnClose: true, + blocks: [ + { + blockId: 'date', + type: 'input', + element: { + actionId: 'element', + type: 'datepicker', + initialDate: '2020-07-01', + placeholder: { + type: 'plain_text', + text: 'Date', + emoji: true, + }, + }, + label: { + type: 'plain_text', + text: 'Date', + emoji: true, + }, + }, + { + blockId: 'time', + type: 'input', + element: { + actionId: 'element', + type: 'plain_text_input', + initialValue: '10:00', + }, + label: { + type: 'plain_text', + text: 'Time', + emoji: true, + }, + }, + { + blockId: 'checked', + type: 'input', + element: { + actionId: 'element', + type: 'checkboxes', + options: [ + { + text: { + type: 'plain_text', + text: '1', + emoji: true, + }, + value: '1', + }, + { + text: { + type: 'plain_text', + text: '2', + emoji: true, + }, + value: '2', + }, + { + text: { + type: 'plain_text', + text: '3', + emoji: true, + }, + value: '3', + }, + ], + }, + label: { + type: 'plain_text', + text: 'Check', + emoji: true, + }, + }, + { + blockId: 'radio', + type: 'input', + element: { + actionId: 'element', + type: 'radio_buttons', + options: [ + { + text: { + type: 'plain_text', + text: '1', + emoji: true, + }, + value: '1', + }, + { + text: { + type: 'plain_text', + text: '2', + emoji: true, + }, + value: '2', + }, + { + text: { + type: 'plain_text', + text: '3', + emoji: true, + }, + value: '3', + }, + ], + }, + label: { + type: 'plain_text', + text: 'Radio', + emoji: true, + }, + }, + ], + }, + }); +} + +function getFormValues(view) { + const v = view.state.values; + return { + date: v.date && v.date.element.selectedDate, + time: v.time && v.time.element.value, + checked: v.checked && v.checked.element.selectedOptions.map((o) => o.value), + radio: v.radio && v.radio.element.selectedOption.value, + }; +} + +async function OnBlockActions(context) { + if (context.event.action.value === 'show modal') { + return ShowModal; + } +} + +async function OnViewSubmission(context) { + const values = getFormValues(context.event.rawEvent.view); + await context.chat.postMessage({ + text: `You submited the form. The contents you provided as follow: + date: ${values.date} + time: ${values.time} + checked: ${values.checked} + radio: ${values.radio} + `, + }); +} + +async function OnFormClosed(context) { + const values = getFormValues(context.event.rawEvent.view); + await context.chat.postMessage({ + text: `You closed the form. The contents you provided as follow: + date: ${values.date} + time: ${values.time} + checked: ${values.checked} + radio: ${values.radio} + `, + }); +} + +async function Default(context) { + await context.chat.postMessage({ + blocks: getBlocks('message', 'show modal'), + }); +} + +module.exports = async function App(context) { + return router([ + slack.event('block_actions', OnBlockActions), + slack.event('view_submission', OnFormSubmitted), + slack.event('view_closed', OnFormClosed), + route('*', Default), + ]); +}; diff --git a/examples/slack-modal-form/package.json b/examples/slack-modal-form/package.json new file mode 100644 index 000000000..d39ea1d45 --- /dev/null +++ b/examples/slack-modal-form/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "bottender": "1.5.1-alpha.4" + } +} diff --git a/examples/slack-modal-on-home/.env b/examples/slack-modal-on-home/.env new file mode 100644 index 000000000..c31340e02 --- /dev/null +++ b/examples/slack-modal-on-home/.env @@ -0,0 +1,2 @@ +SLACK_ACCESS_TOKEN= +SLACK_SIGNING_SECRET= diff --git a/examples/slack-modal-on-home/README.md b/examples/slack-modal-on-home/README.md new file mode 100644 index 000000000..e7b905fb9 --- /dev/null +++ b/examples/slack-modal-on-home/README.md @@ -0,0 +1,57 @@ +# Slack Modal on Home + +## Install and Run + +Download this example or clone [bottender](https://github.com/Yoctol/bottender). + +```sh +curl https://codeload.github.com/Yoctol/bottender/tar.gz/master | tar -xz --strip=2 bottender-master/examples/slack-modal-on-home +cd slack-modal-on-home +``` + +Install dependencies: + +```sh +npm install +``` + +You must fill `SLACK_ACCESS_TOKEN` and `SLACK_SIGNING_SECRET` in your `.env` file. + +If you are not familiar with Slack Bot, you may refer to Bottender's doc, [Slack Setup](https://bottender.js.org/docs/channel-slack-setup), to find detailed instructions. + +After that, you can run the bot with this npm script: + +```sh +npm run dev +``` + +This command starts a server listening at `http://localhost:5000` for bot development. + +If you successfully start the server, you get a webhook URL in the format of `https://xxxxxxxx.ngrok.io/webhooks/slack` from your terminal. + +## Slack App Settings + +To enable the home tab, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / App Home / Show Tabs, and enable the home tab. + +## Set Webhook + +To set the webhook, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Event Subscriptions and [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Interactivity & Shortcuts, and use the webhook URL you got from running `npm run dev` to edit Request URLs for your bot. + +You must subscribed the `app_home_opened` event. + +## Idea of This Example + +This example is a bot running on [Slack](https://slack.com/). + +This example contains the following topics: + +- How to use modal on home tab. + +For more information, check our [Slack guides](https://bottender.js.org/docs/en/channel-slack-block-kit). + +## Related Examples + +- [slack-home-tab](../slack-home-tab) +- [slack-modal-update](../slack-modal-update) +- [slack-modal-push](../slack-modal-push) +- [slack-modal-form](../slack-modal-form) diff --git a/examples/slack-modal-on-home/bottender.config.js b/examples/slack-modal-on-home/bottender.config.js new file mode 100644 index 000000000..9578fa52b --- /dev/null +++ b/examples/slack-modal-on-home/bottender.config.js @@ -0,0 +1,10 @@ +module.exports = { + channels: { + slack: { + enabled: true, + path: '/webhooks/slack', + accessToken: process.env.SLACK_ACCESS_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + }, + }, +}; diff --git a/examples/slack-modal-on-home/index.js b/examples/slack-modal-on-home/index.js new file mode 100644 index 000000000..fb6b8c1b9 --- /dev/null +++ b/examples/slack-modal-on-home/index.js @@ -0,0 +1,103 @@ +const { router, route, slack } = require('bottender/router'); + +function getBlocks(text, buttonValue) { + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: buttonValue, + emoji: true, + }, + value: buttonValue, + }, + ], + }, + ]; +} + +let userName = 'home'; + +async function Home(context) { + await context.views.publish({ + userId: context.session.user.id, + view: { + type: 'home', + blocks: getBlocks(`Hello, *${userName}*.`, 'show modal'), + }, + }); +} + +async function ShowModal(context) { + const { triggerId } = context.event.rawEvent; + await context.views.open({ + triggerId, + view: { + type: 'modal', + title: { + type: 'plain_text', + text: 'What is your name?', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'Submit', + emoji: true, + }, + blocks: [ + { + blockId: 'name', + type: 'input', + element: { + actionId: 'element', + type: 'plain_text_input', + placeholder: { + type: 'plain_text', + text: 'type your name here', + }, + }, + label: { + type: 'plain_text', + text: 'Name', + emoji: true, + }, + }, + ], + }, + }); +} + +async function OnBlockActions(context) { + if (context.event.action.value === 'show modal') { + return ShowModal; + } +} + +async function OnViewSubmission(context) { + const v = context.event.rawEvent.view.state.values; + userName = (v.name && v.name.element.value) || userName; + return Home; +} + +async function Default(context) { + await context.sendText('Hello World'); +} + +module.exports = async function App(context) { + return router([ + slack.event('app_home_opened', Home), + slack.event('block_actions', OnBlockActions), + slack.event('view_submission', OnViewSubmission), + route('*', Default), + ]); +}; diff --git a/examples/slack-modal-on-home/package.json b/examples/slack-modal-on-home/package.json new file mode 100644 index 000000000..d39ea1d45 --- /dev/null +++ b/examples/slack-modal-on-home/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "bottender": "1.5.1-alpha.4" + } +} diff --git a/examples/slack-modal-push/README.md b/examples/slack-modal-push/README.md index 5acbfc9ce..8a458c531 100644 --- a/examples/slack-modal-push/README.md +++ b/examples/slack-modal-push/README.md @@ -48,4 +48,4 @@ For more information, check our [Slack guides](https://bottender.js.org/docs/en/ - [slack-home-tab](../slack-home-tab) - [slack-modal-update](../slack-modal-update) - [slack-modal-form](../slack-modal-form) -- [slack-modal-on-home](../slack-modal-on-home) \ No newline at end of file +- [slack-modal-on-home](../slack-modal-on-home) diff --git a/examples/slack-modal-update/.env b/examples/slack-modal-update/.env new file mode 100644 index 000000000..c31340e02 --- /dev/null +++ b/examples/slack-modal-update/.env @@ -0,0 +1,2 @@ +SLACK_ACCESS_TOKEN= +SLACK_SIGNING_SECRET= diff --git a/examples/slack-modal-update/README.md b/examples/slack-modal-update/README.md new file mode 100644 index 000000000..00c719e9f --- /dev/null +++ b/examples/slack-modal-update/README.md @@ -0,0 +1,51 @@ +# Slack Modal Update + +## Install and Run + +Download this example or clone [bottender](https://github.com/Yoctol/bottender). + +```sh +curl https://codeload.github.com/Yoctol/bottender/tar.gz/master | tar -xz --strip=2 bottender-master/examples/slack-modal-update +cd slack-modal-update +``` + +Install dependencies: + +```sh +npm install +``` + +You must fill `SLACK_ACCESS_TOKEN` and `SLACK_SIGNING_SECRET` in your `.env` file. + +If you are not familiar with Slack Bot, you may refer to Bottender's doc, [Slack Setup](https://bottender.js.org/docs/channel-slack-setup), to find detailed instructions. + +After that, you can run the bot with this npm script: + +```sh +npm run dev +``` + +This command starts a server listening at `http://localhost:5000` for bot development. + +If you successfully start the server, you get a webhook URL in the format of `https://xxxxxxxx.ngrok.io/webhooks/slack` from your terminal. + +## Set Webhook + +To set the webhook, go to [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Event Subscriptions and [Slack Developer Console](https://api.slack.com/apps) / [YourApp] / Interactivity & Shortcuts, and use the webhook URL you got from running `npm run dev` to edit Request URLs for your bot. + +## Idea of This Example + +This example is a bot running on [Slack](https://slack.com/). + +This example contains the following topics: + +- How to update a modal view. + +For more information, check our [Slack guides](https://bottender.js.org/docs/en/channel-slack-block-kit). + +## Related Examples + +- [slack-home-tab](../slack-home-tab) +- [slack-modal-push](../slack-modal-push) +- [slack-modal-form](../slack-modal-form) +- [slack-modal-on-home](../slack-modal-on-home) diff --git a/examples/slack-modal-update/bottender.config.js b/examples/slack-modal-update/bottender.config.js new file mode 100644 index 000000000..9578fa52b --- /dev/null +++ b/examples/slack-modal-update/bottender.config.js @@ -0,0 +1,10 @@ +module.exports = { + channels: { + slack: { + enabled: true, + path: '/webhooks/slack', + accessToken: process.env.SLACK_ACCESS_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + }, + }, +}; diff --git a/examples/slack-modal-update/index.js b/examples/slack-modal-update/index.js new file mode 100644 index 000000000..f091fee98 --- /dev/null +++ b/examples/slack-modal-update/index.js @@ -0,0 +1,79 @@ +const { router, route, slack } = require('bottender/router'); + +function getBlocks(text, buttonValue) { + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: buttonValue, + emoji: true, + }, + value: buttonValue, + }, + ], + }, + ]; +} + +async function ShowModal(context) { + const { triggerId } = context.event.rawEvent; + await context.views.open({ + triggerId, + view: { + type: 'modal', + title: { + type: 'plain_text', + text: 'Modal Title', + }, + blocks: getBlocks('in modal', 'update modal'), + }, + }); +} + +async function UpdateModal(context) { + const viewId = context.event.rawEvent.view.id; + await context.views.update({ + viewId, + view: { + type: 'modal', + title: { + type: 'plain_text', + text: 'Modal Title', + }, + blocks: getBlocks('modal updated', 'update modal again'), + }, + }); +} + +async function OnBlockActions(context) { + if (context.event.action.value === 'show modal') { + return ShowModal; + } + if (context.event.action.value === 'update modal') { + return UpdateModal; + } +} + +async function Default(context) { + await context.chat.postMessage({ + blocks: getBlocks('message', 'show modal'), + }); +} + +module.exports = async function App(context) { + return router([ + slack.event('block_actions', OnBlockActions), + route('*', Default), + ]); +}; diff --git a/examples/slack-modal-update/package.json b/examples/slack-modal-update/package.json new file mode 100644 index 000000000..d39ea1d45 --- /dev/null +++ b/examples/slack-modal-update/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "dev": "bottender dev", + "start": "bottender start" + }, + "dependencies": { + "bottender": "1.5.1-alpha.4" + } +} diff --git a/examples/telegram-game/gameSession.js b/examples/telegram-game/gameSession.js index d1798a497..66b642126 100644 --- a/examples/telegram-game/gameSession.js +++ b/examples/telegram-game/gameSession.js @@ -1,9 +1,7 @@ const session = {}; function createSession(data) { - const key = Math.random() - .toString(36) - .substr(2, 10); + const key = Math.random().toString(36).substr(2, 10); session[key] = data; return key; } diff --git a/examples/telegram-game/server.js b/examples/telegram-game/server.js index 374e09053..234ad556d 100644 --- a/examples/telegram-game/server.js +++ b/examples/telegram-game/server.js @@ -48,7 +48,7 @@ app.prepare().then(() => { return handle(req, res); }); - server.listen(port, err => { + server.listen(port, (err) => { if (err) throw err; console.log(`> Ready on http://localhost:${port}`); }); diff --git a/examples/telegram-inline-keyboard/index.js b/examples/telegram-inline-keyboard/index.js index 439d6bbab..b724ddc3b 100644 --- a/examples/telegram-inline-keyboard/index.js +++ b/examples/telegram-inline-keyboard/index.js @@ -2,8 +2,8 @@ const { router, telegram, text } = require('bottender/router'); function generateInlineKeyboard(table) { return { - inlineKeyboard: table.map(row => - row.map(cell => ({ + inlineKeyboard: table.map((row) => + row.map((cell) => ({ text: cell, callbackData: cell, })) diff --git a/examples/telegram-poll/index.js b/examples/telegram-poll/index.js index 8832e461b..2d0c6991f 100644 --- a/examples/telegram-poll/index.js +++ b/examples/telegram-poll/index.js @@ -2,7 +2,7 @@ const { router, telegram, text, route } = require('bottender/router'); telegram.pollAnswer = function pollAnswer(action) { return route( - context => context.event.rawEvent.pollAnswer !== undefined, + (context) => context.event.rawEvent.pollAnswer !== undefined, action ); }; @@ -26,7 +26,7 @@ async function NewPoll(context) { async function RecordPollAnswer(context) { const { pollId, user, optionIds } = context.event.rawEvent.pollAnswer; const username = user.username || `${user.firstName} ${user.lastName}`; - const voteOptions = optionIds.map(id => pollOptions[id]).join(', '); + const voteOptions = optionIds.map((id) => pollOptions[id]).join(', '); const replyText = voteOptions.length === 0 diff --git a/examples/with-aws-lambda/server.js b/examples/with-aws-lambda/server.js index 4f99a3073..88a70085c 100644 --- a/examples/with-aws-lambda/server.js +++ b/examples/with-aws-lambda/server.js @@ -26,7 +26,7 @@ server.all('*', (req, res) => { }); }); -server.listen(port, err => { +server.listen(port, (err) => { if (err) { console.log(err); throw err; diff --git a/examples/with-gcp-cloud-function/server.js b/examples/with-gcp-cloud-function/server.js index cce280b91..02cf074d4 100644 --- a/examples/with-gcp-cloud-function/server.js +++ b/examples/with-gcp-cloud-function/server.js @@ -25,7 +25,7 @@ server.all('*', (req, res) => { }); }); -server.listen(port, err => { +server.listen(port, (err) => { if (err) { console.log(err); throw err; diff --git a/lerna.json b/lerna.json index 5c74f2b6d..05f02e302 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "lerna": "3.4.0", "npmClient": "yarn", "useWorkspaces": true, - "version": "1.4.12", + "version": "1.5.1-alpha.9", "command": { "publish": { "ignoreChanges": [ diff --git a/package.json b/package.json index fc7566fbe..6431c4c31 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,6 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "workspaces": [ - "packages/*" - ], "scripts": { "bootstrap": "lerna bootstrap", "clean": "git clean -dfqX -- ./node_modules **/{dist,node_modules}/ ./packages/*/tsconfig*tsbuildinfo", @@ -27,50 +24,48 @@ "testonly": "jest", "testonly:cov": "jest --coverage", "testonly:watch": "jest --watch", - "version": "cp README.md packages/bottender" + "version": "cp README.md packages/bottender", + "prepare": "husky install" }, + "workspaces": [ + "packages/*" + ], + "dependencies": {}, "devDependencies": { - "@types/jest": "^25.1.3", - "@types/prettier": "^1.19.0", - "@typescript-eslint/eslint-plugin": "^2.21.0", - "@typescript-eslint/parser": "^2.21.0", - "chalk": "^3.0.0", - "cross-env": "^7.0.0", - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.10.0", - "eslint-config-react-app": "^5.2.0", - "eslint-config-yoctol": "^0.24.0", - "eslint-config-yoctol-base": "^0.22.0", - "eslint-plugin-import": "2.20.0", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-prettier": "^3.1.2", - "eslint-plugin-react": "^7.18.3", - "eslint-plugin-react-hooks": "^2.5.0", - "eslint-plugin-sort-imports-es6-autofix": "^0.5.0", - "glob": "^7.1.6", - "husky": "^4.2.3", - "jest": "^25.1.0", - "jest-create-mock-instance": "^1.1.0", - "jest-junit": "^10.0.0", - "lerna": "^3.20.2", - "lint-staged": "^10.0.8", - "micromatch": "^4.0.2", - "mkdirp": "^1.0.3", - "nock": "^12.0.1", + "@types/jest": "^26.0.4", + "@types/prettier": "^2.3.2", + "@typescript-eslint/eslint-plugin": "^4.31.0", + "@typescript-eslint/parser": "^4.31.0", + "axios-mock-adapter": "^1.20.0", + "chalk": "^4.1.2", + "cross-env": "^7.0.3", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-config-yoctol-base": "^0.24.1", + "eslint-import-resolver-typescript": "^2.5.0", + "eslint-plugin-import": "2.24.2", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", + "eslint-plugin-tsdoc": "^0.2.14", + "glob": "^7.1.7", + "husky": "^7.0.0", + "jest": "^26.1.0", + "jest-junit": "^12.2.0", + "lerna": "^3.22.1", + "lint-staged": "^11.1.2", + "micromatch": "^4.0.4", + "mkdirp": "^1.0.4", + "mockdate": "^3.0.5", + "nock": "^13.1.3", "once": "^1.4.0", - "prettier": "^1.19.1", - "prettier-package-json": "^2.1.3", + "prettier": "^2.4.0", + "prettier-package-json": "^2.6.0", "rimraf": "^3.0.2", - "supertest": "^4.0.2", - "ts-jest": "^25.2.1", - "typescript": "^3.8.3" + "supertest": "^6.1.6", + "ts-jest": "^26.1.1", + "typescript": "^3.9.6" }, "engines": { "node": ">=8" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } } } diff --git a/packages/bottender-dialogflow/package.json b/packages/bottender-dialogflow/package.json index cd261b81f..5f9508624 100644 --- a/packages/bottender-dialogflow/package.json +++ b/packages/bottender-dialogflow/package.json @@ -1,5 +1,6 @@ { "name": "@bottender/dialogflow", + "version": "1.5.1-alpha.9", "description": "Dialogflow integration for Bottender.", "license": "MIT", "homepage": "https://bottender.js.org/", @@ -7,22 +8,20 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.11", "main": "dist/index.js", "files": [ "dist" ], "types": "dist/index.d.ts", "dependencies": { - "@types/dialogflow": "^4.0.4", - "dialogflow": "^1.2.0", + "@google-cloud/dialogflow": "^4.3.1", "invariant": "^2.2.4" }, "peerDependencies": { "bottender": ">= 1.2.0-0" }, "devDependencies": { - "bottender": "^1.4.10" + "bottender": "^1.5.1-alpha.9" }, "keywords": [ "bot", diff --git a/packages/bottender-dialogflow/src/__tests__/index.spec.ts b/packages/bottender-dialogflow/src/__tests__/index.spec.ts index ab1f5899b..e7122cc93 100644 --- a/packages/bottender-dialogflow/src/__tests__/index.spec.ts +++ b/packages/bottender-dialogflow/src/__tests__/index.spec.ts @@ -1,16 +1,17 @@ -import dialogflowSdk from 'dialogflow'; +import dialogflowSdk, { protos } from '@google-cloud/dialogflow'; import { Context, chain } from 'bottender'; // FIXME: export public API for testing +import { mocked } from 'ts-jest/utils'; import { run } from 'bottender/dist/bot/Bot'; -process.env.GOOGLE_APPLICATION_CREDENTIALS = 'test'; +import dialogflow from '..'; -const dialogflow = require('..'); // eslint-disable-line @typescript-eslint/no-var-requires +process.env.GOOGLE_APPLICATION_CREDENTIALS = 'test'; -jest.mock('dialogflow'); +jest.mock('@google-cloud/dialogflow'); // FIXME: export public test-utils for testing -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } @@ -18,6 +19,19 @@ class TestContext extends Context { sendText = jest.fn(); } +const sessionPath = 'SESSION_PATH'; + +const defaultDetectIntentRequest = { + session: sessionPath, + queryInput: { + text: { + languageCode: 'en', + text: 'text', + }, + }, + queryParams: { timeZone: undefined }, +}; + function setup({ event = { isMessage: true, @@ -34,7 +48,43 @@ function setup({ }, }, }, -} = {}) { + detectIntentRequest = defaultDetectIntentRequest, + detectIntentResponse, +}: { + event?: { + isMessage: boolean; + isText: boolean; + text: string; + message: { + id: string; + text: string; + }; + rawEvent: { + message: { + id: string; + text: string; + }; + }; + }; + detectIntentRequest?: protos.google.cloud.dialogflow.v2.IDetectIntentRequest; + detectIntentResponse: protos.google.cloud.dialogflow.v2.IDetectIntentResponse; +}) { + const detectIntentResolvedValue: [ + protos.google.cloud.dialogflow.v2.IDetectIntentResponse, + protos.google.cloud.dialogflow.v2.IDetectIntentRequest, + {} // eslint-disable-line @typescript-eslint/ban-types + ] = [detectIntentResponse, detectIntentRequest, {}]; + + mocked( + dialogflowSdk.SessionsClient.prototype + ).projectAgentSessionPath.mockReturnValueOnce(sessionPath); + mocked( + dialogflowSdk.SessionsClient.prototype + ).detectIntent.mockResolvedValueOnce( + // @ts-ignore: this resolved type should not be hinted to never + detectIntentResolvedValue + ); + const context = new TestContext({ client: {}, event, @@ -48,6 +98,8 @@ function setup({ return { context, + detectIntentRequest, + detectIntentResponse, }; } @@ -60,31 +112,20 @@ async function Unknown(context) { } it('should resolve corresponding action if intent match name', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - queryResult: { - queryText: 'text', - languageCode: 'en', - intent: { - name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', - displayName: 'greeting', - isFallback: false, - }, - parameters: {}, - }, + const { context, detectIntentRequest } = setup({ + detectIntentResponse: { + responseId: 'RESPONSE_ID', + queryResult: { + queryText: 'text', + languageCode: 'en', + intent: { + name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', + displayName: 'greeting', + isFallback: false, }, - ]), - }; - - return sessionClient; + parameters: {}, + }, + }, }); const app = run( @@ -99,114 +140,95 @@ it('should resolve corresponding action if intent match name', async () => { ]) ); - await app(context); + await app(context, {}); expect(context.sendText).toBeCalledWith('Hello!'); - expect(context.intent).toEqual('greeting'); - expect(context.isHandled).toEqual(true); + expect(context.intent).toBe('greeting'); + expect(context.isHandled).toBe(true); - expect(sessionClient.sessionPath).toBeCalledWith('PROJECT_ID', 'test:1'); - expect(sessionClient.detectIntent).toBeCalledWith({ - session: sessionPath, - queryInput: { - text: { - languageCode: 'en', - text: 'text', - }, - }, - queryParams: { timeZone: undefined }, - }); + const sessionClient = mocked(dialogflowSdk.SessionsClient).mock.instances[0]; + + expect(sessionClient.projectAgentSessionPath).toBeCalledWith( + 'PROJECT_ID', + 'test:1' + ); + expect(sessionClient.detectIntent).toBeCalledWith(detectIntentRequest); }); it('should resolve corresponding action if intent match id', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', - queryResult: { - fulfillmentMessages: [ - { - platform: 'PLATFORM_UNSPECIFIED', - text: { - text: ['?'], + const { context } = setup({ + detectIntentResponse: { + responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', + queryResult: { + fulfillmentMessages: [ + { + platform: 'PLATFORM_UNSPECIFIED', + text: { + text: ['?'], + }, + message: 'text', + }, + ], + outputContexts: [ + { + name: 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', + lifespanCount: 1, + parameters: { + fields: { + 'no-match': { + numberValue: 5, + kind: 'numberValue', }, - message: 'text', - }, - ], - outputContexts: [ - { - name: - 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', - lifespanCount: 1, - parameters: { - fields: { - 'no-match': { - numberValue: 5, - kind: 'numberValue', - }, - 'no-input': { - numberValue: 0, - kind: 'numberValue', - }, - }, + 'no-input': { + numberValue: 0, + kind: 'numberValue', }, }, - ], - queryText: 'hi', - speechRecognitionConfidence: 0, - action: 'input.unknown', - parameters: { - fields: {}, - }, - allRequiredParamsPresent: true, - fulfillmentText: '?', - webhookSource: '', - webhookPayload: null, - intent: { - inputContextNames: [], - events: [], - trainingPhrases: [], - outputContexts: [], - parameters: [], - messages: [], - defaultResponsePlatforms: [], - followupIntentInfo: [], - name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', - displayName: 'greeting', - priority: 0, - isFallback: true, - webhookState: 'WEBHOOK_STATE_UNSPECIFIED', - action: '', - resetContexts: false, - rootFollowupIntentName: '', - parentFollowupIntentName: '', - mlDisabled: false, }, - intentDetectionConfidence: 1, - diagnosticInfo: null, - languageCode: 'zh-tw', - sentimentAnalysisResult: null, }, - webhookStatus: null, - outputAudio: { - type: 'Buffer', - data: [], - }, - outputAudioConfig: null, + ], + queryText: 'hi', + speechRecognitionConfidence: 0, + action: 'input.unknown', + parameters: { + fields: {}, }, - null, - null, - ]), - }; - - return sessionClient; + allRequiredParamsPresent: true, + fulfillmentText: '?', + webhookSource: '', + webhookPayload: null, + intent: { + inputContextNames: [], + events: [], + trainingPhrases: [], + outputContexts: [], + parameters: [], + messages: [], + defaultResponsePlatforms: [], + followupIntentInfo: [], + name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', + displayName: 'greeting', + priority: 0, + isFallback: true, + webhookState: 'WEBHOOK_STATE_UNSPECIFIED', + action: '', + resetContexts: false, + rootFollowupIntentName: '', + parentFollowupIntentName: '', + mlDisabled: false, + }, + intentDetectionConfidence: 1, + diagnosticInfo: null, + languageCode: 'zh-tw', + sentimentAnalysisResult: null, + }, + webhookStatus: null, + outputAudio: { + type: 'Buffer', + data: [], + }, + outputAudioConfig: null, + }, }); const app = run( @@ -221,102 +243,87 @@ it('should resolve corresponding action if intent match id', async () => { ]) ); - await app(context); + await app(context, {}); expect(context.sendText).toBeCalledWith('Hello!'); - expect(context.intent).toEqual('greeting'); - expect(context.isHandled).toEqual(true); + expect(context.intent).toBe('greeting'); + expect(context.isHandled).toBe(true); }); it('should resolve by fulfillmentMessages if intent match name without actions', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', - queryResult: { - fulfillmentMessages: [ - { - platform: 'PLATFORM_UNSPECIFIED', - text: { - text: ['?'], + const { context } = setup({ + detectIntentResponse: { + responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', + queryResult: { + fulfillmentMessages: [ + { + platform: 'PLATFORM_UNSPECIFIED', + text: { + text: ['?'], + }, + message: 'text', + }, + ], + outputContexts: [ + { + name: 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', + lifespanCount: 1, + parameters: { + fields: { + 'no-match': { + numberValue: 5, + kind: 'numberValue', }, - message: 'text', - }, - ], - outputContexts: [ - { - name: - 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', - lifespanCount: 1, - parameters: { - fields: { - 'no-match': { - numberValue: 5, - kind: 'numberValue', - }, - 'no-input': { - numberValue: 0, - kind: 'numberValue', - }, - }, + 'no-input': { + numberValue: 0, + kind: 'numberValue', }, }, - ], - queryText: 'hi', - speechRecognitionConfidence: 0, - action: 'input.unknown', - parameters: { - fields: {}, }, - allRequiredParamsPresent: true, - fulfillmentText: '?', - webhookSource: '', - webhookPayload: null, - intent: { - inputContextNames: [], - events: [], - trainingPhrases: [], - outputContexts: [], - parameters: [], - messages: [], - defaultResponsePlatforms: [], - followupIntentInfo: [], - name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', - displayName: 'greeting', - priority: 0, - isFallback: true, - webhookState: 'WEBHOOK_STATE_UNSPECIFIED', - action: '', - resetContexts: false, - rootFollowupIntentName: '', - parentFollowupIntentName: '', - mlDisabled: false, - }, - intentDetectionConfidence: 1, - diagnosticInfo: null, - languageCode: 'zh-tw', - sentimentAnalysisResult: null, - }, - webhookStatus: null, - outputAudio: { - type: 'Buffer', - data: [], }, - outputAudioConfig: null, + ], + queryText: 'hi', + speechRecognitionConfidence: 0, + action: 'input.unknown', + parameters: { + fields: {}, }, - null, - null, - ]), - }; - - return sessionClient; + allRequiredParamsPresent: true, + fulfillmentText: '?', + webhookSource: '', + webhookPayload: null, + intent: { + inputContextNames: [], + events: [], + trainingPhrases: [], + outputContexts: [], + parameters: [], + messages: [], + defaultResponsePlatforms: [], + followupIntentInfo: [], + name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', + displayName: 'greeting', + priority: 0, + isFallback: true, + webhookState: 'WEBHOOK_STATE_UNSPECIFIED', + action: '', + resetContexts: false, + rootFollowupIntentName: '', + parentFollowupIntentName: '', + mlDisabled: false, + }, + intentDetectionConfidence: 1, + diagnosticInfo: null, + languageCode: 'zh-tw', + sentimentAnalysisResult: null, + }, + webhookStatus: null, + outputAudio: { + type: 'Buffer', + data: [], + }, + outputAudioConfig: null, + }, }); const app = run( @@ -328,94 +335,79 @@ it('should resolve by fulfillmentMessages if intent match name without actions', ]) ); - await app(context); + await app(context, {}); expect(context.sendText).toBeCalledWith('?'); - expect(context.intent).toEqual('greeting'); - expect(context.isHandled).toEqual(true); + expect(context.intent).toBe('greeting'); + expect(context.isHandled).toBe(true); }); it('should go next if intent does not match any name', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', - queryResult: { - fulfillmentMessages: [], - outputContexts: [ - { - name: - 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', - lifespanCount: 1, - parameters: { - fields: { - 'no-match': { - numberValue: 5, - kind: 'numberValue', - }, - 'no-input': { - numberValue: 0, - kind: 'numberValue', - }, - }, + const { context } = setup({ + detectIntentResponse: { + responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', + queryResult: { + fulfillmentMessages: [], + outputContexts: [ + { + name: 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', + lifespanCount: 1, + parameters: { + fields: { + 'no-match': { + numberValue: 5, + kind: 'numberValue', + }, + 'no-input': { + numberValue: 0, + kind: 'numberValue', }, }, - ], - queryText: 'hi', - speechRecognitionConfidence: 0, - action: 'input.unknown', - parameters: { - fields: {}, - }, - allRequiredParamsPresent: true, - fulfillmentText: '?', - webhookSource: '', - webhookPayload: null, - intent: { - inputContextNames: [], - events: [], - trainingPhrases: [], - outputContexts: [], - parameters: [], - messages: [], - defaultResponsePlatforms: [], - followupIntentInfo: [], - name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', - displayName: 'order', - priority: 0, - isFallback: true, - webhookState: 'WEBHOOK_STATE_UNSPECIFIED', - action: '', - resetContexts: false, - rootFollowupIntentName: '', - parentFollowupIntentName: '', - mlDisabled: false, }, - intentDetectionConfidence: 1, - diagnosticInfo: null, - languageCode: 'zh-tw', - sentimentAnalysisResult: null, }, - webhookStatus: null, - outputAudio: { - type: 'Buffer', - data: [], - }, - outputAudioConfig: null, + ], + queryText: 'hi', + speechRecognitionConfidence: 0, + action: 'input.unknown', + parameters: { + fields: {}, }, - null, - null, - ]), - }; - - return sessionClient; + allRequiredParamsPresent: true, + fulfillmentText: '?', + webhookSource: '', + webhookPayload: null, + intent: { + inputContextNames: [], + events: [], + trainingPhrases: [], + outputContexts: [], + parameters: [], + messages: [], + defaultResponsePlatforms: [], + followupIntentInfo: [], + name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', + displayName: 'order', + priority: 0, + isFallback: true, + webhookState: 'WEBHOOK_STATE_UNSPECIFIED', + action: '', + resetContexts: false, + rootFollowupIntentName: '', + parentFollowupIntentName: '', + mlDisabled: false, + }, + intentDetectionConfidence: 1, + diagnosticInfo: null, + languageCode: 'zh-tw', + sentimentAnalysisResult: null, + }, + webhookStatus: null, + outputAudio: { + type: 'Buffer', + data: [], + }, + outputAudioConfig: null, + }, }); const app = run( @@ -430,83 +422,68 @@ it('should go next if intent does not match any name', async () => { ]) ); - await app(context); + await app(context, {}); expect(context.sendText).toBeCalledWith('Sorry, I don’t know what you say.'); - expect(context.intent).toEqual('order'); - expect(context.isHandled).toEqual(true); + expect(context.intent).toBe('order'); + expect(context.isHandled).toBe(true); }); it('should go next if no intent', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', - queryResult: { - fulfillmentMessages: [ - { - platform: 'PLATFORM_UNSPECIFIED', - text: { - text: ['?'], + const { context } = setup({ + detectIntentResponse: { + responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', + queryResult: { + fulfillmentMessages: [ + { + platform: 'PLATFORM_UNSPECIFIED', + text: { + text: ['?'], + }, + message: 'text', + }, + ], + outputContexts: [ + { + name: 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', + lifespanCount: 1, + parameters: { + fields: { + 'no-match': { + numberValue: 5, + kind: 'numberValue', }, - message: 'text', - }, - ], - outputContexts: [ - { - name: - 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', - lifespanCount: 1, - parameters: { - fields: { - 'no-match': { - numberValue: 5, - kind: 'numberValue', - }, - 'no-input': { - numberValue: 0, - kind: 'numberValue', - }, - }, + 'no-input': { + numberValue: 0, + kind: 'numberValue', }, }, - ], - queryText: 'hi', - speechRecognitionConfidence: 0, - action: 'input.unknown', - parameters: { - fields: {}, }, - allRequiredParamsPresent: true, - fulfillmentText: '?', - webhookSource: '', - webhookPayload: null, - intent: null, - intentDetectionConfidence: 1, - diagnosticInfo: null, - languageCode: 'zh-tw', - sentimentAnalysisResult: null, }, - webhookStatus: null, - outputAudio: { - type: 'Buffer', - data: [], - }, - outputAudioConfig: null, + ], + queryText: 'hi', + speechRecognitionConfidence: 0, + action: 'input.unknown', + parameters: { + fields: {}, }, - null, - null, - ]), - }; - - return sessionClient; + allRequiredParamsPresent: true, + fulfillmentText: '?', + webhookSource: '', + webhookPayload: null, + intent: null, + intentDetectionConfidence: 1, + diagnosticInfo: null, + languageCode: 'zh-tw', + sentimentAnalysisResult: null, + }, + webhookStatus: null, + outputAudio: { + type: 'Buffer', + data: [], + }, + outputAudioConfig: null, + }, }); const app = run( @@ -521,102 +498,87 @@ it('should go next if no intent', async () => { ]) ); - await app(context); + await app(context, {}); expect(context.sendText).toBeCalledWith('Sorry, I don’t know what you say.'); expect(context.intent).toBeNull(); - expect(context.isHandled).toEqual(false); + expect(context.isHandled).toBe(false); }); it('should support parameters of dialogflow', async () => { - const { context } = setup(); - - const sessionPath = {}; - let sessionClient; - - dialogflowSdk.SessionsClient.mockImplementationOnce(() => { - sessionClient = { - sessionPath: jest.fn().mockReturnValue(sessionPath), - detectIntent: jest.fn().mockResolvedValue([ - { - responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', - queryResult: { - fulfillmentMessages: [ - { - platform: 'PLATFORM_UNSPECIFIED', - text: { - text: ['?'], + const { context } = setup({ + detectIntentResponse: { + responseId: 'cb8e7a38-910a-4386-b312-eac8660d66f7-b4ef8d5f', + queryResult: { + fulfillmentMessages: [ + { + platform: 'PLATFORM_UNSPECIFIED', + text: { + text: ['?'], + }, + message: 'text', + }, + ], + outputContexts: [ + { + name: 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', + lifespanCount: 1, + parameters: { + fields: { + 'no-match': { + numberValue: 5, + kind: 'numberValue', }, - message: 'text', - }, - ], - outputContexts: [ - { - name: - 'projects/PROJECT_ID/agent/sessions/console:1/contexts/__system_counters__', - lifespanCount: 1, - parameters: { - fields: { - 'no-match': { - numberValue: 5, - kind: 'numberValue', - }, - 'no-input': { - numberValue: 0, - kind: 'numberValue', - }, - }, + 'no-input': { + numberValue: 0, + kind: 'numberValue', }, }, - ], - queryText: 'hi', - speechRecognitionConfidence: 0, - action: 'input.unknown', - parameters: { - fields: {}, - }, - allRequiredParamsPresent: true, - fulfillmentText: '?', - webhookSource: '', - webhookPayload: null, - intent: { - inputContextNames: [], - events: [], - trainingPhrases: [], - outputContexts: [], - parameters: [], - messages: [], - defaultResponsePlatforms: [], - followupIntentInfo: [], - name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', - displayName: 'greeting', - priority: 0, - isFallback: true, - webhookState: 'WEBHOOK_STATE_UNSPECIFIED', - action: '', - resetContexts: false, - rootFollowupIntentName: '', - parentFollowupIntentName: '', - mlDisabled: false, }, - intentDetectionConfidence: 1, - diagnosticInfo: null, - languageCode: 'zh-tw', - sentimentAnalysisResult: null, }, - webhookStatus: null, - outputAudio: { - type: 'Buffer', - data: [], - }, - outputAudioConfig: null, + ], + queryText: 'hi', + speechRecognitionConfidence: 0, + action: 'input.unknown', + parameters: { + fields: {}, }, - null, - null, - ]), - }; - - return sessionClient; + allRequiredParamsPresent: true, + fulfillmentText: '?', + webhookSource: '', + webhookPayload: null, + intent: { + inputContextNames: [], + events: [], + trainingPhrases: [], + outputContexts: [], + parameters: [], + messages: [], + defaultResponsePlatforms: [], + followupIntentInfo: [], + name: 'projects/PROJECT_ID/agent/intents/INTENT_ID', + displayName: 'greeting', + priority: 0, + isFallback: true, + webhookState: 'WEBHOOK_STATE_UNSPECIFIED', + action: '', + resetContexts: false, + rootFollowupIntentName: '', + parentFollowupIntentName: '', + mlDisabled: false, + }, + intentDetectionConfidence: 1, + diagnosticInfo: null, + languageCode: 'zh-tw', + sentimentAnalysisResult: null, + }, + webhookStatus: null, + outputAudio: { + type: 'Buffer', + data: [], + }, + outputAudioConfig: null, + }, }); const app = run( @@ -633,7 +595,9 @@ it('should support parameters of dialogflow', async () => { ]) ); - await app(context); + await app(context, {}); + + const sessionClient = mocked(dialogflowSdk.SessionsClient).mock.instances[0]; expect(context.sendText).toBeCalledWith('Hello!'); diff --git a/packages/bottender-dialogflow/src/index.ts b/packages/bottender-dialogflow/src/index.ts index ad06be9d1..690d8b0c1 100644 --- a/packages/bottender-dialogflow/src/index.ts +++ b/packages/bottender-dialogflow/src/index.ts @@ -1,26 +1,48 @@ -import dialogflowSdk from 'dialogflow'; +import dialogflowSdk, { protos } from '@google-cloud/dialogflow'; import invariant from 'invariant'; import { Action, Context, withProps } from 'bottender'; -import { Message, QueryResult } from './types'; - -function getFulfillments(fulfillmentMessages: Message[]): string[] { - if (!fulfillmentMessages) { +function getFulfillments( + messages?: protos.google.cloud.dialogflow.v2.Intent.IMessage[] | null +): string[] { + if (!messages) { return []; } - const fulfillmentTexts = fulfillmentMessages.filter( - m => m.platform === 'PLATFORM_UNSPECIFIED' && m.text !== undefined - ); + return messages + .filter((message) => message.platform === 'PLATFORM_UNSPECIFIED') + .map((message) => message.text) + .filter( + (text): text is protos.google.cloud.dialogflow.v2.Intent.Message.IText => + Boolean(text) + ) + .map((text) => text.text) + .filter((text): text is string[] => Boolean(text)) + .map((texts) => { + const index = Math.floor(Math.random() * texts.length); + return texts[index]; + }); +} + +function getTargetAction( + actions: Record< + string, + Action + >, + intent: protos.google.cloud.dialogflow.v2.IIntent +): Action | void { + if (intent.name && actions[intent.name]) { + return actions[intent.name]; + } - return fulfillmentTexts.map(fulfillmentText => { - const texts: string[] = fulfillmentText.text.text; - const index = Math.floor(Math.random() * texts.length); - return texts[index]; - }); + if (intent.displayName && actions[intent.displayName]) { + return actions[intent.displayName]; + } } /** + * @example + * ``` * const Dialogflow = dialogflow({ * projectId: 'PROJECT_ID', * actions: { @@ -29,8 +51,9 @@ function getFulfillments(fulfillmentMessages: Message[]): string[] { * }, * }, * }); + * ``` */ -module.exports = function dialogflow({ +export = function dialogflow({ projectId, languageCode = 'en', actions = {}, @@ -38,9 +61,12 @@ module.exports = function dialogflow({ }: { projectId: string; languageCode: string; - actions: Record, QueryResult>>; + actions: Record< + string, + Action + >; timeZone?: string; -}): Action> { +}): Action { invariant( typeof projectId === 'string' && projectId.length > 0, 'dialogflow: `projectId` is a required parameter.' @@ -55,14 +81,14 @@ module.exports = function dialogflow({ const sessionsClient = new dialogflowSdk.SessionsClient(); return async function Dialogflow( - context: Context, - { next }: { next?: Action> } - ): Promise> | void> { + context: Context, + { next }: { next?: Action } + ): Promise | void> { if (!context.event.isText || !context.session) { return next; } - const sessionPath = sessionsClient.sessionPath( + const sessionPath = sessionsClient.projectAgentSessionPath( projectId, context.session.id ); @@ -81,31 +107,40 @@ module.exports = function dialogflow({ }; // API Reference: https://cloud.google.com/dialogflow/docs/reference/rest/v2/projects.agent.sessions/detectIntent - const responses = await sessionsClient.detectIntent(request); - const queryResult = responses[0].queryResult as QueryResult; + const [response] = await sessionsClient.detectIntent(request); + const queryResult = response.queryResult; + + if (!queryResult) { + context.setAsNotHandled(); + return next; + } + const { intent, fulfillmentMessages } = queryResult; - if (intent) { + if (!intent) { + context.setAsNotHandled(); + return next; + } + + if (intent.displayName) { context.setIntent(intent.displayName); - context.setAsHandled(); + } + context.setAsHandled(); - // fulfillment by Bottender - const TargetAction = actions[intent.name] || actions[intent.displayName]; - if (TargetAction) { - return withProps(TargetAction, { ...queryResult }); - } + // fulfillment by Bottender + const TargetAction = getTargetAction(actions, intent); + if (TargetAction) { + return withProps(TargetAction, { ...queryResult }); + } - // fulfillment by Dialogflow - const fulfillments = getFulfillments(fulfillmentMessages); - if (fulfillments.length > 0) { - for (const fulfillment of fulfillments) { - // eslint-disable-next-line no-await-in-loop - await context.sendText(fulfillment); - } - return; + // fulfillment by Dialogflow + const fulfillments = getFulfillments(fulfillmentMessages); + if (fulfillments.length > 0) { + for (const fulfillment of fulfillments) { + // eslint-disable-next-line no-await-in-loop + await context.sendText(fulfillment); } - } else { - context.setAsNotHandled(); + return; } return next; diff --git a/packages/bottender-dialogflow/src/types.ts b/packages/bottender-dialogflow/src/types.ts deleted file mode 100644 index df294d762..000000000 --- a/packages/bottender-dialogflow/src/types.ts +++ /dev/null @@ -1,106 +0,0 @@ -export type QueryResult = { - queryText: string; - languageCode: string; - speechRecognitionConfidence: number; - action: string; - parameters: Record; - allRequiredParamsPresent: boolean; - fulfillmentText: string; - fulfillmentMessages: Message[]; - webhookSource: string; - webhookPayload: Record; - outputContexts: DialogflowContext[]; - intent: Intent; - intentDetectionConfidence: number; - diagnosticInfo: Record; - sentimentAnalysisResult: SentimentAnalysisResult; -}; - -export type Message = { - platform: Platform; -} & Record; - -export type DialogflowContext = { - name: string; - lifespanCount?: number; - parameters?: Record; -}; - -export type Platform = - | 'PLATFORM_UNSPECIFIED' - | 'FACEBOOK' - | 'SLACK' - | 'TELEGRAM' - | 'KIK' - | 'SKYPE' - | 'LINE' - | 'VIBER' - | 'ACTIONS_ON_GOOGLE' - | 'GOOGLE_HANGOUTS'; - -export type WebhookState = - | 'WEBHOOK_STATE_UNSPECIFIED' - | 'WEBHOOK_STATE_ENABLED' - | 'WEBHOOK_STATE_ENABLED_FOR_SLOT_FILLING'; - -export type Parameter = { - name: string; - displayName: string; - value?: string; - defaultValue?: string; - entityTypeDisplayName?: string; - mandatory?: boolean; - prompts?: string[]; - isList?: boolean; -}; - -export type TrainingPhrase = { - name: string; - type: Type; - parts: Part[]; - timesAddedCount?: number; -}; - -export type Type = 'TYPE_UNSPECIFIED' | 'EXAMPLE' | 'TEMPLATE'; - -export type Part = { - text: string; - entityType?: string; - alias?: string; - userDefined?: boolean; -}; - -export type FollowupIntentInfo = { - followupIntentName: string; - parentFollowupIntentName: string; -}; - -export type Intent = { - name: string; - displayName: string; - webhookState?: WebhookState; - priority?: number; - isFallback?: boolean; - mlDisabled?: boolean; - inputContextNames?: string[]; - events?: string[]; - trainingPhrases?: TrainingPhrase[]; - action?: string; - outputContexts?: DialogflowContext[]; - resetContexts?: boolean; - parameters?: Parameter[]; - messages?: Message[]; - defaultResponsePlatforms?: Platform[]; - rootFollowupIntentName: string; - parentFollowupIntentName: string; - followupIntentInfo: FollowupIntentInfo[]; -}; - -export type SentimentAnalysisResult = { - queryTextSentiment: Sentiment; -}; - -export type Sentiment = { - score: number; - magnitude: number; -}; diff --git a/packages/bottender-express/package.json b/packages/bottender-express/package.json index 90880478f..5b0a11eb1 100644 --- a/packages/bottender-express/package.json +++ b/packages/bottender-express/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.0", + "version": "1.5.1-alpha.5", "main": "dist/index.js", "files": [ "dist" diff --git a/packages/bottender-express/src/__tests__/createServer.spec.ts b/packages/bottender-express/src/__tests__/createServer.spec.ts index 587b1bfb4..bf5b6299c 100644 --- a/packages/bottender-express/src/__tests__/createServer.spec.ts +++ b/packages/bottender-express/src/__tests__/createServer.spec.ts @@ -30,13 +30,11 @@ it('should respond accordingly if shouldNext = false', async () => { }); const server = createServer(bot); - const { status, text } = await request(server) - .get('/') - .query({ - 'hub.mode': 'subscribe', - 'hub.verify_token': bot.connector.verifyToken, - 'hub.challenge': 'chatbot is awesome', - }); + const { status, text } = await request(server).get('/').query({ + 'hub.mode': 'subscribe', + 'hub.verify_token': bot.connector.verifyToken, + 'hub.challenge': 'chatbot is awesome', + }); expect(status).toBe(200); expect(text).toBe('chatbot is awesome'); @@ -50,9 +48,7 @@ it('should handle bot request if shouldNext = true', async () => { requestHandler.mockResolvedValue(); const server = createServer(bot); - const { status } = await request(server) - .post('/') - .send({}); + const { status } = await request(server).post('/').send({}); expect(status).toBe(200); }); diff --git a/packages/bottender-express/src/createMiddleware.ts b/packages/bottender-express/src/createMiddleware.ts index 305550b41..2e25fb455 100644 --- a/packages/bottender-express/src/createMiddleware.ts +++ b/packages/bottender-express/src/createMiddleware.ts @@ -6,11 +6,10 @@ import { Bot } from './types'; function createMiddleware(bot: Bot) { const requestHandler = bot.createRequestHandler(); - const wrapper = (fn: (req: Request, res: Response) => Promise) => ( - req: Request, - res: Response, - next: NextFunction - ) => fn(req, res).catch((err: Error) => next(err)); + const wrapper = + (fn: (req: Request, res: Response) => Promise) => + (req: Request, res: Response, next: NextFunction) => + fn(req, res).catch((err: Error) => next(err)); return wrapper(async (req: Request, res: Response) => { if (isEmpty(req.query) && !req.body) { diff --git a/packages/bottender-express/src/registerRoutes.ts b/packages/bottender-express/src/registerRoutes.ts index ba7042cd6..36895400c 100644 --- a/packages/bottender-express/src/registerRoutes.ts +++ b/packages/bottender-express/src/registerRoutes.ts @@ -10,7 +10,7 @@ function registerRoutes( ) { const path = config.path || '/'; - server.use((req, res, next) => { + server.use(async (req, res, next) => { const { rawBody } = req as express.Request & { rawBody: string }; if (req.path !== path) { next(); @@ -19,7 +19,7 @@ function registerRoutes( const url = `https://${req.get('host')}${req.originalUrl}`; - const { shouldNext, response } = bot.connector.preprocess({ + const { shouldNext, response } = await bot.connector.preprocess({ method: req.method, url, headers: req.headers, diff --git a/packages/bottender-facebook/package.json b/packages/bottender-facebook/package.json new file mode 100644 index 000000000..64931eee0 --- /dev/null +++ b/packages/bottender-facebook/package.json @@ -0,0 +1,28 @@ +{ + "name": "@bottender/facebook", + "version": "1.5.1-alpha.9", + "license": "MIT", + "homepage": "https://bottender.js.org/", + "repository": { + "type": "git", + "url": "https://github.com/Yoctol/bottender.git" + }, + "main": "dist/index.js", + "files": [ + "dist" + ], + "types": "dist/index.d.ts", + "dependencies": { + "axios-error": "^1.0.4", + "facebook-batch": "1.0.6", + "lodash": "^4.17.21", + "type-fest": "^1.4.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "bottender": ">= 1.5.1-alpha.4" + }, + "devDependencies": { + "bottender": "^1.5.1-alpha.9" + } +} diff --git a/packages/bottender-facebook/src/FacebookBatch.ts b/packages/bottender-facebook/src/FacebookBatch.ts new file mode 100644 index 000000000..ff6b46c43 --- /dev/null +++ b/packages/bottender-facebook/src/FacebookBatch.ts @@ -0,0 +1,234 @@ +import querystring from 'querystring'; + +import isNil from 'lodash/isNil'; +import omitBy from 'lodash/omitBy'; +import { MergeExclusive } from 'type-fest'; +import { MessengerTypes } from 'bottender'; + +import * as Types from './FacebookTypes'; + +function formatDate(date: Date | string | undefined) { + if (date instanceof Date) { + return date.toISOString(); + } + return date; +} + +/** + * Publish new comments to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v6.0/object/comments + * + * @param objectId - ID of the object. + * @param comment - A comment text or a comment object. + * @param options - + */ +function sendComment( + objectId: string, + comment: string | Types.InputComment, + options?: { + accessToken?: string; + } +): MessengerTypes.BatchItem { + const body = typeof comment === 'string' ? { message: comment } : comment; + + return { + method: 'POST', + relativeUrl: `${objectId}/comments`, + body: { + ...body, + ...options, + }, + }; +} + +/** + * Add new likes to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v6.0/object/likes + * + * @param objectId - ID of the object. + * @param options - + */ +function sendLike( + objectId: string, + options?: { + accessToken?: string; + } +): MessengerTypes.BatchItem { + return { + method: 'POST', + relativeUrl: `${objectId}/likes`, + body: { + ...options, + }, + }; +} + +/** + * Get the data of the comment. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v6.0/comment + * + * @param commentId - ID of the comment. + * @param options - + */ +function getComment( + commentId: string, + { + summary, + filter, + fields, + accessToken, + }: { + summary?: boolean; + filter?: 'toplevel' | 'stream'; + fields?: string | Types.CommentField[]; + accessToken?: string; + } = {} +): MessengerTypes.BatchItem { + const conjunctFields = Array.isArray(fields) ? fields.join(',') : fields; + + const query = { + ...(summary && { summary: 'true' }), + ...(filter && { filter }), + ...(conjunctFields && { fields: conjunctFields }), + ...(accessToken && { access_token: accessToken }), + }; + + return { + method: 'GET', + relativeUrl: `${commentId}?${querystring.stringify(query)}`, + }; +} + +/** + * Get the data of likes on the object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v6.0/object/likes + * + * @param objectId - ID of the comment. + * @param options - + */ +function getLikes( + objectId: string, + { + summary, + accessToken, + }: { + summary?: boolean; + accessToken?: string; + } = {} +): MessengerTypes.BatchItem { + const query = { + ...(summary && { summary: 'true' }), + ...(accessToken && { access_token: accessToken }), + }; + + return { + method: 'GET', + relativeUrl: `${objectId}/likes?${querystring.stringify(query)}`, + }; +} + +/** + * Get metrics for pages. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/insights + * + * @param options - + */ +function getPageInsights

( + options: { + accessToken?: string; + metric: P extends 'day' + ? Types.PageInsightsMetricDay[] + : P extends 'week' + ? Types.PageInsightsMetricWeek[] + : P extends 'days_28' + ? Types.PageInsightsMetricDays28[] + : never; + period: P; + } & MergeExclusive< + { datePreset?: Types.DatePreset }, + { + since?: string | Date; + until?: string | Date; + } + > +): MessengerTypes.BatchItem { + const query = omitBy( + { + access_token: options.accessToken, + period: options.period, + metric: options.metric.join(','), + date_preset: 'datePreset' in options ? options.datePreset : undefined, + since: 'since' in options ? formatDate(options.since) : undefined, + until: 'until' in options ? formatDate(options.until) : undefined, + }, + isNil + ); + + return { + method: 'GET', + relativeUrl: `/me/insights?${querystring.stringify(query)}`, + }; +} + +/** + * Get metrics for page posts. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/insights + * + * @param postId - ID of the post. + * @param options - + */ +function getPostInsights

( + postId: string, + options: { + accessToken?: string; + metric: P extends 'day' + ? Types.PostInsightsMetricDay[] + : P extends 'lifetime' + ? Types.PostInsightsMetricLifetime[] + : never; + period?: P; + } & MergeExclusive< + { + datePreset?: Types.DatePreset; + }, + { + since?: string | Date; + until?: string | Date; + } + > +): MessengerTypes.BatchItem { + const query = omitBy( + { + access_token: options.accessToken, + period: options.period, + metric: options.metric.join(','), + date_preset: 'datePreset' in options ? options.datePreset : undefined, + since: 'since' in options ? formatDate(options.since) : undefined, + until: 'until' in options ? formatDate(options.until) : undefined, + }, + isNil + ); + + return { + method: 'GET', + relativeUrl: `/${postId}/insights?${querystring.stringify(query)}`, + }; +} + +const FacebookBatch = { + sendComment, + sendLike, + getComment, + getLikes, + + getPageInsights, + getPostInsights, +}; + +export default FacebookBatch; diff --git a/packages/bottender-facebook/src/FacebookClient.ts b/packages/bottender-facebook/src/FacebookClient.ts new file mode 100644 index 000000000..19bf8daba --- /dev/null +++ b/packages/bottender-facebook/src/FacebookClient.ts @@ -0,0 +1,455 @@ +import AxiosError from 'axios-error'; +import get from 'lodash/get'; +import { MergeExclusive } from 'type-fest'; +import { MessengerClient } from 'bottender'; + +import * as Types from './FacebookTypes'; + +function handleError(err: AxiosError): never { + if (err.response && err.response.data) { + const error = get(err, 'response.data.error'); + if (error) { + const msg = `Facebook API - ${error.code} ${error.type} ${error.message}`; + throw new AxiosError(msg, err); + } + } + throw new AxiosError(err.message, err); +} + +export default class FacebookClient extends MessengerClient { + /** + * Publish new comments to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/comments + * + * @param objectId - ID of the object. + * @param comment - A comment text or a comment object. + * @param options - + */ + public sendComment( + objectId: string, + comment: string | Types.InputComment + ): Promise<{ id: string }> { + const body = typeof comment === 'string' ? { message: comment } : comment; + + return this.axios + .post<{ id: string }>(`/${objectId}/comments`, body, { + params: { + access_token: this.accessToken, + }, + }) + .then((res) => res.data, handleError); + } + + /** + * Add new likes to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/likes + * + * @param objectId - ID of the object. + */ + public sendLike(objectId: string): Promise<{ success: true }> { + return this.axios + .post<{ success: true }>(`/${objectId}/likes`, undefined, { + params: { + access_token: this.accessToken, + }, + }) + .then((res) => res.data, handleError); + } + + /** + * Get the data of the comment. + * + * @see https://developers.facebook.com/docs/graph-api/reference/comment + * + * @param commentId - ID of the comment. + * @param options - + */ + public getComment< + T extends Types.CommentField = 'id' | 'message' | 'created_time' + >( + commentId: string, + { + fields = ['id' as T, 'message' as T, 'created_time' as T], + }: { fields?: T[] } = {} + ): Promise< + Pick< + Types.Comment, + Types.CamelCaseUnion + > + > { + const conjunctFields = Array.isArray(fields) ? fields.join(',') : fields; + + return this.axios + .get< + Pick< + Types.Comment, + Types.CamelCaseUnion + > + >(`/${commentId}`, { + params: { + fields: conjunctFields, + access_token: this.accessToken, + }, + }) + .then((res) => res.data, handleError); + } + + /** + * Get comments of the object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/object/comments + * + * @param objectId - ID of the object. + * @param options - + */ + public getComments< + T extends Types.CommentField = 'id' | 'message' | 'created_time', + U extends boolean = false + >( + objectId: string, + { + limit, + summary, + filter, + order, + fields = ['id' as T, 'message' as T, 'created_time' as T], + }: Types.GetCommentsOptions = {} + ): Promise< + Types.PagingData< + Pick< + Types.Comment, + Types.CamelCaseUnion + >[] + > & + (U extends true + ? { + summary: { + order: 'ranked' | 'chronological' | 'reverse_chronological'; + totalCount: number; + canComment: boolean; + }; + } + : any) + > { + const conjunctFields = Array.isArray(fields) ? fields.join(',') : fields; + + return this.axios + .get< + Types.PagingData< + Pick< + Types.Comment, + Types.CamelCaseUnion + >[] + > & + (U extends true + ? { + summary: { + order: 'ranked' | 'chronological' | 'reverse_chronological'; + totalCount: number; + canComment: boolean; + }; + } + : any) + >(`/${objectId}/comments`, { + params: { + limit, + summary: summary ? 'true' : undefined, + filter, + order, + fields: conjunctFields, + access_token: this.accessToken, + }, + }) + .then((res) => res.data, handleError); + } + + /** + * Get the data of likes on the object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/likes + * + * @param objectId - ID of the comment. + * @param options - + */ + public getLikes( + objectId: string, + { summary }: Types.GetLikesOptions = {} + ): Promise { + return this.axios + .get<{ + id: string; + likes: Types.Likes; + }>(`/${objectId}/likes`, { + params: { + summary: summary ? 'true' : undefined, + access_token: this.accessToken, + }, + }) + .then((res) => res.data.likes, handleError); + } + + /** + * Get metrics for pages. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/insights + * + * @param options - + */ + public getPageInsights< + D extends ( + | ({ + id: string; + title: string; + description: string; + period: P; + } & { + name: Exclude< + P extends 'day' + ? Types.PageInsightsMetricDay + : P extends 'week' + ? Types.PageInsightsMetricWeek + : P extends 'days_28' + ? Types.PageInsightsMetricDays28 + : never, + | 'page_tab_views_login_top_unique' + | 'page_tab_views_login_top' + | 'page_tab_views_logout_top' + | 'page_negative_feedback_by_type' + | 'page_positive_feedback_by_type' + | 'page_fans_by_like_source' + | 'page_fans_by_like_source_unique' + | 'page_fans_by_unlike_source_unique' + | 'page_fans_by_unlike_source' + | 'page_content_activity_by_action_type_unique' + | 'page_content_activity_by_action_type' + >; + values: { + value: number; + endTime: string; + }[]; + }) + | { + name: + | 'page_tab_views_login_top_unique' + | 'page_tab_views_login_top' + | 'page_tab_views_logout_top'; + values: { + value: { + allactivity?: number; + app?: number; + info?: number; + insights?: number; + likes?: number; + locations?: number; + photos?: number; + photosAlbums?: number; + photosStream?: number; + profile?: number; + profileInfo?: number; + profileLikes?: number; + profilePhotos?: number; + timeline?: number; + events?: number; + videos?: number; + wall?: number; + tabHome?: number; + reviews?: number; + about?: number; + profileHome?: number; + profileVideos?: number; + profilePosts?: number; + community?: number; + posts?: number; + home?: number; + }; + endTime: string; + }[]; + } + | { + name: 'page_negative_feedback_by_type'; + values: { + value: { + hideClicks?: number; + hideAllClicks?: number; + reportSpamClicks?: number; + unlikePageClicks?: number; + }; + endTime: string; + }[]; + } + | { + name: 'page_positive_feedback_by_type'; + values: { + value: { + answer?: number; + claim?: number; + comment?: number; + like?: number; + link?: number; + other?: number; + rsvp?: number; + }; + endTime: string; + }[]; + } + | { + name: 'page_fans_by_like_source' | 'page_fans_by_like_source_unique'; + values: { + value: { + ads?: number; + newsFeed?: number; + pageSuggestions?: number; + restoredLikesFromReactivatedAccounts?: number; + search?: number; + yourPage?: number; + other?: number; + }; + endTime: string; + }[]; + } + | { + name: + | 'page_fans_by_unlike_source_unique' + | 'page_fans_by_unlike_source'; + values: { + value: { + deactivatedOrMemorializedAccountRemovals?: number; + other?: number; + suspiciousAccountRemovals?: number; + unlikesFromPagePostsOrNewsFeed?: number; + unlikesFromSearch?: number; + }; + endTime: string; + }[]; + } + | { + name: + | 'page_content_activity_by_action_type_unique' + | 'page_content_activity_by_action_type'; + values: { + value: { + checkin?: number; + coupon?: number; + event?: number; + fan?: number; + mention?: number; + pagePost?: number; + question?: number; + userPost?: number; + other?: number; + }; + endTime: string; + }[]; + } + )[], + P extends 'day' | 'week' | 'days_28' + >( + options: { + metric: P extends 'day' + ? Types.PageInsightsMetricDay[] + : P extends 'week' + ? Types.PageInsightsMetricWeek[] + : P extends 'days_28' + ? Types.PageInsightsMetricDays28[] + : never; + period: P; + } & MergeExclusive< + { datePreset?: Types.DatePreset }, + { + since?: string | Date; + until?: string | Date; + } + > + ): Promise { + return this.axios + .get<{ + data: D; + paging: { + previous: string; + next: string; + }; + }>('/me/insights', { + params: { + access_token: this.accessToken, + period: options.period, + metric: options.metric.join(','), + date_preset: 'datePreset' in options ? options.datePreset : undefined, + since: 'since' in options ? options.since : undefined, + until: 'until' in options ? options.until : undefined, + }, + }) + .then((res) => res.data.data, handleError); + } + + /** + * Get metrics for page posts. + * + * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/insights + * + * @param postId - ID of the post. + * @param options - + */ + public getPostInsights< + D extends { + id: string; + title: string; + description: string; + name: P extends 'day' + ? Types.PostInsightsMetricDay + : P extends 'lifetime' + ? Types.PostInsightsMetricLifetime + : never; + period: P; + values: P extends 'day' + ? { + value: number; + endTime: string; + }[] + : P extends 'lifetime' + ? { + value: number; + } + : never; + }[], + P extends 'day' | 'lifetime' + >( + postId: string, + options: { + metric: P extends 'day' + ? Types.PostInsightsMetricDay[] + : P extends 'lifetime' + ? Types.PostInsightsMetricLifetime[] + : never; + period?: P; + } & MergeExclusive< + { + datePreset?: Types.DatePreset; + }, + { + since?: string | Date; + until?: string | Date; + } + > + ): Promise { + return this.axios + .get<{ + data: D; + paging: { + previous: string; + next: string; + }; + }>(`/${postId}/insights`, { + params: { + access_token: this.accessToken, + period: options.period, + metric: options.metric.join(','), + date_preset: 'datePreset' in options ? options.datePreset : undefined, + since: 'since' in options ? options.since : undefined, + until: 'until' in options ? options.until : undefined, + }, + }) + .then((res) => res.data.data, handleError); + } +} diff --git a/packages/bottender-facebook/src/FacebookConnector.ts b/packages/bottender-facebook/src/FacebookConnector.ts new file mode 100644 index 000000000..fa4a04886 --- /dev/null +++ b/packages/bottender-facebook/src/FacebookConnector.ts @@ -0,0 +1,200 @@ +import { EventEmitter } from 'events'; + +import warning from 'warning'; +import { BatchConfig } from 'facebook-batch'; +import { + Connector, + FacebookBaseConnector, + MessengerConnector, + MessengerContext, + MessengerEvent, + MessengerTypes, + RequestContext, +} from 'bottender'; + +import FacebookClient from './FacebookClient'; +import FacebookContext from './FacebookContext'; +import FacebookEvent from './FacebookEvent'; +import { ChangesEntry, FacebookWebhookRequestBody } from './FacebookTypes'; + +// TODO: use exported type +type Session = Record; + +export type FacebookConnectorOptions = { + appId: string; + appSecret: string; + accessToken?: string; + client?: FacebookClient; + mapPageToAccessToken?: (pageId: string) => Promise; + verifyToken?: string; + batchConfig?: BatchConfig; + origin?: string; + skipAppSecretProof?: boolean; + skipProfile?: boolean; +}; + +export default class FacebookConnector + extends FacebookBaseConnector + implements Connector +{ + _mapPageToAccessToken: ((pageId: string) => Promise) | null = null; + + _messengerConnector: MessengerConnector; + + public constructor(options: FacebookConnectorOptions) { + super({ + ...options, + ClientClass: FacebookClient, + }); + + const { mapPageToAccessToken } = options; + + this._mapPageToAccessToken = mapPageToAccessToken ?? null; + this._messengerConnector = new MessengerConnector({ + ...options, + ClientClass: FacebookClient, + mapPageToAccessToken, + }); + } + + /** + * The name of the platform. + * + */ + get platform(): 'facebook' { + return 'facebook'; + } + + getUniqueSessionKey(event: FacebookEvent | MessengerEvent): string | null { + if (event instanceof MessengerEvent) { + return this._messengerConnector.getUniqueSessionKey(event); + } + + // TODO: How to determine session key in facebook feed events + return null; + } + + public async updateSession( + session: Session, + event: FacebookEvent | MessengerEvent + ): Promise { + if (!session.user) { + session.page = { + id: event.pageId, + _updatedAt: new Date().toISOString(), + }; + + session.user = { + _updatedAt: new Date().toISOString(), + id: this.getUniqueSessionKey(event), + }; + } + + Object.freeze(session.user); + Object.defineProperty(session, 'user', { + configurable: false, + enumerable: true, + writable: false, + value: session.user, + }); + + Object.freeze(session.page); + Object.defineProperty(session, 'page', { + configurable: false, + enumerable: true, + writable: false, + value: session.page, + }); + } + + public mapRequestToEvents( + body: FacebookWebhookRequestBody + ): (FacebookEvent | MessengerEvent)[] { + // TODO: returns InstagramEvent (object === 'instagram') + if (body.object !== 'page') { + return []; + } + + const bodyEntry: (MessengerTypes.MessagingEntry | ChangesEntry)[] = + body.entry; + + return bodyEntry + .map((entry) => { + const pageId = entry.id; + const timestamp = entry.time; + if ('messaging' in entry) { + return new MessengerEvent(entry.messaging[0], { + pageId, + isStandby: false, + }); + } + + if ('standby' in entry) { + return new MessengerEvent(entry.standby[0], { + pageId, + isStandby: true, + }); + } + + if ('changes' in entry) { + return new FacebookEvent(entry.changes[0], { pageId, timestamp }); + } + + return null; + }) + .filter( + (event): event is FacebookEvent | MessengerEvent => event !== null + ); + } + + public async createContext(params: { + event: FacebookEvent | MessengerEvent; + session?: Session; + initialState?: Record; + requestContext?: RequestContext; + emitter?: EventEmitter; + }): Promise { + let customAccessToken; + + if (this._mapPageToAccessToken) { + const { pageId } = params.event; + + if (!pageId) { + warning(false, 'Could not find pageId from request body.'); + } else { + customAccessToken = await this._mapPageToAccessToken(pageId); + } + } + + let client; + if (customAccessToken) { + client = new FacebookClient({ + accessToken: customAccessToken, + appSecret: this._appSecret, + origin: this._origin, + skipAppSecretProof: this._skipAppSecretProof, + }); + } else { + client = this._client; + } + + if (params.event instanceof FacebookEvent) { + return new FacebookContext({ + ...params, + event: params.event, + client, + customAccessToken, + batchQueue: this._batchQueue, + appId: this._appId, + }); + } + return new MessengerContext({ + ...params, + event: params.event, + client, + customAccessToken, + batchQueue: this._batchQueue, + appId: this._appId, + }); + } +} diff --git a/packages/bottender-facebook/src/FacebookContext.ts b/packages/bottender-facebook/src/FacebookContext.ts new file mode 100644 index 000000000..aaebbaedd --- /dev/null +++ b/packages/bottender-facebook/src/FacebookContext.ts @@ -0,0 +1,298 @@ +import { EventEmitter } from 'events'; + +import warning from 'warning'; +import { + Context, + MessengerBatch, + MessengerTypes, + RequestContext, +} from 'bottender'; +import { FacebookBatchQueue } from 'facebook-batch'; + +import FacebookBatch from './FacebookBatch'; +import FacebookClient from './FacebookClient'; +import FacebookEvent from './FacebookEvent'; +import * as Types from './FacebookTypes'; + +// TODO: use exported type +type Session = Record; + +export type FacebookContextOptions = { + appId?: string; + client: FacebookClient; + event: FacebookEvent; + session?: Session; + initialState?: Record; + requestContext?: RequestContext; + customAccessToken?: string; + batchQueue?: FacebookBatchQueue | null; + emitter?: EventEmitter; +}; + +export default class FacebookContext extends Context< + FacebookClient, + FacebookEvent +> { + _appId: string | undefined; + + _customAccessToken: string | undefined; + + _batchQueue: FacebookBatchQueue | undefined; + + public constructor({ + appId, + client, + event, + session, + initialState, + requestContext, + customAccessToken, + batchQueue, + emitter, + }: FacebookContextOptions) { + super({ client, event, session, initialState, requestContext, emitter }); + this._customAccessToken = customAccessToken; + this._batchQueue = batchQueue || undefined; + this._appId = appId; + } + + /** + * The name of the platform. + * + */ + get platform(): 'facebook' { + return 'facebook'; + } + + /** + * Send a text private reply to the post or comment. + * + * @see https://developers.facebook.com/docs/messenger-platform/discovery/private-replies/ + * + * @param text - The text to be sent in the reply. + */ + public async sendText( + text: string + ): Promise { + if (!['comment', 'post'].includes(this._event.rawEvent.value.item)) { + warning(false, 'sendText: can only work with comment and post events.'); + return; + } + + const value = this._event.rawEvent.value as + | Types.FeedPost + | Types.FeedComment; + + if (value.verb === 'remove') { + warning(false, "sendText: can't work with remove verb"); + return; + } + + let recipient; + if (value.item === 'comment') { + recipient = { + commentId: value.commentId, + }; + } else { + recipient = { + postId: value.postId, + }; + } + + if (this._batchQueue) { + return this._batchQueue.push( + // FIXME: this type should be fixed in MessengerBatch + MessengerBatch.sendText(recipient, text, { + accessToken: this._customAccessToken, + } as any) + ); + } + return this._client.sendText(recipient, text); + } + + /** + * Send a private reply to the post or comment. + * + * @see https://developers.facebook.com/docs/messenger-platform/discovery/private-replies/ + * + * @param message - The Messenger message object to be sent in the reply. + */ + public async sendMessage( + message: MessengerTypes.Message + ): Promise { + if (!['comment', 'post'].includes(this._event.rawEvent.value.item)) { + warning( + false, + 'sendMessage: can only work with comment and post events.' + ); + return; + } + + const value = this._event.rawEvent.value as + | Types.FeedPost + | Types.FeedComment; + + if (value.verb === 'remove') { + warning(false, "sendMessage: can't work with remove verb"); + } + + let recipient; + if (value.item === 'comment') { + recipient = { + commentId: value.commentId, + }; + } else { + recipient = { + postId: value.postId, + }; + } + + if (this._batchQueue) { + return this._batchQueue.push( + // FIXME: this type should be fixed in MessengerBatch + MessengerBatch.sendMessage(recipient, message, { + accessToken: this._customAccessToken, + } as any) + ); + } + return this._client.sendMessage(recipient, message); + } + + // TODO: implement other send methods + + /** + * Publish new comments to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/comments + * + * @param comment - A comment text or a comment object. + */ + public async sendComment( + comment: string | Types.InputComment + ): Promise<{ id: string } | undefined> { + let objectId; + if (this._event.isComment) { + objectId = this._event.isFirstLayerComment + ? (this._event.rawEvent.value as Types.FeedComment).commentId + : (this._event.rawEvent.value as Types.FeedComment).parentId; + } else if (this._event.isPost) { + objectId = (this._event.rawEvent.value as Types.FeedComment).postId; + } + + // TODO: support more type: Album, Event, Life Event, Link, Live Video, Note, Photo, Thread, User, Video + if (!objectId) { + warning(false, 'sendComment: only support comment and post events now.'); + return; + } + + if (this._event.isSentByPage) { + warning(false, "sendComment: can't send to page itself."); + return; + } + + if (this._batchQueue) { + return this._batchQueue.push<{ id: string }>( + FacebookBatch.sendComment(objectId, comment, { + accessToken: this._customAccessToken, + }) + ); + } + return this._client.sendComment(objectId, comment); + } + + /** + * Add new likes to any object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/likes + */ + public async sendLike(): Promise<{ success: boolean }> { + let objectId; + if (this._event.isComment) { + objectId = (this._event.rawEvent.value as Types.FeedComment).commentId; + } else if (this._event.isPost) { + objectId = (this._event.rawEvent.value as Types.FeedComment).postId; + } + + // TODO: support more type: Album, Event, Life Event, Link, Live Video, Note, Photo, Thread, User, Video + if (!objectId) { + warning(false, 'sendLike: only support comment and post events now.'); + return { success: false }; + } + + if (this._batchQueue) { + return this._batchQueue.push<{ success: boolean }>( + FacebookBatch.sendLike(objectId, { + accessToken: this._customAccessToken, + }) + ); + } + return this._client.sendLike(objectId); + } + + /** + * Get the data of the comment. + * + * @see https://developers.facebook.com/docs/graph-api/reference/comment + */ + public async getComment< + T extends Types.CommentField = 'id' | 'message' | 'created_time' + >({ + fields = ['id' as T, 'message' as T, 'created_time' as T], + }: Types.GetCommentOptions = {}): Promise + > | null> { + const commentId = (this._event.rawEvent.value as Types.FeedComment) + .commentId; + + if (!commentId) { + warning(false, 'Could not getComment if there is no comment.'); + return null; + } + + if (this._batchQueue) { + return this._batchQueue.push< + Pick< + Types.Comment, + Types.CamelCaseUnion + > + >( + FacebookBatch.getComment(commentId, { + accessToken: this._customAccessToken, + fields, + }) + ); + } + return this._client.getComment(commentId, { fields }); + } + + /** + * Get the data of likes on the object. + * + * @see https://developers.facebook.com/docs/graph-api/reference/object/likes + * + * @param options - + */ + public getLikes(options: Types.GetLikesOptions): Promise { + const objectId = (this._event.rawEvent.value as Types.FeedComment) + .commentId; // FIXME: postId + + if (this._batchQueue) { + return this._batchQueue.push( + FacebookBatch.getLikes(objectId, { + accessToken: this._customAccessToken, + ...options, + }) + ); + } + return this._client.getLikes(objectId, options); + } + + public async canReplyPrivately(): Promise { + const comment = await this.getComment({ fields: ['can_reply_privately'] }); + + if (!comment) return false; + + return Boolean(comment.canReplyPrivately); + } +} diff --git a/packages/bottender-facebook/src/FacebookEvent.ts b/packages/bottender-facebook/src/FacebookEvent.ts new file mode 100644 index 000000000..1cb30dced --- /dev/null +++ b/packages/bottender-facebook/src/FacebookEvent.ts @@ -0,0 +1,270 @@ +import { + FacebookRawEvent, + FeedComment, + FeedPost, + FeedReaction, + FeedStatus, +} from './FacebookTypes'; + +export default class FacebookEvent { + _rawEvent: FacebookRawEvent; + + _pageId: string | undefined; + + _timestamp: number | undefined; + + constructor( + rawEvent: FacebookRawEvent, + options: { pageId?: string; timestamp?: number } = {} + ) { + this._rawEvent = rawEvent; + this._pageId = options.pageId; + this._timestamp = options.timestamp; + } + + get pageId(): string | undefined { + return this._pageId; + } + + /** + * Underlying raw event from Facebook. + * + */ + get rawEvent(): FacebookRawEvent { + return this._rawEvent; + } + + /** + * The timestamp when the event was sent. + * + */ + get timestamp(): number | undefined { + return 'createdTime' in this.rawEvent.value + ? this.rawEvent.value.createdTime + : this._timestamp; + } + + /** + * Determine if the event is a message event. + * + */ + get isMessage(): boolean { + return false; + } + + /** + * Determine if the event is a message event which includes text. + * + */ + get isText(): boolean { + return false; + } + + /** + * The text string from Facebook raw event. + * + */ + get text(): string | null { + return null; + } + + get isFeed(): boolean { + return 'field' in this.rawEvent && this.rawEvent.field === 'feed'; + } + + get isStatus(): boolean { + return Boolean( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'status' + ); + } + + get isStatusAdd(): boolean { + return Boolean( + this.isStatus && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'add' + ); + } + + get isStatusEdited(): boolean { + return Boolean( + this.isStatus && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'edited' + ); + } + + get status(): FeedStatus | null { + if ( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'status' + ) { + return this.rawEvent.value; + } + return null; + } + + get isPost(): boolean { + return Boolean( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'post' + ); + } + + get isPostRemove(): boolean { + return Boolean( + this.isPost && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'remove' + ); + } + + get post(): FeedPost | null { + if ( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'post' + ) { + return this.rawEvent.value; + } + return null; + } + + get isComment(): boolean { + return Boolean( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'comment' + ); + } + + get isCommentAdd(): boolean { + return Boolean( + this.isComment && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'add' + ); + } + + get isCommentEdited(): boolean { + return Boolean( + this.isComment && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'edited' + ); + } + + get isCommentRemove(): boolean { + return Boolean( + this.isComment && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'remove' + ); + } + + get isFirstLayerComment(): boolean { + if (!this.isComment) return false; + + const comment = this.comment as FeedComment; + + return comment.parentId === comment.postId; + } + + get comment(): FeedComment | null { + if ( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'comment' + ) { + return this.rawEvent.value; + } + return null; + } + + get isReaction(): boolean { + return Boolean( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'reaction' + ); + } + + get isReactionAdd(): boolean { + return Boolean( + this.isReaction && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'add' + ); + } + + get isReactionEdit(): boolean { + return Boolean( + this.isReaction && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'edit' + ); + } + + get isReactionRemove(): boolean { + return Boolean( + this.isReaction && + 'value' in this.rawEvent && + this.rawEvent.value.verb === 'remove' + ); + } + + get isPostReaction(): boolean { + return Boolean( + this.isReaction && 'value' in this.rawEvent && !this.reaction?.commentId + ); + } + + get reaction(): FeedReaction | null { + if ( + this.isFeed && + 'value' in this.rawEvent && + this.rawEvent.value.item === 'reaction' + ) { + return this.rawEvent.value; + } + return null; + } + + get isSentByPage(): boolean { + if (!this.isFeed) { + return false; + } + + if ( + 'value' in this.rawEvent && + 'from' in this.rawEvent.value && + this.rawEvent.value.from.id === this.pageId + ) { + return true; + } + + return false; + } + + // Notifications for Page likes will only be sent for Pages that have fewer than 10K likes. + // ref: https://developers.facebook.com/docs/graph-api/webhooks/reference/page/#feed + get isPageLike(): boolean { + if (!this.isFeed) { + return false; + } + + if ( + 'value' in this.rawEvent && + this.rawEvent.value.item === 'like' && + !('from' in this.rawEvent.value) + ) { + return true; + } + + return false; + } +} diff --git a/packages/bottender-facebook/src/FacebookTypes.ts b/packages/bottender-facebook/src/FacebookTypes.ts new file mode 100644 index 000000000..214433304 --- /dev/null +++ b/packages/bottender-facebook/src/FacebookTypes.ts @@ -0,0 +1,1490 @@ +import { MessengerTypes } from 'bottender'; + +export { FacebookConnectorOptions } from './FacebookConnector'; +export { FacebookContextOptions } from './FacebookContext'; + +export type CamelCaseUnion = U extends string + ? KM[U] + : never; + +export type FeedStatus = { + from: { + id: string; + name: string; + }; + item: 'status'; + postId: string; + verb: 'add' | 'edited'; + published: number; + createdTime: number; + message: string; +}; + +export type FeedPost = { + recipientId: string; + from: { + id: string; + }; + item: 'post'; + postId: string; + verb: 'add' | 'edited' | 'remove'; + createdTime: number; +}; + +export type FeedComment = { + from: { + /** + * The ID of the sender + */ + id: string; + /** + * The name of the sender + */ + name?: string; + }; + item: 'comment'; + /** + * The comment ID + */ + commentId: string; + /** + * The post ID + */ + postId: string; + /** + * The parent ID + */ + parentId: string; + /** + * The timestamp of when the object was created + */ + createdTime: number; + /** + * Provide additional content about a post + */ + post?: { + /** + * The ID of the post + */ + id: string; + /** + * The type of the post + */ + type: 'photo' | 'video' | string; + /** + * Description of the type of a status update. + */ + statusType: 'added_photos' | 'added_videos' | string; + /** + * The time the post was last updated, which occurs when a user comments on the post. + */ + updatedTime: string; + /** + * Status of the promotion, if the post was promoted + */ + promotionStatus: 'inactive' | 'extendable' | string; + /** + * The permanent static URL to the post on www.facebook.com. + */ + permalinkUrl: string; + /** + * Indicates whether a scheduled post was published (applies to scheduled Page Post only, for users post and instanlty published posts this value is always true) + */ + isPublished: boolean; + }; +} & ( + | { + verb: 'add' | 'edited'; + /** + * The message that is part of the content + */ + message: string; + } + | { verb: 'remove' } +); + +export type FeedReaction = { + reactionType: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry' | 'care'; + from: { + id: string; + }; + parentId: string; + commentId?: string; + postId: string; + verb: 'add' | 'edit' | 'remove'; + item: 'reaction'; + createdTime: number; +}; + +export type FeedEvent = { + item: 'event'; + message?: string; + postId: string; + story?: string; + eventId: string; + verb: 'add'; + createdTime: number; +}; + +export type FeedPhoto = { + item: 'photo'; + from: { + id: string; + name?: string; + }; + link?: string; + message?: string; + postId: string; + createdTime: number; + photoId: string; + published: 0 | 1; + verb: 'add' | 'edited'; +}; + +export type FeedVideo = + | { + item: 'video'; + from: { + id: string; + name?: string; + }; + link?: string; + message?: string; + postId: string; + createdTime: number; + published: 0 | 1; + verb: 'add' | 'edited'; + videoId: string; + } + | { + item: 'video'; + from: { + id: string; + name?: string; + }; + postId: string; + createdTime: number; + verb: 'remove'; + recipientId: string; + } + | { + item: 'video'; + link: string; + message?: string; + videoFlagReason: 1; + verb: 'mute'; + videoId: string; + } + | { + item: 'video'; + message?: string; + videoFlagReason: 1; + verb: 'block'; + videoId: string; + } + | { + item: 'video'; + link: string; + message?: string; + videoFlagReason: 1; + verb: 'unblock'; + videoId: string; + }; + +export type FeedShare = { + item: 'share'; + from: { + id: string; + name?: string; + }; + link?: string; + message?: string; + postId: string; + createdTime: number; + published: 0 | 1; + verb: 'add' | 'edited'; + shareId: string; +}; + +export type FeedPageLike = { + item: 'like'; + verb: 'add'; +}; + +/** + * item: album, address, comment, connection, coupon, event, experience, group, group_message, interest, like, link, mention, milestone, note, page, picture, platform-story, photo, photo-album, post, profile, question, rating, reaction, relationship-status, share, status, story, timeline cover, tag, video + */ +export type FacebookRawEvent = { + field: 'feed'; + value: + | FeedStatus + | FeedPost + | FeedComment + | FeedReaction + | FeedEvent + | FeedPhoto + | FeedVideo + | FeedShare + | FeedPageLike; +}; + +export type ChangesEntry = { + id: string; + time: number; + changes: FacebookRawEvent[]; +}; + +type FacebookRequestBody = { + object: 'page'; + entry: ChangesEntry[]; +}; + +export type FacebookWebhookRequestBody = + | MessengerTypes.MessengerRequestBody + | FacebookRequestBody; + +/** + * https://developers.facebook.com/docs/graph-api/reference/v6.0/comment + */ +export type CommentField = + | 'id' + | 'attachment' + | 'can_comment' + | 'can_remove' + | 'can_hide' + | 'can_like' + | 'can_reply_privately' + | 'comment_count' + | 'created_time' + | 'from' + | 'like_count' + | 'message' + | 'message_tags' + | 'object' + | 'parent' + | 'private_reply_conversation' + | 'user_likes'; + +export type CommentKeyMap = { + id: 'id'; + attachment: 'attachment'; + can_comment: 'canComment'; + can_remove: 'canRemove'; + can_hide: 'canHide'; + can_like: 'canLike'; + can_reply_privately: 'canReplyPrivately'; + comment_count: 'commentCount'; + created_time: 'createdTime'; + from: 'from'; + like_count: 'likeCount'; + message: 'message'; + message_tags: 'messageTags'; + object: 'object'; + parent: 'parent'; + private_reply_conversation: 'privateReplyConversation'; + user_likes: 'userLikes'; +}; + +export type InputComment = + | { + /** + * An optional ID of a unpublished photo (see no_story field in `/{user-id}/photos`) uploaded to Facebook to include as a photo comment. + */ + attachmentId?: string; + } + | { + /** + * The URL of a GIF to include as a animated GIF comment. + */ + attachmentShareUrl?: string; + } + | { + /** + * The URL of an image to include as a photo comment. + */ + attachmentUrl?: string; + } + | { + /** + * The comment text. + */ + message?: string; + }; + +export type Comment = { + /** + * The comment ID + */ + id: string; + /** + * Link, video, sticker, or photo attached to the comment + */ + attachment: StoryAttachment; + /** + * Whether the viewer can reply to this comment + */ + canComment: boolean; + /** + * Whether the viewer can remove this comment + */ + canRemove: boolean; + /** + * Whether the viewer can hide this comment. Only visible to a page admin + */ + canHide: boolean; + /** + * Whether the viewer can like this comment + */ + canLike: boolean; + /** + * Whether the viewer can send a private reply to this comment (Page viewers only) + */ + canReplyPrivately: boolean; + /** + * Number of replies to this comment + */ + commentCount: number; + /** + * The time this comment was made + */ + createdTime: string; + /** + * The person that made this comment + */ + from: User; + /** + * Number of times this comment was liked + */ + likeCount: number; + /** + * The comment text + */ + message: string; + /** + * An array of Profiles tagged in message. + */ + messageTags: MessageTag[]; + /** + * For comments on a photo or video, this is that object. Otherwise, this is empty. + */ + object: any; + /** + * For comment replies, this the comment that this is a reply to. + */ + parent: Comment; + /** + * For comments with private replies, gets conversation between the Page and author of the comment (Page viewers only) + */ + privateReplyConversation: Conversation; + /** + * Whether the viewer has liked this comment. + */ + userLikes: boolean; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/v6.0/conversation + */ +export type Conversation = { + /** + * The ID of a conversation, in a format similar to t_000000000000000 + */ + id: string; + /** + * The url to the thread + */ + link: string; + /** + * The snippet of the most recent message in a conversation + */ + snippet: string; + /** + * Last update time + */ + updatedTime: string; + /** + * An estimate of the number of messages + */ + messageCount: number; + /** + * An estimate of the number of unread messages + */ + unreadCount: number; + /** + * People and Pages who are on this conversation + */ + participants: { + name: string; + email: string; + id: string; + }[]; + /** + * People who have sent a message + */ + senders: { + name: string; + email: string; + id: string; + }[]; + /** + * Whether the Page is able to reply + */ + canReply: boolean; + /** + * Whether the Page is subscribed to the conversation + */ + isSubscribed: boolean; +}; + +export type MessageTag = { + /** + * ID of the profile that was tagged. + */ + id: string; + /** + * The text used in the tag. + */ + name: string; + /** + * Indicates which type of profile is tagged. + */ + type: 'user' | 'page' | 'group'; + /** + * Where the first character of the tagged text is in the message, measured in unicode code points. + */ + offset: number; + /** + * How many unicode code points this tag consists of, after the offset. + */ + length: number; +}; + +export type StoryAttachment = { + /** + * Text accompanying the attachment + */ + description: string; + /** + * Profiles tagged in the text accompanying the attachment + */ + descriptionTags: EntityAtTextRange[]; + /** + * Media object (photo, link etc.) contained in the attachment + */ + media: StoryAttachmentMedia; + /** + * Type of the media such as (photo, video, link etc) + */ + mediaType?: string; + /** + * Object that the attachment links to + */ + target: StoryAttachmentTarget; + /** + * Title of the attachment + */ + title: string; + /** + * Type of the attachment. + */ + type: + | 'album' + | 'animated_image_autoplay' + | 'checkin' + | 'cover_photo' + | 'event' + | 'link' + | 'multiple' + | 'music' + | 'note' + | 'offer' + | 'photo' + | 'profile_media' + | 'status' + | 'video' + | 'video_autoplay'; + /** + * Unshimmed URL of the attachment + */ + unshimmedUrl?: string; + /** + * URL of the attachment + */ + url: string; +}; + +/** + * Location and identity of an object in some source text + * + * https://developers.facebook.com/docs/graph-api/reference/entity-at-text-range/ + */ +type EntityAtTextRange = { + /** + * ID of the profile + */ + id: string; + /** + * Number of characters in the text indicating the object + */ + length: number; + /** + * Name of the object + */ + name: string; + /** + * The object itself + */ + object: any; + /** + * The character offset in the source text of the text indicating the object + */ + offset: number; +} & ( + | { + /** + * Type of the object + */ + type: 'user'; + /** + * The object itself + */ + object: User; + } + | { + type: 'page'; + object: Page; + } + | { + type: 'event'; + object: Event; + } + | { + type: 'group'; + object: Group; + } + | { + type: 'application'; + object: Application; + } +); + +/** + * https://developers.facebook.com/docs/graph-api/reference/user + */ +type User = { + /** + * The ID of this person's user account. This ID is unique to each app and cannot be used across different apps. + */ + id: string; + /** + * The User's address. + */ + address?: Location; + /** + * Notes added by viewing page on this User. + */ + adminNotes?: PageAdminNote[]; + + /** + * The age segment for this person expressed as a minimum and maximum age. For example, more than 18, less than 21. + */ + ageRange: AgeRange; + /** + * The authentication method a Workplace User has configured for their account. + */ + authMethod?: 'password' | 'sso'; + /** + * The person's birthday. This is a fixed format string, like MM/DD/YYYY. + */ + birthday: string; + /** + * Can the person review brand polls + */ + canReviewMeasurementRequest: boolean; + /** + * The User's primary email address listed on their profile. This field will not be returned if no valid email address is available. + */ + email?: string; + /** + * Athletes the User likes. + */ + favoriteAthletes?: Experience[]; + /** + * Sports teams the User likes. + */ + favoriteTeams?: Experience[]; + /** + * The person's first name + */ + firstName: string; + /** + * The gender selected by this person, male or female. If the gender is set to a custom value, this value will be based off of the preferred pronoun; it will be omitted if the preferred pronoun is neutral + */ + gender: string; + /** + * The person's hometown + */ + hometown: Page; + /** + * The person's inspirational people + */ + inspirationalPeople: Experience[]; + /** + * Install type + */ + installType?: string; + /** + * Is the app making the request installed + */ + installed?: boolean; + /** + * if the current user is a guest user. should always return false. + */ + isGuestUser?: false; + /** + * Is this a shared login (e.g. a gray user) + */ + isSharedLogin?: boolean; + /** + * Facebook Pages representing the languages this person knows + */ + languages?: Experience[]; + /** + * The person's last name + */ + lastName: string; + /** + * A link to the person's Timeline. The link will only resolve if the person clicking the link is logged into Facebook and is a friend of the person whose profile is being viewed. + */ + link: string; + /** + * The person's current location as entered by them on their profile. This field requires the user_location permission. + */ + location: Page; + /** + * What the person is interested in meeting for + */ + meetingFor?: string[]; + /** + * The person's middle name + */ + middleName?: string; + /** + * The person's full name + */ + name: string; + /** + * The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering + */ + nameFormat?: string; + /** + * The person's payment pricepoints + */ + paymentPricepoints?: PaymentPricepoints; + /** + * The profile picture URL of the Messenger user. The URL will expire. + */ + profilePic?: string; + /** + * The person's PGP public key + */ + publicKey?: string; + /** + * The person's favorite quotes + */ + quotes?: string; + /** + * Security settings + */ + securitySettings: SecuritySettings; + /** + * The time that the shared login needs to be upgraded to Business Manager by + */ + sharedLoginUpgradeRequiredBy: string; + /** + * Shortened, locale-aware name for the person + */ + shortName?: string; + /** + * The person's significant other + */ + significantOther?: User; + /** + * Sports played by the person + */ + sports?: Experience[]; + /** + * Whether the user can add a Donate Button to their Live Videos + */ + supportsDonateButtonInLiveVideo?: boolean; + /** + * Platform test group + */ + testGroup?: number; + /** + * A token that is the same across a business's apps. Access to this token requires that the person be logged into your app or have a role on your app. This token will change if the business owning the app changes + */ + tokenForBusiness?: string; + /** + * Video upload limits + */ + videoUploadLimits: VideoUploadLimits; + /** + * Can the viewer send a gift to this person? + */ + viewerCanSendGift?: boolean; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/location/ + */ +type Location = { + city: string; + cityId?: number; + country: string; + countryCode?: string; + latitude: number; + locatedIn: string; + longitude: number; + name: string; + region?: string; + regionId?: number; + state: string; + street: string; + zip: string; +}; + +type PageAdminNote = any; + +/** + * https://developers.facebook.com/docs/graph-api/reference/age-range/ + */ +type AgeRange = { + /** + * The upper bounds of the range for this person's age. `enum{17, 20, or empty}`. + */ + max: number; + /** + * The lower bounds of the range for this person's age. `enum{13, 18, 21}` + */ + min: number; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/experience/ + */ +type Experience = { + id: string; + description: string; + from: User; + name: string; + with: User[]; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/payment-pricepoints/ + */ +type PaymentPricepoints = { + mobile: PaymentPricepoint[]; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/payment-pricepoint/ + */ +type PaymentPricepoint = { + credits: number; + localCurrency: string; + userPrice: string; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/security-settings/ + */ +type SecuritySettings = { + secureBrowsing: SecureBrowsing; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/secure-browsing/ + */ +type SecureBrowsing = { + enabled: boolean; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/video-upload-limits/ + */ +type VideoUploadLimits = { + length: number; + size: number; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/page + */ +type Page = Record; + +/** + * https://developers.facebook.com/docs/graph-api/reference/event + */ +type Event = Record; + +/** + * https://developers.facebook.com/docs/graph-api/reference/v6.0/group + */ +type Group = { + /** + * The Group ID + */ + id: string; + /** + * Information about the Group's cover photo. + */ + cover: CoverPhoto; + /** + * A brief description of the Group. + */ + description: string; + /** + * The email address to upload content to the Group. Only current members of the Group can use this. + */ + email: string; + /** + * The URL for the Group's icon. + */ + icon: string; + /** + * The number of members in the Group. + */ + memberCount: number; + /** + * The number of pending member requests. Requires an access token of an Admin of the Group.we The count is only returned when the number of pending member requests is over 50. + */ + memberRequestCount: number; + /** + * The name of the Group. + */ + name: string; + /** + * The parent of this Group, if it exists. + */ + parent: Group | Page; + /** + * The permissions a User has granted for an app installed in the Group. + */ + permissions: string; + /** + * The privacy setting of the Group. Requires an access token of an Admin of the Group. + */ + privacy: 'CLOSED' | 'OPEN' | 'SECRET'; + /** + * The last time the Group was updated (includes changes Group properties, Posts, and Comments). Requires an access token of an Admin of the Group. + */ + updatedTime: string; +}; + +type CoverPhoto = { + /** + * ID of the Photo that represents this cover photo. + */ + id: string; + /** + * URL to the Photo that represents this cover photo. + */ + source: string; + /** + * The vertical offset in pixels of the photo from the bottom. + */ + offsetY: number; + /** + * The horizontal offset in pixels of the photo from the left. + */ + offsetX: number; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/application + */ +type Application = Record; + +/** + * https://developers.facebook.com/docs/graph-api/reference/story-attachment-media/ + */ +type StoryAttachmentMedia = { + image: any; + source: string; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/story-attachment-target/ + */ +type StoryAttachmentTarget = { + id: string; + unshimmedUrl?: string; + url: string; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/v6.0/object/likes + */ +export type Likes = { + data: Like[]; + paging: { + cursors: { + before: string; + after: string; + }; + next: string; + }; + summary?: { + totalCount: number; + }; +}; + +export type Like = { + name: string; + id: string; + createdTime: string; +}; + +export type CommonPaginationOptions = { + limit?: number; +}; + +export type CursorBasedPaginationOptions = CommonPaginationOptions & { + before?: string; + after?: string; +}; + +export type TimeBasedPaginationOptions = CommonPaginationOptions & { + until?: string; + since?: string; +}; + +export type OffsetBasedPaginationOptions = CommonPaginationOptions & { + offset?: number; +}; + +export type PaginationOptions = + | CursorBasedPaginationOptions + | TimeBasedPaginationOptions + | OffsetBasedPaginationOptions; + +export type PagingData = { + data: T; + paging: { + previous?: string; + next?: string; + }; +}; + +export type GetCommentOptions = { + fields?: T[]; +}; + +export type GetCommentsOptions< + T extends CommentField, + U extends boolean +> = PaginationOptions & { + summary?: U; + filter?: 'toplevel' | 'stream'; + order?: 'chronological' | 'reverse_chronological'; + fields?: T[]; +}; + +export type GetLikesOptions = { + summary?: boolean; +}; + +/** + * https://developers.facebook.com/docs/graph-api/reference/v7.0/insights + */ +export type DatePreset = + | 'today' + | 'yesterday' + | 'this_month' + | 'last_month' + | 'this_quarter' + | 'lifetime' + | 'last_3d' + | 'last_7d' + | 'last_14d' + | 'last_28d' + | 'last_30d' + | 'last_90d' + | 'last_week_mon_sun' + | 'last_week_sun_sat' + | 'last_quarter' + | 'last_year' + | 'this_week_mon_today' + | 'this_week_sun_today' + | 'this_year'; + +export type PageInsightsMetricDay = + | 'page_tab_views_login_top_unique' + | 'page_tab_views_login_top' + | 'page_tab_views_logout_top' + | 'page_total_actions' + | 'page_cta_clicks_logged_in_total' + | 'page_cta_clicks_logged_in_unique' + | 'page_cta_clicks_by_site_logged_in_unique' + | 'page_cta_clicks_by_age_gender_logged_in_unique' + | 'page_cta_clicks_logged_in_by_country_unique' + | 'page_cta_clicks_logged_in_by_city_unique' + | 'page_call_phone_clicks_logged_in_unique' + | 'page_call_phone_clicks_by_age_gender_logged_in_unique' + | 'page_call_phone_clicks_logged_in_by_country_unique' + | 'page_call_phone_clicks_logged_in_by_city_unique' + | 'page_call_phone_clicks_by_site_logged_in_unique' + | 'page_get_directions_clicks_logged_in_unique' + | 'page_get_directions_clicks_by_age_gender_logged_in_unique' + | 'page_get_directions_clicks_logged_in_by_country_unique' + | 'page_get_directions_clicks_logged_in_by_city_unique' + | 'page_get_directions_clicks_by_site_logged_in_unique' + | 'page_website_clicks_logged_in_unique' + | 'page_website_clicks_by_age_gender_logged_in_unique' + | 'page_website_clicks_logged_in_by_country_unique' + | 'page_website_clicks_logged_in_by_city_unique' + | 'page_website_clicks_by_site_logged_in_unique' + | 'page_engaged_users' + | 'page_post_engagements' + | 'page_consumptions' + | 'page_consumptions_unique' + | 'page_consumptions_by_consumption_type' + | 'page_consumptions_by_consumption_type_unique' + | 'page_places_checkin_total' + | 'page_places_checkin_total_unique' + | 'page_places_checkin_mobile' + | 'page_places_checkin_mobile_unique' + | 'page_places_checkins_by_age_gender' + | 'page_places_checkins_by_locale' + | 'page_places_checkins_by_country' + | 'page_negative_feedback' + | 'page_negative_feedback_unique' + | 'page_negative_feedback_by_type' + | 'page_negative_feedback_by_type_unique' + | 'page_positive_feedback_by_type' + | 'page_positive_feedback_by_type_unique' + | 'page_fans_online' + | 'page_fans_online_per_day' + | 'page_fan_adds_by_paid_non_paid_unique' + | 'page_impressions' + | 'page_impressions_unique' + | 'page_impressions_paid' + | 'page_impressions_paid_unique' + | 'page_impressions_organic' + | 'page_impressions_organic_unique' + | 'page_impressions_viral' + | 'page_impressions_viral_unique' + | 'page_impressions_nonviral' + | 'page_impressions_nonviral_unique' + | 'page_impressions_by_story_type' + | 'page_impressions_by_story_type_unique' + | 'page_impressions_by_city_unique' + | 'page_impressions_by_country_unique' + | 'page_impressions_by_locale_unique' + | 'page_impressions_by_age_gender_unique' + | 'page_impressions_frequency_distribution' + | 'page_impressions_viral_frequency_distribution' + | 'page_posts_impressions' + | 'page_posts_impressions_unique' + | 'page_posts_impressions_paid' + | 'page_posts_impressions_paid_unique' + | 'page_posts_impressions_organic' + | 'page_posts_impressions_organic_unique' + | 'page_posts_served_impressions_organic_unique' + | 'page_posts_impressions_viral' + | 'page_posts_impressions_viral_unique' + | 'page_posts_impressions_nonviral' + | 'page_posts_impressions_nonviral_unique' + | 'page_posts_impressions_frequency_distribution' + | 'page_actions_post_reactions_like_total' + | 'page_actions_post_reactions_love_total' + | 'page_actions_post_reactions_wow_total' + | 'page_actions_post_reactions_haha_total' + | 'page_actions_post_reactions_sorry_total' + | 'page_actions_post_reactions_anger_total' + | 'page_actions_post_reactions_total' + | 'page_fans' + | 'page_fans_locale' + | 'page_fans_city' + | 'page_fans_country' + | 'page_fans_gender_age' + | 'page_fan_adds' + | 'page_fan_adds_unique' + | 'page_fans_by_like_source' + | 'page_fans_by_like_source_unique' + | 'page_fan_removes' + | 'page_fan_removes_unique' + | 'page_fans_by_unlike_source' + | 'page_fans_by_unlike_source_unique' + | 'page_video_views' + | 'page_video_views_paid' + | 'page_video_views_organic' + | 'page_video_views_by_paid_non_paid' + | 'page_video_views_autoplayed' + | 'page_video_views_click_to_play' + | 'page_video_views_unique' + | 'page_video_repeat_views' + | 'page_video_complete_views_30s' + | 'page_video_complete_views_30s_paid' + | 'page_video_complete_views_30s_organic' + | 'page_video_complete_views_30s_autoplayed' + | 'page_video_complete_views_30s_click_to_play' + | 'page_video_complete_views_30s_unique' + | 'page_video_complete_views_30s_repeat_views' + | 'page_video_views_10s' + | 'page_video_views_10s_paid' + | 'page_video_views_10s_organic' + | 'page_video_views_10s_autoplayed' + | 'page_video_views_10s_click_to_play' + | 'page_video_views_10s_unique' + | 'page_video_views_10s_repeat' + | 'page_video_view_time' + | 'page_views_total' + | 'page_views_logout' + | 'page_views_logged_in_total' + | 'page_views_logged_in_unique' + | 'page_views_external_referrals' + | 'page_views_by_profile_tab_total' + | 'page_views_by_profile_tab_logged_in_unique' + | 'page_views_by_internal_referer_logged_in_unique' + | 'page_views_by_site_logged_in_unique' + | 'page_views_by_age_gender_logged_in_unique' + | 'page_views_by_referers_logged_in_unique' + | 'page_content_activity_by_action_type_unique' + | 'page_content_activity_by_age_gender_unique' + | 'page_content_activity_by_city_unique' + | 'page_content_activity_by_country_unique' + | 'page_content_activity_by_locale_unique' + | 'page_content_activity' + | 'page_content_activity_by_action_type' + | 'page_daily_video_ad_break_ad_impressions_by_crosspost_status' + | 'page_daily_video_ad_break_cpm_by_crosspost_status' + | 'page_daily_video_ad_break_earnings_by_crosspost_status'; + +export type PageInsightsMetricWeek = + | 'page_tab_views_login_top_unique' + | 'page_tab_views_login_top' + | 'page_total_actions' + | 'page_cta_clicks_logged_in_total' + | 'page_cta_clicks_logged_in_unique' + | 'page_cta_clicks_by_site_logged_in_unique' + | 'page_cta_clicks_by_age_gender_logged_in_unique' + | 'page_cta_clicks_logged_in_by_country_unique' + | 'page_cta_clicks_logged_in_by_city_unique' + | 'page_call_phone_clicks_logged_in_unique' + | 'page_call_phone_clicks_by_age_gender_logged_in_unique' + | 'page_call_phone_clicks_logged_in_by_country_unique' + | 'page_call_phone_clicks_logged_in_by_city_unique' + | 'page_call_phone_clicks_by_site_logged_in_unique' + | 'page_get_directions_clicks_logged_in_unique' + | 'page_get_directions_clicks_by_age_gender_logged_in_unique' + | 'page_get_directions_clicks_logged_in_by_country_unique' + | 'page_get_directions_clicks_logged_in_by_city_unique' + | 'page_get_directions_clicks_by_site_logged_in_unique' + | 'page_website_clicks_logged_in_unique' + | 'page_website_clicks_by_age_gender_logged_in_unique' + | 'page_website_clicks_logged_in_by_country_unique' + | 'page_website_clicks_logged_in_by_city_unique' + | 'page_website_clicks_by_site_logged_in_unique' + | 'page_engaged_users' + | 'page_post_engagements' + | 'page_consumptions' + | 'page_consumptions_unique' + | 'page_consumptions_by_consumption_type' + | 'page_consumptions_by_consumption_type_unique' + | 'page_places_checkin_total' + | 'page_places_checkin_total_unique' + | 'page_places_checkin_mobile' + | 'page_places_checkin_mobile_unique' + | 'page_negative_feedback' + | 'page_negative_feedback_unique' + | 'page_negative_feedback_by_type' + | 'page_negative_feedback_by_type_unique' + | 'page_positive_feedback_by_type' + | 'page_positive_feedback_by_type_unique' + | 'page_impressions' + | 'page_impressions_unique' + | 'page_impressions_paid' + | 'page_impressions_paid_unique' + | 'page_impressions_organic' + | 'page_impressions_organic_unique' + | 'page_impressions_viral' + | 'page_impressions_viral_unique' + | 'page_impressions_nonviral' + | 'page_impressions_nonviral_unique' + | 'page_impressions_by_story_type' + | 'page_impressions_by_story_type_unique' + | 'page_impressions_by_city_unique' + | 'page_impressions_by_country_unique' + | 'page_impressions_by_locale_unique' + | 'page_impressions_by_age_gender_unique' + | 'page_impressions_frequency_distribution' + | 'page_impressions_viral_frequency_distribution' + | 'page_posts_impressions' + | 'page_posts_impressions_unique' + | 'page_posts_impressions_paid' + | 'page_posts_impressions_paid_unique' + | 'page_posts_impressions_organic' + | 'page_posts_impressions_organic_unique' + | 'page_posts_served_impressions_organic_unique' + | 'page_posts_impressions_viral' + | 'page_posts_impressions_viral_unique' + | 'page_posts_impressions_nonviral' + | 'page_posts_impressions_nonviral_unique' + | 'page_posts_impressions_frequency_distribution' + | 'page_actions_post_reactions_like_total' + | 'page_actions_post_reactions_love_total' + | 'page_actions_post_reactions_wow_total' + | 'page_actions_post_reactions_haha_total' + | 'page_actions_post_reactions_sorry_total' + | 'page_actions_post_reactions_anger_total' + | 'page_actions_post_reactions_total' + | 'page_fan_adds_unique' + | 'page_fan_removes_unique' + | 'page_fans_by_unlike_source' + | 'page_fans_by_unlike_source_unique' + | 'page_video_views' + | 'page_video_views_paid' + | 'page_video_views_organic' + | 'page_video_views_by_paid_non_paid' + | 'page_video_views_autoplayed' + | 'page_video_views_click_to_play' + | 'page_video_views_unique' + | 'page_video_repeat_views' + | 'page_video_complete_views_30s' + | 'page_video_complete_views_30s_paid' + | 'page_video_complete_views_30s_organic' + | 'page_video_complete_views_30s_autoplayed' + | 'page_video_complete_views_30s_click_to_play' + | 'page_video_complete_views_30s_unique' + | 'page_video_complete_views_30s_repeat_views' + | 'page_video_views_10s' + | 'page_video_views_10s_paid' + | 'page_video_views_10s_organic' + | 'page_video_views_10s_autoplayed' + | 'page_video_views_10s_click_to_play' + | 'page_video_views_10s_unique' + | 'page_video_views_10s_repeat' + | 'page_views_total' + | 'page_views_logged_in_total' + | 'page_views_logged_in_unique' + | 'page_views_external_referrals' + | 'page_views_by_profile_tab_total' + | 'page_views_by_profile_tab_logged_in_unique' + | 'page_views_by_internal_referer_logged_in_unique' + | 'page_views_by_site_logged_in_unique' + | 'page_views_by_age_gender_logged_in_unique' + | 'page_views_by_referers_logged_in_unique' + | 'page_content_activity_by_action_type_unique' + | 'page_content_activity_by_age_gender_unique' + | 'page_content_activity_by_city_unique' + | 'page_content_activity_by_country_unique' + | 'page_content_activity_by_locale_unique' + | 'page_content_activity' + | 'page_content_activity_by_action_type'; + +export type PageInsightsMetricDays28 = + | 'page_total_actions' + | 'page_cta_clicks_logged_in_total' + | 'page_cta_clicks_logged_in_unique' + | 'page_cta_clicks_by_site_logged_in_unique' + | 'page_cta_clicks_by_age_gender_logged_in_unique' + | 'page_call_phone_clicks_logged_in_unique' + | 'page_call_phone_clicks_by_age_gender_logged_in_unique' + | 'page_call_phone_clicks_by_site_logged_in_unique' + | 'page_get_directions_clicks_logged_in_unique' + | 'page_get_directions_clicks_by_age_gender_logged_in_unique' + | 'page_get_directions_clicks_by_site_logged_in_unique' + | 'page_website_clicks_logged_in_unique' + | 'page_website_clicks_by_age_gender_logged_in_unique' + | 'page_website_clicks_by_site_logged_in_unique' + | 'page_engaged_users' + | 'page_post_engagements' + | 'page_consumptions' + | 'page_consumptions_unique' + | 'page_consumptions_by_consumption_type' + | 'page_consumptions_by_consumption_type_unique' + | 'page_places_checkin_total' + | 'page_places_checkin_total_unique' + | 'page_places_checkin_mobile' + | 'page_places_checkin_mobile_unique' + | 'page_negative_feedback' + | 'page_negative_feedback_unique' + | 'page_negative_feedback_by_type' + | 'page_negative_feedback_by_type_unique' + | 'page_positive_feedback_by_type' + | 'page_positive_feedback_by_type_unique' + | 'page_impressions' + | 'page_impressions_unique' + | 'page_impressions_paid' + | 'page_impressions_paid_unique' + | 'page_impressions_organic' + | 'page_impressions_organic_unique' + | 'page_impressions_viral' + | 'page_impressions_viral_unique' + | 'page_impressions_nonviral' + | 'page_impressions_nonviral_unique' + | 'page_impressions_by_story_type' + | 'page_impressions_by_story_type_unique' + | 'page_impressions_by_city_unique' + | 'page_impressions_by_country_unique' + | 'page_impressions_by_locale_unique' + | 'page_impressions_by_age_gender_unique' + | 'page_impressions_frequency_distribution' + | 'page_impressions_viral_frequency_distribution' + | 'page_posts_impressions' + | 'page_posts_impressions_unique' + | 'page_posts_impressions_paid' + | 'page_posts_impressions_paid_unique' + | 'page_posts_impressions_organic' + | 'page_posts_impressions_organic_unique' + | 'page_posts_served_impressions_organic_unique' + | 'page_posts_impressions_viral' + | 'page_posts_impressions_viral_unique' + | 'page_posts_impressions_nonviral' + | 'page_posts_impressions_nonviral_unique' + | 'page_posts_impressions_frequency_distribution' + | 'page_actions_post_reactions_like_total' + | 'page_actions_post_reactions_love_total' + | 'page_actions_post_reactions_wow_total' + | 'page_actions_post_reactions_haha_total' + | 'page_actions_post_reactions_sorry_total' + | 'page_actions_post_reactions_anger_total' + | 'page_actions_post_reactions_total' + | 'page_fan_adds_unique' + | 'page_fan_removes_unique' + | 'page_fans_by_unlike_source' + | 'page_fans_by_unlike_source_unique' + | 'page_video_views' + | 'page_video_views_paid' + | 'page_video_views_organic' + | 'page_video_views_by_paid_non_paid' + | 'page_video_views_autoplayed' + | 'page_video_views_click_to_play' + | 'page_video_views_unique' + | 'page_video_repeat_views' + | 'page_video_complete_views_30s' + | 'page_video_complete_views_30s_paid' + | 'page_video_complete_views_30s_organic' + | 'page_video_complete_views_30s_autoplayed' + | 'page_video_complete_views_30s_click_to_play' + | 'page_video_complete_views_30s_unique' + | 'page_video_complete_views_30s_repeat_views' + | 'page_video_views_10s' + | 'page_video_views_10s_paid' + | 'page_video_views_10s_organic' + | 'page_video_views_10s_autoplayed' + | 'page_video_views_10s_click_to_play' + | 'page_video_views_10s_unique' + | 'page_video_views_10s_repeat' + | 'page_views_total' + | 'page_views_logged_in_total' + | 'page_views_logged_in_unique' + | 'page_views_external_referrals' + | 'page_views_by_profile_tab_total' + | 'page_views_by_profile_tab_logged_in_unique' + | 'page_views_by_internal_referer_logged_in_unique' + | 'page_views_by_site_logged_in_unique' + | 'page_views_by_age_gender_logged_in_unique' + | 'page_content_activity_by_action_type_unique' + | 'page_content_activity_by_age_gender_unique' + | 'page_content_activity_by_city_unique' + | 'page_content_activity_by_country_unique' + | 'page_content_activity_by_locale_unique' + | 'page_content_activity' + | 'page_content_activity_by_action_type'; + +export type PostInsightsMetricDay = + | 'post_video_views_organic' + | 'post_video_views_paid' + | 'post_video_views' + | 'post_video_views_unique' + | 'post_video_views_10s' + | 'post_video_views_10s_paid' + | 'post_video_view_time' + | 'post_video_view_time_organic' + | 'post_video_view_time_by_region_id' + | 'post_video_ad_break_ad_impressions' + | 'post_video_ad_break_earnings' + | 'post_video_ad_break_ad_cpm'; + +export type PostInsightsMetricLifetime = + | 'post_engaged_users' + | 'post_negative_feedback' + | 'post_negative_feedback_unique' + | 'post_negative_feedback_by_type' + | 'post_negative_feedback_by_type_unique' + | 'post_engaged_fan' + | 'post_clicks' + | 'post_clicks_unique' + | 'post_clicks_by_type' + | 'post_clicks_by_type_unique' + | 'post_impressions' + | 'post_impressions_unique' + | 'post_impressions_paid' + | 'post_impressions_paid_unique' + | 'post_impressions_fan' + | 'post_impressions_fan_unique' + | 'post_impressions_fan_paid' + | 'post_impressions_fan_paid_unique' + | 'post_impressions_organic' + | 'post_impressions_organic_unique' + | 'post_impressions_viral' + | 'post_impressions_viral_unique' + | 'post_impressions_nonviral' + | 'post_impressions_nonviral_unique' + | 'post_impressions_by_story_type' + | 'post_impressions_by_story_type_unique' + | 'post_reactions_like_total' + | 'post_reactions_love_total' + | 'post_reactions_wow_total' + | 'post_reactions_haha_total' + | 'post_reactions_sorry_total' + | 'post_reactions_anger_total' + | 'post_reactions_by_type_total' + | 'post_video_complete_views_30s_autoplayed' + | 'post_video_complete_views_30s_clicked_to_play' + | 'post_video_complete_views_30s_organic' + | 'post_video_complete_views_30s_paid' + | 'post_video_complete_views_30s_unique' + | 'post_video_avg_time_watched' + | 'post_video_complete_views_organic' + | 'post_video_complete_views_organic_unique' + | 'post_video_complete_views_paid' + | 'post_video_complete_views_paid_unique' + | 'post_video_retention_graph' + | 'post_video_retention_graph_clicked_to_play' + | 'post_video_retention_graph_autoplayed' + | 'post_video_views_organic' + | 'post_video_views_organic_unique' + | 'post_video_views_paid' + | 'post_video_views_paid_unique' + | 'post_video_length' + | 'post_video_views' + | 'post_video_views_unique' + | 'post_video_views_autoplayed' + | 'post_video_views_clicked_to_play' + | 'post_video_views_10s' + | 'post_video_views_10s_unique' + | 'post_video_views_10s_autoplayed' + | 'post_video_views_10s_clicked_to_play' + | 'post_video_views_10s_organic' + | 'post_video_views_10s_paid' + | 'post_video_views_10s_sound_on' + | 'post_video_views_sound_on' + | 'post_video_view_time' + | 'post_video_view_time_organic' + | 'post_video_view_time_by_age_bucket_and_gender' + | 'post_video_view_time_by_region_id' + | 'post_video_views_by_distribution_type' + | 'post_video_view_time_by_distribution_type' + | 'post_video_view_time_by_country_id' + | 'post_activity' + | 'post_activity_unique' + | 'post_activity_by_action_type' + | 'post_activity_by_action_type_unique' + | 'post_video_ad_break_ad_impressions' + | 'post_video_ad_break_earnings' + | 'post_video_ad_break_ad_cpm'; diff --git a/packages/bottender-facebook/src/__tests__/FacebookBatch.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookBatch.spec.ts new file mode 100644 index 000000000..9e12515ba --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookBatch.spec.ts @@ -0,0 +1,244 @@ +import FacebookBatch from '../FacebookBatch'; + +const COMMENT_ID = '1234567890'; +const ACCESS_TOKEN = 'custom-access-token'; + +describe('sendComment', () => { + it('should create send text comment request', () => { + expect(FacebookBatch.sendComment(COMMENT_ID, 'ok')).toEqual({ + method: 'POST', + relativeUrl: '1234567890/comments', + body: { + message: 'ok', + }, + }); + }); + + it('should create send attachmentUrl comment request', () => { + expect( + FacebookBatch.sendComment(COMMENT_ID, { + attachmentUrl: 'https://www.example.com/img.png', + }) + ).toEqual({ + method: 'POST', + relativeUrl: '1234567890/comments', + body: { + attachmentUrl: 'https://www.example.com/img.png', + }, + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.sendComment(COMMENT_ID, 'ok', { + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'POST', + relativeUrl: '1234567890/comments', + body: { + message: 'ok', + accessToken: ACCESS_TOKEN, + }, + }); + }); +}); + +describe('sendLike', () => { + it('should create send like request', () => { + expect(FacebookBatch.sendLike(COMMENT_ID)).toEqual({ + method: 'POST', + relativeUrl: '1234567890/likes', + body: {}, + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.sendLike(COMMENT_ID, { + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'POST', + relativeUrl: '1234567890/likes', + body: { + accessToken: ACCESS_TOKEN, + }, + }); + }); +}); + +describe('getComment', () => { + it('should create get comment request', () => { + expect(FacebookBatch.getComment(COMMENT_ID)).toEqual({ + method: 'GET', + relativeUrl: '1234567890?', + }); + }); + + it('should support other options', () => { + expect( + FacebookBatch.getComment(COMMENT_ID, { + summary: true, + filter: 'toplevel', + fields: ['can_reply_privately'], + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '1234567890?summary=true&filter=toplevel&fields=can_reply_privately', + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.getComment(COMMENT_ID, { + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'GET', + relativeUrl: `1234567890?access_token=${ACCESS_TOKEN}`, + }); + }); +}); + +describe('getLikes', () => { + it('should create get likes request', () => { + expect(FacebookBatch.getLikes(COMMENT_ID)).toEqual({ + method: 'GET', + relativeUrl: '1234567890/likes?', + }); + }); + + it('should support other options', () => { + expect(FacebookBatch.getLikes(COMMENT_ID, { summary: true })).toEqual({ + method: 'GET', + relativeUrl: '1234567890/likes?summary=true', + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.getLikes(COMMENT_ID, { + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'GET', + relativeUrl: `1234567890/likes?access_token=${ACCESS_TOKEN}`, + }); + }); +}); + +describe('getPageInsights', () => { + it('should create get page insights request', () => { + expect( + FacebookBatch.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'day', + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/me/insights?period=day&metric=page_total_actions%2Cpage_engaged_users', + }); + }); + + it('should support datePreset', () => { + expect( + FacebookBatch.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'day', + datePreset: 'today', + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/me/insights?period=day&metric=page_total_actions%2Cpage_engaged_users&date_preset=today', + }); + }); + + it('should support since and until', () => { + expect( + FacebookBatch.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'day', + since: new Date('2020-07-06T03:24:00Z'), + until: new Date('2020-07-07T03:24:00Z'), + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/me/insights?period=day&metric=page_total_actions%2Cpage_engaged_users&since=2020-07-06T03%3A24%3A00.000Z&until=2020-07-07T03%3A24%3A00.000Z', + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'day', + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'GET', + relativeUrl: `/me/insights?access_token=${ACCESS_TOKEN}&period=day&metric=page_total_actions%2Cpage_engaged_users`, + }); + }); +}); + +describe('getPostInsights', () => { + it('should create get post insights request', () => { + expect( + FacebookBatch.getPostInsights('POST_ID', { + metric: ['post_video_views_organic', 'post_video_views_paid'], + period: 'day', + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/POST_ID/insights?period=day&metric=post_video_views_organic%2Cpost_video_views_paid', + }); + }); + + it('should support datePreset', () => { + expect( + FacebookBatch.getPostInsights('POST_ID', { + metric: ['post_video_views_organic', 'post_video_views_paid'], + period: 'day', + datePreset: 'today', + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/POST_ID/insights?period=day&metric=post_video_views_organic%2Cpost_video_views_paid&date_preset=today', + }); + }); + + it('should support since and until', () => { + expect( + FacebookBatch.getPostInsights('POST_ID', { + metric: ['post_video_views_organic', 'post_video_views_paid'], + period: 'day', + since: new Date('2020-07-06T03:24:00Z'), + until: new Date('2020-07-07T03:24:00Z'), + }) + ).toEqual({ + method: 'GET', + relativeUrl: + '/POST_ID/insights?period=day&metric=post_video_views_organic%2Cpost_video_views_paid&since=2020-07-06T03%3A24%3A00.000Z&until=2020-07-07T03%3A24%3A00.000Z', + }); + }); + + it('should support custom access token', () => { + expect( + FacebookBatch.getPostInsights('POST_ID', { + metric: ['post_video_views_organic', 'post_video_views_paid'], + period: 'day', + accessToken: ACCESS_TOKEN, + }) + ).toEqual({ + method: 'GET', + relativeUrl: `/POST_ID/insights?access_token=${ACCESS_TOKEN}&period=day&metric=post_video_views_organic%2Cpost_video_views_paid`, + }); + }); +}); diff --git a/packages/bottender-facebook/src/__tests__/FacebookClient-insights.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookClient-insights.spec.ts new file mode 100644 index 000000000..ff3af3bd4 --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookClient-insights.spec.ts @@ -0,0 +1,1295 @@ +import nock from 'nock'; + +import FacebookClient from '../FacebookClient'; + +nock.disableNetConnect(); +nock.enableNetConnect('127.0.0.1'); + +afterEach(() => nock.cleanAll()); + +const ACCESS_TOKEN = '1234567890'; + +describe('#getPageInsights', () => { + it('should support day period', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const PAGE_ID = 'PAGE_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'page_total_actions', + period: 'day', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Total: total action count per Page', + description: + "Daily: The number of clicks on your Page's contact info and call-to-action button.", + id: `${PAGE_ID}/insights/page_total_actions/day`, + }, + { + name: 'page_engaged_users', + period: 'day', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Page Engaged Users', + description: + 'Daily: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: `${PAGE_ID}/insights/page_engaged_users/day`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=day&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=day&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'day', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'page_total_actions,page_engaged_users', + period: 'day', + }); + expect(res).toEqual([ + { + description: + "Daily: The number of clicks on your Page's contact info and call-to-action button.", + id: 'PAGE_ID/insights/page_total_actions/day', + name: 'page_total_actions', + period: 'day', + title: 'Daily Total: total action count per Page', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 0 }, + { endTime: '2020-07-06T07:00:00+0000', value: 0 }, + ], + }, + { + description: + 'Daily: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: 'PAGE_ID/insights/page_engaged_users/day', + name: 'page_engaged_users', + period: 'day', + title: 'Daily Page Engaged Users', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 0 }, + { endTime: '2020-07-06T07:00:00+0000', value: 0 }, + ], + }, + ]); + }); + + it('should support day period (non-numeric value)', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const PAGE_ID = 'PAGE_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'page_tab_views_login_top_unique', + period: 'day', + values: [ + { + value: { + tab_home: 3, + about: 2, + profile_home: 1, + videos: 3, + posts: 6, + photos: 2, + home: 34, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + tab_home: 3, + reviews: 1, + about: 2, + profile_home: 2, + videos: 2, + community: 1, + posts: 5, + photos: 2, + home: 68, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Logged-in Tab Views', + description: + 'Daily: Tabs on your Page that were viewed when logged-in users visited your Page. (Unique Users)', + id: `${PAGE_ID}/insights/page_tab_views_login_top_unique/day`, + }, + { + name: 'page_tab_views_login_top', + period: 'day', + values: [ + { + value: { + tab_home: 4, + about: 2, + profile_home: 1, + videos: 3, + posts: 8, + photos: 2, + home: 54, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + tab_home: 7, + reviews: 1, + about: 2, + profile_home: 2, + videos: 2, + community: 1, + posts: 5, + photos: 2, + home: 110, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Logged-in Tab Views', + description: + 'Daily: Tabs on your Page that were viewed when logged-in users visited your Page. (Total Count)', + id: `${PAGE_ID}/insights/page_tab_views_login_top/day`, + }, + { + name: 'page_tab_views_logout_top', + period: 'day', + values: [ + { + value: { + profile_home: 1, + posts: 1, + home: 4, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + profile_videos: 1, + profile_posts: 1, + profile_home: 3, + about: 1, + photos: 2, + posts: 1, + home: 3, + profile_photos: 1, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Logged-out Page Views Per Tab', + description: + 'Daily: Page views by tab from users not logged into Facebook (Total Count)', + id: `${PAGE_ID}/insights/page_tab_views_logout_top/day`, + }, + { + name: 'page_negative_feedback_by_type', + period: 'day', + values: [ + { + value: { + hide_clicks: 0, + hide_all_clicks: 0, + report_spam_clicks: 0, + unlike_page_clicks: 0, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + hide_clicks: 0, + hide_all_clicks: 0, + report_spam_clicks: 0, + unlike_page_clicks: 0, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Negative Feedback From Users', + description: + 'Daily: The number of times people have given negative feedback to your Page, by type. (Total Count)', + id: `${PAGE_ID}/insights/page_negative_feedback_by_type/day`, + }, + { + name: 'page_positive_feedback_by_type', + period: 'day', + values: [ + { + value: { + link: 10, + like: 25, + comment: 19, + other: 6, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + link: 6, + like: 43, + comment: 25, + other: 11, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Positive Feedback From Users', + description: + 'Daily: The number of times people have given positive feedback to your Page, by type. (Total Count)', + id: `${PAGE_ID}/insights/page_positive_feedback_by_type/day`, + }, + { + name: 'page_fans_by_like_source', + period: 'day', + values: [ + { + value: { + Other: 1, + 'Your Page': 5, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + Other: 1, + 'Your Page': 10, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Like Sources', + description: + 'Daily: This is a breakdown of the number of Page likes from the most common places where people can like your Page. (Total Count)', + id: `${PAGE_ID}/insights/page_fans_by_like_source/day`, + }, + { + name: 'page_fans_by_like_source_unique', + period: 'day', + values: [ + { + value: { + Other: 1, + 'Your Page': 5, + 'News Feed': 0, + 'Page Suggestions': 0, + 'Restored Likes from Reactivated Accounts': 0, + Search: 0, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + Other: 1, + 'Your Page': 10, + 'News Feed': 0, + 'Page Suggestions': 0, + 'Restored Likes from Reactivated Accounts': 0, + Search: 0, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Like Sources', + description: + 'Daily: The number of people who liked your Page, broken down by the most common places where people can like your Page. (Unique Users)', + id: `${PAGE_ID}/insights/page_fans_by_like_source_unique/day`, + }, + { + name: 'page_fans_by_unlike_source_unique', + period: 'day', + values: [ + { + value: {}, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + Other: 1, + 'Suspicious Account Removals': 1, + 'Deactivated or Memorialized Account Removals': 0, + 'Unlikes from Page, Posts, or News Feed': 0, + 'Unlikes from Search': 0, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Unlike Sources', + description: + 'Daily: The number of people who unliked your Page, broken down by the most common places where people can unlike your Page. (Unique Users)', + id: `${PAGE_ID}/insights/page_fans_by_unlike_source_unique/day`, + }, + { + name: 'page_fans_by_unlike_source', + period: 'day', + values: [ + { + value: {}, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + Other: 1, + 'Suspicious Account Removals': 1, + 'Deactivated or Memorialized Account Removals': 0, + 'Unlikes from Page, Posts, or News Feed': 0, + 'Unlikes from Search': 0, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Unlike Sources', + description: + 'Daily: This is a breakdown of the number of Page unlikes from the most common places where people can unlike your Page. (Total Count)', + id: `${PAGE_ID}/insights/page_fans_by_unlike_source/day`, + }, + { + name: 'page_content_activity_by_action_type_unique', + period: 'day', + values: [ + { + value: { + 'page post': 9, + fan: 6, + other: 30, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + 'page post': 6, + fan: 11, + other: 53, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Talking About This By Story Type', + description: + 'Daily: The number of people talking about your Page, by story type. (Unique Users)', + id: `${PAGE_ID}/insights/page_content_activity_by_action_type_unique/day`, + }, + { + name: 'page_content_activity_by_action_type', + period: 'day', + values: [ + { + value: { + 'page post': 10, + other: 44, + fan: 6, + }, + end_time: '2020-07-06T07:00:00+0000', + }, + { + value: { + other: 68, + fan: 11, + 'page post': 6, + }, + end_time: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Page Stories By Story Type', + description: + 'Daily: The number of stories about your Page by story type. (Total Count)', + id: `${PAGE_ID}/insights/page_content_activity_by_action_type/day`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=day&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=day&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPageInsights({ + metric: [ + 'page_tab_views_login_top_unique', + 'page_tab_views_login_top', + 'page_tab_views_logout_top', + 'page_negative_feedback_by_type', + 'page_positive_feedback_by_type', + 'page_fans_by_like_source_unique', + 'page_fans_by_like_source', + 'page_fans_by_unlike_source_unique', + 'page_fans_by_unlike_source', + 'page_content_activity_by_action_type_unique', + 'page_content_activity_by_action_type', + ], + period: 'day', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: + 'page_tab_views_login_top_unique,page_tab_views_login_top,page_tab_views_logout_top,page_negative_feedback_by_type,page_positive_feedback_by_type,page_fans_by_like_source_unique,page_fans_by_like_source,page_fans_by_unlike_source_unique,page_fans_by_unlike_source,page_content_activity_by_action_type_unique,page_content_activity_by_action_type', + period: 'day', + }); + expect(res).toEqual([ + { + description: + 'Daily: Tabs on your Page that were viewed when logged-in users visited your Page. (Unique Users)', + id: 'PAGE_ID/insights/page_tab_views_login_top_unique/day', + name: 'page_tab_views_login_top_unique', + period: 'day', + title: 'Daily Logged-in Tab Views', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + about: 2, + home: 34, + photos: 2, + posts: 6, + profileHome: 1, + tabHome: 3, + videos: 3, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + about: 2, + community: 1, + home: 68, + photos: 2, + posts: 5, + profileHome: 2, + reviews: 1, + tabHome: 3, + videos: 2, + }, + }, + ], + }, + { + description: + 'Daily: Tabs on your Page that were viewed when logged-in users visited your Page. (Total Count)', + id: 'PAGE_ID/insights/page_tab_views_login_top/day', + name: 'page_tab_views_login_top', + period: 'day', + title: 'Daily Logged-in Tab Views', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + about: 2, + home: 54, + photos: 2, + posts: 8, + profileHome: 1, + tabHome: 4, + videos: 3, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + about: 2, + community: 1, + home: 110, + photos: 2, + posts: 5, + profileHome: 2, + reviews: 1, + tabHome: 7, + videos: 2, + }, + }, + ], + }, + { + description: + 'Daily: Page views by tab from users not logged into Facebook (Total Count)', + id: 'PAGE_ID/insights/page_tab_views_logout_top/day', + name: 'page_tab_views_logout_top', + period: 'day', + title: 'Daily Logged-out Page Views Per Tab', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + home: 4, + posts: 1, + profileHome: 1, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + about: 1, + home: 3, + photos: 2, + posts: 1, + profileHome: 3, + profilePhotos: 1, + profilePosts: 1, + profileVideos: 1, + }, + }, + ], + }, + { + description: + 'Daily: The number of times people have given negative feedback to your Page, by type. (Total Count)', + id: 'PAGE_ID/insights/page_negative_feedback_by_type/day', + name: 'page_negative_feedback_by_type', + period: 'day', + title: 'Daily Negative Feedback From Users', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + hideAllClicks: 0, + hideClicks: 0, + reportSpamClicks: 0, + unlikePageClicks: 0, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + hideAllClicks: 0, + hideClicks: 0, + reportSpamClicks: 0, + unlikePageClicks: 0, + }, + }, + ], + }, + { + name: 'page_positive_feedback_by_type', + period: 'day', + values: [ + { + value: { + link: 10, + like: 25, + comment: 19, + other: 6, + }, + endTime: '2020-07-06T07:00:00+0000', + }, + { + value: { + link: 6, + like: 43, + comment: 25, + other: 11, + }, + endTime: '2020-07-07T07:00:00+0000', + }, + ], + title: 'Daily Positive Feedback From Users', + description: + 'Daily: The number of times people have given positive feedback to your Page, by type. (Total Count)', + id: 'PAGE_ID/insights/page_positive_feedback_by_type/day', + }, + { + description: + 'Daily: This is a breakdown of the number of Page likes from the most common places where people can like your Page. (Total Count)', + id: 'PAGE_ID/insights/page_fans_by_like_source/day', + name: 'page_fans_by_like_source', + period: 'day', + title: 'Daily Like Sources', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + other: 1, + yourPage: 5, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + other: 1, + yourPage: 10, + }, + }, + ], + }, + { + description: + 'Daily: The number of people who liked your Page, broken down by the most common places where people can like your Page. (Unique Users)', + id: 'PAGE_ID/insights/page_fans_by_like_source_unique/day', + name: 'page_fans_by_like_source_unique', + period: 'day', + title: 'Daily Like Sources', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + other: 1, + yourPage: 5, + newsFeed: 0, + pageSuggestions: 0, + restoredLikesFromReactivatedAccounts: 0, + search: 0, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + other: 1, + yourPage: 10, + newsFeed: 0, + pageSuggestions: 0, + restoredLikesFromReactivatedAccounts: 0, + search: 0, + }, + }, + ], + }, + { + description: + 'Daily: The number of people who unliked your Page, broken down by the most common places where people can unlike your Page. (Unique Users)', + id: 'PAGE_ID/insights/page_fans_by_unlike_source_unique/day', + name: 'page_fans_by_unlike_source_unique', + period: 'day', + title: 'Daily Unlike Sources', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: {}, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + deactivatedOrMemorializedAccountRemovals: 0, + other: 1, + suspiciousAccountRemovals: 1, + unlikesFromPagePostsOrNewsFeed: 0, + unlikesFromSearch: 0, + }, + }, + ], + }, + { + description: + 'Daily: This is a breakdown of the number of Page unlikes from the most common places where people can unlike your Page. (Total Count)', + id: 'PAGE_ID/insights/page_fans_by_unlike_source/day', + name: 'page_fans_by_unlike_source', + period: 'day', + title: 'Daily Unlike Sources', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: {}, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + deactivatedOrMemorializedAccountRemovals: 0, + other: 1, + suspiciousAccountRemovals: 1, + unlikesFromPagePostsOrNewsFeed: 0, + unlikesFromSearch: 0, + }, + }, + ], + }, + { + description: + 'Daily: The number of people talking about your Page, by story type. (Unique Users)', + id: 'PAGE_ID/insights/page_content_activity_by_action_type_unique/day', + name: 'page_content_activity_by_action_type_unique', + period: 'day', + title: 'Daily Talking About This By Story Type', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + fan: 6, + other: 30, + pagePost: 9, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + fan: 11, + other: 53, + pagePost: 6, + }, + }, + ], + }, + { + description: + 'Daily: The number of stories about your Page by story type. (Total Count)', + id: 'PAGE_ID/insights/page_content_activity_by_action_type/day', + name: 'page_content_activity_by_action_type', + period: 'day', + title: 'Daily Page Stories By Story Type', + values: [ + { + endTime: '2020-07-06T07:00:00+0000', + value: { + fan: 6, + other: 44, + pagePost: 10, + }, + }, + { + endTime: '2020-07-07T07:00:00+0000', + value: { + fan: 11, + other: 68, + pagePost: 6, + }, + }, + ], + }, + ]); + }); + + it('should support week period', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const PAGE_ID = 'PAGE_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'page_total_actions', + period: 'week', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Weekly Total: total action count per Page', + description: + "Weekly: The number of clicks on your Page's contact info and call-to-action button.", + id: `${PAGE_ID}/insights/page_total_actions/week`, + }, + { + name: 'page_engaged_users', + period: 'week', + values: [ + { + value: 14, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 14, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Weekly Page Engaged Users', + description: + 'Weekly: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: `${PAGE_ID}/insights/page_engaged_users/week`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=week&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=week&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'week', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'page_total_actions,page_engaged_users', + period: 'week', + }); + expect(res).toEqual([ + { + description: + "Weekly: The number of clicks on your Page's contact info and call-to-action button.", + id: 'PAGE_ID/insights/page_total_actions/week', + name: 'page_total_actions', + period: 'week', + title: 'Weekly Total: total action count per Page', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 0 }, + { endTime: '2020-07-06T07:00:00+0000', value: 0 }, + ], + }, + { + description: + 'Weekly: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: 'PAGE_ID/insights/page_engaged_users/week', + name: 'page_engaged_users', + period: 'week', + title: 'Weekly Page Engaged Users', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 14 }, + { endTime: '2020-07-06T07:00:00+0000', value: 14 }, + ], + }, + ]); + }); + + it('should support days_28 period', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const PAGE_ID = 'PAGE_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'page_total_actions', + period: 'days_28', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: '28 Days Total: total action count per Page', + description: + "28 Days: The number of clicks on your Page's contact info and call-to-action button.", + id: `${PAGE_ID}/insights/page_total_actions/days_28`, + }, + { + name: 'page_engaged_users', + period: 'days_28', + values: [ + { + value: 36, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 34, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: '28 Days Page Engaged Users', + description: + '28 Days: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: `${PAGE_ID}/insights/page_engaged_users/days_28`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=days_28&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${PAGE_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=page_total_actions%2Cpage_engaged_users&period=days_28&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPageInsights({ + metric: ['page_total_actions', 'page_engaged_users'], + period: 'days_28', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'page_total_actions,page_engaged_users', + period: 'days_28', + }); + expect(res).toEqual([ + { + description: + "28 Days: The number of clicks on your Page's contact info and call-to-action button.", + id: 'PAGE_ID/insights/page_total_actions/days_28', + name: 'page_total_actions', + period: 'days_28', + title: '28 Days Total: total action count per Page', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 0 }, + { endTime: '2020-07-06T07:00:00+0000', value: 0 }, + ], + }, + { + description: + '28 Days: The number of people who engaged with your Page. Engagement includes any click or story created. (Unique Users)', + id: 'PAGE_ID/insights/page_engaged_users/days_28', + name: 'page_engaged_users', + period: 'days_28', + title: '28 Days Page Engaged Users', + values: [ + { endTime: '2020-07-05T07:00:00+0000', value: 36 }, + { endTime: '2020-07-06T07:00:00+0000', value: 34 }, + ], + }, + ]); + }); + + it('should support datePreset', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, {}); + + await client.getPageInsights({ + metric: ['page_tab_views_login_top_unique'], + period: 'day', + datePreset: 'today', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'page_tab_views_login_top_unique', + period: 'day', + date_preset: 'today', + }); + }); + + it('should support since and until', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get('/me/insights') + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, {}); + + await client.getPageInsights({ + metric: ['page_tab_views_login_top_unique'], + period: 'day', + since: new Date('2020-07-06T03:24:00Z'), + until: new Date('2020-07-07T03:24:00Z'), + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'page_tab_views_login_top_unique', + period: 'day', + since: '2020-07-06T03:24:00.000Z', + until: '2020-07-07T03:24:00.000Z', + }); + }); +}); + +describe('#getPostInsights', () => { + it('should support day period', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const POST_ID = 'POST_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get(`/${POST_ID}/insights`) + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'post_video_views_organic', + period: 'day', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Organic Video Views', + description: + 'Daily: Number of times your video was viewed for more than 3 seconds without any paid promotion. (Total Count)', + id: `${POST_ID}/insights/post_video_views_organic/day`, + }, + { + name: 'post_video_views_paid', + period: 'day', + values: [ + { + value: 0, + end_time: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + end_time: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Total Paid Video Views', + description: + 'Daily: Total number of times your video was viewed for more than 3 seconds after paid promotion.', + id: `${POST_ID}/insights/post_video_views_paid/day`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${POST_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=post_video_views_organic%2Cpost_video_views_paid&period=day&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${POST_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=post_video_views_organic%2Cpost_video_views_paid&period=day&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPostInsights(POST_ID, { + metric: ['post_video_views_organic', 'post_video_views_paid'], + period: 'day', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'post_video_views_organic,post_video_views_paid', + period: 'day', + }); + expect(res).toEqual([ + { + name: 'post_video_views_organic', + period: 'day', + values: [ + { + value: 0, + endTime: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + endTime: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Organic Video Views', + description: + 'Daily: Number of times your video was viewed for more than 3 seconds without any paid promotion. (Total Count)', + id: `${POST_ID}/insights/post_video_views_organic/day`, + }, + { + name: 'post_video_views_paid', + period: 'day', + values: [ + { + value: 0, + endTime: '2020-07-05T07:00:00+0000', + }, + { + value: 0, + endTime: '2020-07-06T07:00:00+0000', + }, + ], + title: 'Daily Total Paid Video Views', + description: + 'Daily: Total number of times your video was viewed for more than 3 seconds after paid promotion.', + id: `${POST_ID}/insights/post_video_views_paid/day`, + }, + ]); + }); + + it('should support lifetime period', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const POST_ID = 'POST_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get(`/${POST_ID}/insights`) + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, { + data: [ + { + name: 'post_engaged_users', + period: 'lifetime', + values: [ + { + value: 11, + }, + ], + title: 'Lifetime Engaged Users', + description: + 'Lifetime: The number of unique people who engaged in certain ways with your Page post, for example by commenting on, liking, sharing, or clicking upon particular elements of the post. (Unique Users)', + id: `${POST_ID}/insights/post_engaged_users/lifetime`, + }, + { + name: 'post_negative_feedback', + period: 'lifetime', + values: [ + { + value: 0, + }, + ], + title: 'Lifetime Negative Feedback', + description: + 'Lifetime: The number of times people have given negative feedback to your post. (Total Count)', + id: `${POST_ID}/insights/post_negative_feedback/lifetime`, + }, + ], + paging: { + previous: `https://graph.facebook.com/v6.0/${POST_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=post_engaged_users%2Cpost_negative_feedback&period=lifetime&since=1593673200&until=1593846000`, + next: `https://graph.facebook.com/v6.0/${POST_ID}/insights?access_token=${ACCESS_TOKEN}&pretty=0&metric=post_engaged_users%2Cpost_negative_feedback&period=lifetime&since=1594018800&until=1594191600`, + }, + }); + + const res = await client.getPostInsights(POST_ID, { + metric: ['post_engaged_users', 'post_negative_feedback'], + period: 'lifetime', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'post_engaged_users,post_negative_feedback', + period: 'lifetime', + }); + expect(res).toEqual([ + { + description: + 'Lifetime: The number of unique people who engaged in certain ways with your Page post, for example by commenting on, liking, sharing, or clicking upon particular elements of the post. (Unique Users)', + id: 'POST_ID/insights/post_engaged_users/lifetime', + name: 'post_engaged_users', + period: 'lifetime', + title: 'Lifetime Engaged Users', + values: [{ value: 11 }], + }, + { + description: + 'Lifetime: The number of times people have given negative feedback to your post. (Total Count)', + id: 'POST_ID/insights/post_negative_feedback/lifetime', + name: 'post_negative_feedback', + period: 'lifetime', + title: 'Lifetime Negative Feedback', + values: [{ value: 0 }], + }, + ]); + }); + + it('should support datePreset', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const POST_ID = 'POST_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get(`/${POST_ID}/insights`) + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, {}); + + await client.getPostInsights(POST_ID, { + metric: ['post_video_views_organic'], + period: 'day', + datePreset: 'today', + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'post_video_views_organic', + period: 'day', + date_preset: 'today', + }); + }); + + it('should support since and until', async () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + const POST_ID = 'POST_ID'; + + let receviedQuery; + nock('https://graph.facebook.com/v6.0') + .get(`/${POST_ID}/insights`) + .query((query) => { + receviedQuery = query; + return true; + }) + .reply(200, {}); + + await client.getPostInsights(POST_ID, { + metric: ['post_video_views_organic'], + period: 'day', + since: new Date('2020-07-06T03:24:00Z'), + until: new Date('2020-07-07T03:24:00Z'), + }); + + expect(receviedQuery).toEqual({ + access_token: ACCESS_TOKEN, + metric: 'post_video_views_organic', + period: 'day', + since: '2020-07-06T03:24:00.000Z', + until: '2020-07-07T03:24:00.000Z', + }); + }); +}); diff --git a/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts new file mode 100644 index 000000000..b49af2845 --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts @@ -0,0 +1,545 @@ +import MockAdapter from 'axios-mock-adapter'; + +import FacebookClient from '../FacebookClient'; + +const OBJECT_ID = '123456'; +const COMMENT_ID = '123456'; +const ACCESS_TOKEN = '1234567890'; + +const createMock = () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + const mock = new MockAdapter(client.axios); + return { client, mock }; +}; + +describe('#axios', () => { + it('should return underlying http client', () => { + const client = new FacebookClient({ + accessToken: ACCESS_TOKEN, + }); + + expect(client.axios.get).toBeDefined(); + expect(client.axios.post).toBeDefined(); + expect(client.axios.put).toBeDefined(); + expect(client.axios.delete).toBeDefined(); + }); +}); + +describe('send api', () => { + describe('#sendComment', () => { + it('should call comments api', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + let data; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + data = config.data; + return [200, reply]; + }); + + const res = await client.sendComment(OBJECT_ID, 'Hello!'); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(JSON.parse(data)).toEqual({ + message: 'Hello!', + }); + expect(res).toEqual(reply); + }); + + it('should support object with message', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + let data; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + data = config.data; + return [200, reply]; + }); + + const res = await client.sendComment(OBJECT_ID, { message: 'Hello!' }); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(JSON.parse(data)).toEqual({ + message: 'Hello!', + }); + expect(res).toEqual(reply); + }); + + it('should support object with attachmentId', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + let data; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + data = config.data; + return [200, reply]; + }); + + const res = await client.sendComment(OBJECT_ID, { + attachmentId: '', + }); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(JSON.parse(data)).toEqual({ + attachment_id: '', + }); + expect(res).toEqual(reply); + }); + + it('should support object with attachmentShareUrl', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + let data; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + data = config.data; + return [200, reply]; + }); + + const res = await client.sendComment(OBJECT_ID, { + attachmentShareUrl: 'https://example.com/img.gif', + }); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(JSON.parse(data)).toEqual({ + attachment_share_url: 'https://example.com/img.gif', + }); + expect(res).toEqual(reply); + }); + + it('should support object with attachmentUrl', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + let data; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + data = config.data; + return [200, reply]; + }); + + const res = await client.sendComment(OBJECT_ID, { + attachmentUrl: 'https://example.com/img.jpg', + }); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(JSON.parse(data)).toEqual({ + attachment_url: 'https://example.com/img.jpg', + }); + expect(res).toEqual(reply); + }); + }); + + describe('#sendLike', () => { + it('should call likes api', async () => { + const { client, mock } = createMock(); + + const reply = { success: true }; + + let url; + let params; + mock.onPost().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.sendLike(OBJECT_ID); + + expect(url).toEqual(`/${OBJECT_ID}/likes`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(res).toEqual(reply); + }); + }); + + describe('#getComment', () => { + it('should call comments api', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getComment(COMMENT_ID); + + expect(url).toEqual(`/${COMMENT_ID}`); + expect(params).toEqual({ + access_token: ACCESS_TOKEN, + fields: 'id,message,created_time', + }); + expect(res).toEqual(reply); + }); + + it('should support other options', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + canReplyPrivately: true, + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getComment(COMMENT_ID, { + fields: ['can_reply_privately'], + }); + + expect(url).toEqual(`/${COMMENT_ID}`); + expect(params).toEqual({ + access_token: ACCESS_TOKEN, + fields: 'can_reply_privately', + }); + expect(res).toEqual(reply); + }); + }); + + describe('#getComments', () => { + it('should call comments api', async () => { + const { client, mock } = createMock(); + + const reply = { + data: [ + { + created_time: '2020-07-02T15:10:26+0000', + message: 'hello world!', + id: '128744825536666_128747222206666', + }, + ], + paging: { + cursors: { + before: 'MQZDZD', + after: 'MQZDZD', + }, + }, + summary: { + order: 'ranked', + total_count: 1, + can_comment: true, + }, + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getComments(OBJECT_ID); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ + access_token: ACCESS_TOKEN, + fields: 'id,message,created_time', + filter: undefined, + limit: undefined, + order: undefined, + summary: undefined, + }); + expect(res).toEqual({ + data: [ + { + createdTime: '2020-07-02T15:10:26+0000', + message: 'hello world!', + id: '128744825536666_128747222206666', + }, + ], + paging: { + cursors: { + before: 'MQZDZD', + after: 'MQZDZD', + }, + }, + summary: { + order: 'ranked', + totalCount: 1, + canComment: true, + }, + }); + }); + + it('should support other options', async () => { + const { client, mock } = createMock(); + + const reply = { + data: [ + { + created_time: '2020-07-02T15:10:26+0000', + message: 'hello world!', + id: '128744825536666_128747222206666', + can_reply_privately: true, + }, + ], + paging: { + cursors: { + before: 'MQZDZD', + after: 'MQZDZD', + }, + }, + summary: { + order: 'reverse_chronological', + total_count: 1, + can_comment: true, + }, + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getComments(OBJECT_ID, { + summary: true, + filter: 'toplevel', + order: 'reverse_chronological', + fields: ['can_reply_privately'], + }); + + expect(url).toEqual(`/${OBJECT_ID}/comments`); + expect(params).toEqual({ + summary: 'true', + filter: 'toplevel', + order: 'reverse_chronological', + fields: 'can_reply_privately', + access_token: ACCESS_TOKEN, + }); + expect(res).toEqual({ + data: [ + { + createdTime: '2020-07-02T15:10:26+0000', + message: 'hello world!', + id: '128744825536666_128747222206666', + canReplyPrivately: true, + }, + ], + paging: { + cursors: { + before: 'MQZDZD', + after: 'MQZDZD', + }, + }, + summary: { + order: 'reverse_chronological', + totalCount: 1, + canComment: true, + }, + }); + }); + }); + + describe('#getLikes', () => { + it('should call likes api', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + likes: { + data: [ + { + name: 'Bill the Cat', + id: '155111347875779', + created_time: '2017-06-18T18:21:04+0000', + }, + { + name: 'Calvin and Hobbes', + id: '257573197608192', + created_time: '2017-06-18T18:21:02+0000', + }, + { + name: "Berkeley Breathed's Bloom County", + id: '108793262484769', + created_time: '2017-06-18T18:20:58+0000', + }, + ], + paging: { + cursors: { + before: 'Nzc0Njg0MTQ3OAZDZD', + after: 'NTcxODc1ODk2NgZDZD', + }, + next: 'https://graph.facebook.com/vX.X/me/likes?access_token=user-access-token&pretty=0&summary=true&limit=25&after=NTcxODc1ODk2NgZDZD', + }, + }, + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getLikes(OBJECT_ID); + + expect(url).toEqual(`/${OBJECT_ID}/likes`); + expect(params).toEqual({ access_token: ACCESS_TOKEN }); + expect(res).toEqual({ + data: [ + { + name: 'Bill the Cat', + id: '155111347875779', + createdTime: '2017-06-18T18:21:04+0000', + }, + { + name: 'Calvin and Hobbes', + id: '257573197608192', + createdTime: '2017-06-18T18:21:02+0000', + }, + { + name: "Berkeley Breathed's Bloom County", + id: '108793262484769', + createdTime: '2017-06-18T18:20:58+0000', + }, + ], + paging: { + cursors: { + before: 'Nzc0Njg0MTQ3OAZDZD', + after: 'NTcxODc1ODk2NgZDZD', + }, + next: 'https://graph.facebook.com/vX.X/me/likes?access_token=user-access-token&pretty=0&summary=true&limit=25&after=NTcxODc1ODk2NgZDZD', + }, + }); + }); + + it('should support other options', async () => { + const { client, mock } = createMock(); + + const reply = { + id: '1809938745705498_1809941802371859', + likes: { + data: [ + { + name: 'Bill the Cat', + id: '155111347875779', + created_time: '2017-06-18T18:21:04+0000', + }, + { + name: 'Calvin and Hobbes', + id: '257573197608192', + created_time: '2017-06-18T18:21:02+0000', + }, + { + name: "Berkeley Breathed's Bloom County", + id: '108793262484769', + created_time: '2017-06-18T18:20:58+0000', + }, + ], + paging: { + cursors: { + before: 'Nzc0Njg0MTQ3OAZDZD', + after: 'NTcxODc1ODk2NgZDZD', + }, + next: 'https://graph.facebook.com/vX.X/me/likes?access_token=user-access-token&pretty=0&summary=true&limit=25&after=NTcxODc1ODk2NgZDZD', + }, + summary: { + total_count: 136, + }, + }, + }; + + let url; + let params; + mock.onGet().reply((config) => { + url = config.url; + params = config.params; + return [200, reply]; + }); + + const res = await client.getLikes(OBJECT_ID, { summary: true }); + + expect(url).toEqual(`/${OBJECT_ID}/likes`); + expect(params).toEqual({ + summary: 'true', + access_token: ACCESS_TOKEN, + }); + expect(res).toEqual({ + data: [ + { + name: 'Bill the Cat', + id: '155111347875779', + createdTime: '2017-06-18T18:21:04+0000', + }, + { + name: 'Calvin and Hobbes', + id: '257573197608192', + createdTime: '2017-06-18T18:21:02+0000', + }, + { + name: "Berkeley Breathed's Bloom County", + id: '108793262484769', + createdTime: '2017-06-18T18:20:58+0000', + }, + ], + paging: { + cursors: { + before: 'Nzc0Njg0MTQ3OAZDZD', + after: 'NTcxODc1ODk2NgZDZD', + }, + next: 'https://graph.facebook.com/vX.X/me/likes?access_token=user-access-token&pretty=0&summary=true&limit=25&after=NTcxODc1ODk2NgZDZD', + }, + summary: { + totalCount: 136, + }, + }); + }); + }); +}); diff --git a/packages/bottender-facebook/src/__tests__/FacebookConnector.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookConnector.spec.ts new file mode 100644 index 000000000..587de8e38 --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookConnector.spec.ts @@ -0,0 +1,399 @@ +import { MessengerContext, MessengerEvent } from 'bottender'; + +import FacebookClient from '../FacebookClient'; +import FacebookConnector from '../FacebookConnector'; +import FacebookEvent from '../FacebookEvent'; + +jest.mock('warning'); + +const ACCESS_TOKEN = 'FAKE_TOKEN'; +const APP_SECRET = 'FAKE_SECRET'; +const VERIFY_TOKEN = 'VERIFY_TOKEN'; + +const request = { + body: { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + seq: 339979, + text: 'text', + }, + }, + ], + }, + ], + }, +}; + +const differentPagebatchRequest = { + body: { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + seq: 339979, + text: 'test 1', + }, + }, + ], + }, + { + id: '189538289069256', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '189538289069256', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a656', + seq: 339979, + text: 'test 2', + }, + }, + ], + }, + ], + }, +}; + +const samePageBatchRequest = { + body: { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + seq: 339979, + text: 'test 1', + }, + }, + ], + }, + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a656', + seq: 339979, + text: 'test 2', + }, + }, + ], + }, + ], + }, +}; + +const standbyRequest = { + body: { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + standby: [ + { + sender: { + id: '', + }, + recipient: { + id: '', + }, + + // FIXME: standby is still beta + // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/standby + /* ... */ + }, + ], + }, + ], + }, +}; + +const commentAddRequest = { + body: { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + changes: [ + { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }, + }, + ], + }, + { + id: '', + time: 1458692752478, + changes: [ + { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }, + }, + ], + }, + ], + }, +}; + +function setup({ + accessToken = ACCESS_TOKEN, + appSecret = APP_SECRET, + mapPageToAccessToken, + verifyToken = VERIFY_TOKEN, +}: { + accessToken?: string; + appSecret?: string; + mapPageToAccessToken?: (pageId: string) => Promise; + verifyToken?: string; +} = {}) { + const mockGraphAPIClient = { + getUserProfile: jest.fn(), + }; + + FacebookClient.connect = jest.fn(); + FacebookClient.connect.mockReturnValue(mockGraphAPIClient); + + return { + mockGraphAPIClient, + connector: new FacebookConnector({ + accessToken, + appSecret, + mapPageToAccessToken, + verifyToken, + }), + }; +} + +describe('#mapRequestToEvents', () => { + it('should map request to FacebookEvents', () => { + const { connector } = setup(); + const events = connector.mapRequestToEvents(request.body); + + expect(events).toHaveLength(1); + expect(events[0]).toBeInstanceOf(MessengerEvent); + expect(events[0].pageId).toBe('1895382890692545'); + }); + + it('should work with batch entry from same page', () => { + const { connector } = setup(); + const events = connector.mapRequestToEvents(samePageBatchRequest.body); + + expect(events).toHaveLength(2); + expect(events[0]).toBeInstanceOf(MessengerEvent); + expect(events[0].pageId).toBe('1895382890692545'); + expect(events[1]).toBeInstanceOf(MessengerEvent); + expect(events[1].pageId).toBe('1895382890692545'); + }); + + it('should work with batch entry from different page', () => { + const { connector } = setup(); + const events = connector.mapRequestToEvents(differentPagebatchRequest.body); + + expect(events).toHaveLength(2); + expect(events[0]).toBeInstanceOf(MessengerEvent); + expect(events[0].pageId).toBe('1895382890692545'); + expect(events[1]).toBeInstanceOf(MessengerEvent); + expect(events[1].pageId).toBe('189538289069256'); + }); + + it('should map request to standby FacebookEvents', () => { + const { connector } = setup(); + const events = connector.mapRequestToEvents(standbyRequest.body); + + expect(events).toHaveLength(1); + expect(events[0]).toBeInstanceOf(MessengerEvent); + expect(events[0].pageId).toBe(''); + expect(events[0].isStandby).toBe(true); + }); + + it('should map request to changes FacebookEvents', () => { + const { connector } = setup(); + const events = connector.mapRequestToEvents(commentAddRequest.body); + + expect(events).toHaveLength(2); + expect(events[0]).toBeInstanceOf(FacebookEvent); + expect(events[0].pageId).toBe(''); + expect(events[1]).toBeInstanceOf(FacebookEvent); + expect(events[1].pageId).toBe(''); + }); + + it('should be filtered if body is not messaging or standby', () => { + const otherRequest = { + body: { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + other: [ + { + sender: { + id: '', + }, + recipient: { + id: '', + }, + }, + ], + }, + ], + }, + }; + const { connector } = setup(); + const events = connector.mapRequestToEvents(otherRequest.body); + + expect(events).toHaveLength(0); + }); + + it('should pass pageId when the body.object is `page`', () => { + const { connector } = setup(); + const commentAddRequestByPage = { + body: { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + changes: [ + { + field: 'feed', + value: { + from: { + id: '', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }, + }, + ], + }, + ], + }, + }; + const events = connector.mapRequestToEvents(commentAddRequestByPage.body); + expect(events[0].pageId).toBe(''); + expect(events[0].isSentByPage).toBe(true); + }); +}); + +describe('#createContext', () => { + it('should create MessengerContext', async () => { + const { connector } = setup(); + const event = { + rawEvent: { + recipient: { + id: 'anyPageId', + }, + }, + }; + const session = {}; + + const context = await connector.createContext({ + event, + session, + }); + + expect(context).toBeDefined(); + expect(context).toBeInstanceOf(MessengerContext); + }); + + it('should create MessengerContext and has customAccessToken', async () => { + const mapPageToAccessToken = jest.fn(() => Promise.resolve('anyToken')); + const { connector } = setup({ mapPageToAccessToken }); + const event = { + pageId: 'anyPageId', + }; + const session = {}; + + const context = await connector.createContext({ + event, + session, + }); + + expect(context).toBeDefined(); + expect(context.accessToken).toBe('anyToken'); + }); +}); diff --git a/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts new file mode 100644 index 000000000..594988201 --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts @@ -0,0 +1,379 @@ +import FacebookContext from '../FacebookContext'; +import FacebookEvent from '../FacebookEvent'; +import { FacebookRawEvent } from '../FacebookTypes'; + +jest.mock('warning'); + +const userRawEvent: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'OK', + }, +}; + +const sentByPageRawEvent: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '137542570280222', + name: 'page', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'OK', + }, +}; + +const postAdd: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '156407155145863', + name: 'Cinderella Hoover', + }, + item: 'post', + postId: '1353269864728879_1611108832278313', + verb: 'add', + createdTime: 1520544814, + isHidden: false, + message: "It's Thursday and I want to eat cake.", + }, +}; + +const commentAdd: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }, +}; + +const setup = ({ + rawEvent = userRawEvent, + pageId, +}: { + rawEvent?: FacebookRawEvent; + pageId?: string; +} = {}) => { + const client = { + sendText: jest.fn(), + sendMessage: jest.fn(), + sendComment: jest.fn(), + sendLike: jest.fn(), + getComment: jest.fn(), + getLikes: jest.fn(), + }; + + const context = new FacebookContext({ + client, + event: new FacebookEvent(rawEvent, { pageId }), + }); + return { + client, + context, + }; +}; + +describe('#sendText', () => { + it('should work with posts', async () => { + const { context, client } = setup({ + rawEvent: postAdd, + }); + + await context.sendText('Private Reply!'); + + expect(client.sendText).toBeCalledWith( + { postId: '1353269864728879_1611108832278313' }, + 'Private Reply!' + ); + }); + + it('should work with comments', async () => { + const { context, client } = setup({ + rawEvent: commentAdd, + }); + + await context.sendText('Private Reply!'); + + expect(client.sendText).toBeCalledWith( + { commentId: '139560936744456_139620233405726' }, + 'Private Reply!' + ); + }); +}); + +describe('#sendMessage', () => { + it('should work with posts', async () => { + const { context, client } = setup({ + rawEvent: postAdd, + }); + + await context.sendMessage({ + text: 'Private Reply!', + }); + + expect(client.sendMessage).toBeCalledWith( + { postId: '1353269864728879_1611108832278313' }, + { + text: 'Private Reply!', + } + ); + }); + + it('should work with comments', async () => { + const { context, client } = setup({ + rawEvent: commentAdd, + }); + + await context.sendMessage({ + text: 'Private Reply!', + }); + + expect(client.sendMessage).toBeCalledWith( + { commentId: '139560936744456_139620233405726' }, + { + text: 'Private Reply!', + } + ); + }); +}); + +describe('#sendComment', () => { + it('should call client with comment id, when incoming comment is a first layer comment', async () => { + const { context, client } = setup({ + rawEvent: { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '137542570280222_139560936744456', + createdTime: 1511951015, + message: 'OK', + }, + }, + }); + + await context.sendComment('Public Reply!'); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139620233405726', + 'Public Reply!' + ); + }); + + it('should call client with parent id, when the incoming comment is not a first layer comment', async () => { + const { context, client } = setup(); + + await context.sendComment('Public Reply!'); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139562213411528', + 'Public Reply!' + ); + }); + + it('should work with posts', async () => { + const { context, client } = setup({ + rawEvent: postAdd, + }); + + await context.sendComment('Public Reply!'); + + expect(client.sendComment).toBeCalledWith( + '1353269864728879_1611108832278313', + 'Public Reply!' + ); + }); + + it('should not reply to page itself', async () => { + const { context, client } = setup({ + rawEvent: sentByPageRawEvent, + pageId: '137542570280222', + }); + + await context.sendComment('Public Reply!'); + + expect(client.sendComment).not.toBeCalled(); + }); + + it('should call client with message', async () => { + const { context, client } = setup(); + + await context.sendComment({ message: 'Public Reply!' }); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139562213411528', + { message: 'Public Reply!' } + ); + }); + + it('should call client with attachmentId', async () => { + const { context, client } = setup(); + + await context.sendComment({ attachmentId: '' }); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139562213411528', + { attachmentId: '' } + ); + }); + + it('should call client with attachmentShareUrl', async () => { + const { context, client } = setup(); + + await context.sendComment({ + attachmentShareUrl: 'https://example.com/img.gif', + }); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139562213411528', + { attachmentShareUrl: 'https://example.com/img.gif' } + ); + }); + + it('should call client with attachmentUrl', async () => { + const { context, client } = setup(); + + await context.sendComment({ + attachmentUrl: 'https://example.com/img.jpg', + }); + + expect(client.sendComment).toBeCalledWith( + '139560936744456_139562213411528', + { attachmentUrl: 'https://example.com/img.jpg' } + ); + }); +}); + +describe('#sendLike', () => { + it('should call client with comment id', async () => { + const { context, client } = setup(); + + await context.sendLike(); + + expect(client.sendLike).toBeCalledWith('139560936744456_139620233405726'); + }); + + it('should work with posts', async () => { + const { context, client } = setup({ + rawEvent: postAdd, + }); + + await context.sendLike(); + + expect(client.sendLike).toBeCalledWith('1353269864728879_1611108832278313'); + }); +}); + +describe('#getComment', () => { + it('should call client with comment id', async () => { + const { context, client } = setup(); + + await context.getComment(); + + expect(client.getComment).toBeCalledWith( + '139560936744456_139620233405726', + { fields: ['id', 'message', 'created_time'] } + ); + }); + + it('should call client with fields', async () => { + const { context, client } = setup(); + + await context.getComment({ fields: ['message_tags'] }); + + expect(client.getComment).toBeCalledWith( + '139560936744456_139620233405726', + { fields: ['message_tags'] } + ); + }); +}); + +describe('#canReplyPrivately', () => { + it('should be false when is not commet', async () => { + const { context, client } = setup(); + + client.getComment.mockResolvedValue(null); + + expect(await context.canReplyPrivately()).toBe(false); + + expect(client.getComment).toBeCalledWith( + '139560936744456_139620233405726', + { + fields: ['can_reply_privately'], + } + ); + }); + + it('should be true when can_reply_privately: true', async () => { + const { context, client } = setup(); + + client.getComment.mockResolvedValue({ + canReplyPrivately: true, + }); + + expect(await context.canReplyPrivately()).toBe(true); + }); + + it('should be false when can_reply_privately: false', async () => { + const { context, client } = setup(); + + client.getComment.mockResolvedValue({ + canReplyPrivately: false, + }); + + expect(await context.canReplyPrivately()).toBe(false); + }); +}); + +describe('#getLikes', () => { + it('should call client with comment id', async () => { + const { context, client } = setup(); + + await context.getLikes(); + + expect(client.getLikes).toBeCalledWith( + '139560936744456_139620233405726', + undefined + ); + }); + + it('should call client with summary: true', async () => { + const { context, client } = setup(); + + await context.getLikes({ summary: true }); + + expect(client.getLikes).toBeCalledWith('139560936744456_139620233405726', { + summary: true, + }); + }); +}); diff --git a/packages/bottender-facebook/src/__tests__/FacebookEvent.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookEvent.spec.ts new file mode 100644 index 000000000..e6b10b49c --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/FacebookEvent.spec.ts @@ -0,0 +1,768 @@ +import FacebookEvent from '../FacebookEvent'; +import { ChangesEntry, FacebookRawEvent } from '../FacebookTypes'; + +const statusAdd: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'status', + postId: '137542570280222_139610053406744', + verb: 'add', + published: 1, + createdTime: 1511949030, + message: 'test', + }, +}; + +const statusEdited: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'status', + postId: '137542570280222_139560936744456', + verb: 'edited', + published: 1, + createdTime: 1511927135, + message: '1234', + }, +}; + +const postRemove: FacebookRawEvent = { + field: 'feed', + value: { + recipientId: '137542570280222', + from: { + id: '139560936744123', + }, + item: 'post', + postId: '137542570280222_139610053406744', + verb: 'remove', + createdTime: 1511949068, + }, +}; + +const commentAdd: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }, +}; + +const commentEdited: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139597043408045', + postId: '137542570280222_139560936744456', + verb: 'edited', + parentId: '137542570280222_139560936744456', + createdTime: 1511948891, + message: 'Great', + }, +}; + +const commentRemove: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '139560936744123', + }, + parentId: '137542570280222_139560936744456', + commentId: '139560936744456_139597043408045', + postId: '137542570280222_139560936744456', + verb: 'remove', + item: 'comment', + createdTime: 1511948944, + }, +}; + +const pageLikeAdd: FacebookRawEvent = { + value: { + item: 'like', + verb: 'add', + }, + field: 'feed', +}; + +const timestamp = 1511948944; + +const reactionAdd: FacebookRawEvent = { + field: 'feed', + value: { + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'add', + item: 'reaction', + createdTime: 1511948636, + }, +}; + +const reactionEdit: FacebookRawEvent = { + field: 'feed', + value: { + reactionType: 'sad', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'edit', + item: 'reaction', + createdTime: 1511948713, + }, +}; + +const reactionRemove: FacebookRawEvent = { + field: 'feed', + value: { + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'remove', + item: 'reaction', + createdTime: 1511948666, + }, +}; + +const postReaction: FacebookRawEvent = { + field: 'feed', + value: { + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '137542570280222_139560936744456', + postId: '137542570280222_139560936744456', + verb: 'add', + item: 'reaction', + createdTime: 1568176139, + }, +}; + +const pageId = '137542570280111'; + +const sentByPage: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '137542570280111', + name: 'Bottender', + }, + item: 'comment', + commentId: '139560936755999_140077616693321', + postId: '137542570280111_139560936755999', + verb: 'add', + parentId: '139560936755999_140077593359990', + createdTime: 1512121727, + message: 'Public reply!', + }, +}; + +const videoCommentAdd: FacebookRawEvent = { + field: 'feed', + value: { + from: { + id: '1440971912666228', + name: 'Hsuan-Yih Shawn Chu', + }, + item: 'comment', + commentId: '139560936755999_140077616693321', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '137542570280222_139560936744456', + createdTime: 1545829217, + post: { + type: 'video', + updatedTime: '2018-12-26T13:00:17+0000', + promotionStatus: 'inactive', + permalinkUrl: 'https://www.facebook.com/xxx/posts/1111', + id: '137542570280222_139560936744456', + statusType: 'added_video', + isPublished: true, + }, + message: '100', + }, +}; + +it('#isFeed', () => { + expect(new FacebookEvent(statusAdd).isFeed).toEqual(true); + expect(new FacebookEvent(statusEdited).isFeed).toEqual(true); + expect(new FacebookEvent(postRemove).isFeed).toEqual(true); + expect(new FacebookEvent(commentAdd).isFeed).toEqual(true); + expect(new FacebookEvent(videoCommentAdd).isFeed).toEqual(true); + expect(new FacebookEvent(commentEdited).isFeed).toEqual(true); + expect(new FacebookEvent(commentRemove).isFeed).toEqual(true); + expect(new FacebookEvent(reactionAdd).isFeed).toEqual(true); + expect(new FacebookEvent(reactionEdit).isFeed).toEqual(true); + expect(new FacebookEvent(reactionRemove).isFeed).toEqual(true); + expect(new FacebookEvent(postReaction).isFeed).toEqual(true); + expect(new FacebookEvent(sentByPage).isFeed).toEqual(true); +}); + +it('#isStatus', () => { + expect(new FacebookEvent(statusAdd).isStatus).toEqual(true); + expect(new FacebookEvent(statusEdited).isStatus).toEqual(true); + expect(new FacebookEvent(postRemove).isStatus).toEqual(false); + expect(new FacebookEvent(commentAdd).isStatus).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isStatus).toEqual(false); + expect(new FacebookEvent(commentEdited).isStatus).toEqual(false); + expect(new FacebookEvent(commentRemove).isStatus).toEqual(false); + expect(new FacebookEvent(reactionAdd).isStatus).toEqual(false); + expect(new FacebookEvent(reactionEdit).isStatus).toEqual(false); + expect(new FacebookEvent(reactionRemove).isStatus).toEqual(false); + expect(new FacebookEvent(postReaction).isStatus).toEqual(false); + expect(new FacebookEvent(sentByPage).isStatus).toEqual(false); +}); + +it('#isStatusAdd', () => { + expect(new FacebookEvent(statusAdd).isStatusAdd).toEqual(true); + expect(new FacebookEvent(statusEdited).isStatusAdd).toEqual(false); + expect(new FacebookEvent(postRemove).isStatusAdd).toEqual(false); + expect(new FacebookEvent(commentAdd).isStatusAdd).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isStatusAdd).toEqual(false); + expect(new FacebookEvent(commentEdited).isStatusAdd).toEqual(false); + expect(new FacebookEvent(commentRemove).isStatusAdd).toEqual(false); + expect(new FacebookEvent(reactionAdd).isStatusAdd).toEqual(false); + expect(new FacebookEvent(reactionEdit).isStatusAdd).toEqual(false); + expect(new FacebookEvent(reactionRemove).isStatusAdd).toEqual(false); + expect(new FacebookEvent(postReaction).isStatusAdd).toEqual(false); + expect(new FacebookEvent(sentByPage).isStatusAdd).toEqual(false); +}); + +it('#isStatusEdited', () => { + expect(new FacebookEvent(statusAdd).isStatusEdited).toEqual(false); + expect(new FacebookEvent(statusEdited).isStatusEdited).toEqual(true); + expect(new FacebookEvent(postRemove).isStatusEdited).toEqual(false); + expect(new FacebookEvent(commentAdd).isStatusEdited).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isStatusEdited).toEqual(false); + expect(new FacebookEvent(commentEdited).isStatusEdited).toEqual(false); + expect(new FacebookEvent(commentRemove).isStatusEdited).toEqual(false); + expect(new FacebookEvent(reactionAdd).isStatusEdited).toEqual(false); + expect(new FacebookEvent(reactionEdit).isStatusEdited).toEqual(false); + expect(new FacebookEvent(reactionRemove).isStatusEdited).toEqual(false); + expect(new FacebookEvent(postReaction).isStatusEdited).toEqual(false); + expect(new FacebookEvent(sentByPage).isStatusEdited).toEqual(false); +}); + +it('#status', () => { + expect(new FacebookEvent(statusAdd).status).toEqual({ + from: { + id: '139560936744123', + name: 'user', + }, + item: 'status', + postId: '137542570280222_139610053406744', + verb: 'add', + published: 1, + createdTime: 1511949030, + message: 'test', + }); + expect(new FacebookEvent(statusEdited).status).toEqual({ + from: { + id: '139560936744123', + name: 'user', + }, + item: 'status', + postId: '137542570280222_139560936744456', + verb: 'edited', + published: 1, + createdTime: 1511927135, + message: '1234', + }); + expect(new FacebookEvent(postRemove).status).toEqual(null); + expect(new FacebookEvent(commentAdd).status).toEqual(null); + expect(new FacebookEvent(commentAdd).status).toEqual(null); + expect(new FacebookEvent(videoCommentAdd).status).toEqual(null); + expect(new FacebookEvent(commentEdited).status).toEqual(null); + expect(new FacebookEvent(commentRemove).status).toEqual(null); + expect(new FacebookEvent(reactionAdd).status).toEqual(null); + expect(new FacebookEvent(reactionEdit).status).toEqual(null); + expect(new FacebookEvent(reactionRemove).status).toEqual(null); + expect(new FacebookEvent(postReaction).status).toEqual(null); + expect(new FacebookEvent(sentByPage).status).toEqual(null); +}); + +it('#isPost', () => { + expect(new FacebookEvent(statusAdd).isPost).toEqual(false); + expect(new FacebookEvent(statusEdited).isPost).toEqual(false); + expect(new FacebookEvent(postRemove).isPost).toEqual(true); + expect(new FacebookEvent(commentAdd).isPost).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isPost).toEqual(false); + expect(new FacebookEvent(commentEdited).isPost).toEqual(false); + expect(new FacebookEvent(commentRemove).isPost).toEqual(false); + expect(new FacebookEvent(reactionAdd).isPost).toEqual(false); + expect(new FacebookEvent(reactionEdit).isPost).toEqual(false); + expect(new FacebookEvent(reactionRemove).isPost).toEqual(false); + expect(new FacebookEvent(postReaction).isPost).toEqual(false); + expect(new FacebookEvent(sentByPage).isPost).toEqual(false); +}); + +it('#isPostRemove', () => { + expect(new FacebookEvent(statusAdd).isPostRemove).toEqual(false); + expect(new FacebookEvent(statusEdited).isPostRemove).toEqual(false); + expect(new FacebookEvent(postRemove).isPostRemove).toEqual(true); + expect(new FacebookEvent(commentAdd).isPostRemove).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isPostRemove).toEqual(false); + expect(new FacebookEvent(commentEdited).isPostRemove).toEqual(false); + expect(new FacebookEvent(commentRemove).isPostRemove).toEqual(false); + expect(new FacebookEvent(reactionAdd).isPostRemove).toEqual(false); + expect(new FacebookEvent(reactionEdit).isPostRemove).toEqual(false); + expect(new FacebookEvent(reactionRemove).isPostRemove).toEqual(false); + expect(new FacebookEvent(postReaction).isPostRemove).toEqual(false); + expect(new FacebookEvent(sentByPage).isPostRemove).toEqual(false); +}); + +it('#post', () => { + expect(new FacebookEvent(statusAdd).post).toEqual(null); + expect(new FacebookEvent(statusEdited).post).toEqual(null); + expect(new FacebookEvent(postRemove).post).toEqual({ + recipientId: '137542570280222', + from: { + id: '139560936744123', + }, + item: 'post', + postId: '137542570280222_139610053406744', + verb: 'remove', + createdTime: 1511949068, + }); + expect(new FacebookEvent(commentAdd).post).toEqual(null); + expect(new FacebookEvent(videoCommentAdd).post).toEqual(null); + expect(new FacebookEvent(commentEdited).post).toEqual(null); + expect(new FacebookEvent(commentRemove).post).toEqual(null); + expect(new FacebookEvent(reactionAdd).post).toEqual(null); + expect(new FacebookEvent(reactionEdit).post).toEqual(null); + expect(new FacebookEvent(reactionRemove).post).toEqual(null); + expect(new FacebookEvent(postReaction).post).toEqual(null); + expect(new FacebookEvent(sentByPage).post).toEqual(null); +}); + +it('#isComment', () => { + expect(new FacebookEvent(statusAdd).isComment).toEqual(false); + expect(new FacebookEvent(statusEdited).isComment).toEqual(false); + expect(new FacebookEvent(postRemove).isComment).toEqual(false); + expect(new FacebookEvent(commentAdd).isComment).toEqual(true); + expect(new FacebookEvent(videoCommentAdd).isComment).toEqual(true); + expect(new FacebookEvent(commentEdited).isComment).toEqual(true); + expect(new FacebookEvent(commentRemove).isComment).toEqual(true); + expect(new FacebookEvent(reactionAdd).isComment).toEqual(false); + expect(new FacebookEvent(reactionEdit).isComment).toEqual(false); + expect(new FacebookEvent(reactionRemove).isComment).toEqual(false); + expect(new FacebookEvent(postReaction).isComment).toEqual(false); + expect(new FacebookEvent(sentByPage).isComment).toEqual(true); +}); + +it('#isCommentAdd', () => { + expect(new FacebookEvent(statusAdd).isCommentAdd).toEqual(false); + expect(new FacebookEvent(statusEdited).isCommentAdd).toEqual(false); + expect(new FacebookEvent(postRemove).isCommentAdd).toEqual(false); + expect(new FacebookEvent(commentAdd).isCommentAdd).toEqual(true); + expect(new FacebookEvent(videoCommentAdd).isCommentAdd).toEqual(true); + expect(new FacebookEvent(commentEdited).isCommentAdd).toEqual(false); + expect(new FacebookEvent(commentRemove).isCommentAdd).toEqual(false); + expect(new FacebookEvent(reactionAdd).isCommentAdd).toEqual(false); + expect(new FacebookEvent(reactionEdit).isCommentAdd).toEqual(false); + expect(new FacebookEvent(reactionRemove).isCommentAdd).toEqual(false); + expect(new FacebookEvent(postReaction).isCommentAdd).toEqual(false); + expect(new FacebookEvent(sentByPage).isCommentAdd).toEqual(true); +}); + +it('#isCommentEdited', () => { + expect(new FacebookEvent(statusAdd).isCommentEdited).toEqual(false); + expect(new FacebookEvent(statusEdited).isCommentEdited).toEqual(false); + expect(new FacebookEvent(postRemove).isCommentEdited).toEqual(false); + expect(new FacebookEvent(commentAdd).isCommentEdited).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isCommentEdited).toEqual(false); + expect(new FacebookEvent(commentEdited).isCommentEdited).toEqual(true); + expect(new FacebookEvent(commentRemove).isCommentEdited).toEqual(false); + expect(new FacebookEvent(reactionAdd).isCommentEdited).toEqual(false); + expect(new FacebookEvent(reactionEdit).isCommentEdited).toEqual(false); + expect(new FacebookEvent(reactionRemove).isCommentEdited).toEqual(false); + expect(new FacebookEvent(postReaction).isCommentEdited).toEqual(false); + expect(new FacebookEvent(sentByPage).isCommentEdited).toEqual(false); +}); + +it('#isCommentRemove', () => { + expect(new FacebookEvent(statusAdd).isCommentRemove).toEqual(false); + expect(new FacebookEvent(statusEdited).isCommentRemove).toEqual(false); + expect(new FacebookEvent(postRemove).isCommentRemove).toEqual(false); + expect(new FacebookEvent(commentAdd).isCommentRemove).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isCommentRemove).toEqual(false); + expect(new FacebookEvent(commentEdited).isCommentRemove).toEqual(false); + expect(new FacebookEvent(commentRemove).isCommentRemove).toEqual(true); + expect(new FacebookEvent(reactionAdd).isCommentRemove).toEqual(false); + expect(new FacebookEvent(reactionEdit).isCommentRemove).toEqual(false); + expect(new FacebookEvent(reactionRemove).isCommentRemove).toEqual(false); + expect(new FacebookEvent(postReaction).isCommentRemove).toEqual(false); + expect(new FacebookEvent(sentByPage).isCommentRemove).toEqual(false); +}); + +it('#isFirstLayerComment', () => { + expect(new FacebookEvent(statusAdd).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(statusEdited).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(postRemove).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(commentAdd).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isFirstLayerComment).toEqual(true); + expect(new FacebookEvent(commentEdited).isFirstLayerComment).toEqual(true); + expect(new FacebookEvent(commentRemove).isFirstLayerComment).toEqual(true); + expect(new FacebookEvent(reactionAdd).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(reactionEdit).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(reactionRemove).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(postReaction).isFirstLayerComment).toEqual(false); + expect(new FacebookEvent(sentByPage).isFirstLayerComment).toEqual(false); +}); + +it('#comment', () => { + expect(new FacebookEvent(statusAdd).comment).toEqual(null); + expect(new FacebookEvent(statusEdited).comment).toEqual(null); + expect(new FacebookEvent(postRemove).comment).toEqual(null); + expect(new FacebookEvent(commentAdd).comment).toEqual({ + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139620233405726', + postId: '137542570280222_139560936744456', + verb: 'add', + parentId: '139560936744456_139562213411528', + createdTime: 1511951015, + message: 'Good', + }); + expect(new FacebookEvent(videoCommentAdd).comment).toEqual({ + commentId: '139560936755999_140077616693321', + createdTime: 1545829217, + from: { + id: '1440971912666228', + name: 'Hsuan-Yih Shawn Chu', + }, + item: 'comment', + message: '100', + parentId: '137542570280222_139560936744456', + post: { + id: '137542570280222_139560936744456', + isPublished: true, + permalinkUrl: 'https://www.facebook.com/xxx/posts/1111', + promotionStatus: 'inactive', + statusType: 'added_video', + type: 'video', + updatedTime: '2018-12-26T13:00:17+0000', + }, + postId: '137542570280222_139560936744456', + verb: 'add', + }); + expect(new FacebookEvent(commentEdited).comment).toEqual({ + from: { + id: '139560936744123', + name: 'user', + }, + item: 'comment', + commentId: '139560936744456_139597043408045', + postId: '137542570280222_139560936744456', + verb: 'edited', + parentId: '137542570280222_139560936744456', + createdTime: 1511948891, + message: 'Great', + }); + expect(new FacebookEvent(commentRemove).comment).toEqual({ + from: { + id: '139560936744123', + }, + parentId: '137542570280222_139560936744456', + commentId: '139560936744456_139597043408045', + postId: '137542570280222_139560936744456', + verb: 'remove', + item: 'comment', + createdTime: 1511948944, + }); + expect(new FacebookEvent(reactionAdd).comment).toEqual(null); + expect(new FacebookEvent(reactionEdit).comment).toEqual(null); + expect(new FacebookEvent(reactionRemove).comment).toEqual(null); + expect(new FacebookEvent(postReaction).comment).toEqual(null); + expect(new FacebookEvent(sentByPage).comment).toEqual({ + from: { + id: '137542570280111', + name: 'Bottender', + }, + item: 'comment', + commentId: '139560936755999_140077616693321', + postId: '137542570280111_139560936755999', + verb: 'add', + parentId: '139560936755999_140077593359990', + createdTime: 1512121727, + message: 'Public reply!', + }); +}); + +it('#isReaction', () => { + expect(new FacebookEvent(statusAdd).isReaction).toEqual(false); + expect(new FacebookEvent(statusEdited).isReaction).toEqual(false); + expect(new FacebookEvent(postRemove).isReaction).toEqual(false); + expect(new FacebookEvent(commentAdd).isReaction).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isReaction).toEqual(false); + expect(new FacebookEvent(commentEdited).isReaction).toEqual(false); + expect(new FacebookEvent(commentRemove).isReaction).toEqual(false); + expect(new FacebookEvent(reactionAdd).isReaction).toEqual(true); + expect(new FacebookEvent(reactionEdit).isReaction).toEqual(true); + expect(new FacebookEvent(reactionRemove).isReaction).toEqual(true); + expect(new FacebookEvent(postReaction).isReaction).toEqual(true); + expect(new FacebookEvent(sentByPage).isReaction).toEqual(false); +}); + +it('#isReactionAdd', () => { + expect(new FacebookEvent(statusAdd).isReactionAdd).toEqual(false); + expect(new FacebookEvent(statusEdited).isReactionAdd).toEqual(false); + expect(new FacebookEvent(postRemove).isReactionAdd).toEqual(false); + expect(new FacebookEvent(commentAdd).isReactionAdd).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isReactionAdd).toEqual(false); + expect(new FacebookEvent(commentEdited).isReactionAdd).toEqual(false); + expect(new FacebookEvent(commentRemove).isReactionAdd).toEqual(false); + expect(new FacebookEvent(reactionAdd).isReactionAdd).toEqual(true); + expect(new FacebookEvent(reactionEdit).isReactionAdd).toEqual(false); + expect(new FacebookEvent(reactionRemove).isReactionAdd).toEqual(false); + expect(new FacebookEvent(postReaction).isReactionAdd).toEqual(true); + expect(new FacebookEvent(sentByPage).isReactionAdd).toEqual(false); +}); + +it('#isReactionEdit', () => { + expect(new FacebookEvent(statusAdd).isReactionEdit).toEqual(false); + expect(new FacebookEvent(statusEdited).isReactionEdit).toEqual(false); + expect(new FacebookEvent(postRemove).isReactionEdit).toEqual(false); + expect(new FacebookEvent(commentAdd).isReactionEdit).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isReactionEdit).toEqual(false); + expect(new FacebookEvent(commentEdited).isReactionEdit).toEqual(false); + expect(new FacebookEvent(commentRemove).isReactionEdit).toEqual(false); + expect(new FacebookEvent(reactionAdd).isReactionEdit).toEqual(false); + expect(new FacebookEvent(reactionEdit).isReactionEdit).toEqual(true); + expect(new FacebookEvent(reactionRemove).isReactionEdit).toEqual(false); + expect(new FacebookEvent(postReaction).isReactionEdit).toEqual(false); + expect(new FacebookEvent(sentByPage).isReactionEdit).toEqual(false); +}); + +it('#isReactionRemove', () => { + expect(new FacebookEvent(statusAdd).isReactionRemove).toEqual(false); + expect(new FacebookEvent(statusEdited).isReactionRemove).toEqual(false); + expect(new FacebookEvent(postRemove).isReactionRemove).toEqual(false); + expect(new FacebookEvent(commentAdd).isReactionRemove).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isReactionRemove).toEqual(false); + expect(new FacebookEvent(commentEdited).isReactionRemove).toEqual(false); + expect(new FacebookEvent(commentRemove).isReactionRemove).toEqual(false); + expect(new FacebookEvent(reactionAdd).isReactionRemove).toEqual(false); + expect(new FacebookEvent(reactionEdit).isReactionRemove).toEqual(false); + expect(new FacebookEvent(reactionRemove).isReactionRemove).toEqual(true); + expect(new FacebookEvent(postReaction).isReactionRemove).toEqual(false); + expect(new FacebookEvent(sentByPage).isReactionRemove).toEqual(false); +}); + +it('#isPostReaction', () => { + expect(new FacebookEvent(statusAdd).isPostReaction).toEqual(false); + expect(new FacebookEvent(statusEdited).isPostReaction).toEqual(false); + expect(new FacebookEvent(postRemove).isPostReaction).toEqual(false); + expect(new FacebookEvent(commentAdd).isPostReaction).toEqual(false); + expect(new FacebookEvent(videoCommentAdd).isPostReaction).toEqual(false); + expect(new FacebookEvent(commentEdited).isPostReaction).toEqual(false); + expect(new FacebookEvent(commentRemove).isPostReaction).toEqual(false); + expect(new FacebookEvent(reactionAdd).isPostReaction).toEqual(false); + expect(new FacebookEvent(reactionEdit).isPostReaction).toEqual(false); + expect(new FacebookEvent(reactionRemove).isPostReaction).toEqual(false); + expect(new FacebookEvent(postReaction).isPostReaction).toEqual(true); + expect(new FacebookEvent(sentByPage).isPostReaction).toEqual(false); +}); + +it('#reaction', () => { + expect(new FacebookEvent(statusAdd).reaction).toEqual(null); + expect(new FacebookEvent(statusEdited).reaction).toEqual(null); + expect(new FacebookEvent(postRemove).reaction).toEqual(null); + expect(new FacebookEvent(commentAdd).reaction).toEqual(null); + expect(new FacebookEvent(videoCommentAdd).reaction).toEqual(null); + expect(new FacebookEvent(commentEdited).reaction).toEqual(null); + expect(new FacebookEvent(commentRemove).reaction).toEqual(null); + expect(new FacebookEvent(reactionAdd).reaction).toEqual({ + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'add', + item: 'reaction', + createdTime: 1511948636, + }); + expect(new FacebookEvent(reactionEdit).reaction).toEqual({ + reactionType: 'sad', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'edit', + item: 'reaction', + createdTime: 1511948713, + }); + expect(new FacebookEvent(reactionRemove).reaction).toEqual({ + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '139560936744456_139597043408045', + commentId: '139560936744456_139597090074707', + postId: '137542570280222_139560936744456', + verb: 'remove', + item: 'reaction', + createdTime: 1511948666, + }); + expect(new FacebookEvent(postReaction).reaction).toEqual({ + reactionType: 'like', + from: { + id: '139560936744123', + }, + parentId: '137542570280222_139560936744456', + postId: '137542570280222_139560936744456', + verb: 'add', + item: 'reaction', + createdTime: 1568176139, + }); + expect(new FacebookEvent(sentByPage).reaction).toEqual(null); +}); + +it('#pageId', () => { + expect(new FacebookEvent(statusAdd, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(statusEdited, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(postRemove, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(commentAdd, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(videoCommentAdd, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(commentEdited, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(commentRemove, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(reactionAdd, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(reactionEdit, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(reactionRemove, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(postReaction, { pageId }).pageId).toEqual( + '137542570280111' + ); + expect(new FacebookEvent(sentByPage, { pageId }).pageId).toEqual( + '137542570280111' + ); +}); + +it('#isSentByPage', () => { + expect(new FacebookEvent(statusAdd, { pageId }).isSentByPage).toEqual(false); + expect(new FacebookEvent(statusEdited, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(postRemove, { pageId }).isSentByPage).toEqual(false); + expect(new FacebookEvent(commentAdd, { pageId }).isSentByPage).toEqual(false); + expect(new FacebookEvent(videoCommentAdd, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(commentEdited, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(commentRemove, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(reactionAdd, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(reactionEdit, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(reactionRemove, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(postReaction, { pageId }).isSentByPage).toEqual( + false + ); + expect(new FacebookEvent(sentByPage, { pageId }).isSentByPage).toEqual(true); +}); + +it('#isPageLike', () => { + expect(new FacebookEvent(statusAdd, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(statusEdited, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(postRemove, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(commentAdd, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(videoCommentAdd, { pageId }).isPageLike).toEqual( + false + ); + expect(new FacebookEvent(commentEdited, { pageId }).isPageLike).toEqual( + false + ); + expect(new FacebookEvent(commentRemove, { pageId }).isPageLike).toEqual( + false + ); + expect(new FacebookEvent(reactionAdd, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(reactionEdit, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(reactionRemove, { pageId }).isPageLike).toEqual( + false + ); + expect(new FacebookEvent(postReaction, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(sentByPage, { pageId }).isPageLike).toEqual(false); + expect(new FacebookEvent(pageLikeAdd, { pageId }).isPageLike).toEqual(true); +}); + +it('#timestamp', () => { + expect(new FacebookEvent(statusAdd).timestamp).toEqual(1511949030); + expect(new FacebookEvent(statusEdited).timestamp).toEqual(1511927135); + expect(new FacebookEvent(postRemove).timestamp).toEqual(1511949068); + expect(new FacebookEvent(commentAdd).timestamp).toEqual(1511951015); + expect(new FacebookEvent(videoCommentAdd).timestamp).toEqual(1545829217); + expect(new FacebookEvent(commentEdited).timestamp).toEqual(1511948891); + expect(new FacebookEvent(commentRemove).timestamp).toEqual(1511948944); + expect(new FacebookEvent(reactionAdd).timestamp).toEqual(1511948636); + expect(new FacebookEvent(reactionEdit).timestamp).toEqual(1511948713); + expect(new FacebookEvent(reactionRemove).timestamp).toEqual(1511948666); + expect(new FacebookEvent(sentByPage).timestamp).toEqual(1512121727); + expect(new FacebookEvent(pageLikeAdd, { timestamp }).timestamp).toEqual( + 1511948944 + ); +}); diff --git a/packages/bottender-facebook/src/__tests__/index.spec.ts b/packages/bottender-facebook/src/__tests__/index.spec.ts new file mode 100644 index 000000000..cfafa1e0a --- /dev/null +++ b/packages/bottender-facebook/src/__tests__/index.spec.ts @@ -0,0 +1,15 @@ +import { + FacebookBatch, + FacebookClient, + FacebookConnector, + FacebookContext, + FacebookEvent, +} from '..'; + +it('should export public api', () => { + expect(FacebookBatch).toBeDefined(); + expect(FacebookClient).toBeDefined(); + expect(FacebookConnector).toBeDefined(); + expect(FacebookContext).toBeDefined(); + expect(FacebookEvent).toBeDefined(); +}); diff --git a/packages/bottender-facebook/src/index.ts b/packages/bottender-facebook/src/index.ts new file mode 100644 index 000000000..6503f4feb --- /dev/null +++ b/packages/bottender-facebook/src/index.ts @@ -0,0 +1,9 @@ +import * as FacebookTypes from './FacebookTypes'; + +export { default as FacebookBatch } from './FacebookBatch'; +export { default as FacebookClient } from './FacebookClient'; +export { default as FacebookConnector } from './FacebookConnector'; +export { default as FacebookContext } from './FacebookContext'; +export { default as FacebookEvent } from './FacebookEvent'; + +export { FacebookTypes }; diff --git a/packages/bottender-facebook/tsconfig.json b/packages/bottender-facebook/tsconfig.json new file mode 100644 index 000000000..c7690e71a --- /dev/null +++ b/packages/bottender-facebook/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__"] +} diff --git a/packages/bottender-handlers/package.json b/packages/bottender-handlers/package.json index d852b9d46..6d533cf37 100644 --- a/packages/bottender-handlers/package.json +++ b/packages/bottender-handlers/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.0", + "version": "1.5.1-alpha.5", "main": "dist/index.js", "files": [ "dist" diff --git a/packages/bottender-handlers/src/Handler.ts b/packages/bottender-handlers/src/Handler.ts index 81e20356b..f2642957e 100644 --- a/packages/bottender-handlers/src/Handler.ts +++ b/packages/bottender-handlers/src/Handler.ts @@ -74,7 +74,7 @@ export default class Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isMessage, handler); + this.on((context) => context.event.isMessage, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -87,7 +87,7 @@ export default class Handler { ); this.on( - context => + (context) => context.event.isMessage && predicate(context.event.message, context), handler ); @@ -104,7 +104,7 @@ export default class Handler { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isText, handler); + this.on((context) => context.event.isText, handler); } else { // eslint-disable-next-line prefer-const let [pattern, handler] = args as [Pattern, FunctionalHandler | Builder]; @@ -123,7 +123,7 @@ export default class Handler { if (typeof pattern === 'function') { const predicate: Predicate = pattern; this.on( - context => + (context) => context.event.isText && predicate(context.event.text, context), handler ); @@ -132,7 +132,7 @@ export default class Handler { const patternRegExp: RegExp = pattern; const _handler = handler; - handler = context => { + handler = (context) => { const match = patternRegExp.exec(context.event.text); if (!match) return _handler(context); @@ -145,7 +145,7 @@ export default class Handler { } this.on( - context => + (context) => context.event.isText && matchPattern(pattern, context.event.text), handler ); diff --git a/packages/bottender-handlers/src/LineHandler.ts b/packages/bottender-handlers/src/LineHandler.ts index c8f178837..7b0b5732f 100644 --- a/packages/bottender-handlers/src/LineHandler.ts +++ b/packages/bottender-handlers/src/LineHandler.ts @@ -16,7 +16,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPostback, handler); + this.on((context) => context.event.isPostback, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -29,7 +29,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isPostback && predicate(context.event.postback, context), handler @@ -47,7 +47,7 @@ export default class LineHandler extends Handler { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPayload, handler); + this.on((context) => context.event.isPayload, handler); } else { // eslint-disable-next-line prefer-const let [pattern, handler] = args as [Pattern, FunctionalHandler | Builder]; @@ -66,7 +66,7 @@ export default class LineHandler extends Handler { if (typeof pattern === 'function') { const predicate: Predicate = pattern; this.on( - context => + (context) => context.event.isPayload && predicate(context.event.payload, context), handler @@ -76,7 +76,7 @@ export default class LineHandler extends Handler { const patternRegExp: RegExp = pattern; const _handler = handler; - handler = context => { + handler = (context) => { const match = patternRegExp.exec(context.event.payload); if (!match) return _handler(context); @@ -89,7 +89,7 @@ export default class LineHandler extends Handler { } this.on( - context => + (context) => context.event.isPayload && matchPattern(pattern, context.event.payload), handler @@ -108,7 +108,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isFollow, handler); + this.on((context) => context.event.isFollow, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -121,7 +121,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isFollow && predicate(context.event.follow, context), handler ); @@ -137,7 +137,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isUnfollow, handler); + this.on((context) => context.event.isUnfollow, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -150,7 +150,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isUnfollow && predicate(context.event.unfollow, context), handler @@ -168,7 +168,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isJoin, handler); + this.on((context) => context.event.isJoin, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -181,7 +181,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isJoin && predicate(context.event.join, context), handler ); @@ -198,7 +198,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isLeave, handler); + this.on((context) => context.event.isLeave, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -211,7 +211,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isLeave && predicate(context.event.leave, context), handler ); @@ -227,7 +227,7 @@ export default class LineHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isBeacon, handler); + this.on((context) => context.event.isBeacon, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -240,7 +240,7 @@ export default class LineHandler extends Handler { ); this.on( - context => + (context) => context.event.isBeacon && predicate(context.event.beacon, context), handler ); diff --git a/packages/bottender-handlers/src/MessengerHandler.ts b/packages/bottender-handlers/src/MessengerHandler.ts index 51e620273..a65b0145f 100644 --- a/packages/bottender-handlers/src/MessengerHandler.ts +++ b/packages/bottender-handlers/src/MessengerHandler.ts @@ -16,7 +16,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPostback, handler); + this.on((context) => context.event.isPostback, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -29,7 +29,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isPostback && predicate(context.event.postback, context), handler @@ -70,7 +70,7 @@ export default class MessengerHandler extends Handler { if (typeof pattern === 'function') { const predicate: Predicate = pattern; this.on( - context => + (context) => context.event.isPayload && predicate(context.event.payload, context), handler @@ -80,7 +80,7 @@ export default class MessengerHandler extends Handler { const patternRegExp: RegExp = pattern; const _handler = handler; - handler = context => { + handler = (context) => { let message; if (context.event.isPostback) { message = context.event.postback.payload; @@ -115,7 +115,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPayment, handler); + this.on((context) => context.event.isPayment, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -128,7 +128,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isPayment && predicate(context.event.payment, context), handler ); @@ -144,7 +144,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isOptin, handler); + this.on((context) => context.event.isOptin, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -157,7 +157,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isOptin && predicate(context.event.optin, context), handler ); @@ -173,7 +173,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isCheckoutUpdate, handler); + this.on((context) => context.event.isCheckoutUpdate, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -186,7 +186,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isCheckoutUpdate && predicate(context.event.checkoutUpdate, context), handler @@ -203,7 +203,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPreCheckout, handler); + this.on((context) => context.event.isPreCheckout, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -216,7 +216,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isPreCheckout && predicate(context.event.preCheckout, context), handler @@ -233,7 +233,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isQuickReply, handler); + this.on((context) => context.event.isQuickReply, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -246,7 +246,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isQuickReply && predicate(context.event.quickReply, context), handler @@ -263,14 +263,14 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isEcho, handler); + this.on((context) => context.event.isEcho, handler); } else { const [predicate, handler] = args as [ Predicate, FunctionalHandler | Builder ]; this.on( - context => + (context) => context.event.isEcho && predicate(context.event.message, context), handler ); @@ -286,7 +286,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isRead, handler); + this.on((context) => context.event.isRead, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -299,7 +299,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isRead && predicate(context.event.read, context), handler ); @@ -315,7 +315,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isDelivery, handler); + this.on((context) => context.event.isDelivery, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -328,7 +328,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isDelivery && predicate(context.event.delivery, context), handler @@ -345,7 +345,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isLocation, handler); + this.on((context) => context.event.isLocation, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -358,7 +358,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isLocation && predicate(context.event.location, context), handler @@ -375,7 +375,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isImage, handler); + this.on((context) => context.event.isImage, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -388,7 +388,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isImage && predicate(context.event.image, context), handler ); @@ -404,7 +404,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isAudio, handler); + this.on((context) => context.event.isAudio, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -417,7 +417,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isAudio && predicate(context.event.audio, context), handler ); @@ -433,7 +433,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isVideo, handler); + this.on((context) => context.event.isVideo, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -446,7 +446,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isVideo && predicate(context.event.video, context), handler ); @@ -462,7 +462,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isFile, handler); + this.on((context) => context.event.isFile, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -475,7 +475,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isFile && predicate(context.event.file, context), handler ); @@ -491,7 +491,7 @@ export default class MessengerHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isFallback, handler); + this.on((context) => context.event.isFallback, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -504,7 +504,7 @@ export default class MessengerHandler extends Handler { ); this.on( - context => + (context) => context.event.isFallback && predicate(context.event.fallback, context), handler diff --git a/packages/bottender-handlers/src/TelegramHandler.ts b/packages/bottender-handlers/src/TelegramHandler.ts index 0f6690462..226f6b941 100644 --- a/packages/bottender-handlers/src/TelegramHandler.ts +++ b/packages/bottender-handlers/src/TelegramHandler.ts @@ -16,7 +16,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isCallbackQuery, handler); + this.on((context) => context.event.isCallbackQuery, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -29,7 +29,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isCallbackQuery && predicate(context.event.callbackQuery, context), handler @@ -47,13 +47,11 @@ export default class TelegramHandler extends Handler { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPayload, handler); + this.on((context) => context.event.isPayload, handler); } else { // eslint-disable-next-line prefer-const - let [pattern, handler]: [ - Pattern, - FunctionalHandler | Builder - ] = args as any; + let [pattern, handler]: [Pattern, FunctionalHandler | Builder] = + args as any; if ('build' in handler) { handler = handler.build(); @@ -69,7 +67,7 @@ export default class TelegramHandler extends Handler { if (typeof pattern === 'function') { const predicate: Predicate = pattern; this.on( - context => + (context) => context.event.isPayload && predicate(context.event.payload, context), handler @@ -79,7 +77,7 @@ export default class TelegramHandler extends Handler { const patternRegExp: RegExp = pattern; const _handler = handler; - handler = context => { + handler = (context) => { const match = patternRegExp.exec(context.event.payload); if (!match) return _handler(context); @@ -109,7 +107,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isPhoto, handler); + this.on((context) => context.event.isPhoto, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -122,7 +120,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isPhoto && predicate(context.event.photo, context), handler ); @@ -138,7 +136,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isDocument, handler); + this.on((context) => context.event.isDocument, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -151,7 +149,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isDocument && predicate(context.event.document, context), handler @@ -168,7 +166,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isAudio, handler); + this.on((context) => context.event.isAudio, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -181,7 +179,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isAudio && predicate(context.event.audio, context), handler ); @@ -197,7 +195,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isGame, handler); + this.on((context) => context.event.isGame, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -210,7 +208,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isGame && predicate(context.event.game, context), handler ); @@ -226,7 +224,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isSticker, handler); + this.on((context) => context.event.isSticker, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -239,7 +237,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isSticker && predicate(context.event.sticker, context), handler ); @@ -255,7 +253,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isVideo, handler); + this.on((context) => context.event.isVideo, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -268,7 +266,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isVideo && predicate(context.event.video, context), handler ); @@ -284,7 +282,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isVoice, handler); + this.on((context) => context.event.isVoice, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -297,7 +295,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isVoice && predicate(context.event.voice, context), handler ); @@ -313,7 +311,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isVideoNote, handler); + this.on((context) => context.event.isVideoNote, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -326,7 +324,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isVideoNote && predicate(context.event.videoNote, context), handler @@ -343,7 +341,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isContact, handler); + this.on((context) => context.event.isContact, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -356,7 +354,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isContact && predicate(context.event.contact, context), handler ); @@ -372,7 +370,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isLocation, handler); + this.on((context) => context.event.isLocation, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -385,7 +383,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isLocation && predicate(context.event.location, context), handler @@ -402,7 +400,7 @@ export default class TelegramHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isVenue, handler); + this.on((context) => context.event.isVenue, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -415,7 +413,7 @@ export default class TelegramHandler extends Handler { ); this.on( - context => + (context) => context.event.isVenue && predicate(context.event.venue, context), handler ); diff --git a/packages/bottender-handlers/src/ViberHandler.ts b/packages/bottender-handlers/src/ViberHandler.ts index f2ffc2ad1..f1c359c53 100644 --- a/packages/bottender-handlers/src/ViberHandler.ts +++ b/packages/bottender-handlers/src/ViberHandler.ts @@ -10,7 +10,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isSubscribed, handler); + this.on((context) => context.event.isSubscribed, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -23,7 +23,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isSubscribed && predicate(context.event.subscribed, context), handler @@ -40,7 +40,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isUnsubscribed, handler); + this.on((context) => context.event.isUnsubscribed, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -53,7 +53,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isUnsubscribed && predicate(context.event.unsubscribed, context), handler @@ -70,7 +70,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isConversationStarted, handler); + this.on((context) => context.event.isConversationStarted, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -83,7 +83,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isConversationStarted && predicate(context.event.conversationStarted, context), handler @@ -100,7 +100,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isDelivered, handler); + this.on((context) => context.event.isDelivered, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -113,7 +113,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isDelivered && predicate(context.event.delivered, context), handler @@ -130,7 +130,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isSeen, handler); + this.on((context) => context.event.isSeen, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -143,7 +143,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isSeen && predicate(context.event.seen, context), handler ); @@ -159,7 +159,7 @@ export default class ViberHandler extends Handler { ) { if (args.length < 2) { const [handler] = args as [FunctionalHandler | Builder]; - this.on(context => context.event.isFailed, handler); + this.on((context) => context.event.isFailed, handler); } else { const [predicate, handler] = args as [ Predicate, @@ -172,7 +172,7 @@ export default class ViberHandler extends Handler { ); this.on( - context => + (context) => context.event.isFailed && predicate(context.event.failed, context), handler ); diff --git a/packages/bottender-handlers/src/__tests__/Handler.spec.ts b/packages/bottender-handlers/src/__tests__/Handler.spec.ts index 83af99845..870018ac0 100644 --- a/packages/bottender-handlers/src/__tests__/Handler.spec.ts +++ b/packages/bottender-handlers/src/__tests__/Handler.spec.ts @@ -410,7 +410,7 @@ describe('#onText', () => { text: 'awesome', }, }; - builder.onText(text => text === 'awesome', handler); + builder.onText((text) => text === 'awesome', handler); await builder.build()(context); expect(handler).toBeCalledWith(context); }); @@ -428,7 +428,7 @@ describe('#onText', () => { text: 'awesome', }, }; - builder.onText(text => text !== 'awesome', handler); + builder.onText((text) => text !== 'awesome', handler); await builder.build()(context); expect(handler).not.toBeCalled(); }); @@ -525,7 +525,7 @@ describe('#onError', () => { .onEvent(() => { throw new Error('Boom!'); }) - .onError(ctx => { + .onError((ctx) => { ctx.sendText('Boom!'); }); await builder.build()(context); @@ -542,7 +542,7 @@ describe('#onError', () => { throw new Error('Boom!'); }); - builder.onEvent(builder2.build()).onError(ctx => { + builder.onEvent(builder2.build()).onError((ctx) => { ctx.sendText('Boom!'); }); await builder.build()(context); diff --git a/packages/bottender-handlers/src/__tests__/LineHandler.spec.ts b/packages/bottender-handlers/src/__tests__/LineHandler.spec.ts index 081279afc..d772a24f4 100644 --- a/packages/bottender-handlers/src/__tests__/LineHandler.spec.ts +++ b/packages/bottender-handlers/src/__tests__/LineHandler.spec.ts @@ -192,7 +192,7 @@ describe('#onPayload', () => { }, }; - builder.onPayload(payload => payload === 'cool', handler); + builder.onPayload((payload) => payload === 'cool', handler); await builder.build()(context); expect(handler).toBeCalledWith(context); }); diff --git a/packages/bottender-handlers/src/__tests__/MessengerHandler.spec.ts b/packages/bottender-handlers/src/__tests__/MessengerHandler.spec.ts index fb17e45f7..0a5a7cec6 100644 --- a/packages/bottender-handlers/src/__tests__/MessengerHandler.spec.ts +++ b/packages/bottender-handlers/src/__tests__/MessengerHandler.spec.ts @@ -264,7 +264,7 @@ describe('#onPayload', () => { }, }; - builder.onPayload(payload => payload === 'cool', handler); + builder.onPayload((payload) => payload === 'cool', handler); await builder.build()(context); expect(handler).toBeCalledWith(context); }); @@ -290,7 +290,7 @@ describe('#onPayload', () => { }, }; - builder.onPayload(payload => payload === 'so quick!', handler); + builder.onPayload((payload) => payload === 'so quick!', handler); await builder.build()(context); expect(handler).toBeCalledWith(context); }); diff --git a/packages/bottender-handlers/src/__tests__/TelegramHandler.spec.ts b/packages/bottender-handlers/src/__tests__/TelegramHandler.spec.ts index 74411953c..9a5664639 100644 --- a/packages/bottender-handlers/src/__tests__/TelegramHandler.spec.ts +++ b/packages/bottender-handlers/src/__tests__/TelegramHandler.spec.ts @@ -206,7 +206,7 @@ describe('#onPayload', () => { const { builder } = setup(); const handler = jest.fn(); - builder.onPayload(payload => payload === 'data', handler); + builder.onPayload((payload) => payload === 'data', handler); await builder.build()(contextWithCallbackQuery); expect(handler).toBeCalledWith(contextWithCallbackQuery); }); @@ -215,7 +215,7 @@ describe('#onPayload', () => { const { builder } = setup(); const handler = jest.fn(); - builder.onPayload(payload => payload === 'awful', handler); + builder.onPayload((payload) => payload === 'awful', handler); await builder.build()(contextWithCallbackQuery); expect(handler).not.toBeCalled(); }); diff --git a/packages/bottender-handlers/src/middleware.ts b/packages/bottender-handlers/src/middleware.ts index 4a1f2862a..3ac394c29 100644 --- a/packages/bottender-handlers/src/middleware.ts +++ b/packages/bottender-handlers/src/middleware.ts @@ -5,6 +5,6 @@ import { Builder } from './Handler'; type Middleware = (context?: any, next?: Middleware) => {}; const middleware = (m: (Middleware | Builder)[]) => - compose(m.map(item => ('build' in item ? item.build() : item))); + compose(m.map((item) => ('build' in item ? item.build() : item))); export default middleware; diff --git a/packages/bottender-luis/package.json b/packages/bottender-luis/package.json index 9e098f93a..46fd0e455 100644 --- a/packages/bottender-luis/package.json +++ b/packages/bottender-luis/package.json @@ -1,5 +1,6 @@ { "name": "@bottender/luis", + "version": "1.5.1-alpha.9", "description": "LUIS integration for Bottender.", "license": "MIT", "homepage": "https://bottender.js.org/", @@ -7,21 +8,20 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.10", "main": "dist/index.js", "files": [ "dist" ], "types": "dist/index.d.ts", "dependencies": { - "axios": "^0.19.2", + "axios": "^0.21.4", "invariant": "^2.2.4" }, "peerDependencies": { "bottender": ">= 1.2.0-0" }, "devDependencies": { - "bottender": "^1.4.10" + "bottender": "^1.5.1-alpha.9" }, "keywords": [ "bot", diff --git a/packages/bottender-luis/src/__tests__/index.spec.ts b/packages/bottender-luis/src/__tests__/index.spec.ts index 5a3321cab..11677c2cb 100644 --- a/packages/bottender-luis/src/__tests__/index.spec.ts +++ b/packages/bottender-luis/src/__tests__/index.spec.ts @@ -3,10 +3,10 @@ import { Context, chain } from 'bottender'; // FIXME: export public API for testing import { run } from 'bottender/dist/bot/Bot'; -const luis = require('..'); // eslint-disable-line @typescript-eslint/no-var-requires +import luis from '..'; // FIXME: export public test-utils for testing -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } @@ -62,11 +62,11 @@ it('should resolve corresponding action if intent score > scoreThreshold', async let requestHeaders; nock('https://westus.api.cognitive.microsoft.com') .get('/luis/v2.0/apps/APP_ID') - .query(query => { + .query((query) => { requestQuery = query; return true; }) - .reply(200, function() { + .reply(200, function () { requestHeaders = this.req.headers; return { query: 'forward to frank 30 dollars through HSBC', @@ -229,7 +229,7 @@ it('should support parameters of luis', async () => { let requestQuery; nock('https://westus.api.cognitive.microsoft.com') .get('/luis/v2.0/apps/APP_ID') - .query(query => { + .query((query) => { requestQuery = query; return true; }) diff --git a/packages/bottender-luis/src/index.ts b/packages/bottender-luis/src/index.ts index 87b72a7e0..fcb025bfc 100644 --- a/packages/bottender-luis/src/index.ts +++ b/packages/bottender-luis/src/index.ts @@ -5,6 +5,8 @@ import { Action, Context, withProps } from 'bottender'; import { EntityModel, IntentModel, LuisResult, Sentiment } from './types'; /** + * @example + * ``` * const Luis = luis({ * appId: 'APP_ID', * appKey: 'APP_KEY', @@ -16,9 +18,9 @@ import { EntityModel, IntentModel, LuisResult, Sentiment } from './types'; * }, * }, * }); - * + * ``` */ -module.exports = function luis({ +export = function luis({ appId, appKey, endpoint, @@ -38,7 +40,7 @@ module.exports = function luis({ actions: Record< string, Action< - Context, + Context, { topScoringIntent: IntentModel; entities: EntityModel[]; @@ -52,7 +54,7 @@ module.exports = function luis({ staging?: boolean; bingSpellCheckSubscriptionKey?: string; log?: boolean; -}): Action> { +}): Action { invariant( typeof appId === 'string' && appId.length > 0, 'luis: `appId` is a required parameter.' @@ -69,9 +71,9 @@ module.exports = function luis({ ); return async function Luis( - context: Context, - { next }: { next?: Action> } - ): Promise> | void> { + context: Context, + { next }: { next?: Action } + ): Promise | void> { if (!context.event.isText) { return next; } diff --git a/packages/bottender-qna-maker/package.json b/packages/bottender-qna-maker/package.json index 308dd8212..12f72f3d8 100644 --- a/packages/bottender-qna-maker/package.json +++ b/packages/bottender-qna-maker/package.json @@ -1,5 +1,6 @@ { "name": "@bottender/qna-maker", + "version": "1.5.1-alpha.9", "description": "QnA Maker integration for Bottender.", "license": "MIT", "homepage": "https://bottender.js.org/", @@ -7,21 +8,20 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.10", "main": "dist/index.js", "files": [ "dist" ], "types": "dist/index.d.ts", "dependencies": { - "axios": "^0.19.2", + "axios": "^0.21.4", "invariant": "^2.2.4" }, "peerDependencies": { "bottender": ">= 1.2.0-0" }, "devDependencies": { - "bottender": "^1.4.10" + "bottender": "^1.5.1-alpha.9" }, "keywords": [ "bot", diff --git a/packages/bottender-qna-maker/src/__tests__/index.spec.ts b/packages/bottender-qna-maker/src/__tests__/index.spec.ts index 2cead7d05..f0971f731 100644 --- a/packages/bottender-qna-maker/src/__tests__/index.spec.ts +++ b/packages/bottender-qna-maker/src/__tests__/index.spec.ts @@ -3,10 +3,10 @@ import { Context, chain } from 'bottender'; // FIXME: export public API for testing import { run } from 'bottender/dist/bot/Bot'; -const qnaMaker = require('..'); // eslint-disable-line @typescript-eslint/no-var-requires +import qnaMaker from '..'; // FIXME: export public test-utils for testing -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender-qna-maker/src/index.ts b/packages/bottender-qna-maker/src/index.ts index b6ff9d74c..24bf579a1 100644 --- a/packages/bottender-qna-maker/src/index.ts +++ b/packages/bottender-qna-maker/src/index.ts @@ -9,14 +9,17 @@ import { MetadataDTO, QnaSearchResultList } from './types'; const RECOMMENDED_THRESHOLD = 50; /** + * @example + * ``` * const QnaMaker = qnaMaker({ * resourceName: 'RESOURCE_NAME', * knowledgeBaseId: 'KNOWLEDGE_BASE_ID', * endpointKey: 'ENDPOINT_KEY', * scoreThreshold: 70, * }); + * ``` */ -module.exports = function qnaMaker({ +export = function qnaMaker({ resourceName, knowledgeBaseId, endpointKey, @@ -32,7 +35,7 @@ module.exports = function qnaMaker({ qnaId?: string; scoreThreshold?: number; strictFilters?: MetadataDTO[]; -}): Action> { +}): Action { invariant( typeof resourceName === 'string' && resourceName.length > 0, 'qna-maker: `resourceName` is a required parameter.' @@ -49,9 +52,9 @@ module.exports = function qnaMaker({ ); return async function QnaMaker( - context: Context, - { next }: { next?: Action> } - ): Promise> | void> { + context: Context, + { next }: { next?: Action } + ): Promise | void> { if (!context.event.isText || !context.session) { return next; } diff --git a/packages/bottender-rasa/package.json b/packages/bottender-rasa/package.json index 92b80a5da..21ca41669 100644 --- a/packages/bottender-rasa/package.json +++ b/packages/bottender-rasa/package.json @@ -1,5 +1,6 @@ { "name": "@bottender/rasa", + "version": "1.5.1-alpha.9", "description": "Rasa NLU integration for Bottender.", "license": "MIT", "homepage": "https://bottender.js.org/", @@ -7,7 +8,6 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.10", "main": "dist/index.js", "files": [ "dist" @@ -15,14 +15,14 @@ "types": "dist/index.d.ts", "dependencies": { "@types/lodash": "^4.14.149", - "axios": "^0.19.2", + "axios": "^0.21.4", "lodash": "^4.17.15" }, "peerDependencies": { "bottender": ">= 1.2.0-0" }, "devDependencies": { - "bottender": "^1.4.10" + "bottender": "^1.5.1-alpha.9" }, "keywords": [ "bot", diff --git a/packages/bottender-rasa/src/__tests__/index.spec.ts b/packages/bottender-rasa/src/__tests__/index.spec.ts index e3978f692..fc7a3f42b 100644 --- a/packages/bottender-rasa/src/__tests__/index.spec.ts +++ b/packages/bottender-rasa/src/__tests__/index.spec.ts @@ -3,10 +3,10 @@ import { Context, chain } from 'bottender'; // FIXME: export public API for testing import { run } from 'bottender/dist/bot/Bot'; -const rasa = require('..'); // eslint-disable-line @typescript-eslint/no-var-requires +import rasa from '..'; // FIXME: export public test-utils for testing -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } @@ -166,7 +166,7 @@ it('should support JWT', async () => { let requestHeaders; nock('http://localhost:5005') .post('/model/parse') - .reply(200, function() { + .reply(200, function () { requestHeaders = this.req.headers; return { entities: [ @@ -219,7 +219,7 @@ it('should support parameters of rasa', async () => { let requestQuery; nock('http://localhost:5005') .post('/model/parse') - .query(query => { + .query((query) => { requestQuery = query; return true; }) diff --git a/packages/bottender-rasa/src/index.ts b/packages/bottender-rasa/src/index.ts index 1c104a5eb..cdead3e37 100644 --- a/packages/bottender-rasa/src/index.ts +++ b/packages/bottender-rasa/src/index.ts @@ -5,6 +5,8 @@ import { get } from 'lodash'; import { Entity, Intent, ParsedResult } from './types'; /** + * @example + * ``` * const Rasa = rasa({ * origin: 'http://localhost:5005', * actions: { @@ -14,8 +16,9 @@ import { Entity, Intent, ParsedResult } from './types'; * }, * confidenceThreshold: 0.7 * }); + * ``` */ -module.exports = function rasa({ +export = function rasa({ origin = 'http://localhost:5005', actions, confidenceThreshold, @@ -26,7 +29,7 @@ module.exports = function rasa({ actions: Record< string, Action< - Context, + Context, { intent: Intent; entities: Entity[]; @@ -36,11 +39,11 @@ module.exports = function rasa({ confidenceThreshold: number; emulationMode?: 'WIT' | 'LUIS' | 'DIALOGFLOW'; jwt?: string; -}): Action> { +}): Action { return async function Rasa( - context: Context, - { next }: { next?: Action> } - ): Promise> | void> { + context: Context, + { next }: { next?: Action } + ): Promise | void> { if (!context.event.isText) { return next; } diff --git a/packages/bottender/package.json b/packages/bottender/package.json index e67eff089..e5ca709bc 100644 --- a/packages/bottender/package.json +++ b/packages/bottender/package.json @@ -1,5 +1,6 @@ { "name": "bottender", + "version": "1.5.1-alpha.9", "description": "A framework for building conversational user interfaces.", "license": "MIT", "homepage": "https://bottender.js.org/", @@ -7,7 +8,6 @@ "type": "git", "url": "https://github.com/Yoctol/bottender.git" }, - "version": "1.4.10", "main": "dist/index.js", "browser": "dist/browser.js", "bin": { @@ -23,7 +23,7 @@ ], "types": "dist/index.d.ts", "dependencies": { - "@bottender/express": "^1.4.0", + "@bottender/express": "^1.5.1-alpha.5", "@hapi/joi": "^15.1.1", "@slack/rtm-api": "^5.0.3", "@types/debug": "^4.1.5", @@ -38,12 +38,14 @@ "@types/lodash": "^4.14.149", "@types/lru-cache": "^5.1.0", "@types/mongodb": "^3.3.16", + "@types/object.fromentries": "^2.0.0", "@types/shortid": "^0.0.29", "@types/update-notifier": "^4.1.0", "@types/warning": "^3.0.0", "arg": "^4.1.3", - "axios": "^0.19.2", - "axios-error": "^1.0.0-beta.16", + "axios": "^0.21.4", + "axios-error": "^1.0.0-beta.27", + "body-parser": "^1.19.0", "chalk": "~2.4.2", "cli-table3": "^0.5.1", "date-fns": "^2.10.0", @@ -52,9 +54,9 @@ "delay": "^4.3.0", "dotenv": "^8.2.0", "express": "^4.17.1", + "facebook-batch": "1.0.6", "figures": "^3.2.0", "file-type": "^14.1.3", - "fromentries": "^1.2.0", "fs-extra": "^8.1.0", "hasha": "^5.2.0", "import-fresh": "^3.2.1", @@ -65,26 +67,28 @@ "jsonfile": "^6.0.0", "lodash": "^4.17.15", "lru-cache": "^5.1.1", - "messaging-api-common": "1.0.0-beta.16", - "messaging-api-line": "1.0.0-beta.20", - "messaging-api-messenger": "1.0.0-beta.18", - "messaging-api-slack": "1.0.0-beta.16", - "messaging-api-telegram": "1.0.0-beta.16", - "messaging-api-viber": "1.0.0-beta.16", - "messenger-batch": "^0.3.1", + "messaging-api-common": "1.0.4", + "messaging-api-line": "1.0.6", + "messaging-api-messenger": "1.0.6", + "messaging-api-slack": "1.0.6", + "messaging-api-telegram": "1.0.6", + "messaging-api-viber": "1.0.6", "minimist": "^1.2.0", "mongodb": "^3.5.4", - "ngrok": "^3.2.5", + "ngrok": "^3.4.1", "nodemon": "^2.0.2", + "object.fromentries": "^2.0.2", "p-map": "^3.0.0", "p-props": "^3.1.0", "pascal-case": "^3.1.1", + "path-to-regexp": "^6.1.0", "pkg-dir": "^4.2.0", "prompt-confirm": "^2.0.4", "read-chunk": "^3.2.0", "readline": "^1.3.0", "recursive-readdir": "^2.2.2", "shortid": "^2.2.15", + "type-fest": "^0.13.1", "update-notifier": "^4.1.0", "warning": "^4.0.3" }, diff --git a/packages/bottender/src/__tests__/index.spec.ts b/packages/bottender/src/__tests__/index.spec.ts index 668049bda..3c418601c 100644 --- a/packages/bottender/src/__tests__/index.spec.ts +++ b/packages/bottender/src/__tests__/index.spec.ts @@ -14,6 +14,7 @@ describe('core', () => { it('export connectors', () => { expect(core.ConsoleConnector).toBeDefined(); + expect(core.FacebookBaseConnector).toBeDefined(); expect(core.MessengerConnector).toBeDefined(); expect(core.WhatsappConnector).toBeDefined(); expect(core.LineConnector).toBeDefined(); @@ -22,6 +23,15 @@ describe('core', () => { expect(core.ViberConnector).toBeDefined(); }); + it('export clients', () => { + expect(core.MessengerClient).toBeDefined(); + expect(core.TwilioClient).toBeDefined(); + expect(core.LineClient).toBeDefined(); + expect(core.SlackOAuthClient).toBeDefined(); + expect(core.TelegramClient).toBeDefined(); + expect(core.ViberClient).toBeDefined(); + }); + it('export cache implements', () => { expect(core.MemoryCacheStore).toBeDefined(); expect(core.RedisCacheStore).toBeDefined(); diff --git a/packages/bottender/src/bot/Bot.ts b/packages/bottender/src/bot/Bot.ts index 1aa1ef00a..cd3ab4237 100644 --- a/packages/bottender/src/bot/Bot.ts +++ b/packages/bottender/src/bot/Bot.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; import debug from 'debug'; import invariant from 'invariant'; import pMap from 'p-map'; +import { JsonObject } from 'type-fest'; import { camelcaseKeysDeep } from 'messaging-api-common'; import CacheBasedSessionStore from '../session/CacheBasedSessionStore'; @@ -10,20 +11,12 @@ import Context from '../context/Context'; import MemoryCacheStore from '../cache/MemoryCacheStore'; import Session from '../session/Session'; import SessionStore from '../session/SessionStore'; -import { - Action, - AnyContext, - Body, - Client, - Event, - Plugin, - Props, - RequestContext, -} from '../types'; +import { Action, Client, Plugin, Props, RequestContext } from '../types'; +import { Event } from '../context/Event'; import { Connector } from './Connector'; -type Builder = { +type Builder = { build: () => Action; }; @@ -40,9 +33,7 @@ function createMemorySessionStore(): SessionStore { return new CacheBasedSessionStore(cache, MINUTES_IN_ONE_YEAR); } -export function run( - action: Action -): Action { +export function run(action: Action): Action { return async function Run(context: C, props: Props = {}): Promise { let nextDialog: Action | void = action; @@ -72,8 +63,13 @@ type RequestHandler = ( requestContext?: RequestContext ) => void | Promise; +export type OnRequest = ( + body: JsonObject, + requestContext?: RequestContext +) => void; + export default class Bot< - B extends Body, + B extends JsonObject, C extends Client, E extends Event, Ctx extends Context @@ -88,7 +84,7 @@ export default class Bot< _errorHandler: Action | null; - _initialState: Record = {}; + _initialState: JsonObject = {}; _plugins: Function[] = []; @@ -96,14 +92,18 @@ export default class Bot< _emitter: EventEmitter; + _onRequest: OnRequest | undefined; + constructor({ connector, sessionStore = createMemorySessionStore(), sync = false, + onRequest, }: { connector: Connector; sessionStore?: SessionStore; sync?: boolean; + onRequest?: OnRequest; }) { this._sessions = sessionStore; this._initialized = false; @@ -112,6 +112,7 @@ export default class Bot< this._errorHandler = null; this._sync = sync; this._emitter = new EventEmitter(); + this._onRequest = onRequest; } get connector(): Connector { @@ -122,7 +123,7 @@ export default class Bot< return this._sessions; } - get handler(): Action | null { + get handler(): Action | null { return this._handler; } @@ -148,7 +149,7 @@ export default class Bot< return this; } - setInitialState(initialState: Record): Bot { + setInitialState(initialState: JsonObject): Bot { this._initialState = initialState; return this; } @@ -191,15 +192,23 @@ export default class Bot< const body = camelcaseKeysDeep(inputBody) as B; + if (this._onRequest) { + this._onRequest(body, requestContext); + } + const events = this._connector.mapRequestToEvents(body); const contexts = await pMap( events, - async event => { + async (event) => { const { platform } = this._connector; const sessionKey = this._connector.getUniqueSessionKey( - // TODO: may deprecating passing request body in v2 - events.length === 1 ? body : event, + // TODO: deprecating passing request body in those connectors + ['telegram', 'slack', 'viber', 'whatsapp'].includes( + this._connector.platform + ) + ? body + : event, requestContext ); @@ -234,8 +243,12 @@ export default class Bot< await this._connector.updateSession( session, - // TODO: may deprecating passing request body in v2 - events.length === 1 ? body : event + // TODO: deprecating passing request body in those connectors + ['telegram', 'slack', 'viber', 'whatsapp'].includes( + this._connector.platform + ) + ? body + : event ); } @@ -254,8 +267,8 @@ export default class Bot< // Call all of extension functions before passing to handler. await Promise.all( - contexts.map(async context => - Promise.all(this._plugins.map(ext => ext(context))) + contexts.map(async (context) => + Promise.all(this._plugins.map((ext) => ext(context))) ) ); @@ -277,13 +290,13 @@ export default class Bot< return context.handlerDidEnd(); } }) - .catch(err => { + .catch((err) => { if (errorHandler) { return run(errorHandler)(context, { error: err }); } throw err; }) - .catch(err => { + .catch((err) => { context.emitError(err); throw err; }) @@ -295,7 +308,7 @@ export default class Bot< await promises; await Promise.all( - contexts.map(async context => { + contexts.map(async (context) => { context.isSessionWritten = true; const { session } = context; @@ -324,27 +337,25 @@ export default class Bot< return response; } promises - .then( - async (): Promise => { - await Promise.all( - contexts.map(async context => { - context.isSessionWritten = true; + .then(async (): Promise => { + await Promise.all( + contexts.map(async (context) => { + context.isSessionWritten = true; - const { session } = context; + const { session } = context; - if (session) { - session.lastActivity = Date.now(); + if (session) { + session.lastActivity = Date.now(); - debugSessionWrite(`Write session: ${session.id}`); - debugSessionWrite(JSON.stringify(session, null, 2)); + debugSessionWrite(`Write session: ${session.id}`); + debugSessionWrite(JSON.stringify(session, null, 2)); - // eslint-disable-next-line no-await-in-loop - await this._sessions.write(session.id, session); - } - }) - ); - } - ) + // eslint-disable-next-line no-await-in-loop + await this._sessions.write(session.id, session); + } + }) + ); + }) .catch(console.error); }; } diff --git a/packages/bottender/src/bot/Connector.ts b/packages/bottender/src/bot/Connector.ts index 4ead0dccc..aed94b831 100644 --- a/packages/bottender/src/bot/Connector.ts +++ b/packages/bottender/src/bot/Connector.ts @@ -1,11 +1,13 @@ import { EventEmitter } from 'events'; +import { JsonObject } from 'type-fest'; + import Session from '../session/Session'; import { Event } from '../context/Event'; import { RequestContext } from '../types'; export interface Connector { - client: C; + client?: C; platform: string; getUniqueSessionKey( bodyOrEvent: B | Event, @@ -16,7 +18,7 @@ export interface Connector { createContext(params: { event: Event; session?: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter?: EventEmitter | null; }): any; diff --git a/packages/bottender/src/bot/__tests__/Bot.spec.ts b/packages/bottender/src/bot/__tests__/Bot.spec.ts index 460d4daaf..5acee8ea7 100644 --- a/packages/bottender/src/bot/__tests__/Bot.spec.ts +++ b/packages/bottender/src/bot/__tests__/Bot.spec.ts @@ -1,30 +1,76 @@ +import { mocked } from 'ts-jest/utils'; + import Bot from '../Bot'; -import Context from '../../context/Context'; +import MemorySessionStore from '../../session/MemorySessionStore'; +import { Connector } from '../Connector'; + +jest.mock('../../session/MemorySessionStore'); + +const event = { + rawEvent: { + message: { + text: 'hi', + }, + }, + isMessage: true, + isText: true, + message: { + text: 'hi', + }, +}; + +const session = { + user: { + id: '__id__', + }, +}; + +const body = {}; + +const requestContext = { + method: 'post', + path: '/webhooks/messengr', + query: {}, + headers: {}, + rawBody: '{}', + body: {}, + params: {}, + url: 'https://www.example.com/webhooks/messengr', +}; + +function App() {} function setup({ connector = { platform: 'any', - getUniqueSessionKey: jest.fn(), + getUniqueSessionKey: jest.fn(() => '__id__'), updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [{}]), - createContext: jest.fn(() => ({})), - customAccessToken: 'anyToken', + mapRequestToEvents: jest.fn(() => [event]), + createContext: jest.fn((params) => ({ ...params, session })), }, - sessionStore = { - init: jest.fn(), - read: jest.fn(), - write: jest.fn(), - }, - sync = true, - mapPageToAccessToken, -}) { - const bot = new Bot({ connector, sessionStore, sync, mapPageToAccessToken }); + sessionStore = new MemorySessionStore(), +}: { + connector?: Connector; + sessionStore?: MemorySessionStore; +} = {}): { + bot: Bot; + connector: Connector; + sessionStore: MemorySessionStore; + onRequest: Function; +} { + const onRequest = jest.fn(); + const bot = new Bot({ + connector, + sessionStore, + sync: true, + onRequest, + }); return { bot, connector, sessionStore, - mapPageToAccessToken, + onRequest, }; } @@ -33,113 +79,118 @@ beforeEach(() => { }); describe('#connector', () => { - it('can be access', () => { + it('should be the connector provided to the constructor', () => { const { bot, connector } = setup({}); + expect(bot.connector).toBe(connector); }); }); describe('#sessions', () => { - it('can be access', () => { - const { bot, sessionStore } = setup({}); + it('should be the session store provided to the constructor', () => { + const { bot, sessionStore } = setup(); + expect(bot.sessions).toBe(sessionStore); }); }); describe('#handler', () => { - it('can be access', () => { - const { bot } = setup({}); + it('should be the action registered by onEvent', () => { + const { bot } = setup(); expect(bot.handler).toBeNull(); - const handler = () => {}; - bot.onEvent(handler); - expect(bot.handler).toBe(handler); + bot.onEvent(App); + + expect(bot.handler).toBe(App); }); }); describe('#createRequestHandler', () => { - it('throw when no handler', () => { - const { bot } = setup({}); + it('should throw when no registered action', () => { + const { bot } = setup(); + expect(() => bot.createRequestHandler()).toThrow(); }); - it('throw when call without request body', async () => { - const { bot } = setup({}); + it('requestHandler should throw when be called without a request body', async () => { + const { bot } = setup(); - const handler = () => {}; - bot.onEvent(handler); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); let error; + try { + // @ts-expect-error await requestHandler(); } catch (err) { error = err; } - expect(error).toBeDefined(); + + expect(error).toBeInstanceOf(Error); }); - it('should call updateSession with session and body', async () => { - const { bot, connector, sessionStore } = setup({}); + it('should call updateSession with the session and the event', async () => { + const { bot, connector, sessionStore } = setup(); - connector.getUniqueSessionKey.mockReturnValue('__id__'); - sessionStore.read.mockResolvedValue(null); + mocked(sessionStore).read.mockResolvedValue(null); - const handler = () => {}; - bot.onEvent(handler); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); - expect(connector.updateSession).toBeCalledWith(expect.any(Object), body); + expect(connector.updateSession).toBeCalledWith(expect.any(Object), event); }); - it('should call handler', async () => { - const event = {}; - const session = { user: {} }; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session })), - }; + it('should call the registered action with the context', async () => { + const { bot, sessionStore } = setup(); - const { bot, sessionStore } = setup({ connector }); - - connector.getUniqueSessionKey.mockReturnValue('__id__'); - sessionStore.read.mockResolvedValue(session); + mocked(sessionStore).read.mockResolvedValue(session); let receivedContext; - const Action = context => { + bot.onEvent(function MyAction(context) { receivedContext = context; - }; - bot.onEvent(Action); + }); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); - expect(receivedContext).toEqual({ - event: {}, - isSessionWritten: true, - session: expect.objectContaining({ - id: 'any:__id__', - platform: 'any', - user: {}, - lastActivity: expect.any(Number), - }), - }); + expect(receivedContext).toEqual( + expect.objectContaining({ + event, + isSessionWritten: true, + session: expect.objectContaining({ + id: 'any:__id__', + platform: 'any', + user: session.user, + lastActivity: expect.any(Number), + }), + }) + ); + }); + + it('should call the onRequest function with the body and the request context', async () => { + const { bot, sessionStore, onRequest } = setup(); + + mocked(sessionStore).read.mockResolvedValue(session); + + bot.onEvent(App); + + const requestHandler = bot.createRequestHandler(); + + await requestHandler(body, requestContext); + + expect(onRequest).toBeCalledWith(body, requestContext); }); - it('should return response in sync mode', async () => { - const event = {}; - const session = { user: {} }; - const context = { + it('should return the response in sync mode', async () => { + const { bot, connector, sessionStore } = setup(); + + const ctx = { event, session, response: { @@ -148,33 +199,22 @@ describe('#createRequestHandler', () => { body: null, }, }; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => context), - }; - const { bot, sessionStore } = setup({ connector, sync: true }); + mocked(connector).createContext.mockReturnValue(ctx); + mocked(sessionStore).read.mockResolvedValue(session); - connector.getUniqueSessionKey.mockReturnValue('__id__'); - sessionStore.read.mockResolvedValue(session); - - const handler = ({ response }) => { - response.status = 200; - response.headers = { + bot.onEvent(function MyAction(context) { + context.response.status = 200; + context.response.headers = { 'X-Header': 'x', }; - response.body = { + context.response.body = { name: 'x', }; - }; - bot.onEvent(handler); + }); const requestHandler = bot.createRequestHandler(); - const body = {}; const response = await requestHandler(body); expect(response).toEqual({ @@ -189,21 +229,12 @@ describe('#createRequestHandler', () => { }); it('should throw error if no handler after createRequestHandler', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; + const { bot } = setup(); - const { bot } = setup({ connector, sync: true }); + bot.onEvent(App); - connector.getUniqueSessionKey.mockReturnValue(null); - - bot.onEvent(() => {}); const requestHandler = bot.createRequestHandler(); + bot._handler = null; let error; @@ -212,32 +243,26 @@ describe('#createRequestHandler', () => { } catch (err) { error = err; } + expect(error).toBeDefined(); }); it('should call handler without session if unique session id is null', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot, connector } = setup(); - connector.getUniqueSessionKey.mockReturnValue(null); + mocked(connector).getUniqueSessionKey.mockReturnValue(null); + mocked(connector).createContext.mockReturnValue({ + event, + session: undefined, + }); let receivedContext; - const Action = context => { + bot.onEvent(function MyAction(context) { receivedContext = context; - }; - bot.onEvent(Action); + }); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); expect(receivedContext).toEqual( @@ -248,27 +273,15 @@ describe('#createRequestHandler', () => { }); it('should handle error thrown by async handler', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: 123456 })), - }; - - const { bot } = setup({ connector, sync: true }); + const { bot } = setup(); - connector.getUniqueSessionKey.mockReturnValue(null); - - const handler = jest.fn(async () => { + const handler = async () => { throw new Error('async handler error'); - }); + }; bot.onEvent(handler); const requestHandler = bot.createRequestHandler(); - const body = {}; let error; try { await requestHandler(body); @@ -276,32 +289,19 @@ describe('#createRequestHandler', () => { error = err; } - expect(handler).toBeCalled(); expect(error).toBeUndefined(); }); it('should handle error thrown by sync handler', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: 123456 })), - }; - - const { bot } = setup({ connector, sync: true }); + const { bot } = setup(); - connector.getUniqueSessionKey.mockReturnValue(null); - - const handler = jest.fn(() => { + const handler = () => { throw new Error('sync handler error'); - }); + }; bot.onEvent(handler); const requestHandler = bot.createRequestHandler(); - const body = {}; let error; try { await requestHandler(body); @@ -309,30 +309,18 @@ describe('#createRequestHandler', () => { error = err; } - expect(handler).toBeCalled(); expect(error).toBeUndefined(); }); it('should call write on sessionStore', async () => { const _now = Date.now; Date.now = jest.fn(() => 1504514594622); - const event = {}; - const session = { user: {} }; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session })), - }; - const { bot, sessionStore } = setup({ connector, sync: true }); + const { bot, sessionStore } = setup(); - connector.getUniqueSessionKey.mockReturnValue('__id__'); - sessionStore.read.mockResolvedValue(session); + mocked(sessionStore).read.mockResolvedValue(session); - const handler = () => {}; - bot.onEvent(handler); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); @@ -341,35 +329,55 @@ describe('#createRequestHandler', () => { expect(sessionStore.write).toBeCalledWith('any:__id__', { id: 'any:__id__', platform: 'any', - user: {}, + user: session.user, lastActivity: Date.now(), }); Date.now = _now; }); it('should work with multiple events in one request', async () => { - const event1 = {}; - const event2 = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event1, event2]), - createContext: jest.fn(({ event, session }) => ({ event, session })), + const { bot, connector, sessionStore } = setup(); + + const event1 = { + rawEvent: { + message: { + text: 'hi', + }, + }, + isMessage: true, + isText: true, + message: { + text: 'hi', + }, + }; + const event2 = { + rawEvent: { + message: { + text: 'hi', + }, + }, + isMessage: true, + isText: true, + message: { + text: 'hi', + }, }; - const { bot, sessionStore } = setup({ connector }); - connector.getUniqueSessionKey - .mockReturnValueOnce('1') + mocked(connector) + .getUniqueSessionKey.mockReturnValueOnce('1') .mockReturnValueOnce('2'); - sessionStore.read.mockResolvedValue(null); + mocked(connector).mapRequestToEvents.mockReturnValue([event1, event2]); + mocked(connector).createContext.mockImplementation((params) => ({ + event: params.event, + session: params.session, + })); - const handler = () => {}; - bot.onEvent(handler); + mocked(sessionStore).read.mockResolvedValue(null); + + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); expect(sessionStore.read).toBeCalledWith('any:1'); @@ -391,8 +399,8 @@ describe('#createRequestHandler', () => { }) ); - expect(connector.updateSession).toBeCalledWith(expect.any(Object), body); - expect(connector.updateSession).toBeCalledWith(expect.any(Object), body); + expect(connector.updateSession).toBeCalledWith(expect.any(Object), event1); + expect(connector.updateSession).toBeCalledWith(expect.any(Object), event2); expect(sessionStore.write).toBeCalledWith('any:1', { id: 'any:1', @@ -409,46 +417,26 @@ describe('#createRequestHandler', () => { describe('#use', () => { it('should return this', () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot } = setup(); - expect(bot.use(() => {})).toBe(bot); + expect(bot.use(function plugin() {})).toBe(bot); }); it('can extend function to context', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); - - connector.getUniqueSessionKey.mockReturnValue('__id__'); + const { bot } = setup(); const monkeyPatchFn = jest.fn(); - bot.use(context => { + bot.use(function plugin(context) { context.monkeyPatchFn = monkeyPatchFn; }); - const handler = context => context.monkeyPatchFn(); - bot.onEvent(handler); + bot.onEvent(function MyAction(context) { + context.monkeyPatchFn(); + }); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); expect(monkeyPatchFn).toBeCalled(); @@ -457,39 +445,20 @@ describe('#use', () => { describe('#onEvent', () => { it('should return this', () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot } = setup(); - expect(bot.onEvent(() => {})).toBe(bot); + expect(bot.onEvent(App)).toBe(bot); }); }); describe('#onError', () => { it('should catch thrown handler error', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: ({ emitter }) => - new Context({ event, session: undefined, emitter }), - }; - - const { bot } = setup({ connector }); + const { bot } = setup(); let receivedError; let receivedContext; - const promise = new Promise(resolve => { + const promise = new Promise((resolve) => { bot .onEvent(() => { throw new Error('boom'); @@ -508,54 +477,25 @@ describe('#onError', () => { await promise; expect(receivedError).toBeInstanceOf(Error); - expect(receivedContext).toBeInstanceOf(Context); + expect(receivedContext).toBeDefined(); }); it('should return this', () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot } = setup(); - expect(bot.onError(() => {})).toBe(bot); + expect(bot.onError(function HandleError() {})).toBe(bot); }); }); describe('#setInitialState', () => { it('should return this', () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot } = setup(); expect(bot.setInitialState({})).toBe(bot); }); it('initialState should be passed to createContext', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); - - connector.getUniqueSessionKey.mockReturnValue('__id__'); + const { bot, connector } = setup(); bot.setInitialState({ a: 1, @@ -564,11 +504,10 @@ describe('#setInitialState', () => { }, }); - bot.onEvent(() => {}); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); expect(connector.createContext).toBeCalledWith( @@ -586,25 +525,12 @@ describe('#setInitialState', () => { describe('request context', () => { it('requestContext should be passed to createContext', async () => { - const event = {}; - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ event, session: undefined })), - }; - - const { bot } = setup({ connector }); + const { bot, connector } = setup(); - connector.getUniqueSessionKey.mockReturnValue('__id__'); - - bot.onEvent(() => {}); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); - const body = {}; - const requestContext = { req: {}, res: {} }; await requestHandler(body, requestContext); expect(connector.createContext).toBeCalledWith( @@ -615,31 +541,22 @@ describe('request context', () => { }); }); -describe('context lifecycle', () => { +describe('context life cycle', () => { it('should call context.handlerDidEnd when it exists', async () => { - const event = {}; const handlerDidEnd = jest.fn(); - const connector = { - platform: 'any', - getUniqueSessionKey: jest.fn(), - updateSession: jest.fn(), - mapRequestToEvents: jest.fn(() => [event]), - createContext: jest.fn(() => ({ - event, - session: undefined, - handlerDidEnd, - })), - }; - const { bot } = setup({ connector, sync: true }); + const { bot, connector } = setup(); - connector.getUniqueSessionKey.mockReturnValue('__id__'); + mocked(connector).createContext.mockReturnValue({ + event, + session: undefined, + handlerDidEnd, + }); - bot.onEvent(() => {}); + bot.onEvent(App); const requestHandler = bot.createRequestHandler(); - const body = {}; await requestHandler(body); expect(handlerDidEnd).toBeCalled(); diff --git a/packages/bottender/src/cache/CacheStore.ts b/packages/bottender/src/cache/CacheStore.ts index 79203c9c1..63a7e1fbc 100644 --- a/packages/bottender/src/cache/CacheStore.ts +++ b/packages/bottender/src/cache/CacheStore.ts @@ -1,4 +1,6 @@ -export type CacheValue = number | Record; +import { JsonObject } from 'type-fest'; + +export type CacheValue = number | JsonObject; type CacheStore = { get(key: string): Promise; diff --git a/packages/bottender/src/chain.ts b/packages/bottender/src/chain.ts index 02d9afa63..a7c997b30 100644 --- a/packages/bottender/src/chain.ts +++ b/packages/bottender/src/chain.ts @@ -1,6 +1,7 @@ -import { Action, AnyContext, Props } from './types'; +import Context from './context/Context'; +import { Action, Props } from './types'; -function chain(actions: Action[]) { +function chain(actions: Action[]) { if (!Array.isArray(actions)) throw new TypeError('Chain stack must be an array!'); for (const action of actions) { diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts index a131097de..c22370b3b 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { createPersona } from '../persona'; @@ -17,41 +18,37 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - _client = { - createPersona: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); + process.exit = jest.fn(); -it('be defined', () => { - expect(createPersona).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call createPersona', async () => { const ctx = { + config: null, argv: { + _: [], '--name': 'kpman', '--pic': 'https://i.imgur.com/zV6uy4T.jpg', }, }; - process.exit = jest.fn(); - - _client.createPersona.mockResolvedValue({}); + mocked(MessengerClient.prototype.createPersona).mockResolvedValue({ + id: 'PERSONA_ID', + }); await createPersona(ctx); - expect(MessengerClient.connect).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(MessengerClient).toBeCalledWith({ accessToken: '__FAKE_TOKEN__', }); - expect(_client.createPersona).toBeCalledWith({ + expect(client.createPersona).toBeCalledWith({ name: 'kpman', profilePictureUrl: 'https://i.imgur.com/zV6uy4T.jpg', }); @@ -59,15 +56,15 @@ describe('resolved', () => { it('error when no config setting', async () => { const ctx = { + config: null, argv: { + _: [], '--name': 'kpman', '--pic': 'https://i.imgur.com/zV6uy4T.jpg', }, }; - process.exit = jest.fn(); - - _client.createPersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.createPersona).mockResolvedValue(null); await createPersona(ctx); @@ -76,12 +73,13 @@ describe('resolved', () => { it('error when no persona name and pic', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - process.exit = jest.fn(); - - _client.createPersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.createPersona).mockResolvedValue(null); await createPersona(ctx); @@ -92,7 +90,9 @@ describe('resolved', () => { describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { + config: null, argv: { + _: [], '--name': 'kpman', '--pic': 'https://i.imgur.com/zV6uy4T.jpg', }, @@ -102,9 +102,7 @@ describe('reject', () => { status: 400, }, }; - _client.createPersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.createPersona).mockRejectedValue(error); await createPersona(ctx); @@ -114,7 +112,9 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { + config: null, argv: { + _: [], '--name': 'kpman', '--pic': 'https://i.imgur.com/zV6uy4T.jpg', }, @@ -133,30 +133,28 @@ describe('reject', () => { }, }, }; - _client.createPersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.createPersona).mockRejectedValue(error); await createPersona(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { + config: null, argv: { + _: [], '--name': 'kpman', '--pic': 'https://i.imgur.com/zV6uy4T.jpg', }, }; - const error = { - message: 'something wrong happened', - }; - _client.createPersona.mockRejectedValue(error); - process.exit = jest.fn(); + mocked(MessengerClient.prototype.createPersona).mockRejectedValue( + new Error('something wrong happened') + ); await createPersona(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/deleteMessengerProfile.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/deleteMessengerProfile.spec.ts index 4a2a3435d..2fb7d2c8a 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/deleteMessengerProfile.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/deleteMessengerProfile.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { deleteMessengerProfile } from '../profile'; @@ -17,34 +18,32 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { process.exit = jest.fn(); - _client = { - deleteMessengerProfile: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); -it('be defined', () => { - expect(deleteMessengerProfile).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call deleteMessengerProfile', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.deleteMessengerProfile.mockResolvedValue(); + mocked(MessengerClient.prototype.deleteMessengerProfile).mockResolvedValue( + {} + ); await deleteMessengerProfile(ctx); - expect(_client.deleteMessengerProfile).toBeCalledWith([ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.deleteMessengerProfile).toBeCalledWith([ 'account_linking_url', 'persistent_menu', 'get_started', @@ -58,14 +57,19 @@ describe('resolved', () => { describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; const error = { response: { status: 400, }, }; - _client.deleteMessengerProfile.mockRejectedValue(error); + mocked(MessengerClient.prototype.deleteMessengerProfile).mockRejectedValue( + error + ); await deleteMessengerProfile(ctx); @@ -75,7 +79,10 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; const error = { response: { @@ -91,23 +98,27 @@ describe('reject', () => { }, }, }; - _client.deleteMessengerProfile.mockRejectedValue(error); + mocked(MessengerClient.prototype.deleteMessengerProfile).mockRejectedValue( + error + ); await deleteMessengerProfile(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { - argv: {}, - }; - const error = { - message: 'something wrong happened', + config: null, + argv: { + _: [], + }, }; - _client.deleteMessengerProfile.mockRejectedValue(error); + mocked(MessengerClient.prototype.deleteMessengerProfile).mockRejectedValue( + new Error('something wrong happened') + ); await deleteMessengerProfile(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts index fb6c1e692..3393fdab6 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { deletePersona } from '../persona'; @@ -17,48 +18,48 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - _client = { - deletePersona: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); + process.exit = jest.fn(); -it('be defined', () => { - expect(deletePersona).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call deletePersona', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - process.exit = jest.fn(); - - _client.deletePersona.mockResolvedValue({}); + mocked(MessengerClient.prototype.deletePersona).mockResolvedValue({ + success: true, + }); await deletePersona(ctx); - expect(MessengerClient.connect).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(MessengerClient).toBeCalledWith({ accessToken: '__FAKE_TOKEN__', }); - expect(_client.deletePersona).toBeCalledWith('54321'); + expect(client.deletePersona).toBeCalledWith('54321'); }); it('error when no config setting', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - process.exit = jest.fn(); - - _client.deletePersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.deletePersona).mockResolvedValue(null); await deletePersona(ctx); @@ -67,12 +68,13 @@ describe('resolved', () => { it('error when no persona id', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - process.exit = jest.fn(); - - _client.deletePersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.deletePersona).mockResolvedValue(null); await deletePersona(ctx); @@ -83,16 +85,18 @@ describe('resolved', () => { describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { status: 400, }, }; - _client.deletePersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.deletePersona).mockRejectedValue(error); await deletePersona(ctx); @@ -102,7 +106,11 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { @@ -118,27 +126,26 @@ describe('reject', () => { }, }, }; - _client.deletePersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.deletePersona).mockRejectedValue(error); await deletePersona(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { - argv: { '--id': '54321' }, - }; - const error = { - message: 'something wrong happened', + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - _client.deletePersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.deletePersona).mockRejectedValue( + new Error('something wrong happened') + ); await deletePersona(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/getMessengerProfile.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/getMessengerProfile.spec.ts index 1716ad66a..1a099e3a4 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/getMessengerProfile.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/getMessengerProfile.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { getMessengerProfile } from '../profile'; @@ -17,33 +18,30 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - _client = { - getMessengerProfile: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); + process.exit = jest.fn(); -it('be defined', () => { - expect(getMessengerProfile).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call getMessengerProfile', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.getMessengerProfile.mockResolvedValue({}); + mocked(MessengerClient.prototype.getMessengerProfile).mockResolvedValue({}); await getMessengerProfile(ctx); - expect(_client.getMessengerProfile).toBeCalledWith([ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.getMessengerProfile).toBeCalledWith([ 'account_linking_url', 'persistent_menu', 'get_started', @@ -55,31 +53,41 @@ describe('resolved', () => { it('error when no config setting', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.getMessengerProfile.mockResolvedValue(null); + mocked(MessengerClient.prototype.getMessengerProfile).mockResolvedValue( + null + ); await getMessengerProfile(ctx); + const client = mocked(MessengerClient).mock.instances[0]; + expect(log.error).toBeCalled(); - expect(_client.getMessengerProfile).toBeCalled(); + expect(client.getMessengerProfile).toBeCalled(); }); }); describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; const error = { response: { status: 400, }, }; - _client.getMessengerProfile.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getMessengerProfile).mockRejectedValue( + error + ); await getMessengerProfile(ctx); @@ -89,7 +97,10 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; const error = { response: { @@ -105,27 +116,27 @@ describe('reject', () => { }, }, }; - _client.getMessengerProfile.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getMessengerProfile).mockRejectedValue( + error + ); await getMessengerProfile(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { - argv: {}, - }; - const error = { - message: 'something wrong happened', + config: null, + argv: { + _: [], + }, }; - _client.getMessengerProfile.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getMessengerProfile).mockRejectedValue( + new Error('something wrong happened') + ); await getMessengerProfile(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts index 7999f5090..bf8be3684 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { getPersona } from '../persona'; @@ -17,48 +18,50 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - _client = { - getPersona: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); + process.exit = jest.fn(); -it('be defined', () => { - expect(getPersona).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call getPersona', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - process.exit = jest.fn(); - - _client.getPersona.mockResolvedValue({}); + mocked(MessengerClient.prototype.getPersona).mockResolvedValue({ + id: 'PERSONA_ID', + name: 'PERSONA_NAME', + profilePictureUrl: 'PERSONA_PROFILE_PICTURE_URL', + }); await getPersona(ctx); - expect(MessengerClient.connect).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(MessengerClient).toBeCalledWith({ accessToken: '__FAKE_TOKEN__', }); - expect(_client.getPersona).toBeCalledWith('54321'); + expect(client.getPersona).toBeCalledWith('54321'); }); it('error when no config setting', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - process.exit = jest.fn(); - - _client.getPersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.getPersona).mockResolvedValue(null); await getPersona(ctx); @@ -67,12 +70,13 @@ describe('resolved', () => { it('error when no persona id', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - process.exit = jest.fn(); - - _client.getPersona.mockResolvedValue(null); + mocked(MessengerClient.prototype.getPersona).mockResolvedValue(null); await getPersona(ctx); @@ -83,16 +87,18 @@ describe('resolved', () => { describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { status: 400, }, }; - _client.getPersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getPersona).mockRejectedValue(error); await getPersona(ctx); @@ -102,7 +108,11 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { @@ -118,27 +128,26 @@ describe('reject', () => { }, }, }; - _client.getPersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getPersona).mockRejectedValue(error); await getPersona(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { - argv: { '--id': '54321' }, - }; - const error = { - message: 'something wrong happened', + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - _client.getPersona.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getPersona).mockRejectedValue( + new Error('something wrong happened') + ); await getPersona(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts index 61ab5c6a0..9cffa0dea 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { listPersona } from '../persona'; @@ -17,49 +18,46 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - _client = { - getAllPersonas: jest.fn(), - }; - MessengerClient.connect = jest.fn(() => _client); - log.error = jest.fn(); - log.print = jest.fn(); + process.exit = jest.fn(); console.log = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); -it('be defined', () => { - expect(listPersona).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); }); describe('resolved', () => { it('call listPersona', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - process.exit = jest.fn(); - - _client.getAllPersonas.mockResolvedValue({}); + mocked(MessengerClient.prototype.getAllPersonas).mockResolvedValue({}); await listPersona(ctx); - expect(MessengerClient.connect).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(MessengerClient).toBeCalledWith({ accessToken: '__FAKE_TOKEN__', }); - expect(_client.getAllPersonas).toBeCalled(); + expect(client.getAllPersonas).toBeCalled(); }); it('error when no config setting', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - process.exit = jest.fn(); - - _client.getAllPersonas.mockResolvedValue(null); + mocked(MessengerClient.prototype.getAllPersonas).mockResolvedValue(null); await listPersona(ctx); @@ -70,16 +68,18 @@ describe('resolved', () => { describe('reject', () => { it('handle error thrown with only status', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { status: 400, }, }; - _client.getAllPersonas.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getAllPersonas).mockRejectedValue(error); await listPersona(ctx); @@ -89,7 +89,11 @@ describe('reject', () => { it('handle error thrown by messenger', async () => { const ctx = { - argv: { '--id': '54321' }, + config: null, + argv: { + _: [], + '--id': '54321', + }, }; const error = { response: { @@ -105,27 +109,26 @@ describe('reject', () => { }, }, }; - _client.getAllPersonas.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getAllPersonas).mockRejectedValue(error); await listPersona(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); it('handle error thrown by ourselves', async () => { const ctx = { - argv: { '--id': '54321' }, - }; - const error = { - message: 'something wrong happened', + config: null, + argv: { + _: [], + '--id': '54321', + }, }; - _client.getAllPersonas.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.getAllPersonas).mockRejectedValue( + new Error('something wrong happened') + ); await listPersona(ctx); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/profile.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/profile.spec.ts index 2dac31fd8..643295d31 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/profile.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/profile.spec.ts @@ -1,15 +1,13 @@ import { trimDomain } from '../profile'; -it('should be defined', () => { - expect(trimDomain).toBeDefined(); -}); - describe('#trimDomain', () => { it('should remove whitelisted_domains end slash', () => { const testProfile = { whitelistedDomains: ['https://www.facebook.com/', 'https://facebook.com'], }; + const trimProfile = trimDomain(testProfile); + expect(trimProfile).toEqual({ whitelistedDomains: ['https://www.facebook.com', 'https://facebook.com'], }); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/setMessengerProfile.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/setMessengerProfile.spec.ts index bfe8eb510..50fad9495 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/setMessengerProfile.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/setMessengerProfile.spec.ts @@ -1,4 +1,5 @@ import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { setMessengerProfile } from '../profile'; @@ -36,17 +37,14 @@ const MOCK_FILE_WITH_PLATFORM = { }, }; -let _client; - beforeEach(() => { - process.NODE_ENV = 'test'; process.exit = jest.fn(); - _client = { - setMessengerProfile: jest.fn(), - deleteMessengerProfile: jest.fn(), - getMessengerProfile: jest.fn(), - }; - _client.getMessengerProfile.mockReturnValue([ + + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.messenger + ); + + mocked(MessengerClient.prototype.getMessengerProfile).mockReturnValue([ { persistentMenu: [ { @@ -63,22 +61,24 @@ beforeEach(() => { ], }, ]); - MessengerClient.connect = jest.fn(() => _client); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.messenger); -}); - -it('be defined', () => { - expect(setMessengerProfile).toBeDefined(); }); describe('resolve', () => { describe('--force', () => { it('will delete all fields and set profile', async () => { const ctx = { - argv: { '--force': true }, + config: null, + argv: { + _: [], + '--force': true, + }, }; + await setMessengerProfile(ctx); - expect(_client.deleteMessengerProfile).toBeCalledWith([ + + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.deleteMessengerProfile).toBeCalledWith([ 'account_linking_url', 'persistent_menu', 'get_started', @@ -86,7 +86,7 @@ describe('resolve', () => { 'ice_breakers', 'whitelisted_domains', ]); - expect(_client.setMessengerProfile).toBeCalledWith({ + expect(client.setMessengerProfile).toBeCalledWith({ getStarted: { payload: '', }, @@ -108,11 +108,14 @@ describe('resolve', () => { it('should set whole profile once', async () => { const ctx = { + config: null, argv: { + _: [], '--force': true, }, }; - getChannelConfig.mockReturnValue({ + + mocked(getChannelConfig).mockReturnValue({ accessToken: '__FAKE_TOKEN__', profile: { getStarted: { @@ -137,7 +140,9 @@ describe('resolve', () => { await setMessengerProfile(ctx); - expect(_client.setMessengerProfile).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.setMessengerProfile).toBeCalledWith({ getStarted: { payload: '', }, @@ -160,11 +165,13 @@ describe('resolve', () => { it('should set whitelisted_domains before other fields', async () => { const ctx = { + config: null, argv: { + _: [], '--force': true, }, }; - getChannelConfig.mockReturnValue({ + mocked(getChannelConfig).mockReturnValue({ accessToken: '__FAKE_TOKEN__', profile: { getStarted: { @@ -196,10 +203,12 @@ describe('resolve', () => { await setMessengerProfile(ctx); - expect(_client.setMessengerProfile.mock.calls[0][0]).toEqual({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(mocked(client.setMessengerProfile).mock.calls[0][0]).toEqual({ whitelistedDomains: ['http://example.com'], }); - expect(_client.setMessengerProfile.mock.calls[1][0]).toEqual({ + expect(mocked(client.setMessengerProfile).mock.calls[1][0]).toEqual({ getStarted: { payload: '', }, @@ -229,9 +238,12 @@ describe('resolve', () => { it('successfully set diff fields `persistent_menu`', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.getMessengerProfile.mockReturnValue([ + mocked(MessengerClient.prototype.getMessengerProfile).mockReturnValue([ { persistentMenu: [ { @@ -251,7 +263,9 @@ describe('resolve', () => { await setMessengerProfile(ctx); - expect(_client.setMessengerProfile).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.setMessengerProfile).toBeCalledWith({ getStarted: { payload: '', }, @@ -273,9 +287,12 @@ describe('resolve', () => { it('should set whitelisted_domains before other fields', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.getMessengerProfile.mockReturnValue([ + mocked(MessengerClient.prototype.getMessengerProfile).mockReturnValue([ { persistent_menu: [ { @@ -292,7 +309,7 @@ describe('resolve', () => { ], }, ]); - getChannelConfig.mockReturnValue({ + mocked(getChannelConfig).mockReturnValue({ accessToken: '__FAKE_TOKEN__', profile: { getStarted: { @@ -324,10 +341,12 @@ describe('resolve', () => { await setMessengerProfile(ctx); - expect(_client.setMessengerProfile.mock.calls[0][0]).toEqual({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(mocked(client.setMessengerProfile).mock.calls[0][0]).toEqual({ whitelistedDomains: ['http://example.com'], }); - expect(_client.setMessengerProfile.mock.calls[1][0]).toEqual({ + expect(mocked(client.setMessengerProfile).mock.calls[1][0]).toEqual({ getStarted: { payload: '', }, @@ -356,12 +375,17 @@ describe('resolve', () => { it('successfully set diff fields `get_started`', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; await setMessengerProfile(ctx); - expect(_client.setMessengerProfile).toBeCalledWith({ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.setMessengerProfile).toBeCalledWith({ getStarted: { payload: '', }, @@ -369,7 +393,7 @@ describe('resolve', () => { }); it('successfully delete diff fields `whitelisted_domains`', async () => { - _client.getMessengerProfile.mockReturnValue([ + mocked(MessengerClient.prototype.getMessengerProfile).mockReturnValue([ { persistentMenu: [ { @@ -391,18 +415,23 @@ describe('resolve', () => { }, ]); const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; await setMessengerProfile(ctx); - expect(_client.deleteMessengerProfile).toBeCalledWith([ + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.deleteMessengerProfile).toBeCalledWith([ 'whitelisted_domains', ]); }); it('do nothing and log info', async () => { - _client.getMessengerProfile.mockReturnValue([ + mocked(MessengerClient.prototype.getMessengerProfile).mockReturnValue([ { getStarted: { payload: '', @@ -423,13 +452,18 @@ describe('resolve', () => { }, ]); const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; await setMessengerProfile(ctx); - expect(_client.deleteMessengerProfile).not.toBeCalled(); - expect(_client.setMessengerProfile).not.toBeCalled(); + const client = mocked(MessengerClient).mock.instances[0]; + + expect(client.deleteMessengerProfile).not.toBeCalled(); + expect(client.setMessengerProfile).not.toBeCalled(); expect(log.print).toHaveBeenCalledWith( `No change apply because the profile settings is the same.` ); @@ -444,12 +478,15 @@ describe('reject', () => { }, }; const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.setMessengerProfile.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.setMessengerProfile).mockRejectedValue( + error + ); await setMessengerProfile(ctx); @@ -473,16 +510,19 @@ describe('reject', () => { }, }; const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - _client.setMessengerProfile.mockRejectedValue(error); - - process.exit = jest.fn(); + mocked(MessengerClient.prototype.setMessengerProfile).mockRejectedValue( + error + ); await setMessengerProfile(ctx); expect(log.error).toBeCalled(); - expect(log.error.mock.calls[2][0]).not.toMatch(/\[object Object\]/); + expect(mocked(log.error).mock.calls[2][0]).not.toMatch(/\[object Object\]/); expect(process.exit).toBeCalled(); }); }); diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts index e1dc3fc9e..59abd01cd 100644 --- a/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts @@ -1,5 +1,6 @@ import Confirm from 'prompt-confirm'; import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import getWebhookFromNgrok from '../../../../shared/getWebhookFromNgrok'; @@ -23,10 +24,17 @@ const WEBHOOK = 'http://example.com/webhook'; function setup({ config, success = true, -}: { config?: Record; success?: boolean } = {}) { - getWebhookFromNgrok.mockResolvedValue('https://fakeDomain.ngrok.io'); +}: { + config?: Record; + success?: boolean; +} = {}) { + process.exit = jest.fn(); + + log.bold = jest.fn((s) => s); + + mocked(getWebhookFromNgrok).mockResolvedValue('https://fakeDomain.ngrok.io'); - getChannelConfig.mockReturnValue( + mocked(getChannelConfig).mockReturnValue( config || { accessToken: ACCESS_TOKEN, appId: APP_ID, @@ -35,14 +43,11 @@ function setup({ } ); - const client = { - createSubscription: jest.fn(), - debugToken: jest.fn(), - getPageInfo: jest.fn(), - }; + mocked(MessengerClient.prototype.createSubscription).mockResolvedValue({ + success, + }); - client.createSubscription.mockResolvedValue({ success }); - client.debugToken.mockResolvedValue({ + mocked(MessengerClient.prototype.debugToken).mockResolvedValue({ type: 'PAGE', appId: '000000000000000', application: 'Social Cafe', @@ -53,39 +58,28 @@ function setup({ userId: 1207059, }); - client.getPageInfo.mockResolvedValue({ id: '123456789', name: 'Page Name' }); - client.axios = { - post: jest.fn(), - }; - - client.axios.post.mockResolvedValue({ data: { success: true } }); + mocked(MessengerClient.prototype.getPageInfo).mockResolvedValue({ + id: '123456789', + name: 'Page Name', + }); - MessengerClient.connect = jest.fn(() => client); + MessengerClient.prototype.axios = { + post: jest.fn().mockResolvedValue({ data: { success: true } }), + }; Confirm.mockImplementation(() => ({ run: jest.fn().mockResolvedValue(true), })); - log.print = jest.fn(); - log.error = jest.fn(); - log.bold = jest.fn(s => s); - - process.exit = jest.fn(); - - return { - client, - }; + return {}; } -it('be defined', () => { - expect(setWebhook).toBeDefined(); -}); - describe('resolve', () => { it('successfully set webhook with default fields and show messages', async () => { - const { client } = setup(); + setup(); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set', `--webhook=${WEBHOOK}`], }, @@ -93,6 +87,8 @@ describe('resolve', () => { await setWebhook(ctx); + const client = mocked(MessengerClient).mock.instances[0]; + expect(client.createSubscription).toBeCalledWith({ accessToken: '__APP_ID__|__APP_SECRET__', callbackUrl: 'http://example.com/webhook', @@ -112,13 +108,14 @@ describe('resolve', () => { }); it('get ngrok webhook to set up', async () => { - const { client } = setup(); + setup(); - client.axios.post = jest - .fn() - .mockResolvedValue({ data: { success: true } }); + mocked(MessengerClient.prototype.axios.post).mockResolvedValue({ + data: { success: true }, + }); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set'], }, @@ -126,6 +123,8 @@ describe('resolve', () => { await setWebhook(ctx); + const client = mocked(MessengerClient).mock.instances[0]; + expect(getWebhookFromNgrok).toBeCalledWith('4040'); expect(client.createSubscription).toBeCalledWith({ accessToken: '__APP_ID__|__APP_SECRET__', @@ -147,6 +146,7 @@ describe('resolve', () => { setup(); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set', `--ngrok-port=5555`], }, @@ -158,7 +158,7 @@ describe('resolve', () => { }); it('should subscribe app for the page if pageId is provided', async () => { - const { client } = setup({ + setup({ config: { pageId: PAGE_ID, accessToken: ACCESS_TOKEN, @@ -169,6 +169,7 @@ describe('resolve', () => { }); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set'], }, @@ -176,6 +177,8 @@ describe('resolve', () => { await setWebhook(ctx); + const client = mocked(MessengerClient).mock.instances[0]; + expect(getWebhookFromNgrok).toBeCalledWith('4040'); expect(client.createSubscription).toBeCalledWith({ accessToken: '__APP_ID__|__APP_SECRET__', @@ -212,6 +215,7 @@ describe('reject', () => { }); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set'], }, @@ -232,6 +236,7 @@ describe('reject', () => { }); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set'], }, @@ -247,6 +252,7 @@ describe('reject', () => { setup({ success: false }); const ctx = { + config: null, argv: { _: ['messenger', 'webhook', 'set'], }, diff --git a/packages/bottender/src/cli/providers/messenger/persona.ts b/packages/bottender/src/cli/providers/messenger/persona.ts index 236982e5b..95805c6d9 100644 --- a/packages/bottender/src/cli/providers/messenger/persona.ts +++ b/packages/bottender/src/cli/providers/messenger/persona.ts @@ -73,7 +73,7 @@ export async function createPersona(ctx: CliContext): Promise { '`pic` is required but not found. Use --pic to specify persona profile picture URL' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); @@ -112,7 +112,7 @@ export async function listPersona(_: CliContext): Promise { '`accessToken` is not found in the `bottender.config.js` file' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); @@ -124,7 +124,7 @@ export async function listPersona(_: CliContext): Promise { head: ['id', 'name', 'image URL'], colWidths: [30, 30, 30], }); - personas.forEach(item => { + personas.forEach((item) => { table.push([item.id, item.name, item.profilePictureUrl] as any); }); console.log(table.toString()); // eslint-disable-line no-console @@ -166,7 +166,7 @@ export async function getPersona(ctx: CliContext): Promise { '`id` is required but not found. Use --id to specify persona id' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); @@ -218,7 +218,7 @@ export async function deletePersona(ctx: CliContext): Promise { '`id` is required but not found. Use --id to specify persona id' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); diff --git a/packages/bottender/src/cli/providers/messenger/profile.ts b/packages/bottender/src/cli/providers/messenger/profile.ts index 3099de5c4..d53671295 100644 --- a/packages/bottender/src/cli/providers/messenger/profile.ts +++ b/packages/bottender/src/cli/providers/messenger/profile.ts @@ -73,7 +73,7 @@ export async function getMessengerProfile(_: CliContext): Promise { '`accessToken` is not found in the `bottender.config.js` file' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); @@ -122,7 +122,7 @@ export async function setMessengerProfile(ctx: CliContext): Promise { const { profile: _profile } = getChannelConfig(Channel.Messenger); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); @@ -162,7 +162,7 @@ export async function setMessengerProfile(ctx: CliContext): Promise { if (shouldDeleteFields.length > 0) { await client.deleteMessengerProfile( - shouldDeleteFields.map(field => snakecase(field)) + shouldDeleteFields.map((field) => snakecase(field)) ); const deleteFileds = shouldDeleteFields.join(', '); print(`Successfully delete ${bold(deleteFileds)} settings`); @@ -221,7 +221,7 @@ export async function deleteMessengerProfile(_: CliContext): Promise { const accessToken = config.accessToken; - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, }); diff --git a/packages/bottender/src/cli/providers/messenger/webhook.ts b/packages/bottender/src/cli/providers/messenger/webhook.ts index 3b320c50f..334625638 100644 --- a/packages/bottender/src/cli/providers/messenger/webhook.ts +++ b/packages/bottender/src/cli/providers/messenger/webhook.ts @@ -72,7 +72,7 @@ export async function setWebhook(ctx: CliContext): Promise { '`verifyToken` is not found in the `bottender.config.js` file' ); - const client = MessengerClient.connect({ + const client = new MessengerClient({ accessToken, appId, appSecret, diff --git a/packages/bottender/src/cli/providers/sh/start.ts b/packages/bottender/src/cli/providers/sh/start.ts index f8a8873c3..559f4376c 100644 --- a/packages/bottender/src/cli/providers/sh/start.ts +++ b/packages/bottender/src/cli/providers/sh/start.ts @@ -1,4 +1,9 @@ -import initializeServer from '../../../initializeServer'; +import * as http from 'http'; + +import bodyParser from 'body-parser'; +import express from 'express'; + +import bottender from '../../../bottender'; import { CliContext } from '../..'; import getSubArgs from './utils/getSubArgs'; @@ -13,16 +18,42 @@ const start = async (ctx: CliContext): Promise => { '-p': '--port', }); - const isConsole = argv['--console'] || false; + const useConsole = argv['--console'] || false; const port = argv['--port'] || process.env.PORT || 5000; - const server = initializeServer({ isConsole }); + const app = bottender({ + dev: process.env.NODE_ENV !== 'production', + useConsole, + }); + + const handle = app.getRequestHandler(); + + await app.prepare(); - if (server) { - server.listen(port, () => { - console.log(`server is running on ${port} port...`); - }); + if (useConsole) { + return; } + + const server = express(); + + const verify = ( + req: http.IncomingMessage & { rawBody?: string }, + _: http.ServerResponse, + buf: Buffer + ) => { + req.rawBody = buf.toString(); + }; + server.use(bodyParser.json({ verify })); + server.use(bodyParser.urlencoded({ extended: false, verify })); + + server.all('*', (req, res) => { + return handle(req, res); + }); + + server.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`server is running on ${port} port...`); + }); }; export default start; diff --git a/packages/bottender/src/cli/providers/telegram/__tests__/deleteWebhook.spec.ts b/packages/bottender/src/cli/providers/telegram/__tests__/deleteWebhook.spec.ts index 5b6275089..039aa95e7 100644 --- a/packages/bottender/src/cli/providers/telegram/__tests__/deleteWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/telegram/__tests__/deleteWebhook.spec.ts @@ -1,4 +1,5 @@ import { TelegramClient } from 'messaging-api-telegram'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { deleteWebhook } from '../webhook'; @@ -19,48 +20,53 @@ const MOCK_FILE_WITH_PLATFORM = { beforeEach(() => { process.exit = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.telegram); - TelegramClient.connect.mockReturnValue({ - deleteWebhook: jest.fn().mockResolvedValue(true), - }); -}); - -it('be defined', () => { - expect(deleteWebhook).toBeDefined(); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.telegram + ); }); describe('resolve', () => { it('successfully delete webhook', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; + mocked(TelegramClient.prototype.deleteWebhook).mockResolvedValue(true); + await deleteWebhook(ctx); expect(log.print).toHaveBeenCalledTimes(1); - expect(log.print.mock.calls[0][0]).toMatch(/Successfully/); + expect(mocked(log.print).mock.calls[0][0]).toMatch(/Successfully/); }); }); describe('reject', () => { it('reject when Telegram return not success', () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - TelegramClient.connect().deleteWebhook.mockResolvedValueOnce({ - ok: false, - }); + mocked(TelegramClient.prototype.deleteWebhook).mockResolvedValue(false); expect(deleteWebhook(ctx).then).toThrow(); }); it('reject when `accessToken` is not found in the `bottender.config.js` file', () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - getChannelConfig.mockReturnValueOnce({}); + + mocked(getChannelConfig).mockReturnValueOnce({}); expect(deleteWebhook(ctx).then).toThrow(); }); diff --git a/packages/bottender/src/cli/providers/telegram/__tests__/getWebhook.spec.ts b/packages/bottender/src/cli/providers/telegram/__tests__/getWebhook.spec.ts index e4ca08c8c..8845a1f2e 100644 --- a/packages/bottender/src/cli/providers/telegram/__tests__/getWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/telegram/__tests__/getWebhook.spec.ts @@ -1,4 +1,5 @@ import { TelegramClient } from 'messaging-api-telegram'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { getWebhook } from '../webhook'; @@ -19,26 +20,26 @@ const MOCK_FILE_WITH_PLATFORM = { beforeEach(() => { process.exit = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.telegram); - TelegramClient.connect.mockReturnValue({ - getWebhookInfo: jest.fn().mockResolvedValue({ - url: 'https://4a16faff.ngrok.io/', - has_custom_certificate: false, - pending_update_count: 0, - max_connections: 40, - }), - }); -}); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.telegram + ); -it('be defined', () => { - expect(getWebhook).toBeDefined(); + mocked(TelegramClient.prototype.getWebhookInfo).mockResolvedValue({ + url: 'https://4a16faff.ngrok.io/', + hasCustomCertificate: false, + pendingUpdateCount: 0, + maxConnections: 40, + }); }); describe('resolve', () => { it('successfully get webhook', async () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; await getWebhook(ctx); @@ -50,9 +51,12 @@ describe('resolve', () => { describe('reject', () => { it('reject when Telegram return not success', () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - TelegramClient.connect().getWebhookInfo.mockResolvedValueOnce({ + mocked(TelegramClient.prototype.getWebhookInfo).mockResolvedValueOnce({ ok: false, }); @@ -61,10 +65,13 @@ describe('reject', () => { it('reject when `accessToken` is not found in the `bottender.config.js` file', () => { const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - getChannelConfig.mockReturnValueOnce({}); + mocked(getChannelConfig).mockReturnValueOnce({}); expect(getWebhook(ctx).then).toThrow(); }); diff --git a/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts index e35336e24..41f2663ed 100644 --- a/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts @@ -1,5 +1,6 @@ import Confirm from 'prompt-confirm'; import { TelegramClient } from 'messaging-api-telegram'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import getWebhookFromNgrok from '../../../../shared/getWebhookFromNgrok'; @@ -23,13 +24,14 @@ const MOCK_FILE_WITH_PLATFORM = { }; const setup = ( - { webhook = undefined, ngrokPort = undefined, token = undefined } = { + { webhook = undefined, ngrokPort = undefined } = { webhook: undefined, ngrokPort: undefined, - token: undefined, } ) => ({ + config: null, argv: { + _: [], '--webhook': webhook, '--ngrok-port': ngrokPort, }, @@ -37,21 +39,18 @@ const setup = ( beforeEach(() => { process.exit = jest.fn(); - getChannelConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.telegram); - getWebhookFromNgrok.mockResolvedValue('https://fakeDomain.ngrok.io'); + mocked(getChannelConfig).mockReturnValue( + MOCK_FILE_WITH_PLATFORM.channels.telegram + ); + + mocked(getWebhookFromNgrok).mockResolvedValue('https://fakeDomain.ngrok.io'); Confirm.mockImplementation(() => ({ run: jest.fn().mockResolvedValue(true), })); - TelegramClient.connect.mockReturnValue({ - setWebhook: jest.fn().mockResolvedValue(true), - }); -}); - -it('be defined', () => { - expect(setWebhook).toBeDefined(); + mocked(TelegramClient.prototype.setWebhook).mockResolvedValue(true); }); describe('resolve', () => { @@ -61,7 +60,7 @@ describe('resolve', () => { await setWebhook(ctx); expect(log.print).toHaveBeenCalledTimes(1); - expect(log.print.mock.calls[0][0]).toMatch(/Successfully/); + expect(mocked(log.print).mock.calls[0][0]).toMatch(/Successfully/); }); it('get ngrok webhook to set up', async () => { @@ -71,12 +70,11 @@ describe('resolve', () => { expect(getWebhookFromNgrok).toBeCalledWith('4040'); expect(log.print).toHaveBeenCalledTimes(1); - expect(log.print.mock.calls[0][0]).toMatch(/Successfully/); + expect(mocked(log.print).mock.calls[0][0]).toMatch(/Successfully/); }); it('set ngrok webhook port', async () => { const ctx = setup({ ngrokPort: '5555' }); - ctx.argv['ngrok-port'] = ctx.argv.ngrokPort; await setWebhook(ctx); @@ -88,7 +86,7 @@ describe('reject', () => { it('reject when accessToken not found in config file', async () => { const ctx = setup({ webhook: 'http://example.com/webhook' }); - getChannelConfig.mockReturnValue({}); + mocked(getChannelConfig).mockReturnValue({}); await setWebhook(ctx); @@ -101,9 +99,7 @@ describe('reject', () => { it('reject when telegram return not success', async () => { const ctx = setup({ webhook: 'http://example.com/webhook' }); - TelegramClient.connect().setWebhook.mockImplementation(() => { - throw new Error(); - }); + mocked(TelegramClient.prototype.setWebhook).mockRejectedValue(new Error()); await setWebhook(ctx); diff --git a/packages/bottender/src/cli/providers/telegram/webhook.ts b/packages/bottender/src/cli/providers/telegram/webhook.ts index 6594b0e4e..be25f2f5f 100644 --- a/packages/bottender/src/cli/providers/telegram/webhook.ts +++ b/packages/bottender/src/cli/providers/telegram/webhook.ts @@ -22,7 +22,7 @@ export async function getWebhook(_: CliContext): Promise { '`accessToken` is not found in the `bottender.config.js` file' ); - const client = TelegramClient.connect({ + const client = new TelegramClient({ accessToken, }); @@ -65,7 +65,7 @@ export async function setWebhook(ctx: CliContext): Promise { '`accessToken` is not found in the `bottender.config.js` file' ); - const client = TelegramClient.connect({ + const client = new TelegramClient({ accessToken, }); @@ -115,7 +115,7 @@ export async function deleteWebhook(_: CliContext): Promise { '`accessToken` is not found in the `bottender.config.js` file' ); - const client = TelegramClient.connect({ + const client = new TelegramClient({ accessToken, }); diff --git a/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts b/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts index b1dd050a1..e7e8517d2 100644 --- a/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts @@ -1,4 +1,5 @@ import { ViberClient } from 'messaging-api-viber'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import { deleteWebhook } from '../webhook'; @@ -11,7 +12,9 @@ jest.mock('../../../../shared/getChannelConfig'); const ACCESS_TOKEN = '__ACCESS_TOKEN__'; function setup({ config }: { config?: Record } = {}) { - getChannelConfig.mockReturnValue( + process.exit = jest.fn(); + + mocked(getChannelConfig).mockReturnValue( config || { accessToken: ACCESS_TOKEN, sender: { @@ -19,29 +22,24 @@ function setup({ config }: { config?: Record } = {}) { }, } ); - - process.exit = jest.fn(); - - ViberClient.connect.mockReturnValue({ - removeWebhook: jest.fn(() => ({ - status: 0, - status_message: 'ok', - })), - }); } -it('be defined', () => { - expect(deleteWebhook).toBeDefined(); -}); - describe('resolve', () => { it('successfully delete webhook', async () => { setup(); const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; + mocked(ViberClient.prototype.removeWebhook).mockResolvedValue({ + status: 0, + statusMessage: 'ok', + }); + await deleteWebhook(ctx); expect(log.print).toBeCalled(); @@ -53,10 +51,13 @@ describe('reject', () => { setup(); const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; - ViberClient.connect().removeWebhook.mockRejectedValueOnce( + mocked(ViberClient.prototype.removeWebhook).mockRejectedValueOnce( new Error('removeWebhook failed') ); @@ -66,10 +67,13 @@ describe('reject', () => { it('reject when `accessToken` is not found in the `bottender.config.js` file', () => { setup(); - getChannelConfig.mockReturnValueOnce(null); + mocked(getChannelConfig).mockReturnValueOnce(null); const ctx = { - argv: {}, + config: null, + argv: { + _: [], + }, }; expect(deleteWebhook(ctx).then).toThrow(); diff --git a/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts index f43e0c8dd..6f1e643bb 100644 --- a/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts +++ b/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts @@ -1,5 +1,6 @@ import Confirm from 'prompt-confirm'; import { ViberClient } from 'messaging-api-viber'; +import { mocked } from 'ts-jest/utils'; import getChannelConfig from '../../../../shared/getChannelConfig'; import getWebhookFromNgrok from '../../../../shared/getWebhookFromNgrok'; @@ -17,9 +18,13 @@ const ACCESS_TOKEN = '__ACCESS_TOKEN__'; const WEBHOOK = 'http://example.com/webhook'; function setup({ config }: { config?: Record } = {}) { - getWebhookFromNgrok.mockResolvedValue('https://fakeDomain.ngrok.io'); + process.exit = jest.fn(); + + log.bold = jest.fn((s) => s); - getChannelConfig.mockReturnValue( + mocked(getWebhookFromNgrok).mockResolvedValue('https://fakeDomain.ngrok.io'); + + mocked(getChannelConfig).mockReturnValue( config || { accessToken: ACCESS_TOKEN, sender: { @@ -28,14 +33,10 @@ function setup({ config }: { config?: Record } = {}) { } ); - const client = { - setWebhook: jest.fn(), - }; - - client.setWebhook.mockResolvedValue({ + mocked(ViberClient.prototype.setWebhook).mockResolvedValue({ status: 0, - status_message: 'ok', - event_types: [ + statusMessage: 'ok', + eventTypes: [ 'delivered', 'seen', 'failed', @@ -45,21 +46,11 @@ function setup({ config }: { config?: Record } = {}) { ], }); - ViberClient.connect.mockReturnValue(client); - Confirm.mockImplementation(() => ({ run: jest.fn().mockResolvedValue(true), })); - log.print = jest.fn(); - log.error = jest.fn(); - log.bold = jest.fn(s => s); - - process.exit = jest.fn(); - - return { - client, - }; + return {}; } it('be defined', () => { @@ -68,7 +59,7 @@ it('be defined', () => { describe('resolve', () => { it('successfully set webhook', async () => { - const { client } = setup({ + setup({ config: { accessToken: ACCESS_TOKEN, sender: { @@ -79,6 +70,7 @@ describe('resolve', () => { }); const ctx = { + config: null, argv: { _: ['viber', 'webhook', 'set', `--webhook=${WEBHOOK}`], }, @@ -86,6 +78,8 @@ describe('resolve', () => { await setWebhook(ctx); + const client = mocked(ViberClient).mock.instances[0]; + expect(client.setWebhook).toBeCalledWith('http://example.com/webhook', [ 'delivered', 'seen', @@ -95,9 +89,10 @@ describe('resolve', () => { }); it('get ngrok webhook to set up', async () => { - const { client } = setup(); + setup(); const ctx = { + config: null, argv: { _: ['viber', 'webhook', 'set'], }, @@ -105,6 +100,8 @@ describe('resolve', () => { await setWebhook(ctx); + const client = mocked(ViberClient).mock.instances[0]; + expect(getWebhookFromNgrok).toBeCalledWith('4040'); expect(client.setWebhook).toBeCalledWith( 'https://fakeDomain.ngrok.io/webhooks/viber', @@ -117,6 +114,7 @@ describe('resolve', () => { setup(); const ctx = { + config: null, argv: { _: ['viber', 'webhook', 'set', `--ngrok-port=5555`], }, @@ -133,6 +131,7 @@ describe('reject', () => { setup({ config: {} }); const ctx = { + config: null, argv: { _: ['viber', 'webhoook', 'set'], }, @@ -147,11 +146,14 @@ describe('reject', () => { }); it('reject when viber return not success', async () => { - const { client } = setup(); + setup(); - client.setWebhook.mockRejectedValueOnce(new Error('setWebhook failed')); + mocked(ViberClient.prototype.setWebhook).mockRejectedValueOnce( + new Error('setWebhook failed') + ); const ctx = { + config: null, argv: { _: ['viber', 'webhoook', 'set'], }, diff --git a/packages/bottender/src/cli/providers/viber/webhook.ts b/packages/bottender/src/cli/providers/viber/webhook.ts index 7f4ebda30..e6f7d2891 100644 --- a/packages/bottender/src/cli/providers/viber/webhook.ts +++ b/packages/bottender/src/cli/providers/viber/webhook.ts @@ -42,13 +42,10 @@ export async function setWebhook(ctx: CliContext): Promise { '`sender` is not found in the `bottender.config.js` file' ); - const client = ViberClient.connect( - { - accessToken, - sender, - }, - sender - ); + const client = new ViberClient({ + accessToken, + sender, + }); if (!webhook) { warn('We can not find the webhook callback URL you provided.'); @@ -103,13 +100,10 @@ export async function deleteWebhook(_: CliContext): Promise { '`sender` is not found in the `bottender.config.js` file' ); - const client = ViberClient.connect( - { - accessToken, - sender, - }, - sender - ); + const client = new ViberClient({ + accessToken, + sender, + }); await client.removeWebhook(); diff --git a/packages/bottender/src/console/ConsoleBot.ts b/packages/bottender/src/console/ConsoleBot.ts index 903e38c44..2ecc08ac2 100644 --- a/packages/bottender/src/console/ConsoleBot.ts +++ b/packages/bottender/src/console/ConsoleBot.ts @@ -1,15 +1,14 @@ import readline from 'readline'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import Session from '../session/Session'; import SessionStore from '../session/SessionStore'; +import { ConsoleContext } from '..'; import ConsoleConnector from './ConsoleConnector'; import ConsoleEvent, { ConsoleRawEvent } from './ConsoleEvent'; import { ConsoleClient } from './ConsoleClient'; -import { ConsoleContext } from '..'; - export default class ConsoleBot extends Bot< ConsoleRawEvent, ConsoleClient, @@ -20,13 +19,15 @@ export default class ConsoleBot extends Bot< sessionStore, fallbackMethods, mockPlatform, + onRequest, }: { sessionStore?: SessionStore; fallbackMethods?: boolean; mockPlatform?: string; + onRequest?: OnRequest; } = {}) { const connector = new ConsoleConnector({ fallbackMethods, mockPlatform }); - super({ connector, sessionStore, sync: true }); + super({ connector, sessionStore, sync: true, onRequest }); } async getSession(): Promise { @@ -58,16 +59,16 @@ export default class ConsoleBot extends Bot< process.exit(); } + const client = this._connector.client as ConsoleClient; + if (lowerCaseLine === '/session') { const session = await this.getSession(); - this._connector.client.sendText(JSON.stringify(session, null, 2)); + client.sendText(JSON.stringify(session, null, 2)); } else if (lowerCaseLine === '/state') { const session = await this.getSession(); - this._connector.client.sendText( - JSON.stringify(session._state || {}, null, 2) - ); + client.sendText(JSON.stringify(session._state || {}, null, 2)); } else { let rawEvent; diff --git a/packages/bottender/src/console/ConsoleConnector.ts b/packages/bottender/src/console/ConsoleConnector.ts index 7b3eea75c..3b516fea0 100644 --- a/packages/bottender/src/console/ConsoleConnector.ts +++ b/packages/bottender/src/console/ConsoleConnector.ts @@ -1,5 +1,7 @@ import { EventEmitter } from 'events'; +import { JsonObject } from 'type-fest'; + import Session from '../session/Session'; import { Connector } from '../bot/Connector'; import { RequestContext } from '../types'; @@ -17,7 +19,8 @@ type ConstructorOptions = { }; export default class ConsoleConnector - implements Connector { + implements Connector +{ _client: ConsoleClient; _fallbackMethods: boolean; @@ -75,7 +78,7 @@ export default class ConsoleConnector createContext(params: { event: ConsoleEvent; session: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter: EventEmitter | null; }): ConsoleContext { diff --git a/packages/bottender/src/console/ConsoleContext.ts b/packages/bottender/src/console/ConsoleContext.ts index 09d467259..425ac3c81 100644 --- a/packages/bottender/src/console/ConsoleContext.ts +++ b/packages/bottender/src/console/ConsoleContext.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; -import sleep from 'delay'; +import { JsonObject } from 'type-fest'; import Context from '../context/Context'; import Session from '../session/Session'; @@ -13,7 +13,7 @@ type Options = { client: ConsoleClient; event: ConsoleEvent; session: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; fallbackMethods: boolean; mockPlatform: string; @@ -76,16 +76,6 @@ export default class ConsoleContext extends Context< return this._mockPlatform || 'console'; } - /** - * Delay and show indicators for milliseconds. - * - */ - async typing(milliseconds: number): Promise { - if (milliseconds > 0) { - await sleep(milliseconds); - } - } - /** * Send text to the owner of then session. * diff --git a/packages/bottender/src/console/__tests__/ConsoleBot.spec.ts b/packages/bottender/src/console/__tests__/ConsoleBot.spec.ts index 0a95cb724..a7002e837 100644 --- a/packages/bottender/src/console/__tests__/ConsoleBot.spec.ts +++ b/packages/bottender/src/console/__tests__/ConsoleBot.spec.ts @@ -126,7 +126,7 @@ describe('createRuntime', () => { const bot = new ConsoleBot(); let context; - bot.onEvent(ctx => { + bot.onEvent((ctx) => { context = ctx; }); @@ -147,7 +147,7 @@ describe('createRuntime', () => { const bot = new ConsoleBot(); let context; - bot.onEvent(ctx => { + bot.onEvent((ctx) => { context = ctx; }); diff --git a/packages/bottender/src/console/__tests__/ConsoleContext.spec.ts b/packages/bottender/src/console/__tests__/ConsoleContext.spec.ts index ef6c825f8..100720ec9 100644 --- a/packages/bottender/src/console/__tests__/ConsoleContext.spec.ts +++ b/packages/bottender/src/console/__tests__/ConsoleContext.spec.ts @@ -142,21 +142,3 @@ describe('method missing', () => { expect(context.isSessionWritten).toBe(false); }); }); - -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context } = setup(); - - await context.typing(10); - - expect(sleep).toBeCalled(); - }); -}); diff --git a/packages/bottender/src/context/Context.ts b/packages/bottender/src/context/Context.ts index fdc5a45f5..b70ad979c 100644 --- a/packages/bottender/src/context/Context.ts +++ b/packages/bottender/src/context/Context.ts @@ -2,7 +2,9 @@ import { EventEmitter } from 'events'; import cloneDeep from 'lodash/cloneDeep'; import debug from 'debug'; +import delay from 'delay'; import warning from 'warning'; +import { JsonObject } from 'type-fest'; import Session from '../session/Session'; import { Client, Event, RequestContext } from '../types'; @@ -13,7 +15,7 @@ type Options = { client: C; event: E; session?: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter?: EventEmitter | null; }; @@ -24,7 +26,10 @@ type Response = { body: any; }; -export default abstract class Context { +export default abstract class Context< + C extends Client = any, + E extends Event = any +> { /** * The name of the platform. * @@ -32,7 +37,7 @@ export default abstract class Context { abstract get platform(): string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - abstract sendText(text: string, options?: Record): any; + abstract sendText(text: string, options?: JsonObject): any; _isHandled: boolean | null = null; @@ -44,7 +49,7 @@ export default abstract class Context { _session: Session | null; - _initialState?: Record | null; + _initialState?: JsonObject | null; _requestContext: RequestContext | null; @@ -133,7 +138,7 @@ export default abstract class Context { * The state of the conversation context. * */ - get state(): Record { + get state(): JsonObject { if (this._session) { return this._session._state; } @@ -148,7 +153,7 @@ export default abstract class Context { * Shallow merge changes to the state. * */ - setState(state: Record): void { + setState(state: JsonObject): void { if (this._session) { const sess = this._session; @@ -189,6 +194,16 @@ export default abstract class Context { } } + /** + * Delay and show indicators for milliseconds. + * + */ + async typing(milliseconds: number): Promise { + if (milliseconds > 0) { + await delay(milliseconds); + } + } + /** * The intent of the conversation context. * diff --git a/packages/bottender/src/context/Event.ts b/packages/bottender/src/context/Event.ts index 56a3d25af..75e756efa 100644 --- a/packages/bottender/src/context/Event.ts +++ b/packages/bottender/src/context/Event.ts @@ -1,6 +1,8 @@ -export interface Event { +import { JsonObject } from 'type-fest'; + +export interface Event { readonly rawEvent: RE; readonly isMessage: boolean; readonly isText: boolean; - readonly message?: Record | null; + readonly message?: JsonObject | null; } diff --git a/packages/bottender/src/context/__tests__/Context.spec.ts b/packages/bottender/src/context/__tests__/Context.spec.ts index 14aaf5d24..ab7a83009 100644 --- a/packages/bottender/src/context/__tests__/Context.spec.ts +++ b/packages/bottender/src/context/__tests__/Context.spec.ts @@ -1,6 +1,10 @@ +import delay from 'delay'; + import Context from '../Context'; -class TestContext extends Context { +jest.mock('delay'); + +class TestContext extends Context { get platform() { return 'test'; } @@ -64,3 +68,21 @@ describe('handled', () => { expect(context.isHandled).toEqual(false); }); }); + +describe('typing', () => { + it('should avoid calling delay with 0 ms', async () => { + const context = new TestContext({ client: {}, event: {} }); + + await context.typing(0); + + expect(delay).not.toBeCalled(); + }); + + it('should call delay if ms > 0', async () => { + const context = new TestContext({ client: {}, event: {} }); + + await context.typing(10); + + expect(delay).toBeCalledWith(10); + }); +}); diff --git a/packages/bottender/src/index.ts b/packages/bottender/src/index.ts index ba94b4132..7f213bcaa 100644 --- a/packages/bottender/src/index.ts +++ b/packages/bottender/src/index.ts @@ -1,13 +1,19 @@ import bottender from './bottender'; +import * as LineTypes from './line/LineTypes'; +import * as MessengerTypes from './messenger/MessengerTypes'; +import * as SlackTypes from './slack/SlackTypes'; +import * as TelegramTypes from './telegram/TelegramTypes'; +import * as ViberTypes from './viber/ViberTypes'; import * as WhatsappTypes from './whatsapp/WhatsappTypes'; export { bottender }; /* Core */ export { default as Bot } from './bot/Bot'; +export { Connector } from './bot/Connector'; export { default as Context } from './context/Context'; -export { default as getSessionStore } from './getSessionStore'; -export { default as getClient } from './getClient'; +export { default as getSessionStore } from './shared/getSessionStore'; +export { default as getClient } from './shared/getClient'; /* Action */ export { default as chain } from './chain'; @@ -33,16 +39,23 @@ export { default as ConsoleEvent } from './console/ConsoleEvent'; /* Messenger */ export { default as MessengerBot } from './messenger/MessengerBot'; +export { default as FacebookBaseConnector } from './messenger/FacebookBaseConnector'; export { default as MessengerConnector } from './messenger/MessengerConnector'; export { default as MessengerContext } from './messenger/MessengerContext'; export { default as MessengerEvent } from './messenger/MessengerEvent'; -export { MessengerTypes } from 'messaging-api-messenger'; +export { + Messenger, + MessengerClient, + MessengerBatch, +} from 'messaging-api-messenger'; +export { MessengerTypes }; /* WhatsApp */ export { default as WhatsappBot } from './whatsapp/WhatsappBot'; export { default as WhatsappConnector } from './whatsapp/WhatsappConnector'; export { default as WhatsappContext } from './whatsapp/WhatsappContext'; export { default as WhatsappEvent } from './whatsapp/WhatsappEvent'; +export { default as TwilioClient } from './whatsapp/TwilioClient'; export { WhatsappTypes }; /* LINE */ @@ -50,28 +63,35 @@ export { default as LineBot } from './line/LineBot'; export { default as LineConnector } from './line/LineConnector'; export { default as LineContext } from './line/LineContext'; export { default as LineEvent } from './line/LineEvent'; -export { LineTypes, LineNotify } from 'messaging-api-line'; +export { Line, LineNotify, LineClient } from 'messaging-api-line'; +export { LineTypes }; /* Slack */ export { default as SlackBot } from './slack/SlackBot'; export { default as SlackConnector } from './slack/SlackConnector'; export { default as SlackContext } from './slack/SlackContext'; export { default as SlackEvent } from './slack/SlackEvent'; -export { SlackTypes } from 'messaging-api-slack'; +export { SlackOAuthClient } from 'messaging-api-slack'; +export { SlackTypes }; /* Telegram */ export { default as TelegramBot } from './telegram/TelegramBot'; export { default as TelegramConnector } from './telegram/TelegramConnector'; export { default as TelegramContext } from './telegram/TelegramContext'; export { default as TelegramEvent } from './telegram/TelegramEvent'; -export { TelegramTypes } from 'messaging-api-telegram'; +export { TelegramClient } from 'messaging-api-telegram'; +export { TelegramTypes }; /* Viber */ export { default as ViberBot } from './viber/ViberBot'; export { default as ViberConnector } from './viber/ViberConnector'; export { default as ViberContext } from './viber/ViberContext'; export { default as ViberEvent } from './viber/ViberEvent'; -export { ViberTypes } from 'messaging-api-viber'; +export { ViberClient } from 'messaging-api-viber'; +export { ViberTypes }; + +/* Types */ +export * from './types'; /** * Private Exports (unstable) diff --git a/packages/bottender/src/initializeServer.ts b/packages/bottender/src/initializeServer.ts index da81407e1..39346bd1e 100644 --- a/packages/bottender/src/initializeServer.ts +++ b/packages/bottender/src/initializeServer.ts @@ -12,7 +12,7 @@ import TelegramBot from './telegram/TelegramBot'; import ViberBot from './viber/ViberBot'; import WhatsappBot from './whatsapp/WhatsappBot'; import getBottenderConfig from './shared/getBottenderConfig'; -import getSessionStore from './getSessionStore'; +import getSessionStore from './shared/getSessionStore'; import { Action, BottenderConfig, Channel, Plugin } from './types'; const BOT_MAP = { diff --git a/packages/bottender/src/line/LineBot.ts b/packages/bottender/src/line/LineBot.ts index 6cdfa91f5..a33780ed4 100644 --- a/packages/bottender/src/line/LineBot.ts +++ b/packages/bottender/src/line/LineBot.ts @@ -1,11 +1,12 @@ import { LineClient } from 'messaging-api-line'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; -import LineConnector, { LineRequestBody } from './LineConnector'; +import LineConnector, { LineConnectorOptions } from './LineConnector'; import LineContext from './LineContext'; import LineEvent from './LineEvent'; +import { LineRequestBody } from './LineTypes'; export default class LineBot extends Bot< LineRequestBody, @@ -14,35 +15,16 @@ export default class LineBot extends Bot< LineContext > { constructor({ - accessToken, - channelSecret, sessionStore, - origin, sync, - mapDestinationToAccessToken, - shouldBatch, - sendMethod, - skipLegacyProfile, - }: { - accessToken: string; - channelSecret: string; + onRequest, + ...connectorOptions + }: LineConnectorOptions & { sessionStore?: SessionStore; sync?: boolean; - mapDestinationToAccessToken?: (destination: string) => Promise; - shouldBatch?: boolean; - sendMethod?: string; - origin?: string; - skipLegacyProfile?: boolean; + onRequest?: OnRequest; }) { - const connector = new LineConnector({ - accessToken, - channelSecret, - mapDestinationToAccessToken, - shouldBatch, - sendMethod, - origin, - skipLegacyProfile, - }); - super({ connector, sessionStore, sync }); + const connector = new LineConnector(connectorOptions); + super({ connector, sessionStore, sync, onRequest }); } } diff --git a/packages/bottender/src/line/LineConnector.ts b/packages/bottender/src/line/LineConnector.ts index cd6bee91f..513f051f6 100644 --- a/packages/bottender/src/line/LineConnector.ts +++ b/packages/bottender/src/line/LineConnector.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; import invariant from 'invariant'; import warning from 'warning'; +import { JsonObject } from 'type-fest'; import { LineClient } from 'messaging-api-line'; import Session from '../session/Session'; @@ -10,90 +11,127 @@ import { Connector } from '../bot/Connector'; import { RequestContext } from '../types'; import LineContext from './LineContext'; -import LineEvent, { LineRawEvent } from './LineEvent'; +import LineEvent from './LineEvent'; +import { LineRawEvent, LineRequestBody, LineRequestContext } from './LineTypes'; -export type LineRequestBody = { - destination: string; - events: LineRawEvent[]; -}; - -type CommonConstructorOptions = { - mapDestinationToAccessToken?: (destination: string) => Promise; +type CommonConnectorOptions = { + getConfig?: GetConfigFunction; + getSessionKeyPrefix?: GetSessionKeyPrefixFunction; shouldBatch?: boolean; sendMethod?: string; skipLegacyProfile?: boolean; }; -type ConstructorOptionsWithoutClient = { - accessToken: string; - channelSecret: string; +type Credential = { accessToken: string; channelSecret: string }; + +type GetConfigFunction = ({ + params, +}: { + params: Record; +}) => Credential | Promise; + +export type GetSessionKeyPrefixFunction = ( + event: LineEvent, + requestContext?: RequestContext +) => string; + +type CredentialOptions = + | Credential + | { + getConfig: GetConfigFunction; + }; + +type ConnectorOptionsWithoutClient = CredentialOptions & { origin?: string; -} & CommonConstructorOptions; +} & CommonConnectorOptions; -type ConstructorOptionsWithClient = { +type ConnectorOptionsWithClient = { client: LineClient; -} & CommonConstructorOptions; + channelSecret: string; +} & CommonConnectorOptions; -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; +export type LineConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; export default class LineConnector - implements Connector { - _client: LineClient; + implements Connector +{ + _client: LineClient | undefined; - _channelSecret: string; + _channelSecret: string | undefined; + + _origin: string | undefined; _skipLegacyProfile: boolean; - _mapDestinationToAccessToken: - | ((destination: string) => Promise) - | null; + _getConfig: GetConfigFunction | undefined; + + _getSessionKeyPrefix: GetSessionKeyPrefixFunction | undefined; _shouldBatch: boolean; + /** + * @deprecated + */ _sendMethod: string; - constructor(options: ConstructorOptions) { + constructor(options: LineConnectorOptions) { const { - mapDestinationToAccessToken, + getConfig, shouldBatch, sendMethod, skipLegacyProfile, + getSessionKeyPrefix, } = options; if ('client' in options) { this._client = options.client; - this._channelSecret = ''; + this._channelSecret = options.channelSecret; } else { - const { accessToken, channelSecret, origin } = options; + const { origin } = options; + if ('getConfig' in options) { + this._getConfig = getConfig; + } else { + const { accessToken, channelSecret } = options; + invariant( + accessToken, + 'LINE access token is required. Please make sure you have filled it correctly in your `bottender.config.js` or `.env` file.' + ); + invariant( + channelSecret, + 'LINE channel secret is required. Please make sure you have filled it correctly in your `bottender.config.js` or the `.env` file.' + ); - invariant( - options.accessToken, - 'LINE access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' - ); - invariant( - options.channelSecret, - 'LINE channel secret is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' - ); + this._client = new LineClient({ + accessToken, + channelSecret, + origin, + }); - this._client = LineClient.connect({ - accessToken, - channelSecret, - origin, - }); + this._channelSecret = channelSecret; + } - this._channelSecret = channelSecret; + this._origin = origin; } - this._mapDestinationToAccessToken = mapDestinationToAccessToken || null; - this._shouldBatch = typeof shouldBatch === 'boolean' ? shouldBatch : true; warning( !sendMethod || sendMethod === 'reply' || sendMethod === 'push', 'sendMethod should be one of `reply` or `push`' ); - this._sendMethod = sendMethod || 'reply'; + + if (sendMethod) { + warning( + false, + '`sendMethod` is deprecated. The value will always be `reply` in v2.' + ); + this._sendMethod = sendMethod; + } else { + this._sendMethod = 'reply'; + } + + this._getSessionKeyPrefix = getSessionKeyPrefix; this._skipLegacyProfile = typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true; @@ -101,8 +139,9 @@ export default class LineConnector _isWebhookVerifyEvent(event: LineRawEvent): boolean { return ( - (event as any).replyToken === '00000000000000000000000000000000' || - (event as any).replyToken === 'ffffffffffffffffffffffffffffffff' + 'replyToken' in event && + (event.replyToken === '00000000000000000000000000000000' || + event.replyToken === 'ffffffffffffffffffffffffffffffff') ); } @@ -115,30 +154,43 @@ export default class LineConnector ); } - get platform(): string { + get platform(): 'line' { return 'line'; } - get client(): LineClient { + get client(): LineClient | undefined { return this._client; } - getUniqueSessionKey(bodyOrEvent: LineRequestBody | LineEvent): string { + getUniqueSessionKey( + bodyOrEvent: LineRequestBody | LineEvent, + requestContext?: RequestContext + ): string { const rawEvent = bodyOrEvent instanceof LineEvent ? bodyOrEvent.rawEvent : bodyOrEvent.events[0]; + let prefix = ''; + if (this._getSessionKeyPrefix) { + const event = + bodyOrEvent instanceof LineEvent + ? bodyOrEvent + : new LineEvent(rawEvent, { destination: bodyOrEvent.destination }); + + prefix = this._getSessionKeyPrefix(event, requestContext); + } + const { source } = rawEvent; if (source.type === 'user') { - return source.userId; + return `${prefix}${source.userId}`; } if (source.type === 'group') { - return source.groupId; + return `${prefix}${source.groupId}`; } if (source.type === 'room') { - return source.roomId; + return `${prefix}${source.roomId}`; } throw new TypeError( 'LineConnector: sender type should be one of user, group, room.' @@ -164,19 +216,20 @@ export default class LineConnector let user = null; if (source.userId) { - user = this._skipLegacyProfile - ? { - id: source.userId, - _updatedAt: new Date().toISOString(), - } - : { - id: source.userId, - _updatedAt: new Date().toISOString(), - ...(await this._client.getGroupMemberProfile( - source.groupId, - source.userId - )), - }; + user = + this._skipLegacyProfile || !this._client + ? { + id: source.userId, + _updatedAt: new Date().toISOString(), + } + : { + id: source.userId, + _updatedAt: new Date().toISOString(), + ...(await this._client.getGroupMemberProfile( + source.groupId, + source.userId + )), + }; } session.user = user; @@ -184,8 +237,10 @@ export default class LineConnector let memberIds: string[] = []; try { - memberIds = await this._client.getAllGroupMemberIds(source.groupId); - } catch (e) { + if (this._client) { + memberIds = await this._client.getAllGroupMemberIds(source.groupId); + } + } catch (err) { // FIXME: handle no memberIds // only LINE@ Approved accounts or official accounts can use this API // https://developers.line.me/en/docs/messaging-api/reference/#get-group-member-user-ids @@ -193,26 +248,27 @@ export default class LineConnector session.group = { id: source.groupId, - members: memberIds.map(id => ({ id })), + members: memberIds.map((id) => ({ id })), _updatedAt: new Date().toISOString(), }; } else if (source.type === 'room') { let user = null; if (source.userId) { - user = this._skipLegacyProfile - ? { - id: source.userId, - _updatedAt: new Date().toISOString(), - } - : { - id: source.userId, - _updatedAt: new Date().toISOString(), - ...(await this._client.getRoomMemberProfile( - source.roomId, - source.userId - )), - }; + user = + this._skipLegacyProfile || !this._client + ? { + id: source.userId, + _updatedAt: new Date().toISOString(), + } + : { + id: source.userId, + _updatedAt: new Date().toISOString(), + ...(await this._client.getRoomMemberProfile( + source.roomId, + source.userId + )), + }; } session.user = user; @@ -220,8 +276,10 @@ export default class LineConnector let memberIds: string[] = []; try { - memberIds = await this._client.getAllRoomMemberIds(source.roomId); - } catch (e) { + if (this._client) { + memberIds = await this._client.getAllRoomMemberIds(source.roomId); + } + } catch (err) { // FIXME: handle no memberIds // only LINE@ Approved accounts or official accounts can use this API // https://developers.line.me/en/docs/messaging-api/reference/#get-room-member-user-ids @@ -229,14 +287,15 @@ export default class LineConnector session.room = { id: source.roomId, - members: memberIds.map(id => ({ id })), + members: memberIds.map((id) => ({ id })), _updatedAt: new Date().toISOString(), }; } else if (source.type === 'user') { if (!session.user) { - const user = this._skipLegacyProfile - ? {} - : await this._client.getUserProfile(source.userId); + const user = + this._skipLegacyProfile || !this._client + ? {} + : await this._client.getUserProfile(source.userId); session.user = { id: source.userId, @@ -281,42 +340,64 @@ export default class LineConnector const { destination } = body; return body.events - .filter(event => !this._isWebhookVerifyEvent(event)) - .map(event => new LineEvent(event, { destination })); + .filter((event) => !this._isWebhookVerifyEvent(event)) + .map((event) => new LineEvent(event, { destination })); } async createContext(params: { event: LineEvent; session?: Session | null; - initialState?: Record | null; - requestContext?: RequestContext; + initialState?: JsonObject | null; + requestContext?: LineRequestContext; emitter?: EventEmitter | null; }): Promise { - let customAccessToken; - if (this._mapDestinationToAccessToken) { - const { destination } = params.event; + const { requestContext } = params; - if (!destination) { - warning(false, 'Could not find destination from request body.'); - } else { - customAccessToken = await this._mapDestinationToAccessToken( - destination - ); - } + let client: LineClient; + if (this._getConfig) { + invariant( + requestContext, + 'getConfig: `requestContext` is required to execute the function.' + ); + + const config = await this._getConfig({ + params: requestContext.params, + }); + + invariant( + config.accessToken, + 'getConfig: `accessToken` is missing in the resolved value.' + ); + + invariant( + config.channelSecret, + 'getConfig: `accessToken` is missing in the resolved value.' + ); + + client = new LineClient({ + accessToken: config.accessToken, + channelSecret: config.channelSecret, + origin: this._origin, + }); + } else { + client = this._client as LineClient; } return new LineContext({ ...params, - client: this._client, - customAccessToken, + client, shouldBatch: this._shouldBatch, sendMethod: this._sendMethod, }); } - verifySignature(rawBody: string, signature: string): boolean { + verifySignature( + rawBody: string, + signature: string, + { channelSecret }: { channelSecret: string } + ): boolean { const hashBufferFromBody = crypto - .createHmac('sha256', this._channelSecret) + .createHmac('sha256', channelSecret) .update(rawBody, 'utf8') .digest(); @@ -331,25 +412,39 @@ export default class LineConnector return crypto.timingSafeEqual(bufferFromSignature, hashBufferFromBody); } - preprocess({ + async preprocess({ method, headers, rawBody, body, - }: { - method: string; - headers: Record; - query: Record; - rawBody: string; - body: Record; - }) { + params, + }: LineRequestContext) { if (method.toLowerCase() !== 'post') { return { shouldNext: true, }; } - if (!this.verifySignature(rawBody, headers['x-line-signature'])) { + let channelSecret: string; + if (this._getConfig) { + const config = await this._getConfig({ params }); + + invariant( + config.channelSecret, + 'getConfig: `accessToken` is missing in the resolved value.' + ); + + channelSecret = config.channelSecret; + } else { + channelSecret = this._channelSecret as string; + } + + if ( + !headers['x-line-signature'] || + !this.verifySignature(rawBody, headers['x-line-signature'], { + channelSecret, + }) + ) { const error = { message: 'LINE Signature Validation Failed!', request: { @@ -369,7 +464,7 @@ export default class LineConnector }; } - if (this.isWebhookVerifyRequest(body as any)) { + if (this.isWebhookVerifyRequest(body)) { return { shouldNext: false, response: { diff --git a/packages/bottender/src/line/LineContext.ts b/packages/bottender/src/line/LineContext.ts index 20c1cc4a1..9315f6eb6 100644 --- a/packages/bottender/src/line/LineContext.ts +++ b/packages/bottender/src/line/LineContext.ts @@ -2,21 +2,22 @@ import { EventEmitter } from 'events'; import chunk from 'lodash/chunk'; import invariant from 'invariant'; -import sleep from 'delay'; import warning from 'warning'; -import { Line, LineClient, LineTypes } from 'messaging-api-line'; +import { JsonObject } from 'type-fest'; +import { Line, LineClient } from 'messaging-api-line'; import Context from '../context/Context'; import Session from '../session/Session'; import { RequestContext } from '../types'; import LineEvent from './LineEvent'; +import * as LineTypes from './LineTypes'; -type Options = { +export type LineContextOptions = { client: LineClient; event: LineEvent; session?: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; customAccessToken?: string; shouldBatch?: boolean; @@ -25,8 +26,6 @@ type Options = { }; class LineContext extends Context { - _customAccessToken: string | null; - _isReplied = false; _shouldBatch: boolean; @@ -47,11 +46,15 @@ class LineContext extends Context { shouldBatch, sendMethod, emitter, - }: Options) { + }: LineContextOptions) { super({ client, event, session, initialState, requestContext, emitter }); - this._customAccessToken = customAccessToken || null; this._shouldBatch = shouldBatch || false; this._sendMethod = sendMethod || 'reply'; + + // TODO: remove this in v2 + if (customAccessToken) { + this.useAccessToken(customAccessToken); + } } /** @@ -67,15 +70,17 @@ class LineContext extends Context { * */ get accessToken(): string { - return this._customAccessToken || this._client.accessToken; + return this._client.accessToken; } /** * Inject access token for the context. * */ - useAccessToken(accessToken: string) { - this._customAccessToken = accessToken; + useAccessToken(accessToken: string): void { + warning(false, '`useAccessToken` is deprecated.'); + // @ts-expect-error + this._client.accessToken = accessToken; } /** @@ -89,7 +94,7 @@ class LineContext extends Context { /** * Context Lifecycle Hook */ - async handlerDidEnd() { + async handlerDidEnd(): Promise { if (this._shouldBatch) { // After starting this batch, every api should be called out of batch mode this._shouldBatch = false; @@ -101,11 +106,7 @@ class LineContext extends Context { 'one replyToken can only be used to reply 5 messages' ); if (this._event.replyToken) { - await this._client.reply(this._event.replyToken, messageChunks[0], { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - }); + await this._client.reply(this._event.replyToken, messageChunks[0]); } } @@ -119,11 +120,7 @@ class LineContext extends Context { const messages = messageChunks[i]; // eslint-disable-next-line no-await-in-loop - await this._client.push(sessionTypeId, messages, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - }); + await this._client.push(sessionTypeId, messages); } } else { warning( @@ -135,16 +132,6 @@ class LineContext extends Context { } } - /** - * Delay and show indicators for milliseconds. - * - */ - async typing(milliseconds: number): Promise { - if (milliseconds > 0) { - await sleep(milliseconds); - } - } - /** * Retrieve content for image, video and audio message. * @@ -160,11 +147,7 @@ class LineContext extends Context { const messageId = (this._event.message as any).id; - return this._client.getMessageContent(messageId, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - }); + return this._client.getMessageContent(messageId); } /** @@ -179,17 +162,9 @@ class LineContext extends Context { switch (this._session.type) { case 'room': - return this._client.leaveRoom(this._session.room.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.leaveRoom(this._session.room.id); case 'group': - return this._client.leaveGroup(this._session.group.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.leaveGroup(this._session.group.id); default: warning( false, @@ -223,30 +198,16 @@ class LineContext extends Context { case 'room': return this._client.getRoomMemberProfile( this._session.room.id, - this._session.user.id, - { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any + this._session.user.id ); case 'group': return this._client.getGroupMemberProfile( this._session.group.id, - this._session.user.id, - { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any + this._session.user.id ); case 'user': default: - return this._client.getUserProfile(this._session.user.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.getUserProfile(this._session.user.id); } } @@ -266,24 +227,11 @@ class LineContext extends Context { switch (this._session.type) { case 'room': - return this._client.getRoomMemberProfile( - this._session.room.id, - userId, - { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any - ); + return this._client.getRoomMemberProfile(this._session.room.id, userId); case 'group': return this._client.getGroupMemberProfile( this._session.group.id, - userId, - { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any + userId ); default: warning( @@ -294,6 +242,29 @@ class LineContext extends Context { } } + /** + * Gets the member count in group/room. + * + */ + async getMembersCount(): Promise { + if (!this._session) { + warning( + false, + 'getMembersCount: should not be called in context without session' + ); + return null; + } + + switch (this._session.type) { + case 'room': + return this._client.getRoomMembersCount(this._session.room.id); + case 'group': + return this._client.getGroupMembersCount(this._session.group.id); + default: + return 1; + } + } + /** * Gets the ID of the users of the members of the group/room that the bot is in. * This includes the user IDs of users who have not added the bot as a friend or has blocked the bot. @@ -312,17 +283,9 @@ class LineContext extends Context { switch (this._session.type) { case 'room': - return this._client.getRoomMemberIds(this._session.room.id, start, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.getRoomMemberIds(this._session.room.id, start); case 'group': - return this._client.getGroupMemberIds(this._session.group.id, start, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.getGroupMemberIds(this._session.group.id, start); default: warning( false, @@ -349,17 +312,9 @@ class LineContext extends Context { switch (this._session.type) { case 'room': - return this._client.getAllRoomMemberIds(this._session.room.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - }); + return this._client.getAllRoomMemberIds(this._session.room.id); case 'group': - return this._client.getAllGroupMemberIds(this._session.group.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - }); + return this._client.getAllGroupMemberIds(this._session.group.id); default: warning( false, @@ -375,11 +330,7 @@ class LineContext extends Context { */ async getLinkedRichMenu(): Promise { if (this._session && this._session.user) { - return this._client.getLinkedRichMenu(this._session.user.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.getLinkedRichMenu(this._session.user.id); } warning( false, @@ -393,11 +344,7 @@ class LineContext extends Context { */ async linkRichMenu(richMenuId: string): Promise { if (this._session && this._session.user) { - return this._client.linkRichMenu(this._session.user.id, richMenuId, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.linkRichMenu(this._session.user.id, richMenuId); } warning( false, @@ -411,11 +358,7 @@ class LineContext extends Context { */ async unlinkRichMenu(): Promise { if (this._session && this._session.user) { - return this._client.unlinkRichMenu(this._session.user.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.unlinkRichMenu(this._session.user.id); } warning( false, @@ -429,11 +372,7 @@ class LineContext extends Context { */ async issueLinkToken(): Promise { if (this._session && this._session.user) { - return this._client.issueLinkToken(this._session.user.id, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - } as any); + return this._client.issueLinkToken(this._session.user.id); } warning( false, @@ -441,7 +380,7 @@ class LineContext extends Context { ); } - reply(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) { + reply(messages: LineTypes.Message[]) { invariant(!this._isReplied, 'Can not reply event multiple times'); if (this._shouldBatch) { @@ -453,19 +392,14 @@ class LineContext extends Context { this._isReplied = true; // FIXME: throw or warn for no replyToken - return this._client.reply(this._event.replyToken as string, messages, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - ...options, - }); + return this._client.reply(this._event.replyToken as string, messages); } replyText( text: string, options?: LineTypes.MessageOptions & { emojis?: LineTypes.Emoji[] } ) { - return this.reply([Line.createText(text, options)], options); + return this.reply([Line.createText(text, options)]); } replyImage( @@ -475,7 +409,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createImage(image, options)], options); + return this.reply([Line.createImage(image, options)]); } replyVideo( @@ -485,7 +419,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createVideo(video, options)], options); + return this.reply([Line.createVideo(video, options)]); } replyAudio( @@ -495,21 +429,21 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createAudio(audio, options)], options); + return this.reply([Line.createAudio(audio, options)]); } replyLocation( location: LineTypes.Location, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createLocation(location, options)], options); + return this.reply([Line.createLocation(location, options)]); } replySticker( sticker: Omit, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createSticker(sticker, options)], options); + return this.reply([Line.createSticker(sticker, options)]); } replyImagemap( @@ -517,10 +451,7 @@ class LineContext extends Context { imagemap: Omit, options?: LineTypes.MessageOptions ) { - return this.reply( - [Line.createImagemap(altText, imagemap, options)], - options - ); + return this.reply([Line.createImagemap(altText, imagemap, options)]); } replyFlex( @@ -528,7 +459,7 @@ class LineContext extends Context { flex: LineTypes.FlexContainer, options?: LineTypes.MessageOptions ) { - return this.reply([Line.createFlex(altText, flex, options)], options); + return this.reply([Line.createFlex(altText, flex, options)]); } replyTemplate( @@ -536,10 +467,7 @@ class LineContext extends Context { template: LineTypes.Template, options?: LineTypes.MessageOptions ) { - return this.reply( - [Line.createTemplate(altText, template, options)], - options - ); + return this.reply([Line.createTemplate(altText, template, options)]); } replyButtonTemplate( @@ -547,10 +475,9 @@ class LineContext extends Context { buttonTemplate: Omit, options?: LineTypes.MessageOptions ) { - return this.reply( - [Line.createButtonTemplate(altText, buttonTemplate, options)], - options - ); + return this.reply([ + Line.createButtonTemplate(altText, buttonTemplate, options), + ]); } replyButtonsTemplate( @@ -566,34 +493,20 @@ class LineContext extends Context { confirmTemplate: Omit, options: LineTypes.MessageOptions ) { - return this.reply( - [Line.createConfirmTemplate(altText, confirmTemplate, options)], - options - ); + return this.reply([ + Line.createConfirmTemplate(altText, confirmTemplate, options), + ]); } replyCarouselTemplate( altText: string, columns: LineTypes.ColumnObject[], - { - imageAspectRatio, - imageSize, - ...options - }: { + options: { imageAspectRatio?: 'rectangle' | 'square'; imageSize?: 'cover' | 'contain'; - } & LineTypes.MessageOptions = {} - ) { - return this.reply( - [ - Line.createCarouselTemplate(altText, columns, { - imageAspectRatio, - imageSize, - ...options, - }), - ], - options - ); + } & LineTypes.MessageOptions + ) { + return this.reply([Line.createCarouselTemplate(altText, columns, options)]); } replyImageCarouselTemplate( @@ -601,13 +514,12 @@ class LineContext extends Context { columns: LineTypes.ImageCarouselColumnObject[], options: LineTypes.MessageOptions ) { - return this.reply( - [Line.createImageCarouselTemplate(altText, columns, options)], - options - ); + return this.reply([ + Line.createImageCarouselTemplate(altText, columns, options), + ]); } - push(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) { + push(messages: LineTypes.Message[]) { if (!this._session) { warning(false, `push: should not be called in context without session`); return; @@ -619,19 +531,14 @@ class LineContext extends Context { } const sessionType = this._session.type; - return this._client.push(this._session[sessionType].id, messages, { - ...(this._customAccessToken - ? { accessToken: this._customAccessToken } - : undefined), - ...options, - }); + return this._client.push(this._session[sessionType].id, messages); } pushText( text: string, options?: LineTypes.MessageOptions & { emojis?: LineTypes.Emoji[] } ) { - return this.push([Line.createText(text, options)], options); + return this.push([Line.createText(text, options)]); } pushImage( @@ -641,7 +548,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.push([Line.createImage(image, options)], options); + return this.push([Line.createImage(image, options)]); } pushVideo( @@ -651,7 +558,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.push([Line.createVideo(video, options)], options); + return this.push([Line.createVideo(video, options)]); } pushAudio( @@ -661,21 +568,21 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.push([Line.createAudio(audio, options)], options); + return this.push([Line.createAudio(audio, options)]); } pushLocation( location: LineTypes.Location, options?: LineTypes.MessageOptions ) { - return this.push([Line.createLocation(location, options)], options); + return this.push([Line.createLocation(location, options)]); } pushSticker( sticker: Omit, options?: LineTypes.MessageOptions ) { - return this.push([Line.createSticker(sticker, options)], options); + return this.push([Line.createSticker(sticker, options)]); } pushImagemap( @@ -683,10 +590,7 @@ class LineContext extends Context { imagemap: Omit, options?: LineTypes.MessageOptions ) { - return this.push( - [Line.createImagemap(altText, imagemap, options)], - options - ); + return this.push([Line.createImagemap(altText, imagemap, options)]); } pushFlex( @@ -694,7 +598,7 @@ class LineContext extends Context { flex: LineTypes.FlexContainer, options?: LineTypes.MessageOptions ) { - return this.push([Line.createFlex(altText, flex, options)], options); + return this.push([Line.createFlex(altText, flex, options)]); } pushTemplate( @@ -702,10 +606,7 @@ class LineContext extends Context { template: LineTypes.Template, options?: LineTypes.MessageOptions ) { - return this.push( - [Line.createTemplate(altText, template, options)], - options - ); + return this.push([Line.createTemplate(altText, template, options)]); } pushButtonTemplate( @@ -713,10 +614,9 @@ class LineContext extends Context { buttonTemplate: Omit, options?: LineTypes.MessageOptions ) { - return this.push( - [Line.createButtonTemplate(altText, buttonTemplate, options)], - options - ); + return this.push([ + Line.createButtonTemplate(altText, buttonTemplate, options), + ]); } pushButtonsTemplate( @@ -732,34 +632,20 @@ class LineContext extends Context { confirmTemplate: Omit, options: LineTypes.MessageOptions ) { - return this.push( - [Line.createConfirmTemplate(altText, confirmTemplate, options)], - options - ); + return this.push([ + Line.createConfirmTemplate(altText, confirmTemplate, options), + ]); } pushCarouselTemplate( altText: string, columns: LineTypes.ColumnObject[], - { - imageAspectRatio, - imageSize, - ...options - }: { + options: { imageAspectRatio?: 'rectangle' | 'square'; imageSize?: 'cover' | 'contain'; - } & LineTypes.MessageOptions = {} - ) { - return this.push( - [ - Line.createCarouselTemplate(altText, columns, { - imageAspectRatio, - imageSize, - ...options, - }), - ], - options - ); + } & LineTypes.MessageOptions + ) { + return this.push([Line.createCarouselTemplate(altText, columns, options)]); } pushImageCarouselTemplate( @@ -767,24 +653,23 @@ class LineContext extends Context { columns: LineTypes.ImageCarouselColumnObject[], options: LineTypes.MessageOptions ) { - return this.push( - [Line.createImageCarouselTemplate(altText, columns, options)], - options - ); + return this.push([ + Line.createImageCarouselTemplate(altText, columns, options), + ]); } - send(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) { + send(messages: LineTypes.Message[]) { if (this._sendMethod === 'push') { - return this.push(messages, options); + return this.push(messages); } - return this.reply(messages, options); + return this.reply(messages); } sendText( text: string, options?: LineTypes.MessageOptions & { emojis?: LineTypes.Emoji[] } ) { - return this.send([Line.createText(text, options)], options); + return this.send([Line.createText(text, options)]); } sendImage( @@ -794,7 +679,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.send([Line.createImage(image, options)], options); + return this.send([Line.createImage(image, options)]); } sendVideo( @@ -804,7 +689,7 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.send([Line.createVideo(video, options)], options); + return this.send([Line.createVideo(video, options)]); } sendAudio( @@ -814,21 +699,21 @@ class LineContext extends Context { }, options?: LineTypes.MessageOptions ) { - return this.send([Line.createAudio(audio, options)], options); + return this.send([Line.createAudio(audio, options)]); } sendLocation( location: LineTypes.Location, options?: LineTypes.MessageOptions ) { - return this.send([Line.createLocation(location, options)], options); + return this.send([Line.createLocation(location, options)]); } sendSticker( sticker: Omit, options?: LineTypes.MessageOptions ) { - return this.send([Line.createSticker(sticker, options)], options); + return this.send([Line.createSticker(sticker, options)]); } sendImagemap( @@ -836,10 +721,7 @@ class LineContext extends Context { imagemap: Omit, options?: LineTypes.MessageOptions ) { - return this.send( - [Line.createImagemap(altText, imagemap, options)], - options - ); + return this.send([Line.createImagemap(altText, imagemap, options)]); } sendFlex( @@ -847,7 +729,7 @@ class LineContext extends Context { flex: LineTypes.FlexContainer, options?: LineTypes.MessageOptions ) { - return this.send([Line.createFlex(altText, flex, options)], options); + return this.send([Line.createFlex(altText, flex, options)]); } sendTemplate( @@ -855,10 +737,7 @@ class LineContext extends Context { template: LineTypes.Template, options?: LineTypes.MessageOptions ) { - return this.send( - [Line.createTemplate(altText, template, options)], - options - ); + return this.send([Line.createTemplate(altText, template, options)]); } sendButtonTemplate( @@ -866,10 +745,9 @@ class LineContext extends Context { buttonTemplate: Omit, options?: LineTypes.MessageOptions ) { - return this.send( - [Line.createButtonTemplate(altText, buttonTemplate, options)], - options - ); + return this.send([ + Line.createButtonTemplate(altText, buttonTemplate, options), + ]); } sendButtonsTemplate( @@ -883,36 +761,22 @@ class LineContext extends Context { sendConfirmTemplate( altText: string, confirmTemplate: Omit, - options: LineTypes.MessageOptions + options?: LineTypes.MessageOptions ) { - return this.send( - [Line.createConfirmTemplate(altText, confirmTemplate, options)], - options - ); + return this.send([ + Line.createConfirmTemplate(altText, confirmTemplate, options), + ]); } sendCarouselTemplate( altText: string, columns: LineTypes.ColumnObject[], - { - imageAspectRatio, - imageSize, - ...options - }: { + options?: { imageAspectRatio?: 'rectangle' | 'square'; imageSize?: 'cover' | 'contain'; - } & LineTypes.MessageOptions = {} - ) { - return this.send( - [ - Line.createCarouselTemplate(altText, columns, { - imageAspectRatio, - imageSize, - ...options, - }), - ], - options - ); + } & LineTypes.MessageOptions + ) { + return this.send([Line.createCarouselTemplate(altText, columns, options)]); } sendImageCarouselTemplate( @@ -920,10 +784,9 @@ class LineContext extends Context { columns: LineTypes.ImageCarouselColumnObject[], options: LineTypes.MessageOptions ) { - return this.send( - [Line.createImageCarouselTemplate(altText, columns, options)], - options - ); + return this.send([ + Line.createImageCarouselTemplate(altText, columns, options), + ]); } } diff --git a/packages/bottender/src/line/LineEvent.ts b/packages/bottender/src/line/LineEvent.ts index 60ae27ee2..b1715ee4d 100644 --- a/packages/bottender/src/line/LineEvent.ts +++ b/packages/bottender/src/line/LineEvent.ts @@ -1,233 +1,26 @@ +import warning from 'warning'; + import { Event } from '../context/Event'; -import warning = require('warning'); - -type UserSource = { - type: 'user'; - userId: string; -}; - -type GroupSource = { - type: 'group'; - groupId: string; - userId?: string; -}; - -type RoomSource = { - type: 'room'; - roomId: string; - userId?: string; -}; - -type Source = UserSource | GroupSource | RoomSource; - -type TextMessage = { - id: string; - type: 'text'; - text: string; -}; - -type ContentProviderLine = { - type: 'line'; -}; - -type ContentProviderExternal = { - type: 'external'; - originalContentUrl: string; - previewImageUrl?: string; -}; - -type ImageMessage = { - id: string; - type: 'image'; - contentProvider: ContentProviderLine | ContentProviderExternal; -}; - -type VideoMessage = { - id: string; - type: 'video'; - duration: number; - contentProvider: ContentProviderLine | ContentProviderExternal; -}; - -type AudioMessage = { - id: string; - type: 'audio'; - duration: number; - contentProvider: ContentProviderLine | ContentProviderExternal; -}; - -type FileMessage = { - id: string; - type: 'file'; - fileName: string; - fileSize: number; -}; - -type LocationMessage = { - id: string; - type: 'location'; - title: string; - address: string; - latitude: number; - longitude: number; -}; - -type StickerMessage = { - id: string; - type: 'sticker'; - packageId: string; - stickerId: string; -}; - -type Message = - | TextMessage - | ImageMessage - | VideoMessage - | AudioMessage - | FileMessage - | LocationMessage - | StickerMessage; - -type MessageEvent = { - replyToken: string; - type: 'message'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; - message: Message; -}; - -type FollowEvent = { - replyToken: string; - type: 'follow'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; -}; - -type UnfollowEvent = { - type: 'unfollow'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; -}; - -type JoinEvent = { - replyToken: string; - type: 'join'; - mode: 'active' | 'standby'; - timestamp: number; - source: GroupSource | RoomSource; -}; - -type LeaveEvent = { - type: 'leave'; - mode: 'active' | 'standby'; - timestamp: number; - source: GroupSource | RoomSource; -}; - -type MemberJoinedEvent = { - replyToken: string; - type: 'memberJoined'; - mode: 'active' | 'standby'; - timestamp: number; - source: GroupSource | RoomSource; - joined: { - members: UserSource[]; - }; -}; - -type MemberLeftEvent = { - type: 'memberLeft'; - mode: 'active' | 'standby'; - timestamp: number; - source: GroupSource | RoomSource; - left: { - members: UserSource[]; - }; -}; - -type PostbackEvent = { - replyToken: string; - type: 'postback'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; - postback: Postback; -}; - -type PostbackParams = - | { date: string } - | { time: string } - | { datetime: string }; - -type Postback = { - data: string; - params?: PostbackParams; -}; - -type BeaconEvent = { - replyToken: string; - type: 'beacon'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; - beacon: Beacon; -}; - -type Beacon = { - hwid: string; - type: 'enter' | 'banner' | 'stay'; - dm?: string; -}; - -type AccountLinkEvent = { - replyToken: string; - type: 'accountLink'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; - link: AccountLink; -}; - -type AccountLink = { - result: 'ok' | 'failed'; - nonce: string; -}; - -type ThingsEvent = { - replyToken: string; - type: 'things'; - mode: 'active' | 'standby'; - timestamp: number; - source: Source; - things: Things; -}; - -type Things = { - deviceId: string; - type: 'link' | 'unlink' | 'scenarioResult'; - result?: any; -}; - -type LineEventOptions = { - destination?: string; -}; - -export type LineRawEvent = - | MessageEvent - | FollowEvent - | UnfollowEvent - | JoinEvent - | LeaveEvent - | MemberJoinedEvent - | MemberLeftEvent - | PostbackEvent - | BeaconEvent - | AccountLinkEvent - | ThingsEvent; +import { + AccountLink, + AccountLinkEvent, + Beacon, + BeaconEvent, + EventMessage, + LineEventOptions, + LineRawEvent, + MemberJoinedEvent, + MemberLeftEvent, + MessageEvent, + Postback, + PostbackEvent, + Source, + TextMessage, + Things, + ThingsEvent, + UserSource, +} from './LineTypes'; export default class LineEvent implements Event { _rawEvent: LineRawEvent; @@ -247,6 +40,14 @@ export default class LineEvent implements Event { return this._rawEvent; } + /** + * The timestamp when the event was sent. + * + */ + get timestamp(): number { + return this._rawEvent.timestamp; + } + /** * The destination is the id of the bot which this event is sent to. * @@ -283,7 +84,7 @@ export default class LineEvent implements Event { * The message object from LINE raw event. * */ - get message(): Message | null { + get message(): EventMessage | null { return (this._rawEvent as MessageEvent).message || null; } @@ -292,7 +93,7 @@ export default class LineEvent implements Event { * */ get isText(): boolean { - return this.isMessage && (this.message as Message).type === 'text'; + return this.isMessage && (this.message as EventMessage).type === 'text'; } /** @@ -311,14 +112,14 @@ export default class LineEvent implements Event { * */ get isImage(): boolean { - return this.isMessage && (this.message as Message).type === 'image'; + return this.isMessage && (this.message as EventMessage).type === 'image'; } /** * The image object from LINE raw event. * */ - get image(): Message | null { + get image(): EventMessage | null { if (this.isImage) { return this.message; } @@ -330,14 +131,14 @@ export default class LineEvent implements Event { * */ get isVideo(): boolean { - return this.isMessage && (this.message as Message).type === 'video'; + return this.isMessage && (this.message as EventMessage).type === 'video'; } /** * The video object from LINE raw event. * */ - get video(): Message | null { + get video(): EventMessage | null { if (this.isVideo) { return this.message; } @@ -349,14 +150,14 @@ export default class LineEvent implements Event { * */ get isAudio(): boolean { - return this.isMessage && (this.message as Message).type === 'audio'; + return this.isMessage && (this.message as EventMessage).type === 'audio'; } /** * The audio object from LINE raw event. * */ - get audio(): Message | null { + get audio(): EventMessage | null { if (this.isAudio) { return this.message; } @@ -368,14 +169,14 @@ export default class LineEvent implements Event { * */ get isLocation(): boolean { - return this.isMessage && (this.message as Message).type === 'location'; + return this.isMessage && (this.message as EventMessage).type === 'location'; } /** * The location object from LINE raw event. * */ - get location(): Message | null { + get location(): EventMessage | null { if (this.isLocation) { return this.message; } @@ -387,14 +188,14 @@ export default class LineEvent implements Event { * */ get isSticker(): boolean { - return this.isMessage && (this.message as Message).type === 'sticker'; + return this.isMessage && (this.message as EventMessage).type === 'sticker'; } /** * The sticker object from LINE raw event. * */ - get sticker(): Message | null { + get sticker(): EventMessage | null { if (this.isSticker) { return this.message; } diff --git a/packages/bottender/src/line/LineTypes.ts b/packages/bottender/src/line/LineTypes.ts new file mode 100644 index 000000000..424075e3e --- /dev/null +++ b/packages/bottender/src/line/LineTypes.ts @@ -0,0 +1,270 @@ +import { RequestContext } from '../types'; + +export * from 'messaging-api-line/dist/LineTypes'; +export { LineConnectorOptions } from './LineConnector'; +export { LineContextOptions } from './LineContext'; + +export type UserSource = { + type: 'user'; + userId: string; +}; + +export type GroupSource = { + type: 'group'; + groupId: string; + userId?: string; +}; + +export type RoomSource = { + type: 'room'; + roomId: string; + userId?: string; +}; + +export type Source = UserSource | GroupSource | RoomSource; + +// conflict +export type EventTextMessage = { + id: string; + type: 'text'; + text: string; + /** + * One or more LINE emojis. Unicode-defined emojis and older versions of LINE emojis may not be retrieved correctly. + */ + emojis?: MessageEmoji[]; +}; + +// conflict +/** + * references: https://developers.line.biz/en/reference/messaging-api/#wh-text + */ +export type MessageEmoji = { + /** + * Index position for a character in text, with the first character being at position 0. + */ + index: number; + /** + * The length of the LINE emoji string. For LINE emoji (hello), 7 is the length. + */ + length: number; + /** + * Product ID for a set of LINE emoji. See LINE Available Emoji List: https://d.line-scdn.net/r/devcenter/sendable_line_emoji_list.pdf . + */ + productId: string; + /** + * ID for a LINE emoji inside a set. See LINE Available Emoji List: https://d.line-scdn.net/r/devcenter/sendable_line_emoji_list.pdf . + */ + emojiId: string; +}; + +export type ContentProviderLine = { + type: 'line'; +}; + +export type ContentProviderExternal = { + type: 'external'; + originalContentUrl: string; + previewImageUrl?: string; +}; + +export type EventImageMessage = { + id: string; + type: 'image'; + contentProvider: ContentProviderLine | ContentProviderExternal; +}; + +export type EventVideoMessage = { + id: string; + type: 'video'; + duration: number; + contentProvider: ContentProviderLine | ContentProviderExternal; +}; + +export type EventAudioMessage = { + id: string; + type: 'audio'; + duration: number; + contentProvider: ContentProviderLine | ContentProviderExternal; +}; + +export type EventFileMessage = { + id: string; + type: 'file'; + fileName: string; + fileSize: number; +}; + +export type EventLocationMessage = { + id: string; + type: 'location'; + title: string; + address: string; + latitude: number; + longitude: number; +}; + +export type EventStickerMessage = { + id: string; + type: 'sticker'; + packageId: string; + stickerId: string; +}; + +export type EventMessage = + | EventTextMessage + | EventImageMessage + | EventVideoMessage + | EventAudioMessage + | EventFileMessage + | EventLocationMessage + | EventStickerMessage; + +export type MessageEvent = { + replyToken: string; + type: 'message'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; + message: EventMessage; +}; + +export type FollowEvent = { + replyToken: string; + type: 'follow'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; +}; + +export type UnfollowEvent = { + type: 'unfollow'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; +}; + +export type JoinEvent = { + replyToken: string; + type: 'join'; + mode: 'active' | 'standby'; + timestamp: number; + source: GroupSource | RoomSource; +}; + +export type LeaveEvent = { + type: 'leave'; + mode: 'active' | 'standby'; + timestamp: number; + source: GroupSource | RoomSource; +}; + +export type MemberJoinedEvent = { + replyToken: string; + type: 'memberJoined'; + mode: 'active' | 'standby'; + timestamp: number; + source: GroupSource | RoomSource; + joined: { + members: UserSource[]; + }; +}; + +export type MemberLeftEvent = { + type: 'memberLeft'; + mode: 'active' | 'standby'; + timestamp: number; + source: GroupSource | RoomSource; + left: { + members: UserSource[]; + }; +}; + +export type PostbackEvent = { + replyToken: string; + type: 'postback'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; + postback: Postback; +}; + +export type PostbackParams = + | { date: string } + | { time: string } + | { datetime: string }; + +export type Postback = { + data: string; + params?: PostbackParams; +}; + +export type BeaconEvent = { + replyToken: string; + type: 'beacon'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; + beacon: Beacon; +}; + +export type Beacon = { + hwid: string; + type: 'enter' | 'banner' | 'stay'; + dm?: string; +}; + +export type AccountLinkEvent = { + replyToken: string; + type: 'accountLink'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; + link: AccountLink; +}; + +export type AccountLink = { + result: 'ok' | 'failed'; + nonce: string; +}; + +export type ThingsEvent = { + replyToken: string; + type: 'things'; + mode: 'active' | 'standby'; + timestamp: number; + source: Source; + things: Things; +}; + +export type Things = { + deviceId: string; + type: 'link' | 'unlink' | 'scenarioResult'; + result?: any; +}; + +export type LineEventOptions = { + destination?: string; +}; + +export type LineRawEvent = + | MessageEvent + | FollowEvent + | UnfollowEvent + | JoinEvent + | LeaveEvent + | MemberJoinedEvent + | MemberLeftEvent + | PostbackEvent + | BeaconEvent + | AccountLinkEvent + | ThingsEvent; + +export type LineRequestBody = { + destination: string; + events: LineRawEvent[]; +}; + +export type LineRequestContext = RequestContext< + LineRequestBody, + { 'x-line-signature'?: string } +>; diff --git a/packages/bottender/src/line/__tests__/LineBot.spec.ts b/packages/bottender/src/line/__tests__/LineBot.spec.ts index c0ee5c03d..d475e54e4 100644 --- a/packages/bottender/src/line/__tests__/LineBot.spec.ts +++ b/packages/bottender/src/line/__tests__/LineBot.spec.ts @@ -6,6 +6,7 @@ it('should construct bot with LineConnector', () => { accessToken: 'FAKE_TOKEN', channelSecret: 'FAKE_SECRET', }); + expect(bot).toBeDefined(); expect(bot.onEvent).toBeDefined(); expect(bot.createRequestHandler).toBeDefined(); diff --git a/packages/bottender/src/line/__tests__/LineConnector.spec.ts b/packages/bottender/src/line/__tests__/LineConnector.spec.ts index e3bd0cf42..e9f2702de 100644 --- a/packages/bottender/src/line/__tests__/LineConnector.spec.ts +++ b/packages/bottender/src/line/__tests__/LineConnector.spec.ts @@ -1,9 +1,11 @@ import warning from 'warning'; import { LineClient } from 'messaging-api-line'; +import { mocked } from 'ts-jest/utils'; -import LineConnector from '../LineConnector'; +import LineConnector, { GetSessionKeyPrefixFunction } from '../LineConnector'; import LineContext from '../LineContext'; import LineEvent from '../LineEvent'; +import { LineRequestBody } from '../LineTypes'; jest.mock('messaging-api-line'); jest.mock('warning'); @@ -11,182 +13,201 @@ jest.mock('warning'); const ACCESS_TOKEN = 'FAKE_TOKEN'; const CHANNEL_SECRET = 'FAKE_SECRET'; -const request = { - body: { - destination: 'Uea8667adaf43586706170ff25ff47ae6', - events: [ - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'message', - timestamp: 1462629479859, - source: { - type: 'user', - userId: 'U206d25c2ea6bd87c17655609a1c37cb8', - }, - message: { - id: '325708', - type: 'text', - text: 'Hello, world', - }, +const requestBody: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', + events: [ + { + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', }, - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'follow', - timestamp: 1462629479859, - source: { - type: 'user', - userId: 'U206d25c2ea6bd87c17655609a1c37cb8', - }, + message: { + id: '325708', + type: 'text', + text: 'Hello, world', }, - ], - }, - rawBody: 'fake_raw_body', - header: { - 'x-line-signature': 'fake_signature', - }, + }, + { + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'follow', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', + }, + }, + ], }; -const webhookVerifyRequest = { - body: { - events: [ - { - replyToken: '00000000000000000000000000000000', - type: 'message', - timestamp: 1513065174862, - source: { - type: 'user', - userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', - }, - message: { - id: '100001', - type: 'text', - text: 'Hello, world', - }, +const webhookVerifyRequestBody: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', + events: [ + { + replyToken: '00000000000000000000000000000000', + type: 'message', + mode: 'active', + timestamp: 1513065174862, + source: { + type: 'user', + userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', }, - { - replyToken: 'ffffffffffffffffffffffffffffffff', - type: 'message', - timestamp: 1513065174862, - source: { - type: 'user', - userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', - }, - message: { - id: '100002', - type: 'sticker', - packageId: '1', - stickerId: '1', - }, + message: { + id: '100001', + type: 'text', + text: 'Hello, world', + }, + }, + { + replyToken: 'ffffffffffffffffffffffffffffffff', + type: 'message', + mode: 'active', + timestamp: 1513065174862, + source: { + type: 'user', + userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', + }, + message: { + id: '100002', + type: 'sticker', + packageId: '1', + stickerId: '1', }, - ], - }, + }, + ], }; -function setup({ sendMethod, skipLegacyProfile } = {}) { - const mockLineAPIClient = { - getUserProfile: jest.fn(), - isValidSignature: jest.fn(), - getGroupMemberProfile: jest.fn(), - getAllGroupMemberIds: jest.fn(), - getRoomMemberProfile: jest.fn(), - getAllRoomMemberIds: jest.fn(), - }; - LineClient.connect = jest.fn(); - LineClient.connect.mockReturnValue(mockLineAPIClient); +beforeEach(() => { + // Clear all instances and calls to constructor and all methods: + mocked(LineClient).mockClear(); +}); + +function setup({ + sendMethod, + skipLegacyProfile, + getSessionKeyPrefix, +}: { + sendMethod?: string; + skipLegacyProfile?: boolean; + getSessionKeyPrefix?: GetSessionKeyPrefixFunction; +} = {}) { + const connector = new LineConnector({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + sendMethod, + skipLegacyProfile, + getSessionKeyPrefix, + }); + + const client = mocked(LineClient).mock.instances[0]; + return { - mockLineAPIClient, - connector: new LineConnector({ - accessToken: ACCESS_TOKEN, - channelSecret: CHANNEL_SECRET, - sendMethod, - skipLegacyProfile, - }), + client, + connector, }; } describe('#platform', () => { it('should be line', () => { const { connector } = setup(); + expect(connector.platform).toBe('line'); }); }); describe('#client', () => { - it('should be client', () => { - const { connector, mockLineAPIClient } = setup(); - expect(connector.client).toBe(mockLineAPIClient); + it('should be the client', () => { + const { connector, client } = setup(); + + expect(connector.client).toBe(client); }); - it('support custom client', () => { - const client = {}; - const connector = new LineConnector({ client }); + it('support using custom client', () => { + const client = new LineClient({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + }); + const connector = new LineConnector({ + client, + channelSecret: CHANNEL_SECRET, + }); + expect(connector.client).toBe(client); }); }); describe('#getUniqueSessionKey', () => { - it('extract userId from user source', () => { + it('extract userId from the HTTP body', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(request.body); + const senderId = connector.getUniqueSessionKey(requestBody); + expect(senderId).toBe('U206d25c2ea6bd87c17655609a1c37cb8'); }); - it('extract groupId from group source', () => { + it('extract userId from user source', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey({ - events: [ - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'message', - timestamp: 1462629479859, - source: { - type: 'group', - groupId: 'U206d25c2ea6bd87c17655609a1c37cb8', - }, - message: { - id: '325708', - type: 'text', - text: 'Hello, world', - }, + + const senderId = connector.getUniqueSessionKey( + new LineEvent({ + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', }, - ], - }); + message: { + id: '325708', + type: 'text', + text: 'Hello, world', + }, + }) + ); + expect(senderId).toBe('U206d25c2ea6bd87c17655609a1c37cb8'); }); - it('extract roomId from room source', () => { + it('extract groupId from the group source', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey({ - events: [ - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'message', - timestamp: 1462629479859, - source: { - type: 'room', - roomId: 'U206d25c2ea6bd87c17655609a1c37cb8', - }, - message: { - id: '325708', - type: 'text', - text: 'Hello, world', - }, + + const senderId = connector.getUniqueSessionKey( + new LineEvent({ + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'group', + groupId: 'U206d25c2ea6bd87c17655609a1c37cb8', }, - ], - }); + message: { + id: '325708', + type: 'text', + text: 'Hello, world', + }, + }) + ); + expect(senderId).toBe('U206d25c2ea6bd87c17655609a1c37cb8'); }); - it('extract from line event', () => { + it('extract roomId from the room source', () => { const { connector } = setup(); + const senderId = connector.getUniqueSessionKey( new LineEvent({ replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'message', + mode: 'active', timestamp: 1462629479859, source: { - type: 'user', - userId: 'U206d25c2ea6bd87c17655609a1c37cb8', + type: 'room', + roomId: 'U206d25c2ea6bd87c17655609a1c37cb8', }, message: { id: '325708', @@ -195,44 +216,69 @@ describe('#getUniqueSessionKey', () => { }, }) ); + expect(senderId).toBe('U206d25c2ea6bd87c17655609a1c37cb8'); }); - it('should throw error if source.type is not user, group or room', () => { - const { connector } = setup(); - let error; - try { - connector.getUniqueSessionKey({ - events: [ - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'message', - timestamp: 1462629479859, - source: { - type: 'other', // other type - roomId: 'U206d25c2ea6bd87c17655609a1c37cb8', - }, - message: { - id: '325708', - type: 'text', - text: 'Hello, world', - }, - }, - ], - }); - } catch (err) { - error = err; - } + it('should add the prefix to the session key when getSessionKeyPrefix exists', () => { + const getSessionKeyPrefix: GetSessionKeyPrefixFunction = jest.fn( + (_, requestContext) => { + if (requestContext) { + return `${requestContext.params.channelId}:`; + } + throw new Error('no request context'); + } + ); + const { connector } = setup({ + getSessionKeyPrefix, + }); + + const requestContext = { + method: 'post', + path: '/webhooks/line/CHANNEL_ID', + query: {}, + headers: {}, + rawBody: '{}', + body: {}, + params: { + channelId: 'CHANNEL_ID', + }, + url: 'https://www.example.com/webhooks/line/CHANNEL_ID', + }; + + const senderId = connector.getUniqueSessionKey( + new LineEvent({ + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', + }, + message: { + id: '325708', + type: 'text', + text: 'Hello, world', + }, + }), + requestContext + ); - expect(error).toEqual(expect.any(TypeError)); + expect(senderId).toBe('CHANNEL_ID:U206d25c2ea6bd87c17655609a1c37cb8'); + expect(getSessionKeyPrefix).toBeCalledWith( + expect.any(LineEvent), + requestContext + ); }); }); describe('#updateSession', () => { it('update session with data needed', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); + const user = { id: 'U206d25c2ea6bd87c17655609a1c37cb8', displayName: 'LINE taro', @@ -241,13 +287,14 @@ describe('#updateSession', () => { statusMessage: 'Hello, LINE!', _updatedAt: expect.any(String), }; - mockLineAPIClient.getUserProfile.mockResolvedValue(user); + mocked(client).getUserProfile.mockResolvedValue(user); - const session = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; - await connector.updateSession(session, request.body); + await connector.updateSession(session, requestBody); - expect(mockLineAPIClient.getUserProfile).toBeCalledWith( + expect(client.getUserProfile).toBeCalledWith( 'U206d25c2ea6bd87c17655609a1c37cb8' ); @@ -265,7 +312,7 @@ describe('#updateSession', () => { }); it('update session if session.user exists', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const user = { @@ -279,9 +326,9 @@ describe('#updateSession', () => { const session = { type: 'user', user }; - await connector.updateSession(session, request.body); + await connector.updateSession(session, requestBody); - expect(mockLineAPIClient.getUserProfile).not.toBeCalled(); + expect(client.getUserProfile).not.toBeCalled(); expect(session).toEqual({ type: 'user', user, @@ -296,14 +343,16 @@ describe('#updateSession', () => { }); it('update session with group type message', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); - const body = { + const body: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', events: [ { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'message', + mode: 'active', timestamp: 1462629479859, source: { type: 'group', @@ -332,18 +381,19 @@ describe('#updateSession', () => { 'Uxxxxxxxxxxxxxx...3', ]; - mockLineAPIClient.getGroupMemberProfile.mockResolvedValue(user); - mockLineAPIClient.getAllGroupMemberIds.mockResolvedValue(memberIds); + mocked(client).getGroupMemberProfile.mockResolvedValue(user); + mocked(client).getAllGroupMemberIds.mockResolvedValue(memberIds); - const session = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; await connector.updateSession(session, body); - expect(mockLineAPIClient.getGroupMemberProfile).toBeCalledWith( + expect(client.getGroupMemberProfile).toBeCalledWith( 'Ca56f94637cc4347f90a25382909b24b9', 'U206d25c2ea6bd87c17655609a1c37cb8' ); - expect(mockLineAPIClient.getAllGroupMemberIds).toBeCalledWith( + expect(client.getAllGroupMemberIds).toBeCalledWith( 'Ca56f94637cc4347f90a25382909b24b9' ); @@ -370,14 +420,16 @@ describe('#updateSession', () => { }); it('update session with group type event without userId', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); - const body = { + const body: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', events: [ { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'join', + mode: 'active', timestamp: 1462629479859, source: { type: 'group', @@ -393,14 +445,15 @@ describe('#updateSession', () => { 'Uxxxxxxxxxxxxxx...3', ]; - mockLineAPIClient.getAllGroupMemberIds.mockResolvedValue(memberIds); + mocked(client).getAllGroupMemberIds.mockResolvedValue(memberIds); - const session = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; await connector.updateSession(session, body); - expect(mockLineAPIClient.getGroupMemberProfile).not.toBeCalled(); - expect(mockLineAPIClient.getAllGroupMemberIds).toBeCalledWith( + expect(client.getGroupMemberProfile).not.toBeCalled(); + expect(client.getAllGroupMemberIds).toBeCalledWith( 'Ca56f94637cc4347f90a25382909b24b9' ); @@ -427,14 +480,16 @@ describe('#updateSession', () => { }); it('update session with room type message', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); - const body = { + const body: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', events: [ { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'message', + mode: 'active', timestamp: 1462629479859, source: { type: 'room', @@ -463,18 +518,19 @@ describe('#updateSession', () => { 'Uxxxxxxxxxxxxxx...3', ]; - mockLineAPIClient.getRoomMemberProfile.mockResolvedValue(user); - mockLineAPIClient.getAllRoomMemberIds.mockResolvedValue(memberIds); + mocked(client).getRoomMemberProfile.mockResolvedValue(user); + mocked(client).getAllRoomMemberIds.mockResolvedValue(memberIds); - const session = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; await connector.updateSession(session, body); - expect(mockLineAPIClient.getRoomMemberProfile).toBeCalledWith( + expect(client.getRoomMemberProfile).toBeCalledWith( 'Ra8dbf4673c4c812cd491258042226c99', 'U206d25c2ea6bd87c17655609a1c37cb8' ); - expect(mockLineAPIClient.getAllRoomMemberIds).toBeCalledWith( + expect(client.getAllRoomMemberIds).toBeCalledWith( 'Ra8dbf4673c4c812cd491258042226c99' ); @@ -501,14 +557,16 @@ describe('#updateSession', () => { }); it('update session with room type event without userId', async () => { - const { connector, mockLineAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); - const body = { + const body: LineRequestBody = { + destination: 'Uea8667adaf43586706170ff25ff47ae6', events: [ { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'join', + mode: 'active', timestamp: 1462629479859, source: { type: 'room', @@ -524,14 +582,15 @@ describe('#updateSession', () => { 'Uxxxxxxxxxxxxxx...3', ]; - mockLineAPIClient.getAllRoomMemberIds.mockResolvedValue(memberIds); + mocked(client).getAllRoomMemberIds.mockResolvedValue(memberIds); - const session = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; await connector.updateSession(session, body); - expect(mockLineAPIClient.getRoomMemberProfile).not.toBeCalled(); - expect(mockLineAPIClient.getAllRoomMemberIds).toBeCalledWith( + expect(client.getRoomMemberProfile).not.toBeCalled(); + expect(client.getAllRoomMemberIds).toBeCalledWith( 'Ra8dbf4673c4c812cd491258042226c99' ); @@ -557,50 +616,15 @@ describe('#updateSession', () => { }); }); - it('update session with other type message', async () => { - const { connector } = setup({ - skipLegacyProfile: false, - }); - const body = { - events: [ - { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'message', - timestamp: 1462629479859, - source: { - type: 'other', - }, - message: { - id: '325708', - type: 'text', - text: 'Hello, world', - }, - }, - ], - }; - - const session = {}; - - await connector.updateSession(session, body); - - expect(session).toEqual({ - type: 'other', - }); - expect(Object.getOwnPropertyDescriptor(session, 'user')).toEqual({ - configurable: false, - enumerable: true, - writable: false, - value: undefined, - }); - }); - it('update userId without calling any api while skipLegacyProfile set to true', async () => { - const { connector, mockLineAPIClient } = setup(); - const session = {}; + const { connector, client } = setup(); - await connector.updateSession(session, request.body); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session: any = {}; - expect(mockLineAPIClient.getUserProfile).not.toBeCalled(); + await connector.updateSession(session, requestBody); + + expect(client.getUserProfile).not.toBeCalled(); expect(session).toEqual({ type: 'user', @@ -620,9 +644,9 @@ describe('#updateSession', () => { }); describe('#mapRequestToEvents', () => { - it('should map request to LineEvents', () => { + it('should map the HTTP body to the LineEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(request.body); + const events = connector.mapRequestToEvents(requestBody); expect(events).toHaveLength(2); expect(events[0]).toBeInstanceOf(LineEvent); @@ -633,9 +657,24 @@ describe('#mapRequestToEvents', () => { }); describe('#createContext', () => { - it('should create LineContext', async () => { + it('should create a LineContext', async () => { const { connector } = setup(); - const event = {}; + + const event = new LineEvent({ + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', + }, + message: { + id: '325708', + type: 'text', + text: 'Hello, world', + }, + }); const session = {}; const context = await connector.createContext({ @@ -646,15 +685,72 @@ describe('#createContext', () => { expect(context).toBeDefined(); expect(context).toBeInstanceOf(LineContext); }); + + it('should create a LineContext using the config from getConfig', async () => { + const getConfig = jest.fn(); + + getConfig.mockResolvedValue({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + }); + + const connector = new LineConnector({ + getConfig, + }); + + const event = new LineEvent({ + replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', + type: 'message', + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'user', + userId: 'U206d25c2ea6bd87c17655609a1c37cb8', + }, + message: { + id: '325708', + type: 'text', + text: 'Hello, world', + }, + }); + const session = {}; + + await connector.createContext({ + event, + session, + requestContext: { + path: '/webhooks/line/11111111111', + params: { + channelId: '11111111111', + }, + url: `https://www.example.com/webhooks/line/11111111111`, + method: 'post', + headers: { + 'x-line-signature': '5+SUnXZ8+G1ErXUewxeZ0T9j4yD6cmwYn5XCO4tBFic', + }, + query: {}, + rawBody: JSON.stringify(requestBody), + body: requestBody, + }, + }); + + expect(getConfig).toBeCalledWith({ params: { channelId: '11111111111' } }); + expect(LineClient).toBeCalledWith({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + origin: undefined, + }); + }); }); describe('#verifySignature', () => { - it('should return true if signature is equal app sercret after crypto', () => { + it('should return true if signature is equal app secret after crypto', () => { const { connector } = setup(); const result = connector.verifySignature( 'rawBody', - 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=' + 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=', + { channelSecret: CHANNEL_SECRET } ); expect(result).toBe(true); @@ -668,73 +764,112 @@ it('should warning if sendMethod is not one of `reply`, `push`', () => { }); describe('#isWebhookVerifyRequest', () => { - it('check if request is for webhook verification', async () => { + it('check if the HTTP body is for webhook verification', async () => { const { connector } = setup(); - await connector.isWebhookVerifyRequest(webhookVerifyRequest.body); - - expect(connector.isWebhookVerifyRequest(webhookVerifyRequest.body)).toEqual( + expect(connector.isWebhookVerifyRequest(webhookVerifyRequestBody)).toEqual( true ); - expect(connector.isWebhookVerifyRequest({ events: [] })).toEqual(false); + expect( + connector.isWebhookVerifyRequest({ + destination: 'Uea8667adaf43586706170ff25ff47ae6', + events: [], + }) + ).toEqual(false); }); }); describe('#preprocess', () => { - it('should return shouldNext: true if request method is get', () => { + it('should return shouldNext: true if the method is get', async () => { const { connector } = setup(); expect( - connector.preprocess({ + await connector.preprocess({ + path: '/webhooks/line', + params: {}, + url: `https://www.example.com/webhooks/line`, method: 'get', headers: { 'x-line-signature': 'abc', }, query: {}, - rawBody: '', - body: {}, + rawBody: JSON.stringify(requestBody), + body: requestBody, }) ).toEqual({ shouldNext: true, }); }); - it('should return shouldNext: true if signature match', () => { + it('should return shouldNext: true if the signature match', async () => { const { connector } = setup(); expect( - connector.preprocess({ + await connector.preprocess({ + path: '/webhooks/line', + params: {}, + url: `https://www.example.com/webhooks/line`, method: 'post', headers: { - 'x-line-signature': 'RI34eF3TZ0LkP6PK2d4uyozrY5HnnHQuM+UsZjfzLaw=', + 'x-line-signature': '5+SUnXZ8+G1ErXUewxeZ0T9j4yD6cmwYn5XCO4tBFic', }, query: {}, - rawBody: '{}', - body: {}, + rawBody: JSON.stringify(requestBody), + body: requestBody, }) ).toEqual({ shouldNext: true, }); }); - it('should return shouldNext: false and 200 OK if is webhook verify request', () => { - const { connector } = setup(); + it('should return shouldNext: true if the signature match (using getConfig)', async () => { + const getConfig = jest.fn(); + + getConfig.mockResolvedValue({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + }); + + const connector = new LineConnector({ + getConfig, + }); expect( - connector.preprocess({ + await connector.preprocess({ + path: '/webhooks/line/11111111111', + params: { + channelId: '11111111111', + }, + url: `https://www.example.com/webhooks/line/11111111111`, method: 'post', headers: { - 'x-line-signature': '8nCfywRHp6BCJRiIALL7L3s/qaUK4TDDQ1h52j4hVLk=', + 'x-line-signature': '5+SUnXZ8+G1ErXUewxeZ0T9j4yD6cmwYn5XCO4tBFic', }, query: {}, - rawBody: - '{"events":[{"replyToken":"00000000000000000000000000000000"},{"replyToken":"ffffffffffffffffffffffffffffffff"}]}', - body: { - events: [ - { replyToken: '00000000000000000000000000000000' }, - { replyToken: 'ffffffffffffffffffffffffffffffff' }, - ], + rawBody: JSON.stringify(requestBody), + body: requestBody, + }) + ).toEqual({ + shouldNext: true, + }); + expect(getConfig).toBeCalledWith({ params: { channelId: '11111111111' } }); + }); + + it('should return shouldNext: false and 200 OK if the HTTP body is for webhook verification', async () => { + const { connector } = setup(); + + expect( + await connector.preprocess({ + path: '/webhooks/line', + params: {}, + url: `https://www.example.com/webhooks/line`, + method: 'post', + headers: { + 'x-line-signature': 'VYLhgSyybnkWRb9qqCreJSTQTkbS6KtXVuw55BcAS7o', }, + query: {}, + rawBody: JSON.stringify(webhookVerifyRequestBody), + body: webhookVerifyRequestBody, }) ).toEqual({ shouldNext: false, @@ -745,18 +880,68 @@ describe('#preprocess', () => { }); }); - it('should return shouldNext: false and error if signature does not match', () => { + it('should return shouldNext: false and the error if the signature does not match', async () => { const { connector } = setup(); expect( - connector.preprocess({ + await connector.preprocess({ + path: '/webhooks/line', + params: {}, + url: `https://www.example.com/webhooks/line`, + method: 'post', + headers: { + 'x-line-signature': 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=', + }, + query: {}, + rawBody: JSON.stringify(requestBody), + body: requestBody, + }) + ).toEqual({ + shouldNext: false, + response: { + status: 400, + body: { + error: { + message: 'LINE Signature Validation Failed!', + request: { + headers: { + 'x-line-signature': + 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=', + }, + rawBody: JSON.stringify(requestBody), + }, + }, + }, + }, + }); + }); + + it('should return shouldNext: false and the error if the signature does not match (using getConfig)', async () => { + const getConfig = jest.fn(); + + getConfig.mockResolvedValue({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + }); + + const connector = new LineConnector({ + getConfig, + }); + + expect( + await connector.preprocess({ + path: '/webhooks/line/11111111111', + params: { + channelId: '11111111111', + }, + url: `https://www.example.com/webhooks/line/11111111111`, method: 'post', headers: { 'x-line-signature': 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=', }, query: {}, - rawBody: '{}', - body: {}, + rawBody: JSON.stringify(requestBody), + body: requestBody, }) ).toEqual({ shouldNext: false, @@ -770,11 +955,12 @@ describe('#preprocess', () => { 'x-line-signature': 'XtFE4w+/e5cw8ys6BSALGj3ZCYgRtBdCBxyEfrkgLPc=', }, - rawBody: '{}', + rawBody: JSON.stringify(requestBody), }, }, }, }, }); + expect(getConfig).toBeCalledWith({ params: { channelId: '11111111111' } }); }); }); diff --git a/packages/bottender/src/line/__tests__/LineContext.spec.ts b/packages/bottender/src/line/__tests__/LineContext.spec.ts index 71129ec43..c2380a175 100644 --- a/packages/bottender/src/line/__tests__/LineContext.spec.ts +++ b/packages/bottender/src/line/__tests__/LineContext.spec.ts @@ -1,28 +1,20 @@ -jest.mock('delay'); +import warning from 'warning'; +import { Line, LineClient } from 'messaging-api-line'; +import { mocked } from 'ts-jest/utils'; + +import LineContext from '../LineContext'; +import LineEvent from '../LineEvent'; +import { FlexContainer, LineRawEvent, QuickReply } from '../LineTypes'; + jest.mock('messaging-api-line'); jest.mock('warning'); -let Line; -let LineClient; -let LineContext; -let LineEvent; -let sleep; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - Line = require('messaging-api-line').Line; - LineClient = require('messaging-api-line').LineClient; - LineContext = require('../LineContext').default; - LineEvent = require('../LineEvent').default; - sleep = require('delay'); - warning = require('warning'); - /* eslint-enable global-require */ -}); +const ACCESS_TOKEN = 'FAKE_TOKEN'; +const CHANNEL_SECRET = 'FAKE_SECRET'; const REPLY_TOKEN = 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA'; -const rawEventText = { +const rawEventText: LineRawEvent = { replyToken: REPLY_TOKEN, type: 'message', timestamp: 1462629479859, @@ -64,7 +56,7 @@ const groupSession = { }, }; -const quickReply = { +const quickReply: QuickReply = { items: [ { type: 'action', @@ -91,7 +83,11 @@ const setup = ({ customAccessToken, rawEvent = rawEventText, } = {}) => { - const client = LineClient.connect(); + const client = new LineClient({ + accessToken: ACCESS_TOKEN, + channelSecret: CHANNEL_SECRET, + }); + const context = new LineContext({ client, event: new LineEvent(rawEvent), @@ -101,6 +97,7 @@ const setup = ({ sendMethod, customAccessToken, }); + return { context, session, @@ -108,33 +105,33 @@ const setup = ({ }; }; -it('be defined', () => { - const { context } = setup(); - expect(context).toBeDefined(); -}); - it('#platform to be `line`', () => { const { context } = setup(); + expect(context.platform).toBe('line'); }); it('#isReplied default to be false', () => { const { context } = setup(); + expect(context.isReplied).toBe(false); }); it('get #session works', () => { const { context, session } = setup(); + expect(context.session).toBe(session); }); it('get #event works', () => { const { context } = setup(); + expect(context.event).toBeInstanceOf(LineEvent); }); it('get #client works', () => { const { context, client } = setup(); + expect(context.client).toBe(client); }); @@ -161,11 +158,11 @@ describe('#getMessageContent', () => { }); const buf = Buffer.from(''); - client.getMessageContent.mockResolvedValue(buf); + mocked(client.getMessageContent).mockResolvedValue(buf); const res = await context.getMessageContent(); - expect(client.getMessageContent).toBeCalledWith('325708', {}); + expect(client.getMessageContent).toBeCalledWith('325708'); expect(res).toEqual(buf); }); @@ -196,7 +193,7 @@ describe('#leave', () => { await context.leave(); - expect(client.leaveGroup).toBeCalledWith('fakeGroupId', {}); + expect(client.leaveGroup).toBeCalledWith('fakeGroupId'); }); it('leave room', async () => { @@ -204,7 +201,7 @@ describe('#leave', () => { await context.leave(); - expect(client.leaveRoom).toBeCalledWith('fakeRoomId', {}); + expect(client.leaveRoom).toBeCalledWith('fakeRoomId'); }); it('not leave user', async () => { @@ -219,7 +216,7 @@ describe('#leave', () => { }); describe('#reply', () => { - it('#reply to call client.reply', async () => { + it('to call client.reply', async () => { const { context, client } = setup(); await context.reply([ @@ -229,16 +226,12 @@ describe('#reply', () => { }, ]); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: 'hello', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: 'hello', + }, + ]); }); it('should work with quickReply', async () => { @@ -252,17 +245,13 @@ describe('#reply', () => { }, ]); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: 'hello', - quickReply, - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: 'hello', + quickReply, + }, + ]); }); }); @@ -272,11 +261,9 @@ describe('#replyText', () => { await context.replyText('hello'); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { text: 'hello', type: 'text' }, + ]); }); it('should work with quickReply', async () => { @@ -286,19 +273,13 @@ describe('#replyText', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: 'hello', - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'text', + text: 'hello', quickReply, - } - ); + }, + ]); }); it('should throw when reply multiple times', async () => { @@ -316,22 +297,6 @@ describe('#replyText', () => { expect(error).toBeDefined(); expect(error.message).toEqual('Can not reply event multiple times'); }); - - it('should support custom token', async () => { - const { context, client } = setup({ - customAccessToken: 'anyToken', - }); - - await context.replyText('hello'); - - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [{ text: 'hello', type: 'text' }], - { - accessToken: 'anyToken', - } - ); - }); }); describe('#push', () => { @@ -345,16 +310,12 @@ describe('#push', () => { }, ]); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: 'hello', - }, - ], - { accessToken: undefined } - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: 'hello', + }, + ]); }); it('should work with quickReply', async () => { @@ -368,17 +329,13 @@ describe('#push', () => { }, ]); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: 'hello', - quickReply, - }, - ], - {} - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: 'hello', + quickReply, + }, + ]); }); }); @@ -388,11 +345,9 @@ describe('#pushText', () => { await context.pushText('hello'); - expect(client.push).toBeCalledWith( - session.user.id, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { text: 'hello', type: 'text' }, + ]); }); it('should work with quickReply', async () => { @@ -402,19 +357,13 @@ describe('#pushText', () => { quickReply, }); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - text: 'hello', - type: 'text', - quickReply, - }, - ], + expect(client.push).toBeCalledWith(session.user.id, [ { + text: 'hello', + type: 'text', quickReply, - } - ); + }, + ]); }); it('should work with room session', async () => { @@ -424,11 +373,9 @@ describe('#pushText', () => { await context.pushText('hello'); - expect(client.push).toBeCalledWith( - session.room.id, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.push).toBeCalledWith(session.room.id, [ + { text: 'hello', type: 'text' }, + ]); }); it('should work with group session', async () => { @@ -438,11 +385,9 @@ describe('#pushText', () => { await context.pushText('hello'); - expect(client.push).toBeCalledWith( - session.group.id, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.push).toBeCalledWith(session.group.id, [ + { text: 'hello', type: 'text' }, + ]); }); it('should call warning and not to send if dont have session', async () => { @@ -453,22 +398,6 @@ describe('#pushText', () => { expect(warning).toBeCalled(); expect(client.push).not.toBeCalled(); }); - - it('should support custom token', async () => { - const { context, client, session } = setup({ - customAccessToken: 'anyToken', - }); - - await context.pushText('hello'); - - expect(client.push).toBeCalledWith( - session.user.id, - [{ text: 'hello', type: 'text' }], - { - accessToken: 'anyToken', - } - ); - }); }); describe('send APIs', () => { @@ -478,20 +407,16 @@ describe('send APIs', () => { await context.send([Line.createText('2'), Line.createText('3')]); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); }); }); @@ -501,16 +426,12 @@ describe('send APIs', () => { await context.sendText('hello'); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: 'hello', - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: 'hello', + }, + ]); }); it('should work with quickReply', async () => { @@ -520,19 +441,13 @@ describe('send APIs', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: 'hello', - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'text', + text: 'hello', quickReply, - } - ); + }, + ]); }); }); @@ -545,17 +460,13 @@ describe('send APIs', () => { previewImageUrl: 'yyy.jpg', }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'image', - originalContentUrl: 'xxx.jpg', - previewImageUrl: 'yyy.jpg', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'image', + originalContentUrl: 'xxx.jpg', + previewImageUrl: 'yyy.jpg', + }, + ]); }); it('should work with quickReply', async () => { @@ -571,20 +482,14 @@ describe('send APIs', () => { } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'image', - originalContentUrl: 'xxx.jpg', - previewImageUrl: 'yyy.jpg', - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'image', + originalContentUrl: 'xxx.jpg', + previewImageUrl: 'yyy.jpg', quickReply, - } - ); + }, + ]); }); }); @@ -597,17 +502,13 @@ describe('send APIs', () => { duration: 240000, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'audio', - originalContentUrl: 'xxx.m4a', - duration: 240000, - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'audio', + originalContentUrl: 'xxx.m4a', + duration: 240000, + }, + ]); }); it('should work with quickReply', async () => { @@ -623,20 +524,14 @@ describe('send APIs', () => { } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'audio', - originalContentUrl: 'xxx.m4a', - duration: 240000, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'audio', + originalContentUrl: 'xxx.m4a', + duration: 240000, quickReply, - } - ); + }, + ]); }); }); @@ -649,17 +544,13 @@ describe('send APIs', () => { previewImageUrl: 'yyy.jpg', }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'video', - originalContentUrl: 'xxx.mp4', - previewImageUrl: 'yyy.jpg', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'video', + originalContentUrl: 'xxx.mp4', + previewImageUrl: 'yyy.jpg', + }, + ]); }); it('should work with quickReply', async () => { @@ -675,20 +566,14 @@ describe('send APIs', () => { } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'video', - originalContentUrl: 'xxx.mp4', - previewImageUrl: 'yyy.jpg', - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'video', + originalContentUrl: 'xxx.mp4', + previewImageUrl: 'yyy.jpg', quickReply, - } - ); + }, + ]); }); }); @@ -703,19 +588,15 @@ describe('send APIs', () => { longitude: 139.70372892916203, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'location', - title: 'my location', - address: '〒150-0002 東京都渋谷区渋谷2丁目21−1', - latitude: 35.65910807942215, - longitude: 139.70372892916203, - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'location', + title: 'my location', + address: '〒150-0002 東京都渋谷区渋谷2丁目21−1', + latitude: 35.65910807942215, + longitude: 139.70372892916203, + }, + ]); }); it('should work with quickReply', async () => { @@ -733,22 +614,16 @@ describe('send APIs', () => { } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'location', - title: 'my location', - address: '〒150-0002 東京都渋谷区渋谷2丁目21−1', - latitude: 35.65910807942215, - longitude: 139.70372892916203, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'location', + title: 'my location', + address: '〒150-0002 東京都渋谷区渋谷2丁目21−1', + latitude: 35.65910807942215, + longitude: 139.70372892916203, quickReply, - } - ); + }, + ]); }); }); @@ -761,17 +636,13 @@ describe('send APIs', () => { stickerId: '1', }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'sticker', - packageId: '1', - stickerId: '1', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'sticker', + packageId: '1', + stickerId: '1', + }, + ]); }); it('should work with quickReply', async () => { @@ -787,20 +658,14 @@ describe('send APIs', () => { } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'sticker', - packageId: '1', - stickerId: '1', - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'sticker', + packageId: '1', + stickerId: '1', quickReply, - } - ); + }, + ]); }); }); @@ -840,17 +705,13 @@ describe('send APIs', () => { await context.sendImagemap('this is an imagemap', template); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'imagemap', - altText: 'this is an imagemap', - ...template, - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'imagemap', + altText: 'this is an imagemap', + ...template, + }, + ]); }); it('should work with quickReply', async () => { @@ -860,25 +721,19 @@ describe('send APIs', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'imagemap', - altText: 'this is an imagemap', - ...template, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'imagemap', + altText: 'this is an imagemap', + ...template, quickReply, - } - ); + }, + ]); }); }); describe('#sendFlex', () => { - const contents = { + const contents: FlexContainer = { type: 'bubble', header: { type: 'box', @@ -924,17 +779,13 @@ describe('send APIs', () => { await context.sendFlex('this is a flex', contents); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'flex', - altText: 'this is a flex', - contents, - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'flex', + altText: 'this is a flex', + contents, + }, + ]); }); it('should work with quickReply', async () => { @@ -944,20 +795,14 @@ describe('send APIs', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'flex', - altText: 'this is a flex', - contents, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'flex', + altText: 'this is a flex', + contents, quickReply, - } - ); + }, + ]); }); }); @@ -991,17 +836,13 @@ describe('send APIs', () => { await context.sendTemplate('this is a template', template); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a template', - template, - }, - ], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a template', + template, + }, + ]); }); it('should work with quickReply', async () => { @@ -1011,20 +852,14 @@ describe('send APIs', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a template', - template, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'template', + altText: 'this is a template', + template, quickReply, - } - ); + }, + ]); }); }); @@ -1057,20 +892,16 @@ describe('send APIs', () => { await context.sendButtonTemplate('this is a button template', template); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a button template', - template: { - type: 'buttons', - ...template, - }, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a button template', + template: { + type: 'buttons', + ...template, }, - ], - { accessToken: undefined } - ); + }, + ]); }); it('should work with quickReply', async () => { @@ -1080,23 +911,17 @@ describe('send APIs', () => { quickReply, }); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a button template', - template: { - type: 'buttons', - ...template, - }, - quickReply, - }, - ], + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ { + type: 'template', + altText: 'this is a button template', + template: { + type: 'buttons', + ...template, + }, quickReply, - } - ); + }, + ]); }); it('should support sendButtonsTemplate alias', async () => { @@ -1104,20 +929,16 @@ describe('send APIs', () => { await context.sendButtonsTemplate('this is a button template', template); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a button template', - template: { - type: 'buttons', - ...template, - }, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a button template', + template: { + type: 'buttons', + ...template, }, - ], - {} - ); + }, + ]); }); }); @@ -1143,20 +964,16 @@ describe('send APIs', () => { await context.sendConfirmTemplate('this is a confirm template', template); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a confirm template', - template: { - type: 'confirm', - ...template, - }, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a confirm template', + template: { + type: 'confirm', + ...template, }, - ], - { accessToken: undefined } - ); + }, + ]); }); it('should work with quickReply', async () => { @@ -1168,21 +985,17 @@ describe('send APIs', () => { { quickReply } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a confirm template', - template: { - type: 'confirm', - ...template, - }, - quickReply, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a confirm template', + template: { + type: 'confirm', + ...template, }, - ], - { quickReply } - ); + quickReply, + }, + ]); }); }); @@ -1242,22 +1055,18 @@ describe('send APIs', () => { template ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a carousel template', - template: { - type: 'carousel', - imageAspectRatio: undefined, - imageSize: undefined, - columns: template, - }, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a carousel template', + template: { + type: 'carousel', + imageAspectRatio: undefined, + imageSize: undefined, + columns: template, }, - ], - { accessToken: undefined } - ); + }, + ]); }); it('should work with quickReply', async () => { @@ -1269,23 +1078,19 @@ describe('send APIs', () => { { quickReply } ); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'template', - altText: 'this is a carousel template', - template: { - type: 'carousel', - imageAspectRatio: undefined, - imageSize: undefined, - columns: template, - }, - quickReply, + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'template', + altText: 'this is a carousel template', + template: { + type: 'carousel', + imageAspectRatio: undefined, + imageSize: undefined, + columns: template, }, - ], - { quickReply } - ); + quickReply, + }, + ]); }); }); }); @@ -1329,8 +1134,7 @@ describe('profile APIs', () => { expect(client.getGroupMemberProfile).toBeCalledWith( 'fakeGroupId', - 'fakeUserId', - { accessToken: undefined } + 'fakeUserId' ); }); @@ -1341,8 +1145,7 @@ describe('profile APIs', () => { expect(client.getRoomMemberProfile).toBeCalledWith( 'fakeRoomId', - 'fakeUserId', - { accessToken: undefined } + 'fakeUserId' ); }); @@ -1351,7 +1154,7 @@ describe('profile APIs', () => { await context.getUserProfile(); - expect(client.getUserProfile).toBeCalledWith('fakeUserId', {}); + expect(client.getUserProfile).toBeCalledWith('fakeUserId'); }); }); @@ -1373,8 +1176,7 @@ describe('profile APIs', () => { expect(client.getGroupMemberProfile).toBeCalledWith( 'fakeGroupId', - 'anotherUser', - { accessToken: undefined } + 'anotherUser' ); }); @@ -1385,8 +1187,7 @@ describe('profile APIs', () => { expect(client.getRoomMemberProfile).toBeCalledWith( 'fakeRoomId', - 'anotherUser', - { accessToken: undefined } + 'anotherUser' ); }); @@ -1421,8 +1222,7 @@ describe('member IDs APIs', () => { expect(client.getGroupMemberIds).toBeCalledWith( 'fakeGroupId', - 'startToken', - { accessToken: undefined } + 'startToken' ); }); @@ -1433,8 +1233,7 @@ describe('member IDs APIs', () => { expect(client.getRoomMemberIds).toBeCalledWith( 'fakeRoomId', - 'startToken', - { accessToken: undefined } + 'startToken' ); }); @@ -1465,7 +1264,7 @@ describe('member IDs APIs', () => { await context.getAllMemberIds(); - expect(client.getAllGroupMemberIds).toBeCalledWith('fakeGroupId', {}); + expect(client.getAllGroupMemberIds).toBeCalledWith('fakeGroupId'); }); it('get memeber ids in room', async () => { @@ -1473,7 +1272,7 @@ describe('member IDs APIs', () => { await context.getAllMemberIds(); - expect(client.getAllRoomMemberIds).toBeCalledWith('fakeRoomId', {}); + expect(client.getAllRoomMemberIds).toBeCalledWith('fakeRoomId'); }); it('not get user profile in user session', async () => { @@ -1486,6 +1285,44 @@ describe('member IDs APIs', () => { expect(warning).toBeCalled(); }); }); + + describe('#getMembersCount', () => { + it('not get profile without session', async () => { + const { context, client } = setup({ session: null }); + + await context.getMembersCount(); + + expect(client.getGroupMembersCount).not.toBeCalled(); + expect(client.getRoomMembersCount).not.toBeCalled(); + expect(warning).toBeCalled(); + }); + + it('get member count in group', async () => { + const { context, client } = setup({ session: groupSession }); + + await context.getMembersCount(); + + expect(client.getGroupMembersCount).toBeCalledWith('fakeGroupId'); + }); + + it('get member count in room', async () => { + const { context, client } = setup({ session: roomSession }); + + await context.getMembersCount(); + + expect(client.getRoomMembersCount).toBeCalledWith('fakeRoomId'); + }); + + it('get member count = 1 in private chat', async () => { + const { context, client } = setup({ session: userSession }); + + const res = await context.getMembersCount(); + + expect(client.getGroupMembersCount).not.toBeCalled(); + expect(client.getRoomMembersCount).not.toBeCalled(); + expect(res).toBe(1); + }); + }); }); describe('ruchmenu APIs', () => { @@ -1499,7 +1336,7 @@ describe('ruchmenu APIs', () => { const result = await context.getLinkedRichMenu(); - expect(client.getLinkedRichMenu).toBeCalledWith(session.user.id, {}); + expect(client.getLinkedRichMenu).toBeCalledWith(session.user.id); expect(result.richMenuId).toEqual('richMenuId'); }); @@ -1518,11 +1355,7 @@ describe('ruchmenu APIs', () => { await context.linkRichMenu('richMenuId'); - expect(client.linkRichMenu).toBeCalledWith( - session.user.id, - 'richMenuId', - { accessToken: undefined } - ); + expect(client.linkRichMenu).toBeCalledWith(session.user.id, 'richMenuId'); }); it('should warn without user', async () => { @@ -1540,7 +1373,7 @@ describe('ruchmenu APIs', () => { await context.unlinkRichMenu(); - expect(client.unlinkRichMenu).toBeCalledWith(session.user.id, {}); + expect(client.unlinkRichMenu).toBeCalledWith(session.user.id); }); it('should warn without user', async () => { @@ -1557,13 +1390,13 @@ describe('account link APIs', () => { describe('#issueLinkToken', () => { it('should call client.issueLinkToken', async () => { const { context, client, session } = setup(); - client.issueLinkToken.mockResolvedValue({ + mocked(client.issueLinkToken).mockResolvedValue({ token: 'xxxxx', }); const result = await context.issueLinkToken(); - expect(client.issueLinkToken).toBeCalledWith(session.user.id, {}); + expect(client.issueLinkToken).toBeCalledWith(session.user.id); expect(result).toEqual({ token: 'xxxxx', }); @@ -1580,24 +1413,6 @@ describe('account link APIs', () => { }); }); -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context } = setup(); - - await context.typing(10); - - expect(sleep).toBeCalled(); - }); -}); - describe('batch', () => { it('should not batch when shouldBatch: false', async () => { const { client, context, session } = setup({ shouldBatch: false }); @@ -1608,36 +1423,24 @@ describe('batch', () => { await context.handlerDidEnd(); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '1', - }, - ], - { accessToken: undefined } - ); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '2', - }, - ], - { accessToken: undefined } - ); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '1', + }, + ]); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '2', + }, + ]); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '3', + }, + ]); }); it('should batch reply', async () => { @@ -1649,24 +1452,20 @@ describe('batch', () => { await context.handlerDidEnd(); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); }); it('should work with context.reply', async () => { @@ -1677,24 +1476,20 @@ describe('batch', () => { await context.handlerDidEnd(); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); }); it('should warning when reply over 5 messages', async () => { @@ -1711,32 +1506,28 @@ describe('batch', () => { await context.handlerDidEnd(); expect(client.reply).toHaveBeenCalledTimes(1); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - { - type: 'text', - text: '4', - }, - { - type: 'text', - text: '5', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + { + type: 'text', + text: '4', + }, + { + type: 'text', + text: '5', + }, + ]); expect(warning).toBeCalledWith(false, expect.any(String)); }); @@ -1749,24 +1540,20 @@ describe('batch', () => { await context.handlerDidEnd(); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); }); it('should work with context.push', async () => { @@ -1777,24 +1564,20 @@ describe('batch', () => { await context.handlerDidEnd(); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); }); it('should have more requests when push over 5 messages', async () => { @@ -1811,42 +1594,34 @@ describe('batch', () => { await context.handlerDidEnd(); expect(client.push).toHaveBeenCalledTimes(2); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '1', - }, - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - { - type: 'text', - text: '4', - }, - { - type: 'text', - text: '5', - }, - ], - { accessToken: undefined } - ); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '6', - }, - ], - { accessToken: undefined } - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '1', + }, + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + { + type: 'text', + text: '4', + }, + { + type: 'text', + text: '5', + }, + ]); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '6', + }, + ]); expect(warning).not.toBeCalled(); }); @@ -1861,40 +1636,28 @@ describe('batch', () => { await context.pushText('4'); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [ - { - type: 'text', - text: '1', - }, - ], - { accessToken: undefined } - ); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '2', - }, - { - type: 'text', - text: '3', - }, - ], - { accessToken: undefined } - ); - expect(client.push).toBeCalledWith( - session.user.id, - [ - { - type: 'text', - text: '4', - }, - ], - { accessToken: undefined } - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { + type: 'text', + text: '1', + }, + ]); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '2', + }, + { + type: 'text', + text: '3', + }, + ]); + expect(client.push).toBeCalledWith(session.user.id, [ + { + type: 'text', + text: '4', + }, + ]); }); }); @@ -1904,11 +1667,9 @@ describe('sendMethod', () => { await context.sendText('hello'); - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.reply).toBeCalledWith(REPLY_TOKEN, [ + { text: 'hello', type: 'text' }, + ]); expect(client.push).not.toBeCalled(); }); @@ -1919,11 +1680,9 @@ describe('sendMethod', () => { await context.sendText('hello'); - expect(client.push).toBeCalledWith( - session.user.id, - [{ text: 'hello', type: 'text' }], - {} - ); + expect(client.push).toBeCalledWith(session.user.id, [ + { text: 'hello', type: 'text' }, + ]); expect(client.reply).not.toBeCalled(); }); }); @@ -1934,14 +1693,6 @@ describe('#useAccessToken', () => { context.useAccessToken('anyToken'); - await context.replyText('hello'); - - expect(client.reply).toBeCalledWith( - REPLY_TOKEN, - [{ text: 'hello', type: 'text' }], - { - accessToken: 'anyToken', - } - ); + expect(client.accessToken).toEqual('anyToken'); }); }); diff --git a/packages/bottender/src/line/__tests__/LineEvent.spec.ts b/packages/bottender/src/line/__tests__/LineEvent.spec.ts index 927edeb96..649cf9930 100644 --- a/packages/bottender/src/line/__tests__/LineEvent.spec.ts +++ b/packages/bottender/src/line/__tests__/LineEvent.spec.ts @@ -11,7 +11,15 @@ const textMessage = { message: { id: '325708', type: 'text', - text: 'Hello, world', + text: 'Hello, world! (love)', + emojis: [ + { + index: 14, + length: 6, + productId: '5ac1bfd5040ab15980c9b435', + emojiId: '001', + }, + ], }, }; @@ -445,6 +453,22 @@ it('#source', () => { expect(new LineEvent(noSourceMessage).source).toEqual(null); }); +it('#timestamp', () => { + expect(new LineEvent(textMessage).timestamp).toEqual(1462629479859); + expect(new LineEvent(follow).timestamp).toEqual(1462629479859); + expect(new LineEvent(unfollow).timestamp).toEqual(1462629479859); + expect(new LineEvent(join).timestamp).toEqual(1462629479859); + expect(new LineEvent(leave).timestamp).toEqual(1462629479859); + expect(new LineEvent(postback).timestamp).toEqual(1462629479859); + expect(new LineEvent(beacon).timestamp).toEqual(1462629479859); + expect(new LineEvent(accountLink).timestamp).toEqual(1513669370317); + expect(new LineEvent(memberJoined).timestamp).toEqual(1462629479859); + expect(new LineEvent(memberLeft).timestamp).toEqual(1462629479960); + expect(new LineEvent(thingsLink).timestamp).toEqual(1462629479859); + expect(new LineEvent(thingsUnlink).timestamp).toEqual(1462629479859); + expect(new LineEvent(thingsScenarioResult).timestamp).toEqual(1547817848122); +}); + it('#isMessage', () => { expect(new LineEvent(textMessage).isMessage).toEqual(true); expect(new LineEvent(follow).isMessage).toEqual(false); @@ -465,7 +489,15 @@ it('#message', () => { expect(new LineEvent(textMessage).message).toEqual({ id: '325708', type: 'text', - text: 'Hello, world', + text: 'Hello, world! (love)', + emojis: [ + { + index: 14, + length: 6, + productId: '5ac1bfd5040ab15980c9b435', + emojiId: '001', + }, + ], }); expect(new LineEvent(imageMessage).message).toEqual({ id: '325708', @@ -512,7 +544,7 @@ it('#isText', () => { }); it('#text', () => { - expect(new LineEvent(textMessage).text).toEqual('Hello, world'); + expect(new LineEvent(textMessage).text).toEqual('Hello, world! (love)'); expect(new LineEvent(imageMessage).text).toEqual(null); expect(new LineEvent(videoMessage).text).toEqual(null); expect(new LineEvent(audioMessage).text).toEqual(null); diff --git a/packages/bottender/src/line/__tests__/routes.spec.ts b/packages/bottender/src/line/__tests__/routes.spec.ts index 672b9d47b..7fd9190d8 100644 --- a/packages/bottender/src/line/__tests__/routes.spec.ts +++ b/packages/bottender/src/line/__tests__/routes.spec.ts @@ -296,7 +296,7 @@ async function expectRouteNotMatchLineEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender/src/line/routes.ts b/packages/bottender/src/line/routes.ts index d4830aec8..43e294df7 100644 --- a/packages/bottender/src/line/routes.ts +++ b/packages/bottender/src/line/routes.ts @@ -1,9 +1,10 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import LineContext from './LineContext'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -33,13 +34,13 @@ type Line = Route & { }; }; -const line: Line = (action: Action) => { +const line: Line = (action: Action) => { return route((context: C) => context.platform === 'line', action); }; line.any = line; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isMessage, action @@ -48,7 +49,7 @@ function message(action: Action) { line.message = message; -function follow(action: Action) { +function follow(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isFollow, action @@ -57,7 +58,7 @@ function follow(action: Action) { line.follow = follow; -function unfollow(action: Action) { +function unfollow(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isUnfollow, action @@ -66,7 +67,7 @@ function unfollow(action: Action) { line.unfollow = unfollow; -function join(action: Action) { +function join(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isJoin, action @@ -75,7 +76,7 @@ function join(action: Action) { line.join = join; -function leave(action: Action) { +function leave(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isLeave, action @@ -84,7 +85,7 @@ function leave(action: Action) { line.leave = leave; -function memberJoined(action: Action) { +function memberJoined(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isMemberJoined, action @@ -93,7 +94,7 @@ function memberJoined(action: Action) { line.memberJoined = memberJoined; -function memberLeft(action: Action) { +function memberLeft(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isMemberLeft, action @@ -102,7 +103,7 @@ function memberLeft(action: Action) { line.memberLeft = memberLeft; -function postback(action: Action) { +function postback(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isPostback, action @@ -111,7 +112,7 @@ function postback(action: Action) { line.postback = postback; -function beacon(action: Action) { +function beacon(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isBeacon, action @@ -120,7 +121,7 @@ function beacon(action: Action) { line.beacon = beacon; -function beaconEnter(action: Action) { +function beaconEnter(action: Action) { return route( (context: C) => context.platform === 'line' && @@ -132,7 +133,7 @@ function beaconEnter(action: Action) { beacon.enter = beaconEnter; -function beaconBanner(action: Action) { +function beaconBanner(action: Action) { return route( (context: C) => context.platform === 'line' && @@ -144,7 +145,7 @@ function beaconBanner(action: Action) { beacon.banner = beaconBanner; -function beaconStay(action: Action) { +function beaconStay(action: Action) { return route( (context: C) => context.platform === 'line' && @@ -156,7 +157,7 @@ function beaconStay(action: Action) { beacon.stay = beaconStay; -function accountLink(action: Action) { +function accountLink(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isAccountLink, action @@ -165,7 +166,7 @@ function accountLink(action: Action) { line.accountLink = accountLink; -function things(action: Action) { +function things(action: Action) { return route( (context: C) => context.platform === 'line' && context.event.isThings, action @@ -174,7 +175,7 @@ function things(action: Action) { line.things = things; -function thingsLink(action: Action) { +function thingsLink(action: Action) { return route( (context: C) => context.platform === 'line' && @@ -186,7 +187,7 @@ function thingsLink(action: Action) { things.link = thingsLink; -function thingsUnlink(action: Action) { +function thingsUnlink(action: Action) { return route( (context: C) => context.platform === 'line' && @@ -198,7 +199,7 @@ function thingsUnlink(action: Action) { things.unlink = thingsUnlink; -function thingsScenarioResult( +function thingsScenarioResult( action: Action ) { return route( diff --git a/packages/bottender/src/messenger/FacebookBaseConnector.ts b/packages/bottender/src/messenger/FacebookBaseConnector.ts new file mode 100644 index 000000000..ab7d26b28 --- /dev/null +++ b/packages/bottender/src/messenger/FacebookBaseConnector.ts @@ -0,0 +1,204 @@ +import crypto from 'crypto'; + +import invariant from 'invariant'; +import shortid from 'shortid'; +import { BatchConfig, FacebookBatchQueue } from 'facebook-batch'; +import { JsonObject } from 'type-fest'; +import { MessengerClient } from 'messaging-api-messenger'; + +import { RequestContext } from '../types'; + +type CommonConnectorOptions = { + appId: string; + appSecret: string; + verifyToken?: string; + batchConfig?: BatchConfig; + mapPageToAccessToken?: (pageId: string) => Promise; +}; + +type ConnectorOptionsWithoutClient = { + ClientClass: typeof MessengerClient; + accessToken?: string; + origin?: string; + skipAppSecretProof?: boolean; +} & CommonConnectorOptions; + +type ConnectorOptionsWithClient = { + client: C; +} & CommonConnectorOptions; + +export type FacebookBaseConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; + +export default class FacebookBaseConnector< + RequestBody extends JsonObject, + Client extends MessengerClient +> { + _client: Client; + + _appId: string; + + _appSecret: string; + + _origin: string | undefined = undefined; + + _skipAppSecretProof: boolean | undefined = undefined; + + _mapPageToAccessToken: ((pageId: string) => Promise) | null = null; + + _verifyToken: string | null = null; + + _batchConfig: BatchConfig | null = null; + + _batchQueue: FacebookBatchQueue | null = null; + + constructor(options: FacebookBaseConnectorOptions) { + const { appId, appSecret, mapPageToAccessToken, verifyToken } = options; + + if ('client' in options) { + this._client = options.client; + + // In the future, batch would be handled by client itself internally. + this._batchConfig = null; + } else { + const { + ClientClass, + accessToken, + origin, + skipAppSecretProof, + batchConfig, + } = options; + + invariant( + accessToken || mapPageToAccessToken, + 'Facebook access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' + ); + invariant( + appSecret, + 'Facebook app secret is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' + ); + + const clientConfig = { + accessToken: accessToken || '', + appSecret, + origin, + skipAppSecretProof, + }; + + this._client = new ClientClass(clientConfig) as Client; + + this._batchConfig = batchConfig || null; + if (this._batchConfig) { + this._batchQueue = new FacebookBatchQueue( + clientConfig, + this._batchConfig + ); + } + + this._origin = origin; + this._skipAppSecretProof = skipAppSecretProof; + } + + this._appId = appId; + this._appSecret = appSecret; + + this._mapPageToAccessToken = mapPageToAccessToken || null; + this._verifyToken = verifyToken || shortid.generate(); + } + + get client(): Client { + return this._client; + } + + get verifyToken(): string | null { + return this._verifyToken; + } + + // https://developers.facebook.com/docs/messenger-platform/webhook#security + verifySignature(rawBody: string, signature: string): boolean { + if (typeof signature !== 'string') return false; + + const sha1 = signature.split('sha1=')[1]; + + if (!sha1) return false; + + const bufferFromSignature = Buffer.from(sha1, 'hex'); + + const hashBufferFromBody = crypto + .createHmac('sha1', this._appSecret) + .update(rawBody, 'utf8') + .digest(); + + // return early here if buffer lengths are not equal since timingSafeEqual + // will throw if buffer lengths are not equal + if (bufferFromSignature.length !== hashBufferFromBody.length) { + return false; + } + + return crypto.timingSafeEqual(bufferFromSignature, hashBufferFromBody); + } + + preprocess({ + method, + headers, + query, + rawBody, + }: RequestContext) { + if (method.toLowerCase() === 'get') { + if ( + query['hub.mode'] === 'subscribe' && + query['hub.verify_token'] === this.verifyToken + ) { + return { + shouldNext: false, + response: { + status: 200, + body: query['hub.challenge'], + }, + }; + } + + return { + shouldNext: false, + response: { + status: 403, + body: 'Forbidden', + }, + }; + } + + if (method.toLowerCase() !== 'post') { + return { + shouldNext: true, + }; + } + + if ( + headers['x-hub-signature'] && + this.verifySignature(rawBody, headers['x-hub-signature']) + ) { + return { + shouldNext: true, + }; + } + + const error = { + message: 'Facebook Signature Validation Failed!', + request: { + rawBody, + headers: { + 'x-hub-signature': headers['x-hub-signature'], + }, + }, + }; + + return { + shouldNext: false, + response: { + status: 400, + body: { error }, + }, + }; + } +} diff --git a/packages/bottender/src/messenger/MessengerBot.ts b/packages/bottender/src/messenger/MessengerBot.ts index a2dbb5516..e8860455a 100644 --- a/packages/bottender/src/messenger/MessengerBot.ts +++ b/packages/bottender/src/messenger/MessengerBot.ts @@ -1,11 +1,14 @@ import { MessengerClient } from 'messaging-api-messenger'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; -import MessengerConnector, { MessengerRequestBody } from './MessengerConnector'; +import MessengerConnector, { + MessengerConnectorOptions, +} from './MessengerConnector'; import MessengerContext from './MessengerContext'; import MessengerEvent from './MessengerEvent'; +import { MessengerRequestBody } from './MessengerTypes'; export default class MessengerBot extends Bot< MessengerRequestBody, @@ -14,41 +17,16 @@ export default class MessengerBot extends Bot< MessengerContext > { constructor({ - accessToken, - appId, - appSecret, sessionStore, sync, - mapPageToAccessToken, - verifyToken, - batchConfig, - origin, - skipAppSecretProof, - skipLegacyProfile, - }: { - accessToken: string; - appId: string; - appSecret: string; + onRequest, + ...connectorOptions + }: MessengerConnectorOptions & { sessionStore?: SessionStore; sync?: boolean; - mapPageToAccessToken?: (pageId: string) => Promise; - verifyToken?: string; - batchConfig?: Record; - origin?: string; - skipAppSecretProof?: boolean; - skipLegacyProfile?: boolean; + onRequest?: OnRequest; }) { - const connector = new MessengerConnector({ - accessToken, - appId, - appSecret, - mapPageToAccessToken, - verifyToken, - batchConfig, - origin, - skipAppSecretProof, - skipLegacyProfile, - }); - super({ connector, sessionStore, sync }); + const connector = new MessengerConnector(connectorOptions); + super({ connector, sessionStore, sync, onRequest }); } } diff --git a/packages/bottender/src/messenger/MessengerConnector.ts b/packages/bottender/src/messenger/MessengerConnector.ts index b56a7cee0..90a63210f 100644 --- a/packages/bottender/src/messenger/MessengerConnector.ts +++ b/packages/bottender/src/messenger/MessengerConnector.ts @@ -1,203 +1,77 @@ -import crypto from 'crypto'; import { EventEmitter } from 'events'; import { URL } from 'url'; -import invariant from 'invariant'; import isAfter from 'date-fns/isAfter'; import isValid from 'date-fns/isValid'; -import shortid from 'shortid'; import warning from 'warning'; -import { MessengerBatchQueue } from 'messenger-batch'; +import { JsonObject } from 'type-fest'; import { MessengerClient } from 'messaging-api-messenger'; import Session from '../session/Session'; import { Connector } from '../bot/Connector'; -import { RequestContext } from '../types'; +import FacebookBaseConnector, { + FacebookBaseConnectorOptions, +} from './FacebookBaseConnector'; import MessengerContext from './MessengerContext'; -import MessengerEvent, { - AppRoles, - Message, +import MessengerEvent from './MessengerEvent'; +import { MessengerRawEvent, - PassThreadControl, - PolicyEnforcement, - Postback, - Recipient, - Sender, - TakeThreadControl, -} from './MessengerEvent'; - -type Entry = { - [key in 'messaging' | 'standby' | 'changes']: { - sender: Sender; - recipient: Recipient; - timestamp: number; - postback?: Postback; - message?: Message; - field?: string; - value?: Record; - }[]; -}; - -type EntryRequestBody = { - type: string; - entry: Entry[]; -}; - -type PolicyEnforcementRequestBody = { - recipient: Recipient; - timestamp: number; - 'policy-enforcement': PolicyEnforcement; -}; - -type AppRolesRequestBody = { - recipient: Recipient; - timestamp: number; - appRoles: AppRoles; -}; - -type PassThreadControlRequestBody = { - sender: Sender; - recipient: Recipient; - timestamp: number; - passThreadControl: PassThreadControl; -}; - -type TakeThreadControlRequestBody = { - sender: Sender; - recipient: Recipient; - timestamp: number; - takeThreadControl: TakeThreadControl; -}; - -export type MessengerRequestBody = - | EntryRequestBody - | PolicyEnforcementRequestBody - | AppRolesRequestBody - | PassThreadControlRequestBody - | TakeThreadControlRequestBody; - -type CommonConstructorOptions = { - appId: string; - appSecret: string; - verifyToken?: string; - batchConfig?: Record; - skipLegacyProfile?: boolean; - mapPageToAccessToken?: (pageId: string) => Promise; -}; - -type ConstructorOptionsWithoutClient = { - accessToken?: string; - origin?: string; - skipAppSecretProof?: boolean; -} & CommonConstructorOptions; - -type ConstructorOptionsWithClient = { - client: MessengerClient; -} & CommonConstructorOptions; - -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; + MessengerRequestBody, + MessengerRequestContext, +} from './MessengerTypes'; -export default class MessengerConnector - implements Connector { - _client: MessengerClient; - - _appId: string; - - _appSecret: string; +export type MessengerConnectorOptions = + FacebookBaseConnectorOptions & { + skipLegacyProfile?: boolean; + mapPageToAccessToken?: (pageId: string) => Promise; + }; +export default class MessengerConnector + extends FacebookBaseConnector + implements Connector +{ _skipLegacyProfile: boolean; _mapPageToAccessToken: ((pageId: string) => Promise) | null = null; - _verifyToken: string | null = null; - - _batchConfig: Record | null = null; - - _batchQueue: Record | null = null; - - constructor(options: ConstructorOptions) { - const { - appId, - appSecret, - mapPageToAccessToken, - verifyToken, - batchConfig, - - skipLegacyProfile, - } = options; - - if ('client' in options) { - this._client = options.client; - } else { - const { accessToken, origin, skipAppSecretProof } = options; - - invariant( - accessToken || mapPageToAccessToken, - 'Messenger access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' - ); - invariant( - appSecret, - 'Messenger app secret is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' - ); - - this._client = MessengerClient.connect({ - accessToken: accessToken || '', - appSecret, - origin, - skipAppSecretProof, - }); - } + constructor(options: MessengerConnectorOptions) { + super({ + ...options, + ClientClass: MessengerClient, + }); - this._appId = appId; - this._appSecret = appSecret; + const { mapPageToAccessToken, skipLegacyProfile } = options; this._mapPageToAccessToken = mapPageToAccessToken || null; - this._verifyToken = verifyToken || shortid.generate(); this._skipLegacyProfile = typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true; - - this._batchConfig = batchConfig || null; - if (this._batchConfig) { - this._batchQueue = new MessengerBatchQueue( - this._client, - this._batchConfig - ); - } } _getRawEventsFromRequest(body: MessengerRequestBody): MessengerRawEvent[] { if ('entry' in body) { - const { entry } = body as EntryRequestBody; - - return entry - .map(ent => { - if (ent.messaging) { - return ent.messaging[0] as MessengerRawEvent; + return body.entry + .map((entry) => { + if ('messaging' in entry) { + return entry.messaging[0] as MessengerRawEvent; } - if (ent.standby) { - return ent.standby[0] as MessengerRawEvent; + if ('standby' in entry) { + return entry.standby[0] as MessengerRawEvent; } // for Webhook Test button request and other page events - if (ent.changes) { - return ent.changes[0] as MessengerRawEvent; - } - return null; }) - .filter(event => event != null) as MessengerRawEvent[]; + .filter((event): event is MessengerRawEvent => event != null); } return [body as MessengerRawEvent]; } _getPageIdFromRawEvent(rawEvent: MessengerRawEvent): string | null { - if (rawEvent.message && rawEvent.message.isEcho && rawEvent.sender) { + if ('message' in rawEvent && rawEvent.message.isEcho && rawEvent.sender) { return rawEvent.sender.id; } if (rawEvent.recipient) { @@ -209,9 +83,10 @@ export default class MessengerConnector _isStandby(body: MessengerRequestBody): boolean { if (!('entry' in body)) return false; - const entry = (body as EntryRequestBody).entry[0]; - return !!entry.standby; + const entry = body.entry[0]; + + return 'standby' in entry; } _profilePicExpired(user: { profilePic: string }): boolean { @@ -230,18 +105,10 @@ export default class MessengerConnector } } - get platform(): string { + get platform(): 'messenger' { return 'messenger'; } - get client(): MessengerClient { - return this._client; - } - - get verifyToken(): string | null { - return this._verifyToken; - } - getUniqueSessionKey( bodyOrEvent: MessengerRequestBody | MessengerEvent ): string | null { @@ -251,13 +118,13 @@ export default class MessengerConnector : this._getRawEventsFromRequest(bodyOrEvent)[0]; if ( rawEvent && - rawEvent.message && + 'message' in rawEvent && rawEvent.message.isEcho && rawEvent.recipient ) { return rawEvent.recipient.id; } - if (rawEvent && rawEvent.sender) { + if (rawEvent && 'sender' in rawEvent) { return rawEvent.sender.id; } return null; @@ -302,9 +169,17 @@ export default class MessengerConnector } else { let user = {}; try { - user = await this._client.getUserProfile(senderId as any, { - accessToken: customAccessToken, - }); + if (customAccessToken) { + const client = new MessengerClient({ + accessToken: customAccessToken, + appSecret: this._appSecret, + origin: this._origin, + skipAppSecretProof: this._skipAppSecretProof, + }); + user = await client.getUserProfile(senderId as any); + } else { + user = await this._client.getUserProfile(senderId as any); + } } catch (err) { warning( false, @@ -343,7 +218,7 @@ export default class MessengerConnector const isStandby = this._isStandby(body); return rawEvents.map( - rawEvent => + (rawEvent) => new MessengerEvent(rawEvent, { isStandby, pageId: this._getPageIdFromRawEvent(rawEvent), @@ -354,8 +229,8 @@ export default class MessengerConnector async createContext(params: { event: MessengerEvent; session?: Session; - initialState?: Record; - requestContext?: RequestContext; + initialState?: JsonObject; + requestContext?: MessengerRequestContext; emitter?: EventEmitter; }): Promise { let customAccessToken; @@ -364,7 +239,7 @@ export default class MessengerConnector let pageId = null; - if (rawEvent.message && rawEvent.message.isEcho && rawEvent.sender) { + if ('message' in rawEvent && rawEvent.message.isEcho && rawEvent.sender) { pageId = rawEvent.sender.id; } else if (rawEvent.recipient) { pageId = rawEvent.recipient.id; @@ -376,102 +251,25 @@ export default class MessengerConnector customAccessToken = await this._mapPageToAccessToken(pageId); } } + + let client; + if (customAccessToken) { + client = new MessengerClient({ + accessToken: customAccessToken, + appSecret: this._appSecret, + origin: this._origin, + skipAppSecretProof: this._skipAppSecretProof, + }); + } else { + client = this._client; + } + return new MessengerContext({ ...params, - client: this._client, + client, customAccessToken, batchQueue: this._batchQueue, appId: this._appId, }); } - - // https://developers.facebook.com/docs/messenger-platform/webhook#security - verifySignature(rawBody: string, signature: string): boolean { - if (typeof signature !== 'string') return false; - - const sha1 = signature.split('sha1=')[1]; - - if (!sha1) return false; - - const bufferFromSignature = Buffer.from(sha1, 'hex'); - - const hashBufferFromBody = crypto - .createHmac('sha1', this._appSecret) - .update(rawBody, 'utf8') - .digest(); - - // return early here if buffer lengths are not equal since timingSafeEqual - // will throw if buffer lengths are not equal - if (bufferFromSignature.length !== hashBufferFromBody.length) { - return false; - } - - return crypto.timingSafeEqual(bufferFromSignature, hashBufferFromBody); - } - - preprocess({ - method, - headers, - query, - rawBody, - }: { - method: string; - headers: Record; - query: Record; - rawBody: string; - body: Record; - }) { - if (method.toLowerCase() === 'get') { - if ( - query['hub.mode'] === 'subscribe' && - query['hub.verify_token'] === this.verifyToken - ) { - return { - shouldNext: false, - response: { - status: 200, - body: query['hub.challenge'], - }, - }; - } - - return { - shouldNext: false, - response: { - status: 403, - body: 'Forbidden', - }, - }; - } - - if (method.toLowerCase() !== 'post') { - return { - shouldNext: true, - }; - } - - if (this.verifySignature(rawBody, headers['x-hub-signature'])) { - return { - shouldNext: true, - }; - } - - const error = { - message: 'Messenger Signature Validation Failed!', - request: { - rawBody, - headers: { - 'x-hub-signature': headers['x-hub-signature'], - }, - }, - }; - - return { - shouldNext: false, - response: { - status: 400, - body: { error }, - }, - }; - } } diff --git a/packages/bottender/src/messenger/MessengerContext.ts b/packages/bottender/src/messenger/MessengerContext.ts index 316c7dc54..c52bd7f3b 100644 --- a/packages/bottender/src/messenger/MessengerContext.ts +++ b/packages/bottender/src/messenger/MessengerContext.ts @@ -1,29 +1,29 @@ +import fs from 'fs'; import { EventEmitter } from 'events'; import invariant from 'invariant'; import sleep from 'delay'; import warning from 'warning'; -import { - MessengerBatch, - MessengerClient, - MessengerTypes, -} from 'messaging-api-messenger'; +import { FacebookBatchQueue } from 'facebook-batch'; +import { JsonObject } from 'type-fest'; +import { MessengerBatch, MessengerClient } from 'messaging-api-messenger'; import Context from '../context/Context'; import Session from '../session/Session'; import { RequestContext } from '../types'; import MessengerEvent from './MessengerEvent'; +import * as MessengerTypes from './MessengerTypes'; -type Options = { +export type MessengerContextOptions = { appId?: string; client: MessengerClient; event: MessengerEvent; session?: Session; - initialState?: Record; + initialState?: JsonObject; requestContext?: RequestContext; customAccessToken?: string; - batchQueue?: Record | null; + batchQueue?: FacebookBatchQueue | null; emitter?: EventEmitter; }; @@ -34,8 +34,7 @@ class MessengerContext extends Context { _personaId: string | null = null; - // FIXME: add typing for BatchQueue - _batchQueue: Record | null; + _batchQueue: FacebookBatchQueue | null; constructor({ appId, @@ -47,7 +46,7 @@ class MessengerContext extends Context { customAccessToken, batchQueue, emitter, - }: Options) { + }: MessengerContextOptions) { super({ client, event, session, initialState, requestContext, emitter }); this._customAccessToken = customAccessToken || null; this._batchQueue = batchQueue || null; @@ -66,16 +65,6 @@ class MessengerContext extends Context { return this._customAccessToken || this._client.accessToken; } - // TODO: Avoid this to improve typing - _callClientMethod(method: string, args: any[]) { - if (this._batchQueue) { - return (this._batchQueue as any).push( - (MessengerBatch as any)[method](...args) - ); - } - return (this._client as any)[method](...args); - } - _getMethodOptions( options: O ): { @@ -126,7 +115,7 @@ class MessengerContext extends Context { * Inject persona for the context. * */ - usePersona(personaId: string) { + usePersona(personaId: string): void { this._personaId = personaId; } @@ -134,7 +123,7 @@ class MessengerContext extends Context { * Inject access token for the context. * */ - useAccessToken(accessToken: string) { + useAccessToken(accessToken: string): void { this._customAccessToken = accessToken; } @@ -157,7 +146,7 @@ class MessengerContext extends Context { async sendText( text: string, options: MessengerTypes.SendOption = {} - ): Promise { + ): Promise { if (!this._session) { warning( false, @@ -174,16 +163,27 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendText', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendText( + this._session.user.id, + text, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendText( this._session.user.id, text, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } - async getUserProfile(options: { - fields?: MessengerTypes.UserProfileField[]; - }): Promise { + async getUserProfile( + options: { + fields?: MessengerTypes.UserProfileField[]; + } = {} + ): Promise { if (!this._session) { warning( false, @@ -192,10 +192,90 @@ class MessengerContext extends Context { return null; } - return this._callClientMethod('getUserProfile', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.getUserProfile( + this._session.user.id, + this._getMethodOptions(options) + ) + ); + } + return this._client.getUserProfile(this._session.user.id, options); + } + + async getUserPersistentMenu(): Promise { + if (!this._session) { + warning( + false, + `getUserPersistentMenu: should not be called in context without session` + ); + return null; + } + + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.getUserPersistentMenu( + this._session.user.id, + this._getMethodOptions({}) + ) + ); + } + + return this._client.getUserPersistentMenu(this._session.user.id); + } + + async setUserPersistentMenu( + attrs: MessengerTypes.PersistentMenu, + options: { + composerInputDisabled?: boolean; + } = {} + ): Promise { + if (!this._session) { + warning( + false, + `setUserPersistentMenu: should not be called in context without session` + ); + return; + } + + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.setUserPersistentMenu( + this._session.user.id, + attrs, + this._getMethodOptions(options) + ) + ); + } + + return this._client.setUserPersistentMenu( this._session.user.id, - this._getMethodOptions(options), - ]); + attrs, + options + ); + } + + async deleteUserPersistentMenu(): Promise< + MessengerTypes.MutationSuccessResponse | undefined + > { + if (!this._session) { + warning( + false, + `deleteUserPersistentMenu: should not be called in context without session` + ); + return; + } + + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.deleteUserPersistentMenu( + this._session.user.id, + this._getMethodOptions({}) + ) + ); + } + + return this._client.deleteUserPersistentMenu(this._session.user.id); } /** @@ -219,11 +299,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendSenderAction', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendSenderAction( + this._session.user.id, + senderAction, + this._getSenderActionMethodOptions(options) + ) + ); + } + return this._client.sendSenderAction( this._session.user.id, senderAction, - this._getSenderActionMethodOptions(options), - ]); + this._getSenderActionMethodOptions(options) + ); } /** @@ -240,10 +329,18 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('typingOn', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.typingOn( + this._session.user.id, + this._getSenderActionMethodOptions(options) + ) + ); + } + return this._client.typingOn( this._session.user.id, - this._getSenderActionMethodOptions(options), - ]); + this._getSenderActionMethodOptions(options) + ); } /** @@ -260,18 +357,26 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('typingOff', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.typingOff( + this._session.user.id, + this._getSenderActionMethodOptions(options) + ) + ); + } + return this._client.typingOff( this._session.user.id, - this._getSenderActionMethodOptions(options), - ]); + this._getSenderActionMethodOptions(options) + ); } /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#markseenuserid */ - async markSeen( - options: MessengerTypes.SendOption = {} - ): Promise { + async markSeen(): Promise< + MessengerTypes.SendSenderActionResponse | undefined + > { if (!this._session) { warning( false, @@ -280,10 +385,16 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('markSeen', [ - this._session.user.id, - this._getSenderActionMethodOptions(options), - ]); + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.markSeen( + this._session.user.id, + // FIXME: this type should be fixed in MessengerBatch + this._getMethodOptions({}) as any + ) + ); + } + return this._client.markSeen(this._session.user.id); } /** @@ -298,7 +409,7 @@ class MessengerContext extends Context { async passThreadControl( targetAppId: number, metadata?: string - ): Promise { + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -307,18 +418,29 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('passThreadControl', [ + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.passThreadControl( + this._session.user.id, + targetAppId, + metadata, + this._getMethodOptions({}) + ) + ); + } + return this._client.passThreadControl( this._session.user.id, targetAppId, - metadata, - this._getMethodOptions({}), - ]); + metadata + ); } /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#passthreadcontroltopageinboxuserid-metadata---official-docs */ - async passThreadControlToPageInbox(metadata?: string): Promise { + async passThreadControlToPageInbox( + metadata?: string + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -327,17 +449,27 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('passThreadControlToPageInbox', [ + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.passThreadControlToPageInbox( + this._session.user.id, + metadata, + this._getMethodOptions({}) + ) + ); + } + return this._client.passThreadControlToPageInbox( this._session.user.id, - metadata, - this._getMethodOptions({}), - ]); + metadata + ); } /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#takethreadcontroluserid-metadata---official-docs */ - async takeThreadControl(metadata?: string): Promise { + async takeThreadControl( + metadata?: string + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -346,17 +478,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('takeThreadControl', [ - this._session.user.id, - metadata, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.takeThreadControl( + this._session.user.id, + metadata, + this._getMethodOptions({}) + ) + ); + } + return this._client.takeThreadControl(this._session.user.id, metadata); } /** * https://github.com/Yoctol/messaging-apis/blob/master/packages/messaging-api-messenger/README.md#requestthreadcontroluserid-metadata---official-docs */ - async requestThreadControl(metadata?: string): Promise { + async requestThreadControl( + metadata?: string + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -365,17 +504,22 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('requestThreadControl', [ - this._session.user.id, - metadata, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.requestThreadControl( + this._session.user.id, + metadata, + this._getMethodOptions({}) + ) + ); + } + return this._client.requestThreadControl(this._session.user.id, metadata); } /** * https://github.com/Yoctol/messaging-apis/blob/master/packages/messaging-api-messenger/README.md#requestthreadcontroluserid-metadata---official-docs */ - async getThreadOwner(): Promise { + async getThreadOwner(): Promise<{ appId: string } | undefined> { if (!this._session) { warning( false, @@ -384,10 +528,15 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('getThreadOwner', [ - this._session.user.id, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ appId: string }>( + MessengerBatch.getThreadOwner( + this._session.user.id, + this._getMethodOptions({}) + ) + ); + } + return this._client.getThreadOwner(this._session.user.id); } async isThreadOwner(): Promise { @@ -395,7 +544,13 @@ class MessengerContext extends Context { this._appId, 'isThreadOwner: must provide appId to use this feature' ); - const { appId } = await this.getThreadOwner(); + const threadOwner = await this.getThreadOwner(); + + if (!threadOwner) { + return false; + } + + const { appId } = threadOwner; return `${appId}` === `${this._appId}`; } @@ -409,7 +564,9 @@ class MessengerContext extends Context { /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#associatelabeluserid-labelid */ - async associateLabel(labelId: number): Promise { + async associateLabel( + labelId: number + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -418,17 +575,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('associateLabel', [ - this._session.user.id, - labelId, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.associateLabel( + this._session.user.id, + labelId, + this._getMethodOptions({}) + ) + ); + } + return this._client.associateLabel(this._session.user.id, labelId); } /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#dissociatelabeluserid-labelid */ - async dissociateLabel(labelId: number): Promise { + async dissociateLabel( + labelId: number + ): Promise<{ success: true } | undefined> { if (!this._session) { warning( false, @@ -437,17 +601,36 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('dissociateLabel', [ - this._session.user.id, - labelId, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ success: true }>( + MessengerBatch.dissociateLabel( + this._session.user.id, + labelId, + this._getMethodOptions({}) + ) + ); + } + return this._client.dissociateLabel(this._session.user.id, labelId); } /** * https://github.com/Yoctol/messaging-apis/tree/master/packages/messaging-api-messenger#getassociatedlabelsuserid */ - async getAssociatedLabels(): Promise { + async getAssociatedLabels(): Promise< + | { + data: { + name: string; + id: string; + }[]; + paging: { + cursors: { + before: string; + after: string; + }; + }; + } + | undefined + > { if (!this._session) { warning( false, @@ -456,10 +639,26 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('getAssociatedLabels', [ - this._session.user.id, - this._getMethodOptions({}), - ]); + if (this._batchQueue) { + return this._batchQueue.push<{ + data: { + name: string; + id: string; + }[]; + paging: { + cursors: { + before: string; + after: string; + }; + }; + }>( + MessengerBatch.getAssociatedLabels( + this._session.user.id, + this._getMethodOptions({}) + ) + ); + } + return this._client.getAssociatedLabels(this._session.user.id); } async sendMessage( @@ -482,11 +681,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendMessage', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendMessage( + this._session.user.id, + message, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendMessage( this._session.user.id, message, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAttachment( @@ -509,11 +717,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAttachment', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendAttachment( + this._session.user.id, + attachment, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAttachment( this._session.user.id, attachment, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendImage( @@ -539,11 +756,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendImage', [ + if ( + this._batchQueue && + !Buffer.isBuffer(image) && + !(image instanceof fs.ReadStream) + ) { + return this._batchQueue.push( + MessengerBatch.sendImage( + this._session.user.id, + image, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendImage( this._session.user.id, image, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAudio( @@ -569,11 +799,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAudio', [ + if ( + this._batchQueue && + !Buffer.isBuffer(audio) && + !(audio instanceof fs.ReadStream) + ) { + return this._batchQueue.push( + MessengerBatch.sendAudio( + this._session.user.id, + audio, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAudio( this._session.user.id, audio, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendVideo( @@ -599,11 +842,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendVideo', [ + if ( + this._batchQueue && + !Buffer.isBuffer(video) && + !(video instanceof fs.ReadStream) + ) { + return this._batchQueue.push( + MessengerBatch.sendVideo( + this._session.user.id, + video, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendVideo( this._session.user.id, video, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendFile( @@ -629,11 +885,24 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendFile', [ + if ( + this._batchQueue && + !Buffer.isBuffer(file) && + !(file instanceof fs.ReadStream) + ) { + return this._batchQueue.push( + MessengerBatch.sendFile( + this._session.user.id, + file, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendFile( this._session.user.id, file, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendTemplate( @@ -656,11 +925,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendTemplate( + this._session.user.id, + payload, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendTemplate( this._session.user.id, payload, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendGenericTemplate( @@ -685,11 +963,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendGenericTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendGenericTemplate( + this._session.user.id, + elements, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendGenericTemplate( this._session.user.id, elements, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendButtonTemplate( @@ -713,12 +1000,22 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendButtonTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendButtonTemplate( + this._session.user.id, + text, + buttons, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendButtonTemplate( this._session.user.id, text, buttons, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendMediaTemplate( @@ -741,11 +1038,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendMediaTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendMediaTemplate( + this._session.user.id, + elements, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendMediaTemplate( this._session.user.id, elements, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendReceiptTemplate( @@ -768,11 +1074,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendReceiptTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendReceiptTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendReceiptTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAirlineBoardingPassTemplate( @@ -795,11 +1110,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAirlineBoardingPassTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendAirlineBoardingPassTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAirlineBoardingPassTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAirlineCheckinTemplate( @@ -822,11 +1146,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAirlineCheckinTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendAirlineCheckinTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAirlineCheckinTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAirlineItineraryTemplate( @@ -849,11 +1182,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAirlineItineraryTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendAirlineItineraryTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAirlineItineraryTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendAirlineUpdateTemplate( @@ -876,11 +1218,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendAirlineUpdateTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendAirlineUpdateTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendAirlineUpdateTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } async sendOneTimeNotifReqTemplate( @@ -903,11 +1254,20 @@ class MessengerContext extends Context { return; } - return this._callClientMethod('sendOneTimeNotifReqTemplate', [ + if (this._batchQueue) { + return this._batchQueue.push( + MessengerBatch.sendOneTimeNotifReqTemplate( + this._session.user.id, + attrs, + this._getSendMethodOptions(options) + ) + ); + } + return this._client.sendOneTimeNotifReqTemplate( this._session.user.id, attrs, - this._getSendMethodOptions(options), - ]); + this._getSendMethodOptions(options) + ); } } diff --git a/packages/bottender/src/messenger/MessengerEvent.ts b/packages/bottender/src/messenger/MessengerEvent.ts index fb48605bf..acb777e7e 100644 --- a/packages/bottender/src/messenger/MessengerEvent.ts +++ b/packages/bottender/src/messenger/MessengerEvent.ts @@ -2,228 +2,33 @@ import { camelcaseKeysDeep } from 'messaging-api-common'; import { Event } from '../context/Event'; -export type Sender = { - id: string; -}; - -export type Recipient = { - id: string; -}; - -type QuickReply = { - payload: string; -}; - -type MediaAttachmentPayload = { - url: string; -}; - -type LocationAttachmentPayload = { - coordinates: { - lat: number; - long: number; - }; -}; - -type AttachmentPayload = MediaAttachmentPayload | LocationAttachmentPayload; - -type FallbackAttachment = { - type: 'fallback'; - payload: null; - title: string; - URL: string; -}; - -type MediaAttachment = { - type: string; - payload: AttachmentPayload; -}; - -type Attachment = MediaAttachment | FallbackAttachment; - -type Tag = { - source: string; -}; - -type ReplyTo = { - mid: string; -}; - -export type Message = { - mid: string; - isEcho?: boolean; - text?: string; - stickerId?: number; - quickReply?: QuickReply; - attachments?: Attachment[]; - tags?: Tag[]; - replyTo?: ReplyTo; - appId?: number; - metadata?: string; -}; - -export type Delivery = { - mids: string[]; - watermark: number; - seq?: number; -}; - -export type Read = { - watermark: number; - seq?: number; -}; - -export type Referral = { - ref: string; - source: string; - type: string; - originDomain?: string; -}; - -export type Postback = { - title: string; - payload?: string; - referral?: Referral; -}; - -export type GamePlay = { - gameId: string; - playerId: string; - contextType: 'SOLO' | 'THREAD' | 'GROUP'; - contextId: string; - score: number; - payload: string; -}; - -export type Optin = - | { - ref: string; - userRef?: string; - } - | { - type: 'one_time_notif_req'; - payload: string; - oneTimeNotifToken: string; - }; - -export type Payment = { - payload: string; - requestedUserInfo: Record; - paymentCredential: Record; - amount: { - currency: string; - amount: string; - }; - shippingOptionId: string; -}; - -export type CheckoutUpdate = { - payload: string; - shippingAddress: { - id: number; - street1: string; - street2: string; - city: string; - state: string; - country: string; - postalCode: string; - }; -}; - -export type PreCheckout = { - payload: string; - requestedUserInfo: { - shippingAddress: { - name: string; - street1: string; - street2: string; - city: string; - state: string; - country: string; - postalCode: string; - }; - contactName: string; - }; - amount: { - currency: string; - amount: string; - }; -}; - -export type PolicyEnforcement = { - action: string; - reason: string; -}; - -export type AppRoles = Record; - -export type PassThreadControl = { - newOwnerAppId: string; - metadata: string; -}; - -export type TakeThreadControl = { - previousOwnerAppId: string; - metadata: string; -}; - -export type RequestThreadControl = { - requestedOwnerAppId: number; - metadata: string; -}; - -export type BrandedCamera = { - contentIds: string[]; - event: string; -}; - -export type AccountLinking = - | { status: 'linked'; authorizationCode: string } - | { status: 'unlinked' }; - -export type Reaction = { - reaction: - | 'smile' - | 'angry' - | 'sad' - | 'wow' - | 'love' - | 'like' - | 'dislike' - | 'other'; - emoji: string; - action: 'react' | 'unreact'; - mid: string; -}; - -export type MessengerRawEvent = { - sender?: Sender; - recipient?: Recipient; - timestamp?: number; - message?: Message; - read?: Read; - delivery?: Delivery; - postback?: Postback; - gamePlay?: GamePlay; - optin?: Optin; - payment?: Payment; - checkoutUpdate?: CheckoutUpdate; - preCheckout?: PreCheckout; - 'policy-enforcement'?: PolicyEnforcement; - appRoles?: AppRoles; - passThreadControl?: PassThreadControl; - takeThreadControl?: TakeThreadControl; - requestThreadControl?: RequestThreadControl; - referral?: Referral; - brandedCamera?: BrandedCamera; - accountLinking?: AccountLinking; - reaction?: Reaction; -}; - -type MessengerEventOptions = { - isStandby?: boolean; - pageId?: string | null; -}; +import { + EventAccountLinking, + EventAppRoles, + EventBrandedCamera, + EventCheckoutUpdate, + EventDelivery, + EventGamePlay, + EventMessage, + EventMessageAttachment, + EventMessageQuickReply, + EventOptin, + EventPassThreadControl, + EventPayment, + EventPolicyEnforcement, + EventPostback, + EventPreCheckout, + EventReaction, + EventRead, + EventReferral, + EventRequestThreadControl, + EventTakeThreadControl, + FallbackAttachment, + LocationAttachmentPayload, + MediaAttachmentPayload, + MessengerEventOptions, + MessengerRawEvent, +} from './MessengerTypes'; export default class MessengerEvent implements Event { _rawEvent: MessengerRawEvent; @@ -249,13 +54,21 @@ export default class MessengerEvent implements Event { return this._rawEvent; } + /** + * The timestamp when the event was sent. + * + */ + get timestamp(): number { + return this._rawEvent.timestamp; + } + /** * Determine if the event is a message event. * */ get isMessage(): boolean { return ( - !!this._rawEvent.message && typeof this._rawEvent.message === 'object' + 'message' in this._rawEvent && typeof this._rawEvent.message === 'object' ); } @@ -263,8 +76,11 @@ export default class MessengerEvent implements Event { * The message object from Messenger raw event. * */ - get message(): Message | null { - return this._rawEvent.message || null; + get message(): EventMessage | null { + if ('message' in this._rawEvent) { + return this._rawEvent.message; + } + return null; } /** @@ -272,7 +88,7 @@ export default class MessengerEvent implements Event { * */ get isText(): boolean { - return this.isMessage && typeof (this.message as any).text === 'string'; + return this.isMessage && typeof this.message!.text === 'string'; } /** @@ -281,7 +97,7 @@ export default class MessengerEvent implements Event { */ get text(): string | null { if (this.isText) { - return (this.message as any).text; + return this.message!.text || null; } return null; } @@ -293,8 +109,8 @@ export default class MessengerEvent implements Event { get hasAttachment(): boolean { return ( this.isMessage && - !!(this.message as any).attachments && - (this.message as any).attachments.length > 0 + !!this.message!.attachments && + this.message!.attachments.length > 0 ); } @@ -302,7 +118,7 @@ export default class MessengerEvent implements Event { * The attachments array from Messenger raw event. * */ - get attachments(): Attachment[] | null { + get attachments(): EventMessageAttachment[] | null { if (this.message && this.message.attachments) { return this.message.attachments; } @@ -314,7 +130,7 @@ export default class MessengerEvent implements Event { * */ get isImage(): boolean { - return this.hasAttachment && (this.attachments as any)[0].type === 'image'; + return this.hasAttachment && this.attachments![0].type === 'image'; } /** @@ -322,7 +138,15 @@ export default class MessengerEvent implements Event { * */ get image(): MediaAttachmentPayload | null { - return this.isImage ? (this.attachments as any)[0].payload : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'image') { + return attachment.payload; + } + return null; } /** @@ -330,7 +154,7 @@ export default class MessengerEvent implements Event { * */ get isAudio(): boolean { - return this.hasAttachment && (this.attachments as any)[0].type === 'audio'; + return this.hasAttachment && this.attachments![0].type === 'audio'; } /** @@ -338,7 +162,15 @@ export default class MessengerEvent implements Event { * */ get audio(): MediaAttachmentPayload | null { - return this.isAudio ? (this.attachments as any)[0].payload : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'audio') { + return attachment.payload; + } + return null; } /** @@ -346,7 +178,7 @@ export default class MessengerEvent implements Event { * */ get isVideo(): boolean { - return this.hasAttachment && (this.attachments as any)[0].type === 'video'; + return this.hasAttachment && this.attachments![0].type === 'video'; } /** @@ -354,7 +186,15 @@ export default class MessengerEvent implements Event { * */ get video(): MediaAttachmentPayload | null { - return this.isVideo ? (this.attachments as any)[0].payload : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'video') { + return attachment.payload; + } + return null; } /** @@ -362,9 +202,7 @@ export default class MessengerEvent implements Event { * */ get isLocation(): boolean { - return ( - this.hasAttachment && (this.attachments as any)[0].type === 'location' - ); + return this.hasAttachment && this.attachments![0].type === 'location'; } /** @@ -372,7 +210,15 @@ export default class MessengerEvent implements Event { * */ get location(): LocationAttachmentPayload | null { - return this.isLocation ? (this.attachments as any)[0].payload : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'location') { + return attachment.payload; + } + return null; } /** @@ -380,7 +226,7 @@ export default class MessengerEvent implements Event { * */ get isFile(): boolean { - return this.hasAttachment && (this.attachments as any)[0].type === 'file'; + return this.hasAttachment && this.attachments![0].type === 'file'; } /** @@ -388,7 +234,15 @@ export default class MessengerEvent implements Event { * */ get file(): MediaAttachmentPayload | null { - return this.isFile ? (this.attachments as any)[0].payload : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'file') { + return attachment.payload; + } + return null; } /** @@ -396,9 +250,7 @@ export default class MessengerEvent implements Event { * */ get isFallback(): boolean { - return ( - this.hasAttachment && (this.attachments as any)[0].type === 'fallback' - ); + return this.hasAttachment && this.attachments![0].type === 'fallback'; } /** @@ -406,7 +258,15 @@ export default class MessengerEvent implements Event { * */ get fallback(): FallbackAttachment | null { - return this.isFallback ? (this.attachments as any)[0] : null; + if (!this.hasAttachment) { + return null; + } + const attachment = this.attachments![0]; + + if (attachment.type === 'fallback') { + return attachment; + } + return null; } /** @@ -414,17 +274,18 @@ export default class MessengerEvent implements Event { * */ get isSticker(): boolean { - return ( - this.isMessage && typeof (this.message as any).stickerId === 'number' - ); + return this.isMessage && typeof this.message!.stickerId === 'number'; } /** * The stickerId from Messenger raw event. * */ - get sticker(): string | null { - return this.isSticker ? (this.message as any).stickerId : null; + get sticker(): number | null { + if (this.isSticker) { + return this.message!.stickerId || null; + } + return null; } /** @@ -436,9 +297,9 @@ export default class MessengerEvent implements Event { get isLikeSticker(): boolean { return ( this.isSticker && - ((this.message as any).stickerId === 369239263222822 || - (this.message as any).stickerId === 369239343222814 || - (this.message as any).stickerId === 369239383222810) + (this.message!.stickerId === 369239263222822 || + this.message!.stickerId === 369239343222814 || + this.message!.stickerId === 369239383222810) ); } @@ -449,8 +310,8 @@ export default class MessengerEvent implements Event { get isQuickReply(): boolean { return ( this.isMessage && - !!(this.message as any).quickReply && - typeof (this.message as any).quickReply === 'object' + !!this.message!.quickReply && + typeof this.message!.quickReply === 'object' ); } @@ -458,7 +319,7 @@ export default class MessengerEvent implements Event { * The quick reply object from Messenger raw event. * */ - get quickReply(): QuickReply | null { + get quickReply(): EventMessageQuickReply | null { if (this.message && this.message.quickReply) { return this.message.quickReply; } @@ -470,7 +331,7 @@ export default class MessengerEvent implements Event { * */ get isEcho(): boolean { - return this.isMessage && !!(this.message as any).isEcho; + return this.isMessage && !!this.message!.isEcho; } /** @@ -479,7 +340,8 @@ export default class MessengerEvent implements Event { */ get isPostback(): boolean { return ( - !!this._rawEvent.postback && typeof this._rawEvent.postback === 'object' + 'postback' in this._rawEvent && + typeof this._rawEvent.postback === 'object' ); } @@ -487,8 +349,11 @@ export default class MessengerEvent implements Event { * The postback object from Messenger raw event. * */ - get postback(): Postback | null { - return this._rawEvent.postback || null; + get postback(): EventPostback | null { + if ('postback' in this._rawEvent) { + return this._rawEvent.postback; + } + return null; } /** @@ -497,7 +362,8 @@ export default class MessengerEvent implements Event { */ get isGamePlay(): boolean { return ( - !!this._rawEvent.gamePlay && typeof this._rawEvent.gamePlay === 'object' + 'gamePlay' in this._rawEvent && + typeof this._rawEvent.gamePlay === 'object' ); } @@ -505,12 +371,12 @@ export default class MessengerEvent implements Event { * The gamePlay object from Messenger raw event. * */ - get gamePlay(): GamePlay | null { - if (!this.isGamePlay) { + get gamePlay(): EventGamePlay | null { + if (!('gamePlay' in this._rawEvent)) { return null; } - const rawGamePlay = (this._rawEvent as any).gamePlay; + const rawGamePlay = this._rawEvent.gamePlay; let payload; try { @@ -519,7 +385,7 @@ export default class MessengerEvent implements Event { parsed && typeof parsed === 'object' ? camelcaseKeysDeep(parsed) : parsed; - } catch (e) { + } catch (err) { payload = rawGamePlay.payload; } @@ -534,15 +400,20 @@ export default class MessengerEvent implements Event { * */ get isOptin(): boolean { - return !!this._rawEvent.optin && typeof this._rawEvent.optin === 'object'; + return ( + 'optin' in this._rawEvent && typeof this._rawEvent.optin === 'object' + ); } /** * The optin object from Messenger raw event. * */ - get optin(): Optin | null { - return this._rawEvent.optin || null; + get optin(): EventOptin | null { + if ('optin' in this._rawEvent) { + return this._rawEvent.optin; + } + return null; } /** @@ -551,7 +422,7 @@ export default class MessengerEvent implements Event { */ get isPayment(): boolean { return ( - !!this._rawEvent.payment && typeof this._rawEvent.payment === 'object' + 'payment' in this._rawEvent && typeof this._rawEvent.payment === 'object' ); } @@ -559,8 +430,11 @@ export default class MessengerEvent implements Event { * The payment object from Messenger raw event. * */ - get payment(): Payment | null { - return this._rawEvent.payment || null; + get payment(): EventPayment | null { + if ('payment' in this._rawEvent) { + return this._rawEvent.payment; + } + return null; } /** @@ -569,7 +443,7 @@ export default class MessengerEvent implements Event { */ get isCheckoutUpdate(): boolean { return ( - !!this._rawEvent.checkoutUpdate && + 'checkoutUpdate' in this._rawEvent && typeof this._rawEvent.checkoutUpdate === 'object' ); } @@ -578,8 +452,11 @@ export default class MessengerEvent implements Event { * The checkoutUpdate object from Messenger raw event. * */ - get checkoutUpdate(): CheckoutUpdate | null { - return this._rawEvent.checkoutUpdate || null; + get checkoutUpdate(): EventCheckoutUpdate | null { + if ('checkoutUpdate' in this._rawEvent) { + return this._rawEvent.checkoutUpdate; + } + return null; } /** @@ -588,7 +465,7 @@ export default class MessengerEvent implements Event { */ get isPreCheckout(): boolean { return ( - !!this._rawEvent.preCheckout && + 'preCheckout' in this._rawEvent && typeof this._rawEvent.preCheckout === 'object' ); } @@ -597,8 +474,11 @@ export default class MessengerEvent implements Event { * The preCheckout object from Messenger raw event. * */ - get preCheckout(): PreCheckout | null { - return this._rawEvent.preCheckout || null; + get preCheckout(): EventPreCheckout | null { + if ('preCheckout' in this._rawEvent) { + return this._rawEvent.preCheckout; + } + return null; } /** @@ -606,15 +486,18 @@ export default class MessengerEvent implements Event { * */ get isRead(): boolean { - return !!this._rawEvent.read && typeof this._rawEvent.read === 'object'; + return 'read' in this._rawEvent && typeof this._rawEvent.read === 'object'; } /** * The read object from Messenger raw event. * */ - get read(): Read | null { - return this.isRead ? (this._rawEvent as any).read : null; + get read(): EventRead | null { + if ('read' in this._rawEvent) { + return this._rawEvent.read; + } + return null; } /** @@ -623,7 +506,8 @@ export default class MessengerEvent implements Event { */ get isDelivery(): boolean { return ( - !!this._rawEvent.delivery && typeof this._rawEvent.delivery === 'object' + 'delivery' in this._rawEvent && + typeof this._rawEvent.delivery === 'object' ); } @@ -631,8 +515,11 @@ export default class MessengerEvent implements Event { * The delivery object from Messenger raw event. * */ - get delivery(): Delivery | null { - return this.isDelivery ? (this._rawEvent as any).delivery : null; + get delivery(): EventDelivery | null { + if ('delivery' in this._rawEvent) { + return this._rawEvent.delivery; + } + return null; } /** @@ -666,7 +553,7 @@ export default class MessengerEvent implements Event { */ get isPolicyEnforcement(): boolean { return ( - !!this._rawEvent['policy-enforcement'] && + 'policy-enforcement' in this._rawEvent && typeof this._rawEvent['policy-enforcement'] === 'object' ); } @@ -675,8 +562,11 @@ export default class MessengerEvent implements Event { * The policy enforcement object from Messenger raw event. * */ - get policyEnforcement(): PolicyEnforcement | null { - return this._rawEvent['policy-enforcement'] || null; + get policyEnforcement(): EventPolicyEnforcement | null { + if ('policy-enforcement' in this._rawEvent) { + return this._rawEvent['policy-enforcement']; + } + return null; } /** @@ -685,7 +575,8 @@ export default class MessengerEvent implements Event { */ get isAppRoles(): boolean { return ( - !!this._rawEvent.appRoles && typeof this._rawEvent.appRoles === 'object' + 'appRoles' in this._rawEvent && + typeof this._rawEvent.appRoles === 'object' ); } @@ -693,8 +584,11 @@ export default class MessengerEvent implements Event { * The app roles object from Messenger raw event. * */ - get appRoles(): AppRoles | null { - return this._rawEvent.appRoles || null; + get appRoles(): EventAppRoles | null { + if ('appRoles' in this._rawEvent) { + return this._rawEvent.appRoles; + } + return null; } /** @@ -711,7 +605,7 @@ export default class MessengerEvent implements Event { */ get isPassThreadControl(): boolean { return ( - !!this._rawEvent.passThreadControl && + 'passThreadControl' in this._rawEvent && typeof this._rawEvent.passThreadControl === 'object' ); } @@ -720,8 +614,11 @@ export default class MessengerEvent implements Event { * The pass thread control object from Messenger raw event. * */ - get passThreadControl(): PassThreadControl | null { - return this._rawEvent.passThreadControl || null; + get passThreadControl(): EventPassThreadControl | null { + if ('passThreadControl' in this._rawEvent) { + return this._rawEvent.passThreadControl; + } + return null; } /** @@ -730,7 +627,7 @@ export default class MessengerEvent implements Event { */ get isTakeThreadControl(): boolean { return ( - !!this._rawEvent.takeThreadControl && + 'takeThreadControl' in this._rawEvent && typeof this._rawEvent.takeThreadControl === 'object' ); } @@ -739,8 +636,11 @@ export default class MessengerEvent implements Event { * The take thread control object from Messenger raw event. * */ - get takeThreadControl(): TakeThreadControl | null { - return this._rawEvent.takeThreadControl || null; + get takeThreadControl(): EventTakeThreadControl | null { + if ('takeThreadControl' in this._rawEvent) { + return this._rawEvent.takeThreadControl; + } + return null; } /** @@ -749,7 +649,7 @@ export default class MessengerEvent implements Event { */ get isRequestThreadControl(): boolean { return ( - !!this._rawEvent.requestThreadControl && + 'requestThreadControl' in this._rawEvent && typeof this._rawEvent.requestThreadControl === 'object' ); } @@ -760,7 +660,7 @@ export default class MessengerEvent implements Event { */ get isRequestThreadControlFromPageInbox(): boolean { return ( - !!this._rawEvent.requestThreadControl && + 'requestThreadControl' in this._rawEvent && typeof this._rawEvent.requestThreadControl === 'object' && this._rawEvent.requestThreadControl.requestedOwnerAppId === 263902037430900 @@ -771,8 +671,11 @@ export default class MessengerEvent implements Event { * The take thread control object from Messenger raw event. * */ - get requestThreadControl(): RequestThreadControl | null { - return this._rawEvent.requestThreadControl || null; + get requestThreadControl(): EventRequestThreadControl | null { + if ('requestThreadControl' in this._rawEvent) { + return this._rawEvent.requestThreadControl; + } + return null; } /** @@ -782,11 +685,9 @@ export default class MessengerEvent implements Event { get isFromCustomerChatPlugin(): boolean { const isMessageFromCustomerChatPlugin = !!( this.isMessage && - !!(this.message as any).tags && - (this.message as any).tags.length !== 0 && - (this.message as any).tags.some( - (tag: any) => tag.source === 'customer_chat_plugin' - ) + 'tags' in this.message! && + this.message!.tags!.length !== 0 && + this.message!.tags!.some((tag) => tag.source === 'customer_chat_plugin') ); const isReferralFromCustomerChatPlugin = !!( @@ -804,8 +705,8 @@ export default class MessengerEvent implements Event { */ get isReferral(): boolean { return !!( - this._rawEvent.referral || - (this._rawEvent.postback && this._rawEvent.postback.referral) + 'referral' in this._rawEvent || + ('postback' in this._rawEvent && this._rawEvent.postback.referral) ); } @@ -813,15 +714,17 @@ export default class MessengerEvent implements Event { * The referral object from Messenger event. * */ - get referral(): Referral | null { + get referral(): EventReferral | null { if (!this.isReferral) { return null; } - return ( - this._rawEvent.referral || - (this._rawEvent.postback && this._rawEvent.postback.referral) || - null - ); + if ('referral' in this._rawEvent) { + return this._rawEvent.referral; + } + if ('postback' in this._rawEvent && 'referral' in this._rawEvent.postback) { + return this._rawEvent.postback.referral || null; + } + return null; } /** @@ -849,7 +752,7 @@ export default class MessengerEvent implements Event { */ get isBrandedCamera(): boolean { return ( - !!this._rawEvent.brandedCamera && + 'brandedCamera' in this._rawEvent && typeof this._rawEvent.brandedCamera === 'object' ); } @@ -858,11 +761,11 @@ export default class MessengerEvent implements Event { * The brandedCamera object from Messenger event. * */ - get brandedCamera(): BrandedCamera | null { - if (!this.isBrandedCamera) { - return null; + get brandedCamera(): EventBrandedCamera | null { + if ('brandedCamera' in this._rawEvent) { + return this._rawEvent.brandedCamera; } - return (this._rawEvent as any).brandedCamera; + return null; } /** @@ -871,7 +774,7 @@ export default class MessengerEvent implements Event { */ get isAccountLinking(): boolean { return ( - !!this._rawEvent.accountLinking && + 'accountLinking' in this._rawEvent && typeof this._rawEvent.accountLinking === 'object' ); } @@ -880,11 +783,11 @@ export default class MessengerEvent implements Event { * The accountLinking object from Messenger event. * */ - get accountLinking(): AccountLinking | null { - if (!this.isAccountLinking) { - return null; + get accountLinking(): EventAccountLinking | null { + if ('accountLinking' in this._rawEvent) { + return this._rawEvent.accountLinking; } - return (this._rawEvent as any).accountLinking; + return null; } /** @@ -893,7 +796,8 @@ export default class MessengerEvent implements Event { */ get isReaction(): boolean { return ( - !!this._rawEvent.reaction && typeof this._rawEvent.reaction === 'object' + 'reaction' in this._rawEvent && + typeof this._rawEvent.reaction === 'object' ); } @@ -901,10 +805,10 @@ export default class MessengerEvent implements Event { * The reaction object from Messenger event. * */ - get reaction(): Reaction | null { - if (!this.isReaction) { - return null; + get reaction(): EventReaction | null { + if ('reaction' in this._rawEvent) { + return this._rawEvent.reaction; } - return (this._rawEvent as any).reaction; + return null; } } diff --git a/packages/bottender/src/messenger/MessengerTypes.ts b/packages/bottender/src/messenger/MessengerTypes.ts new file mode 100644 index 000000000..f860315b6 --- /dev/null +++ b/packages/bottender/src/messenger/MessengerTypes.ts @@ -0,0 +1,362 @@ +import { RequestContext } from '../types'; + +export * from 'messaging-api-messenger/dist/MessengerTypes'; + +export { MessengerConnectorOptions } from './MessengerConnector'; +export { FacebookBaseConnectorOptions } from './FacebookBaseConnector'; +export { MessengerContextOptions } from './MessengerContext'; + +export type EventSender = { + id: string; +}; + +export type EventRecipient = { + id: string; +}; + +export type EventMessageQuickReply = { + payload: string; +}; + +export type MediaAttachmentPayload = { + url: string; +}; + +export type LocationAttachmentPayload = { + coordinates: { + lat: number; + long: number; + }; +}; + +export type FallbackAttachment = { + type: 'fallback'; + payload: null; + title: string; + URL: string; +}; + +export type EventMessageAttachment = + | { + type: 'audio' | 'video' | 'image' | 'file'; + payload: MediaAttachmentPayload; + } + | { + type: 'location'; + payload: LocationAttachmentPayload; + } + | FallbackAttachment; + +type EventMessageTag = { + source: string; +}; + +type EventMessageReplyTo = { + mid: string; +}; + +export type EventMessage = { + mid: string; + isEcho?: boolean; + text?: string; + stickerId?: number; + quickReply?: EventMessageQuickReply; + attachments?: EventMessageAttachment[]; + tags?: EventMessageTag[]; + replyTo?: EventMessageReplyTo; + appId?: number; + metadata?: string; +}; + +export type EventDelivery = { + mids: string[]; + watermark: number; +}; + +export type EventRead = { + watermark: number; +}; + +export type EventReferral = { + ref: string; + source: string; + type: string; + originDomain?: string; +}; + +export type EventPostback = { + title: string; + payload?: string; + referral?: EventReferral; +}; + +export type EventGamePlay = { + gameId: string; + playerId: string; + contextType: 'SOLO' | 'THREAD' | 'GROUP'; + contextId: string; + score: number; + payload: string; +}; + +export type EventOptin = + | { + ref: string; + userRef?: string; + } + | { + type: 'one_time_notif_req'; + payload: string; + oneTimeNotifToken: string; + }; + +export type EventPayment = { + payload: string; + requestedUserInfo: Record; + paymentCredential: Record; + amount: { + currency: string; + amount: string; + }; + shippingOptionId: string; +}; + +export type EventCheckoutUpdate = { + payload: string; + shippingAddress: { + id: number; + street1: string; + street2: string; + city: string; + state: string; + country: string; + postalCode: string; + }; +}; + +export type EventPreCheckout = { + payload: string; + requestedUserInfo: { + shippingAddress: { + name: string; + street1: string; + street2: string; + city: string; + state: string; + country: string; + postalCode: string; + }; + contactName: string; + }; + amount: { + currency: string; + amount: string; + }; +}; + +export type EventPolicyEnforcement = { + action: string; + reason: string; +}; + +export type EventAppRoles = Record; + +export type EventPassThreadControl = { + newOwnerAppId: string; + metadata: string; +}; + +export type EventTakeThreadControl = { + previousOwnerAppId: string; + metadata: string; +}; + +export type EventRequestThreadControl = { + requestedOwnerAppId: number; + metadata: string; +}; + +export type EventBrandedCamera = { + contentIds: string[]; + event: string; +}; + +export type EventAccountLinking = + | { status: 'linked'; authorizationCode: string } + | { status: 'unlinked' }; + +export type EventReaction = { + reaction: + | 'smile' + | 'angry' + | 'sad' + | 'wow' + | 'love' + | 'like' + | 'dislike' + | 'other'; + emoji: string; + action: 'react' | 'unreact'; + mid: string; +}; + +export type MessengerRawEvent = + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + message: EventMessage; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + delivery: EventDelivery; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + read: EventRead; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + postback: EventPostback; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + accountLinking: EventAccountLinking; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + gamePlay: EventGamePlay; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + optin: EventOptin; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + referral: EventReferral; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + payment: EventPayment; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + checkoutUpdate: EventCheckoutUpdate; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + preCheckout: EventPreCheckout; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + passThreadControl: EventPassThreadControl; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + takeThreadControl: EventTakeThreadControl; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + requestThreadControl: EventRequestThreadControl; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + brandedCamera: EventBrandedCamera; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + reaction: EventReaction; + } + | { + recipient: EventRecipient; + timestamp: number; + appRoles: EventAppRoles; + } + | { + recipient: EventRecipient; + timestamp: number; + 'policy-enforcement': EventPolicyEnforcement; + }; + +export type MessengerEventOptions = { + isStandby?: boolean; + pageId?: string | null; +}; + +export type MessengerRequestContext = RequestContext< + MessengerRequestBody, + { 'x-hub-signature'?: string } +>; + +export type MessagingEntry = + | { + id: string; + time: number; + messaging: MessengerRawEvent[]; + } + | { + id: string; + time: number; + // Supported Events: message_reads, message_deliveries, messages, messaging_postbacks + standby: ( + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + message: EventMessage; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + delivery: EventDelivery; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + read: EventRead; + } + | { + sender: EventSender; + recipient: EventRecipient; + timestamp: number; + postback: EventPostback; + } + )[]; + }; + +export type MessengerRequestBody = { + object: 'page'; + entry: MessagingEntry[]; +}; diff --git a/packages/bottender/src/messenger/__tests__/MessengerConnector.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerConnector.spec.ts index e916bad14..220d70326 100644 --- a/packages/bottender/src/messenger/__tests__/MessengerConnector.spec.ts +++ b/packages/bottender/src/messenger/__tests__/MessengerConnector.spec.ts @@ -1,149 +1,140 @@ import warning from 'warning'; import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; import MessengerConnector from '../MessengerConnector'; import MessengerContext from '../MessengerContext'; import MessengerEvent from '../MessengerEvent'; +import { MessengerRequestBody } from '../MessengerTypes'; jest.mock('messaging-api-messenger'); jest.mock('warning'); const ACCESS_TOKEN = 'FAKE_TOKEN'; const APP_SECRET = 'FAKE_SECRET'; - -const request = { - body: { - object: 'page', - entry: [ - { - id: '1895382890692545', - time: 1486464322257, - messaging: [ - { - sender: { - id: '1412611362105802', - }, - recipient: { - id: '1895382890692545', - }, - timestamp: 1486464322190, - message: { - mid: 'mid.1486464322190:cb04e5a654', - seq: 339979, - text: 'text', - }, +const APP_ID = 'FAKE_ID'; + +const request: MessengerRequestBody = { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', }, - ], - }, - ], - }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + text: 'text', + }, + }, + ], + }, + ], }; -const echoRequest = { - body: { - object: 'page', - entry: [ - { - id: '1134713619900975', // page id - time: 1492414608999.0, - messaging: [ - { - sender: { - id: '1134713619900975', - }, - recipient: { - id: '1244813222196986', // user id - }, - timestamp: 1492414608982.0, - message: { - isEcho: true, - appId: 1821152484774199, - mid: 'mid.$cAARS90328R5hrBz-Vlbete17ftIb', - seq: 661428, - text: 'text', - }, +const echoRequest: MessengerRequestBody = { + object: 'page', + entry: [ + { + id: '1134713619900975', // page id + time: 1492414608999.0, + messaging: [ + { + sender: { + id: '1134713619900975', }, - ], - }, - ], - }, + recipient: { + id: '1244813222196986', // user id + }, + timestamp: 1492414608982.0, + message: { + isEcho: true, + appId: 1821152484774199, + mid: 'mid.$cAARS90328R5hrBz-Vlbete17ftIb', + text: 'text', + }, + }, + ], + }, + ], }; -const batchRequest = { - body: { - object: 'page', - entry: [ - { - id: '1895382890692545', - time: 1486464322257, - messaging: [ - { - sender: { - id: '1412611362105802', - }, - recipient: { - id: '1895382890692545', - }, - timestamp: 1486464322190, - message: { - mid: 'mid.1486464322190:cb04e5a654', - seq: 339979, - text: 'test 1', - }, +const batchRequest: MessengerRequestBody = { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', }, - ], - }, - { - id: '189538289069256', - time: 1486464322257, - messaging: [ - { - sender: { - id: '1412611362105802', - }, - recipient: { - id: '1895382890692545', - }, - timestamp: 1486464322190, - message: { - mid: 'mid.1486464322190:cb04e5a656', - seq: 339979, - text: 'test 2', - }, + recipient: { + id: '1895382890692545', }, - ], - }, - ], - }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + text: 'test 1', + }, + }, + ], + }, + { + id: '189538289069256', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a656', + text: 'test 2', + }, + }, + ], + }, + ], }; -const standbyRequest = { - body: { - object: 'page', - entry: [ - { - id: '', - time: 1458692752478, - standby: [ - { - sender: { - id: '', - }, - recipient: { - id: '', - }, - - // FIXME: standby is still beta - // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/standby - /* ... */ +const standbyRequest: MessengerRequestBody = { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + standby: [ + { + sender: { + id: '', }, - ], - }, - ], - }, + recipient: { + id: '', + }, + + // FIXME: standby is still beta + // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/standby + /* ... */ + }, + ], + }, + ], }; -const webhookTestRequest = { +const webhookTestRequest: MessengerRequestBody = { body: { entry: [ { @@ -170,20 +161,19 @@ function setup({ verifyToken = '1qaz2wsx', skipLegacyProfile = true, } = {}) { - const mockGraphAPIClient = { - getUserProfile: jest.fn(), - }; - MessengerClient.connect = jest.fn(); - MessengerClient.connect.mockReturnValue(mockGraphAPIClient); + const connector = new MessengerConnector({ + accessToken, + appSecret, + mapPageToAccessToken, + verifyToken, + skipLegacyProfile, + }); + + const client = mocked(MessengerClient).mock.instances[0]; + return { - mockGraphAPIClient, - connector: new MessengerConnector({ - accessToken, - appSecret, - mapPageToAccessToken, - verifyToken, - skipLegacyProfile, - }), + client, + connector, }; } @@ -197,7 +187,7 @@ it('should use accessToken and appSecret (for appsecret_proof) to create default appSecret: APP_SECRET, }); - expect(MessengerClient.connect).toBeCalledWith({ + expect(MessengerClient).toBeCalledWith({ accessToken: ACCESS_TOKEN, appSecret: APP_SECRET, }); @@ -219,13 +209,22 @@ describe('#verifyToken', () => { describe('#client', () => { it('should be client', () => { - const { connector, mockGraphAPIClient } = setup(); - expect(connector.client).toBe(mockGraphAPIClient); + const { connector, client } = setup(); + expect(connector.client).toBe(client); }); it('support custom client', () => { - const client = {}; - const connector = new MessengerConnector({ client }); + const client = new MessengerClient({ + accessToken: ACCESS_TOKEN, + appSecret: APP_SECRET, + }); + + const connector = new MessengerConnector({ + appId: APP_ID, + appSecret: APP_SECRET, + client, + }); + expect(connector.client).toBe(client); }); }); @@ -233,19 +232,19 @@ describe('#client', () => { describe('#getUniqueSessionKey', () => { it('extract correct sender id', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(request.body); + const senderId = connector.getUniqueSessionKey(request); expect(senderId).toBe('1412611362105802'); }); it('return recipient id when request is an echo event', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(echoRequest.body); + const senderId = connector.getUniqueSessionKey(echoRequest); expect(senderId).toBe('1244813222196986'); }); it('extract sender id from first event', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(batchRequest.body); + const senderId = connector.getUniqueSessionKey(batchRequest); expect(senderId).toBe('1412611362105802'); }); @@ -257,7 +256,7 @@ describe('#getUniqueSessionKey', () => { it('return null if is webhook test event or other null rawEvent requests', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(webhookTestRequest.body); + const senderId = connector.getUniqueSessionKey(webhookTestRequest); expect(senderId).toBe(null); }); @@ -274,7 +273,6 @@ describe('#getUniqueSessionKey', () => { timestamp: 1486464322190, message: { mid: 'mid.1486464322190:cb04e5a654', - seq: 339979, text: 'text', }, }) @@ -285,7 +283,7 @@ describe('#getUniqueSessionKey', () => { describe('#updateSession', () => { it('update session with data needed', async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const user = { @@ -297,15 +295,12 @@ describe('#updateSession', () => { timezone: 8, gender: 'male', }; - mockGraphAPIClient.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const session = {}; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).toBeCalledWith( - '1412611362105802', - { accessToken: undefined } - ); + expect(client.getUserProfile).toBeCalledWith('1412611362105802'); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -319,7 +314,7 @@ describe('#updateSession', () => { }); it('update session when profilePic expired', async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const user = { @@ -331,19 +326,16 @@ describe('#updateSession', () => { timezone: 8, gender: 'male', }; - mockGraphAPIClient.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const session = { user: { profilePic: 'https://example.com/pic.png?oe=386D4380', // expired at 2000-01-01T00:00:00.000Z }, }; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).toBeCalledWith( - '1412611362105802', - { accessToken: undefined } - ); + expect(client.getUserProfile).toBeCalledWith('1412611362105802'); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -357,7 +349,7 @@ describe('#updateSession', () => { }); it('update session when expired date is invalid', async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const user = { @@ -369,19 +361,16 @@ describe('#updateSession', () => { timezone: 8, gender: 'male', }; - mockGraphAPIClient.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const session = { user: { profilePic: 'https://example.com/pic.png?oe=abc666666666', // wrong timestamp }, }; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).toBeCalledWith( - '1412611362105802', - { accessToken: undefined } - ); + expect(client.getUserProfile).toBeCalledWith('1412611362105802'); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -395,7 +384,7 @@ describe('#updateSession', () => { }); it('update session when something wrong', async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const user = { @@ -407,19 +396,16 @@ describe('#updateSession', () => { timezone: 8, gender: 'male', }; - mockGraphAPIClient.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const session = { user: { profilePic123: 'https://example.com/pic.png?oe=386D4380', // wrong name }, }; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).toBeCalledWith( - '1412611362105802', - { accessToken: undefined } - ); + expect(client.getUserProfile).toBeCalledWith('1412611362105802'); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -433,20 +419,17 @@ describe('#updateSession', () => { }); it('update session when getUserProfile() failed', async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const error = new Error('fail'); - mockGraphAPIClient.getUserProfile.mockRejectedValue(error); + mocked(client.getUserProfile).mockRejectedValue(error); const session = {}; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).toBeCalledWith( - '1412611362105802', - { accessToken: undefined } - ); + expect(client.getUserProfile).toBeCalledWith('1412611362105802'); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -465,14 +448,14 @@ describe('#updateSession', () => { }); it(`update session without getting user's profile when skipLegacyProfile set to true`, async () => { - const { connector, mockGraphAPIClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: true, }); const session = {}; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockGraphAPIClient.getUserProfile).not.toBeCalled(); + expect(client.getUserProfile).not.toBeCalled(); expect(session).toEqual({ page: { _updatedAt: expect.any(String), @@ -489,7 +472,7 @@ describe('#updateSession', () => { describe('#mapRequestToEvents', () => { it('should map request to MessengerEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(request.body); + const events = connector.mapRequestToEvents(request); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(MessengerEvent); @@ -498,7 +481,7 @@ describe('#mapRequestToEvents', () => { it('should work with batch entry', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(batchRequest.body); + const events = connector.mapRequestToEvents(batchRequest); expect(events).toHaveLength(2); expect(events[0]).toBeInstanceOf(MessengerEvent); @@ -510,7 +493,7 @@ describe('#mapRequestToEvents', () => { it('should map request to standby MessengerEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(standbyRequest.body); + const events = connector.mapRequestToEvents(standbyRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(MessengerEvent); @@ -520,7 +503,7 @@ describe('#mapRequestToEvents', () => { it('should map request to echo MessengerEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(echoRequest.body); + const events = connector.mapRequestToEvents(echoRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(MessengerEvent); @@ -528,29 +511,27 @@ describe('#mapRequestToEvents', () => { }); it('should be filtered if body is not messaging or standby', () => { - const otherRequest = { - body: { - object: 'page', - entry: [ - { - id: '', - time: 1458692752478, - other: [ - { - sender: { - id: '', - }, - recipient: { - id: '', - }, + const otherRequest: MessengerRequestBody = { + object: 'page', + entry: [ + { + id: '', + time: 1458692752478, + other: [ + { + sender: { + id: '', }, - ], - }, - ], - }, + recipient: { + id: '', + }, + }, + ], + }, + ], }; const { connector } = setup(); - const events = connector.mapRequestToEvents(otherRequest.body); + const events = connector.mapRequestToEvents(otherRequest); expect(events).toHaveLength(0); }); @@ -733,7 +714,7 @@ describe('#preprocess', () => { status: 400, body: { error: { - message: 'Messenger Signature Validation Failed!', + message: 'Facebook Signature Validation Failed!', request: { headers: { 'x-hub-signature': diff --git a/packages/bottender/src/messenger/__tests__/MessengerContext-send.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerContext-send.spec.ts index 5d23dff43..9e2d484ac 100644 --- a/packages/bottender/src/messenger/__tests__/MessengerContext-send.spec.ts +++ b/packages/bottender/src/messenger/__tests__/MessengerContext-send.spec.ts @@ -1,26 +1,18 @@ +import warning from 'warning'; +import { MessengerClient } from 'messaging-api-messenger'; + +import MessengerContext from '../MessengerContext'; +import MessengerEvent from '../MessengerEvent'; + import { delivery, echoMessage as echo, read } from './MessengerEvent.spec'; -jest.mock('delay'); jest.mock('messaging-api-messenger'); jest.mock('warning'); -let MessengerClient; -let MessengerContext; -let MessengerEvent; -let sleep; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - MessengerClient = require('messaging-api-messenger').MessengerClient; - MessengerContext = require('../MessengerContext').default; - MessengerEvent = require('../MessengerEvent').default; - sleep = require('delay'); - warning = require('warning'); - /* eslint-enable global-require */ -}); +const ACCESS_TOKEN = 'FAKE_TOKEN'; +const APP_SECRET = 'FAKE_SECRET'; -const _rawEvent = { +const defaultRawEvent = { sender: { id: '1423587017700273' }, recipient: { id: '404217156637689' }, timestamp: 1491796363181, @@ -38,13 +30,16 @@ const userSession = { }; const setup = ( - { session = userSession, customAccessToken, rawEvent = _rawEvent } = { + { session = userSession, customAccessToken, rawEvent = defaultRawEvent } = { session: userSession, customAccessToken: undefined, - _rawEvent, + defaultRawEvent, } ) => { - const client = MessengerClient.connect(); + const client = new MessengerClient({ + accessToken: customAccessToken ?? ACCESS_TOKEN, + appSecret: APP_SECRET, + }); const args = { client, event: new MessengerEvent(rawEvent), @@ -501,26 +496,7 @@ describe('#sendSenderAction', () => { expect(client.sendSenderAction).toBeCalledWith( session.user.id, 'typing_on', - { - accessToken: undefined, - } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.sendSenderAction('typing_on'); - - expect(client.sendSenderAction).toBeCalledWith( - session.user.id, - 'typing_on', - { - accessToken: 'anyToken', - } + {} ); }); @@ -543,22 +519,7 @@ describe('#typingOn', () => { await context.typingOn(); - expect(client.typingOn).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.typingOn(); - - expect(client.typingOn).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', - }); + expect(client.typingOn).toBeCalledWith(session.user.id, {}); }); it('should call warning and not to send if dont have session', async () => { @@ -580,22 +541,7 @@ describe('#typingOff', () => { await context.typingOff(); - expect(client.typingOff).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.typingOff(); - - expect(client.typingOff).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', - }); + expect(client.typingOff).toBeCalledWith(session.user.id, {}); }); it('should call warning and not to send if dont have session', async () => { @@ -617,22 +563,7 @@ describe('#markSeen', () => { await context.markSeen(); - expect(client.markSeen).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.markSeen(); - - expect(client.markSeen).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', - }); + expect(client.markSeen).toBeCalledWith(session.user.id); }); it('should call warning and not to send if dont have session', async () => { @@ -648,26 +579,6 @@ describe('#markSeen', () => { }); }); -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context, client } = setup(); - - await context.typing(10); - - expect(client.typingOn).toBeCalled(); - expect(sleep).toBeCalledWith(10); - expect(client.typingOff).toBeCalled(); - }); -}); - describe('#useAccessToken', () => { it('should support inject custom token', async () => { const { context, client, session } = setup(); diff --git a/packages/bottender/src/messenger/__tests__/MessengerContext-sendTemplate.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerContext-sendTemplate.spec.ts index 17e95325d..35c283533 100644 --- a/packages/bottender/src/messenger/__tests__/MessengerContext-sendTemplate.spec.ts +++ b/packages/bottender/src/messenger/__tests__/MessengerContext-sendTemplate.spec.ts @@ -1,24 +1,16 @@ +import warning from 'warning'; +import { MessengerClient } from 'messaging-api-messenger'; + +import MessengerContext from '../MessengerContext'; +import MessengerEvent from '../MessengerEvent'; + import { delivery, echoMessage as echo, read } from './MessengerEvent.spec'; -jest.mock('delay'); jest.mock('messaging-api-messenger'); jest.mock('warning'); +jest.mock('delay'); -let MessengerClient; -let MessengerContext; -let MessengerEvent; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - MessengerClient = require('messaging-api-messenger').MessengerClient; - MessengerContext = require('../MessengerContext').default; - MessengerEvent = require('../MessengerEvent').default; - warning = require('warning'); - /* eslint-enable global-require */ -}); - -const _rawEvent = { +const defaultRawEvent = { sender: { id: '1423587017700273' }, recipient: { id: '404217156637689' }, timestamp: 1491796363181, @@ -35,21 +27,28 @@ const userSession = { }, }; +const ACCESS_TOKEN = 'FAKE_TOKEN'; +const APP_SECRET = 'FAKE_SECRET'; + const setup = ( - { session = userSession, customAccessToken, rawEvent = _rawEvent } = { + { session = userSession, customAccessToken, rawEvent = defaultRawEvent } = { session: userSession, customAccessToken: undefined, - _rawEvent, + defaultRawEvent, } ) => { - const client = MessengerClient.connect(); - const args = { + const client = new MessengerClient({ + accessToken: customAccessToken ?? ACCESS_TOKEN, + appSecret: APP_SECRET, + }); + + const context = new MessengerContext({ client, event: new MessengerEvent(rawEvent), session, customAccessToken, - }; - const context = new MessengerContext(args); + }); + return { context, session, diff --git a/packages/bottender/src/messenger/__tests__/MessengerContext.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerContext.spec.ts index 701f3dbfa..cfa1774fe 100644 --- a/packages/bottender/src/messenger/__tests__/MessengerContext.spec.ts +++ b/packages/bottender/src/messenger/__tests__/MessengerContext.spec.ts @@ -1,25 +1,18 @@ -jest.mock('delay'); +import warning from 'warning'; +import { MessengerClient } from 'messaging-api-messenger'; +import { mocked } from 'ts-jest/utils'; + +import MessengerContext from '../MessengerContext'; +import MessengerEvent from '../MessengerEvent'; + jest.mock('messaging-api-messenger'); jest.mock('warning'); - -let MessengerClient; -let MessengerContext; -let MessengerEvent; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - MessengerClient = require('messaging-api-messenger').MessengerClient; - MessengerContext = require('../MessengerContext').default; - MessengerEvent = require('../MessengerEvent').default; - warning = require('warning'); - /* eslint-enable global-require */ -}); +jest.mock('delay'); const APP_ID = '1234567890'; const PERSONA_ID = '987654321'; -const _rawEvent = { +const defaultRawEvent = { sender: { id: '1423587017700273' }, recipient: { id: '404217156637689' }, timestamp: 1491796363181, @@ -37,13 +30,15 @@ const userSession = { }; const setup = ( - { session = userSession, customAccessToken, rawEvent = _rawEvent } = { + { session = userSession, customAccessToken, rawEvent = defaultRawEvent } = { session: userSession, customAccessToken: undefined, - _rawEvent, + defaultRawEvent, } ) => { - const client = MessengerClient.connect(); + const client = new MessengerClient({ + accessToken: customAccessToken ?? '', + }); const args = { appId: APP_ID, client, @@ -59,11 +54,6 @@ const setup = ( }; }; -it('be defined', () => { - const { context } = setup(); - expect(context).toBeDefined(); -}); - it('#platform to be `messenger`', () => { const { context } = setup(); expect(context.platform).toBe('messenger'); @@ -85,7 +75,7 @@ it('get #client works', () => { }); describe('#getUserProfile', () => { - it('should call client sendSenderAction', async () => { + it('should call client getUserProfile', async () => { const { context, client, session } = setup(); const user = { @@ -96,13 +86,11 @@ describe('#getUserProfile', () => { profilePic: 'https://example.com/pic.png', }; - client.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const result = await context.getUserProfile(); - expect(client.getUserProfile).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); + expect(client.getUserProfile).toBeCalledWith(session.user.id, {}); expect(result).toEqual(user); }); @@ -120,7 +108,7 @@ describe('#getUserProfile', () => { gender: 'male', }; - client.getUserProfile.mockResolvedValue(user); + mocked(client.getUserProfile).mockResolvedValue(user); const result = await context.getUserProfile({ fields: [ @@ -146,31 +134,6 @@ describe('#getUserProfile', () => { 'timezone', 'gender', ], - accessToken: undefined, - }); - expect(result).toEqual(user); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - const user = { - id: session.user.id, - name: 'Kevin Durant', - firstName: 'Kevin', - lastName: 'Durant', - profilePic: 'https://example.com/pic.png', - }; - - client.getUserProfile.mockResolvedValue(user); - - const result = await context.getUserProfile(); - - expect(client.getUserProfile).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', }); expect(result).toEqual(user); }); @@ -188,37 +151,133 @@ describe('#getUserProfile', () => { }); }); -describe('#passThreadControl', () => { - it('should call to pass user thread control to other app', async () => { - const { context, client, session } = setup(); +describe('Persistent Menu', () => { + describe('#getUserPersistentMenu', () => { + it('should call client getUserPersistentMenu', async () => { + const { context, client, session } = setup(); - await context.passThreadControl(263902037430900, 'metadata'); + const persistentMenu = [ + { + locale: 'default', + composer_input_disabled: false, + call_to_actions: [ + { + type: 'postback', + title: 'Restart Conversation', + payload: 'RESTART', + }, + { + type: 'web_url', + title: 'Powered by ALOHA.AI, Yoctol', + url: 'https://www.yoctol.com/', + }, + ], + }, + ]; - expect(client.passThreadControl).toBeCalledWith( - session.user.id, - 263902037430900, - 'metadata', - { - accessToken: undefined, - } - ); + client.getUserPersistentMenu.mockResolvedValue(persistentMenu); + + const result = await context.getUserPersistentMenu(); + + expect(client.getUserPersistentMenu).toBeCalledWith(session.user.id); + expect(result).toEqual(persistentMenu); + }); + + it('should call warning and not to send if dont have session', async () => { + const { context, client } = setup({ session: false }); + + await context.getUserPersistentMenu(); + + expect(warning).toBeCalledWith( + false, + 'getUserPersistentMenu: should not be called in context without session' + ); + expect(client.getUserPersistentMenu).not.toBeCalled(); + }); }); - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', + describe('#setUserPersistentMenu', () => { + it('should call client setUserPersistentMenu', async () => { + const { context, client, session } = setup(); + + const persistentMenu = [ + { + locale: 'default', + composer_input_disabled: false, + call_to_actions: [ + { + type: 'postback', + title: 'Restart Conversation', + payload: 'RESTART', + }, + { + type: 'web_url', + title: 'Powered by ALOHA.AI, Yoctol', + url: 'https://www.yoctol.com/', + }, + ], + }, + ]; + + const result = await context.setUserPersistentMenu(persistentMenu); + + expect(client.setUserPersistentMenu).toBeCalledWith( + session.user.id, + persistentMenu, + {} + ); + + expect(result).toBeUndefined(); }); + it('should call warning and not to send if dont have session', async () => { + const { context, client } = setup({ session: false }); + + await context.setUserPersistentMenu(); + + expect(warning).toBeCalledWith( + false, + 'setUserPersistentMenu: should not be called in context without session' + ); + expect(client.setPersistentMenu).not.toBeCalled(); + }); + }); + + describe('#deleteUserPersistentMenu', () => { + it('should call client deleteUserPersistentMenu', async () => { + const { context, client, session } = setup(); + + const result = await context.deleteUserPersistentMenu(); + + expect(client.deleteUserPersistentMenu).toBeCalledWith(session.user.id); + + expect(result).toBeUndefined(); + }); + + it('should call warning and not to send if dont have session', async () => { + const { context, client } = setup({ session: false }); + + await context.deleteUserPersistentMenu(); + + expect(warning).toBeCalledWith( + false, + 'deleteUserPersistentMenu: should not be called in context without session' + ); + expect(client.deleteUserPersistentMenu).not.toBeCalled(); + }); + }); +}); + +describe('#passThreadControl', () => { + it('should call to pass user thread control to other app', async () => { + const { context, client, session } = setup(); + await context.passThreadControl(263902037430900, 'metadata'); expect(client.passThreadControl).toBeCalledWith( session.user.id, 263902037430900, - 'metadata', - { - accessToken: 'anyToken', - } + 'metadata' ); }); @@ -243,27 +302,7 @@ describe('#passThreadControlToPageInbox', () => { expect(client.passThreadControlToPageInbox).toBeCalledWith( session.user.id, - 'metadata', - { - accessToken: undefined, - } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.passThreadControlToPageInbox(); - - expect(client.passThreadControlToPageInbox).toBeCalledWith( - session.user.id, - undefined, - { - accessToken: 'anyToken', - } + 'metadata' ); }); @@ -288,27 +327,7 @@ describe('#takeThreadControl', () => { expect(client.takeThreadControl).toBeCalledWith( session.user.id, - 'metadata', - { - accessToken: undefined, - } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.takeThreadControl('metadata'); - - expect(client.takeThreadControl).toBeCalledWith( - session.user.id, - 'metadata', - { - accessToken: 'anyToken', - } + 'metadata' ); }); @@ -333,27 +352,7 @@ describe('#requestThreadControl', () => { expect(client.requestThreadControl).toBeCalledWith( session.user.id, - 'metadata', - { - accessToken: undefined, - } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.requestThreadControl('metadata'); - - expect(client.requestThreadControl).toBeCalledWith( - session.user.id, - 'metadata', - { - accessToken: 'anyToken', - } + 'metadata' ); }); @@ -376,22 +375,7 @@ describe('#getThreadOwner', () => { await context.getThreadOwner(); - expect(client.getThreadOwner).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.getThreadOwner(); - - expect(client.getThreadOwner).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', - }); + expect(client.getThreadOwner).toBeCalledWith(session.user.id); }); it('should call warning if dont have session', async () => { @@ -433,23 +417,7 @@ describe('#associateLabel', () => { expect(client.associateLabel).toBeCalledWith( session.user.id, - 1712444532121303, - { accessToken: undefined } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.associateLabel(1712444532121303); - - expect(client.associateLabel).toBeCalledWith( - session.user.id, - 1712444532121303, - { accessToken: 'anyToken' } + 1712444532121303 ); }); @@ -474,23 +442,7 @@ describe('#dissociateLabel', () => { expect(client.dissociateLabel).toBeCalledWith( session.user.id, - 1712444532121303, - { accessToken: undefined } - ); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - await context.dissociateLabel(1712444532121303); - - expect(client.dissociateLabel).toBeCalledWith( - session.user.id, - 1712444532121303, - { accessToken: 'anyToken' } + 1712444532121303 ); }); @@ -534,43 +486,7 @@ describe('#getAssociatedLabels', () => { await context.getAssociatedLabels(); - expect(client.getAssociatedLabels).toBeCalledWith(session.user.id, { - accessToken: undefined, - }); - }); - - it('should use custom access token', async () => { - const { context, client, session } = setup({ - session: userSession, - customAccessToken: 'anyToken', - }); - - client.getAssociatedLabels.mockReturnValue({ - data: [ - { - name: 'myLabel', - id: '1001200005003', - }, - { - name: 'myOtherLabel', - id: '1001200005002', - }, - ], - paging: { - cursors: { - before: - 'QVFIUmx1WTBpMGpJWXprYzVYaVhabW55dVpycko4U2xURGE5ODNtNFZAPal94a1hTUnNVMUtoMVVoTzlzSDktUkMtQkUzWEFLSXlMS3ZALYUw3TURLelZAPOGVR', - after: - 'QVFIUmItNkpTbjVzakxFWGRydzdaVUFNNnNPaUl0SmwzVHN5ZAWZAEQ3lZANDAzTXFIM0NHbHdYSkQ5OG1GaEozdjkzRmxpUFhxTDl4ZAlBibnE4LWt1eGlTa3Bn', - }, - }, - }); - - await context.getAssociatedLabels(); - - expect(client.getAssociatedLabels).toBeCalledWith(session.user.id, { - accessToken: 'anyToken', - }); + expect(client.getAssociatedLabels).toBeCalledWith(session.user.id); }); it('should call warning if dont have session', async () => { @@ -592,17 +508,10 @@ describe('persona', () => { const { context, client, session } = setup(); context.usePersona(PERSONA_ID); - await context.markSeen(); await context.typingOn(); await context.typingOff(); await context.sendText('hi'); - expect(client.markSeen).toBeCalledWith( - session.user.id, - expect.objectContaining({ - personaId: PERSONA_ID, - }) - ); expect(client.typingOn).toBeCalledWith( session.user.id, expect.objectContaining({ @@ -629,17 +538,10 @@ describe('persona', () => { it('should call API with personaId', async () => { const { context, client, session } = setup(); - await context.markSeen({ personaId: PERSONA_ID }); await context.typingOn({ personaId: PERSONA_ID }); await context.typingOff({ personaId: PERSONA_ID }); await context.sendText('hi', { personaId: PERSONA_ID }); - expect(client.markSeen).toBeCalledWith( - session.user.id, - expect.objectContaining({ - personaId: PERSONA_ID, - }) - ); expect(client.typingOn).toBeCalledWith( session.user.id, expect.objectContaining({ diff --git a/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts index fae2b0ffe..742f26972 100644 --- a/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts +++ b/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts @@ -26,8 +26,7 @@ const imageMessage = { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', + url: 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', }, }, ], @@ -163,8 +162,7 @@ const likeStickerMessage = { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', stickerId: 369239263222822, }, }, @@ -188,8 +186,7 @@ const largeLikeStickerMessage = { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/p100x100/851587_369239346556147_162929011_n.png?_nc_ad=z-m&oh=2008c832edbd2376b09a1008358b8fd9&oe=598FC1B0', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/p100x100/851587_369239346556147_162929011_n.png?_nc_ad=z-m&oh=2008c832edbd2376b09a1008358b8fd9&oe=598FC1B0', stickerId: 369239343222814, }, }, @@ -213,8 +210,7 @@ const hugeLikeStickerMessage = { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/p100x100/851587_369239346556147_162929011_n.png?_nc_ad=z-m&oh=2008c832edbd2376b09a1008358b8fd9&oe=598FC1B0', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/p100x100/851587_369239346556147_162929011_n.png?_nc_ad=z-m&oh=2008c832edbd2376b09a1008358b8fd9&oe=598FC1B0', stickerId: 369239383222810, }, }, @@ -662,6 +658,26 @@ it('#rawEvent', () => { ); }); +it('#timestamp', () => { + expect(new MessengerEvent(textMessage).timestamp).toEqual(1491796363181); + expect(new MessengerEvent(imageMessage).timestamp).toEqual(1491797604411); + expect(new MessengerEvent(likeStickerMessage).timestamp).toEqual( + 1491797086506 + ); + expect(new MessengerEvent(quickReplyMessage).timestamp).toEqual( + 1491798262319 + ); + expect(new MessengerEvent(echoMessage).timestamp).toEqual(1491798024994); + expect(new MessengerEvent(postback).timestamp).toEqual(1491798782090); + expect(new MessengerEvent(payment).timestamp).toEqual(1473208792799); + expect(new MessengerEvent(accountLinkingLinked).timestamp).toEqual( + 1469111400000 + ); + expect(new MessengerEvent(accountLinkingUnlinked).timestamp).toEqual( + 1469111400000 + ); +}); + it('#isMessage', () => { expect(new MessengerEvent(textMessage).isMessage).toEqual(true); expect(new MessengerEvent(imageMessage).isMessage).toEqual(true); @@ -690,8 +706,7 @@ it('#message', () => { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', + url: 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', }, }, ], @@ -704,8 +719,7 @@ it('#message', () => { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', stickerId: 369239263222822, }, }, @@ -738,8 +752,7 @@ it('#attachments', () => { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', + url: 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', }, }, ]); @@ -747,8 +760,7 @@ it('#attachments', () => { { type: 'image', payload: { - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', stickerId: 369239263222822, }, }, @@ -775,13 +787,11 @@ it('#image', () => { expect(new MessengerEvent(fileMessage).image).toEqual(null); expect(new MessengerEvent(fallbackMessage).image).toEqual(null); expect(new MessengerEvent(imageMessage).image).toEqual({ - url: - 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', + url: 'https://scontent.xx.fbcdn.net/v/t35.0-12/17887258_1429713783754592_1626047672_o.jpg?_nc_ad=z-m&oh=e44af5a4c973541ef56333202f160720&oe=58ECF78B', }); expect(new MessengerEvent(likeStickerMessage).image).toEqual({ stickerId: 369239263222822, - url: - 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', + url: 'https://scontent.xx.fbcdn.net/v/t39.1997-6/851557_369239266556155_759568595_n.png?_nc_ad=z-m&oh=547beb90237e24a9682810a5144c9fba&oe=5988CFDC', }); }); diff --git a/packages/bottender/src/messenger/__tests__/routes.spec.ts b/packages/bottender/src/messenger/__tests__/routes.spec.ts index 56f80ed15..8b6379534 100644 --- a/packages/bottender/src/messenger/__tests__/routes.spec.ts +++ b/packages/bottender/src/messenger/__tests__/routes.spec.ts @@ -406,7 +406,7 @@ async function expectRouteNotMatchMessengerEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender/src/messenger/routes.ts b/packages/bottender/src/messenger/routes.ts index 543fb7b03..54fcda5a0 100644 --- a/packages/bottender/src/messenger/routes.ts +++ b/packages/bottender/src/messenger/routes.ts @@ -1,9 +1,10 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import MessengerContext from './MessengerContext'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -39,7 +40,7 @@ type Messenger = Route & { }; }; -const messenger: Messenger = ( +const messenger: Messenger = ( action: Action ) => { return route((context: C) => context.platform === 'messenger', action); @@ -47,7 +48,7 @@ const messenger: Messenger = ( messenger.any = messenger; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isMessage, action @@ -56,7 +57,7 @@ function message(action: Action) { messenger.message = message; -function accountLinking( +function accountLinking( action: Action ) { return route( @@ -68,7 +69,7 @@ function accountLinking( messenger.accountLinking = accountLinking; -function accountLinkingLinked( +function accountLinkingLinked( action: Action ) { return route( @@ -82,7 +83,7 @@ function accountLinkingLinked( accountLinking.linked = accountLinkingLinked; -function accountLinkingUnlinked( +function accountLinkingUnlinked( action: Action ) { return route( @@ -96,7 +97,7 @@ function accountLinkingUnlinked( accountLinking.unlinked = accountLinkingUnlinked; -function checkoutUpdate( +function checkoutUpdate( action: Action ) { return route( @@ -108,7 +109,7 @@ function checkoutUpdate( messenger.checkoutUpdate = checkoutUpdate; -function delivery(action: Action) { +function delivery(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isDelivery, @@ -118,7 +119,7 @@ function delivery(action: Action) { messenger.delivery = delivery; -function echo(action: Action) { +function echo(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isEcho, action @@ -127,7 +128,7 @@ function echo(action: Action) { messenger.echo = echo; -function gamePlay(action: Action) { +function gamePlay(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isGamePlay, @@ -137,7 +138,7 @@ function gamePlay(action: Action) { messenger.gamePlay = gamePlay; -function passThreadControl( +function passThreadControl( action: Action ) { return route( @@ -149,7 +150,7 @@ function passThreadControl( messenger.passThreadControl = passThreadControl; -function takeThreadControl( +function takeThreadControl( action: Action ) { return route( @@ -161,7 +162,7 @@ function takeThreadControl( messenger.takeThreadControl = takeThreadControl; -function requestThreadControl( +function requestThreadControl( action: Action ) { return route( @@ -173,7 +174,7 @@ function requestThreadControl( messenger.requestThreadControl = requestThreadControl; -function appRoles(action: Action) { +function appRoles(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isAppRoles, @@ -183,7 +184,7 @@ function appRoles(action: Action) { messenger.appRoles = appRoles; -function optin(action: Action) { +function optin(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isOptin, action @@ -192,7 +193,7 @@ function optin(action: Action) { messenger.optin = optin; -function payment(action: Action) { +function payment(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isPayment, action @@ -201,7 +202,7 @@ function payment(action: Action) { messenger.payment = payment; -function policyEnforcement( +function policyEnforcement( action: Action ) { return route( @@ -213,7 +214,7 @@ function policyEnforcement( messenger.policyEnforcement = policyEnforcement; -function postback(action: Action) { +function postback(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isPostback, @@ -223,9 +224,7 @@ function postback(action: Action) { messenger.postback = postback; -function preCheckout( - action: Action -) { +function preCheckout(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isPreCheckout, @@ -235,7 +234,7 @@ function preCheckout( messenger.preCheckout = preCheckout; -function read(action: Action) { +function read(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isRead, action @@ -244,7 +243,7 @@ function read(action: Action) { messenger.read = read; -function referral(action: Action) { +function referral(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isReferral, @@ -254,7 +253,7 @@ function referral(action: Action) { messenger.referral = referral; -function standby(action: Action) { +function standby(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isStandby, action @@ -263,7 +262,7 @@ function standby(action: Action) { messenger.standby = standby; -function reaction(action: Action) { +function reaction(action: Action) { return route( (context: C) => context.platform === 'messenger' && context.event.isReaction, @@ -273,7 +272,7 @@ function reaction(action: Action) { messenger.reaction = reaction; -function reactionReact( +function reactionReact( action: Action ) { return route( @@ -287,7 +286,7 @@ function reactionReact( reaction.react = reactionReact; -function reactionUnreact( +function reactionUnreact( action: Action ) { return route( diff --git a/packages/bottender/src/plugins/withTyping.ts b/packages/bottender/src/plugins/withTyping.ts index 86354b8c4..d0d82c40b 100644 --- a/packages/bottender/src/plugins/withTyping.ts +++ b/packages/bottender/src/plugins/withTyping.ts @@ -109,7 +109,7 @@ export default (options: Options) => (context: any) => { if (context[method]) { const _method = context[method]; /* eslint-disable func-names */ - context[method] = async function(...args: any[]) { + context[method] = async function (...args: any[]) { const methodOptions = args[len - 1]; const delay = diff --git a/packages/bottender/src/router/__tests__/router.spec.ts b/packages/bottender/src/router/__tests__/router.spec.ts index d27defa86..d7a1bf86e 100644 --- a/packages/bottender/src/router/__tests__/router.spec.ts +++ b/packages/bottender/src/router/__tests__/router.spec.ts @@ -1,5 +1,3 @@ -import { run } from '../../bot/Bot'; - import router, { line, messenger, @@ -11,6 +9,7 @@ import router, { viber, whatsapp, } from '..'; +import { run } from '../../bot/Bot'; function textContext(message = '') { return { @@ -155,7 +154,7 @@ describe('#route', () => { it('should work with predicate', async () => { const Router = router([ route( - context => context.event.isText && context.event.text.startsWith('h'), + (context) => context.event.isText && context.event.text.startsWith('h'), sendText('hello') ), ]); diff --git a/packages/bottender/src/router/index.ts b/packages/bottender/src/router/index.ts index 9cf2ce760..ec758841b 100644 --- a/packages/bottender/src/router/index.ts +++ b/packages/bottender/src/router/index.ts @@ -1,25 +1,28 @@ +import { JsonObject } from 'type-fest'; + +import Context from '../context/Context'; import line from '../line/routes'; import messenger from '../messenger/routes'; import slack from '../slack/routes'; import telegram from '../telegram/routes'; import viber from '../viber/routes'; import whatsapp from '../whatsapp/routes'; -import { Action, AnyContext, Props } from '../types'; +import { Action, Props } from '../types'; type MatchPattern = string | Array | RegExp; -type RoutePattern = '*' | RoutePredicate; +type RoutePattern = '*' | RoutePredicate; -export type RoutePredicate = ( +export type RoutePredicate = ( context: C -) => boolean | Record | Promise>; +) => boolean | JsonObject | Promise; -type Route = { +type Route = { predicate: RoutePredicate; action: Action; }; -function router(routes: Route[]) { +function router(routes: Route[]) { return async function Router(context: C, props: Props = {}) { for (const r of routes) { // eslint-disable-next-line no-await-in-loop @@ -38,7 +41,7 @@ function router(routes: Route[]) { }; } -function route( +function route( pattern: RoutePattern, action: Action ) { @@ -55,7 +58,7 @@ function route( }; } -function text( +function text( pattern: MatchPattern, action: Action ) { @@ -100,7 +103,7 @@ function text( }; } -function payload( +function payload( pattern: MatchPattern, action: Action ) { @@ -145,7 +148,7 @@ function payload( }; } -function platform( +function platform( pattern: MatchPattern, action: Action ) { diff --git a/packages/bottender/src/server/Server.ts b/packages/bottender/src/server/Server.ts index 1ef95f5dc..8b2b97047 100644 --- a/packages/bottender/src/server/Server.ts +++ b/packages/bottender/src/server/Server.ts @@ -1,15 +1,7 @@ -import path from 'path'; -import url from 'url'; import { IncomingMessage, ServerResponse } from 'http'; -import fromEntries from 'fromentries'; -import merge from 'lodash/merge'; -import { pascalcase } from 'messaging-api-common'; - -import Bot from '../bot/Bot'; -import getBottenderConfig from '../shared/getBottenderConfig'; -import getSessionStore from '../getSessionStore'; -import { Action, BottenderConfig, Plugin } from '../types'; +import getChannelBotAndRequestContext from '../shared/getChannelBotAndRequestContext'; +import getConsoleBot from '../shared/getConsoleBot'; export type ServerOptions = { useConsole?: boolean; @@ -17,8 +9,6 @@ export type ServerOptions = { }; class Server { - _channelBots: { webhookPath: string; bot: Bot }[] = []; - useConsole: boolean; constructor({ useConsole = false } = {}) { @@ -30,83 +20,40 @@ class Server { res: ServerResponse ): Promise { res.statusCode = 200; - return this.run(req, res).catch(err => { + return this.run(req, res).catch((err) => { console.error(err); res.statusCode = 500; res.end('Internal Server Error'); }); } - public async prepare(): Promise { - const bottenderConfig = getBottenderConfig(); - - const { initialState, plugins, channels } = merge( - bottenderConfig /* , config */ - ) as BottenderConfig; - - const sessionStore = getSessionStore(); - - // TODO: refine handler entry, improve error message and hint - // eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires - const Entry: Action = require(path.resolve('index.js')); - let ErrorEntry: Action; - try { - // eslint-disable-next-line import/no-dynamic-require - ErrorEntry = require(path.resolve('_error.js')); - } catch (err) {} // eslint-disable-line no-empty - - function initializeBot(bot: Bot): void { - if (initialState) { - bot.setInitialState(initialState); - } - - if (plugins) { - plugins.forEach((plugin: Plugin) => { - bot.use(plugin); - }); - } - - bot.onEvent(Entry); - if (ErrorEntry) { - bot.onError(ErrorEntry); + private sendResponse(res: ServerResponse, response: any): void { + if (response) { + Object.entries(response.headers || {}).forEach(([key, value]) => { + res.setHeader(key, value as string); + }); + res.statusCode = response.status || 200; + if ( + response.body && + typeof response.body === 'object' && + !Buffer.isBuffer(response.body) + ) { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.end(JSON.stringify(response.body)); + } else { + res.end(response.body || ''); } + } else { + res.statusCode = 200; + res.end(''); } + } + public async prepare(): Promise { if (this.useConsole) { - const ConsoleBot = require('../console/ConsoleBot').default; - - const bot = new ConsoleBot({ - fallbackMethods: true, - sessionStore, - }); - - initializeBot(bot); - + const bot = getConsoleBot(); bot.createRuntime(); - - return; } - - const channelBots = Object.entries(channels || {}) - .filter(([, { enabled }]) => enabled) - .map(([channel, { path: webhookPath, ...channelConfig }]) => { - // eslint-disable-next-line import/no-dynamic-require - const ChannelBot = require(`../${channel}/${pascalcase(channel)}Bot`) - .default; - const channelBot = new ChannelBot({ - ...channelConfig, - sessionStore, - }) as Bot; - - initializeBot(channelBot); - - return { - webhookPath: webhookPath || `/webhooks/${channel}`, - bot: channelBot, - }; - }); - - this._channelBots = channelBots; } public getRequestHandler() { @@ -120,91 +67,37 @@ class Server { if (this.useConsole) { return; } + const { channelBot, requestContext } = + getChannelBotAndRequestContext(req) || {}; + if (!channelBot || !requestContext) { + return; + } - // TODO: add proxy support in Bottender to apply X-Forwarded-Host and X-Forwarded-Proto - // conditionally instead of replying on express. - const hostname = (req as any).hostname || req.headers.host; - const protocol = (req as any).protocol || 'https'; - - const requestUrl = `${protocol}://${hostname}${req.url}`; - - const { pathname, searchParams } = new url.URL(requestUrl); - - const query = fromEntries(searchParams.entries()); - - for (let i = 0; i < this._channelBots.length; i++) { - const { webhookPath, bot } = this._channelBots[i]; - - if (pathname === webhookPath) { - const result = (bot.connector as any).preprocess({ - method: req.method, - url: requestUrl, - headers: req.headers, - query, - rawBody: (req as any).rawBody, - body: (req as any).body, - }); - - const { shouldNext } = result; - let { response } = result; - - if (!shouldNext) { - if (response) { - Object.entries(response.headers || {}).forEach(([key, value]) => { - res.setHeader(key, value as string); - }); - res.statusCode = response.status || 200; - if ( - response.body && - typeof response.body === 'object' && - !Buffer.isBuffer(response.body) - ) { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.end(JSON.stringify(response.body)); - } else { - res.end(response.body || ''); - } - } else { - res.statusCode = 200; - res.end(''); - } - return; - } - - const requestHandler = bot.createRequestHandler(); - - // eslint-disable-next-line no-await-in-loop - response = await requestHandler( - { - ...query, - ...(req as any).body, - }, - { - method: req.method as string, - path: pathname, - query, - headers: req.headers as Record, - } - ); - - if (response) { - Object.entries(response.headers || {}).forEach(([key, value]) => { - res.setHeader(key, value as string); - }); - res.statusCode = response.status || 200; - if (response.body && typeof response.body === 'object') { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(response.body)); - } else { - res.end(response.body || ''); - } - } else { - res.statusCode = 200; - res.end(''); - } - return; - } + const bot = channelBot.bot; + + // eslint-disable-next-line no-await-in-loop + const result = await (bot.connector as any).preprocess(requestContext); + + const { shouldNext } = result; + let { response } = result; + + if (!shouldNext) { + this.sendResponse(res, response); + return; } + + const requestHandler = bot.createRequestHandler(); + + // eslint-disable-next-line no-await-in-loop + response = await requestHandler( + { + ...requestContext.query, + ...requestContext.body, + }, + requestContext + ); + + this.sendResponse(res, response); } } diff --git a/packages/bottender/src/server/__tests__/Server.spec.ts b/packages/bottender/src/server/__tests__/Server.spec.ts new file mode 100644 index 000000000..722c1208b --- /dev/null +++ b/packages/bottender/src/server/__tests__/Server.spec.ts @@ -0,0 +1,176 @@ +import http from 'http'; +import net from 'net'; +import path from 'path'; + +import { JsonArray, JsonObject } from 'type-fest'; +import { mocked } from 'ts-jest/utils'; + +import Server from '../Server'; +import getBottenderConfig from '../../shared/getBottenderConfig'; +import { cleanChannelBots } from '../../shared/getChannelBots'; + +let receivedContext; + +jest.mock('../../messenger/MessengerContext'); +jest.mock('../../shared/getBottenderConfig'); +jest.mock( + '/Users/username/bot/index.js', + () => + async function App(context) { + receivedContext = context; + await context.sendText('Hello World'); + }, + { virtual: true } +); + +const pathResolve = path.resolve; + +beforeEach(() => { + const customPathResolve = jest.fn((...args) => { + if (args[0] === 'index.js') return '/Users/username/bot/index.js'; + return pathResolve(...args); + }); + path.resolve = customPathResolve; + cleanChannelBots(); +}); + +afterEach(() => { + path.resolve = pathResolve; +}); + +it('support built-in connectors', async () => { + mocked(getBottenderConfig).mockReturnValue({ + channels: { + messenger: { + enabled: true, + path: '/webhooks/messenger', + sync: true, + accessToken: 'ACCESS_TOKEN', + appId: 'APP_ID', + appSecret: 'APP_SECRET', + }, + }, + }); + + const server = new Server(); + + await server.prepare(); + + const requestHandler = server.getRequestHandler(); + + const socket = new net.Socket(); + const req: http.IncomingMessage & { + rawBody?: string; + body?: JsonObject | JsonArray; + } = new http.IncomingMessage(socket); + + const body = { + object: 'page', + entry: [ + { + id: '1895382890692545', + time: 1486464322257, + messaging: [ + { + sender: { + id: '1412611362105802', + }, + recipient: { + id: '1895382890692545', + }, + timestamp: 1486464322190, + message: { + mid: 'mid.1486464322190:cb04e5a654', + seq: 339979, + text: 'text', + }, + }, + ], + }, + ], + }; + + req.rawBody = JSON.stringify(body); + req.body = body; + req.method = 'post'; + req.headers.host = 'www.example.com'; + req.headers['x-hub-signature'] = + 'sha1=0c9b77eea92817a0fa756ee52388061d18112a48'; + req.url = '/webhooks/messenger'; + + const res = new http.ServerResponse(req); + + await requestHandler(req, res); + + expect(receivedContext.sendText).toBeCalledWith('Hello World'); +}); + +it('support custom connectors', async () => { + const event = { + rawEvent: { + message: { + text: 'hi', + }, + }, + isMessage: true, + isText: true, + message: { + text: 'hi', + }, + }; + + const session = { + user: { + id: '__id__', + }, + }; + + const context = { + event, + session, + sendText: jest.fn(), + }; + const connector = { + platform: 'any', + getUniqueSessionKey: jest.fn(() => '__id__'), + updateSession: jest.fn(), + mapRequestToEvents: jest.fn(() => [event]), + createContext: jest.fn(() => context), + preprocess: jest.fn(() => ({ shouldNext: true })), + }; + + mocked(getBottenderConfig).mockReturnValue({ + channels: { + custom: { + enabled: true, + path: '/webhooks/custom', + sync: true, + connector, + }, + }, + }); + + const server = new Server(); + + await server.prepare(); + + const requestHandler = server.getRequestHandler(); + + const socket = new net.Socket(); + const req: http.IncomingMessage & { + rawBody?: string; + body?: JsonObject | JsonArray; + } = new http.IncomingMessage(socket); + + req.rawBody = '{}'; + req.body = {}; + req.headers.host = 'www.example.com'; + req.method = 'post'; + req.url = '/webhooks/custom'; + + const res = new http.ServerResponse(req); + + await requestHandler(req, res); + + expect(context.sendText).toBeCalledWith('Hello World'); +}); diff --git a/packages/bottender/src/session/FileSessionStore.ts b/packages/bottender/src/session/FileSessionStore.ts index 12a4a0673..5da3d4411 100644 --- a/packages/bottender/src/session/FileSessionStore.ts +++ b/packages/bottender/src/session/FileSessionStore.ts @@ -85,7 +85,7 @@ export default class FileSessionStore implements SessionStore { sess.lastActivity = Date.now(); await new Promise((resolve, reject) => { - this._jfs.save(safeKey, sess, err => { + this._jfs.save(safeKey, sess, (err) => { if (err) { reject(err); } else { @@ -99,7 +99,7 @@ export default class FileSessionStore implements SessionStore { const safeKey = os.platform() === 'win32' ? key.replace(':', '@') : key; return new Promise((resolve, reject) => { - this._jfs.delete(safeKey, err => { + this._jfs.delete(safeKey, (err) => { if (err) { reject(err); } else { diff --git a/packages/bottender/src/session/MemorySessionStore.ts b/packages/bottender/src/session/MemorySessionStore.ts index e64f64124..b1d6581f4 100644 --- a/packages/bottender/src/session/MemorySessionStore.ts +++ b/packages/bottender/src/session/MemorySessionStore.ts @@ -22,8 +22,10 @@ function getMaxSize(arg?: MemoryOption): number | undefined { return; } -export default class MemorySessionStore extends CacheBasedSessionStore - implements SessionStore { +export default class MemorySessionStore + extends CacheBasedSessionStore + implements SessionStore +{ constructor(arg?: MemoryOption, expiresIn?: number) { const maxSize = getMaxSize(arg); diff --git a/packages/bottender/src/session/RedisSessionStore.ts b/packages/bottender/src/session/RedisSessionStore.ts index 32dc6e009..a39094ec4 100644 --- a/packages/bottender/src/session/RedisSessionStore.ts +++ b/packages/bottender/src/session/RedisSessionStore.ts @@ -14,8 +14,10 @@ type RedisOption = db?: number; }; -export default class RedisSessionStore extends CacheBasedSessionStore - implements SessionStore { +export default class RedisSessionStore + extends CacheBasedSessionStore + implements SessionStore +{ constructor(arg: RedisOption, expiresIn?: number) { const cache = new RedisCacheStore(arg); super(cache, expiresIn); diff --git a/packages/bottender/src/session/__tests__/FileSessionStore.spec.ts b/packages/bottender/src/session/__tests__/FileSessionStore.spec.ts index 6c41b3d8b..68efd18d0 100644 --- a/packages/bottender/src/session/__tests__/FileSessionStore.spec.ts +++ b/packages/bottender/src/session/__tests__/FileSessionStore.spec.ts @@ -83,7 +83,7 @@ describe('#all', () => { const { store, jfs } = setup(); await store.init(); - jfs.all.mockImplementation(cb => cb(null, [{ id: 1 }, { id: 2 }])); + jfs.all.mockImplementation((cb) => cb(null, [{ id: 1 }, { id: 2 }])); expect(await store.all()).toEqual([{ id: 1 }, { id: 2 }]); expect(jfs.all).toBeCalledWith(expect.any(Function)); @@ -93,7 +93,7 @@ describe('#all', () => { const { store, jfs } = setup(); await store.init(); - jfs.all.mockImplementation(cb => cb(null, [])); + jfs.all.mockImplementation((cb) => cb(null, [])); expect(await store.all()).toEqual([]); expect(jfs.all).toBeCalledWith(expect.any(Function)); diff --git a/packages/bottender/src/shared/__tests__/getChannelBots.spec.ts b/packages/bottender/src/shared/__tests__/getChannelBots.spec.ts new file mode 100644 index 000000000..1a3c463d7 --- /dev/null +++ b/packages/bottender/src/shared/__tests__/getChannelBots.spec.ts @@ -0,0 +1,60 @@ +import path from 'path'; + +import { mocked } from 'ts-jest/utils'; + +import getBottenderConfig from '../getBottenderConfig'; +import getChannelBots, { cleanChannelBots } from '../getChannelBots'; + +jest.mock('../../shared/getBottenderConfig'); +jest.mock( + '/Users/username/bot/index.js', + () => + async function App(context) { + await context.sendText('Hello World'); + }, + { virtual: true } +); + +const pathResolve = path.resolve; + +beforeEach(() => { + const customPathResolve = jest.fn((...args) => { + if (args[0] === 'index.js') return '/Users/username/bot/index.js'; + return pathResolve(...args); + }); + path.resolve = customPathResolve; + cleanChannelBots(); +}); + +afterEach(() => { + path.resolve = pathResolve; +}); + +it('be defined', () => { + expect(getChannelBots).toBeDefined(); +}); + +it('should be empty array', () => { + mocked(getBottenderConfig).mockReturnValue({}); + expect(getChannelBots()).toEqual([]); +}); + +it('should create channelBots', () => { + mocked(getBottenderConfig).mockReturnValue({ + channels: { + messenger: { + enabled: true, + path: '/webhooks/messenger', + sync: true, + accessToken: 'ACCESS_TOKEN', + appId: 'APP_ID', + appSecret: 'APP_SECRET', + }, + }, + }); + const channelBots = getChannelBots(); + expect(channelBots.length).toEqual(1); + + const webhookPaths = channelBots.map((c) => c.webhookPath); + expect(webhookPaths[0]).toEqual('/webhooks/messenger'); +}); diff --git a/packages/bottender/src/shared/__tests__/getConsoleBot.spec.ts b/packages/bottender/src/shared/__tests__/getConsoleBot.spec.ts new file mode 100644 index 000000000..66f11f6f9 --- /dev/null +++ b/packages/bottender/src/shared/__tests__/getConsoleBot.spec.ts @@ -0,0 +1,40 @@ +import path from 'path'; + +import { mocked } from 'ts-jest/utils'; + +import ConsoleBot from '../../console/ConsoleBot'; +import getBottenderConfig from '../getBottenderConfig'; +import getConsoleBot from '../getConsoleBot'; + +jest.mock('../../shared/getBottenderConfig'); +jest.mock( + '/Users/username/bot/index.js', + () => + async function App(context) { + await context.sendText('Hello World'); + }, + { virtual: true } +); + +const pathResolve = path.resolve; + +beforeEach(() => { + const customPathResolve = jest.fn((...args) => { + if (args[0] === 'index.js') return '/Users/username/bot/index.js'; + return pathResolve(...args); + }); + path.resolve = customPathResolve; +}); + +afterEach(() => { + path.resolve = pathResolve; +}); + +it('be defined', () => { + expect(getConsoleBot).toBeDefined(); +}); + +it('should be instance of ConsoleBot', () => { + mocked(getBottenderConfig).mockReturnValue({}); + expect(getConsoleBot()).toBeInstanceOf(ConsoleBot); +}); diff --git a/packages/bottender/src/__tests__/getSessionStore.spec.ts b/packages/bottender/src/shared/__tests__/getSessionStore.spec.ts similarity index 91% rename from packages/bottender/src/__tests__/getSessionStore.spec.ts rename to packages/bottender/src/shared/__tests__/getSessionStore.spec.ts index b99e733f6..8db5f2535 100644 --- a/packages/bottender/src/__tests__/getSessionStore.spec.ts +++ b/packages/bottender/src/shared/__tests__/getSessionStore.spec.ts @@ -1,9 +1,9 @@ -import MemorySessionStore from '../session/MemorySessionStore'; -import getBottenderConfig from '../shared/getBottenderConfig'; +import MemorySessionStore from '../../session/MemorySessionStore'; +import getBottenderConfig from '../getBottenderConfig'; import getSessionStore from '../getSessionStore'; -import { SessionDriver } from '../types'; +import { SessionDriver } from '../../types'; -jest.mock('../shared/getBottenderConfig'); +jest.mock('../getBottenderConfig'); const getBottenderConfigMocked = getBottenderConfig as jest.MockedFunction< typeof getBottenderConfig diff --git a/packages/bottender/src/shared/getChannelBotAndRequestContext.ts b/packages/bottender/src/shared/getChannelBotAndRequestContext.ts new file mode 100644 index 000000000..c9577a3cb --- /dev/null +++ b/packages/bottender/src/shared/getChannelBotAndRequestContext.ts @@ -0,0 +1,54 @@ +import url from 'url'; +import { IncomingMessage } from 'http'; + +import fromEntries from 'object.fromentries'; +import { match } from 'path-to-regexp'; + +import { ChannelBot, RequestContext } from '..'; + +import getChannelBots from './getChannelBots'; + +function getChannelBotAndRequestContext(req: IncomingMessage): + | { + requestContext: RequestContext; + channelBot: ChannelBot; + } + | undefined { + // TODO: add proxy support in Bottender to apply X-Forwarded-Host and X-Forwarded-Proto + // conditionally instead of replying on express. + const hostname = (req as any).hostname || req.headers.host; + const protocol = (req as any).protocol || 'https'; + + const requestUrl = `${protocol}://${hostname}${req.url}`; + + const { pathname, searchParams } = new url.URL(requestUrl); + + const query = fromEntries(searchParams.entries()); + + const channelBots = getChannelBots(); + for (let i = 0; i < channelBots.length; i++) { + const channelBot = channelBots[i]; + const matchPath = match>(channelBot.webhookPath); + const matchResult = matchPath(pathname); + + if (matchResult) { + return { + requestContext: { + id: (req as any).id, + method: req.method as string, + path: pathname, + query, + headers: req.headers, + rawBody: (req as any).rawBody, + body: (req as any).body, + params: matchResult.params, + url: requestUrl, + }, + channelBot, + }; + } + } + return undefined; +} + +export default getChannelBotAndRequestContext; diff --git a/packages/bottender/src/shared/getChannelBots.ts b/packages/bottender/src/shared/getChannelBots.ts new file mode 100644 index 000000000..cf4868961 --- /dev/null +++ b/packages/bottender/src/shared/getChannelBots.ts @@ -0,0 +1,112 @@ +import path from 'path'; + +import invariant from 'invariant'; +import { merge } from 'lodash'; +import { pascalcase } from 'messaging-api-common'; + +import { + Action, + Bot, + BottenderConfig, + ChannelBot, + Plugin, + getSessionStore, +} from '..'; + +import getBottenderConfig from './getBottenderConfig'; + +let channelBots: ChannelBot[] = []; + +export function cleanChannelBots(): void { + channelBots = []; +} + +function getChannelBots(): ChannelBot[] { + if (channelBots.length > 0) { + return channelBots; + } + + const bottenderConfig = getBottenderConfig(); + + const { + initialState, + plugins, + channels = {}, + } = merge(bottenderConfig /* , config */) as BottenderConfig; + + const sessionStore = getSessionStore(); + + // TODO: refine handler entry, improve error message and hint + // eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires + const Entry: Action = require(path.resolve('index.js')); + let ErrorEntry: Action; + try { + // eslint-disable-next-line import/no-dynamic-require + ErrorEntry = require(path.resolve('_error.js')); + } catch (err) {} // eslint-disable-line no-empty + + function initializeBot(bot: Bot): void { + if (initialState) { + bot.setInitialState(initialState); + } + + if (plugins) { + plugins.forEach((plugin: Plugin) => { + bot.use(plugin); + }); + } + + bot.onEvent(Entry); + if (ErrorEntry) { + bot.onError(ErrorEntry); + } + } + + channelBots = (Object.entries(channels) as [string, any][]) + .filter(([, { enabled }]) => enabled) + .map( + ([ + channel, + { path: webhookPath, sync, onRequest, connector, ...connectorConfig }, + ]) => { + let channelConnector; + if ( + [ + 'messenger', + 'line', + 'telegram', + 'slack', + 'viber', + 'whatsapp', + ].includes(channel) + ) { + // eslint-disable-next-line import/no-dynamic-require + const ChannelConnector = require(`../${channel}/${pascalcase( + channel + )}Connector`).default; + channelConnector = new ChannelConnector(connectorConfig); + } else { + invariant(connector, `The connector of ${channel} is missing.`); + channelConnector = connector; + } + + const channelBot = new Bot({ + sessionStore, + sync, + onRequest, + connector: channelConnector, + }) as Bot; + + initializeBot(channelBot); + + return { + webhookPath: webhookPath || `/webhooks/${channel}`, + bot: channelBot, + }; + } + ); + + return channelBots; +} + +export default getChannelBots; diff --git a/packages/bottender/src/shared/getChannelSchema.ts b/packages/bottender/src/shared/getChannelSchema.ts index 0475ef2a2..56122d287 100644 --- a/packages/bottender/src/shared/getChannelSchema.ts +++ b/packages/bottender/src/shared/getChannelSchema.ts @@ -41,10 +41,7 @@ const messengerSchema = Joi.object().keys({ .max(3) .when('composerInputDisabled', { is: true, - then: Joi.array() - .items(menuItemSchema) - .max(3) - .required(), + then: Joi.array().items(menuItemSchema).max(3).required(), }), }) ), diff --git a/packages/bottender/src/getClient.ts b/packages/bottender/src/shared/getClient.ts similarity index 75% rename from packages/bottender/src/getClient.ts rename to packages/bottender/src/shared/getClient.ts index a75eba3bf..58410e351 100644 --- a/packages/bottender/src/getClient.ts +++ b/packages/bottender/src/shared/getClient.ts @@ -4,16 +4,17 @@ import { SlackOAuthClient } from 'messaging-api-slack'; import { TelegramClient } from 'messaging-api-telegram'; import { ViberClient } from 'messaging-api-viber'; -import LineBot from './line/LineBot'; -import MessengerBot from './messenger/MessengerBot'; -import SlackBot from './slack/SlackBot'; -import TelegramBot from './telegram/TelegramBot'; -import TwilioClient from './whatsapp/TwilioClient'; -import ViberBot from './viber/ViberBot'; -import WhatsappBot from './whatsapp/WhatsappBot'; -import getBottenderConfig from './shared/getBottenderConfig'; +import LineBot from '../line/LineBot'; +import MessengerBot from '../messenger/MessengerBot'; +import SlackBot from '../slack/SlackBot'; +import TelegramBot from '../telegram/TelegramBot'; +import TwilioClient from '../whatsapp/TwilioClient'; +import ViberBot from '../viber/ViberBot'; +import WhatsappBot from '../whatsapp/WhatsappBot'; +import { Channel } from '../types'; + +import getBottenderConfig from './getBottenderConfig'; import getSessionStore from './getSessionStore'; -import { Channel } from './types'; const BOT_MAP = { messenger: MessengerBot, diff --git a/packages/bottender/src/shared/getConsoleBot.ts b/packages/bottender/src/shared/getConsoleBot.ts new file mode 100644 index 000000000..dc21ba6a1 --- /dev/null +++ b/packages/bottender/src/shared/getConsoleBot.ts @@ -0,0 +1,55 @@ +import path from 'path'; + +import { merge } from 'lodash'; + +import ConsoleBot from '../console/ConsoleBot'; +import { Action, Bot, BottenderConfig, Plugin, getSessionStore } from '..'; + +import getBottenderConfig from './getBottenderConfig'; + +function getConsoleBot(): ConsoleBot { + const bottenderConfig = getBottenderConfig(); + + const { initialState, plugins } = merge( + bottenderConfig /* , config */ + ) as BottenderConfig; + + const sessionStore = getSessionStore(); + + // TODO: refine handler entry, improve error message and hint + // eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires + const Entry: Action = require(path.resolve('index.js')); + let ErrorEntry: Action; + try { + // eslint-disable-next-line import/no-dynamic-require + ErrorEntry = require(path.resolve('_error.js')); + } catch (err) {} // eslint-disable-line no-empty + + function initializeBot(bot: Bot): void { + if (initialState) { + bot.setInitialState(initialState); + } + + if (plugins) { + plugins.forEach((plugin: Plugin) => { + bot.use(plugin); + }); + } + + bot.onEvent(Entry); + if (ErrorEntry) { + bot.onError(ErrorEntry); + } + } + + const bot = new ConsoleBot({ + fallbackMethods: true, + sessionStore, + }); + + initializeBot(bot); + + return bot; +} + +export default getConsoleBot; diff --git a/packages/bottender/src/getSessionStore.ts b/packages/bottender/src/shared/getSessionStore.ts similarity index 66% rename from packages/bottender/src/getSessionStore.ts rename to packages/bottender/src/shared/getSessionStore.ts index d4e5deae4..d09891358 100644 --- a/packages/bottender/src/getSessionStore.ts +++ b/packages/bottender/src/shared/getSessionStore.ts @@ -1,7 +1,8 @@ import warning from 'warning'; -import SessionStore from './session/SessionStore'; -import getBottenderConfig from './shared/getBottenderConfig'; +import SessionStore from '../session/SessionStore'; + +import getBottenderConfig from './getBottenderConfig'; function getSessionStore(): SessionStore { const { session } = getBottenderConfig(); @@ -12,8 +13,8 @@ function getSessionStore(): SessionStore { switch (sessionDriver) { case 'memory': { - const MemorySessionStore = require('./session/MemorySessionStore') - .default; + const MemorySessionStore = + require('../session/MemorySessionStore').default; return new MemorySessionStore( storesConfig.memory || {}, @@ -21,7 +22,7 @@ function getSessionStore(): SessionStore { ); } case 'file': { - const FileSessionStore = require('./session/FileSessionStore').default; + const FileSessionStore = require('../session/FileSessionStore').default; return new FileSessionStore( storesConfig.file || {}, @@ -29,7 +30,7 @@ function getSessionStore(): SessionStore { ); } case 'mongo': { - const MongoSessionStore = require('./session/MongoSessionStore').default; + const MongoSessionStore = require('../session/MongoSessionStore').default; return new MongoSessionStore( storesConfig.mongo || {}, @@ -37,7 +38,7 @@ function getSessionStore(): SessionStore { ); } case 'redis': { - const RedisSessionStore = require('./session/RedisSessionStore').default; + const RedisSessionStore = require('../session/RedisSessionStore').default; return new RedisSessionStore( storesConfig.redis || {}, @@ -46,9 +47,9 @@ function getSessionStore(): SessionStore { } default: { // Support custom session stores by returning the session store instance - const customSessionStore: - | SessionStore - | undefined = (storesConfig as any)[sessionDriver]; + const customSessionStore: SessionStore | undefined = ( + storesConfig as any + )[sessionDriver]; if (customSessionStore) { return customSessionStore; } @@ -57,8 +58,8 @@ function getSessionStore(): SessionStore { false, `Received unknown driver: ${sessionDriver}, so fallback it to \`memory\` driver.` ); - const MemorySessionStore = require('./session/MemorySessionStore') - .default; + const MemorySessionStore = + require('../session/MemorySessionStore').default; return new MemorySessionStore( storesConfig.memory || {}, diff --git a/packages/bottender/src/shared/getWebhookFromNgrok.ts b/packages/bottender/src/shared/getWebhookFromNgrok.ts index 79b7aec01..1f9883cf6 100644 --- a/packages/bottender/src/shared/getWebhookFromNgrok.ts +++ b/packages/bottender/src/shared/getWebhookFromNgrok.ts @@ -50,7 +50,7 @@ export default async function getWebhookFromNgrok( throw new Error('Failed to get tunnels from ngrok'); } - const httpsTunnel = data.tunnels.find(tunnel => tunnel.proto === 'https'); + const httpsTunnel = data.tunnels.find((tunnel) => tunnel.proto === 'https'); if (!httpsTunnel) { throw new Error('Can not find a https tunnel'); diff --git a/packages/bottender/src/slack/SlackBot.ts b/packages/bottender/src/slack/SlackBot.ts index ff308639c..f7cd26984 100644 --- a/packages/bottender/src/slack/SlackBot.ts +++ b/packages/bottender/src/slack/SlackBot.ts @@ -1,12 +1,13 @@ import { RTMClient } from '@slack/rtm-api'; import { SlackOAuthClient } from 'messaging-api-slack'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; -import SlackConnector, { SlackRequestBody } from './SlackConnector'; +import SlackConnector from './SlackConnector'; import SlackContext from './SlackContext'; import SlackEvent from './SlackEvent'; +import { SlackRequestBody } from './SlackTypes'; export default class SlackBot extends Bot< SlackRequestBody, @@ -25,6 +26,7 @@ export default class SlackBot extends Bot< origin, skipLegacyProfile, includeBotMessages, + onRequest, }: { accessToken: string; sessionStore?: SessionStore; @@ -34,6 +36,7 @@ export default class SlackBot extends Bot< origin?: string; skipLegacyProfile?: boolean; includeBotMessages?: boolean; + onRequest?: OnRequest; }) { const connector = new SlackConnector({ accessToken, @@ -43,7 +46,7 @@ export default class SlackBot extends Bot< skipLegacyProfile, includeBotMessages, }); - super({ connector, sessionStore, sync }); + super({ connector, sessionStore, sync, onRequest }); this._accessToken = accessToken; } diff --git a/packages/bottender/src/slack/SlackConnector.ts b/packages/bottender/src/slack/SlackConnector.ts index 8f7aa1464..d8e296e04 100644 --- a/packages/bottender/src/slack/SlackConnector.ts +++ b/packages/bottender/src/slack/SlackConnector.ts @@ -4,6 +4,7 @@ import { EventEmitter } from 'events'; import invariant from 'invariant'; import pProps from 'p-props'; import warning from 'warning'; +import { JsonObject } from 'type-fest'; import { SlackOAuthClient } from 'messaging-api-slack'; import { camelcaseKeysDeep } from 'messaging-api-common'; @@ -12,55 +13,40 @@ import { Connector } from '../bot/Connector'; import { RequestContext } from '../types'; import SlackContext from './SlackContext'; -import SlackEvent, { +import SlackEvent from './SlackEvent'; +import { BlockActionEvent, CommandEvent, - EventTypes, InteractiveMessageEvent, Message, SlackRawEvent, + SlackRequestBody, UIEvent, -} from './SlackEvent'; -// FIXME -export type SlackUser = { - id: string; -}; - -type EventsAPIBody = { - token: string; - teamId: string; - apiAppId: string; - type: EventTypes; - event: Message; - authedUsers: string[]; - eventId: string; - eventTime: number; -}; - -export type SlackRequestBody = EventsAPIBody | { payload: string }; +} from './SlackTypes'; -type CommonConstructorOptions = { +type CommonConnectorOptions = { skipLegacyProfile?: boolean; verificationToken?: string; signingSecret?: string; includeBotMessages?: boolean; }; -type ConstructorOptionsWithoutClient = { +type ConnectorOptionsWithoutClient = { accessToken: string; origin?: string; -} & CommonConstructorOptions; +} & CommonConnectorOptions; -type ConstructorOptionsWithClient = { +type ConnectorOptionsWithClient = { client: SlackOAuthClient; -} & CommonConstructorOptions; +} & CommonConnectorOptions; -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; +export type SlackConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; export default class SlackConnector - implements Connector { + implements Connector +{ _client: SlackOAuthClient; _verificationToken: string; @@ -71,7 +57,7 @@ export default class SlackConnector _includeBotMessages: boolean; - constructor(options: ConstructorOptions) { + constructor(options: SlackConnectorOptions) { const { verificationToken, skipLegacyProfile, @@ -88,7 +74,7 @@ export default class SlackConnector 'Slack access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' ); - this._client = SlackOAuthClient.connect({ + this._client = new SlackOAuthClient({ accessToken, origin, }); @@ -131,7 +117,7 @@ export default class SlackConnector return payload as BlockActionEvent; } // for RTM WebSocket messages and Slash Command messages - return (body as any) as Message; + return body as any as Message; } _isBotEventRequest(body: SlackRequestBody): boolean { @@ -142,7 +128,7 @@ export default class SlackConnector ); } - get platform(): string { + get platform(): 'slack' { return 'slack'; } @@ -150,10 +136,8 @@ export default class SlackConnector return this._client; } - getUniqueSessionKey(body: SlackRequestBody): string { - // FIXME: define types for every slack events - const rawEvent = this._getRawEventFromRequest(body) as any; - + // FIXME: define types for every slack events + _getChannelId(rawEvent: any): string | null { // For interactive_message format if ( rawEvent.channel && @@ -170,7 +154,10 @@ export default class SlackConnector // For slack modal if (rawEvent.view && rawEvent.view.privateMetadata) { - return JSON.parse(rawEvent.view.privateMetadata).channelId; + const channelId = JSON.parse(rawEvent.view.privateMetadata).channelId; + if (channelId) { + return channelId; + } } // For reaction_added format @@ -187,43 +174,52 @@ export default class SlackConnector ); } - async updateSession(session: Session, body: SlackRequestBody): Promise { - if (this._isBotEventRequest(body)) { - return; - } - - const rawEvent = this._getRawEventFromRequest(body); - let userFromBody; + _getUserId(rawEvent: SlackRawEvent): string | null { if ( rawEvent.type === 'interactive_message' || rawEvent.type === 'block_actions' || rawEvent.type === 'view_submission' || rawEvent.type === 'view_closed' ) { - userFromBody = (rawEvent as UIEvent).user.id; - } else { - userFromBody = - (rawEvent as Message).user || (rawEvent as CommandEvent).userId; + return (rawEvent as UIEvent).user.id; + } + return ( + (rawEvent as Message).user || + (rawEvent as CommandEvent).userId || + (rawEvent as UIEvent).user?.id + ); + } + + getUniqueSessionKey(body: SlackRequestBody): string | null { + const rawEvent = this._getRawEventFromRequest(body); + return this._getChannelId(rawEvent) || this._getUserId(rawEvent); + } + + async updateSession(session: Session, body: SlackRequestBody): Promise { + if (this._isBotEventRequest(body)) { + return; } + const rawEvent = this._getRawEventFromRequest(body); + const userId = this._getUserId(rawEvent); + const channelId = this._getChannelId(rawEvent); + if ( typeof session.user === 'object' && session.user && session.user.id && - session.user.id === userFromBody + session.user.id === userId ) { return; } - const channelId = this.getUniqueSessionKey(body); - const senderId = userFromBody; - if (!senderId) { + if (!userId) { return; } if (this._skipLegacyProfile) { session.user = { - id: senderId, + id: userId, _updatedAt: new Date().toISOString(), }; @@ -252,7 +248,7 @@ export default class SlackConnector } const promises: Record = { - sender: this._client.getUserInfo(senderId), + sender: this._client.getUserInfo(userId), }; // TODO: check join or leave events? @@ -260,12 +256,13 @@ export default class SlackConnector !session.channel || (session.channel.members && Array.isArray(session.channel.members) && - session.channel.members.indexOf(senderId) < 0) + session.channel.members.indexOf(userId) < 0) ) { - promises.channel = this._client.getConversationInfo(channelId); - promises.channelMembers = this._client.getAllConversationMembers( - channelId - ); + if (channelId) { + promises.channel = this._client.getConversationInfo(channelId); + promises.channelMembers = + this._client.getAllConversationMembers(channelId); + } } // TODO: how to know if user leave team? @@ -274,7 +271,7 @@ export default class SlackConnector !session.team || (session.team.members && Array.isArray(session.team.members) && - session.team.members.indexOf(senderId) < 0) + session.team.members.indexOf(userId) < 0) ) { promises.allUsers = this._client.getAllUserList(); } @@ -283,7 +280,7 @@ export default class SlackConnector // FIXME: refine user session.user = { - id: senderId, + id: userId, _updatedAt: new Date().toISOString(), ...results.sender, }; @@ -340,7 +337,7 @@ export default class SlackConnector createContext(params: { event: SlackEvent; session: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter?: EventEmitter | null; }): SlackContext { @@ -370,13 +367,14 @@ export default class SlackConnector }: { rawBody: string; signature: string; - timestamp: number; + timestamp: string; }): boolean { // ignore this request if the timestamp is 5 more minutes away from now const FIVE_MINUTES_IN_MILLISECONDS = 5 * 1000 * 60; if ( - Math.abs(Date.now() - timestamp * 1000) > FIVE_MINUTES_IN_MILLISECONDS + Math.abs(Date.now() - Number(timestamp) * 1000) > + FIVE_MINUTES_IN_MILLISECONDS ) { return false; } @@ -404,8 +402,8 @@ export default class SlackConnector rawBody, }: { method: string; - headers: Record; - query: Record; + headers: Record; + query: Record; rawBody: string; body: Record; }) { diff --git a/packages/bottender/src/slack/SlackContext.ts b/packages/bottender/src/slack/SlackContext.ts index a70d2a47a..091c67a78 100644 --- a/packages/bottender/src/slack/SlackContext.ts +++ b/packages/bottender/src/slack/SlackContext.ts @@ -1,20 +1,21 @@ import { EventEmitter } from 'events'; -import sleep from 'delay'; import warning from 'warning'; +import { JsonObject } from 'type-fest'; import { SlackOAuthClient, SlackTypes } from 'messaging-api-slack'; import Context from '../context/Context'; import Session from '../session/Session'; import { RequestContext } from '../types'; -import SlackEvent, { Message, UIEvent } from './SlackEvent'; +import SlackEvent from './SlackEvent'; +import { Message, UIEvent } from './SlackTypes'; -type Options = { +export type SlackContextOptions = { client: SlackOAuthClient; event: SlackEvent; session?: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter?: EventEmitter | null; }; @@ -65,7 +66,7 @@ export default class SlackContext extends Context< initialState, requestContext, emitter, - }: Options) { + }: SlackContextOptions) { super({ client, event, session, initialState, requestContext, emitter }); this.chat = { @@ -98,16 +99,6 @@ export default class SlackContext extends Context< return 'slack'; } - /** - * Delay and show indicators for milliseconds. - * - */ - async typing(milliseconds: number): Promise { - if (milliseconds > 0) { - await sleep(milliseconds); - } - } - // FIXME: this is to fix type checking _getChannelIdFromSession(callerMethodName = ''): string | null { if ( @@ -360,7 +351,7 @@ export default class SlackContext extends Context< ...options.view, privateMetadata: JSON.stringify({ original: options.view.privateMetadata, - channelId: (this._event.rawEvent as UIEvent).channel.id, + channelId: (this._event.rawEvent as UIEvent).channel?.id, }), }, }); diff --git a/packages/bottender/src/slack/SlackEvent.ts b/packages/bottender/src/slack/SlackEvent.ts index 57ed7d129..cc819ef1e 100644 --- a/packages/bottender/src/slack/SlackEvent.ts +++ b/packages/bottender/src/slack/SlackEvent.ts @@ -2,148 +2,21 @@ import { pascalCase } from 'pascal-case'; import { Event } from '../context/Event'; -// https://api.slack.com/events -export type EventTypes = - | 'message' - | 'app_uninstalled' - | 'channel_archive' - | 'channel_created' - | 'channel_deleted' - | 'channel_history_changed' - | 'channel_rename' - | 'channel_unarchive' - | 'dnd_updated' - | 'dnd_updated_user' - | 'email_domain_changed' - | 'emoji_changed' - | 'file_change' - | 'file_comment_added' - | 'file_comment_deleted' - | 'file_comment_edited' - | 'file_created' - | 'file_deleted' - | 'file_public' - | 'file_shared' - | 'file_unshared' - | 'grid_migration_finished' - | 'grid_migration_started' - | 'group_archive' - | 'group_close' - | 'group_history_changed' - | 'group_open' - | 'group_rename' - | 'group_unarchive' - | 'im_close' - | 'im_created' - | 'im_history_changed' - | 'im_open' - | 'link_shared' - | 'member_joined_channel' - | 'member_left_channel' - | 'pin_added' - | 'pin_removed' - | 'reaction_added' - | 'reaction_removed' - | 'star_added' - | 'star_removed' - | 'subteam_created' - | 'subteam_members_changed' - | 'subteam_self_added' - | 'subteam_self_removed' - | 'subteam_updated' - | 'team_domain_change' - | 'team_join' - | 'team_rename' - | 'tokens_revoked' - | 'url_verification' - | 'user_change'; - -// https://api.slack.com/reference/interaction-payloads -// https://api.slack.com/reference/interaction-payloads/shortcuts -export type InteractionTypes = - | 'interactive_message' - | 'block_actions' - | 'message_actions' - | 'view_closed' - | 'view_submission' - | 'shortcut'; - -export type Message = { - type: EventTypes; - subtype?: string; - channel: string; - user: string; - text: string; - ts: string; - threadTs?: string; - botId?: string; -}; - -export type UIEvent = { - actions: {}[]; - callbackId: string; - team: { - id: string; - domain: string; - }; - channel: { - id: string; - name: string; - }; - user: { - id: string; - name: string; - }; - actionTs: string; - messageTs: string; - attachmentId: string; - token: string; - originalMessage: Message; - responseUrl: string; - triggerId: string; - threadTs?: string; - botId?: string; -}; - -export type InteractiveMessageEvent = UIEvent & { - type: 'interactive_message'; -}; - -export type BlockActionEvent = UIEvent & { - type: 'block_actions'; -}; - -export type ViewEvent = UIEvent & { - type: 'view_submission' | 'view_closed'; -}; - -export type CommandEvent = { - type: string | null; - token: string; - teamId: string; - teamDomain: string; - channelId: string; - channelName: string; - userId: string; - userName: string; - command: string; - text: string; - responseUrl: string; - triggerId: string; -}; - -export type SlackRawEvent = - | Message - | InteractiveMessageEvent - | BlockActionEvent - | ViewEvent - | CommandEvent; +import { + CommandEvent, + InteractiveMessageEvent, + Message, + SlackRawEvent, +} from './SlackTypes'; export default class SlackEvent implements Event { _rawEvent: SlackRawEvent; + _timestamp: number; + constructor(rawEvent: SlackRawEvent) { this._rawEvent = rawEvent; + this._timestamp = Date.now(); } /** @@ -154,6 +27,22 @@ export default class SlackEvent implements Event { return this._rawEvent; } + /** + * The timestamp when the event was sent + * + */ + get timestamp(): number | undefined { + let timestamp: number | undefined; + if ('eventTs' in this._rawEvent && this._rawEvent.eventTs !== undefined) { + timestamp = Math.round(parseFloat(this._rawEvent.eventTs) * 1000); + } else if ('ts' in this._rawEvent) { + timestamp = Math.round(parseFloat(this._rawEvent.ts) * 1000); + } else { + timestamp = Math.round(this._timestamp); + } + return timestamp; + } + /** * Determine if the event is a message event. * @@ -388,7 +277,7 @@ const Events = [ 'user_change', ]; -Events.forEach(event => { +Events.forEach((event) => { Object.defineProperty(SlackEvent.prototype, `is${pascalCase(event)}`, { enumerable: false, configurable: true, diff --git a/packages/bottender/src/slack/SlackTypes.ts b/packages/bottender/src/slack/SlackTypes.ts new file mode 100644 index 000000000..297f2f0f6 --- /dev/null +++ b/packages/bottender/src/slack/SlackTypes.ts @@ -0,0 +1,155 @@ +export * from 'messaging-api-slack/dist/SlackTypes'; +export { SlackConnectorOptions } from './SlackConnector'; +export { SlackContextOptions } from './SlackContext'; + +// https://api.slack.com/events +export type EventTypes = + | '*' + | 'message' + | 'app_uninstalled' + | 'channel_archive' + | 'channel_created' + | 'channel_deleted' + | 'channel_history_changed' + | 'channel_rename' + | 'channel_unarchive' + | 'dnd_updated' + | 'dnd_updated_user' + | 'email_domain_changed' + | 'emoji_changed' + | 'file_change' + | 'file_comment_added' + | 'file_comment_deleted' + | 'file_comment_edited' + | 'file_created' + | 'file_deleted' + | 'file_public' + | 'file_shared' + | 'file_unshared' + | 'grid_migration_finished' + | 'grid_migration_started' + | 'group_archive' + | 'group_close' + | 'group_history_changed' + | 'group_open' + | 'group_rename' + | 'group_unarchive' + | 'im_close' + | 'im_created' + | 'im_history_changed' + | 'im_open' + | 'link_shared' + | 'member_joined_channel' + | 'member_left_channel' + | 'pin_added' + | 'pin_removed' + | 'reaction_added' + | 'reaction_removed' + | 'star_added' + | 'star_removed' + | 'subteam_created' + | 'subteam_members_changed' + | 'subteam_self_added' + | 'subteam_self_removed' + | 'subteam_updated' + | 'team_domain_change' + | 'team_join' + | 'team_rename' + | 'tokens_revoked' + | 'url_verification' + | 'user_change'; + +// https://api.slack.com/reference/interaction-payloads +// https://api.slack.com/reference/interaction-payloads/shortcuts +export type InteractionTypes = + | 'interactive_message' + | 'block_actions' + | 'message_actions' + | 'view_closed' + | 'view_submission' + | 'shortcut'; + +export type Message = { + type: EventTypes; + subtype?: string; + channel: string; + user: string; + text: string; + ts: string; + eventTs?: string; + threadTs?: string; + botId?: string; +}; + +export type UIEvent = { + actions: {}[]; + callbackId: string; + team: { + id: string; + domain: string; + }; + channel?: { + id: string; + name: string; + }; + user: { + id: string; + name: string; + }; + actionTs: string; + messageTs: string; + attachmentId: string; + token: string; + originalMessage: Message; + responseUrl: string; + triggerId: string; + threadTs?: string; + botId?: string; +}; + +export type InteractiveMessageEvent = UIEvent & { + type: 'interactive_message'; +}; + +export type BlockActionEvent = UIEvent & { + type: 'block_actions'; +}; + +export type ViewEvent = UIEvent & { + type: 'view_submission' | 'view_closed'; +}; + +export type CommandEvent = { + type: string | null; + token: string; + teamId: string; + teamDomain: string; + channelId: string; + channelName: string; + userId: string; + userName: string; + command: string; + text: string; + responseUrl: string; + triggerId: string; +}; + +export type SlackRawEvent = + | Message + | InteractiveMessageEvent + | BlockActionEvent + | ViewEvent + | CommandEvent; + +type EventsAPIBody = { + token: string; + teamId: string; + apiAppId: string; + type: EventTypes; + event: Message; + authedUsers: string[]; + eventId: string; + eventTime: number; +}; + +export type SlackRequestBody = EventsAPIBody | { payload: string }; diff --git a/packages/bottender/src/slack/__tests__/SlackBot.spec.ts b/packages/bottender/src/slack/__tests__/SlackBot.spec.ts index 380e64a24..d5da050cc 100644 --- a/packages/bottender/src/slack/__tests__/SlackBot.spec.ts +++ b/packages/bottender/src/slack/__tests__/SlackBot.spec.ts @@ -1,4 +1,5 @@ import { RTMClient } from '@slack/rtm-api'; +import { mocked } from 'ts-jest/utils'; import SlackBot from '../SlackBot'; import SlackConnector from '../SlackConnector'; @@ -13,6 +14,7 @@ it('should construct bot with SlackConnector', () => { const bot = new SlackBot({ accessToken: 'zzzzzZZZZZ', }); + expect(bot).toBeDefined(); expect(bot.onEvent).toBeDefined(); expect(bot.createRequestHandler).toBeDefined(); @@ -29,7 +31,7 @@ describe('createRtmRuntime', () => { const on = jest.fn(); const handler = jest.fn(); - RTMClient.mockImplementation(() => ({ + mocked(RTMClient).mockImplementation(() => ({ on, start, })); diff --git a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts index 5f3792978..3c265bba6 100644 --- a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts +++ b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts @@ -1,8 +1,10 @@ import { SlackOAuthClient } from 'messaging-api-slack'; +import { mocked } from 'ts-jest/utils'; import SlackConnector from '../SlackConnector'; import SlackContext from '../SlackContext'; import SlackEvent from '../SlackEvent'; +import { SlackRequestBody } from '../SlackTypes'; jest.mock('messaging-api-slack'); jest.mock('warning'); @@ -10,135 +12,800 @@ jest.mock('warning'); const accessToken = 'SLACK_accessTOKEN'; const SLACK_SIGNING_SECRET = '8f742231b10e8888abcd99yyyzzz85a5'; -const request = { - body: { - token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', - teamId: 'T02R00000', - apiAppId: 'A6A00000', - event: { +const request: SlackRequestBody = { + token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', + teamId: 'T02R00000', + apiAppId: 'A6A00000', + event: { + type: 'message', + user: 'U13A00000', + text: 'hello', + ts: '1500435914.425136', + channel: 'C6A900000', + eventTs: '1500435914.425136', + }, + type: 'event_callback', + authedUsers: ['U6AK00000'], + eventId: 'Ev6BEYTAK0', + eventTime: 1500435914, +}; + +const botRequest: SlackRequestBody = { + token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', + teamId: 'T02R00000', + apiAppId: 'A6A00000', + event: { + type: 'message', + user: 'U13A00000', + text: 'hello', + botId: 'B6AK00000', + ts: '1500435914.425136', + channel: 'C6A900000', + eventTs: '1500435914.425136', + }, + type: 'event_callback', + authedUsers: ['U6AK00000'], + eventId: 'Ev6BEYTAK0', + eventTime: 1500435914, +}; + +const ReactionAddedRequest: SlackRequestBody = { + token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', + teamId: 'T02R00000', + apiAppId: 'A6A00000', + event: { + type: 'reaction_added', + user: 'U024BE7LH', + reaction: 'thumbsup', + itemUser: 'U0G9QF9C6', + item: { type: 'message', - user: 'U13A00000', - text: 'hello', - ts: '1500435914.425136', - channel: 'C6A900000', - eventTs: '1500435914.425136', + channel: 'C0G9QF9GZ', + ts: '1360782400.498405', }, - type: 'event_callback', - authedUsers: ['U6AK00000'], - eventId: 'Ev6BEYTAK0', - eventTime: 1500435914, + eventTs: '1360782804.083113', }, + type: 'event_callback', + authedUsers: ['U6AK00000'], + eventId: 'Ev6BEYTAK0', + eventTime: 1500435914, }; -const botRequest = { - body: { - token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', - teamId: 'T02R00000', - apiAppId: 'A6A00000', - event: { - type: 'message', - user: 'U13A00000', - text: 'hello', - botId: 'B6AK00000', - ts: '1500435914.425136', - channel: 'C6A900000', - eventTs: '1500435914.425136', +const PinAddedRequest: SlackRequestBody = { + token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', + teamId: 'T02R00000', + apiAppId: 'A6A00000', + event: { + type: 'pin_added', + user: 'U024BE7LH', + channelId: 'C02ELGNBH', + item: {}, + eventTs: '1360782804.083113', + }, + type: 'event_callback', + authedUsers: ['U6AK00000'], + eventId: 'Ev6BEYTAK0', + eventTime: 1500435914, +}; + +const interactiveMessageRequest: SlackRequestBody = { + payload: + '{"type":"interactive_message","actions":[{"name":"game","type":"button","value":"chess"}],"callback_id":"wopr_game","team":{"id":"T056K3CM5","domain":"ricebug"},"channel":{"id":"D7WTL9ECE","name":"directmessage"},"user":{"id":"U056K3CN1","name":"tw0517tw"},"action_ts":"1511153911.446899","message_ts":"1511153905.000093","attachment_id":"1","token":"xxxxxxxxxxxxxxxxxxxxxxxx","is_app_unfurl":false,"original_message":{"type":"message","user":"U7W1PH7MY","text":"Would you like to play a game?","bot_id":"B7VUVQTK5","attachments":[{"callback_id":"wopr_game","fallback":"You are unable to choose a game","text":"Choose a game to play","id":1,"color":"3AA3E3","actions":[{"id":"1","name":"game","text":"Chess","type":"button","value":"chess","style":""},{"id":"2","name":"game","text":"Falken\'s Maze","type":"button","value":"maze","style":""},{"id":"3","name":"game","text":"Thermonuclear War","type":"button","value":"war","style":"danger","confirm":{"text":"Wouldn\'t you prefer a good game of chess?","title":"Are you sure?","ok_text":"Yes","dismiss_text":"No"}}]}],"ts":"1511153905.000093"},"response_url":"https:\\/\\/hooks.slack.com\\/actions\\/T056K3CM5\\/274366307953\\/73rSfbP0LcVPWfAYB3GicEdD","trigger_id":"274927463524.5223114719.95a5b9f6d3b30dc7e07dec6bfa4610e5"}', +}; + +// Home +const appHomeOpenedOnMessagesTabRequest: SlackRequestBody = { + type: 'app_home_opened', + user: 'U0HD00000', + channel: 'DQMT00000', + tab: 'messages', + eventTs: '1592278860.498134', +}; + +const appHomeOpenedOnHomeTabRequest: SlackRequestBody = { + type: 'app_home_opened', + user: 'U0HD00000', + channel: 'DQMT00000', + tab: 'home', + view: { + id: 'V0151K00000', + teamId: 'T0HD00000', + type: 'home', + blocks: [ + { + type: 'actions', + blockId: 'Rm7lr', + elements: [ + { + type: 'button', + actionId: 'zrZ', + text: { + type: 'plain_text', + text: 'value', + emoji: true, + }, + value: 'value', + }, + ], + }, + ], + privateMetadata: '', + callbackId: '', + state: { values: {} }, + hash: '1592278862.48a704aa', + title: { type: 'plain_text', text: 'View Title', emoji: true }, + clearOnClose: false, + notifyOnClose: false, + close: null, + submit: null, + previousViewId: null, + rootViewId: 'V0151K00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', + }, + eventTs: '1592278910.074390', +}; + +const blockActionsOnHomeTabRequest: SlackRequestBody = { + type: 'block_actions', + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + container: { type: 'view', viewId: 'V0151K00000' }, + triggerId: '1192207164308.17443231012.b30e1f98a3ac48b9171d3b859a0124a5', + team: { id: 'T0HD00000', domain: 'domain' }, + view: { + id: 'V0151K00000', + teamId: 'T0HD00000', + type: 'home', + blocks: [ + { + type: 'actions', + blockId: 'Rm7lr', + elements: [ + { + type: 'button', + actionId: 'zrZ', + text: { + type: 'plain_text', + text: 'value', + emoji: true, + }, + value: 'value', + }, + ], + }, + ], + privateMetadata: '{}', + callbackId: '', + state: { values: {} }, + hash: '1592278912.3f2f9db2', + title: { type: 'plain_text', text: 'View Title', emoji: true }, + clearOnClose: false, + notifyOnClose: false, + close: null, + submit: null, + previousViewId: null, + rootViewId: 'V0151K00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', + }, + actions: [ + { + actionId: 'zrZ', + blockId: 'Rm7lr', + text: { + type: 'plain_text', + text: 'value', + emoji: true, + }, + value: 'value', + type: 'button', + actionTs: '1592279552.931549', }, - type: 'event_callback', - authedUsers: ['U6AK00000'], - eventId: 'Ev6BEYTAK0', - eventTime: 1500435914, + ], +}; + +// Home Modal +const blockActionsOnHomeModalRequest: SlackRequestBody = { + type: 'block_actions', + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + container: { + type: 'view', + viewId: 'V015LD00000', + }, + triggerId: '1215686778640.17443231012.3a6962eea96e7dac6d2f1a907af6211e', + team: { + id: 'T0HD00000', + domain: 'domain', }, + view: { + id: 'V015LD00000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: '2C4D9', + elements: [ + { + type: 'plain_text', + text: 'text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'DyE', + elements: [ + { + type: 'button', + actionId: 'xQkxX', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + privateMetadata: '{}', + callbackId: '', + state: { + values: {}, + }, + hash: '1592476707.898fb949', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: false, + close: { + type: 'plain_text', + text: 'close', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, + }, + previousViewId: null, + rootViewId: 'V015LD00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', + }, + actions: [ + { + actionId: 'xQkxX', + blockId: 'DyE', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + type: 'button', + actionTs: '1592476752.557160', + }, + ], }; -const ReactionAddedRequest = { - body: { - token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', - teamId: 'T02R00000', - apiAppId: 'A6A00000', - event: { - type: 'reaction_added', - user: 'U024BE7LH', - reaction: 'thumbsup', - itemUser: 'U0G9QF9C6', - item: { - type: 'message', - channel: 'C0G9QF9GZ', - ts: '1360782400.498405', +const viewSubmissionOnHomeModalRequest = { + type: 'view_submission', + team: { + id: 'T0HD00000', + domain: 'domain', + }, + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + triggerId: '1204566923633.17443231012.da8c753637a7f27e40991411664d77a2', + view: { + id: 'V015N200000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: 'Umz', + elements: [ + { + type: 'plain_text', + text: 'text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'Sk7k8', + elements: [ + { + type: 'button', + actionId: '0L7ew', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], }, - eventTs: '1360782804.083113', + ], + privateMetadata: '{}', + callbackId: '', + state: { + values: {}, + }, + hash: '1592477178.60ca56c9', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: false, + close: { + type: 'plain_text', + text: 'close', + emoji: true, }, - type: 'event_callback', - authedUsers: ['U6AK00000'], - eventId: 'Ev6BEYTAK0', - eventTime: 1500435914, + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, + }, + previousViewId: null, + rootViewId: 'V015N200000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', }, + responseUrls: [], }; -const PinAddedRequest = { - body: { - token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', - teamId: 'T02R00000', - apiAppId: 'A6A00000', - event: { - type: 'pin_added', - user: 'U024BE7LH', - channelId: 'C02ELGNBH', - item: {}, - eventTs: '1360782804.083113', +const viewCloseOnHomeModalRequest = { + type: 'view_closed', + team: { + id: 'T0HD00000', + domain: 'domain', + }, + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + view: { + id: 'V0157600000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: 'WC8', + elements: [ + { + type: 'plain_text', + text: 'text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'ZbPy', + elements: [ + { + type: 'button', + actionId: 'muR', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + privateMetadata: '{}', + callbackId: '', + state: { + values: {}, }, - type: 'event_callback', - authedUsers: ['U6AK00000'], - eventId: 'Ev6BEYTAK0', - eventTime: 1500435914, + hash: '1592480265.1283b0b7', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: true, + close: { + type: 'plain_text', + text: 'close', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, + }, + previousViewId: null, + rootViewId: 'V0157600000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', }, + isCleared: true, }; -const interactiveMessageRequest = { - body: { - payload: - '{"type":"interactive_message","actions":[{"name":"game","type":"button","value":"chess"}],"callback_id":"wopr_game","team":{"id":"T056K3CM5","domain":"ricebug"},"channel":{"id":"D7WTL9ECE","name":"directmessage"},"user":{"id":"U056K3CN1","name":"tw0517tw"},"action_ts":"1511153911.446899","message_ts":"1511153905.000093","attachment_id":"1","token":"n8uIomPoBtc7fSnbHbQcmwdy","is_app_unfurl":false,"original_message":{"type":"message","user":"U7W1PH7MY","text":"Would you like to play a game?","bot_id":"B7VUVQTK5","attachments":[{"callback_id":"wopr_game","fallback":"You are unable to choose a game","text":"Choose a game to play","id":1,"color":"3AA3E3","actions":[{"id":"1","name":"game","text":"Chess","type":"button","value":"chess","style":""},{"id":"2","name":"game","text":"Falken\'s Maze","type":"button","value":"maze","style":""},{"id":"3","name":"game","text":"Thermonuclear War","type":"button","value":"war","style":"danger","confirm":{"text":"Wouldn\'t you prefer a good game of chess?","title":"Are you sure?","ok_text":"Yes","dismiss_text":"No"}}]}],"ts":"1511153905.000093"},"response_url":"https:\\/\\/hooks.slack.com\\/actions\\/T056K3CM5\\/274366307953\\/73rSfbP0LcVPWfAYB3GicEdD","trigger_id":"274927463524.5223114719.95a5b9f6d3b30dc7e07dec6bfa4610e5"}', +// Channel +const blockActionsOnChannelRequest: SlackRequestBody = { + type: 'block_actions', + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + container: { + type: 'message', + messageTs: '1592454263.013800', + channelId: 'DQMT00000', + isEphemeral: false, + }, + triggerId: '1215785565232.17443231012.2e89dfa2540e010b618894ff8d2de08f', + team: { + id: 'T0HD00000', + domain: 'domain', }, + channel: { + id: 'DQMT00000', + name: 'directmessage', + }, + message: { + botId: 'BQMT00000', + type: 'message', + text: "This content can't be displayed.", + user: 'UQKL00000', + ts: '1592454263.013800', + team: 'T0HD00000', + blocks: [ + { + type: 'actions', + blockId: 'nxyEU', + elements: [ + { + type: 'button', + actionId: 'bdj', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + }, + responseUrl: + 'https://hooks.slack.com/actions/T0HD00000/1190570555349/zCOfyY2sBayE3Amaj1GgXUtW', + actions: [ + { + actionId: 'bdj', + blockId: 'nxyEU', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + type: 'button', + actionTs: '1592480315.455021', + }, + ], }; -const viewSubmissionRequest = { - body: { - type: 'view_submission', - team: { id: 'T02RUPSBS', domain: 'yoctolinfo' }, - user: { - id: 'UCL2D708M', - username: 'darkbtf', - name: 'darkbtf', - teamId: 'T02RUPSBS', +// Channel Modal +const blockActionsOnChannelModalRequest: SlackRequestBody = { + type: 'block_actions', + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + container: { + type: 'view', + viewId: 'V016BP00000', + }, + triggerId: '1204575603665.17443231012.ccdd4f2d4b840eb48c451235f6b0a84c', + team: { + id: 'T0HD00000', + domain: 'domain', + }, + view: { + id: 'V016BP00000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: 'ikxi3', + elements: [ + { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'HWc', + elements: [ + { + type: 'button', + actionId: 'c3pJ', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + privateMetadata: '{"channelId":"DQMT00000"}', + callbackId: '', + state: { + values: {}, + }, + hash: '1592480318.25f7311a', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: true, + close: { + type: 'plain_text', + text: 'close', + emoji: true, }, - apiAppId: 'A604E7GSJ', - token: 'zBoHd4fjrvVcVuN9yTmlHMKC', - triggerId: '873508362498.2878808400.763026ca2acb11b3dfbcb85836d1c3d8', - view: { - id: 'VRQQ7JA4T', - teamId: 'T02RUPSBS', - type: 'modal', - blocks: [[Object]], - privateMetadata: '{"channelId":"C02ELGNBH"}', - callbackId: '截止', - state: { values: {} }, - hash: '1577340522.d58ea69f', - title: { type: 'plain_text', text: '確認截止?', emoji: true }, - clearOnClose: false, - notifyOnClose: false, - close: { type: 'plain_text', text: '取消', emoji: true }, - submit: { type: 'plain_text', text: '送出 :boom:', emoji: true }, - previousViewId: null, - rootViewId: 'VRQQ7JA4T', - appId: 'A604E7GSJ', - externalId: '', - appInstalledTeamId: 'T02RUPSBS', - botId: 'B618CBATV', + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, }, + previousViewId: null, + rootViewId: 'V016BP00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', }, + actions: [ + { + actionId: 'c3pJ', + blockId: 'HWc', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + type: 'button', + actionTs: '1592480408.122391', + }, + ], }; -const RtmMessage = { +const viewSubmissionOnChannelModalRequest: SlackRequestBody = { + type: 'view_submission', + team: { + id: 'T0HD00000', + domain: 'domain', + }, + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + triggerId: '1215789028880.17443231012.f9236c7807d6bdace1e7809e346b8e6c', + view: { + id: 'V016BP00000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: 'ikxi3', + elements: [ + { + type: 'plain_text', + text: 'text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'HWc', + elements: [ + { + type: 'button', + actionId: 'c3pJ', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + privateMetadata: '{"channelId":"DQMT00000"}', + callbackId: '', + state: { + values: {}, + }, + hash: '1592480318.25f7311a', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: true, + close: { + type: 'plain_text', + text: 'close', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, + }, + previousViewId: null, + rootViewId: 'V016BP00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', + }, + responseUrls: [], +}; + +const viewCloseOnChannelModalRequest: SlackRequestBody = { + type: 'view_closed', + team: { + id: 'T0HD00000', + domain: 'domain', + }, + user: { + id: 'U0HD00000', + username: 'username', + name: 'name', + teamId: 'T0HD00000', + }, + apiAppId: 'AQ8600000', + token: 'xxxxxxxxxxxxxxxxxxxxxxxx', + view: { + id: 'V015LG00000', + teamId: 'T0HD00000', + type: 'modal', + blocks: [ + { + type: 'context', + blockId: 'AQaXQ', + elements: [ + { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + ], + }, + { + type: 'actions', + blockId: 'QUo', + elements: [ + { + type: 'button', + actionId: 'E2G9D', + text: { + type: 'plain_text', + text: 'button text', + emoji: true, + }, + value: 'button value', + }, + ], + }, + ], + privateMetadata: '{"channelId":"DQMT00000"}', + callbackId: '', + state: { + values: {}, + }, + hash: '1592480467.1addf637', + title: { + type: 'plain_text', + text: 'title', + emoji: true, + }, + clearOnClose: false, + notifyOnClose: true, + close: { + type: 'plain_text', + text: 'close', + emoji: true, + }, + submit: { + type: 'plain_text', + text: 'submit', + emoji: true, + }, + previousViewId: null, + rootViewId: 'V015LG00000', + appId: 'AQ8600000', + externalId: '', + appInstalledTeamId: 'T0HD00000', + botId: 'BQMT00000', + }, + isCleared: false, +}; + +const RtmMessage: SlackRequestBody = { type: 'message', channel: 'G7W5WAAAA', user: 'U056KAAAA', @@ -148,7 +815,7 @@ const RtmMessage = { team: 'T056KAAAA', }; -const slashCommandMessage = { +const slashCommandMessage: SlackRequestBody = { token: 'xxxxxxxxxxxxxxxxxxxxxxxx', teamId: 'T056K0000', teamDomain: 'domain', @@ -169,23 +836,19 @@ function setup({ skipLegacyProfile, includeBotMessages, } = {}) { - const mockSlackOAuthClient = { - getUserInfo: jest.fn(), - getConversationInfo: jest.fn(), - getAllConversationMembers: jest.fn(), - getAllUserList: jest.fn(), - }; - SlackOAuthClient.connect = jest.fn(); - SlackOAuthClient.connect.mockReturnValue(mockSlackOAuthClient); + const connector = new SlackConnector({ + accessToken, + signingSecret, + verificationToken, + skipLegacyProfile, + includeBotMessages, + }); + + const client = mocked(SlackOAuthClient).mock.instances[0]; + return { - connector: new SlackConnector({ - accessToken, - signingSecret, - verificationToken, - skipLegacyProfile, - includeBotMessages, - }), - mockSlackOAuthClient, + connector, + client, }; } @@ -198,72 +861,178 @@ describe('#platform', () => { describe('#client', () => { it('should be client', () => { - const { connector, mockSlackOAuthClient } = setup(); - expect(connector.client).toBe(mockSlackOAuthClient); + const { connector, client } = setup(); + expect(connector.client).toBe(client); }); it('support custom client', () => { - const client = {}; + const client = new SlackOAuthClient({ + accessToken, + }); + const connector = new SlackConnector({ client }); + expect(connector.client).toBe(client); }); }); describe('#getUniqueSessionKey', () => { - it('extract correct channel id', () => { + it('extract correct session key', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(request.body); - expect(channelId).toBe('C6A900000'); + + const sessionKey = connector.getUniqueSessionKey(request); + + expect(sessionKey).toBe('C6A900000'); }); - it('extract correct channel id from interactive message request', () => { + it('extract correct session key from interactive message request', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey( - interactiveMessageRequest.body + + const sessionKey = connector.getUniqueSessionKey(interactiveMessageRequest); + + expect(sessionKey).toBe('D7WTL9ECE'); + }); + + it('extract correct session key from RTM WebSocket message', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey(RtmMessage); + + expect(sessionKey).toBe('G7W5WAAAA'); + }); + + it('extract correct session key from reaction_added event', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey(ReactionAddedRequest); + + expect(sessionKey).toBe('C0G9QF9GZ'); + }); + + it('extract correct session key from pin_added event', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey(PinAddedRequest); + + expect(sessionKey).toBe('C02ELGNBH'); + }); + + it('extract correct session key from slash command', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey(slashCommandMessage); + + expect(sessionKey).toBe('G7W5W0000'); + }); + + // home tab + it('extract correct session key from appHomeOpenedOnMessagesTabRequest', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey( + appHomeOpenedOnMessagesTabRequest ); - expect(channelId).toBe('D7WTL9ECE'); + + expect(sessionKey).toBe('DQMT00000'); }); - it('extract correct channel id from RTM WebSocket message', () => { + it('extract correct session key from appHomeOpenedOnHomeTabRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(RtmMessage); - expect(channelId).toBe('G7W5WAAAA'); + + const sessionKey = connector.getUniqueSessionKey( + appHomeOpenedOnHomeTabRequest + ); + + expect(sessionKey).toBe('DQMT00000'); }); - it('extract correct channel id from reaction_added event', () => { + it('extract correct session key from blockActionsOnHomeTabRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(ReactionAddedRequest.body); - expect(channelId).toBe('C0G9QF9GZ'); + + const sessionKey = connector.getUniqueSessionKey( + blockActionsOnHomeTabRequest + ); + + expect(sessionKey).toBe('U0HD00000'); }); - it('extract correct channel id from pin_added event', () => { + // home modal + it('extract correct session key from blockActionsOnHomeModalRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(PinAddedRequest.body); - expect(channelId).toBe('C02ELGNBH'); + + const sessionKey = connector.getUniqueSessionKey( + blockActionsOnHomeModalRequest + ); + + expect(sessionKey).toBe('U0HD00000'); }); - it('extract correct channel id from slash command', () => { + it('extract correct session key from viewSubmissionOnHomeModalRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(slashCommandMessage); - expect(channelId).toBe('G7W5W0000'); + + const sessionKey = connector.getUniqueSessionKey( + viewSubmissionOnHomeModalRequest + ); + + expect(sessionKey).toBe('U0HD00000'); }); - it('extract correct channel id from slash command', () => { + it('extract correct session key from viewCloseOnHomeModalRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(slashCommandMessage); - expect(channelId).toBe('G7W5W0000'); + + const sessionKey = connector.getUniqueSessionKey( + viewCloseOnHomeModalRequest + ); + + expect(sessionKey).toBe('U0HD00000'); }); - it("extract correct channel id from view event's private_metadata", () => { + // channel + it('extract correct session key from blockActionsOnChannelRequest', () => { const { connector } = setup(); - const channelId = connector.getUniqueSessionKey(viewSubmissionRequest.body); - expect(channelId).toBe('C02ELGNBH'); + + const sessionKey = connector.getUniqueSessionKey( + blockActionsOnChannelRequest + ); + + expect(sessionKey).toBe('DQMT00000'); + }); + + // channel modal + it('extract correct session key from blockActionsOnChannelModalRequest', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey( + blockActionsOnChannelModalRequest + ); + + expect(sessionKey).toBe('DQMT00000'); + }); + + it('extract correct session key from viewSubmissionOnChannelModalRequest', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey( + viewSubmissionOnChannelModalRequest + ); + + expect(sessionKey).toBe('DQMT00000'); + }); + + it('extract correct session key from viewCloseOnChannelModalRequest', () => { + const { connector } = setup(); + + const sessionKey = connector.getUniqueSessionKey( + viewCloseOnChannelModalRequest + ); + + expect(sessionKey).toBe('DQMT00000'); }); }); describe('#updateSession', () => { it('update session with data needed', async () => { - const { connector, mockSlackOAuthClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); @@ -276,21 +1045,17 @@ describe('#updateSession', () => { const members = [user]; const session = {}; - mockSlackOAuthClient.getUserInfo.mockResolvedValue(user); - mockSlackOAuthClient.getConversationInfo.mockResolvedValue(channel); - mockSlackOAuthClient.getAllConversationMembers.mockResolvedValue(members); - mockSlackOAuthClient.getAllUserList.mockResolvedValue(members); + mocked(client.getUserInfo).mockResolvedValue(user); + mocked(client.getConversationInfo).mockResolvedValue(channel); + mocked(client.getAllConversationMembers).mockResolvedValue(members); + mocked(client.getAllUserList).mockResolvedValue(members); - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockSlackOAuthClient.getUserInfo).toBeCalledWith('U13A00000'); - expect(mockSlackOAuthClient.getConversationInfo).toBeCalledWith( - 'C6A900000' - ); - expect(mockSlackOAuthClient.getAllConversationMembers).toBeCalledWith( - 'C6A900000' - ); - expect(mockSlackOAuthClient.getAllUserList).toBeCalled(); + expect(client.getUserInfo).toBeCalledWith('U13A00000'); + expect(client.getConversationInfo).toBeCalledWith('C6A900000'); + expect(client.getAllConversationMembers).toBeCalledWith('C6A900000'); + expect(client.getAllUserList).toBeCalled(); expect(session).toEqual({ user: { _updatedAt: expect.any(String), @@ -306,23 +1071,23 @@ describe('#updateSession', () => { }); it('not update session if it is bot event request', async () => { - const { connector, mockSlackOAuthClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); const session = {}; - await connector.updateSession(session, botRequest.body); + await connector.updateSession(session, botRequest); - expect(mockSlackOAuthClient.getUserInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getConversationInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllConversationMembers).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllUserList).not.toBeCalled(); + expect(client.getUserInfo).not.toBeCalled(); + expect(client.getConversationInfo).not.toBeCalled(); + expect(client.getAllConversationMembers).not.toBeCalled(); + expect(client.getAllUserList).not.toBeCalled(); expect(session).toEqual({}); }); it('not update session if no senderId in body', async () => { - const { connector, mockSlackOAuthClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); @@ -347,14 +1112,14 @@ describe('#updateSession', () => { await connector.updateSession(session, body); - expect(mockSlackOAuthClient.getUserInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getConversationInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllConversationMembers).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllUserList).not.toBeCalled(); + expect(client.getUserInfo).not.toBeCalled(); + expect(client.getConversationInfo).not.toBeCalled(); + expect(client.getAllConversationMembers).not.toBeCalled(); + expect(client.getAllUserList).not.toBeCalled(); }); it('update session with data needed when receiving interactive message request', async () => { - const { connector, mockSlackOAuthClient } = setup({ + const { connector, client } = setup({ skipLegacyProfile: false, }); @@ -367,21 +1132,17 @@ describe('#updateSession', () => { const members = [user]; const session = {}; - mockSlackOAuthClient.getUserInfo.mockResolvedValue(user); - mockSlackOAuthClient.getConversationInfo.mockResolvedValue(channel); - mockSlackOAuthClient.getAllConversationMembers.mockResolvedValue(members); - mockSlackOAuthClient.getAllUserList.mockResolvedValue(members); + mocked(client.getUserInfo).mockResolvedValue(user); + mocked(client.getConversationInfo).mockResolvedValue(channel); + mocked(client.getAllConversationMembers).mockResolvedValue(members); + mocked(client.getAllUserList).mockResolvedValue(members); - await connector.updateSession(session, interactiveMessageRequest.body); + await connector.updateSession(session, interactiveMessageRequest); - expect(mockSlackOAuthClient.getUserInfo).toBeCalledWith('U056K3CN1'); - expect(mockSlackOAuthClient.getConversationInfo).toBeCalledWith( - 'D7WTL9ECE' - ); - expect(mockSlackOAuthClient.getAllConversationMembers).toBeCalledWith( - 'D7WTL9ECE' - ); - expect(mockSlackOAuthClient.getAllUserList).toBeCalled(); + expect(client.getUserInfo).toBeCalledWith('U056K3CN1'); + expect(client.getConversationInfo).toBeCalledWith('D7WTL9ECE'); + expect(client.getAllConversationMembers).toBeCalledWith('D7WTL9ECE'); + expect(client.getAllUserList).toBeCalled(); expect(session).toEqual({ user: { _updatedAt: expect.any(String), @@ -397,16 +1158,16 @@ describe('#updateSession', () => { }); it('update session without calling apis while skipLegacyProfile set true', async () => { - const { connector, mockSlackOAuthClient } = setup(); + const { connector, client } = setup(); const session = {}; - await connector.updateSession(session, request.body); + await connector.updateSession(session, request); - expect(mockSlackOAuthClient.getUserInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getConversationInfo).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllConversationMembers).not.toBeCalled(); - expect(mockSlackOAuthClient.getAllUserList).not.toBeCalled(); + expect(client.getUserInfo).not.toBeCalled(); + expect(client.getConversationInfo).not.toBeCalled(); + expect(client.getAllConversationMembers).not.toBeCalled(); + expect(client.getAllUserList).not.toBeCalled(); expect(session).toEqual({ user: { _updatedAt: expect.any(String), @@ -423,7 +1184,7 @@ describe('#updateSession', () => { describe('#mapRequestToEvents', () => { it('should map request to SlackEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(request.body); + const events = connector.mapRequestToEvents(request); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(SlackEvent); @@ -431,7 +1192,7 @@ describe('#mapRequestToEvents', () => { it('should not include bot message by default', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(botRequest.body); + const events = connector.mapRequestToEvents(botRequest); expect(events).toHaveLength(0); }); @@ -440,7 +1201,7 @@ describe('#mapRequestToEvents', () => { const { connector } = setup({ includeBotMessages: true, }); - const events = connector.mapRequestToEvents(botRequest.body); + const events = connector.mapRequestToEvents(botRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(SlackEvent); @@ -448,7 +1209,7 @@ describe('#mapRequestToEvents', () => { it('should include callbackId when request is a interactiveMessageRequest', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(interactiveMessageRequest.body); + const events = connector.mapRequestToEvents(interactiveMessageRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(SlackEvent); diff --git a/packages/bottender/src/slack/__tests__/SlackContext.spec.ts b/packages/bottender/src/slack/__tests__/SlackContext.spec.ts index 0d575ea2c..b7fe2d4cc 100644 --- a/packages/bottender/src/slack/__tests__/SlackContext.spec.ts +++ b/packages/bottender/src/slack/__tests__/SlackContext.spec.ts @@ -1,12 +1,11 @@ -jest.mock('delay'); -// jest.mock('messaging-api-slack'); -jest.mock('warning'); +import warning from 'warning'; +import { SlackOAuthClient as SlackClient } from 'messaging-api-slack'; + +import SlackContext from '../SlackContext'; +import SlackEvent from '../SlackEvent'; -let SlackClient; -let SlackContext; -let SlackEvent; -let sleep; -let warning; +jest.mock('messaging-api-slack'); +jest.mock('warning'); const VIEW_PAYLOAD = { id: 'VMHU10V25', @@ -47,16 +46,6 @@ const VIEW_PAYLOAD = { notifyOnClose: false, }; -beforeEach(() => { - /* eslint-disable global-require */ - SlackClient = require('messaging-api-slack').SlackOAuthClient; - SlackContext = require('../SlackContext').default; - SlackEvent = require('../SlackEvent').default; - sleep = require('delay'); - warning = require('warning'); - /* eslint-enable global-require */ -}); - const messageRawEvent = { type: 'message', user: 'U13AGSN1X', @@ -88,7 +77,7 @@ const userSession = { const setup = ({ session: _session, rawEvent: _rawEvent } = {}) => { const session = _session === undefined ? userSession : _session; const rawEvent = _rawEvent === undefined ? messageRawEvent : _rawEvent; - const client = SlackClient.connect(); + const client = new SlackClient(); client.chat = { postMessage: jest.fn(), @@ -112,12 +101,12 @@ const setup = ({ session: _session, rawEvent: _rawEvent } = {}) => { update: jest.fn(), }; - const args = { + const context = new SlackContext({ client, event: new SlackEvent(rawEvent), session, - }; - const context = new SlackContext(args); + }); + return { context, session, @@ -125,11 +114,6 @@ const setup = ({ session: _session, rawEvent: _rawEvent } = {}) => { }; }; -it('be defined', () => { - const { context } = setup(); - expect(context).toBeDefined(); -}); - it('#platform to be `slack`', () => { const { context } = setup(); expect(context.platform).toBe('slack'); @@ -522,21 +506,3 @@ describe('#views.update', () => { }); }); }); - -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context } = setup(); - - await context.typing(10); - - expect(sleep).toBeCalled(); - }); -}); diff --git a/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts b/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts index 77f88f044..9d2c0efad 100644 --- a/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts +++ b/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts @@ -1,3 +1,5 @@ +import MockDate from 'mockdate'; + import SlackEvent from '../SlackEvent'; const appUninstalled = { @@ -585,6 +587,17 @@ it('#rawEvent', () => { expect(new SlackEvent(message).rawEvent).toEqual(message); }); +it('#timestamp', () => { + MockDate.set('2020-08-17'); // 1597622400 + expect(new SlackEvent(message).timestamp).toEqual(1355517523000); + expect(new SlackEvent(groupOpen).timestamp).toEqual(1597622400000); + expect(new SlackEvent(pinAdded).timestamp).toEqual(1360782804083); + expect(new SlackEvent(channelHistoryChanged).timestamp).toEqual( + 1361482916000 + ); + MockDate.reset(); +}); + it('#isAppUninstalled', () => { expect(new SlackEvent(appUninstalled).isAppUninstalled).toEqual(true); expect(new SlackEvent(message).isAppUninstalled).toEqual(false); diff --git a/packages/bottender/src/slack/__tests__/routes.spec.ts b/packages/bottender/src/slack/__tests__/routes.spec.ts index dc06ced72..19fcec170 100644 --- a/packages/bottender/src/slack/__tests__/routes.spec.ts +++ b/packages/bottender/src/slack/__tests__/routes.spec.ts @@ -169,7 +169,7 @@ async function expectRouteNotMatchSlackEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } @@ -232,6 +232,24 @@ describe('#slack', () => { }); describe('#slack.event', () => { + it('should call action when it receives any slack event event', async () => { + await expectRouteMatchSlackEvent({ + route: slack.event('*', Action), + event: slackEventPinAdded, + }); + await expectRouteMatchSlackEvent({ + route: slack.event('*', Action), + event: slackEventTextMessage, + }); + }); + + it('should not call action when it receives a non-event event', async () => { + await expectRouteNotMatchSlackEvent({ + route: slack.event('*', Action), + event: slackEventSlashCommand, + }); + }); + it('should call action when it receives a slack event event', async () => { await expectRouteMatchSlackEvent({ route: slack.event('pin_added', Action), @@ -252,14 +270,47 @@ describe('#slack', () => { event: slackEventTextMessage, }); }); + + it('should not call action when it receives a command event', async () => { + await expectRouteNotMatchSlackEvent({ + route: slack.event('pin_added', Action), + event: slackEventSlashCommand, + }); + }); }); describe('#slack.command', () => { + it('should call action when it receives any slack slash command event', async () => { + await expectRouteMatchSlackEvent({ + route: slack.command('*', Action), + event: slackEventSlashCommand, + }); + }); + + it('should not call action when it receives a event event', async () => { + await expectRouteNotMatchSlackEvent({ + route: slack.command('*', Action), + event: slackEventTextMessage, + }); + }); + it('should call action when it receives a slack slash command event', async () => { await expectRouteMatchSlackEvent({ route: slack.command('/weather', Action), event: slackEventSlashCommand, }); }); + it("should not call action when it receives a slack slash command event doesn't match the name", async () => { + await expectRouteNotMatchSlackEvent({ + route: slack.command('/others', Action), + event: slackEventSlashCommand, + }); + }); + it('should not call action when it receives a event event', async () => { + await expectRouteNotMatchSlackEvent({ + route: slack.command('/weather', Action), + event: slackEventTextMessage, + }); + }); }); }); diff --git a/packages/bottender/src/slack/routes.ts b/packages/bottender/src/slack/routes.ts index a33821688..996b70ba2 100644 --- a/packages/bottender/src/slack/routes.ts +++ b/packages/bottender/src/slack/routes.ts @@ -1,10 +1,11 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import SlackContext from './SlackContext'; -import { EventTypes, InteractionTypes } from './SlackEvent'; +import { EventTypes, InteractionTypes } from './SlackTypes'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -14,14 +15,14 @@ type Route = ( type Slack = Route & { any: Route; message: Route; - event: ( + event: ( eventType: EventTypes | InteractionTypes, action: Action ) => { predicate: RoutePredicate; action: Action; }; - command: ( + command: ( commandText: string, action: Action ) => { @@ -30,15 +31,13 @@ type Slack = Route & { }; }; -const slack: Slack = ( - action: Action -) => { +const slack: Slack = (action: Action) => { return route((context: C) => context.platform === 'slack', action); }; slack.any = slack; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'slack' && context.event.isMessage, action @@ -47,20 +46,22 @@ function message(action: Action) { slack.message = message; -function event( +function event( eventType: EventTypes | InteractionTypes, action: Action ) { return route( (context: C) => - context.platform === 'slack' && context.event.rawEvent.type === eventType, + context.platform === 'slack' && + context.event.rawEvent.type && + (eventType === '*' || context.event.rawEvent.type === eventType), action ); } slack.event = event; -function command( +function command( commandText: string, action: Action ) { @@ -68,7 +69,7 @@ function command( (context: C) => context.platform === 'slack' && context.event.command && - context.event.command === commandText, + (commandText === '*' || context.event.command === commandText), action ); } diff --git a/packages/bottender/src/telegram/TelegramBot.ts b/packages/bottender/src/telegram/TelegramBot.ts index f6cdb68f2..50428604e 100644 --- a/packages/bottender/src/telegram/TelegramBot.ts +++ b/packages/bottender/src/telegram/TelegramBot.ts @@ -1,18 +1,14 @@ import { TelegramClient } from 'messaging-api-telegram'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; -import TelegramConnector, { TelegramRequestBody } from './TelegramConnector'; +import TelegramConnector, { + TelegramConnectorOptions, +} from './TelegramConnector'; import TelegramContext from './TelegramContext'; import TelegramEvent from './TelegramEvent'; - -type PollingOptions = { - offset?: number; - limit?: number; - timeout?: number; - allowed_updates?: string[]; -}; +import { PollingOptions, TelegramRequestBody } from './TelegramTypes'; export default class TelegramBot extends Bot< TelegramRequestBody, @@ -25,18 +21,17 @@ export default class TelegramBot extends Bot< _shouldGetUpdates: boolean; constructor({ - accessToken, sessionStore, sync, - origin, - }: { - accessToken: string; + onRequest, + ...connectorOptions + }: TelegramConnectorOptions & { sessionStore?: SessionStore; sync?: boolean; - origin?: string; + onRequest?: OnRequest; }) { - const connector = new TelegramConnector({ accessToken, origin }); - super({ connector, sessionStore, sync }); + const connector = new TelegramConnector(connectorOptions); + super({ connector, sessionStore, sync, onRequest }); this._offset = null; this._shouldGetUpdates = false; @@ -69,7 +64,7 @@ export default class TelegramBot extends Bot< } const highestUpdateId = Math.max( - ...updates.map((update: any) => update.update_id) + ...updates.map((update: any) => update.updateId) ); this._offset = highestUpdateId + 1; @@ -81,7 +76,7 @@ export default class TelegramBot extends Bot< /* eslint-enable no-await-in-loop */ } - stop() { + stop(): void { this._shouldGetUpdates = false; } } diff --git a/packages/bottender/src/telegram/TelegramConnector.ts b/packages/bottender/src/telegram/TelegramConnector.ts index 791d048e9..e570b7568 100644 --- a/packages/bottender/src/telegram/TelegramConnector.ts +++ b/packages/bottender/src/telegram/TelegramConnector.ts @@ -1,39 +1,43 @@ import { EventEmitter } from 'events'; import invariant from 'invariant'; +import { JsonObject } from 'type-fest'; import { TelegramClient } from 'messaging-api-telegram'; import Session from '../session/Session'; import { Connector } from '../bot/Connector'; -import { RequestContext } from '../types'; import TelegramContext from './TelegramContext'; -import TelegramEvent, { TelegramRawEvent } from './TelegramEvent'; - -export type TelegramRequestBody = TelegramRawEvent; - -type ConstructorOptionsWithoutClient = { +import TelegramEvent from './TelegramEvent'; +import { + TelegramRawEvent, + TelegramRequestBody, + TelegramRequestContext, +} from './TelegramTypes'; + +type ConnectorOptionsWithoutClient = { accessToken: string; origin?: string; skipLegacyProfile?: boolean; }; -type ConstructorOptionsWithClient = { +type ConnectorOptionsWithClient = { client: TelegramClient; skipLegacyProfile?: boolean; }; -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; +export type TelegramConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; export default class TelegramConnector - implements Connector { + implements Connector +{ _client: TelegramClient; _skipLegacyProfile: boolean; - constructor(options: ConstructorOptions) { + constructor(options: TelegramConnectorOptions) { const { skipLegacyProfile } = options; if ('client' in options) { this._client = options.client; @@ -45,7 +49,7 @@ export default class TelegramConnector 'Telegram access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' ); - this._client = TelegramClient.connect({ + this._client = new TelegramClient({ accessToken, origin, }); @@ -59,7 +63,7 @@ export default class TelegramConnector return body; } - get platform(): string { + get platform(): 'telegram' { return 'telegram'; } @@ -204,8 +208,8 @@ export default class TelegramConnector createContext(params: { event: TelegramEvent; session: Session | null; - initialState?: Record | null; - requestContext?: RequestContext; + initialState?: JsonObject | null; + requestContext?: TelegramRequestContext; emitter?: EventEmitter | null; }): TelegramContext { return new TelegramContext({ diff --git a/packages/bottender/src/telegram/TelegramContext.ts b/packages/bottender/src/telegram/TelegramContext.ts index cc3d4a17a..9f12715c3 100644 --- a/packages/bottender/src/telegram/TelegramContext.ts +++ b/packages/bottender/src/telegram/TelegramContext.ts @@ -1,10 +1,10 @@ -import sleep from 'delay'; import warning from 'warning'; -import { TelegramClient, TelegramTypes as Type } from 'messaging-api-telegram'; +import { TelegramClient } from 'messaging-api-telegram'; import Context from '../context/Context'; import TelegramEvent from './TelegramEvent'; +import * as Type from './TelegramTypes'; class TelegramContext extends Context { /** @@ -15,16 +15,6 @@ class TelegramContext extends Context { return 'telegram'; } - /** - * Delay and show indicators for milliseconds. - * - */ - async typing(milliseconds: number): Promise { - if (milliseconds > 0) { - await sleep(milliseconds); - } - } - /** * Send text to the owner of then session. * @@ -153,6 +143,22 @@ class TelegramContext extends Context { return this._client.answerInlineQuery(inlineQueryId, results, options); } + async answerCallbackQuery( + options: Type.AnswerCallbackQueryOption + ): Promise { + if (!this._event.isCallbackQuery) { + warning( + false, + 'answerCallbackQuery: should only be called to answer CallbackQuery event' + ); + return null; + } + + const callbackQueryId = (this._event.callbackQuery as any).id; + + return this._client.answerCallbackQuery(callbackQueryId, options); + } + async getUserProfilePhotos( options?: Type.GetUserProfilePhotosOption ): Promise { diff --git a/packages/bottender/src/telegram/TelegramEvent.ts b/packages/bottender/src/telegram/TelegramEvent.ts index fd0d3ba32..1d9dbefcf 100644 --- a/packages/bottender/src/telegram/TelegramEvent.ts +++ b/packages/bottender/src/telegram/TelegramEvent.ts @@ -1,201 +1,17 @@ +import { TelegramTypes } from 'messaging-api-telegram'; + import { Event } from '../context/Event'; -type TelegramUser = { - id: number; - firstName: string; - lastName?: string; - username?: string; - languageCode?: string; -}; - -type Photo = { - fileId: string; - width: number; - height: number; -}[]; - -type Audio = { - fileId: string; - width: number; - height: number; -}; - -type Document = { - fileId: string; -}; - -type Sticker = { - fileId: string; - width: number; - height: number; -}; - -type Video = { - fileId: string; - width: number; - height: number; - duration: number; -}; - -type Voice = { - fileId: string; - duration: number; -}; - -type VideoNote = { - fileId: string; - length: number; - duration: number; -}; - -type Contact = { - phoneNumber: string; - firstName: string; -}; - -type Location = { - longitude: number; - latitude: number; -}; - -type Venue = { - location: Location; - title: string; - address: string; -}; - -type File = { - fileId: string; -}; - -type Game = { - title: string; - description: string; - photo: { - fileId: string; - width: number; - height: number; - }[]; -}; - -type Message = { - messageId: number; - from: TelegramUser; - chat: { - id: number; - firstName: string; - lastName: string; - type: 'private' | 'group'; - }; - date: number; - text: string; - entities: { - type: 'bot_command'; - offset: number; - length: number; - }[]; - replyToMessage?: Message; - photo?: Photo; - game?: Game; - audio?: Audio; - document?: Document; - sticker?: Sticker; - video?: Video; - voice?: Voice; - videoNote?: VideoNote; - contact?: Contact; - location?: Location; - venue?: Venue; - file?: File; -}; - -type InlineQuery = { - id: string; - from: TelegramUser; - location?: Location; - query: string; - offset: string; -}; - -type ChosenInlineResult = { - resultId: string; - from: TelegramUser; - location?: Location; - inlineMessageId?: string; - query: string; -}; - -type CallbackQuery = { - from: TelegramUser; - message: Message; - chatInstance: string; - data: string; -}; - -type ShippingAddress = { - countryCode: string; - state: string; - city: string; - streetLine1: string; - streetLine2: string; - postCode: string; -}; - -type ShippingQuery = { - id: string; - from: TelegramUser; - invoicePayload: string; - shippingAddress: ShippingAddress; -}; - -type OrderInfo = { - name?: string; - phoneNumber?: string; - email?: string; - shippingAddress?: ShippingAddress; -}; - -type PreCheckoutQuery = { - id: string; - from: TelegramUser; - currency: string; - totalAmount: number; - invoicePayload: string; - shippingOptionId?: string; - orderInfo?: OrderInfo; -}; - -type PollOption = { - text: string; - voterCount: number; -}; - -type Poll = { - id: string; - question: string; - options: PollOption[]; - isClosed: boolean; -}; - -export type TelegramRawEvent = { - updateId: number; - message?: Message; - editedMessage?: Message; - channelPost?: Message; - editedChannelPost?: Message; - inlineQuery?: InlineQuery; - chosenInlineResult?: ChosenInlineResult; - callbackQuery?: CallbackQuery; - shippingQuery?: ShippingQuery; - preCheckoutQuery?: PreCheckoutQuery; - poll?: Poll; -}; +import { TelegramRawEvent } from './TelegramTypes'; export default class TelegramEvent implements Event { _rawEvent: TelegramRawEvent; + _timestamp: number; + constructor(rawEvent: TelegramRawEvent) { this._rawEvent = rawEvent; + this._timestamp = Date.now(); } /** @@ -206,6 +22,16 @@ export default class TelegramEvent implements Event { return this._rawEvent; } + /** + * The timestamp when the event was sent + * + */ + get timestamp(): number | undefined { + return 'message' in this.rawEvent && this.rawEvent.message + ? this.rawEvent.message.date * 1000 + : this._timestamp; + } + /** * Determine if the event is a message event. * @@ -218,7 +44,7 @@ export default class TelegramEvent implements Event { * The message object from Telegram raw event. * */ - get message(): Message | null { + get message(): TelegramTypes.Message | null { return this._rawEvent.message || null; } @@ -248,7 +74,7 @@ export default class TelegramEvent implements Event { get isReplyToMessage(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return ( !!message.replyToMessage && typeof message.replyToMessage === 'object' @@ -259,7 +85,7 @@ export default class TelegramEvent implements Event { * The Message object from Telegram raw event which includes replyToMessage. * */ - get replyToMessage(): Message | null { + get replyToMessage(): TelegramTypes.Message | null { if (this.isReplyToMessage) { return (this.message as any).replyToMessage; } @@ -273,7 +99,7 @@ export default class TelegramEvent implements Event { get isAudio(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.audio && typeof message.audio === 'object'; } @@ -282,7 +108,7 @@ export default class TelegramEvent implements Event { * The audio object from Telegram raw event. * */ - get audio(): Audio | null { + get audio(): TelegramTypes.Audio | null { if (this.isAudio) { return (this.message as any).audio; } @@ -296,7 +122,7 @@ export default class TelegramEvent implements Event { get isDocument(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.document && typeof message.document === 'object'; } @@ -305,7 +131,7 @@ export default class TelegramEvent implements Event { * The document object from Telegram raw event. * */ - get document(): Document | null { + get document(): TelegramTypes.Document | null { if (this.isDocument) { return (this.message as any).document; } @@ -319,7 +145,7 @@ export default class TelegramEvent implements Event { get isGame(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.game && typeof message.game === 'object'; } @@ -328,7 +154,7 @@ export default class TelegramEvent implements Event { * The game object from Telegram raw event. * */ - get game(): Game | null { + get game(): TelegramTypes.Game | null { if (this.isGame) { return (this.message as any).game; } @@ -342,7 +168,7 @@ export default class TelegramEvent implements Event { get isPhoto(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.photo && message.photo.length > 0; } @@ -351,7 +177,7 @@ export default class TelegramEvent implements Event { * The photo object from Telegram raw event. * */ - get photo(): Photo | null { + get photo(): TelegramTypes.PhotoSize | null { if (this.isPhoto) { return (this.message as any).photo; } @@ -365,7 +191,7 @@ export default class TelegramEvent implements Event { get isSticker(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.sticker && typeof message.sticker === 'object'; } @@ -374,7 +200,7 @@ export default class TelegramEvent implements Event { * The sticker object from Telegram raw event. * */ - get sticker(): Sticker | null { + get sticker(): TelegramTypes.Sticker | null { if (this.isSticker) { return (this.message as any).sticker; } @@ -388,7 +214,7 @@ export default class TelegramEvent implements Event { get isVideo(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.video && typeof message.video === 'object'; } @@ -396,7 +222,7 @@ export default class TelegramEvent implements Event { * The video object from Telegram raw event. * */ - get video(): Video | null { + get video(): TelegramTypes.Video | null { if (this.isVideo) { return (this.message as any).video; } @@ -410,7 +236,7 @@ export default class TelegramEvent implements Event { get isVoice(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.voice && typeof message.voice === 'object'; } @@ -419,7 +245,7 @@ export default class TelegramEvent implements Event { * The voice object from Telegram raw event. * */ - get voice(): Voice | null { + get voice(): TelegramTypes.Voice | null { if (this.isVoice) { return (this.message as any).voice; } @@ -433,7 +259,7 @@ export default class TelegramEvent implements Event { get isVideoNote(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.videoNote && typeof message.videoNote === 'object'; } @@ -442,7 +268,7 @@ export default class TelegramEvent implements Event { * The video note object from Telegram raw event. * */ - get videoNote(): VideoNote | null { + get videoNote(): TelegramTypes.VideoNote | null { if (this.isVideoNote) { return (this.message as any).videoNote; } @@ -456,7 +282,7 @@ export default class TelegramEvent implements Event { get isContact(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.contact && typeof message.contact === 'object'; } @@ -465,7 +291,7 @@ export default class TelegramEvent implements Event { * The contact object from Telegram raw event. * */ - get contact(): Contact | null { + get contact(): TelegramTypes.Contact | null { if (this.isContact) { return (this.message as any).contact; } @@ -479,7 +305,7 @@ export default class TelegramEvent implements Event { get isLocation(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.location && typeof message.location === 'object'; } @@ -488,7 +314,7 @@ export default class TelegramEvent implements Event { * The location object from Telegram raw event. * */ - get location(): Location | null { + get location(): TelegramTypes.Location | null { if (this.isLocation) { return (this.message as any).location; } @@ -502,7 +328,7 @@ export default class TelegramEvent implements Event { get isVenue(): boolean { if (!this.isMessage) return false; - const message: Message = this.message as any; + const message: TelegramTypes.Message = this.message as any; return !!message.venue && typeof message.venue === 'object'; } @@ -511,7 +337,7 @@ export default class TelegramEvent implements Event { * The venue object from Telegram raw event. * */ - get venue(): Venue | null { + get venue(): TelegramTypes.Venue | null { if (this.isVenue) { return (this.message as any).venue; } @@ -530,7 +356,7 @@ export default class TelegramEvent implements Event { * The edited message from Telegram raw event. * */ - get editedMessage(): Message | null { + get editedMessage(): TelegramTypes.Message | null { return this._rawEvent.editedMessage || null; } @@ -546,7 +372,7 @@ export default class TelegramEvent implements Event { * The channel post from Telegram raw event. * */ - get channelPost(): Message | null { + get channelPost(): TelegramTypes.Message | null { return this._rawEvent.channelPost || null; } @@ -564,7 +390,7 @@ export default class TelegramEvent implements Event { * The edited channel post from Telegram raw event. * */ - get editedChannelPost(): Message | null { + get editedChannelPost(): TelegramTypes.Message | null { return this._rawEvent.editedChannelPost || null; } @@ -580,7 +406,7 @@ export default class TelegramEvent implements Event { * The inline query from Telegram raw event. * */ - get inlineQuery(): InlineQuery | null { + get inlineQuery(): TelegramTypes.InlineQuery | null { return this._rawEvent.inlineQuery || null; } @@ -598,7 +424,7 @@ export default class TelegramEvent implements Event { * The chosen inline result from Telegram raw event. * */ - get chosenInlineResult(): ChosenInlineResult | null { + get chosenInlineResult(): TelegramTypes.ChosenInlineResult | null { return this._rawEvent.chosenInlineResult || null; } @@ -614,7 +440,7 @@ export default class TelegramEvent implements Event { * The callback query from Telegram raw event. * */ - get callbackQuery(): CallbackQuery | null { + get callbackQuery(): TelegramTypes.CallbackQuery | null { return this._rawEvent.callbackQuery || null; } @@ -649,7 +475,7 @@ export default class TelegramEvent implements Event { * The shipping query from Telegram raw event. * */ - get shippingQuery(): ShippingQuery | null { + get shippingQuery(): TelegramTypes.ShippingQuery | null { return this._rawEvent.shippingQuery || null; } @@ -665,7 +491,7 @@ export default class TelegramEvent implements Event { * The pre checkout query from Telegram raw event. * */ - get preCheckoutQuery(): PreCheckoutQuery | null { + get preCheckoutQuery(): TelegramTypes.PreCheckoutQuery | null { return this._rawEvent.preCheckoutQuery || null; } @@ -681,7 +507,23 @@ export default class TelegramEvent implements Event { * The poll from Telegram raw event. * */ - get poll(): Poll | null { + get poll(): TelegramTypes.Poll | null { return this._rawEvent.poll || null; } + + /** + * Determine if the event is a pollAnswer event. + * + */ + get isPollAnswer(): boolean { + return !!this.pollAnswer && typeof this.pollAnswer === 'object'; + } + + /** + * The poll from Telegram raw event. + * + */ + get pollAnswer(): TelegramTypes.PollAnswer | null { + return this._rawEvent.pollAnswer || null; + } } diff --git a/packages/bottender/src/telegram/TelegramTypes.ts b/packages/bottender/src/telegram/TelegramTypes.ts new file mode 100644 index 000000000..a5bec43ec --- /dev/null +++ b/packages/bottender/src/telegram/TelegramTypes.ts @@ -0,0 +1,32 @@ +import { TelegramTypes as TelegramTypesInternal } from 'messaging-api-telegram'; + +import { RequestContext } from '../types'; + +export * from 'messaging-api-telegram/dist/TelegramTypes'; +export { TelegramConnectorOptions } from './TelegramConnector'; + +export type TelegramRawEvent = { + updateId: number; + message?: TelegramTypesInternal.Message; + editedMessage?: TelegramTypesInternal.Message; + channelPost?: TelegramTypesInternal.Message; + editedChannelPost?: TelegramTypesInternal.Message; + inlineQuery?: TelegramTypesInternal.InlineQuery; + chosenInlineResult?: TelegramTypesInternal.ChosenInlineResult; + callbackQuery?: TelegramTypesInternal.CallbackQuery; + shippingQuery?: TelegramTypesInternal.ShippingQuery; + preCheckoutQuery?: TelegramTypesInternal.PreCheckoutQuery; + poll?: TelegramTypesInternal.Poll; + pollAnswer?: TelegramTypesInternal.PollAnswer; +}; + +export type TelegramRequestBody = TelegramRawEvent; + +export type TelegramRequestContext = RequestContext; + +export type PollingOptions = { + offset?: number; + limit?: number; + timeout?: number; + allowedUpdates?: string[]; +}; diff --git a/packages/bottender/src/telegram/__tests__/TelegramBot.spec.ts b/packages/bottender/src/telegram/__tests__/TelegramBot.spec.ts index 6bd2e0024..a04ea1e9b 100644 --- a/packages/bottender/src/telegram/__tests__/TelegramBot.spec.ts +++ b/packages/bottender/src/telegram/__tests__/TelegramBot.spec.ts @@ -1,15 +1,9 @@ +import { mocked } from 'ts-jest/utils'; + import TelegramBot from '../TelegramBot'; import TelegramConnector from '../TelegramConnector'; -jest.mock('messaging-api-telegram', () => { - const createMockInstance = require('jest-create-mock-instance').default; - const { TelegramClient } = require.requireActual('messaging-api-telegram'); - return { - TelegramClient: { - connect: () => createMockInstance(TelegramClient), - }, - }; -}); +jest.mock('messaging-api-telegram'); beforeEach(() => { console.error = jest.fn(); @@ -19,6 +13,7 @@ it('should construct bot with TelegramConnector', () => { const bot = new TelegramBot({ accessToken: 'FAKE_TOKEN', }); + expect(bot).toBeDefined(); expect(bot.onEvent).toBeDefined(); expect(bot.createRequestHandler).toBeDefined(); @@ -30,11 +25,12 @@ it('should export createLongPollingRuntime method', () => { const bot = new TelegramBot({ accessToken: 'FAKE_TOKEN', }); + expect(bot.createLongPollingRuntime).toBeDefined(); }); describe('#createLongPollingRuntime', () => { - it('should call updates without params', done => { + it('should call updates without params', (done) => { const bot = new TelegramBot({ accessToken: 'FAKE_TOKEN', }); @@ -45,19 +41,19 @@ describe('#createLongPollingRuntime', () => { const getUpdates = [ { - update_id: 513400512, + updateId: 513400512, message: { - message_id: 3, + messageId: 3, from: { id: 313534466, - first_name: 'first', - last_name: 'last', + firstName: 'first', + lastName: 'last', username: 'username', }, chat: { id: 313534466, - first_name: 'first', - last_name: 'last', + firstName: 'first', + lastName: 'last', username: 'username', type: 'private', }, @@ -67,7 +63,7 @@ describe('#createLongPollingRuntime', () => { }, ]; - bot.connector.client.getUpdates + mocked(bot.connector.client.getUpdates) .mockResolvedValueOnce(getUpdates) .mockImplementationOnce(() => { bot.stop(); @@ -79,7 +75,7 @@ describe('#createLongPollingRuntime', () => { bot.createLongPollingRuntime(); }); - it('should call updates with params', done => { + it('should call updates with params', (done) => { const bot = new TelegramBot({ accessToken: 'FAKE_TOKEN', }); @@ -90,19 +86,19 @@ describe('#createLongPollingRuntime', () => { const getUpdates = [ { - update_id: 513400512, + updateId: 513400512, message: { - message_id: 3, + messageId: 3, from: { id: 313534466, - first_name: 'first', - last_name: 'last', + firstName: 'first', + lastName: 'last', username: 'username', }, chat: { id: 313534466, - first_name: 'first', - last_name: 'last', + firstName: 'first', + lastName: 'last', username: 'username', type: 'private', }, @@ -112,14 +108,14 @@ describe('#createLongPollingRuntime', () => { }, ]; - bot.connector.client.getUpdates + mocked(bot.connector.client.getUpdates) .mockResolvedValueOnce(getUpdates) .mockImplementationOnce(() => { bot.stop(); expect(bot.connector.client.getUpdates).toBeCalledWith({ limit: 100, timeout: 0, - allowed_updates: ['message', 'edited_channel_post', 'callback_query'], + allowedUpdates: ['message', 'edited_channel_post', 'callback_query'], offset: 1, }); expect(handler).toBeCalledWith(expect.any(Object), {}); @@ -129,7 +125,7 @@ describe('#createLongPollingRuntime', () => { bot.createLongPollingRuntime({ limit: 100, timeout: 0, - allowed_updates: ['message', 'edited_channel_post', 'callback_query'], + allowedUpdates: ['message', 'edited_channel_post', 'callback_query'], offset: 1, }); diff --git a/packages/bottender/src/telegram/__tests__/TelegramConnector.spec.ts b/packages/bottender/src/telegram/__tests__/TelegramConnector.spec.ts index 6ca71c382..e2a7d4e60 100644 --- a/packages/bottender/src/telegram/__tests__/TelegramConnector.spec.ts +++ b/packages/bottender/src/telegram/__tests__/TelegramConnector.spec.ts @@ -1,4 +1,5 @@ import { TelegramClient } from 'messaging-api-telegram'; +import { mocked } from 'ts-jest/utils'; import TelegramConnector from '../TelegramConnector'; import TelegramContext from '../TelegramContext'; @@ -9,313 +10,296 @@ jest.mock('messaging-api-telegram'); const ACCESS_TOKEN = 'ACCESS_TOKEN'; const messageRequest = { - body: { - update_id: 141921689, - message: { - messageId: 666, - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - chat: { - id: 427770117, - firstName: 'first', - lastName: 'last', - type: 'private', - }, - date: 1499402829, - text: 'text', + updateId: 141921689, + message: { + messageId: 666, + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', }, + chat: { + id: 427770117, + firstName: 'first', + lastName: 'last', + type: 'private', + }, + date: 1499402829, + text: 'text', }, }; const groupMessageRequest = { - body: { - updateId: 141921689, - message: { - messageId: 238, - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - chat: { - id: -225456171, - title: 'Bottender', - type: 'group', - allMembersAreAdministrators: true, - }, - date: 1515758146, - text: 'hi', + updateId: 141921689, + message: { + messageId: 238, + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + chat: { + id: -225456171, + title: 'Bottender', + type: 'group', + allMembersAreAdministrators: true, }, + date: 1515758146, + text: 'hi', }, }; const editedMessageRequest = { - body: { - updateId: 141921687, - editedMessage: { - messageId: 229, - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - chat: { - id: 427770117, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - date: 1515736358, - editDate: 1515758017, - text: 'hiiiii', + updateId: 141921687, + editedMessage: { + messageId: 229, + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + chat: { + id: 427770117, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', }, + date: 1515736358, + editDate: 1515758017, + text: 'hiiiii', }, }; const groupEditedMessageRequest = { - body: { - updateId: 141921688, - editedMessage: { - messageId: 234, - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - chat: { - id: -225456171, - title: 'Bottender', - type: 'group', - allMembersAreAdministrators: true, - }, - date: 1515736470, - editDate: 1515758048, - text: 'hiiiii', + updateId: 141921688, + editedMessage: { + messageId: 234, + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + chat: { + id: -225456171, + title: 'Bottender', + type: 'group', + allMembersAreAdministrators: true, }, + date: 1515736470, + editDate: 1515758048, + text: 'hiiiii', }, }; const channelPostRequest = { - body: { - updateId: 141921710, - channelPost: { - messageId: 2, - chat: { - id: -1001305240521, - title: 'channel_12345', - type: 'channel', - }, - date: 1515760382, - text: 'post~~~', + updateId: 141921710, + channelPost: { + messageId: 2, + chat: { + id: -1001305240521, + title: 'channel_12345', + type: 'channel', }, + date: 1515760382, + text: 'post~~~', }, }; const editedChannelPostRequest = { - body: { - updateId: 141921711, - editedChannelPost: { - messageId: 2, - chat: { - id: -1001305240521, - title: 'channel_12345', - type: 'channel', - }, - date: 1515760382, - editDate: 1515760478, - text: 'post~~~edited', + updateId: 141921711, + editedChannelPost: { + messageId: 2, + chat: { + id: -1001305240521, + title: 'channel_12345', + type: 'channel', }, + date: 1515760382, + editDate: 1515760478, + text: 'post~~~edited', }, }; const inlineQueryRequest = { - body: { - updateId: 141921700, - inlineQuery: { - id: '1837258670654537434', - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - query: '123', - offset: '', + updateId: 141921700, + inlineQuery: { + id: '1837258670654537434', + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', }, + query: '123', + offset: '', }, }; const chosenInlineResultRequest = { - body: { - updateId: 141921701, - chosenInlineResult: { - resultId: '2837258670654537434', - from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - inlineMessageId: '1837258670654537434', - query: '123', + updateId: 141921701, + chosenInlineResult: { + resultId: '2837258670654537434', + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', }, + inlineMessageId: '1837258670654537434', + query: '123', }, }; const callbackQueryRequest = { - body: { - updateId: 141921689, - callbackQuery: { - id: '1068230107531367617', + updateId: 141921689, + callbackQuery: { + id: '1068230107531367617', + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + message: { + messageId: 3300, from: { + id: 313534466, + isBot: true, + firstName: 'bot_first', + username: 'bot_name', + }, + chat: { id: 427770117, - isBot: false, firstName: 'user_first', lastName: 'user_last', - languageCode: 'en', - }, - message: { - messageId: 3300, - from: { - id: 313534466, - isBot: true, - firstName: 'bot_first', - username: 'bot_name', - }, - chat: { - id: 427770117, - firstName: 'user_first', - lastName: 'user_last', - type: 'private', - }, - date: 1502371827, - text: 'text', + type: 'private', }, - chatInstance: '-1828607021492040088', - data: 'data', + date: 1502371827, + text: 'text', }, + chatInstance: '-1828607021492040088', + data: 'data', }, }; const groupCallbackQueryRequest = { - body: { - updateId: 141921690, - callbackQuery: { - id: '1837258667245133763', + updateId: 141921690, + callbackQuery: { + id: '1837258667245133763', + from: { + id: 427770117, + isBot: false, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + message: { + messageId: 237, from: { - id: 427770117, - isBot: false, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', + id: 313534466, + isBot: true, + firstName: 'bot_first', + username: 'bot_name', }, - message: { - messageId: 237, - from: { - id: 313534466, - isBot: true, - firstName: 'bot_first', - username: 'bot_name', - }, - chat: { - id: -225456171, - title: 'Bottender', - type: 'group', - allMembersAreAdministrators: true, - }, - date: 1515736481, - text: 'Hello World', + chat: { + id: -225456171, + title: 'Bottender', + type: 'group', + allMembersAreAdministrators: true, }, - chatInstance: '-582211693826679000', - data: '123', + date: 1515736481, + text: 'Hello World', }, + chatInstance: '-582211693826679000', + data: '123', }, }; const shippingQueryRequest = { - body: { - updateId: 141921690, - shippingQuery: { - id: '123', - from: { - id: 427770117, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - invoicePayload: 'bot payload', - shippingAddress: { - countryCode: 'US', - state: 'New York', - city: 'New York', - streetLine1: 'xx', - streetLine2: 'xx', - postCode: '10001', - }, + updateId: 141921690, + shippingQuery: { + id: '123', + from: { + id: 427770117, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', + }, + invoicePayload: 'bot payload', + shippingAddress: { + countryCode: 'US', + state: 'New York', + city: 'New York', + streetLine1: 'xx', + streetLine2: 'xx', + postCode: '10001', }, }, }; const preCheckoutQueryRequest = { - body: { - updateId: 141921690, - preCheckoutQuery: { - id: '123', - from: { - id: 427770117, - firstName: 'user_first', - lastName: 'user_last', - languageCode: 'en', - }, - currency: 'USD', - totalAmount: 145, - invoicePayload: 'bot payload', + updateId: 141921690, + preCheckoutQuery: { + id: '123', + from: { + id: 427770117, + firstName: 'user_first', + lastName: 'user_last', + languageCode: 'en', }, + currency: 'USD', + totalAmount: 145, + invoicePayload: 'bot payload', }, }; function setup() { - const mockTelegramClient = {}; - TelegramClient.connect = jest.fn(); - TelegramClient.connect.mockReturnValue(mockTelegramClient); + const connector = new TelegramConnector({ + accessToken: ACCESS_TOKEN, + skipLegacyProfile: false, + }); + + const client = mocked(TelegramClient).mock.instances[0]; + return { - mockTelegramClient, - connector: new TelegramConnector({ - accessToken: ACCESS_TOKEN, - skipLegacyProfile: false, - }), + connector, + client, }; } describe('#platform', () => { it('should be telegram', () => { const { connector } = setup(); + expect(connector.platform).toBe('telegram'); }); }); describe('#client', () => { it('should be client', () => { - const { connector, mockTelegramClient } = setup(); - expect(connector.client).toBe(mockTelegramClient); + const { connector, client } = setup(); + + expect(connector.client).toBe(client); }); it('support custom client', () => { - const client = {}; + const client = new TelegramClient({ + accessToken: ACCESS_TOKEN, + }); + const connector = new TelegramConnector({ client }); + expect(connector.client).toBe(client); }); }); @@ -323,89 +307,105 @@ describe('#client', () => { describe('#getUniqueSessionKey', () => { it('extract correct sender id from messageRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(messageRequest.body); + + const senderId = connector.getUniqueSessionKey(messageRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from groupMessageRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(groupMessageRequest.body); + + const senderId = connector.getUniqueSessionKey(groupMessageRequest); + expect(senderId).toBe('-225456171'); }); it('extract correct sender id from editedMessageRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(editedMessageRequest.body); + + const senderId = connector.getUniqueSessionKey(editedMessageRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from groupEditedMessageRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - groupEditedMessageRequest.body - ); + + const senderId = connector.getUniqueSessionKey(groupEditedMessageRequest); + expect(senderId).toBe('-225456171'); }); it('extract correct sender id from channelPostRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(channelPostRequest.body); + + const senderId = connector.getUniqueSessionKey(channelPostRequest); + expect(senderId).toBe('-1001305240521'); }); it('extract correct sender id from editedChannelPostRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - editedChannelPostRequest.body - ); + + const senderId = connector.getUniqueSessionKey(editedChannelPostRequest); + expect(senderId).toBe('-1001305240521'); }); it('extract correct sender id from inlineQueryRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(inlineQueryRequest.body); + + const senderId = connector.getUniqueSessionKey(inlineQueryRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from chosenInlineResultRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - chosenInlineResultRequest.body - ); + + const senderId = connector.getUniqueSessionKey(chosenInlineResultRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from callbackQueryRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(callbackQueryRequest.body); + + const senderId = connector.getUniqueSessionKey(callbackQueryRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from groupCallbackQueryRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - groupCallbackQueryRequest.body - ); + + const senderId = connector.getUniqueSessionKey(groupCallbackQueryRequest); + expect(senderId).toBe('-225456171'); }); it('extract correct sender id from shippingQueryRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(shippingQueryRequest.body); + + const senderId = connector.getUniqueSessionKey(shippingQueryRequest); + expect(senderId).toBe('427770117'); }); it('extract correct sender id from preCheckoutQueryRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - preCheckoutQueryRequest.body - ); + + const senderId = connector.getUniqueSessionKey(preCheckoutQueryRequest); expect(senderId).toBe('427770117'); }); it('return empty string if type is unknown', () => { const { connector } = setup(); + + // @ts-expect-error const senderId = connector.getUniqueSessionKey({}); + expect(senderId).toBe(''); }); }); @@ -416,7 +416,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, messageRequest.body); + await connector.updateSession(session, messageRequest); expect(session).toEqual({ channel: undefined, @@ -437,7 +437,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, groupMessageRequest.body); + await connector.updateSession(session, groupMessageRequest); expect(session).toEqual({ channel: undefined, @@ -464,7 +464,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, editedMessageRequest.body); + await connector.updateSession(session, editedMessageRequest); expect(session).toEqual({ channel: undefined, @@ -485,7 +485,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, groupEditedMessageRequest.body); + await connector.updateSession(session, groupEditedMessageRequest); expect(session).toEqual({ channel: undefined, @@ -512,7 +512,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, channelPostRequest.body); + await connector.updateSession(session, channelPostRequest); expect(session).toEqual({ group: undefined, @@ -531,7 +531,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, editedChannelPostRequest.body); + await connector.updateSession(session, editedChannelPostRequest); expect(session).toEqual({ group: undefined, @@ -549,7 +549,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, inlineQueryRequest.body); + await connector.updateSession(session, inlineQueryRequest); expect(session).toEqual({ channel: undefined, @@ -569,7 +569,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, chosenInlineResultRequest.body); + await connector.updateSession(session, chosenInlineResultRequest); expect(session).toEqual({ channel: undefined, @@ -589,7 +589,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, callbackQueryRequest.body); + await connector.updateSession(session, callbackQueryRequest); expect(session).toEqual({ channel: undefined, @@ -609,7 +609,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, groupCallbackQueryRequest.body); + await connector.updateSession(session, groupCallbackQueryRequest); expect(session).toEqual({ channel: undefined, @@ -635,7 +635,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, shippingQueryRequest.body); + await connector.updateSession(session, shippingQueryRequest); expect(session).toEqual({ channel: undefined, @@ -654,7 +654,7 @@ describe('#updateSession', () => { const { connector } = setup(); const session = {}; - await connector.updateSession(session, preCheckoutQueryRequest.body); + await connector.updateSession(session, preCheckoutQueryRequest); expect(session).toEqual({ channel: undefined, @@ -673,7 +673,7 @@ describe('#updateSession', () => { describe('#mapRequestToEvents', () => { it('should map request to TelegramEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(messageRequest.body); + const events = connector.mapRequestToEvents(messageRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(TelegramEvent); diff --git a/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts b/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts index ad5f87ce8..df89fd763 100644 --- a/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts +++ b/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts @@ -1,24 +1,15 @@ -jest.mock('delay'); +import warning from 'warning'; +import { TelegramClient } from 'messaging-api-telegram'; +import { mocked } from 'ts-jest/utils'; + +import TelegramContext from '../TelegramContext'; +import TelegramEvent from '../TelegramEvent'; +import { TelegramRawEvent } from '../TelegramTypes'; + jest.mock('messaging-api-telegram'); jest.mock('warning'); -let TelegramClient; -let TelegramContext; -let TelegramEvent; -let sleep; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - TelegramClient = require('messaging-api-telegram').TelegramClient; - TelegramContext = require('../TelegramContext').default; - TelegramEvent = require('../TelegramEvent').default; - sleep = require('delay'); - warning = require('warning'); - /* eslint-enable global-require */ -}); - -const _rawEvent = { +const defaultRawEvent: TelegramRawEvent = { message: { messageId: 666, from: { @@ -41,15 +32,20 @@ const _rawEvent = { const setup = ({ session = { user: { id: 313534466 } }, - rawEvent = _rawEvent, -} = {}) => { - const client = TelegramClient.connect(); - const args = { + rawEvent = defaultRawEvent, +} = {}): { + context: TelegramContext; + session: any; + client: TelegramClient; +} => { + const client = new TelegramClient({}); + + const context = new TelegramContext({ client, event: new TelegramEvent(rawEvent), session, - }; - const context = new TelegramContext(args); + }); + return { context, session, @@ -57,28 +53,27 @@ const setup = ({ }; }; -it('be defined', () => { - const { context } = setup(); - expect(context).toBeDefined(); -}); - it('#platform to be `telegram`', () => { const { context } = setup(); + expect(context.platform).toBe('telegram'); }); it('get #session works', () => { const { context, session } = setup(); + expect(context.session).toBe(session); }); it('get #event works', () => { const { context } = setup(); + expect(context.event).toBeInstanceOf(TelegramEvent); }); it('get #client works', () => { const { context, client } = setup(); + expect(context.client).toBe(client); }); @@ -92,7 +87,7 @@ describe('#sendText', () => { }); it('should call warning and not to send if dont have session', async () => { - const { context, client } = setup({ session: false }); + const { context, client } = setup({ session: null }); await context.sendText('hello'); @@ -449,10 +444,11 @@ describe('#stopMessageLiveLocation', () => { it('should to call client.stopMessageLiveLocation', async () => { const { context, client } = setup(); - await context.stopMessageLiveLocation(); + await context.stopMessageLiveLocation(313534466); expect(client.stopMessageLiveLocation).toBeCalledWith({ chatId: 427770117, + messageId: 313534466, }); }); }); @@ -689,7 +685,7 @@ describe('#answerShippingQuery', () => { result: true, }; - client.answerShippingQuery.mockResolvedValue(response); + mocked(client.answerShippingQuery).mockResolvedValue(response); const result = await context.answerShippingQuery(true); @@ -731,7 +727,7 @@ describe('#answerPreCheckoutQuery', () => { result: true, }; - client.answerPreCheckoutQuery.mockResolvedValue(response); + mocked(client.answerPreCheckoutQuery).mockResolvedValue(response); const result = await context.answerPreCheckoutQuery(true); @@ -776,7 +772,7 @@ describe('#answerInlineQuery', () => { ok: true, }; - client.answerInlineQuery.mockResolvedValue(response); + mocked(client.answerInlineQuery).mockResolvedValue(response); const result = await context.answerInlineQuery( [ @@ -848,6 +844,100 @@ describe('#answerInlineQuery', () => { }); }); +describe('#answerCallbackQuery', () => { + const callbackQuery = { + updateId: 869424, + callbackQuery: { + id: '705303069014561', + from: { + id: 164230, + isBot: false, + firstName: 'user_first', + username: 'username', + languageCode: 'zh-hans', + }, + message: { + messageId: 1474, + from: { + id: 902548, + isBot: true, + firstName: 'bot_first', + username: 'botname', + }, + chat: { + id: -371089, + title: 'group name', + type: 'group', + allMembersAreAdministrators: true, + }, + date: 1588145587, + game: { + title: 'game title', + description: 'game description', + photo: [ + { + fileId: + 'AgACAgUAAxUAAV6pNvEYJWk8Nn7D-P9i8KxCkeBJAAL5qjEbZ5BIVdV5MmS2G44AAfN1w2p0AAMBAAMCAANtAANXSw', + fileUniqueId: 'AQAD83XDanQAA1', + fileSize: 8889, + width: 320, + height: 180, + }, + { + fileId: + 'AgACAgUAAxUAAV6pNvEYJWk8Nn7D-P9i8KxCkeBJAAL5qjEbZ5BIVdV5MmS2G44AAfN1w2p0AAMBAAMCAAN4AANYSw', + fileUniqueId: 'AQAD83XDanQAA1', + fileSize: 20067, + width: 640, + height: 360, + }, + ], + }, + replyMarkup: { + inlineKeyboard: [ + [ + { + text: 'Play gamename', + callbackGame: {}, + }, + ], + ], + }, + }, + chatInstance: '-811839530613755', + gameShortName: 'gamename', + }, + }; + it('should to call client.answerCallbackQuery', async () => { + const { context, client } = setup({ rawEvent: callbackQuery }); + + const response = { + ok: true, + }; + + mocked(client.answerCallbackQuery).mockResolvedValue(response); + + const result = await context.answerCallbackQuery({ + url: 'https://example.com/', + }); + + expect(client.answerCallbackQuery).toBeCalledWith('705303069014561', { + url: 'https://example.com/', + }); + expect(result).toEqual(response); + }); + + it('should not call answerCallbackQuery method if event type is not CallbackQuery', async () => { + const { context, client } = setup(); + + await context.answerCallbackQuery({ + url: 'https://example.com/', + }); + + expect(client.answerCallbackQuery).not.toBeCalled(); + }); +}); + describe('#getUserProfilePhotos', () => { it('should to call client.getUserProfilePhotos', async () => { const { context, client } = setup(); @@ -898,7 +988,7 @@ describe('#getUserProfilePhotos', () => { ], }; - client.getUserProfilePhotos.mockResolvedValue(profile); + mocked(client.getUserProfilePhotos).mockResolvedValue(profile); const result = await context.getUserProfilePhotos({ limit: 2 }); @@ -919,7 +1009,7 @@ describe('#getChat', () => { type: 'private', }; - client.getChat.mockResolvedValue(chat); + mocked(client.getChat).mockResolvedValue(chat); const result = await context.getChat(); @@ -945,7 +1035,7 @@ describe('#getChatAdministrators', () => { }, ]; - client.getChatAdministrators.mockResolvedValue(administrators); + mocked(client.getChatAdministrators).mockResolvedValue(administrators); const result = await context.getChatAdministrators(); @@ -958,7 +1048,7 @@ describe('#getChatMembersCount', () => { it('should to call client.getChatMembersCount', async () => { const { context, client } = setup(); - client.getChatMembersCount.mockResolvedValue('6'); + mocked(client.getChatMembersCount).mockResolvedValue('6'); const result = await context.getChatMembersCount(); @@ -982,7 +1072,7 @@ describe('#getChatMember', () => { status: 'creator', }; - client.getChatMember.mockResolvedValue(member); + mocked(client.getChatMember).mockResolvedValue(member); const result = await context.getChatMember(313534466); @@ -991,24 +1081,6 @@ describe('#getChatMember', () => { }); }); -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context } = setup(); - - await context.typing(10); - - expect(sleep).toBeCalled(); - }); -}); - describe('group chat', () => { it('should call method with chat id when receiving message', async () => { const { context, client } = setup({ diff --git a/packages/bottender/src/telegram/__tests__/TelegramEvent.spec.ts b/packages/bottender/src/telegram/__tests__/TelegramEvent.spec.ts index d487c1015..ae59c17cb 100644 --- a/packages/bottender/src/telegram/__tests__/TelegramEvent.spec.ts +++ b/packages/bottender/src/telegram/__tests__/TelegramEvent.spec.ts @@ -1,3 +1,5 @@ +import MockDate from 'mockdate'; + import TelegramEvent from '../TelegramEvent'; const textMessage = { @@ -621,6 +623,27 @@ it('#rawEvent', () => { ); }); +it('#timestamp', () => { + MockDate.set('2020-07-14'); // 15946848000 + expect(new TelegramEvent(textMessage).timestamp).toEqual(1499402829000); + expect(new TelegramEvent(groupMessage).timestamp).toEqual(1515758146000); + expect(new TelegramEvent(editedMessage).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(channelPost).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(editedChannelPost).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(inlineQuery).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(chosenInlineResult).timestamp).toEqual( + 1594684800000 + ); + expect(new TelegramEvent(callbackQuery).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(shippingQuery).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(preCheckoutQuery).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(poll).timestamp).toEqual(1594684800000); + expect(new TelegramEvent(replyToTextMessage).timestamp).toEqual( + 1499402829000 + ); + MockDate.reset(); +}); + it('#isMessage', () => { expect(new TelegramEvent(textMessage).isMessage).toEqual(true); expect(new TelegramEvent(groupMessage).isMessage).toEqual(true); diff --git a/packages/bottender/src/telegram/__tests__/routes.spec.ts b/packages/bottender/src/telegram/__tests__/routes.spec.ts index 34c582d49..12702476c 100644 --- a/packages/bottender/src/telegram/__tests__/routes.spec.ts +++ b/packages/bottender/src/telegram/__tests__/routes.spec.ts @@ -246,7 +246,7 @@ async function expectRouteNotMatchTelegramEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender/src/telegram/routes.ts b/packages/bottender/src/telegram/routes.ts index 92eea5f5d..5629e413d 100644 --- a/packages/bottender/src/telegram/routes.ts +++ b/packages/bottender/src/telegram/routes.ts @@ -1,9 +1,10 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import TelegramContext from './TelegramContext'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -22,9 +23,10 @@ type Telegram = Route & { shippingQuery: Route; preCheckoutQuery: Route; poll: Route; + pollAnswer: Route; }; -const telegram: Telegram = ( +const telegram: Telegram = ( action: Action ) => { return route((context: C) => context.platform === 'telegram', action); @@ -32,7 +34,7 @@ const telegram: Telegram = ( telegram.any = telegram; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'telegram' && context.event.isMessage, action @@ -41,7 +43,7 @@ function message(action: Action) { telegram.message = message; -function editedMessage( +function editedMessage( action: Action ) { return route( @@ -53,9 +55,7 @@ function editedMessage( telegram.editedMessage = editedMessage; -function channelPost( - action: Action -) { +function channelPost(action: Action) { return route( (context: C) => context.platform === 'telegram' && context.event.isChannelPost, @@ -65,7 +65,7 @@ function channelPost( telegram.channelPost = channelPost; -function editedChannelPost( +function editedChannelPost( action: Action ) { return route( @@ -77,9 +77,7 @@ function editedChannelPost( telegram.editedChannelPost = editedChannelPost; -function inlineQuery( - action: Action -) { +function inlineQuery(action: Action) { return route( (context: C) => context.platform === 'telegram' && context.event.isInlineQuery, @@ -89,7 +87,7 @@ function inlineQuery( telegram.inlineQuery = inlineQuery; -function chosenInlineResult( +function chosenInlineResult( action: Action ) { return route( @@ -101,7 +99,7 @@ function chosenInlineResult( telegram.chosenInlineResult = chosenInlineResult; -function callbackQuery( +function callbackQuery( action: Action ) { return route( @@ -113,7 +111,7 @@ function callbackQuery( telegram.callbackQuery = callbackQuery; -function shippingQuery( +function shippingQuery( action: Action ) { return route( @@ -125,7 +123,7 @@ function shippingQuery( telegram.shippingQuery = shippingQuery; -function preCheckoutQuery( +function preCheckoutQuery( action: Action ) { return route( @@ -137,7 +135,7 @@ function preCheckoutQuery( telegram.preCheckoutQuery = preCheckoutQuery; -function poll(action: Action) { +function poll(action: Action) { return route( (context: C) => context.platform === 'telegram' && context.event.isPoll, action @@ -146,4 +144,14 @@ function poll(action: Action) { telegram.poll = poll; +function pollAnswer(action: Action) { + return route( + (context: C) => + context.platform === 'telegram' && context.event.isPollAnswer, + action + ); +} + +telegram.pollAnswer = pollAnswer; + export default telegram; diff --git a/packages/bottender/src/test-utils/ContextSimulator.ts b/packages/bottender/src/test-utils/ContextSimulator.ts index d039aabc2..cb0761e76 100644 --- a/packages/bottender/src/test-utils/ContextSimulator.ts +++ b/packages/bottender/src/test-utils/ContextSimulator.ts @@ -116,7 +116,7 @@ class ContextSimulator { event: Record; state?: Record; }): Record { - const context: any = new SimulatedContext({ + const context: any = new SimulatedContext({ client: this.createClient(), event: this.createEvent(event), session: { diff --git a/packages/bottender/src/test-utils/SimulatedContext.ts b/packages/bottender/src/test-utils/SimulatedContext.ts index 5ade2961a..38838b01e 100644 --- a/packages/bottender/src/test-utils/SimulatedContext.ts +++ b/packages/bottender/src/test-utils/SimulatedContext.ts @@ -2,8 +2,8 @@ import Context from '../context/Context'; import { Client, Event } from '../types'; export default class SimulatedContext< - C extends Client, - E extends Event + C extends Client = any, + E extends Event = any > extends Context { _platform: string; diff --git a/packages/bottender/src/types.ts b/packages/bottender/src/types.ts index 6658c51ad..97037a4bf 100644 --- a/packages/bottender/src/types.ts +++ b/packages/bottender/src/types.ts @@ -1,58 +1,20 @@ -import { LineClient } from 'messaging-api-line'; -import { MessengerClient } from 'messaging-api-messenger'; -import { SlackOAuthClient } from 'messaging-api-slack'; -import { TelegramClient } from 'messaging-api-telegram'; -import { ViberClient } from 'messaging-api-viber'; +import { IncomingHttpHeaders } from 'http'; -import ConsoleEvent, { ConsoleRawEvent } from './console/ConsoleEvent'; +import { JsonObject } from 'type-fest'; + +import Bot, { OnRequest } from './bot/Bot'; import Context from './context/Context'; -import LineEvent from './line/LineEvent'; -import MessengerEvent from './messenger/MessengerEvent'; import SessionStore from './session/SessionStore'; -import SlackEvent from './slack/SlackEvent'; -import TelegramEvent from './telegram/TelegramEvent'; -import TwilioClient from './whatsapp/TwilioClient'; -import ViberEvent from './viber/ViberEvent'; -import WhatsappEvent from './whatsapp/WhatsappEvent'; -import { ConsoleClient } from './console/ConsoleClient'; -import { LineRequestBody } from './line/LineConnector'; -import { MessengerRequestBody } from './messenger/MessengerConnector'; -import { SlackRequestBody } from './slack/SlackConnector'; -import { TelegramRequestBody } from './telegram/TelegramConnector'; -import { ViberRequestBody } from './viber/ViberConnector'; -import { WhatsappRequestBody } from './whatsapp/WhatsappConnector'; - -export type Client = - | ConsoleClient - | MessengerClient - | LineClient - | SlackOAuthClient - | TelegramClient - | ViberClient - | TwilioClient; - -export type Event = - | ConsoleEvent - | MessengerEvent - | LineEvent - | SlackEvent - | TelegramEvent - | ViberEvent - | WhatsappEvent; - -export type Body = - | ConsoleRawEvent - | MessengerRequestBody - | LineRequestBody - | SlackRequestBody - | TelegramRequestBody - | ViberRequestBody - | WhatsappRequestBody; - -export type AnyContext = Context; +import { Connector } from './bot/Connector'; +import { LineConnectorOptions } from './line/LineConnector'; +import { MessengerConnectorOptions } from './messenger/MessengerConnector'; +import { SlackConnectorOptions } from './slack/SlackConnector'; +import { TelegramConnectorOptions } from './telegram/TelegramConnector'; +import { ViberConnectorOptions } from './viber/ViberConnector'; +import { WhatsappConnectorOptions } from './whatsapp/WhatsappConnector'; export type Action< - C extends AnyContext, + C extends Context, P extends Record = {}, RAP extends Record = {} > = ( @@ -60,12 +22,12 @@ export type Action< props: Props & P ) => void | Action | Promise | void>; -export type Props = { +export type Props = { next?: Action; error?: Error; }; -export type Plugin = (context: C) => void; +export type Plugin = (context: C) => void; export enum Channel { Messenger = 'messenger', @@ -106,61 +68,60 @@ export type SessionConfig = { }; } | { - [P in Exclude]: SessionStore; + [P in Exclude]?: SessionStore; }; }; +type ChannelCommonConfig = { + enabled: boolean; + path?: string; + sync?: boolean; + onRequest?: OnRequest; +}; + export type BottenderConfig = { plugins?: Plugin[]; session?: SessionConfig; - initialState?: Record; - channels?: { - [Channel.Messenger]: { - enabled: boolean; - path: string; - accessToken: string; - verifyToken: string; - appId: string; - appSecret: string; - }; - [Channel.Line]: { - enabled: boolean; - path: string; - accessToken: string; - channelSecret: string; - }; - [Channel.Telegram]: { - enabled: boolean; - path: string; - accessToken: string; - }; - [Channel.Slack]: { - enabled: boolean; - path: string; - accessToken: string; - verificationToken?: string; - signingSecret?: string; - }; - [Channel.Viber]: { - enabled: boolean; - path: string; - accessToken: string; - sender: { - name: string; + initialState?: JsonObject; + channels?: + | { + messenger?: MessengerConnectorOptions & ChannelCommonConfig; + line?: LineConnectorOptions & ChannelCommonConfig; + telegram?: TelegramConnectorOptions & ChannelCommonConfig; + slack?: SlackConnectorOptions & ChannelCommonConfig; + viber?: ViberConnectorOptions & ChannelCommonConfig; + whatsapp?: WhatsappConnectorOptions & ChannelCommonConfig; + } + | { + [key in Exclude< + string, + 'messenger' | 'line' | 'telegram' | 'slack' | 'viber' | 'whatsapp' + >]?: { + connector: Connector; + } & ChannelCommonConfig; }; - }; - [Channel.Whatsapp]: { - enabled: boolean; - path: string; - accountSid: string; - authToken: string; - }; - }; }; -export type RequestContext = { +export type RequestContext< + B extends JsonObject = JsonObject, + H extends Record = {} +> = { + id?: string; method: string; path: string; query: Record; - headers: Record; + headers: IncomingHttpHeaders & H; + rawBody: string; + body: B; + params: Record; + url: string; +}; + +export type Client = object; + +export { Event } from './context/Event'; + +export type ChannelBot = { + webhookPath: string; + bot: Bot; }; diff --git a/packages/bottender/src/viber/ViberBot.ts b/packages/bottender/src/viber/ViberBot.ts index 9aff2f4d1..3e6984eaa 100644 --- a/packages/bottender/src/viber/ViberBot.ts +++ b/packages/bottender/src/viber/ViberBot.ts @@ -1,11 +1,12 @@ -import { ViberClient, ViberTypes } from 'messaging-api-viber'; +import { ViberClient } from 'messaging-api-viber'; -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; -import ViberConnector, { ViberRequestBody } from './ViberConnector'; +import ViberConnector, { ViberConnectorOptions } from './ViberConnector'; import ViberContext from './ViberContext'; import ViberEvent from './ViberEvent'; +import { ViberRequestBody } from './ViberTypes'; export default class ViberBot extends Bot< ViberRequestBody, @@ -14,19 +15,16 @@ export default class ViberBot extends Bot< ViberContext > { constructor({ - accessToken, - sender, sessionStore, sync, - origin, - }: { - accessToken: string; - sender: ViberTypes.Sender; + onRequest, + ...connectorOptions + }: ViberConnectorOptions & { sessionStore?: SessionStore; sync?: boolean; - origin?: string; + onRequest?: OnRequest; }) { - const connector = new ViberConnector({ accessToken, sender, origin }); - super({ connector, sessionStore, sync }); + const connector = new ViberConnector(connectorOptions); + super({ connector, sessionStore, sync, onRequest }); } } diff --git a/packages/bottender/src/viber/ViberConnector.ts b/packages/bottender/src/viber/ViberConnector.ts index f034219b5..0c01b6465 100644 --- a/packages/bottender/src/viber/ViberConnector.ts +++ b/packages/bottender/src/viber/ViberConnector.ts @@ -2,46 +2,52 @@ import crypto from 'crypto'; import { EventEmitter } from 'events'; import invariant from 'invariant'; -import { ViberClient, ViberTypes } from 'messaging-api-viber'; +import { JsonObject } from 'type-fest'; +import { ViberClient } from 'messaging-api-viber'; import { addedDiff } from 'deep-object-diff'; import Session from '../session/Session'; import { Connector } from '../bot/Connector'; -import { RequestContext } from '../types'; import ViberContext from './ViberContext'; -import ViberEvent, { ViberRawEvent } from './ViberEvent'; - -export type ViberRequestBody = ViberRawEvent; - -type ConstructorOptionsWithoutClient = { +import ViberEvent from './ViberEvent'; +import { + Sender, + ViberRawEvent, + ViberRequestBody, + ViberRequestContext, +} from './ViberTypes'; + +type ConnectorOptionsWithoutClient = { accessToken: string; - sender: ViberTypes.Sender; + sender: Sender; origin?: string; skipLegacyProfile?: boolean; }; -type ConstructorOptionsWithClient = { +type ConnectorOptionsWithClient = { client: ViberClient; skipLegacyProfile?: boolean; }; -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; +export type ViberConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; export default class ViberConnector - implements Connector { + implements Connector +{ _accessToken: string; _client: ViberClient; _skipLegacyProfile: boolean; - constructor(options: ConstructorOptions) { + constructor(options: ViberConnectorOptions) { const { skipLegacyProfile } = options; if ('client' in options) { this._client = options.client; + this._accessToken = this._client.accessToken; } else { const { accessToken, sender, origin } = options; @@ -50,17 +56,14 @@ export default class ViberConnector 'Viber access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.' ); - this._client = ViberClient.connect( - { - accessToken, - sender, - origin, - }, - sender - ); + this._client = new ViberClient({ + accessToken, + sender, + origin, + }); + this._accessToken = accessToken; } - this._accessToken = this._client.accessToken; this._skipLegacyProfile = typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true; } @@ -69,7 +72,7 @@ export default class ViberConnector return body; } - get platform(): string { + get platform(): 'viber' { return 'viber'; } @@ -149,8 +152,8 @@ export default class ViberConnector createContext(params: { event: ViberEvent; session: Session | null; - initialState?: Record | null; - requestContext?: RequestContext; + initialState?: JsonObject | null; + requestContext?: ViberRequestContext; emitter?: EventEmitter | null; }): ViberContext { return new ViberContext({ @@ -176,20 +179,11 @@ export default class ViberConnector return crypto.timingSafeEqual(bufferFromSignature, hashBufferFromBody); } - preprocess({ - method, - headers, - rawBody, - }: { - method: string; - headers: Record; - query: Record; - rawBody: string; - body: Record; - }) { + preprocess({ method, headers, rawBody }: ViberRequestContext) { if ( method.toLowerCase() !== 'post' || - this.verifySignature(rawBody, headers['x-viber-content-signature']) + (headers['x-viber-content-signature'] && + this.verifySignature(rawBody, headers['x-viber-content-signature'])) ) { return { shouldNext: true, diff --git a/packages/bottender/src/viber/ViberContext.ts b/packages/bottender/src/viber/ViberContext.ts index 9c49aa9b7..502be85da 100644 --- a/packages/bottender/src/viber/ViberContext.ts +++ b/packages/bottender/src/viber/ViberContext.ts @@ -1,4 +1,3 @@ -import sleep from 'delay'; import warning from 'warning'; import { ViberClient, ViberTypes } from 'messaging-api-viber'; @@ -15,16 +14,6 @@ class ViberContext extends Context { return 'viber'; } - /** - * Delay and show indicators for milliseconds. - * - */ - async typing(milliseconds: number): Promise { - if (milliseconds > 0) { - await sleep(milliseconds); - } - } - /** * Send text to the owner of the session. * diff --git a/packages/bottender/src/viber/ViberEvent.ts b/packages/bottender/src/viber/ViberEvent.ts index 4bf1d9661..133438b94 100644 --- a/packages/bottender/src/viber/ViberEvent.ts +++ b/packages/bottender/src/viber/ViberEvent.ts @@ -1,103 +1,16 @@ import { Event } from '../context/Event'; -type ViberUser = { - id: string; - name: string; - avatar: string; - country: string; - language: string; - apiVersion: number; -}; - -type SubscribedEvent = { - event: 'subscribed'; - timestamp: number; - user: ViberUser; - messageToken: number; -}; - -type UnsubscribedEvent = { - event: 'unsubscribed'; - timestamp: number; - userId: string; - messageToken: number; -}; - -type ConversationStartedEvent = { - event: 'conversation_started'; - timestamp: number; - messageToken: number; - type: 'open'; - context: string; - user: ViberUser; - subscribed: false; -}; - -type DeliveredEvent = { - event: 'delivered'; - timestamp: number; - messageToken: number; - userId: string; -}; - -type SeenEvent = { - event: 'seen'; - timestamp: number; - messageToken: number; - userId: string; -}; - -type FailedEvent = { - event: 'failed'; - timestamp: number; - messageToken: number; - userId: string; - desc: string; -}; - -type ViberMessage = { - type: - | 'text' - | 'picture' - | 'video' - | 'file' - | 'sticker' - | 'contact' - | 'url' - | 'location'; - text?: string; - media?: string; - location?: { - lat: string; - lot: string; - }; - contact?: { - name: string; - phoneNumber: string; - }; - trackingData?: string; - fileName?: string; - fileSize?: number; - duration?: number; - stickerId?: number; -}; - -type MessageEvent = { - event: 'message'; - timestamp: number; - messageToken: number; - sender: ViberUser; - message: ViberMessage; -}; - -export type ViberRawEvent = - | SubscribedEvent - | UnsubscribedEvent - | ConversationStartedEvent - | DeliveredEvent - | SeenEvent - | FailedEvent - | MessageEvent; +import { + ConversationStartedEvent, + DeliveredEvent, + FailedEvent, + MessageEvent, + SeenEvent, + SubscribedEvent, + UnsubscribedEvent, + ViberMessage, + ViberRawEvent, +} from './ViberTypes'; export default class ViberEvent implements Event { _rawEvent: ViberRawEvent; @@ -114,6 +27,14 @@ export default class ViberEvent implements Event { return this._rawEvent; } + /** + * The timestamp when the event was sent. + * + */ + get timestamp(): number { + return this._rawEvent.timestamp; + } + /** * Determine if the event is a message event. * @@ -196,7 +117,7 @@ export default class ViberEvent implements Event { */ get isFile(): boolean { return ( - this.isMessage && ((this.message as any) as ViberMessage).type === 'file' + this.isMessage && (this.message as any as ViberMessage).type === 'file' ); } diff --git a/packages/bottender/src/viber/ViberTypes.ts b/packages/bottender/src/viber/ViberTypes.ts new file mode 100644 index 000000000..8eed2f28b --- /dev/null +++ b/packages/bottender/src/viber/ViberTypes.ts @@ -0,0 +1,110 @@ +import { RequestContext } from '../types'; + +export * from 'messaging-api-viber/dist/ViberTypes'; +export { ViberConnectorOptions } from './ViberConnector'; + +export type ViberUser = { + id: string; + name: string; + avatar: string; + country: string; + language: string; + apiVersion: number; +}; + +export type SubscribedEvent = { + event: 'subscribed'; + timestamp: number; + user: ViberUser; + messageToken: number; +}; + +export type UnsubscribedEvent = { + event: 'unsubscribed'; + timestamp: number; + userId: string; + messageToken: number; +}; + +export type ConversationStartedEvent = { + event: 'conversation_started'; + timestamp: number; + messageToken: number; + type: 'open'; + context: string; + user: ViberUser; + subscribed: false; +}; + +export type DeliveredEvent = { + event: 'delivered'; + timestamp: number; + messageToken: number; + userId: string; +}; + +export type SeenEvent = { + event: 'seen'; + timestamp: number; + messageToken: number; + userId: string; +}; + +export type FailedEvent = { + event: 'failed'; + timestamp: number; + messageToken: number; + userId: string; + desc: string; +}; + +export type ViberMessage = { + type: + | 'text' + | 'picture' + | 'video' + | 'file' + | 'sticker' + | 'contact' + | 'url' + | 'location'; + text?: string; + media?: string; + location?: { + lat: string; + lot: string; + }; + contact?: { + name: string; + phoneNumber: string; + }; + trackingData?: string; + fileName?: string; + fileSize?: number; + duration?: number; + stickerId?: number; +}; + +export type MessageEvent = { + event: 'message'; + timestamp: number; + messageToken: number; + sender: ViberUser; + message: ViberMessage; +}; + +export type ViberRawEvent = + | SubscribedEvent + | UnsubscribedEvent + | ConversationStartedEvent + | DeliveredEvent + | SeenEvent + | FailedEvent + | MessageEvent; + +export type ViberRequestBody = ViberRawEvent; + +export type ViberRequestContext = RequestContext< + ViberRequestBody, + { 'x-viber-content-signature'?: string } +>; diff --git a/packages/bottender/src/viber/__tests__/ViberConnector.spec.ts b/packages/bottender/src/viber/__tests__/ViberConnector.spec.ts index d3bcdc27f..54a9b613c 100644 --- a/packages/bottender/src/viber/__tests__/ViberConnector.spec.ts +++ b/packages/bottender/src/viber/__tests__/ViberConnector.spec.ts @@ -1,140 +1,131 @@ import { ViberClient } from 'messaging-api-viber'; +import { mocked } from 'ts-jest/utils'; import ViberConnector from '../ViberConnector'; import ViberContext from '../ViberContext'; import ViberEvent from '../ViberEvent'; +import { ViberRequestBody } from '../ViberTypes'; -jest.unmock('messaging-api-viber'); +jest.mock('messaging-api-viber'); const ACCESS_TOKEN = 'ACCESS_TOKEN'; -const subscribedRequest = { - body: { - event: 'subscribed', - timestamp: 1457764197627, - user: { - id: '01234567890A=', - name: 'John McClane', - avatar: 'http://avatar.example.com', - country: 'UK', - language: 'en', - apiVersion: 1, - }, - messageToken: 4912661846655238145, +const subscribedRequest: ViberRequestBody = { + event: 'subscribed', + timestamp: 1457764197627, + user: { + id: '01234567890A=', + name: 'John McClane', + avatar: 'http://avatar.example.com', + country: 'UK', + language: 'en', + apiVersion: 1, }, + messageToken: 4912661846655238145, }; -const unsubscribedRequest = { - body: { - event: 'unsubscribed', - timestamp: 1457764197627, - userId: '01234567890A=', - messageToken: 4912661846655238145, - }, +const unsubscribedRequest: ViberRequestBody = { + event: 'unsubscribed', + timestamp: 1457764197627, + userId: '01234567890A=', + messageToken: 4912661846655238145, }; -const conversationStartedRequest = { - body: { - event: 'conversation_started', - timestamp: 1457764197627, - messageToken: 4912661846655238145, - type: 'open', - context: 'context information', - user: { - id: '01234567890A=', - name: 'John McClane', - avatar: 'http://avatar.example.com', - country: 'UK', - language: 'en', - apiVersion: 1, - }, - subscribed: false, +const conversationStartedRequest: ViberRequestBody = { + event: 'conversation_started', + timestamp: 1457764197627, + messageToken: 4912661846655238145, + type: 'open', + context: 'context information', + user: { + id: '01234567890A=', + name: 'John McClane', + avatar: 'http://avatar.example.com', + country: 'UK', + language: 'en', + apiVersion: 1, }, + subscribed: false, }; -const deliveredRequest = { - body: { - event: 'delivered', - timestamp: 1457764197627, - messageToken: 4912661846655238145, - userId: '01234567890A=', - }, +const deliveredRequest: ViberRequestBody = { + event: 'delivered', + timestamp: 1457764197627, + messageToken: 4912661846655238145, + userId: '01234567890A=', }; -const seenRequest = { - body: { - event: 'seen', - timestamp: 1457764197627, - messageToken: 4912661846655238145, - userId: '01234567890A=', - }, +const seenRequest: ViberRequestBody = { + event: 'seen', + timestamp: 1457764197627, + messageToken: 4912661846655238145, + userId: '01234567890A=', }; -const failedRequest = { - body: { - event: 'failed', - timestamp: 1457764197627, - messageToken: 4912661846655238145, - userId: '01234567890A=', - desc: 'failure description', - }, +const failedRequest: ViberRequestBody = { + event: 'failed', + timestamp: 1457764197627, + messageToken: 4912661846655238145, + userId: '01234567890A=', + desc: 'failure description', }; -const messageRequest = { - body: { - event: 'message', - timestamp: 1457764197627, - messageToken: 4912661846655238145, - sender: { - id: '01234567890A=', - name: 'John McClane', - avatar: 'http://avatar.example.com', - country: 'UK', - language: 'en', - apiVersion: 1, - }, - message: { - type: 'text', - text: 'a message to the service', - trackingData: 'tracking data', - }, +const messageRequest: ViberRequestBody = { + event: 'message', + timestamp: 1457764197627, + messageToken: 4912661846655238145, + sender: { + id: '01234567890A=', + name: 'John McClane', + avatar: 'http://avatar.example.com', + country: 'UK', + language: 'en', + apiVersion: 1, + }, + message: { + type: 'text', + text: 'a message to the service', + trackingData: 'tracking data', }, }; function setup() { - const mockViberClient = new ViberClient({ + const connector = new ViberConnector({ accessToken: ACCESS_TOKEN, sender: { name: 'sender' }, + skipLegacyProfile: false, }); - ViberClient.connect = jest.fn(); - ViberClient.connect.mockReturnValue(mockViberClient); + + const client = mocked(ViberClient).mock.instances[0]; return { - mockViberClient, - connector: new ViberConnector({ - accessToken: ACCESS_TOKEN, - sender: { name: 'sender' }, - skipLegacyProfile: false, - }), + connector, + client, }; } describe('#platform', () => { it('should be viber', () => { const { connector } = setup(); + expect(connector.platform).toBe('viber'); }); }); describe('#client', () => { it('should be client', () => { - const { connector, mockViberClient } = setup(); - expect(connector.client).toBe(mockViberClient); + const { connector, client } = setup(); + + expect(connector.client).toBe(client); }); it('support custom client', () => { - const client = {}; + const client = new ViberClient({ + accessToken: ACCESS_TOKEN, + sender: { name: 'sender' }, + }); const connector = new ViberConnector({ client }); + expect(connector.client).toBe(client); }); }); @@ -142,45 +133,57 @@ describe('#client', () => { describe('#getUniqueSessionKey', () => { it('extract correct user id from subscribedRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(subscribedRequest.body); + + const senderId = connector.getUniqueSessionKey(subscribedRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from unsubscribedRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(unsubscribedRequest.body); + + const senderId = connector.getUniqueSessionKey(unsubscribedRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from conversationStartedRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey( - conversationStartedRequest.body - ); + + const senderId = connector.getUniqueSessionKey(conversationStartedRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from deliveredRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(deliveredRequest.body); + + const senderId = connector.getUniqueSessionKey(deliveredRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from seenRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(seenRequest.body); + + const senderId = connector.getUniqueSessionKey(seenRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from failedRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(failedRequest.body); + + const senderId = connector.getUniqueSessionKey(failedRequest); + expect(senderId).toBe('01234567890A='); }); it('extract correct user id from messageRequest', () => { const { connector } = setup(); - const senderId = connector.getUniqueSessionKey(messageRequest.body); + + const senderId = connector.getUniqueSessionKey(messageRequest); + expect(senderId).toBe('01234567890A='); }); }); @@ -188,6 +191,7 @@ describe('#getUniqueSessionKey', () => { describe('#updateSession', () => { it('update session with data needed from subscribedRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', name: 'John McClane', @@ -199,7 +203,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, subscribedRequest.body); + await connector.updateSession(session, subscribedRequest); expect(session).toEqual({ user: { @@ -211,13 +215,14 @@ describe('#updateSession', () => { it('update session with data needed from unsubscribedRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', }; const session = {}; - await connector.updateSession(session, unsubscribedRequest.body); + await connector.updateSession(session, unsubscribedRequest); expect(session).toEqual({ user: { @@ -229,6 +234,7 @@ describe('#updateSession', () => { it('update session with data needed from conversationStartedRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', name: 'John McClane', @@ -240,7 +246,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, conversationStartedRequest.body); + await connector.updateSession(session, conversationStartedRequest); expect(session).toEqual({ user: { @@ -252,13 +258,14 @@ describe('#updateSession', () => { it('update session with data needed from deliveredRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', }; const session = {}; - await connector.updateSession(session, deliveredRequest.body); + await connector.updateSession(session, deliveredRequest); expect(session).toEqual({ user: { @@ -270,13 +277,14 @@ describe('#updateSession', () => { it('update session with data needed from seenRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', }; const session = {}; - await connector.updateSession(session, seenRequest.body); + await connector.updateSession(session, seenRequest); expect(session).toEqual({ user: { @@ -288,13 +296,14 @@ describe('#updateSession', () => { it('update session with data needed from failedRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', }; const session = {}; - await connector.updateSession(session, failedRequest.body); + await connector.updateSession(session, failedRequest); expect(session).toEqual({ user: { @@ -306,6 +315,7 @@ describe('#updateSession', () => { it('update session with data needed from messageRequest', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', name: 'John McClane', @@ -317,7 +327,7 @@ describe('#updateSession', () => { const session = {}; - await connector.updateSession(session, messageRequest.body); + await connector.updateSession(session, messageRequest); expect(session).toEqual({ user: { @@ -329,6 +339,7 @@ describe('#updateSession', () => { it('update session from id-only user', async () => { const { connector } = setup(); + const user = { id: '01234567890A=', name: 'John McClane', @@ -345,7 +356,7 @@ describe('#updateSession', () => { }, }; - await connector.updateSession(session, messageRequest.body); + await connector.updateSession(session, messageRequest); expect(session).toEqual({ user: { @@ -359,7 +370,8 @@ describe('#updateSession', () => { describe('#mapRequestToEvents', () => { it('should map request to ViberEvents', () => { const { connector } = setup(); - const events = connector.mapRequestToEvents(messageRequest.body); + + const events = connector.mapRequestToEvents(messageRequest); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(ViberEvent); @@ -369,7 +381,8 @@ describe('#mapRequestToEvents', () => { describe('#createContext', () => { it('should create ViberContext', () => { const { connector } = setup(); - const event = {}; + + const event = new ViberEvent(deliveredRequest); const session = {}; const context = connector.createContext({ @@ -406,8 +419,11 @@ describe('#preprocess', () => { 'x-viber-content-signature': 'abc', }, query: {}, - rawBody: '', - body: {}, + rawBody: JSON.stringify(subscribedRequest), + body: subscribedRequest, + path: '/webhooks/viber', + params: {}, + url: 'https://www.example.com/webhooks/viber', }) ).toEqual({ shouldNext: true, @@ -422,11 +438,14 @@ describe('#preprocess', () => { method: 'post', headers: { 'x-viber-content-signature': - 'e8b50346f80483e1a6050475af997f4e245a62879cb39732e6daf0f42ed1290c', + 'a4b6913412505d2c6031e7ec75e75c51bdc2fe30dc367301528a28b2008300a4', }, query: {}, - rawBody: '{}', - body: {}, + rawBody: JSON.stringify(subscribedRequest), + body: subscribedRequest, + path: '/webhooks/viber', + params: {}, + url: 'https://www.example.com/webhooks/viber', }) ).toEqual({ shouldNext: true, @@ -444,8 +463,11 @@ describe('#preprocess', () => { '250a5136d2f241195d4cb981a7293958434ec3ba9e50ed20788e9b030a1dd878', }, query: {}, - rawBody: '{}', - body: {}, + rawBody: JSON.stringify(subscribedRequest), + body: subscribedRequest, + path: '/webhooks/viber', + params: {}, + url: 'https://www.example.com/webhooks/viber', }) ).toEqual({ shouldNext: false, @@ -459,7 +481,7 @@ describe('#preprocess', () => { 'x-viber-content-signature': '250a5136d2f241195d4cb981a7293958434ec3ba9e50ed20788e9b030a1dd878', }, - rawBody: '{}', + rawBody: JSON.stringify(subscribedRequest), }, }, }, diff --git a/packages/bottender/src/viber/__tests__/ViberContext.spec.ts b/packages/bottender/src/viber/__tests__/ViberContext.spec.ts index 2074b08ec..3c47e6717 100644 --- a/packages/bottender/src/viber/__tests__/ViberContext.spec.ts +++ b/packages/bottender/src/viber/__tests__/ViberContext.spec.ts @@ -1,24 +1,17 @@ -jest.mock('delay'); +import warning from 'warning'; +import { ViberClient } from 'messaging-api-viber'; +import { mocked } from 'ts-jest/utils'; + +import ViberContext from '../ViberContext'; +import ViberEvent from '../ViberEvent'; +import { RichMedia, UserOnlineStatus, ViberRawEvent } from '../ViberTypes'; + jest.mock('messaging-api-viber'); jest.mock('warning'); -let ViberClient; -let ViberContext; -let ViberEvent; -let sleep; -let warning; - -beforeEach(() => { - /* eslint-disable global-require */ - ViberClient = require('messaging-api-viber').ViberClient; - ViberContext = require('../ViberContext').default; - ViberEvent = require('../ViberEvent').default; - sleep = require('delay'); - warning = require('warning'); - /* eslint-enable global-require */ -}); +const ACCESS_TOKEN = 'ACCESS_TOKEN'; -const rawEvent = { +const rawEvent: ViberRawEvent = { event: 'message', timestamp: 1457764197627, messageToken: 4912661846655238145, @@ -37,14 +30,28 @@ const rawEvent = { }, }; -const setup = ({ session } = { session: { user: { id: 'fakeUserId' } } }) => { - const client = ViberClient.connect(); - const args = { +const defaultSession = { user: { id: 'fakeUserId' } }; + +const setup = ( + { session }: { session: typeof defaultSession | null } = { + session: defaultSession, + } +): { + context: ViberContext; + session: typeof defaultSession | null; + client: ViberClient; +} => { + const client = new ViberClient({ + accessToken: ACCESS_TOKEN, + sender: { name: 'sender' }, + }); + + const context = new ViberContext({ client, event: new ViberEvent(rawEvent), session, - }; - const context = new ViberContext(args); + }); + return { context, session, @@ -52,28 +59,27 @@ const setup = ({ session } = { session: { user: { id: 'fakeUserId' } } }) => { }; }; -it('be defined', () => { - const { context } = setup(); - expect(context).toBeDefined(); -}); - it('#platform to be `viber`', () => { const { context } = setup(); + expect(context.platform).toBe('viber'); }); it('get #session works', () => { const { context, session } = setup(); + expect(context.session).toBe(session); }); it('get #event works', () => { const { context } = setup(); + expect(context.event).toBeInstanceOf(ViberEvent); }); it('get #client works', () => { const { context, client } = setup(); + expect(context.client).toBe(client); }); @@ -87,7 +93,7 @@ describe('#sendText', () => { }); it('should call warning and not to send if dont have session', async () => { - const { context, client } = setup({ session: false }); + const { context, client } = setup({ session: null }); await context.sendText('hello'); @@ -307,7 +313,7 @@ describe('#sendSticker', () => { }); }); -const richMedia = { +const richMedia: RichMedia = { type: 'rich_media', buttonsGroupColumns: 6, buttonsGroupRows: 7, @@ -323,8 +329,7 @@ const richMedia = { { columns: 6, rows: 2, - text: - 'Headphones with Microphone, On-ear Wired earphones
Sound Intone
$17.99', + text: 'Headphones with Microphone, On-ear Wired earphones
Sound Intone
$17.99', actionType: 'open-url', actionBody: 'https://www.google.com', textSize: 'medium', @@ -362,8 +367,7 @@ const richMedia = { { columns: 6, rows: 2, - text: - "Hanes Men's Humor Graphic T-Shirt
Hanes
$10.99", + text: "Hanes Men's Humor Graphic T-Shirt
Hanes
$10.99", actionType: 'open-url', actionBody: 'https://www.google.com', textSize: 'medium', @@ -434,7 +438,7 @@ describe('#getUserDetails', () => { deviceType: 'iPhone9,4', }; - client.getUserDetails.mockResolvedValue(user); + mocked(client.getUserDetails).mockResolvedValue(user); const result = await context.getUserDetails(); @@ -455,13 +459,13 @@ describe('#getOnlineStatus', () => { it('should call client.getOnlineStatus', async () => { const { context, client, session } = setup(); - const user = { + const user: UserOnlineStatus = { id: '01234567890=', onlineStatus: 0, onlineStatusMessage: 'online', }; - client.getOnlineStatus.mockResolvedValue([user]); + mocked(client.getOnlineStatus).mockResolvedValue([user]); const result = await context.getOnlineStatus(); @@ -477,21 +481,3 @@ describe('#getOnlineStatus', () => { expect(client.getOnlineStatus).not.toBeCalled(); }); }); - -describe('#typing', () => { - it('avoid delay 0', async () => { - const { context } = setup(); - - await context.typing(0); - - expect(sleep).not.toBeCalled(); - }); - - it('should call sleep', async () => { - const { context } = setup(); - - await context.typing(10); - - expect(sleep).toBeCalled(); - }); -}); diff --git a/packages/bottender/src/viber/__tests__/ViberEvent.spec.ts b/packages/bottender/src/viber/__tests__/ViberEvent.spec.ts index 3baa2911d..18c9b1b97 100644 --- a/packages/bottender/src/viber/__tests__/ViberEvent.spec.ts +++ b/packages/bottender/src/viber/__tests__/ViberEvent.spec.ts @@ -226,6 +226,10 @@ it('#rawEvent', () => { expect(new ViberEvent(textMessage).rawEvent).toEqual(textMessage); }); +it('#timestamp', () => { + expect(new ViberEvent(textMessage).timestamp).toEqual(1457764197627); +}); + it('#isMessage', () => { expect(new ViberEvent(subscribed).isMessage).toEqual(false); expect(new ViberEvent(unsubscribed).isMessage).toEqual(false); diff --git a/packages/bottender/src/viber/__tests__/routes.spec.ts b/packages/bottender/src/viber/__tests__/routes.spec.ts index 587fae133..fb9264c30 100644 --- a/packages/bottender/src/viber/__tests__/routes.spec.ts +++ b/packages/bottender/src/viber/__tests__/routes.spec.ts @@ -136,7 +136,7 @@ async function expectRouteNotMatchViberEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender/src/viber/routes.ts b/packages/bottender/src/viber/routes.ts index 92ae249e3..fbe4b3bc1 100644 --- a/packages/bottender/src/viber/routes.ts +++ b/packages/bottender/src/viber/routes.ts @@ -1,9 +1,10 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import ViberContext from './ViberContext'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -21,15 +22,13 @@ type Viber = Route & { failed: Route; }; -const viber: Viber = ( - action: Action -) => { +const viber: Viber = (action: Action) => { return route((context: C) => context.platform === 'viber', action); }; viber.any = viber; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.isMessage, action @@ -38,7 +37,7 @@ function message(action: Action) { viber.message = message; -function subscribed(action: Action) { +function subscribed(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.isSubscribed, action @@ -47,7 +46,7 @@ function subscribed(action: Action) { viber.subscribed = subscribed; -function unsubscribed(action: Action) { +function unsubscribed(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.isUnsubscribed, @@ -57,7 +56,7 @@ function unsubscribed(action: Action) { viber.unsubscribed = unsubscribed; -function conversationStarted( +function conversationStarted( action: Action ) { return route( @@ -69,7 +68,7 @@ function conversationStarted( viber.conversationStarted = conversationStarted; -function delivered(action: Action) { +function delivered(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.delivered, action @@ -78,7 +77,7 @@ function delivered(action: Action) { viber.delivered = delivered; -function seen(action: Action) { +function seen(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.seen, action @@ -87,7 +86,7 @@ function seen(action: Action) { viber.seen = seen; -function failed(action: Action) { +function failed(action: Action) { return route( (context: C) => context.platform === 'viber' && context.event.failed, action diff --git a/packages/bottender/src/whatsapp/WhatsappBot.ts b/packages/bottender/src/whatsapp/WhatsappBot.ts index 7500a02df..0ed75924f 100644 --- a/packages/bottender/src/whatsapp/WhatsappBot.ts +++ b/packages/bottender/src/whatsapp/WhatsappBot.ts @@ -1,10 +1,13 @@ -import Bot from '../bot/Bot'; +import Bot, { OnRequest } from '../bot/Bot'; import SessionStore from '../session/SessionStore'; import TwilioClient from './TwilioClient'; -import WhatsappConnector, { WhatsappRequestBody } from './WhatsappConnector'; +import WhatsappConnector, { + WhatsappConnectorOptions, +} from './WhatsappConnector'; import WhatsappContext from './WhatsappContext'; import WhatsappEvent from './WhatsappEvent'; +import { WhatsappRequestBody } from './WhatsappTypes'; export default class WhatsappBot extends Bot< WhatsappRequestBody, @@ -13,26 +16,16 @@ export default class WhatsappBot extends Bot< WhatsappContext > { constructor({ - accountSid, - authToken, - phoneNumber, sessionStore, sync, - origin, - }: { - accountSid: string; - authToken: string; - phoneNumber: string; + onRequest, + ...connectorOptions + }: WhatsappConnectorOptions & { sessionStore?: SessionStore; sync?: boolean; - origin?: string; + onRequest?: OnRequest; }) { - const connector = new WhatsappConnector({ - accountSid, - authToken, - phoneNumber, - origin, - }); - super({ connector, sessionStore, sync }); + const connector = new WhatsappConnector(connectorOptions); + super({ connector, sessionStore, sync, onRequest }); } } diff --git a/packages/bottender/src/whatsapp/WhatsappConnector.ts b/packages/bottender/src/whatsapp/WhatsappConnector.ts index 9a07f35f8..6b126ea87 100644 --- a/packages/bottender/src/whatsapp/WhatsappConnector.ts +++ b/packages/bottender/src/whatsapp/WhatsappConnector.ts @@ -1,6 +1,8 @@ import crypto from 'crypto'; import { EventEmitter } from 'events'; +import { JsonObject } from 'type-fest'; + import Session from '../session/Session'; import { Connector } from '../bot/Connector'; import { RequestContext } from '../types'; @@ -8,29 +10,28 @@ import { RequestContext } from '../types'; import TwilioClient from './TwilioClient'; import WhatsappContext from './WhatsappContext'; import WhatsappEvent from './WhatsappEvent'; +import { WhatsappRequestBody, WhatsappRequestContext } from './WhatsappTypes'; -export type WhatsappRequestBody = any; - -type ConstructorOptionsWithoutClient = { +type ConnectorOptionsWithoutClient = { accountSid: string; authToken: string; phoneNumber: string; origin?: string; }; -type ConstructorOptionsWithClient = { +type ConnectorOptionsWithClient = { client: TwilioClient; origin?: string; }; -type ConstructorOptions = - | ConstructorOptionsWithoutClient - | ConstructorOptionsWithClient; +export type WhatsappConnectorOptions = + | ConnectorOptionsWithoutClient + | ConnectorOptionsWithClient; function getExpectedTwilioSignature( authToken: string, url: string, - params: Record = {} + params: Record = {} ) { const data = Object.keys(params) .sort() @@ -43,16 +44,17 @@ function getExpectedTwilioSignature( } export default class WhatsappConnector - implements Connector { + implements Connector +{ _client: TwilioClient; - constructor(options: ConstructorOptions) { + constructor(options: WhatsappConnectorOptions) { if ('client' in options) { this._client = options.client; } else { const { accountSid, authToken, phoneNumber, origin } = options; - this._client = TwilioClient.connect({ + this._client = new TwilioClient({ accountSid, authToken, phoneNumber, @@ -61,7 +63,7 @@ export default class WhatsappConnector } } - get platform(): string { + get platform(): 'whatsapp' { return 'whatsapp'; } @@ -100,7 +102,7 @@ export default class WhatsappConnector createContext(params: { event: WhatsappEvent; session: Session | null; - initialState?: Record | null; + initialState?: JsonObject | null; requestContext?: RequestContext; emitter?: EventEmitter | null; }): WhatsappContext { @@ -115,10 +117,13 @@ export default class WhatsappConnector url, headers, }: { - headers: Record; + headers: WhatsappRequestContext['headers']; url: string; - body: Record; + body: WhatsappRequestBody; }): boolean { + if (!headers['x-twilio-signature']) { + return false; + } const authToken = this._client.authToken; const bufferFromActualSignature = Buffer.from( @@ -143,19 +148,7 @@ export default class WhatsappConnector ); } - preprocess({ - url, - headers, - rawBody, - body, - }: { - method: string; - url: string; - headers: Record; - query: Record; - rawBody: string; - body: Record; - }) { + preprocess({ url, headers, rawBody, body }: WhatsappRequestContext) { if (this.verifySignature({ body, url, headers })) { return { shouldNext: true, diff --git a/packages/bottender/src/whatsapp/WhatsappTypes.ts b/packages/bottender/src/whatsapp/WhatsappTypes.ts index e1eb12c8d..c2eeb29e7 100644 --- a/packages/bottender/src/whatsapp/WhatsappTypes.ts +++ b/packages/bottender/src/whatsapp/WhatsappTypes.ts @@ -1,3 +1,7 @@ +import { RequestContext } from '../types'; + +export { WhatsappConnectorOptions } from './WhatsappConnector'; + export type MessageReceivedCommon = { /** * A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API. @@ -129,3 +133,10 @@ export type WhatsappRawEvent = | MessageSent | MessageDelivered | MessageRead; + +export type WhatsappRequestBody = any; + +export type WhatsappRequestContext = RequestContext< + WhatsappRequestBody, + { 'x-twilio-signature'?: string } +>; diff --git a/packages/bottender/src/whatsapp/__tests__/WhatsappConnector.spec.ts b/packages/bottender/src/whatsapp/__tests__/WhatsappConnector.spec.ts index 621ffd539..7ba7ffe1a 100644 --- a/packages/bottender/src/whatsapp/__tests__/WhatsappConnector.spec.ts +++ b/packages/bottender/src/whatsapp/__tests__/WhatsappConnector.spec.ts @@ -93,7 +93,7 @@ describe('#client', () => { }); it('support custom client', () => { - const client = TwilioClient.connect({ + const client = new TwilioClient({ accountSid: 'ACCOUNT_SID', authToken: 'AUTH_TOKEN', }); diff --git a/packages/bottender/src/whatsapp/__tests__/WhatsappEvent.spec.ts b/packages/bottender/src/whatsapp/__tests__/WhatsappEvent.spec.ts index 835ecf429..cfd414579 100644 --- a/packages/bottender/src/whatsapp/__tests__/WhatsappEvent.spec.ts +++ b/packages/bottender/src/whatsapp/__tests__/WhatsappEvent.spec.ts @@ -145,8 +145,7 @@ it('#media', () => { expect(new WhatsappEvent(textMessageReceived).media).toEqual(null); expect(new WhatsappEvent(imageMessageReceived).media).toEqual({ contentType: 'image/jpeg', - url: - 'https://api.twilio.com/2010-04-01/Accounts/ACf19dfb164f82b2c9d6178c6ada3XXXXX/Messages/MMad0463f6e2a946b3fc91d9a04a2XXXXX/Media/MEfaf3decca478ebeb4924fe523ff7fdb2', + url: 'https://api.twilio.com/2010-04-01/Accounts/ACf19dfb164f82b2c9d6178c6ada3XXXXX/Messages/MMad0463f6e2a946b3fc91d9a04a2XXXXX/Media/MEfaf3decca478ebeb4924fe523ff7fdb2', }); expect(new WhatsappEvent(messageSent).media).toEqual(null); expect(new WhatsappEvent(messageDelivered).media).toEqual(null); diff --git a/packages/bottender/src/whatsapp/__tests__/routes.spec.ts b/packages/bottender/src/whatsapp/__tests__/routes.spec.ts index 877de22e6..e1a2cbe81 100644 --- a/packages/bottender/src/whatsapp/__tests__/routes.spec.ts +++ b/packages/bottender/src/whatsapp/__tests__/routes.spec.ts @@ -133,7 +133,7 @@ async function expectRouteNotMatchWhatsappEvent({ route, event }) { }); } -class TestContext extends Context { +class TestContext extends Context { get platform() { return 'test'; } diff --git a/packages/bottender/src/whatsapp/routes.ts b/packages/bottender/src/whatsapp/routes.ts index 4effcc16a..9774431bc 100644 --- a/packages/bottender/src/whatsapp/routes.ts +++ b/packages/bottender/src/whatsapp/routes.ts @@ -1,9 +1,10 @@ -import { Action, AnyContext } from '../types'; +import Context from '../context/Context'; +import { Action } from '../types'; import { RoutePredicate, route } from '../router'; import WhatsappContext from './WhatsappContext'; -type Route = ( +type Route = ( action: Action ) => { predicate: RoutePredicate; @@ -20,7 +21,7 @@ type Whatsapp = Route & { read: Route; }; -const whatsapp: Whatsapp = ( +const whatsapp: Whatsapp = ( action: Action ) => { return route((context: C) => context.platform === 'whatsapp', action); @@ -28,7 +29,7 @@ const whatsapp: Whatsapp = ( whatsapp.any = whatsapp; -function message(action: Action) { +function message(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isMessage, action @@ -37,7 +38,7 @@ function message(action: Action) { whatsapp.message = message; -function media(action: Action) { +function media(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isMedia, action @@ -46,7 +47,7 @@ function media(action: Action) { whatsapp.media = media; -function received(action: Action) { +function received(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isReceived, action @@ -55,7 +56,7 @@ function received(action: Action) { whatsapp.received = received; -function sent(action: Action) { +function sent(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isSent, action @@ -64,7 +65,7 @@ function sent(action: Action) { whatsapp.sent = sent; -function delivered(action: Action) { +function delivered(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isDelivered, @@ -74,7 +75,7 @@ function delivered(action: Action) { whatsapp.delivered = delivered; -function read(action: Action) { +function read(action: Action) { return route( (context: C) => context.platform === 'whatsapp' && context.event.isRead, action diff --git a/packages/bottender/src/withProps.ts b/packages/bottender/src/withProps.ts index 46b4f7516..71c76fc39 100644 --- a/packages/bottender/src/withProps.ts +++ b/packages/bottender/src/withProps.ts @@ -1,8 +1,9 @@ import partial from 'lodash/partial'; -import { Action, AnyContext } from './types'; +import Context from './context/Context'; +import { Action } from './types'; -function withProps>( +function withProps>( action: Action, props: P ): Action { diff --git a/packages/create-bottender-app/package.json b/packages/create-bottender-app/package.json index a847b28e8..6e51f2656 100644 --- a/packages/create-bottender-app/package.json +++ b/packages/create-bottender-app/package.json @@ -1,6 +1,6 @@ { "name": "create-bottender-app", - "version": "1.4.12", + "version": "1.5.1-alpha.8", "bin": { "create-bottender-app": "bin/cli.js" }, diff --git a/packages/create-bottender-app/src/index.ts b/packages/create-bottender-app/src/index.ts index f3aedbcb8..cfe47273e 100644 --- a/packages/create-bottender-app/src/index.ts +++ b/packages/create-bottender-app/src/index.ts @@ -32,7 +32,7 @@ const program = new commander.Command(pkg.name) .version(pkg.version) .arguments('') .usage(`${chalk.green('')} [options]`) - .action(name => { + .action((name) => { projectName = name; }) .option('--info', 'print environment debug info') @@ -123,12 +123,13 @@ const isSafeToCreateProjectIn = (root: string, name: string): boolean => { const conflicts = fs .readdirSync(root) - .filter(file => !validFiles.includes(file)) + .filter((file) => !validFiles.includes(file)) // IntelliJ IDEA creates module files before CRA is launched - .filter(file => !/\.iml$/.test(file)) + .filter((file) => !/\.iml$/.test(file)) // Don't treat log files from previous installation as conflicts .filter( - file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0) + (file) => + !errorLogFilePatterns.some((pattern) => file.indexOf(pattern) === 0) ); if (conflicts.length > 0) { @@ -149,8 +150,8 @@ const isSafeToCreateProjectIn = (root: string, name: string): boolean => { // Remove any remnant files from a previous installation const currentFiles = fs.readdirSync(path.join(root)); - currentFiles.forEach(file => { - errorLogFilePatterns.forEach(errorLogFilePattern => { + currentFiles.forEach((file) => { + errorLogFilePatterns.forEach((errorLogFilePattern) => { // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files if (file.indexOf(errorLogFilePattern) === 0) { fs.removeSync(path.join(root, file)); @@ -162,7 +163,7 @@ const isSafeToCreateProjectIn = (root: string, name: string): boolean => { const printValidationResults = (results: string[]): void => { if (typeof results !== 'undefined') { - results.forEach(Error => { + results.forEach((Error) => { error(` * ${Error}`); }); } @@ -218,7 +219,7 @@ const install = ( } const child = spawn(command, args, { stdio: 'inherit' }); - child.on('close', code => { + child.on('close', (code) => { if (code !== 0) { const err = new Error('install failed'); (err as any).command = `${command} ${args.join(' ')}`; @@ -302,8 +303,8 @@ const run = async ( 'node_modules', ]; const currentFiles = fs.readdirSync(path.join(root)); - currentFiles.forEach(file => { - knownGeneratedFiles.forEach(fileToMatch => { + currentFiles.forEach((file) => { + knownGeneratedFiles.forEach((fileToMatch) => { if ( (fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) || file === fileToMatch diff --git a/packages/create-bottender-app/src/utils/generateAppEntry.ts b/packages/create-bottender-app/src/utils/generateAppEntry.ts index 5a7a36728..f6788e9be 100644 --- a/packages/create-bottender-app/src/utils/generateAppEntry.ts +++ b/packages/create-bottender-app/src/utils/generateAppEntry.ts @@ -22,7 +22,8 @@ module.exports = async function App(context) { } const contexts = platforms.map( - platform => `${platform.charAt(0).toUpperCase()}${platform.slice(1)}Context` + (platform) => + `${platform.charAt(0).toUpperCase()}${platform.slice(1)}Context` ); return prettier.format( diff --git a/tsconfig.build.json b/tsconfig.build.json index d34409a85..5588b96fe 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -8,6 +8,7 @@ { "path": "./packages/bottender" }, { "path": "./packages/bottender-dialogflow" }, { "path": "./packages/bottender-express" }, + { "path": "./packages/bottender-facebook" }, { "path": "./packages/bottender-handlers" }, { "path": "./packages/bottender-luis" }, { "path": "./packages/bottender-qna-maker" }, diff --git a/types/dialogflow.d.ts b/types/dialogflow.d.ts deleted file mode 100644 index 3cfb9daf3..000000000 --- a/types/dialogflow.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'dialogflow'; diff --git a/types/fromentries.d.ts b/types/fromentries.d.ts deleted file mode 100644 index 9dabb59d9..000000000 --- a/types/fromentries.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'fromentries'; diff --git a/types/messenger-batch.d.ts b/types/messenger-batch.d.ts deleted file mode 100644 index 2d5c29f14..000000000 --- a/types/messenger-batch.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'messenger-batch'; diff --git a/yarn.lock b/yarn.lock index 3039f626b..0275254e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -9,6 +16,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" + integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== + dependencies: + "@babel/highlight" "^7.10.1" + "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -115,6 +129,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== +"@babel/helper-plugin-utils@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127" + integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA== + "@babel/helper-plugin-utils@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" @@ -134,6 +153,16 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-validator-identifier@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" + integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== + +"@babel/helper-validator-identifier@^7.14.5": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" + integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== + "@babel/helpers@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" @@ -161,6 +190,24 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" + integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== + dependencies: + "@babel/helper-validator-identifier" "^7.10.1" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/highlight@^7.10.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" @@ -175,38 +222,85 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.10.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" + integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== + "@babel/parser@^7.7.5", "@babel/parser@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== -"@babel/plugin-syntax-bigint@^7.0.0": +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-object-rest-spread@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" - integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5" + integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/runtime@^7.4.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" - integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: - regenerator-runtime "^0.13.2" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/runtime@^7.6.3": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" - integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.1.tgz#fffee77b4934ce77f3b427649ecdddbec1958550" + integrity sha512-XyHIFa9kdrgJS91CUH+ccPVTnJShr8nLGc5bG2IhGXv5p1Rd+8BleGE5yzIg2Nc1QZAdHDa0Qp4m6066OL96Iw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99" + integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg== dependencies: - regenerator-runtime "^0.13.2" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" "@babel/template@^7.1.0", "@babel/template@^7.4.4": version "7.4.4" @@ -217,6 +311,15 @@ "@babel/parser" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/template@^7.3.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" + integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig== + dependencies: + "@babel/code-frame" "^7.10.1" + "@babel/parser" "^7.10.1" + "@babel/types" "^7.10.1" + "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -265,6 +368,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.1", "@babel/types@^7.3.3": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d" + integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng== + dependencies: + "@babel/helper-validator-identifier" "^7.10.1" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@babel/types@^7.8.3", "@babel/types@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" @@ -287,6 +399,21 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@evocateur/libnpmaccess@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" @@ -361,20 +488,31 @@ unique-filename "^1.1.1" which "^1.3.1" -"@grpc/grpc-js@^0.6.12": - version "0.6.15" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.6.15.tgz#534d1051ddced4e5e5849212789dd64014214dd4" - integrity sha512-BFK5YMu8JILedibo0nr3NYM0ZC5hCZuXtzk10wEUp3d3pH11PjdvTfN1yEJ0VsfBY5Gtp3WOQ+t7Byq0NzH/iQ== +"@google-cloud/dialogflow@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@google-cloud/dialogflow/-/dialogflow-4.3.1.tgz#7974b38c7cf2c75ea2a54e6327882ec1baacb600" + integrity sha512-3jtGrqKZn0C9mUKjuQo/PnR95ng27jS6FzZTnnF24KslEd16Ps+vcFJKlHvd8OZEbmKNt2BrhvesV22HgWlIFQ== dependencies: - semver "^6.2.0" + google-gax "^2.24.1" + protobufjs "^6.8.9" -"@grpc/proto-loader@^0.5.1": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.3.tgz#a233070720bf7560c4d70e29e7950c72549a132c" - integrity sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ== +"@grpc/grpc-js@~1.3.0": + version "1.3.7" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.3.7.tgz#58b687aff93b743aafde237fd2ee9a3259d7f2d8" + integrity sha512-CKQVuwuSPh40tgOkR7c0ZisxYRiN05PcKPW72mQL5y++qd7CwBRoaJZvU5xfXnCJDFBmS3qZGQ71Frx6Ofo2XA== + dependencies: + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.6.1": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.4.tgz#5438c0d771e92274e77e631babdc14456441cbdc" + integrity sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ== dependencies: + "@types/long" "^4.0.1" lodash.camelcase "^4.3.0" - protobufjs "^6.8.6" + long "^4.0.0" + protobufjs "^6.10.0" + yargs "^16.1.1" "@hapi/address@2.x.x": version "2.1.2" @@ -408,6 +546,20 @@ dependencies: "@hapi/hoek" "8.x.x" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -423,182 +575,190 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.1.0.tgz#1fc765d44a1e11aec5029c08e798246bd37075ab" - integrity sha512-3P1DpqAMK/L07ag/Y9/Jup5iDEG9P4pRAuZiMQnU0JB3UOvCyYCjCoxr7sIA80SeyUCUKrr24fKAxVpmBgQonA== +"@jest/console@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.1.0.tgz#f67c89e4f4d04dbcf7b052aed5ab9c74f915b954" + integrity sha512-+0lpTHMd/8pJp+Nd4lyip+/Iyf2dZJvcCqrlkeZQoQid+JlThA4M9vxHtheyrQ99jJTMQam+es4BcvZ5W5cC3A== dependencies: - "@jest/source-map" "^25.1.0" - chalk "^3.0.0" - jest-util "^25.1.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" + jest-message-util "^26.1.0" + jest-util "^26.1.0" slash "^3.0.0" -"@jest/core@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.1.0.tgz#3d4634fc3348bb2d7532915d67781cdac0869e47" - integrity sha512-iz05+NmwCmZRzMXvMo6KFipW7nzhbpEawrKrkkdJzgytavPse0biEnCNr2wRlyCsp3SmKaEY+SGv7YWYQnIdig== +"@jest/core@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.1.0.tgz#4580555b522de412a7998b3938c851e4f9da1c18" + integrity sha512-zyizYmDJOOVke4OO/De//aiv8b07OwZzL2cfsvWF3q9YssfpcKfcnZAwDY8f+A76xXSMMYe8i/f/LPocLlByfw== dependencies: - "@jest/console" "^25.1.0" - "@jest/reporters" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^26.1.0" + "@jest/reporters" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/transform" "^26.1.0" + "@jest/types" "^26.1.0" ansi-escapes "^4.2.1" - chalk "^3.0.0" + chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.3" - jest-changed-files "^25.1.0" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-resolve-dependencies "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - jest-watcher "^25.1.0" + graceful-fs "^4.2.4" + jest-changed-files "^26.1.0" + jest-config "^26.1.0" + jest-haste-map "^26.1.0" + jest-message-util "^26.1.0" + jest-regex-util "^26.0.0" + jest-resolve "^26.1.0" + jest-resolve-dependencies "^26.1.0" + jest-runner "^26.1.0" + jest-runtime "^26.1.0" + jest-snapshot "^26.1.0" + jest-util "^26.1.0" + jest-validate "^26.1.0" + jest-watcher "^26.1.0" micromatch "^4.0.2" p-each-series "^2.1.0" - realpath-native "^1.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.1.0.tgz#4a97f64770c9d075f5d2b662b5169207f0a3f787" - integrity sha512-cTpUtsjU4cum53VqBDlcW0E4KbQF03Cn0jckGPW/5rrE9tb+porD3+hhLtHAwhthsqfyF+bizyodTlsRA++sHg== +"@jest/environment@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.1.0.tgz#378853bcdd1c2443b4555ab908cfbabb851e96da" + integrity sha512-86+DNcGongbX7ai/KE/S3/NcUVZfrwvFzOOWX/W+OOTvTds7j07LtC+MgGydH5c8Ri3uIrvdmVgd1xFD5zt/xA== + dependencies: + "@jest/fake-timers" "^26.1.0" + "@jest/types" "^26.1.0" + jest-mock "^26.1.0" + +"@jest/fake-timers@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.1.0.tgz#9a76b7a94c351cdbc0ad53e5a748789f819a65fe" + integrity sha512-Y5F3kBVWxhau3TJ825iuWy++BAuQzK/xEa+wD9vDH3RytW9f2DbMVodfUQC54rZDX3POqdxCgcKdgcOL0rYUpA== dependencies: - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" + "@jest/types" "^26.1.0" + "@sinonjs/fake-timers" "^6.0.1" + jest-message-util "^26.1.0" + jest-mock "^26.1.0" + jest-util "^26.1.0" -"@jest/fake-timers@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.1.0.tgz#a1e0eff51ffdbb13ee81f35b52e0c1c11a350ce8" - integrity sha512-Eu3dysBzSAO1lD7cylZd/CVKdZZ1/43SF35iYBNV1Lvvn2Undp3Grwsv8PrzvbLhqwRzDd4zxrY4gsiHc+wygQ== +"@jest/globals@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.1.0.tgz#6cc5d7cbb79b76b120f2403d7d755693cf063ab1" + integrity sha512-MKiHPNaT+ZoG85oMaYUmGHEqu98y3WO2yeIDJrs2sJqHhYOy3Z6F7F/luzFomRQ8SQ1wEkmahFAz2291Iv8EAw== dependencies: - "@jest/types" "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - lolex "^5.0.0" + "@jest/environment" "^26.1.0" + "@jest/types" "^26.1.0" + expect "^26.1.0" -"@jest/reporters@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.1.0.tgz#9178ecf136c48f125674ac328f82ddea46e482b0" - integrity sha512-ORLT7hq2acJQa8N+NKfs68ZtHFnJPxsGqmofxW7v7urVhzJvpKZG9M7FAcgh9Ee1ZbCteMrirHA3m5JfBtAaDg== +"@jest/reporters@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.1.0.tgz#08952e90c90282e14ff49e927bdf1873617dae78" + integrity sha512-SVAysur9FOIojJbF4wLP0TybmqwDkdnFxHSPzHMMIYyBtldCW9gG+Q5xWjpMFyErDiwlRuPyMSJSU64A67Pazg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" + "@jest/console" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/transform" "^26.1.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" + graceful-fs "^4.2.4" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.0" + istanbul-lib-instrument "^4.0.3" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.0" - jest-haste-map "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.1.0" + jest-resolve "^26.1.0" + jest-util "^26.1.0" + jest-worker "^26.1.0" slash "^3.0.0" source-map "^0.6.0" - string-length "^3.1.0" + string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^4.0.1" + v8-to-istanbul "^4.1.3" optionalDependencies: - node-notifier "^6.0.0" + node-notifier "^7.0.0" -"@jest/source-map@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.1.0.tgz#b012e6c469ccdbc379413f5c1b1ffb7ba7034fb0" - integrity sha512-ohf2iKT0xnLWcIUhL6U6QN+CwFWf9XnrM2a6ybL9NXxJjgYijjLSitkYHIdzkd8wFliH73qj/+epIpTiWjRtAA== +"@jest/source-map@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.1.0.tgz#a6a020d00e7d9478f4b690167c5e8b77e63adb26" + integrity sha512-XYRPYx4eEVX15cMT9mstnO7hkHP3krNtKfxUYd8L7gbtia8JvZZ6bMzSwa6IQJENbudTwKMw5R1BePRD+bkEmA== dependencies: callsites "^3.0.0" - graceful-fs "^4.2.3" + graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.1.0.tgz#847af2972c1df9822a8200457e64be4ff62821f7" - integrity sha512-FZzSo36h++U93vNWZ0KgvlNuZ9pnDnztvaM7P/UcTx87aPDotG18bXifkf1Ji44B7k/eIatmMzkBapnAzjkJkg== +"@jest/test-result@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.1.0.tgz#a93fa15b21ad3c7ceb21c2b4c35be2e407d8e971" + integrity sha512-Xz44mhXph93EYMA8aYDz+75mFbarTV/d/x0yMdI3tfSRs/vh4CqSxgzVmCps1fPkHDCtn0tU8IH9iCKgGeGpfw== dependencies: - "@jest/console" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^26.1.0" + "@jest/types" "^26.1.0" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz#4df47208542f0065f356fcdb80026e3c042851ab" - integrity sha512-WgZLRgVr2b4l/7ED1J1RJQBOharxS11EFhmwDqknpknE0Pm87HLZVS2Asuuw+HQdfQvm2aXL2FvvBLxOD1D0iw== +"@jest/test-sequencer@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.1.0.tgz#41a6fc8b850c3f33f48288ea9ea517c047e7f14e" + integrity sha512-Z/hcK+rTq56E6sBwMoQhSRDVjqrGtj1y14e2bIgcowARaIE1SgOanwx6gvY4Q9gTKMoZQXbXvptji+q5GYxa6Q== dependencies: - "@jest/test-result" "^25.1.0" - jest-haste-map "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" + "@jest/test-result" "^26.1.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.1.0" + jest-runner "^26.1.0" + jest-runtime "^26.1.0" -"@jest/transform@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.1.0.tgz#221f354f512b4628d88ce776d5b9e601028ea9da" - integrity sha512-4ktrQ2TPREVeM+KxB4zskAT84SnmG1vaz4S+51aTefyqn3zocZUnliLLm5Fsl85I3p/kFPN4CRp1RElIfXGegQ== +"@jest/transform@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.1.0.tgz#697f48898c2a2787c9b4cb71d09d7e617464e509" + integrity sha512-ICPm6sUXmZJieq45ix28k0s+d/z2E8CHDsq+WwtWI6kW8m7I8kPqarSEcUN86entHQ570ZBRci5OWaKL0wlAWw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^25.1.0" + "@jest/types" "^26.1.0" babel-plugin-istanbul "^6.0.0" - chalk "^3.0.0" + chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.3" - jest-haste-map "^25.1.0" - jest-regex-util "^25.1.0" - jest-util "^25.1.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.1.0" + jest-regex-util "^26.0.0" + jest-util "^26.1.0" micromatch "^4.0.2" pirates "^4.0.1" - realpath-native "^1.1.0" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" - integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== +"@jest/types@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" + integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^13.0.0" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" -"@jest/types@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" - integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== +"@jest/types@^26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.1.0.tgz#f8afaaaeeb23b5cad49dd1f7779689941dcb6057" + integrity sha512-GXigDDsp6ZlNMhXQDeuy/iYCDsRIHJabWtDzvnn36+aqFfG14JmFV0e/iXxY4SP9vbXSiPNOWdehU5MeqrYHBQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^15.0.0" - chalk "^3.0.0" + chalk "^4.0.0" -"@lerna/add@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.20.0.tgz#bea7edf36fc93fb72ec34cb9ba854c48d4abf309" - integrity sha512-AnH1oRIEEg/VDa3SjYq4x1/UglEAvrZuV0WssHUMN81RTZgQk3we+Mv3qZNddrZ/fBcZu2IAdN/EQ3+ie2JxKQ== +"@lerna/add@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" + integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A== dependencies: "@evocateur/pacote" "^9.6.3" - "@lerna/bootstrap" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/bootstrap" "3.21.0" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/npm-conf" "3.16.0" "@lerna/validation-error" "3.13.0" @@ -607,12 +767,12 @@ p-map "^2.1.0" semver "^6.2.0" -"@lerna/bootstrap@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-3.20.0.tgz#635d71046830f208e851ab429a63da1747589e37" - integrity sha512-Wylullx3uthKE7r4izo09qeRGL20Y5yONlQEjPCfnbxCC2Elu+QcPu4RC6kqKQ7b+g7pdC3OOgcHZjngrwr5XQ== +"@lerna/bootstrap@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6" + integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/has-npm-version" "3.16.5" "@lerna/npm-install" "3.16.5" @@ -636,13 +796,13 @@ read-package-tree "^5.1.6" semver "^6.2.0" -"@lerna/changed@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-3.20.0.tgz#66b97ebd6c8f8d207152ee524a0791846a9097ae" - integrity sha512-+hzMFSldbRPulZ0vbKk6RD9f36gaH3Osjx34wrrZ62VB4pKmjyuS/rxVYkCA3viPLHoiIw2F8zHM5BdYoDSbjw== +"@lerna/changed@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b" + integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw== dependencies: "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/listable" "3.18.5" "@lerna/output" "3.13.0" @@ -664,12 +824,12 @@ execa "^1.0.0" strong-log-transformer "^2.0.0" -"@lerna/clean@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-3.20.0.tgz#ba777e373ddeae63e57860df75d47a9e5264c5b2" - integrity sha512-9ZdYrrjQvR5wNXmHfDsfjWjp0foOkCwKe3hrckTzkAeQA1ibyz5llGwz5e1AeFrV12e2/OLajVqYfe+qdkZUgg== +"@lerna/clean@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d" + integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/prompt" "3.18.5" "@lerna/pulse-till-done" "3.13.0" @@ -709,14 +869,14 @@ npmlog "^4.1.2" slash "^2.0.0" -"@lerna/command@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/command/-/command-3.18.5.tgz#14c6d2454adbfd365f8027201523e6c289cd3cd9" - integrity sha512-36EnqR59yaTU4HrR1C9XDFti2jRx0BgpIUBeWn129LZZB8kAB3ov1/dJNa1KcNRKp91DncoKHLY99FZ6zTNpMQ== +"@lerna/command@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7" + integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ== dependencies: "@lerna/child-process" "3.16.5" "@lerna/package-graph" "3.18.5" - "@lerna/project" "3.18.0" + "@lerna/project" "3.21.0" "@lerna/validation-error" "3.13.0" "@lerna/write-log-file" "3.13.0" clone-deep "^4.0.1" @@ -725,10 +885,10 @@ is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.18.5.tgz#08efd2e5b45acfaf3f151a53a3ec7ecade58a7bc" - integrity sha512-qcvXIEJ3qSgalxXnQ7Yxp5H9Ta5TVyai6vEor6AAEHc20WiO7UIdbLDCxBtiiHMdGdpH85dTYlsoYUwsCJu3HQ== +"@lerna/conventional-commits@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" + integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== dependencies: "@lerna/validation-error" "3.13.0" conventional-changelog-angular "^5.0.3" @@ -751,14 +911,14 @@ fs-extra "^8.1.0" npmlog "^4.1.2" -"@lerna/create@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.18.5.tgz#11ac539f069248eaf7bc4c42e237784330f4fc47" - integrity sha512-cHpjocbpKmLopCuZFI7cKEM3E/QY8y+yC7VtZ4FQRSaLU8D8i2xXtXmYaP1GOlVNavji0iwoXjuNpnRMInIr2g== +"@lerna/create@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" + integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== dependencies: "@evocateur/pacote" "^9.6.3" "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/npm-conf" "3.16.0" "@lerna/validation-error" "3.13.0" camelcase "^5.0.0" @@ -783,23 +943,23 @@ "@lerna/child-process" "3.16.5" npmlog "^4.1.2" -"@lerna/diff@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-3.18.5.tgz#e9e2cb882f84d5b84f0487c612137305f07accbc" - integrity sha512-u90lGs+B8DRA9Z/2xX4YaS3h9X6GbypmGV6ITzx9+1Ga12UWGTVlKaCXBgONMBjzJDzAQOK8qPTwLA57SeBLgA== +"@lerna/diff@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d" + integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/validation-error" "3.13.0" npmlog "^4.1.2" -"@lerna/exec@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-3.20.0.tgz#29f0c01aee2340eb46f90706731fef2062a49639" - integrity sha512-pS1mmC7kzV668rHLWuv31ClngqeXjeHC8kJuM+W2D6IpUVMGQHLcCTYLudFgQsuKGVpl0DGNYG+sjLhAPiiu6A== +"@lerna/exec@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26" + integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/profiler" "3.20.0" "@lerna/run-topologically" "3.18.5" @@ -842,13 +1002,13 @@ ssri "^6.0.1" tar "^4.4.8" -"@lerna/github-client@3.16.5": - version "3.16.5" - resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e" - integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw== +"@lerna/github-client@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" + integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== dependencies: "@lerna/child-process" "3.16.5" - "@octokit/plugin-enterprise-rest" "^3.6.1" + "@octokit/plugin-enterprise-rest" "^6.0.1" "@octokit/rest" "^16.28.4" git-url-parse "^11.1.2" npmlog "^4.1.2" @@ -875,13 +1035,13 @@ "@lerna/child-process" "3.16.5" semver "^6.2.0" -"@lerna/import@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.18.5.tgz#a9c7d8601870729851293c10abd18b3707f7ba5e" - integrity sha512-PH0WVLEgp+ORyNKbGGwUcrueW89K3Iuk/DDCz8mFyG2IG09l/jOF0vzckEyGyz6PO5CMcz4TI1al/qnp3FrahQ== +"@lerna/import@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" + integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/prompt" "3.18.5" "@lerna/pulse-till-done" "3.13.0" "@lerna/validation-error" "3.13.0" @@ -889,43 +1049,43 @@ fs-extra "^8.1.0" p-map-series "^1.0.0" -"@lerna/info@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/info/-/info-3.20.0.tgz#3a5212f3029f2bc6255f9533bdf4bcb120ef329a" - integrity sha512-Rsz+KQF9mczbGUbPTrtOed1N0C+cA08Qz0eX/oI+NNjvsryZIju/o7uedG4I3P55MBiAioNrJI88fHH3eTgYug== +"@lerna/info@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f" + integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/output" "3.13.0" envinfo "^7.3.1" -"@lerna/init@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/init/-/init-3.18.5.tgz#86dd0b2b3290755a96975069b5cb007f775df9f5" - integrity sha512-oCwipWrha98EcJAHm8AGd2YFFLNI7AW9AWi0/LbClj1+XY9ah+uifXIgYGfTk63LbgophDd8936ZEpHMxBsbAg== +"@lerna/init@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c" + integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" fs-extra "^8.1.0" p-map "^2.1.0" write-json-file "^3.2.0" -"@lerna/link@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/link/-/link-3.18.5.tgz#f24347e4f0b71d54575bd37cfa1794bc8ee91b18" - integrity sha512-xTN3vktJpkT7Nqc3QkZRtHO4bT5NvuLMtKNIBDkks0HpGxC9PRyyqwOoCoh1yOGbrWIuDezhfMg3Qow+6I69IQ== +"@lerna/link@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b" + integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/package-graph" "3.18.5" "@lerna/symlink-dependencies" "3.17.0" p-map "^2.1.0" slash "^2.0.0" -"@lerna/list@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/list/-/list-3.20.0.tgz#7e67cc29c5cf661cfd097e8a7c2d3dcce7a81029" - integrity sha512-fXTicPrfioVnRzknyPawmYIVkzDRBaQqk9spejS1S3O1DOidkihK0xxNkr8HCVC0L22w6f92g83qWDp2BYRUbg== +"@lerna/list@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2" + integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/listable" "3.18.5" "@lerna/output" "3.13.0" @@ -1071,10 +1231,10 @@ npmlog "^4.1.2" upath "^1.2.0" -"@lerna/project@3.18.0": - version "3.18.0" - resolved "https://registry.yarnpkg.com/@lerna/project/-/project-3.18.0.tgz#56feee01daeb42c03cbdf0ed8a2a10cbce32f670" - integrity sha512-+LDwvdAp0BurOAWmeHE3uuticsq9hNxBI0+FMHiIai8jrygpJGahaQrBYWpwbshbQyVLeQgx3+YJdW2TbEdFWA== +"@lerna/project@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" + integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== dependencies: "@lerna/package" "3.16.0" "@lerna/validation-error" "3.13.0" @@ -1097,10 +1257,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.20.2": - version "3.20.2" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.20.2.tgz#a45d29813099b3249657ea913d0dc3f8ebc5cc2e" - integrity sha512-N7Y6PdhJ+tYQPdI1tZum8W25cDlTp4D6brvRacKZusweWexxaopbV8RprBaKexkEX/KIbncuADq7qjDBdQHzaA== +"@lerna/publish@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" + integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -1108,7 +1268,7 @@ "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/describe-ref" "3.16.5" "@lerna/log-packed" "3.16.0" "@lerna/npm-conf" "3.16.0" @@ -1123,7 +1283,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.20.2" + "@lerna/version" "3.22.1" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -1186,12 +1346,12 @@ figgy-pudding "^3.5.1" p-queue "^4.0.0" -"@lerna/run@3.20.0": - version "3.20.0" - resolved "https://registry.yarnpkg.com/@lerna/run/-/run-3.20.0.tgz#a479f7c42bdf9ebabb3a1e5a2bdebb7a8d201151" - integrity sha512-9U3AqeaCeB7KsGS9oyKNp62s9vYoULg/B4cqXTKZkc+OKL6QOEjYHYVSBcMK9lUXrMjCjDIuDSX3PnTCPxQ2Dw== +"@lerna/run@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891" + integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/npm-run-script" "3.16.5" "@lerna/output" "3.13.0" @@ -1236,17 +1396,17 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.20.2": - version "3.20.2" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.20.2.tgz#3709141c0f537741d9bc10cb24f56897bcb30428" - integrity sha512-ckBJMaBWc+xJen0cMyCE7W67QXLLrc0ELvigPIn8p609qkfNM0L0CF803MKxjVOldJAjw84b8ucNWZLvJagP/Q== +"@lerna/version@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" + integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" - "@lerna/conventional-commits" "3.18.5" - "@lerna/github-client" "3.16.5" + "@lerna/command" "3.21.0" + "@lerna/conventional-commits" "3.22.0" + "@lerna/github-client" "3.22.0" "@lerna/gitlab-client" "3.15.0" "@lerna/output" "3.13.0" "@lerna/prerelease-id-from-version" "3.16.0" @@ -1276,6 +1436,21 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@microsoft/tsdoc-config@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz#eb353c93f3b62ab74bdc9ab6f4a82bcf80140f14" + integrity sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA== + dependencies: + "@microsoft/tsdoc" "0.13.2" + ajv "~6.12.6" + jju "~1.4.0" + resolve "~1.19.0" + +"@microsoft/tsdoc@0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz#3b0efb6d3903bd49edb073696f60e90df08efb26" + integrity sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg== + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1284,11 +1459,32 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@octokit/endpoint@^5.1.0": version "5.3.4" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.3.4.tgz#959c61b6db3cb4c77e870d6b8bdc9476b6856085" @@ -1297,10 +1493,10 @@ is-plain-object "^3.0.0" universal-user-agent "^3.0.0" -"@octokit/plugin-enterprise-rest@^3.6.1": - version "3.6.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561" - integrity sha512-3wF5eueS5OHQYuAEudkpN+xVeUsg8vYEMMenEzLphUZ7PRZ8OJtDcsreL3ad9zxXmBbaFWzLmFcdob5CLyZftA== +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2": version "1.0.4" @@ -1394,13 +1590,6 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" - integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg== - dependencies: - any-observable "^0.3.0" - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1413,6 +1602,13 @@ dependencies: type-detect "4.0.8" +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@slack/logger@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-1.0.0.tgz#e529974c7111f8a62d794a79cd807eee1628088b" @@ -1477,10 +1673,26 @@ dependencies: "@types/node" "*" -"@types/babel__core@^7.1.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" - integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== +"@types/append-query@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/append-query/-/append-query-2.0.0.tgz#ed93e5adb9d865a1896ccdff8a2a32f28ef313de" + integrity sha512-0YlmQr+wOjsHrt4cXEWY2Ef8pLPyz4jTQqv/mSiGzdf+q0ZymH7VoI7kvmXV6T31JLvlQwGo2b3B9FgR+qn19g== + +"@types/babel__core@^7.0.0": + version "7.1.9" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" + integrity sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__core@^7.1.7": + version "7.1.8" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.8.tgz#057f725aca3641f49fc11c7a87a9de5ec588a5d7" + integrity sha512-KXBiQG2OXvaPWFPDS1rD8yV9vO0OuWIqAEqLsbfX0oU2REN5KuoMnZ1gClWcBhO5I3n6oTVAmrMufOvRqdmFTQ== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1577,18 +1789,6 @@ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== -"@types/dialogflow@^4.0.4": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/dialogflow/-/dialogflow-4.0.4.tgz#39b784db0d800a5d58ee298aef8b2ac0d4213610" - integrity sha512-W0sSi/Zrn+HJ/Rijhteoh9bcI4dEBx5qAhcfRk614/ovV/E+/WmQTJlKHh/pbHcks17vDxdc0RrJ9IJ7MH6u5w== - dependencies: - dialogflow "*" - -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -1641,6 +1841,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/graceful-fs@^4.1.2": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" + integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ== + dependencies: + "@types/node" "*" + "@types/hapi__joi@*": version "16.0.9" resolved "https://registry.yarnpkg.com/@types/hapi__joi/-/hapi__joi-16.0.9.tgz#0ad11f9de3753748444ac16249a264fc7c798ab4" @@ -1705,23 +1912,28 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@^25.1.3": - version "25.1.3" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.3.tgz#9b0b5addebccfb631175870be8ba62182f1bc35a" - integrity sha512-jqargqzyJWgWAJCXX96LBGR/Ei7wQcZBvRv0PLEu9ZByMfcs23keUJrKv9FMR6YZf9YCbfqDqgmY+JUBsnqhrg== +"@types/jest@^26.0.4": + version "26.0.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.4.tgz#d2e513e85aca16992816f192582b5e67b0b15efb" + integrity sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg== dependencies: - jest-diff "^25.1.0" - pretty-format "^25.1.0" + jest-diff "^25.2.1" + pretty-format "^25.2.1" "@types/jfs@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@types/jfs/-/jfs-0.2.3.tgz#9aa5ccf6eadefdb1e2fb7a55dfc25edf65fdc026" integrity sha512-3nUANvie7NRZyNO2UBczDAfRq7sdjPtSZqSZPlu0Z4AMaHPigAJyGyfMYLElUBFXK999pzes1MhJhgTUJwAUtA== -"@types/json-schema@^7.0.3": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" - integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== +"@types/json-schema@^7.0.7": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/jsonfile@^5.0.0": version "5.0.0" @@ -1759,11 +1971,21 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== +"@types/lodash@^4.14.156": + version "4.14.157" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" + integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== + "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/lru-cache@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" @@ -1792,16 +2014,31 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== -"@types/node@^10.1.0": - version "10.17.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" - integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "16.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" + integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== + +"@types/node@^13.7.0": + version "13.13.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.12.tgz#9c72e865380a7dc99999ea0ef20fc9635b503d20" + integrity sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw== "@types/node@^8.10.50": version "8.10.52" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.52.tgz#ef0ca1809994e20186090408b8cb7f2a6877d5f9" integrity sha512-2RbW7WXeLex6RI+kQSxq6Ym0GiVcODeQ4Km7MnnTX5BHdOGQnqVa+s6AUmAW+OFYAJ8wv9QxvNZXm7/kBdGTVw== +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/object.fromentries@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/object.fromentries/-/object.fromentries-2.0.0.tgz#8349ec6c51011a729da61e0c86ed433609a9f9a6" + integrity sha512-y/4Jx68CzO0Uh+SIbINo1gk+k8H1WVphZTHjMOpLdV3p5NqajFCMNKjg78c7YyCedmeIHyRRFhfcMuVZcWRE6g== + "@types/p-queue@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.2.tgz#16bc5fece69ef85efaf2bce8b13f3ebe39c5a1c8" @@ -1812,10 +2049,15 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/prettier@^1.19.0": - version "1.19.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.0.tgz#a2502fb7ce9b6626fdbfc2e2a496f472de1bdd05" - integrity sha512-gDE8JJEygpay7IjA/u3JiIURvwZW08f0cZSZLAzFoX/ZmeqvS0Sqv+97aKuHpNsalAMMhwPe+iAS6fQbfmbt7A== +"@types/prettier@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.1.tgz#b6e98083f13faa1e5231bfa3bdb1b0feff536b6d" + integrity sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ== + +"@types/prettier@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" + integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== "@types/range-parser@*": version "1.2.3" @@ -1874,6 +2116,11 @@ dependencies: "@types/configstore" "*" +"@types/url-join@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-4.0.0.tgz#72eff71648a429c7d4acf94e03780e06671369bd" + integrity sha512-awrJu8yML4E/xTwr2EMatC+HBnHGoDxc2+ImA9QyeUELI1S7dOCIZcyjki1rkwoA8P2D2NVgLAJLjnclkdLtAw== + "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" @@ -1892,13 +2139,6 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0" integrity sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw== -"@types/yargs@^13.0.0": - version "13.0.2" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.2.tgz#a64674fc0149574ecd90ba746e932b5a5f7b3653" - integrity sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^15.0.0": version "15.0.4" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" @@ -1906,48 +2146,74 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^2.21.0": - version "2.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.21.0.tgz#a34de84a0791cae0357c4dda805c5b4e8203b6c6" - integrity sha512-b5jjjDMxzcjh/Sbjuo7WyhrQmVJg0WipTHQgXh5Xwx10uYm6nPWqN1WGOsaNq4HR3Zh4wUx4IRQdDkCHwyewyw== +"@typescript-eslint/eslint-plugin@^4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.0.tgz#9c3fa6f44bad789a962426ad951b54695bd3af6b" + integrity sha512-iPKZTZNavAlOhfF4gymiSuUkgLne/nh5Oz2/mdiUmuZVD42m9PapnCnzjxuDsnpnbH3wT5s2D8bw6S39TC6GNw== dependencies: - "@typescript-eslint/experimental-utils" "2.21.0" - eslint-utils "^1.4.3" + "@typescript-eslint/experimental-utils" "4.31.0" + "@typescript-eslint/scope-manager" "4.31.0" + debug "^4.3.1" functional-red-black-tree "^1.0.1" - regexpp "^3.0.0" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@2.21.0": - version "2.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.21.0.tgz#71de390a3ec00b280b69138d80733406e6e86bfa" - integrity sha512-olKw9JP/XUkav4lq0I7S1mhGgONJF9rHNhKFn9wJlpfRVjNo3PPjSvybxEldvCXnvD+WAshSzqH5cEjPp9CsBA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.21.0" - eslint-scope "^5.0.0" - -"@typescript-eslint/parser@^2.21.0": - version "2.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.21.0.tgz#4f200995517c3d5fc5ef51b17527bc948992e438" - integrity sha512-VrmbdrrrvvI6cPPOG7uOgGUFXNYTiSbnRq8ZMyuGa4+qmXJXVLEEz78hKuqupvkpwJQNk1Ucz1TenrRP90gmBg== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.21.0" - "@typescript-eslint/typescript-estree" "2.21.0" - eslint-visitor-keys "^1.1.0" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.0.tgz#0ef1d5d86c334f983a00f310e43c1ce4c14e054d" + integrity sha512-Hld+EQiKLMppgKKkdUsLeVIeEOrwKc2G983NmznY/r5/ZtZCDvIOXnXtwqJIgYz/ymsy7n7RGvMyrzf1WaSQrw== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.31.0" + "@typescript-eslint/types" "4.31.0" + "@typescript-eslint/typescript-estree" "4.31.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.31.0.tgz#87b7cd16b24b9170c77595d8b1363f8047121e05" + integrity sha512-oWbzvPh5amMuTmKaf1wp0ySxPt2ZXHnFQBN2Szu1O//7LmOvgaKTCIDNLK2NvzpmVd5A2M/1j/rujBqO37hj3w== + dependencies: + "@typescript-eslint/scope-manager" "4.31.0" + "@typescript-eslint/types" "4.31.0" + "@typescript-eslint/typescript-estree" "4.31.0" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.31.0.tgz#9be33aed4e9901db753803ba233b70d79a87fc3e" + integrity sha512-LJ+xtl34W76JMRLjbaQorhR0hfRAlp3Lscdiz9NeI/8i+q0hdBZ7BsiYieLoYWqy+AnRigaD3hUwPFugSzdocg== + dependencies: + "@typescript-eslint/types" "4.31.0" + "@typescript-eslint/visitor-keys" "4.31.0" + +"@typescript-eslint/types@4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.0.tgz#9a7c86fcc1620189567dc4e46cad7efa07ee8dce" + integrity sha512-9XR5q9mk7DCXgXLS7REIVs+BaAswfdHhx91XqlJklmqWpTALGjygWVIb/UnLh4NWhfwhR5wNe1yTyCInxVhLqQ== + +"@typescript-eslint/typescript-estree@4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.0.tgz#4da4cb6274a7ef3b21d53f9e7147cc76f278a078" + integrity sha512-QHl2014t3ptg+xpmOSSPn5hm4mY8D4s97ftzyk9BZ8RxYQ3j73XcwuijnJ9cMa6DO4aLXeo8XS3z1omT9LA/Eg== + dependencies: + "@typescript-eslint/types" "4.31.0" + "@typescript-eslint/visitor-keys" "4.31.0" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@2.21.0": - version "2.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.21.0.tgz#7e4be29f2e338195a2e8c818949ed0ff727cc943" - integrity sha512-NC/nogZNb9IK2MEFQqyDBAciOT8Lp8O3KgAfvHx2Skx6WBo+KmDqlU3R9KxHONaijfTIKtojRe3SZQyMjr3wBw== +"@typescript-eslint/visitor-keys@4.31.0": + version "4.31.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.0.tgz#4e87b7761cb4e0e627dc2047021aa693fc76ea2b" + integrity sha512-HUcRp2a9I+P21+O21yu3ezv3GEPGjyGiXoEUQwZXjR8UxRApGeLyWH4ZIIUSalE28aG4YsV6GjtaAVB3QKOu0w== dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^6.3.0" - tsutils "^3.17.1" + "@typescript-eslint/types" "4.31.0" + eslint-visitor-keys "^2.0.0" "@zkochan/cmd-shim@^3.1.0": version "3.1.0" @@ -1966,10 +2232,10 @@ JSONStream@^1.0.4, JSONStream@^1.3.4: jsonparse "^1.2.0" through ">=2.2.7 <3" -abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== +abab@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== abbrev@1: version "1.1.1" @@ -1991,38 +2257,33 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-globals@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - -acorn-jsx@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" - integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== + acorn "^7.1.1" + acorn-walk "^7.1.1" -acorn-jsx@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" - integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^6.0.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" - integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== +acorn-walk@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" + integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== -acorn@^6.0.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== +acorn@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" + integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== -acorn@^7.0.0, acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== +acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== agent-base@4, agent-base@^4.3.0: version "4.3.0" @@ -2031,6 +2292,13 @@ agent-base@4, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agent-base@6: + version "6.0.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" + integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== + dependencies: + debug "4" + agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -2053,7 +2321,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^3.2.0" -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== @@ -2063,6 +2331,26 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.4, ajv@~6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.6.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" + integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -2187,6 +2475,16 @@ ansi-colors@^0.2.0: ansi-yellow "^0.1.1" lazy-cache "^2.0.1" +ansi-colors@^3.2.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-cyan@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" @@ -2201,7 +2499,7 @@ ansi-dim@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -2213,6 +2511,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.5.2" +ansi-escapes@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + ansi-gray@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" @@ -2279,7 +2584,7 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0, ansi-regex@^4.1.0: +ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== @@ -2303,11 +2608,6 @@ ansi-strikethrough@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2356,11 +2656,6 @@ ansi-yellow@^0.1.1: dependencies: ansi-wrap "0.1.0" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -2419,14 +2714,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" - integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= - dependencies: - ast-types-flow "0.0.7" - commander "^2.11.0" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -2454,11 +2741,6 @@ array-differ@^2.0.3: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -2474,21 +2756,15 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" - integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - -array-includes@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" - integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== +array-includes@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" + integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.1.1" is-string "^1.0.5" array-union@^1.0.2: @@ -2498,6 +2774,11 @@ array-union@^1.0.2: dependencies: array-uniq "^1.0.1" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -2508,13 +2789,14 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== +array.prototype.flat@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" + integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + es-abstract "^1.18.0-next.1" arrify@^1.0.1: version "1.0.1" @@ -2548,15 +2830,10 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types-flow@0.0.7, ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async-limiter@~1.0.0: version "1.0.1" @@ -2600,12 +2877,30 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -axios-error@^1.0.0-beta.16: - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/axios-error/-/axios-error-1.0.0-beta.16.tgz#013d58d2590ea8d7461727cff1d344daa6fd5344" - integrity sha512-Ntn6ryoGA/XNR/o4EXa87Ek+xP5yEzCvLPjH+fwFogLcHF8el0zP8K7Up0m6Nf2kGk4xo1SrQ/Ss1SUNCcisAA== +axios-error@^1.0.0-beta.27: + version "1.0.0-beta.27" + resolved "https://registry.yarnpkg.com/axios-error/-/axios-error-1.0.0-beta.27.tgz#b851caf7bebdc22af157ff19dacfa24c1ce9c307" + integrity sha512-8an7/VJ3tFOSTuIrMSRBOcbuYVqHzURQJ+jIxGEVOR+iW4eHpkMOvw/eUM2G9t9rXs/xFKDdbqn8a/OgfUAS9w== dependencies: axios "^0.19.2" + type-fest "^0.15.1" + +axios-error@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/axios-error/-/axios-error-1.0.4.tgz#020b3708c5319c0b7f03fd136efa27bd3925e26b" + integrity sha512-ay1l7dXNW288c39KcNAijpE3w8X3tP4V3Hf5yttKp4Kacrq5mnL7s0Z9GXYHRProTvRXytvs4SOriBlsGcVlfA== + dependencies: + axios "^0.21.1" + type-fest "^0.15.1" + +axios-mock-adapter@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz#21f5b4b625306f43e8c05673616719da86e20dcb" + integrity sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w== + dependencies: + fast-deep-equal "^3.1.3" + is-blob "^2.1.0" + is-buffer "^2.0.5" axios@^0.18.0: version "0.18.1" @@ -2622,24 +2917,25 @@ axios@^0.19.2: dependencies: follow-redirects "1.5.10" -axobject-query@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" - integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== +axios@^0.21.1, axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - ast-types-flow "0.0.7" + follow-redirects "^1.14.0" -babel-jest@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.1.0.tgz#206093ac380a4b78c4404a05b3277391278f80fb" - integrity sha512-tz0VxUhhOE2y+g8R2oFrO/2VtVjA1lkJeavlhExuRBg3LdNJY9gwQ+Vcvqt9+cqy71MCTJhewvTB7Qtnnr9SWg== +babel-jest@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.1.0.tgz#b20751185fc7569a0f135730584044d1cb934328" + integrity sha512-Nkqgtfe7j6PxLO6TnCQQlkMm8wdTdnIF8xrdpooHCuD5hXRzVEPbPneTJKknH5Dsv3L8ip9unHDAp48YQ54Dkg== dependencies: - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - "@types/babel__core" "^7.1.0" + "@jest/transform" "^26.1.0" + "@jest/types" "^26.1.0" + "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^25.1.0" - chalk "^3.0.0" + babel-preset-jest "^26.1.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" slash "^3.0.0" babel-plugin-istanbul@^6.0.0: @@ -2653,21 +2949,39 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz#fb62d7b3b53eb36c97d1bc7fec2072f9bd115981" - integrity sha512-oIsopO41vW4YFZ9yNYoLQATnnN46lp+MZ6H4VvPKFkcc2/fkl3CfE/NZZSmnEIEsJRmJAgkVEK0R7Zbl50CpTw== +babel-plugin-jest-hoist@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.1.0.tgz#c6a774da08247a28285620a64dfadbd05dd5233a" + integrity sha512-qhqLVkkSlqmC83bdMhM8WW4Z9tB+JkjqAqlbbohS9sJLT5Ha2vfzuKqg5yenXrAjOPG2YC0WiXdH3a9PvB+YYw== dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-preset-jest@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz#d0aebfebb2177a21cde710996fce8486d34f1d33" - integrity sha512-eCGn64olaqwUMaugXsTtGAM2I0QTahjEtnRu0ql8Ie+gDWAc1N6wqN0k2NilnyTunM69Pad7gJY7LOtwLimoFQ== - dependencies: - "@babel/plugin-syntax-bigint" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^25.1.0" +babel-preset-current-node-syntax@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6" + integrity sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +babel-preset-jest@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.1.0.tgz#612f714e5b457394acfd863793c564cbcdb7d1c1" + integrity sha512-na9qCqFksknlEj5iSdw1ehMVR06LCCTkZLGKeEtxDDdhg8xpUF09m29Kvh1pRbZ07h7AQ5ttLYUwpXL4tO6w7w== + dependencies: + babel-plugin-jest-hoist "^26.1.0" + babel-preset-current-node-syntax "^0.1.2" balanced-match@^1.0.0: version "1.0.0" @@ -2704,10 +3018,10 @@ before-after-hook@^2.0.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== -bignumber.js@^7.0.0: - version "7.2.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" - integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== +bignumber.js@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== binary-extensions@^2.0.0: version "2.0.0" @@ -2809,17 +3123,10 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-process-hrtime@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" - integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== - -browser-resolve@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== bs-logger@0.x: version "0.2.6" @@ -2929,6 +3236,14 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -2998,6 +3313,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -3022,18 +3342,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@^1.0.0, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.1, chalk@^2.4.2, chalk@~2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3050,6 +3359,27 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -3119,7 +3449,7 @@ cli-boxes@^2.2.0: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== -cli-cursor@^2.0.0, cli-cursor@^2.1.0: +cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= @@ -3143,13 +3473,13 @@ cli-table3@^0.5.1: optionalDependencies: colors "^1.1.2" -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" + slice-ansi "^3.0.0" + string-width "^4.2.0" cli-width@^2.0.0: version "2.2.0" @@ -3174,6 +3504,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-deep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-1.0.0.tgz#b2f354444b5d4a0ce58faca337ef34da2b14a6c7" @@ -3262,6 +3601,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + colors@^1.1.2: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" @@ -3282,11 +3626,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== - commander@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" @@ -3297,6 +3636,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + compare-func@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" @@ -3305,12 +3649,7 @@ compare-func@^1.3.1: array-ify "^1.0.0" dot-prop "^3.0.0" -compare-versions@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - -component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -3372,26 +3711,16 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" -confusing-browser-globals@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.8.tgz#93ffec1f82a6e2bf2bc36769cc3a92fa20e502f3" - integrity sha512-lI7asCibVJ6Qd3FGU7mu4sfG4try4LX3+GVS+Gv8UlrEf2AeW57piecapnog2UHZSbcX/P/1UDWVaTsblowlZg== - -confusing-browser-globals@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" - integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== +confusing-browser-globals@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" + integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -3511,7 +3840,7 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookiejar@^2.1.0: +cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== @@ -3548,16 +3877,16 @@ cosmiconfig@^5.1.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cosmiconfig@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" + yaml "^1.10.0" create-error-class@^3.0.0: version "3.0.2" @@ -3566,10 +3895,10 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -cross-env@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.0.tgz#5a3b2ddce51ec713ea58f2fb79ce22e65b4f5479" - integrity sha512-rV6M9ldNgmwP7bx5u6rZsTbYidzwvrwIYZnT08hSGLcQCcggofgFW+sNe7IhA1SRauPS0QuLbbX+wdNtpqE5CQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: cross-spawn "^7.0.1" @@ -3582,7 +3911,7 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -3611,6 +3940,15 @@ cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -3621,7 +3959,7 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -cssom@^0.4.1: +cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== @@ -3631,10 +3969,10 @@ cssom@~0.3.6: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssstyle@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992" - integrity sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA== +cssstyle@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" @@ -3650,11 +3988,6 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -damerau-levenshtein@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz#780cf7144eb2e8dbd1c3bb83ae31100ccc31a414" - integrity sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA== - dargs@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" @@ -3669,19 +4002,14 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-urls@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" date-fns@^2.10.0: version "2.10.0" @@ -3707,19 +4035,26 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0, debug@^3.2.6: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== +debug@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: - ms "^2.1.1" + ms "2.1.2" debuglog@^1.0.1: version "1.0.1" @@ -3739,6 +4074,11 @@ decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decimal.js@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" + integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -3774,7 +4114,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -3784,6 +4124,11 @@ deep-object-diff@^1.1.0: resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" @@ -3878,26 +4223,15 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" -dialogflow@*: - version "1.1.0" - resolved "https://registry.yarnpkg.com/dialogflow/-/dialogflow-1.1.0.tgz#2032ad7a7818b9cb729e3faf6b638b18623845ca" - integrity sha512-+jRwsLpPOA2XYnVRXikT8pt+mhxiKi97k0RwO27D2KyqvRWko62GCP68r+qCFgN58fc+qvh9bwCX4OLfUBkUdA== - dependencies: - google-gax "^1.7.5" - protobufjs "^6.8.0" - -dialogflow@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/dialogflow/-/dialogflow-1.2.0.tgz#3485c351f95a76a1627515a07b2ed98f2760bfa7" - integrity sha512-pBrPKNRP3WhSUNAeOKtN0GhVBj+CAcl7tqfBAhrhE8da0IrnAi3F1dk2xLFnH7Wd4/vEhcqq1mekYuyfC15FOw== - dependencies: - google-gax "^1.7.5" - protobufjs "^6.8.0" +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== -diff-sequences@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" - integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== +diff-sequences@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6" + integrity sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg== dir-glob@^2.2.2: version "2.2.2" @@ -3906,13 +4240,12 @@ dir-glob@^2.2.2: dependencies: path-type "^3.0.0" -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: - esutils "^2.0.2" - isarray "^1.0.0" + path-type "^4.0.0" doctrine@^2.1.0: version "2.1.0" @@ -3928,12 +4261,12 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== dependencies: - webidl-conversions "^4.0.2" + webidl-conversions "^5.0.0" dot-case@^3.0.3: version "3.0.3" @@ -3989,6 +4322,16 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +duplexify@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3997,7 +4340,7 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -4009,12 +4352,7 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= - -emoji-regex@^7.0.1, emoji-regex@^7.0.2: +emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== @@ -4043,6 +4381,27 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + dependencies: + ansi-colors "^3.2.1" + +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + env-paths@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" @@ -4075,22 +4434,27 @@ error-symbol@^0.1.0: resolved "https://registry.yarnpkg.com/error-symbol/-/error-symbol-0.1.0.tgz#0a4dae37d600d15a29ba453d8ef920f1844333f6" integrity sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y= -es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== +es-abstract@^1.17.0-next.1: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== dependencies: - es-to-primitive "^1.2.0" + es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: - version "1.17.4" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" - integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== +es-abstract@^1.17.5: + version "1.17.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" + integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" @@ -4104,6 +4468,42 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" +es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: + version "1.18.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456" + integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-string "^1.0.7" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" @@ -4134,6 +4534,11 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -4144,15 +4549,25 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@^1.11.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" - integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.14.1: + version "1.14.2" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84" + integrity sha512-InuOIiKk8wwuOFg6x9BQXbzjrQhtyXh46K9bqVTPzSo2FnyMBaYGBMC6PhQy7yxxil9vIedFBweQBMK74/7o8A== dependencies: esprima "^4.0.1" estraverse "^4.2.0" @@ -4161,294 +4576,222 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz#8a7bcb9643d13c55df4dd7444f138bf4efa61e17" - integrity sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA== +eslint-config-airbnb-base@^14.2.1: + version "14.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" + integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== dependencies: - confusing-browser-globals "^1.0.7" - object.assign "^4.1.0" - object.entries "^1.1.0" + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.2" -eslint-config-airbnb@^18.0.1: - version "18.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz#a3a74cc29b46413b6096965025381df8fb908559" - integrity sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ== - dependencies: - eslint-config-airbnb-base "^14.0.0" - object.assign "^4.1.0" - object.entries "^1.1.0" - -eslint-config-prettier@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f" - integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg== - dependencies: - get-stdin "^6.0.0" - -eslint-config-react-app@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.2.0.tgz#135110ba56a9e378f7acfe5f36e2ae76a2317899" - integrity sha512-WrHjoGpKr1kLLiWDD81tme9jMM0hk5cMxasLSdyno6DdPt+IfLOrDJBVo6jN7tn4y1nzhs43TmUaZWO6Sf0blw== - dependencies: - confusing-browser-globals "^1.0.9" +eslint-config-prettier@^8.1.0, eslint-config-prettier@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== -eslint-config-yoctol-base@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/eslint-config-yoctol-base/-/eslint-config-yoctol-base-0.22.0.tgz#a18bbdb69d4dea2ea7b40114d0614accff6483aa" - integrity sha512-ZbYW+sfR2UIQl5amdxKH+ubxOYB3zEltAzuMyGcnpQPDz2qDrJyYCX045QkAnF55+JUWAp29mJaxp2RdwYA+Hw== +eslint-config-yoctol-base@^0.24.1: + version "0.24.1" + resolved "https://registry.yarnpkg.com/eslint-config-yoctol-base/-/eslint-config-yoctol-base-0.24.1.tgz#ba2258aeda7e5c2954d82cb4d40df18e3087f6a7" + integrity sha512-IJYF/7A1KqW5c+Len80QFrJls65bXNB860Cc7A1IYkNwYKDw1mrZ4nG+lBRdFaGbe2Z0xIhIBMwgbfsfnZeNWA== dependencies: - eslint-config-airbnb-base "^14.0.0" - eslint-config-prettier "^6.10.0" - prettier "^1.19.1" + eslint-config-airbnb-base "^14.2.1" + eslint-config-prettier "^8.1.0" -eslint-config-yoctol@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/eslint-config-yoctol/-/eslint-config-yoctol-0.24.0.tgz#0f2bd30ef473e592802050b4bbe8855f0f10fa77" - integrity sha512-TXQO1g/s/WGhn8ZPrpu8rkw9jInkcMTg9aPbsDYWZfrVSc25udNj7US8tyOAzfot+G79iJq43qdK3HSCdIGdIg== +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== dependencies: - eslint-config-airbnb "^18.0.1" - eslint-config-yoctol-base "^0.22.0" + debug "^3.2.7" + resolve "^1.20.0" -eslint-import-resolver-node@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" - integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== +eslint-import-resolver-typescript@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a" + integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ== dependencies: - debug "^2.6.9" - resolve "^1.5.0" + debug "^4.3.1" + glob "^7.1.7" + is-glob "^4.0.1" + resolve "^1.20.0" + tsconfig-paths "^3.9.0" -eslint-module-utils@^2.4.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz#7878f7504824e1b857dd2505b59a8e5eda26a708" - integrity sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q== +eslint-module-utils@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534" + integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q== dependencies: - debug "^2.6.9" + debug "^3.2.7" pkg-dir "^2.0.0" -eslint-plugin-import@2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz#d749a7263fb6c29980def8e960d380a6aa6aecaa" - integrity sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ== +eslint-plugin-import@2.24.2: + version "2.24.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da" + integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q== dependencies: - array-includes "^3.0.3" - array.prototype.flat "^1.2.1" - contains-path "^0.1.0" + array-includes "^3.1.3" + array.prototype.flat "^1.2.4" debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.1" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.6.2" + find-up "^2.0.0" has "^1.0.3" + is-core-module "^2.6.0" minimatch "^3.0.4" - object.values "^1.1.0" - read-pkg-up "^2.0.0" - resolve "^1.12.0" - -eslint-plugin-jsx-a11y@^6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" - integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg== - dependencies: - "@babel/runtime" "^7.4.5" - aria-query "^3.0.0" - array-includes "^3.0.3" - ast-types-flow "^0.0.7" - axobject-query "^2.0.2" - damerau-levenshtein "^1.0.4" - emoji-regex "^7.0.2" - has "^1.0.3" - jsx-ast-utils "^2.2.1" + object.values "^1.1.4" + pkg-up "^2.0.0" + read-pkg-up "^3.0.0" + resolve "^1.20.0" + tsconfig-paths "^3.11.0" -eslint-plugin-prettier@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba" - integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA== +eslint-plugin-prettier@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" + integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.0.tgz#c50ab7ca5945ce6d1cf8248d9e185c80b54171b6" - integrity sha512-bzvdX47Jx847bgAYf0FPX3u1oxU+mKU8tqrpj4UX9A96SbAmj/HVEefEy6rJUog5u8QIlOPTKZcBpGn5kkKfAQ== +eslint-plugin-sort-imports-es6-autofix@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-sort-imports-es6-autofix/-/eslint-plugin-sort-imports-es6-autofix-0.6.0.tgz#b8cd8639d7a54cefce6b17898b102fd5ec31e52b" + integrity sha512-2NVaBGF9NN+727Fyq+jJYihdIeegjXeUUrZED9Q8FVB8MsV3YQEyXG96GVnXqWt0pmn7xfCZOZf3uKnIhBrfeQ== -eslint-plugin-react@^7.18.3: - version "7.18.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz#8be671b7f6be095098e79d27ac32f9580f599bc8" - integrity sha512-Bt56LNHAQCoou88s8ViKRjMB2+36XRejCQ1VoLj716KI1MoE99HpTVvIThJ0rvFmG4E4Gsq+UgToEjn+j044Bg== - dependencies: - array-includes "^3.1.1" - doctrine "^2.1.0" - has "^1.0.3" - jsx-ast-utils "^2.2.3" - object.entries "^1.1.1" - object.fromentries "^2.0.2" - object.values "^1.1.1" - prop-types "^15.7.2" - resolve "^1.14.2" - string.prototype.matchall "^4.0.2" - -eslint-plugin-sort-imports-es6-autofix@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-sort-imports-es6-autofix/-/eslint-plugin-sort-imports-es6-autofix-0.5.0.tgz#dabae09a457eac6e95c52d8edd7855f576d014b6" - integrity sha512-KEX2Uz6bAs67jDYiH/OT1xz1E7AzIJJOIRg1F7OnFAfUVlpws3ldSZj5oZySRHfoVkWqDX9GGExYxckdLrWhwg== +eslint-plugin-tsdoc@^0.2.14: + version "0.2.14" + resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.14.tgz#e32e7c1df8af7b3009c252590bec07a1030afbf2" + integrity sha512-fJ3fnZRsdIoBZgzkQjv8vAj6NeeOoFkTfgosj6mKsFjX70QV256sA/wq+y/R2+OL4L8E79VVaVWrPeZnKNe8Ng== dependencies: - eslint "^6.2.2" + "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc-config" "0.15.2" -eslint-scope@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" - integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" - integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: - eslint-visitor-keys "^1.0.0" + eslint-visitor-keys "^1.1.0" -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== dependencies: - eslint-visitor-keys "^1.1.0" + eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: +eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@^6.2.2: - version "6.3.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a" - integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.2" - eslint-visitor-keys "^1.1.0" - espree "^6.1.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.4.1" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" +eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint@^6.8.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.32.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: - "@babel/code-frame" "^7.0.0" + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" + glob-parent "^5.1.2" + globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" + levn "^0.4.1" + lodash.merge "^4.6.2" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.3" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" - integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== dependencies: - acorn "^7.0.0" - acorn-jsx "^5.0.2" - eslint-visitor-keys "^1.1.0" - -espree@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" - integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== - dependencies: - acorn "^7.1.0" - acorn-jsx "^5.1.0" - eslint-visitor-keys "^1.1.0" + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" - integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: - estraverse "^4.0.0" + estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -4500,10 +4843,10 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^3.2.0, execa@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" - integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g== +execa@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" @@ -4512,10 +4855,24 @@ execa@^3.2.0, execa@^3.4.0: merge-stream "^2.0.0" npm-run-path "^4.0.0" onetime "^5.1.0" - p-finally "^2.0.0" signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4534,17 +4891,17 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-25.1.0.tgz#7e8d7b06a53f7d66ec927278db3304254ee683ee" - integrity sha512-wqHzuoapQkhc3OKPlrpetsfueuEiMf3iWh0R8+duCu9PIjXoP7HgD5aeypwTnXUAjC8aMsiVDaWwlbJ1RlQ38g== +expect@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.1.0.tgz#8c62e31d0f8d5a8ebb186ee81473d15dd2fbf7c8" + integrity sha512-QbH4LZXDsno9AACrN9eM0zfnby9G+OsdNgZUohjg/P0mLy1O+/bzTAJGT6VSIjVCe8yKM6SzEl/ckEOFBT7Vnw== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^26.1.0" ansi-styles "^4.0.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" + jest-get-type "^26.0.0" + jest-matcher-utils "^26.1.0" + jest-message-util "^26.1.0" + jest-regex-util "^26.0.0" express@^4.17.1: version "4.17.1" @@ -4602,7 +4959,7 @@ extend@^2.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-2.0.2.tgz#1b74985400171b85554894459c978de6ef453ab7" integrity sha512-AgFD4VU+lVLP6vjnlNfF7OeInLTyeyckCNPEsuxz1vi786UuK/nk6ynPuhn/h+Ju9++TQyr5EpLRI14fc1QtTQ== -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -4640,11 +4997,24 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +facebook-batch@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/facebook-batch/-/facebook-batch-1.0.6.tgz#38ad1a45a941787a8e73e2ebd78e625eab991ef6" + integrity sha512-s9X3JZpbcqlXGtPMl6pRAdZxxfGWzH4q+aU8arHF+gUbK7iThDQPhNn8SHnDIGu9oEs/BT3iCkWQf9+YRhfXkg== + dependencies: + messaging-api-messenger "^1.0.6" + type-fest "^0.15.1" + fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -4662,21 +5032,49 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" +fast-glob@^3.1.1: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.7: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-text-encoding@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ== +fast-text-encoding@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -4696,14 +5094,6 @@ figures@*: dependencies: escape-string-regexp "^1.0.5" -figures@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -4725,12 +5115,12 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" file-type@^10.10.0: version "10.11.0" @@ -4807,31 +5197,23 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== - dependencies: - semver-regex "^2.0.0" - finity@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11" integrity sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA== -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + flatted "^3.1.0" + rimraf "^3.0.2" -flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +flatted@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" + integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== flush-write-stream@^1.0.0: version "1.1.1" @@ -4848,6 +5230,11 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -4870,15 +5257,6 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@^2.3.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37" @@ -4906,10 +5284,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" - integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== forwarded@~0.1.2: version "0.1.2" @@ -4936,10 +5314,14 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" -fromentries@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" - integrity sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ== +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" fs-extra@^8.1.0: version "8.1.0" @@ -5001,24 +5383,24 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaxios@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.2.0.tgz#7c51653c224415ac9218416cb94ff48610b816bc" - integrity sha512-54Y7s3yvtEO9CZ0yBVQHI5fzS7TzkjlnuLdDEkeyL1SNYMv877VofvA56E/C3dvj3rS7GFiyMWl833Qrr+nrkg== +gaxios@^4.0.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.1.tgz#d45fd94a63ec0fc657d40343c31cab5e24c75f3b" + integrity sha512-9qXV7yrMCGzTrphl9/YGMVH41oSg0rhn1j3wJWed4Oqk45/hXDD2wBT5J1NjQcqTCcv4g3nFnyQ7reSRHNgBgw== dependencies: abort-controller "^3.0.0" extend "^3.0.2" - https-proxy-agent "^3.0.0" + https-proxy-agent "^5.0.0" is-stream "^2.0.0" - node-fetch "^2.3.0" + node-fetch "^2.6.1" -gcp-metadata@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.3.0.tgz#df061f0c0da3fb274b20f4af0d955030bbc21eac" - integrity sha512-uO3P/aByOQmoDu5bOYBODHmD1oDCZw7/R8SYY0MdmMQSZVEmeTSxmiM1vwde+YHYSpkaQnAAMAIZuOqLvgfp/Q== +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== dependencies: - gaxios "^2.1.0" - json-bigint "^0.3.0" + gaxios "^4.0.0" + json-bigint "^1.0.0" genfun@^5.0.0: version "5.0.0" @@ -5030,11 +5412,20 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" @@ -5061,11 +5452,6 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -5085,6 +5471,19 @@ get-stream@^5.0.0, get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -5161,6 +5560,13 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob-parent@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" @@ -5197,6 +5603,18 @@ glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -5211,17 +5629,29 @@ global-dirs@^2.0.1: dependencies: ini "^1.3.5" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" - integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== +globals@^13.6.0, globals@^13.9.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" + integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== dependencies: - type-fest "^0.8.1" + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" globby@^9.2.0: version "9.2.0" @@ -5237,47 +5667,46 @@ globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" -google-auth-library@^5.0.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-5.7.0.tgz#2d8273793a9177f9f37f3920861f4256419c77ae" - integrity sha512-uclMldsQNf64Qr67O8TINdnqbU/Ixv81WryX+sF9g7uP0igJ98aCR/uU399u1ABLa53LNsyji+bo+bP8/iL9dA== +google-auth-library@^7.6.1: + version "7.9.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.9.1.tgz#b90a3a0fa67d6ba78c43ffdeeb0a66fcebe6fb91" + integrity sha512-cWGykH2WBR+UuYPGRnGVZ6Cjq2ftQiEIFjQWNIRIauZH7hUWoYTr/lkKUqLTYt5dex77nlWWVQ8aPV80mhfp5w== dependencies: arrify "^2.0.0" base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" fast-text-encoding "^1.0.0" - gaxios "^2.1.0" - gcp-metadata "^3.2.0" - gtoken "^4.1.0" - jws "^3.1.5" - lru-cache "^5.0.0" - -google-gax@^1.7.5: - version "1.13.0" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-1.13.0.tgz#82ed44155b296b0069178320c31c75d1bd8a6cd3" - integrity sha512-MSDPDz+eK8X6XHkb2C1DGPRX50RZBLeSnXChV5P6ojLkc1zSfII8OWGdxREBw8/izvRJLQ5XnuSk1ylLB1BKfQ== - dependencies: - "@grpc/grpc-js" "^0.6.12" - "@grpc/proto-loader" "^0.5.1" - "@types/fs-extra" "^8.0.1" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^2.24.1: + version "2.25.2" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-2.25.2.tgz#706e807d274de7733c2e66dc5b1d5e51646e8be6" + integrity sha512-2BLLndZgaFRlaqbtCy1HkikKQIng8h81hJ3C7HPrTaVF1g+r/2CZ/9tuFOwHtmVCDJ+WqGuhshM87vV/xMvKhA== + dependencies: + "@grpc/grpc-js" "~1.3.0" + "@grpc/proto-loader" "^0.6.1" "@types/long" "^4.0.0" abort-controller "^3.0.0" - duplexify "^3.6.0" - google-auth-library "^5.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.6.1" is-stream-ended "^0.1.4" - lodash.at "^4.6.0" - lodash.has "^4.5.2" - node-fetch "^2.6.0" - protobufjs "^6.8.8" + node-fetch "^2.6.1" + object-hash "^2.1.1" + proto3-json-serializer "^0.1.1" + protobufjs "6.11.2" retry-request "^4.0.0" - semver "^7.0.0" - walkdir "^0.4.0" -google-p12-pem@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.3.tgz#14ecd78a94bd03bf86d74d9d0724787e85c7731f" - integrity sha512-Tq2kBCANxYYPxaBpTgCpRfdoPs9+/lNzc/Iaee4kuMVW5ascD+HwhpBsTLwH85C9Ev4qfB8KKHmpPQYyD2vg2w== +google-p12-pem@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.2.tgz#c3d61c2da8e10843ff830fdb0d2059046238c1d4" + integrity sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A== dependencies: - node-forge "^0.9.0" + node-forge "^0.10.0" got@^6.7.1: version "6.7.1" @@ -5318,25 +5747,24 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== -graceful-fs@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -gtoken@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-4.1.3.tgz#efa9e42f59d02731f15de466b09331d7afc393cf" - integrity sha512-ofW+FiXjswyKdkjMcDbe6E4K7cDDdE82dGDhZIc++kUECqaE7MSErf6arJPAjcnYn1qxE1/Ti06qQuqgVusovQ== +gtoken@^5.0.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.1.tgz#c1c2598a826f2b5df7c6bb53d7be6cf6d50c3c78" + integrity sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ== dependencies: - gaxios "^2.1.0" - google-p12-pem "^2.0.0" - jws "^3.1.5" - mime "^2.2.0" + gaxios "^4.0.0" + google-p12-pem "^3.0.3" + jws "^4.0.0" handlebars@^4.1.2: version "4.7.7" @@ -5355,7 +5783,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.0, har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -5363,12 +5791,10 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== has-flag@^3.0.0: version "3.0.0" @@ -5390,6 +5816,18 @@ has-symbols@^1.0.1: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -5451,12 +5889,12 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== dependencies: - whatwg-encoding "^1.0.1" + whatwg-encoding "^1.0.5" html-escaper@^2.0.0: version "2.0.0" @@ -5520,19 +5958,24 @@ https-proxy-agent@^2.2.1: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" - integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: - agent-base "^4.3.0" - debug "^3.1.0" + agent-base "6" + debug "4" human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -5540,21 +5983,10 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e" - integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ== - dependencies: - chalk "^3.0.0" - ci-info "^2.0.0" - compare-versions "^3.5.1" - cosmiconfig "^6.0.0" - find-versions "^3.2.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" +husky@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.2.tgz#21900da0f30199acca43a46c043c4ad84ae88dff" + integrity sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg== iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" @@ -5590,6 +6022,11 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + image-type@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/image-type/-/image-type-4.1.0.tgz#72a88d64ff5021371ed67b9a466442100be57cd1" @@ -5613,7 +6050,7 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -5706,7 +6143,7 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" -inquirer@^6.2.0, inquirer@^6.4.1: +inquirer@^6.2.0: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== @@ -5763,14 +6200,14 @@ inquirer@^7.0.5: strip-ansi "^6.0.0" through "^2.3.6" -internal-slot@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" - integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== dependencies: - es-abstract "^1.17.0-next.1" + get-intrinsic "^1.1.0" has "^1.0.3" - side-channel "^1.0.2" + side-channel "^1.0.4" invariant@^2.2.4: version "2.2.4" @@ -5828,6 +6265,13 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -5835,6 +6279,19 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-blob@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-blob/-/is-blob-2.1.0.tgz#e36cd82c90653f1e1b930f11baf9c64216a05385" + integrity sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw== + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5845,6 +6302,11 @@ is-buffer@^2.0.2: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" @@ -5855,6 +6317,11 @@ is-callable@^1.1.5: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== +is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" @@ -5869,6 +6336,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19" + integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -5987,6 +6461,11 @@ is-installed-globally@^0.3.1: global-dirs "^2.0.1" is-path-inside "^3.0.1" +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -5997,6 +6476,13 @@ is-npm@^4.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -6024,13 +6510,6 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" - is-path-inside@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" @@ -6062,6 +6541,11 @@ is-plain-object@^3.0.0: dependencies: isobject "^4.0.0" +is-potential-custom-element-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" + integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= + is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -6086,6 +6570,14 @@ is-regex@^1.0.5: dependencies: has "^1.0.3" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -6123,6 +6615,13 @@ is-string@^1.0.5: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + is-symbol@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" @@ -6130,6 +6629,13 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" +is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-text-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-2.0.0.tgz#b2484e2b720a633feb2e85b67dc193ff72c75636" @@ -6142,6 +6648,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -6167,7 +6678,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -6217,6 +6728,16 @@ istanbul-lib-instrument@^4.0.0: istanbul-lib-coverage "^3.0.0" semver "^6.3.0" +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -6235,394 +6756,398 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70" - integrity sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A== +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.1.0.tgz#73dae9a7d9949fdfa5c278438ce8f2ff3ec78131" - integrity sha512-bdL1aHjIVy3HaBO3eEQeemGttsq1BDlHgWcOjEOIAcga7OOEGWHD2WSu8HhL7I1F0mFFyci8VKU4tRNk+qtwDA== +jest-changed-files@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.1.0.tgz#de66b0f30453bca2aff98e9400f75905da495305" + integrity sha512-HS5MIJp3B8t0NRKGMCZkcDUZo36mVRvrDETl81aqljT1S9tqiHRSpyoOvWg9ZilzZG9TDisDNaN1IXm54fLRZw== dependencies: - "@jest/types" "^25.1.0" - execa "^3.2.0" + "@jest/types" "^26.1.0" + execa "^4.0.0" throat "^5.0.0" -jest-cli@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.1.0.tgz#75f0b09cf6c4f39360906bf78d580be1048e4372" - integrity sha512-p+aOfczzzKdo3AsLJlhs8J5EW6ffVidfSZZxXedJ0mHPBOln1DccqFmGCoO8JWd4xRycfmwy1eoQkMsF8oekPg== +jest-cli@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.1.0.tgz#eb9ec8a18cf3b6aa556d9deaa9e24be12b43ad87" + integrity sha512-Imumvjgi3rU7stq6SJ1JUEMaV5aAgJYXIs0jPqdUnF47N/Tk83EXfmtvNKQ+SnFVI6t6mDOvfM3aA9Sg6kQPSw== dependencies: - "@jest/core" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" + "@jest/core" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" exit "^0.1.2" + graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" + jest-config "^26.1.0" + jest-util "^26.1.0" + jest-validate "^26.1.0" prompts "^2.0.1" - realpath-native "^1.1.0" - yargs "^15.0.0" + yargs "^15.3.1" -jest-config@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.1.0.tgz#d114e4778c045d3ef239452213b7ad3ec1cbea90" - integrity sha512-tLmsg4SZ5H7tuhBC5bOja0HEblM0coS3Wy5LTCb2C8ZV6eWLewHyK+3qSq9Bi29zmWQ7ojdCd3pxpx4l4d2uGw== +jest-config@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.1.0.tgz#9074f7539acc185e0113ad6d22ed589c16a37a73" + integrity sha512-ONTGeoMbAwGCdq4WuKkMcdMoyfs5CLzHEkzFOlVvcDXufZSaIWh/OXMLa2fwKXiOaFcqEw8qFr4VOKJQfn4CVw== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^25.1.0" - "@jest/types" "^25.1.0" - babel-jest "^25.1.0" - chalk "^3.0.0" + "@jest/test-sequencer" "^26.1.0" + "@jest/types" "^26.1.0" + babel-jest "^26.1.0" + chalk "^4.0.0" + deepmerge "^4.2.2" glob "^7.1.1" - jest-environment-jsdom "^25.1.0" - jest-environment-node "^25.1.0" - jest-get-type "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.1.0" + jest-environment-node "^26.1.0" + jest-get-type "^26.0.0" + jest-jasmine2 "^26.1.0" + jest-regex-util "^26.0.0" + jest-resolve "^26.1.0" + jest-util "^26.1.0" + jest-validate "^26.1.0" micromatch "^4.0.2" - pretty-format "^25.1.0" - realpath-native "^1.1.0" - -jest-create-mock-instance@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/jest-create-mock-instance/-/jest-create-mock-instance-1.1.0.tgz#b69e97a4dad34ccebf180424a178217513644d2f" - integrity sha1-tp6XpNrTTM6/GAQkoXghdRNkTS8= + pretty-format "^26.1.0" -jest-diff@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" - integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== +jest-diff@^25.2.1: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== dependencies: chalk "^3.0.0" - diff-sequences "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" -jest-docblock@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.1.0.tgz#0f44bea3d6ca6dfc38373d465b347c8818eccb64" - integrity sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA== +jest-diff@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.1.0.tgz#00a549bdc936c9691eb4dc25d1fbd78bf456abb2" + integrity sha512-GZpIcom339y0OXznsEKjtkfKxNdg7bVbEofK8Q6MnevTIiR1jNhDWKhRX6X0SDXJlwn3dy59nZ1z55fLkAqPWg== dependencies: - detect-newline "^3.0.0" + chalk "^4.0.0" + diff-sequences "^26.0.0" + jest-get-type "^26.0.0" + pretty-format "^26.1.0" -jest-each@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.1.0.tgz#a6b260992bdf451c2d64a0ccbb3ac25e9b44c26a" - integrity sha512-R9EL8xWzoPySJ5wa0DXFTj7NrzKpRD40Jy+zQDp3Qr/2QmevJgkN9GqioCGtAJ2bW9P/MQRznQHQQhoeAyra7A== +jest-docblock@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" + integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== dependencies: - "@jest/types" "^25.1.0" - chalk "^3.0.0" - jest-get-type "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" - -jest-environment-jsdom@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz#6777ab8b3e90fd076801efd3bff8e98694ab43c3" - integrity sha512-ILb4wdrwPAOHX6W82GGDUiaXSSOE274ciuov0lztOIymTChKFtC02ddyicRRCdZlB5YSrv3vzr1Z5xjpEe1OHQ== - dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jsdom "^15.1.1" - -jest-environment-node@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.1.0.tgz#797bd89b378cf0bd794dc8e3dca6ef21126776db" - integrity sha512-U9kFWTtAPvhgYY5upnH9rq8qZkj6mYLup5l1caAjjx9uNnkLHN2xgZy5mo4SyLdmrh/EtB9UPpKFShvfQHD0Iw== - dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - -jest-get-type@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" - integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== - -jest-get-type@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" - integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== - -jest-haste-map@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.1.0.tgz#ae12163d284f19906260aa51fd405b5b2e5a4ad3" - integrity sha512-/2oYINIdnQZAqyWSn1GTku571aAfs8NxzSErGek65Iu5o8JYb+113bZysRMcC/pjE5v9w0Yz+ldbj9NxrFyPyw== - dependencies: - "@jest/types" "^25.1.0" + detect-newline "^3.0.0" + +jest-each@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.1.0.tgz#e35449875009a22d74d1bda183b306db20f286f7" + integrity sha512-lYiSo4Igr81q6QRsVQq9LIkJW0hZcKxkIkHzNeTMPENYYDw/W/Raq28iJ0sLlNFYz2qxxeLnc5K2gQoFYlu2bA== + dependencies: + "@jest/types" "^26.1.0" + chalk "^4.0.0" + jest-get-type "^26.0.0" + jest-util "^26.1.0" + pretty-format "^26.1.0" + +jest-environment-jsdom@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.1.0.tgz#9dc7313ffe1b59761dad1fedb76e2503e5d37c5b" + integrity sha512-dWfiJ+spunVAwzXbdVqPH1LbuJW/kDL+FyqgA5YzquisHqTi0g9hquKif9xKm7c1bKBj6wbmJuDkeMCnxZEpUw== + dependencies: + "@jest/environment" "^26.1.0" + "@jest/fake-timers" "^26.1.0" + "@jest/types" "^26.1.0" + jest-mock "^26.1.0" + jest-util "^26.1.0" + jsdom "^16.2.2" + +jest-environment-node@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.1.0.tgz#8bb387b3eefb132eab7826f9a808e4e05618960b" + integrity sha512-DNm5x1aQH0iRAe9UYAkZenuzuJ69VKzDCAYISFHQ5i9e+2Tbeu2ONGY7YStubCLH8a1wdKBgqScYw85+ySxqxg== + dependencies: + "@jest/environment" "^26.1.0" + "@jest/fake-timers" "^26.1.0" + "@jest/types" "^26.1.0" + jest-mock "^26.1.0" + jest-util "^26.1.0" + +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + +jest-get-type@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" + integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg== + +jest-haste-map@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.1.0.tgz#ef31209be73f09b0d9445e7d213e1b53d0d1476a" + integrity sha512-WeBS54xCIz9twzkEdm6+vJBXgRBQfdbbXD0dk8lJh7gLihopABlJmIQFdWSDDtuDe4PRiObsjZSUjbJ1uhWEpA== + dependencies: + "@jest/types" "^26.1.0" + "@types/graceful-fs" "^4.1.2" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.3" - jest-serializer "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + graceful-fs "^4.2.4" + jest-serializer "^26.1.0" + jest-util "^26.1.0" + jest-worker "^26.1.0" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" + which "^2.0.2" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz#681b59158a430f08d5d0c1cce4f01353e4b48137" - integrity sha512-GdncRq7jJ7sNIQ+dnXvpKO2MyP6j3naNK41DTTjEAhLEdpImaDA9zSAZwDhijjSF/D7cf4O5fdyUApGBZleaEg== +jest-jasmine2@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.1.0.tgz#4dfe349b2b2d3c6b3a27c024fd4cb57ac0ed4b6f" + integrity sha512-1IPtoDKOAG+MeBrKvvuxxGPJb35MTTRSDglNdWWCndCB3TIVzbLThRBkwH9P081vXLgiJHZY8Bz3yzFS803xqQ== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" + "@jest/environment" "^26.1.0" + "@jest/source-map" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" co "^4.6.0" - expect "^25.1.0" + expect "^26.1.0" is-generator-fn "^2.0.0" - jest-each "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" + jest-each "^26.1.0" + jest-matcher-utils "^26.1.0" + jest-message-util "^26.1.0" + jest-runtime "^26.1.0" + jest-snapshot "^26.1.0" + jest-util "^26.1.0" + pretty-format "^26.1.0" throat "^5.0.0" -jest-junit@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-10.0.0.tgz#c94b91c24920a327c9d2a075e897b2dba4af494b" - integrity sha512-dbOVRyxHprdSpwSAR9/YshLwmnwf+RSl5hf0kCGlhAcEeZY9aRqo4oNmaT0tLC16Zy9D0zekDjWkjHGjXlglaQ== +jest-junit@^12.2.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-12.2.0.tgz#cff7f9516e84f8e30f6bdea04cd84db6b095a376" + integrity sha512-ecGzF3KEQwLbMP5xMO7wqmgmyZlY/5yWDvgE/vFa+/uIT0KsU5nluf0D2fjIlOKB+tb6DiuSSpZuGpsmwbf7Fw== dependencies: - jest-validate "^24.9.0" - mkdirp "^0.5.1" + mkdirp "^1.0.4" strip-ansi "^5.2.0" - uuid "^3.3.3" + uuid "^8.3.2" xml "^1.0.1" -jest-leak-detector@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz#ed6872d15aa1c72c0732d01bd073dacc7c38b5c6" - integrity sha512-3xRI264dnhGaMHRvkFyEKpDeaRzcEBhyNrOG5oT8xPxOyUAblIAQnpiR3QXu4wDor47MDTiHbiFcbypdLcLW5w== +jest-leak-detector@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.1.0.tgz#039c3a07ebcd8adfa984b6ac015752c35792e0a6" + integrity sha512-dsMnKF+4BVOZwvQDlgn3MG+Ns4JuLv8jNvXH56bgqrrboyCbI1rQg6EI5rs+8IYagVcfVP2yZFKfWNZy0rK0Hw== dependencies: - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + jest-get-type "^26.0.0" + pretty-format "^26.1.0" -jest-matcher-utils@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz#fa5996c45c7193a3c24e73066fc14acdee020220" - integrity sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ== +jest-matcher-utils@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.1.0.tgz#cf75a41bd413dda784f022de5a65a2a5c73a5c92" + integrity sha512-PW9JtItbYvES/xLn5mYxjMd+Rk+/kIt88EfH3N7w9KeOrHWaHrdYPnVHndGbsFGRJ2d5gKtwggCvkqbFDoouQA== dependencies: - chalk "^3.0.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + chalk "^4.0.0" + jest-diff "^26.1.0" + jest-get-type "^26.0.0" + pretty-format "^26.1.0" -jest-message-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.1.0.tgz#702a9a5cb05c144b9aa73f06e17faa219389845e" - integrity sha512-Nr/Iwar2COfN22aCqX0kCVbXgn8IBm9nWf4xwGr5Olv/KZh0CZ32RKgZWMVDXGdOahicM10/fgjdimGNX/ttCQ== +jest-message-util@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.1.0.tgz#52573fbb8f5cea443c4d1747804d7a238a3e233c" + integrity sha512-dY0+UlldiAJwNDJ08SF0HdF32g9PkbF2NRK/+2iMPU40O6q+iSn1lgog/u0UH8ksWoPv0+gNq8cjhYO2MFtT0g== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/types" "^26.1.0" "@types/stack-utils" "^1.0.1" - chalk "^3.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" micromatch "^4.0.2" slash "^3.0.0" - stack-utils "^1.0.1" + stack-utils "^2.0.2" -jest-mock@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.1.0.tgz#411d549e1b326b7350b2e97303a64715c28615fd" - integrity sha512-28/u0sqS+42vIfcd1mlcg4ZVDmSUYuNvImP4X2lX5hRMLW+CN0BeiKVD4p+ujKKbSPKd3rg/zuhCF+QBLJ4vag== +jest-mock@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.1.0.tgz#80d8286da1f05a345fbad1bfd6fa49a899465d3d" + integrity sha512-1Rm8EIJ3ZFA8yCIie92UbxZWj9SuVmUGcyhLHyAhY6WI3NIct38nVcfOPWhJteqSn8V8e3xOMha9Ojfazfpovw== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^26.1.0" jest-pnp-resolver@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== -jest-regex-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.1.0.tgz#efaf75914267741838e01de24da07b2192d16d87" - integrity sha512-9lShaDmDpqwg+xAd73zHydKrBbbrIi08Kk9YryBEBybQFg/lBWR/2BDjjiSE7KIppM9C5+c03XiDaZ+m4Pgs1w== +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz#8a1789ec64eb6aaa77fd579a1066a783437e70d2" - integrity sha512-Cu/Je38GSsccNy4I2vL12ZnBlD170x2Oh1devzuM9TLH5rrnLW1x51lN8kpZLYTvzx9j+77Y5pqBaTqfdzVzrw== +jest-resolve-dependencies@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.1.0.tgz#1ce36472f864a5dadf7dc82fa158e1c77955691b" + integrity sha512-fQVEPHHQ1JjHRDxzlLU/buuQ9om+hqW6Vo928aa4b4yvq4ZHBtRSDsLdKQLuCqn5CkTVpYZ7ARh2fbA8WkRE6g== dependencies: - "@jest/types" "^25.1.0" - jest-regex-util "^25.1.0" - jest-snapshot "^25.1.0" + "@jest/types" "^26.1.0" + jest-regex-util "^26.0.0" + jest-snapshot "^26.1.0" -jest-resolve@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.1.0.tgz#23d8b6a4892362baf2662877c66aa241fa2eaea3" - integrity sha512-XkBQaU1SRCHj2Evz2Lu4Czs+uIgJXWypfO57L7JYccmAXv4slXA6hzNblmcRmf7P3cQ1mE7fL3ABV6jAwk4foQ== +jest-resolve@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.1.0.tgz#a530eaa302b1f6fa0479079d1561dd69abc00e68" + integrity sha512-KsY1JV9FeVgEmwIISbZZN83RNGJ1CC+XUCikf/ZWJBX/tO4a4NvA21YixokhdR9UnmPKKAC4LafVixJBrwlmfg== dependencies: - "@jest/types" "^25.1.0" - browser-resolve "^1.11.3" - chalk "^3.0.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" + jest-util "^26.1.0" + read-pkg-up "^7.0.1" + resolve "^1.17.0" + slash "^3.0.0" -jest-runner@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.1.0.tgz#fef433a4d42c89ab0a6b6b268e4a4fbe6b26e812" - integrity sha512-su3O5fy0ehwgt+e8Wy7A8CaxxAOCMzL4gUBftSs0Ip32S0epxyZPDov9Znvkl1nhVOJNf4UwAsnqfc3plfQH9w== +jest-runner@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.1.0.tgz#457f7fc522afe46ca6db1dccf19f87f500b3288d" + integrity sha512-elvP7y0fVDREnfqit0zAxiXkDRSw6dgCkzPCf1XvIMnSDZ8yogmSKJf192dpOgnUVykmQXwYYJnCx641uLTgcw== dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" + "@jest/console" "^26.1.0" + "@jest/environment" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-docblock "^25.1.0" - jest-haste-map "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-leak-detector "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + graceful-fs "^4.2.4" + jest-config "^26.1.0" + jest-docblock "^26.0.0" + jest-haste-map "^26.1.0" + jest-jasmine2 "^26.1.0" + jest-leak-detector "^26.1.0" + jest-message-util "^26.1.0" + jest-resolve "^26.1.0" + jest-runtime "^26.1.0" + jest-util "^26.1.0" + jest-worker "^26.1.0" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.1.0.tgz#02683218f2f95aad0f2ec1c9cdb28c1dc0ec0314" - integrity sha512-mpPYYEdbExKBIBB16ryF6FLZTc1Rbk9Nx0ryIpIMiDDkOeGa0jQOKVI/QeGvVGlunKKm62ywcioeFVzIbK03bA== - dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" +jest-runtime@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.1.0.tgz#45a37af42115f123ed5c51f126c05502da2469cb" + integrity sha512-1qiYN+EZLmG1QV2wdEBRf+Ci8i3VSfIYLF02U18PiUDrMbhfpN/EAMMkJtT02jgJUoaEOpHAIXG6zS3QRMzRmA== + dependencies: + "@jest/console" "^26.1.0" + "@jest/environment" "^26.1.0" + "@jest/fake-timers" "^26.1.0" + "@jest/globals" "^26.1.0" + "@jest/source-map" "^26.1.0" + "@jest/test-result" "^26.1.0" + "@jest/transform" "^26.1.0" + "@jest/types" "^26.1.0" "@types/yargs" "^15.0.0" - chalk "^3.0.0" + chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" - graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - realpath-native "^1.1.0" + graceful-fs "^4.2.4" + jest-config "^26.1.0" + jest-haste-map "^26.1.0" + jest-message-util "^26.1.0" + jest-mock "^26.1.0" + jest-regex-util "^26.0.0" + jest-resolve "^26.1.0" + jest-snapshot "^26.1.0" + jest-util "^26.1.0" + jest-validate "^26.1.0" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.0.0" + yargs "^15.3.1" -jest-serializer@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.1.0.tgz#73096ba90e07d19dec4a0c1dd89c355e2f129e5d" - integrity sha512-20Wkq5j7o84kssBwvyuJ7Xhn7hdPeTXndnwIblKDR2/sy1SUm6rWWiG9kSCgJPIfkDScJCIsTtOKdlzfIHOfKA== +jest-serializer@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.1.0.tgz#72a394531fc9b08e173dc7d297440ac610d95022" + integrity sha512-eqZOQG/0+MHmr25b2Z86g7+Kzd5dG9dhCiUoyUNJPgiqi38DqbDEOlHcNijyfZoj74soGBohKBZuJFS18YTJ5w== + dependencies: + graceful-fs "^4.2.4" -jest-snapshot@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.1.0.tgz#d5880bd4b31faea100454608e15f8d77b9d221d9" - integrity sha512-xZ73dFYN8b/+X2hKLXz4VpBZGIAn7muD/DAg+pXtDzDGw3iIV10jM7WiHqhCcpDZfGiKEj7/2HXAEPtHTj0P2A== +jest-snapshot@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.1.0.tgz#c36ed1e0334bd7bd2fe5ad07e93a364ead7e1349" + integrity sha512-YhSbU7eMTVQO/iRbNs8j0mKRxGp4plo7sJ3GzOQ0IYjvsBiwg0T1o0zGQAYepza7lYHuPTrG5J2yDd0CE2YxSw== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - expect "^25.1.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - mkdirp "^0.5.1" + "@jest/types" "^26.1.0" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.1.0" + graceful-fs "^4.2.4" + jest-diff "^26.1.0" + jest-get-type "^26.0.0" + jest-haste-map "^26.1.0" + jest-matcher-utils "^26.1.0" + jest-message-util "^26.1.0" + jest-resolve "^26.1.0" natural-compare "^1.4.0" - pretty-format "^25.1.0" - semver "^7.1.1" + pretty-format "^26.1.0" + semver "^7.3.2" -jest-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.1.0.tgz#7bc56f7b2abd534910e9fa252692f50624c897d9" - integrity sha512-7did6pLQ++87Qsj26Fs/TIwZMUFBXQ+4XXSodRNy3luch2DnRXsSnmpVtxxQ0Yd6WTipGpbhh2IFP1mq6/fQGw== +jest-util@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.1.0.tgz#80e85d4ba820decacf41a691c2042d5276e5d8d8" + integrity sha512-rNMOwFQevljfNGvbzNQAxdmXQ+NawW/J72dmddsK0E8vgxXCMtwQ/EH0BiWEIxh0hhMcTsxwAxINt7Lh46Uzbg== dependencies: - "@jest/types" "^25.1.0" - chalk "^3.0.0" + "@jest/types" "^26.1.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" is-ci "^2.0.0" - mkdirp "^0.5.1" - -jest-validate@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" - integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== - dependencies: - "@jest/types" "^24.9.0" - camelcase "^5.3.1" - chalk "^2.0.1" - jest-get-type "^24.9.0" - leven "^3.1.0" - pretty-format "^24.9.0" + micromatch "^4.0.2" -jest-validate@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.1.0.tgz#1469fa19f627bb0a9a98e289f3e9ab6a668c732a" - integrity sha512-kGbZq1f02/zVO2+t1KQGSVoCTERc5XeObLwITqC6BTRH3Adv7NZdYqCpKIZLUgpLXf2yISzQ465qOZpul8abXA== +jest-validate@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.1.0.tgz#942c85ad3d60f78250c488a7f85d8f11a29788e7" + integrity sha512-WPApOOnXsiwhZtmkDsxnpye+XLb/tUISP+H6cHjfUIXvlG+eKwP+isnivsxlHCPaO9Q5wvbhloIBkdF3qUn+Nw== dependencies: - "@jest/types" "^25.1.0" - camelcase "^5.3.1" - chalk "^3.0.0" - jest-get-type "^25.1.0" + "@jest/types" "^26.1.0" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.0.0" leven "^3.1.0" - pretty-format "^25.1.0" + pretty-format "^26.1.0" -jest-watcher@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.1.0.tgz#97cb4a937f676f64c9fad2d07b824c56808e9806" - integrity sha512-Q9eZ7pyaIr6xfU24OeTg4z1fUqBF/4MP6J801lyQfg7CsnZ/TCzAPvCfckKdL5dlBBEKBeHV0AdyjFZ5eWj4ig== +jest-watcher@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.1.0.tgz#99812a0cd931f0cb3d153180426135ab83e4d8f2" + integrity sha512-ffEOhJl2EvAIki613oPsSG11usqnGUzIiK7MMX6hE4422aXOcVEG3ySCTDFLn1+LZNXGPE8tuJxhp8OBJ1pgzQ== dependencies: - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/test-result" "^26.1.0" + "@jest/types" "^26.1.0" ansi-escapes "^4.2.1" - chalk "^3.0.0" - jest-util "^25.1.0" - string-length "^3.1.0" + chalk "^4.0.0" + jest-util "^26.1.0" + string-length "^4.0.1" -jest-worker@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.1.0.tgz#75d038bad6fdf58eba0d2ec1835856c497e3907a" - integrity sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg== +jest-worker@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.1.0.tgz#65d5641af74e08ccd561c240e7db61284f82f33d" + integrity sha512-Z9P5pZ6UC+kakMbNJn+tA2RdVdNX5WH1x+5UCBZ9MxIK24pjYtFt96fK+UwBTrjLYm232g1xz0L3eTh51OW+yQ== dependencies: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-25.1.0.tgz#b85ef1ddba2fdb00d295deebbd13567106d35be9" - integrity sha512-FV6jEruneBhokkt9MQk0WUFoNTwnF76CLXtwNMfsc0um0TlB/LG2yxUd0KqaFjEJ9laQmVWQWS0sG/t2GsuI0w== +jest@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.1.0.tgz#2f3aa7bcffb9bfd025473f83bbbf46a3af026263" + integrity sha512-LIti8jppw5BcQvmNJe4w2g1N/3V68HUfAv9zDVm7v+VAtQulGhH0LnmmiVkbNE4M4I43Bj2fXPiBGKt26k9tHw== dependencies: - "@jest/core" "^25.1.0" + "@jest/core" "^26.1.0" import-local "^3.0.2" - jest-cli "^25.1.0" + jest-cli "^26.1.0" jfs@^0.3.0: version "0.3.0" @@ -6634,6 +7159,11 @@ jfs@^0.3.0: mkdirp "~0.5.1" uuid "^3.1.0" +jju@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo= + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6652,36 +7182,36 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@^15.1.1: - version "15.2.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" - integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== - dependencies: - abab "^2.0.0" - acorn "^7.1.0" - acorn-globals "^4.3.2" - array-equal "^1.0.0" - cssom "^0.4.1" - cssstyle "^2.0.0" - data-urls "^1.1.0" - domexception "^1.0.1" - escodegen "^1.11.1" - html-encoding-sniffer "^1.0.2" +jsdom@^16.2.2: + version "16.2.2" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.2.2.tgz#76f2f7541646beb46a938f5dc476b88705bedf2b" + integrity sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg== + dependencies: + abab "^2.0.3" + acorn "^7.1.1" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.2.0" + data-urls "^2.0.0" + decimal.js "^10.2.0" + domexception "^2.0.1" + escodegen "^1.14.1" + html-encoding-sniffer "^2.0.1" + is-potential-custom-element-name "^1.0.0" nwsapi "^2.2.0" - parse5 "5.1.0" - pn "^1.1.0" - request "^2.88.0" - request-promise-native "^1.0.7" - saxes "^3.1.9" - symbol-tree "^3.2.2" + parse5 "5.1.1" + request "^2.88.2" + request-promise-native "^1.0.8" + saxes "^5.0.0" + symbol-tree "^3.2.4" tough-cookie "^3.0.1" - w3c-hr-time "^1.0.1" - w3c-xmlserializer "^1.1.2" - webidl-conversions "^4.0.2" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.0.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" - whatwg-url "^7.0.0" - ws "^7.0.0" + whatwg-url "^8.0.0" + ws "^7.2.3" xml-name-validator "^3.0.0" jsesc@^2.5.1: @@ -6689,12 +7219,12 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-bigint@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" - integrity sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4= +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== dependencies: - bignumber.js "^7.0.0" + bignumber.js "^9.0.0" json-buffer@3.0.0: version "3.0.0" @@ -6711,6 +7241,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -6733,6 +7268,13 @@ json5@2.x, json5@^2.1.0: dependencies: minimist "^1.2.0" +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -6749,6 +7291,15 @@ jsonfile@^6.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -6764,37 +7315,21 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" - integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ== - dependencies: - array-includes "^3.0.3" - object.assign "^4.1.0" - -jsx-ast-utils@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" - integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== - dependencies: - array-includes "^3.0.3" - object.assign "^4.1.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== dependencies: buffer-equal-constant-time "1.0.1" ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.1.5: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== dependencies: - jwa "^1.4.1" + jwa "^2.0.0" safe-buffer "^5.0.1" keyv@^3.0.0: @@ -6864,27 +7399,27 @@ lazy-cache@^2.0.1: dependencies: set-getter "^0.1.0" -lerna@^3.20.2: - version "3.20.2" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.20.2.tgz#abf84e73055fe84ee21b46e64baf37b496c24864" - integrity sha512-bjdL7hPLpU3Y8CBnw/1ys3ynQMUjiK6l9iDWnEGwFtDy48Xh5JboR9ZJwmKGCz9A/sarVVIGwf1tlRNKUG9etA== +lerna@^3.22.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" + integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== dependencies: - "@lerna/add" "3.20.0" - "@lerna/bootstrap" "3.20.0" - "@lerna/changed" "3.20.0" - "@lerna/clean" "3.20.0" + "@lerna/add" "3.21.0" + "@lerna/bootstrap" "3.21.0" + "@lerna/changed" "3.21.0" + "@lerna/clean" "3.21.0" "@lerna/cli" "3.18.5" - "@lerna/create" "3.18.5" - "@lerna/diff" "3.18.5" - "@lerna/exec" "3.20.0" - "@lerna/import" "3.18.5" - "@lerna/info" "3.20.0" - "@lerna/init" "3.18.5" - "@lerna/link" "3.18.5" - "@lerna/list" "3.20.0" - "@lerna/publish" "3.20.2" - "@lerna/run" "3.20.0" - "@lerna/version" "3.20.2" + "@lerna/create" "3.22.0" + "@lerna/diff" "3.21.0" + "@lerna/exec" "3.21.0" + "@lerna/import" "3.22.0" + "@lerna/info" "3.21.0" + "@lerna/init" "3.21.0" + "@lerna/link" "3.21.0" + "@lerna/list" "3.21.0" + "@lerna/publish" "3.22.1" + "@lerna/run" "3.21.0" + "@lerna/version" "3.22.1" import-local "^2.0.0" npmlog "^4.1.2" @@ -6893,7 +7428,15 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@^0.3.0, levn@~0.3.0: +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= @@ -6906,68 +7449,38 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@^10.0.8: - version "10.0.8" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.0.8.tgz#0f7849cdc336061f25f5d4fcbcfa385701ff4739" - integrity sha512-Oa9eS4DJqvQMVdywXfEor6F4vP+21fPHF8LUXgBbVWUSWBddjqsvO6Bv1LwMChmgQZZqwUvgJSHlu8HFHAPZmA== - dependencies: - chalk "^3.0.0" - commander "^4.0.1" - cosmiconfig "^6.0.0" - debug "^4.1.1" - dedent "^0.7.0" - execa "^3.4.0" - listr "^0.14.3" - log-symbols "^3.0.0" - micromatch "^4.0.2" +lint-staged@^11.1.2: + version "11.1.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.1.2.tgz#4dd78782ae43ee6ebf2969cad9af67a46b33cd90" + integrity sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" normalize-path "^3.0.0" please-upgrade-node "^3.2.0" string-argv "0.3.1" stringify-object "^3.3.0" -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== +listr2@^3.8.2: + version "3.12.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.12.0.tgz#755d77fdbc5bd290d7860ced28061ba123088b58" + integrity sha512-DLaOIhIBXxSDGfAuGyQPsQs6XPIJrUE1MaNYBq8aUS3bulSAEl9RMNNuRbfdxonTizL5ztAYvCZKKnP3gFSvYg== dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" load-json-file@^1.0.0: version "1.1.0" @@ -6980,16 +7493,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -7039,11 +7542,6 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.at@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" - integrity sha1-k83OZk8KGZTqM9181A4jr9EbD/g= - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -7069,11 +7567,6 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= -lodash.has@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -7084,6 +7577,11 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -7109,12 +7607,17 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.14.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.2.1: +lodash@^4.14.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.2.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7127,28 +7630,23 @@ log-ok@^0.1.1: ansi-green "^0.1.1" success-symbol "^0.1.0" -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - chalk "^2.4.2" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" -log-update@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: - ansi-escapes "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" log-utils@^0.2.1: version "0.2.1" @@ -7163,19 +7661,12 @@ log-utils@^0.2.1: time-stamp "^1.0.1" warning-symbol "^0.1.0" -lolex@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7215,13 +7706,20 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" -lru-cache@^5.0.0, lru-cache@^5.1.1: +lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" @@ -7361,12 +7859,20 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3" integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A== -messaging-api-common@1.0.0-beta.16, messaging-api-common@^1.0.0-beta.16: - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/messaging-api-common/-/messaging-api-common-1.0.0-beta.16.tgz#e07ce89556ed9976ce4b809c817d9fa4cca5f9a2" - integrity sha512-QoUnHLa4akSnkw3jjrYjJPQwzxa65l4xR82UGABtFOxkPje6nzyv7edXHwpVEXohdp+2A6AHrKXK2ok389uIoQ== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +messaging-api-common@1.0.4, messaging-api-common@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/messaging-api-common/-/messaging-api-common-1.0.4.tgz#793d5cca5e97793ecaea238ff61bd901cfffac56" + integrity sha512-riYl+FnUtbUxxBfmUXBZSw4akK9hWAGGobulT81DK4Y5s/zeCKQcwa5uKHLG5HrR0RCtATIj7tQM8J29z61jRg== dependencies: - axios "^0.19.2" + "@types/debug" "^4.1.5" + "@types/lodash" "^4.14.156" + "@types/url-join" "^4.0.0" + axios "^0.21.1" camel-case "^4.1.1" debug "^4.1.1" lodash "^4.17.15" @@ -7375,74 +7881,90 @@ messaging-api-common@1.0.0-beta.16, messaging-api-common@^1.0.0-beta.16: snake-case "^3.0.3" url-join "^4.0.1" -messaging-api-line@1.0.0-beta.20: - version "1.0.0-beta.20" - resolved "https://registry.yarnpkg.com/messaging-api-line/-/messaging-api-line-1.0.0-beta.20.tgz#7470ff978b43ccf0d7d291cd74c0008c78a1284d" - integrity sha512-LNWdAc59YuMh5pp4QZkO692pdkpXe6UOLwcXWNu6ABSxZoJnO6ss49m9bQcx6p8nOc5xbnRqIJAyRt3uNDNxAA== +messaging-api-line@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/messaging-api-line/-/messaging-api-line-1.0.6.tgz#e723e838726652ee513ee6caf400632575169f0c" + integrity sha512-RlJTonApLF4X4zR//9l0+cjCfrxng9InqHGZPya5Afr6PhGmLttzjOFkGVLE5uOM/RpiivciylueAay2tBu9Gw== dependencies: - axios "^0.19.2" - axios-error "^1.0.0-beta.16" + "@types/warning" "^3.0.0" + axios "^0.21.1" + axios-error "^1.0.4" image-type "^4.1.0" - invariant "^2.2.4" - messaging-api-common "^1.0.0-beta.16" + messaging-api-common "^1.0.4" + ts-invariant "^0.4.4" + type-fest "^0.15.1" warning "^4.0.3" -messaging-api-messenger@1.0.0-beta.18: - version "1.0.0-beta.18" - resolved "https://registry.yarnpkg.com/messaging-api-messenger/-/messaging-api-messenger-1.0.0-beta.18.tgz#50ef461dc3743712d7bb459bf5c0d6e1893b342e" - integrity sha512-2epVMxyGReG/mmKn0ugYEEZr6sHrdtz7Np4yRs855zqTOqzNzLYvfDwpEUwNbbGe0serDsyt8s4s2tbr5FJPbg== +messaging-api-messenger@1.0.6, messaging-api-messenger@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/messaging-api-messenger/-/messaging-api-messenger-1.0.6.tgz#606209b615dd539d82ec5b6e813b2a2dfea47585" + integrity sha512-yLrUGMF5v/PHMtEbfB62C70bqHsL9L9XGGA1v/GCJQeyjS4OWmSXOJTwDUm31chuppWaYx4br7WkxGM8UCDCfQ== dependencies: + "@types/append-query" "^2.0.0" + "@types/lodash" "^4.14.156" + "@types/warning" "^3.0.0" append-query "^2.1.0" - axios "^0.19.2" - axios-error "^1.0.0-beta.16" + axios "^0.21.1" + axios-error "^1.0.4" form-data "^3.0.0" - invariant "^2.2.4" lodash "^4.17.15" - messaging-api-common "^1.0.0-beta.16" + messaging-api-common "^1.0.4" + ts-invariant "^0.4.4" warning "^4.0.3" -messaging-api-slack@1.0.0-beta.16: - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/messaging-api-slack/-/messaging-api-slack-1.0.0-beta.16.tgz#0a509b807d16771e2a639f893fbec7d1a4cd0280" - integrity sha512-bu3Nyva9ZLWiSRMnGOasBk4likAvNWU4ZICwwD2if11Cb0emN3pQhtr9DBPk6/FXi/25PQ+f/ZNeB9Y0pz5+qQ== - dependencies: - axios "^0.19.2" - axios-error "^1.0.0-beta.16" - lodash "^4.17.15" - messaging-api-common "^1.0.0-beta.16" - -messaging-api-telegram@1.0.0-beta.16: - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/messaging-api-telegram/-/messaging-api-telegram-1.0.0-beta.16.tgz#9091df3838a35ea0d735dc48814eacc490e77439" - integrity sha512-R9lWthXQrJw6ySEDyM5oOoBHCwERrOc1YriUAwCq1XD7yqBQae6lMPucJqa0jj2Rzcua4ZrOOQrKqvHJKV7Pog== +messaging-api-slack@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/messaging-api-slack/-/messaging-api-slack-1.0.6.tgz#26328c03c70d53ac2ef483bba0029e4bb809e166" + integrity sha512-A49Z+64mHhj42m+MwZiHjFti5A2nZtCv+DCgxQdBSKcG4oXwb3hHcXr0LNCuZBCPVJz+1XszGvHKf+LZy3+K5Q== dependencies: - axios "^0.19.2" - axios-error "^1.0.0-beta.16" + "@types/lodash" "^4.14.156" + "@types/warning" "^3.0.0" + axios "^0.21.1" + axios-error "^1.0.4" lodash "^4.17.15" - messaging-api-common "^1.0.0-beta.16" + messaging-api-common "^1.0.4" + ts-invariant "^0.4.4" + warning "^4.0.3" -messaging-api-viber@1.0.0-beta.16: - version "1.0.0-beta.16" - resolved "https://registry.yarnpkg.com/messaging-api-viber/-/messaging-api-viber-1.0.0-beta.16.tgz#c382ae98170729cd2bd2b15f0c92b495074b671b" - integrity sha512-Gp+MTA3dz/EIOn50NED08bT9kkD4IC/fTDF82dO/UIo57PafCIogP2Mtqooft6gtKMM5PR0Tlcki6rEj7mywtg== +messaging-api-telegram@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/messaging-api-telegram/-/messaging-api-telegram-1.0.6.tgz#16aeb10ec60ee28b2f934787089cc4af4de911ae" + integrity sha512-DX2R1DcnSx4fLvlGAJliSJjixhIaZdXigCtamuBSuNHPIW75WDoOgNtJekK2sJlNW6+Us3wppkkxlg/IukNn/Q== dependencies: - axios "^0.19.2" - axios-error "^1.0.0-beta.16" + "@types/lodash" "^4.14.156" + "@types/warning" "^3.0.0" + axios "^0.21.1" + axios-error "^1.0.4" lodash "^4.17.15" - messaging-api-common "^1.0.0-beta.16" + messaging-api-common "^1.0.4" + ts-invariant "^0.4.4" + warning "^4.0.3" -messenger-batch@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/messenger-batch/-/messenger-batch-0.3.1.tgz#045bc5d9d02fe595c9f54d3e4314326a85d71084" - integrity sha512-mA0QDOOB0WASvJxu32A7medVGJpuY1+IKDzQd7ipbsJS5lwN5cvP3QJWOgpvm7Py1bQ8Woi8zcsYoPwyC4Rv3w== - dependencies: - invariant "^2.2.4" +messaging-api-viber@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/messaging-api-viber/-/messaging-api-viber-1.0.6.tgz#d796a5cc78750e6c9e86fb7d463013ea00664bee" + integrity sha512-e2U72f5L8bPcubRfX4V1mRbL0d/sdxBxpbptNv3gV70K1diSv2kPXWfiDziMojYQ/Ob66SMR4354iHlm8qWXaw== + dependencies: + "@types/warning" "^3.0.0" + axios "^0.21.1" + axios-error "^1.0.4" + messaging-api-common "^1.0.4" + ts-invariant "^0.4.4" + warning "^4.0.3" -methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micromatch@4.x, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -7462,13 +7984,13 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" mime-db@1.40.0: version "1.40.0" @@ -7482,15 +8004,15 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.40.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.2.0: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== +mime@^2.4.6: + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== mimic-fn@^1.0.0: version "1.2.0" @@ -7581,12 +8103,17 @@ mkdirp-promise@^5.0.1: dependencies: mkdirp "*" -mkdirp@*, mkdirp@^1.0.0, mkdirp@^1.0.3: +mkdirp@*: version "1.0.3" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: +mkdirp@1.x, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -7598,6 +8125,11 @@ mkpath@^0.1.0: resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-0.1.0.tgz#7554a6f8d871834cc97b5462b122c4c124d6de91" integrity sha1-dVSm+Nhxg0zJe1RisSLEwSTW3pE= +mockdate@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb" + integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -7638,7 +8170,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.0.0: +ms@2.1.2, ms@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -7714,10 +8246,10 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -ngrok@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/ngrok/-/ngrok-3.2.5.tgz#db2153e7dc4827aeafcc13b187aec331516403d9" - integrity sha512-FWWQJSg8A1L6prZmT53onZMiFiaY+CfDgS9YStKjbE3qf2WDmRdi6kNBFvQKD2ARSv/te+rqeizAOGSUH5X56w== +ngrok@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ngrok/-/ngrok-3.4.1.tgz#ee20a912831e68a7ac86a6576c21fadbd8458df7" + integrity sha512-OTm6Nmi6JINPbzkZff8ysA2WqMeNDg3sOPMFHW2CpatVD5yJxmX1qdyLq3QYNACTKNB3/K9jTkG4wUVpAFX9Dw== dependencies: "@types/node" "^8.10.50" "@types/request" "^2.48.2" @@ -7739,15 +8271,14 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" -nock@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.1.tgz#6d497d01f23cb52c733545c97e09e8318f6af801" - integrity sha512-f5u5k7O5D2YXH2WEFQVLLPa36D5C0dxU9Lrg6KOuaFCMDt7yd1W4S3hbZClCMczxc4EZ0k1bEhPeMWSewrxYNw== +nock@^13.1.3: + version "13.1.3" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.3.tgz#110b005965654a8ffb798e87bad18b467bff15f9" + integrity sha512-YKj0rKQWMGiiIO+Y65Ut8OEgYM3PplLU2+GAhnPmqZdBd6z5IskgdBqWmjzA6lH3RF0S2a3wiAlrMOF5Iv2Jeg== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash "^4.17.13" - mkdirp "^1.0.0" + lodash.set "^4.3.2" propagate "^2.0.0" node-fetch-npm@^2.0.2: @@ -7759,15 +8290,20 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: +node-fetch@^2.3.0, node-fetch@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-forge@^0.9.0: - version "0.9.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" - integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== +node-fetch@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.2.tgz#986996818b73785e47b1965cc34eb093a1d464d0" + integrity sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA== + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-gyp@^5.0.2: version "5.0.3" @@ -7796,16 +8332,17 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-6.0.0.tgz#cea319e06baa16deec8ce5cd7f133c4a46b68e12" - integrity sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw== +node-notifier@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-7.0.1.tgz#a355e33e6bebacef9bf8562689aed0f4230ca6f9" + integrity sha512-VkzhierE7DBmQEElhTGJIoiZa1oqRijOtgOlsXg32KrJRXsPy0NXFBqWGW/wTswnJlDCs5viRYaqWguqzsKcmg== dependencies: growly "^1.3.0" is-wsl "^2.1.1" - semver "^6.3.0" + semver "^7.2.1" shellwords "^0.1.1" - which "^1.3.1" + uuid "^7.0.3" + which "^2.0.2" nodemon@^2.0.2: version "2.0.2" @@ -7922,7 +8459,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^4.0.0: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -7954,7 +8491,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -7968,6 +8505,16 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -7995,24 +8542,23 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" - integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" -object.entries@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" - integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== +object.entries@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== dependencies: define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.17.5" has "^1.0.3" object.fromentries@^2.0.2: @@ -8040,25 +8586,14 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" - integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" - -object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== +object.values@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" + integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + es-abstract "^1.18.2" octokit-pagination-methods@^1.1.0: version "1.1.0" @@ -8093,12 +8628,14 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -opencollective-postinstall@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" - integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" -optionator@^0.8.1, optionator@^0.8.2: +optionator@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= @@ -8110,17 +8647,17 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" os-homedir@^1.0.0: version "1.0.2" @@ -8163,11 +8700,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -8210,7 +8742,7 @@ p-map-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-map@^2.0.0, p-map@^2.1.0: +p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -8222,6 +8754,13 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-pipe@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" @@ -8367,10 +8906,10 @@ parse-url@^5.0.0: parse-path "^4.0.0" protocols "^1.4.0" -parse5@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" - integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parse5@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== parseurl@~1.3.3: version "1.3.3" @@ -8442,6 +8981,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427" + integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -8451,13 +8995,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -8490,6 +9027,11 @@ picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -8545,6 +9087,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= + dependencies: + find-up "^2.1.0" + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -8552,11 +9101,6 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - pointer-symbol@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pointer-symbol/-/pointer-symbol-1.0.0.tgz#60f9110204ea7a929b62644a21315543cbb3d447" @@ -8567,6 +9111,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -8589,17 +9138,18 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier-package-json@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/prettier-package-json/-/prettier-package-json-2.1.3.tgz#c2383552aa226abe7a8a1b289d7844ffc8440d71" - integrity sha512-GP245nK+xMsayxUghlmiCpzWSxfUywU6IADXy7KVc0cfb+Gs/yzzbqkseFTbmwXfld722gA1VpApy8INGdVfyg== +prettier-package-json@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/prettier-package-json/-/prettier-package-json-2.6.0.tgz#624eeb604a4c19af146c1d1c824d31afd2690e2a" + integrity sha512-CS7utu4Jfm6xxCrIA4zZiOtNIwZhq0EyHnK01vmliV2QRU+L6/Ywy1tB6uUpT9Lwt5qpvRMHNApa6jxoRHfafA== dependencies: commander "^4.0.1" - fs-extra "^8.1.0" + cosmiconfig "^7.0.0" + fs-extra "^10.0.0" glob "^7.1.6" minimatch "^3.0.4" parse-author "^2.0.0" - sort-object-keys "^1.1.2" + sort-object-keys "^1.1.3" sort-order "^1.0.1" prettier@^1.19.1: @@ -8607,22 +9157,27 @@ prettier@^1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -pretty-format@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" - integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== +prettier@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" + integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== + +pretty-format@^25.2.1, pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== dependencies: - "@jest/types" "^24.9.0" - ansi-regex "^4.0.0" - ansi-styles "^3.2.0" - react-is "^16.8.4" + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" -pretty-format@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" - integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== +pretty-format@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.1.0.tgz#272b9cd1f1a924ab5d443dc224899d7a65cb96ec" + integrity sha512-GmeO1PEYdM+non4BKCj+XsPJjFOJIPnsLewqhDVoqY1xo0yNmDas7tC2XwpMrRAHR3MaE2hPo37deX5OisJ2Wg== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^26.1.0" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -8730,15 +9285,6 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - propagate@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" @@ -8749,10 +9295,15 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protobufjs@^6.8.0, protobufjs@^6.8.6, protobufjs@^6.8.8: - version "6.8.8" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" - integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== +proto3-json-serializer@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.3.tgz#3b4d5f481dbb923dd88e259ed03b0629abc9a8e7" + integrity sha512-X0DAtxCBsy1NDn84huVFGOFgBslT2gBmM+85nY6/5SOAaCon1jzVNdvi74foIyFvs5CjtSbQsepsM5TsyNhqQw== + +protobufjs@6.11.2, protobufjs@^6.10.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -8764,8 +9315,27 @@ protobufjs@^6.8.0, protobufjs@^6.8.6, protobufjs@^6.8.8: "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.0" - "@types/node" "^10.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + +protobufjs@^6.8.9: + version "6.9.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.9.0.tgz#c08b2bf636682598e6fabbf0edb0b1256ff090bd" + integrity sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" long "^4.0.0" protocols@^1.1.0, protocols@^1.4.0: @@ -8855,16 +9425,23 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1: - version "6.9.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" - integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== +qs@^6.9.4: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" @@ -8909,11 +9486,6 @@ react-is@^16.12.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== -react-is@^16.8.1, react-is@^16.8.4: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" - integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== - read-chunk@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca" @@ -8958,14 +9530,6 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -8974,6 +9538,15 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -8983,15 +9556,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -9001,6 +9565,16 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + read@1, read@~1.0.1: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -9053,6 +9627,15 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.1.1, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-web-to-node-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz#751e632f466552ac0d5c440cc01470352f93c4b7" @@ -9105,13 +9688,6 @@ readline@^1.3.0: resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" integrity sha1-xYDXfvLPyHUrEySYBg3JeTp6wBw= -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -9152,11 +9728,6 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" -regenerator-runtime@^0.13.2: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" - integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -9165,23 +9736,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regexpp@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" - integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== registry-auth-token@^3.0.1: version "3.4.0" @@ -9242,6 +9800,13 @@ request-promise-core@1.1.2: dependencies: lodash "^4.17.11" +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + request-promise-native@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" @@ -9251,6 +9816,15 @@ request-promise-native@^1.0.7: stealthy-require "^1.1.1" tough-cookie "^2.3.3" +request-promise-native@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + dependencies: + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" @@ -9277,11 +9851,42 @@ request@^2.87.0, request@^2.88.0: tunnel-agent "^0.6.0" uuid "^3.3.2" +request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -9334,25 +9939,36 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@1.x, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.5.0: +resolve@^1.10.0, resolve@^1.3.2: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" -resolve@^1.14.2: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== +resolve@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" +resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +resolve@~1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -9399,6 +10015,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -9406,13 +10027,6 @@ rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" @@ -9446,6 +10060,13 @@ run-async@^2.4.0: dependencies: is-promise "^2.1.0" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -9453,7 +10074,7 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.3.3, rxjs@^6.4.0: +rxjs@^6.4.0: version "6.5.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== @@ -9467,6 +10088,13 @@ rxjs@^6.5.3: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -9511,12 +10139,12 @@ saslprep@^1.0.0: dependencies: sparse-bitfield "^3.0.3" -saxes@^3.1.9: - version "3.1.11" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" - integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== +saxes@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== dependencies: - xmlchars "^2.1.1" + xmlchars "^2.2.0" semver-compare@^1.0.0: version "1.0.0" @@ -9537,30 +10165,27 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== - -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@7.x, semver@^7.2.1, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" - integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A== - -semver@^7.1.1: - version "7.1.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6" - integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" semver@~5.3.0: version "5.3.0" @@ -9682,19 +10307,25 @@ shortid@^2.2.15: dependencies: nanoid "^2.1.0" -side-channel@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" - integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: - es-abstract "^1.17.0-next.1" - object-inspect "^1.7.0" + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +signal-exit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + sisteransi@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" @@ -9715,19 +10346,23 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" slide@^1.1.6: version "1.1.6" @@ -9800,10 +10435,10 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -sort-object-keys@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.2.tgz#d3a6c48dc2ac97e6bc94367696e03f6d09d37952" - integrity sha1-06bEjcKsl+a8lDZ2luA/bQnTeVI= +sort-object-keys@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" + integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== sort-order@^1.0.1: version "1.0.1" @@ -9930,10 +10565,12 @@ ssri@^6.0.0, ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" -stack-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" - integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stack-utils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" + integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg== + dependencies: + escape-string-regexp "^2.0.0" standard-as-callback@^2.0.1: version "2.0.1" @@ -9976,13 +10613,13 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-length@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837" - integrity sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA== +string-length@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" + integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== dependencies: - astral-regex "^1.0.0" - strip-ansi "^5.2.0" + char-regex "^1.0.2" + strip-ansi "^6.0.0" string-width@^1.0.1: version "1.0.2" @@ -10028,17 +10665,13 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" -string.prototype.matchall@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" - integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0" - has-symbols "^1.0.1" - internal-slot "^1.0.2" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" string.prototype.trimleft@^2.1.1: version "2.1.1" @@ -10056,6 +10689,14 @@ string.prototype.trimright@^2.1.1: define-properties "^1.1.3" function-bind "^1.1.1" +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -10156,10 +10797,15 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= -strip-json-comments@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-json-comments@~2.0.1: version "2.0.1" @@ -10190,34 +10836,30 @@ success-symbol@^0.1.0: resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= -superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== +superagent@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" -supertest@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" - integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== +supertest@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.6.tgz#6151c518f4c5ced2ac2aadb9f96f1bf8198174c8" + integrity sha512-0hACYGNJ8OHRg8CRITeZOdbjur7NLuNs0mBjVhdpxi7hP6t3QIbOzLON5RTUmZcy2I9riuII3+Pr2C7yztrIIg== dependencies: methods "^1.1.2" - superagent "^3.8.3" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + superagent "^6.1.0" supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" @@ -10241,25 +10883,22 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" -symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - -symbol-tree@^3.2.2: +symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" tar@^4.4.10, tar@^4.4.8: version "4.4.19" @@ -10373,7 +11012,7 @@ through2@^3.0.0, through2@^3.0.1: dependencies: readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -10476,7 +11115,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3: +tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -10508,6 +11147,13 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" + integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== + dependencies: + punycode "^2.1.1" + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -10533,10 +11179,17 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= -ts-jest@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.2.1.tgz#49bf05da26a8b7fbfbc36b4ae2fcdc2fef35c85d" - integrity sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A== +ts-invariant@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + +ts-jest@^26.1.1: + version "26.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" + integrity sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -10544,10 +11197,30 @@ ts-jest@^25.2.1: json5 "2.x" lodash.memoize "4.x" make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "^16.1.0" + micromatch "4.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "18.x" + +tsconfig-paths@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" + integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" tslib@^1.10.0: version "1.11.1" @@ -10559,10 +11232,15 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== +tslib@^1.9.3: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -10578,6 +11256,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -10590,6 +11275,26 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + +type-fest@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.15.1.tgz#d2c4e73d3e4a53cf1a906396dd460a1c5178ca00" + integrity sha512-n+UXrN8i5ioo7kqT/nF8xsEzLaqFra7k32SEsSPwvXVGyAcRgV/FUQN/sgfptJTR1oRmmq7z4IXMFSM7im7C9A== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -10600,11 +11305,21 @@ type-fest@^0.5.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + type-fest@^0.8.0, type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -10625,10 +11340,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@^3.9.6: + version "3.9.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" + integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== uglify-js@^3.1.4: version "3.13.5" @@ -10645,6 +11360,16 @@ umask@^1.1.0: resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + undefsafe@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" @@ -10707,6 +11432,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -10813,33 +11543,35 @@ util-promisify@^2.1.0: dependencies: object.getownpropertydescriptors "^2.0.3" -util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== -v8-to-istanbul@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.2.tgz#387d173be5383dbec209d21af033dcb892e3ac82" - integrity sha512-G9R+Hpw0ITAmPSr47lSlc5A1uekSYzXxTMlFxso2xoffwo4jQnzbv1p9yXIinO8UMZKfAFewaCHwWvnH4Jb4Ug== +v8-to-istanbul@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz#b97936f21c0e2d9996d4985e5c5156e9d4e49cd6" + integrity sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -10874,27 +11606,20 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -w3c-hr-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" - integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== dependencies: - browser-process-hrtime "^0.1.2" + browser-process-hrtime "^1.0.0" -w3c-xmlserializer@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" - integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== dependencies: - domexception "^1.0.1" - webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" -walkdir@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" - integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== - walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -10926,14 +11651,24 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5: +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" -whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: +whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== @@ -10947,16 +11682,31 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +whatwg-url@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.1.0.tgz#c628acdcf45b82274ce7281ee31dd3c839791771" + integrity sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^5.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - which@1, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -10964,7 +11714,7 @@ which@1, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -11016,7 +11766,7 @@ with-open-file@^0.1.6: p-try "^2.1.0" pify "^4.0.1" -word-wrap@~1.2.3: +word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -11026,14 +11776,6 @@ wordwrap@^1.0.0, wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -11052,6 +11794,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -11108,13 +11859,6 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^5.2.0: version "5.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" @@ -11122,10 +11866,10 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^7.0.0: - version "7.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" - integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A== +ws@^7.2.3: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" + integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== xdg-basedir@^3.0.0: version "3.0.0" @@ -11147,7 +11891,7 @@ xml@^1.0.1: resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= -xmlchars@^2.1.1: +xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== @@ -11162,6 +11906,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -11172,37 +11921,37 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2" - integrity sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw== - dependencies: - "@babel/runtime" "^7.6.3" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^15.0.0: - version "15.0.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" - integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@^16.1.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" - integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== +yargs-parser@18.x, yargs-parser@^18.1.1: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^17.1.0: - version "17.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-17.1.0.tgz#b95ff3201e98b89e86070f92bef636016a0b0766" - integrity sha512-67zLl4/kWtp9eyVuxX+fHZ2Ey4ySWh0awDJlk/EtT0vzspsXbzrFsh76WjYSP3L++zhSwHQRUE3MCBe754RuEg== +yargs-parser@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08" + integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs@^14.2.2: version "14.2.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5" @@ -11220,10 +11969,10 @@ yargs@^14.2.2: y18n "^4.0.0" yargs-parser "^15.0.0" -yargs@^15.0.0: - version "15.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.2.0.tgz#cb9fc7f7ec429f7e9329b623f5c707a62dae506a" - integrity sha512-E+o8C37U+M7N15rBJVxr0MoInp+O7XNhMqveSGWA5uhddqs8qtkZ+uvT9FI32QML0SKidXdDONr40Xe3tDO9FA== +yargs@^15.3.1: + version "15.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" + integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== dependencies: cliui "^6.0.0" decamelize "^1.2.0" @@ -11235,4 +11984,17 @@ yargs@^15.0.0: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^17.1.0" + yargs-parser "^18.1.1" + +yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2"