Repository: Yoctol/bottender
Pull request #111 (2017-12-19 17:24:23)

Typing API Refinement
We have some experiments internally and show that instead of

```js
sendTextWithDelay(1000, 'xxx');
```

below api is easier to use and compose:

```js
sendText('xxx', { delay: 1000 });
```





Patch:
diff --git a/src/context/SlackContext.js b/src/context/SlackContext.js
index cc9dfa1b..85f89d37 100644
--- a/src/context/SlackContext.js
+++ b/src/context/SlackContext.js
@@ -66,8 +66,8 @@ export default class SlackContext extends Context implements PlatformContext {
    * Send text to the owner of then session.
    *
    */
-  sendText(text: string): Promise<any> {
-    return this.postMessage(text);
+  sendText(text: string, options?: {}): Promise<any> {
+    return this.postMessage(text, options);
   }
 
   // FIXME: this is to fix type checking
diff --git a/src/plugins/withTyping.js b/src/plugins/withTyping.js
index 34b77c49..9078dba5 100644
--- a/src/plugins/withTyping.js
+++ b/src/plugins/withTyping.js
@@ -1,56 +1,133 @@
 import sleep from 'delay';
+import warning from 'warning';
+import omit from 'lodash/omit';
 
-const methods = [
-  'sendText',
-  'sendMessage',
-  'sendAttachment',
-  'sendImage',
-  'sendAudio',
-  'sendVideo',
-  'sendFile',
-  'sendQuickReplies',
-  'sendTemplate',
-  'sendGenericTemplate',
-  'sendButtonTemplate',
-  'sendListTemplate',
-  'sendOpenGraphTemplate',
-  'sendMediaTemplate',
-  'sendReceiptTemplate',
-  'sendAirlineBoardingPassTemplate',
-  'sendAirlineCheckinTemplate',
-  'sendAirlineItineraryTemplate',
-  'sendAirlineFlightUpdateTemplate',
-  'sendLocation',
-  'sendSticker',
-  'sendImagemap',
-  'sendConfirmTemplate',
-  'sendCarouselTemplate',
-  'sendImageCarouselTemplate',
-  'sendMessage',
-  'sendPhoto',
-  'sendDocument',
-  'sendVoice',
-  'sendVenue',
-  'sendContact',
-  'sendPicture',
-  'sendURL',
-  'sendCarouselContent',
-];
+const methodMap = {
+  // method name, arguments length (includes options)
+  messenger: [
+    ['sendText', 2],
+    ['sendMessage', 2],
+    ['sendAttachment', 2],
+    ['sendImage', 2],
+    ['sendAudio', 2],
+    ['sendVideo', 2],
+    ['sendFile', 2],
+    ['sendQuickReplies', 3], // FIXME: remove later
+    ['sendTemplate', 2],
+    ['sendGenericTemplate', 2],
+    ['sendButtonTemplate', 3],
+    ['sendListTemplate', 3],
+    ['sendOpenGraphTemplate', 2],
+    ['sendMediaTemplate', 2],
+    ['sendReceiptTemplate', 2],
+    ['sendAirlineBoardingPassTemplate', 2],
+    ['sendAirlineCheckinTemplate', 2],
+    ['sendAirlineItineraryTemplate', 2],
+    ['sendAirlineFlightUpdateTemplate', 2],
+  ],
+  line: [
+    // LINE
+    ['sendText', 2],
+    ['sendImage', 3],
+    ['sendVideo', 3],
+    ['sendAudio', 3],
+    ['sendLocation', 2],
+    ['sendSticker', 3],
+    ['sendImagemap', 3],
+    ['sendButtonTemplate', 3],
+    ['sendConfirmTemplate', 3],
+    ['sendCarouselTemplate', 3],
+    ['sendImageCarouselTemplate', 3],
+    ['replyText', 2],
+    ['replyImage', 3],
+    ['replyVideo', 3],
+    ['replyAudio', 3],
+    ['replyLocation', 2],
+    ['replySticker', 3],
+    ['replyImagemap', 3],
+    ['replyButtonTemplate', 3],
+    ['replyConfirmTemplate', 3],
+    ['replyCarouselTemplate', 3],
+    ['replyImageCarouselTemplate', 3],
+    ['pushText', 2],
+    ['pushImage', 3],
+    ['pushVideo', 3],
+    ['pushAudio', 3],
+    ['pushLocation', 2],
+    ['pushSticker', 3],
+    ['pushImagemap', 3],
+    ['pushButtonTemplate', 3],
+    ['pushConfirmTemplate', 3],
+    ['pushCarouselTemplate', 3],
+    ['pushImageCarouselTemplate', 3],
+  ],
+  slack: [['sendText', 2], ['postMessage', 2]],
+  telegram: [
+    ['sendText', 2],
+    ['sendMessage', 2],
+    ['sendPhoto', 2],
+    ['sendAudio', 2],
+    ['sendDocument', 2],
+    ['sendSticker', 2],
+    ['sendVideo', 2],
+    ['sendVoice', 2],
+    ['sendVideoNote', 2],
+    ['sendMediaGroup', 2],
+    ['sendLocation', 2],
+    ['sendVenue', 2],
+    ['sendContact', 2],
+  ],
+  viber: [
+    ['sendText', 2],
+    ['sendMessage', 2],
+    ['sendPicture', 2],
+    ['sendVideo', 2],
+    ['sendFile', 2],
+    ['sendContact', 2],
+    ['sendLocation', 2],
+    ['sendURL', 2],
+    ['sendSticker', 2],
+    ['sendCarouselContent', 2],
+  ],
+};
 
 export default options => context => {
+  const methods = methodMap[context.platform];
+  if (!methods) return;
+
   for (let i = 0; i < methods.length; i++) {
-    if (context[methods[i]]) {
-      const _method = context[methods[i]];
+    const [method, len] = methods[i];
+    if (context[method]) {
+      const _method = context[method];
       /* eslint-disable func-names */
-      context[methods[i]] = async function(...args) {
+      context[method] = async function(...args) {
+        const methodOptions = args[len - 1];
+
+        const delay =
+          methodOptions &&
+          typeof methodOptions === 'object' &&
+          typeof methodOptions.delay === 'number'
+            ? methodOptions.delay
+            : options.delay;
+
+        if (methodOptions) {
+          args[len - 1] = omit(methodOptions, ['delay']);
+        }
+
         if (this.typingOn) {
           await this.typingOn();
         }
-        await sleep(options.delay);
+
+        await sleep(delay);
+
         return _method.call(context, ...args);
       };
 
-      context[`${methods[i]}WithDelay`] = async function(delay, ...args) {
+      context[`${method}WithDelay`] = async function(delay, ...args) {
+        warning(
+          false,
+          `\`${method}WithDelay\` is deprecated. Use ${method} with \`options.delay\` instead`
+        );
         if (this.typingOn) {
           await this.typingOn();
         }


Test patch:
diff --git a/src/plugins/__tests__/withTyping.spec.js b/src/plugins/__tests__/withTyping.spec.js
index 1a8da793..99159595 100644
--- a/src/plugins/__tests__/withTyping.spec.js
+++ b/src/plugins/__tests__/withTyping.spec.js
@@ -6,6 +6,7 @@ jest.mock('delay');
 
 function setup() {
   const context = {
+    platform: 'messenger',
     typingOn: jest.fn(),
     sendText: jest.fn(),
   };
@@ -14,28 +15,169 @@ function setup() {
   };
 }
 
-describe('default typing', () => {
-  it('should typingOn and delay', async () => {
-    const { context } = setup();
+it('should typingOn and delay with default typing', async () => {
+  const { context } = setup();
+
+  const _send = context.sendText;
+
+  withTyping({ delay: 1000 })(context);
+
+  await context.sendText('Hello');
+
+  expect(context.typingOn).toBeCalled();
+  expect(delay).toBeCalledWith(1000);
+  expect(_send).toBeCalledWith('Hello');
+});
+
+it('typing duration can be overwritten', async () => {
+  const { context } = setup();
+
+  const _send = context.sendText;
+
+  withTyping({ delay: 1000 })(context);
+
+  await context.sendText('Hello', { delay: 2000 });
+
+  expect(context.typingOn).toBeCalled();
+  expect(delay).toBeCalledWith(2000);
+  expect(_send).toBeCalledWith('Hello', {});
+});
+
+it('should attch sendTextWithDelay', () => {
+  const { context } = setup();
+
+  withTyping({ delay: 1000 })(context);
+
+  expect(context.sendTextWithDelay).toBeDefined();
+});
+
+describe('platforms', () => {
+  it('works with messenger', async () => {
+    const context = {
+      platform: 'messenger',
+      typingOn: jest.fn(),
+      sendGenericTemplate: jest.fn(),
+    };
+
+    const _send = context.sendGenericTemplate;
+
+    withTyping({ delay: 1000 })(context);
+
+    await context.sendGenericTemplate([]);
+
+    expect(delay).toBeCalledWith(1000);
+    expect(_send).toBeCalledWith([]);
+  });
+
+  it('works with line', async () => {
+    const context = {
+      platform: 'line',
+      sendImagemap: jest.fn(),
+    };
+
+    const _send = context.sendImagemap;
+
+    withTyping({ delay: 1000 })(context);
+
+    const imagemap = {
+      baseUrl: 'https://example.com/bot/images/rm001',
+      baseWidth: 1040,
+      baseHeight: 1040,
+      actions: [
+        {
+          type: 'uri',
+          linkUri: 'https://example.com/',
+          area: {
+            x: 0,
+            y: 0,
+            width: 520,
+            height: 1040,
+          },
+        },
+        {
+          type: 'message',
+          text: 'hello',
+          area: {
+            x: 520,
+            y: 0,
+            width: 520,
+            height: 1040,
+          },
+        },
+      ],
+    };
+
+    await context.sendImagemap('this is an imagemap', imagemap);
+
+    expect(delay).toBeCalledWith(1000);
+    expect(_send).toBeCalledWith('this is an imagemap', imagemap);
+  });
+
+  it('works with telegram', async () => {
+    const context = {
+      platform: 'telegram',
+      sendVenue: jest.fn(),
+    };
+
+    const _send = context.sendVenue;
+
+    withTyping({ delay: 1000 })(context);
+
+    await context.sendVenue({
+      latitude: 30,
+      longitude: 45,
+      title: 'a_title',
+      address: 'an_address',
+    });
+
+    expect(delay).toBeCalledWith(1000);
+    expect(_send).toBeCalledWith({
+      latitude: 30,
+      longitude: 45,
+      title: 'a_title',
+      address: 'an_address',
+    });
+  });
+
+  it('works with slack', async () => {
+    const context = {
+      platform: 'slack',
+      sendText: jest.fn(),
+    };
 
     const _send = context.sendText;
 
     withTyping({ delay: 1000 })(context);
 
-    await context.sendText('Hello');
+    await context.sendText('text');
 
-    expect(context.typingOn).toBeCalled();
     expect(delay).toBeCalledWith(1000);
-    expect(_send).toBeCalledWith('Hello');
+    expect(_send).toBeCalledWith('text');
   });
-});
 
-describe('withDelay methods', () => {
-  it('should attch sendTextWithDelay', () => {
-    const { context } = setup();
+  it('works with viber', async () => {
+    const context = {
+      platform: 'viber',
+      sendVideo: jest.fn(),
+    };
+
+    const _send = context.sendVideo;
 
     withTyping({ delay: 1000 })(context);
 
-    expect(context.sendTextWithDelay).toBeDefined();
+    await context.sendVideo({
+      media: 'http://www.images.com/video.mp4',
+      size: 10000,
+      thumbnail: 'http://www.images.com/thumb.jpg',
+      duration: 10,
+    });
+
+    expect(delay).toBeCalledWith(1000);
+    expect(_send).toBeCalledWith({
+      media: 'http://www.images.com/video.mp4',
+      size: 10000,
+      thumbnail: 'http://www.images.com/thumb.jpg',
+      duration: 10,
+    });
   });
 });