Repository: Yoctol/bottender
Pull request #104 (2017-12-12 10:54:41)

Add viber webhook and help cli
```
bottender viber webhook set
bottender viber webhook delete
```


Patch:
diff --git a/src/cli/index.js b/src/cli/index.js
index bc7ddc61..f91730e7 100644
--- a/src/cli/index.js
+++ b/src/cli/index.js
@@ -18,6 +18,7 @@ const main = async argv => {
     case 'messenger':
     case 'telegram':
     case 'line':
+    case 'viber':
       providerName = argv._[0];
       subcommand = argv._[1];
       break;
diff --git a/src/cli/providers/index.js b/src/cli/providers/index.js
index f4a398c3..48c481c5 100644
--- a/src/cli/providers/index.js
+++ b/src/cli/providers/index.js
@@ -2,10 +2,12 @@ import sh from './sh';
 import messenger from './messenger';
 import telegram from './telegram';
 import line from './line';
+import viber from './viber';
 
 export default {
   sh,
   messenger,
   telegram,
   line,
+  viber,
 };
diff --git a/src/cli/providers/sh/help.js b/src/cli/providers/sh/help.js
index 20077d65..c8b74f39 100644
--- a/src/cli/providers/sh/help.js
+++ b/src/cli/providers/sh/help.js
@@ -11,6 +11,7 @@ const help = () => {
         messenger   <command>   Manage your Messenger command
         telegram    <command>   Manage your Telegram command
         line        <command>   Manage your LINE command
+        viber       <command>   Manage your Viber command
 
       ${chalk.dim('Global')}
 
diff --git a/src/cli/providers/telegram/help.js b/src/cli/providers/telegram/help.js
index c1118889..f6cc9bf5 100644
--- a/src/cli/providers/telegram/help.js
+++ b/src/cli/providers/telegram/help.js
@@ -2,7 +2,7 @@ import chalk from 'chalk';
 
 const help = () => {
   console.log(`
-    bottender telegram <command> <action>
+    bottender telegram <command> <action> [options]
 
     ${chalk.dim('Commands:')}
 
@@ -10,7 +10,13 @@ const help = () => {
 
     ${chalk.dim('Actions:')}
 
+      get                   Get the webhook information
       set                   Set the property you request
+      del, delete           Delete the webhook
+
+    ${chalk.dim('Options:')}
+
+      -w                    Webhook callback URL
 
     ${chalk.dim('Examples:')}
 
diff --git a/src/cli/providers/telegram/webhook.js b/src/cli/providers/telegram/webhook.js
index 4b42931d..8fd87042 100644
--- a/src/cli/providers/telegram/webhook.js
+++ b/src/cli/providers/telegram/webhook.js
@@ -101,15 +101,22 @@ export async function deleteWebhook() {
 
 export default async function main(ctx) {
   const subcommand = ctx.argv._[2];
-  if (subcommand === 'get') {
-    await getWebhook();
-  } else if (subcommand === 'set') {
-    const webhook = ctx.argv.w;
-    await setWebhook(webhook);
-  } else if (subcommand === 'delete') {
-    await deleteWebhook();
-  } else {
-    error(`Please specify a valid subcommand: get, set, delete`);
-    help();
+
+  switch (subcommand) {
+    case 'get':
+      await getWebhook();
+      break;
+    case 'set': {
+      const webhook = ctx.argv.w;
+      await setWebhook(webhook);
+      break;
+    }
+    case 'delete':
+    case 'del':
+      await deleteWebhook();
+      break;
+    default:
+      error(`Please specify a valid subcommand: get, set, delete`);
+      help();
   }
 }
diff --git a/src/cli/providers/viber/help.js b/src/cli/providers/viber/help.js
new file mode 100644
index 00000000..1e279432
--- /dev/null
+++ b/src/cli/providers/viber/help.js
@@ -0,0 +1,31 @@
+import chalk from 'chalk';
+
+const help = () => {
+  console.log(`
+    bottender viber <command> <action> [options]
+
+    ${chalk.dim('Commands:')}
+
+      webhook   <action>    Manage webhook
+
+    ${chalk.dim('Actions:')}
+
+      set                   Set the property you request
+      del, delete           Delete the webhook
+
+    ${chalk.dim('Options:')}
+
+      -w                    Webhook callback URL
+      -e                    The types of Viber events. Seperate events by comma (e.g. delivered,seen)
+
+    ${chalk.dim('Examples:')}
+
+    ${chalk.dim('-')} Set viber webhook
+
+      ${chalk.cyan(
+        '$ bottender viber webhook set -w http://example.com -e delivered,seen'
+      )}
+  `);
+};
+
+export default help;
diff --git a/src/cli/providers/viber/index.js b/src/cli/providers/viber/index.js
new file mode 100644
index 00000000..088612a3
--- /dev/null
+++ b/src/cli/providers/viber/index.js
@@ -0,0 +1,10 @@
+export default {
+  title: 'Viber',
+  subcommands: new Set(['help', 'webhook']),
+  get webhook() {
+    return require('./webhook').default;
+  },
+  get help() {
+    return require('./help').default;
+  },
+};
diff --git a/src/cli/providers/viber/webhook.js b/src/cli/providers/viber/webhook.js
new file mode 100644
index 00000000..1f8e1ba3
--- /dev/null
+++ b/src/cli/providers/viber/webhook.js
@@ -0,0 +1,100 @@
+import { ViberClient } from 'messaging-api-viber';
+import axios from 'axios';
+import get from 'lodash/get';
+import invariant from 'invariant';
+
+import getConfig from '../../shared/getConfig';
+import { print, error, bold } from '../../shared/log';
+
+import help from './help';
+
+export const localClient = axios.create({
+  baseURL: 'http://localhost:4040',
+});
+
+const getWebhookFromNgrok = async () => {
+  const res = await localClient.get('/api/tunnels');
+  return get(res, 'data.tunnels[1].public_url'); // tunnels[1] return `https` protocol
+};
+
+export async function setWebhook(_webhook, eventTypes = []) {
+  try {
+    const { accessToken } = getConfig('bottender.config.js', 'viber');
+
+    invariant(accessToken, '`accessToken` is not found in config file');
+
+    const client = ViberClient.connect(accessToken);
+    const webhook = _webhook || (await getWebhookFromNgrok());
+
+    invariant(
+      webhook,
+      '`webhook` is required but not found. Use -w <webhook> to setup or make sure you are running ngrok server.'
+    );
+
+    await client.setWebhook(webhook, eventTypes);
+
+    print('Successfully set Viber webhook callback URL');
+  } catch (err) {
+    error('Failed to set Viber webhook');
+    if (err.response) {
+      error(`status: ${bold(err.response.status)}`);
+      if (err.response.data) {
+        error(`data: ${bold(JSON.stringify(err.response.data, null, 2))}`);
+      }
+    } else {
+      error(err.message);
+    }
+    return process.exit(1);
+  }
+}
+
+export async function deleteWebhook() {
+  try {
+    const { accessToken } = getConfig('bottender.config.js', 'viber');
+
+    invariant(accessToken, '`accessToken` is not found in config file');
+
+    const client = ViberClient.connect(accessToken);
+
+    await client.removeWebhook();
+
+    print('Successfully delete Viber webhook');
+  } catch (err) {
+    error('Failed to delete Viber webhook');
+    if (err.response) {
+      error(`status: ${bold(err.response.status)}`);
+      if (err.response.data) {
+        error(`data: ${bold(JSON.stringify(err.response.data, null, 2))}`);
+      }
+    } else {
+      error(err.message);
+    }
+    return process.exit(1);
+  }
+}
+
+export default async function main(ctx) {
+  const subcommand = ctx.argv._[2];
+
+  switch (subcommand) {
+    case 'set': {
+      const webhook = ctx.argv.w;
+
+      if (typeof ctx.argv.e === 'string') {
+        const eventTypes = ctx.argv.e.split(',');
+        await setWebhook(webhook, eventTypes);
+      } else {
+        await setWebhook(webhook);
+      }
+
+      break;
+    }
+    case 'delete':
+    case 'del':
+      await deleteWebhook();
+      break;
+    default:
+      error(`Please specify a valid subcommand: set, delete`);
+      help();
+  }
+}


Test patch:
diff --git a/src/cli/providers/__tests__/index.spec.js b/src/cli/providers/__tests__/index.spec.js
index d26be71a..0f4bb798 100644
--- a/src/cli/providers/__tests__/index.spec.js
+++ b/src/cli/providers/__tests__/index.spec.js
@@ -4,6 +4,7 @@ jest.mock('../sh');
 jest.mock('../messenger');
 jest.mock('../telegram');
 jest.mock('../line');
+jest.mock('../viber');
 
 describe('providers', () => {
   it('should exist', () => {
@@ -29,4 +30,9 @@ describe('providers', () => {
     const line = require('../line').default;
     expect(providers.line).toEqual(line);
   });
+
+  it('should return viber', () => {
+    const viber = require('../viber').default;
+    expect(providers.viber).toEqual(viber);
+  });
 });
diff --git a/src/cli/providers/viber/__tests__/deleteWebhook.spec.js b/src/cli/providers/viber/__tests__/deleteWebhook.spec.js
new file mode 100644
index 00000000..6a70e4a0
--- /dev/null
+++ b/src/cli/providers/viber/__tests__/deleteWebhook.spec.js
@@ -0,0 +1,64 @@
+import { deleteWebhook } from '../webhook';
+
+jest.mock('messaging-api-viber');
+jest.mock('../../../shared/log');
+jest.mock('../../../shared/getConfig');
+
+const { ViberClient } = require('messaging-api-viber');
+
+const log = require('../../../shared/log');
+const getConfig = require('../../../shared/getConfig');
+
+const _exit = process.exit;
+const MOCK_FILE_WITH_PLATFORM = {
+  viber: {
+    accessToken: '__accessToken__',
+  },
+};
+
+beforeEach(() => {
+  process.exit = jest.fn();
+  getConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.viber);
+
+  ViberClient.connect.mockReturnValue({
+    removeWebhook: jest.fn(() => ({
+      data: {
+        status: 0,
+        status_message: 'ok',
+      },
+    })),
+  });
+});
+
+afterEach(() => {
+  process.exit = _exit;
+});
+
+it('be defined', () => {
+  expect(deleteWebhook).toBeDefined();
+});
+
+describe('resolve', () => {
+  it('successfully delete webhook', async () => {
+    await deleteWebhook();
+
+    expect(log.print).toHaveBeenCalledTimes(1);
+    expect(log.print.mock.calls[0][0]).toMatch(/Successfully/);
+  });
+});
+
+describe('reject', () => {
+  it('reject when Viber return not success', async () => {
+    ViberClient.connect().removeWebhook.mockImplementation(() =>
+      Promise.reject()
+    );
+
+    expect(deleteWebhook().then).toThrow();
+  });
+
+  it('reject when `accessToken` is not found in config file', async () => {
+    getConfig.mockReturnValueOnce(null);
+
+    expect(deleteWebhook().then).toThrow();
+  });
+});
diff --git a/src/cli/providers/viber/__tests__/help.spec.js b/src/cli/providers/viber/__tests__/help.spec.js
new file mode 100644
index 00000000..57991ad1
--- /dev/null
+++ b/src/cli/providers/viber/__tests__/help.spec.js
@@ -0,0 +1,23 @@
+import help from '../help';
+
+const consoleLog = console.log;
+
+describe('help', () => {
+  beforeEach(() => {
+    console.log = jest.fn();
+  });
+
+  afterEach(() => {
+    console.log = consoleLog;
+  });
+
+  it('shouild exist', () => {
+    expect(help).toBeDefined();
+  });
+
+  it('should call console.log', () => {
+    help();
+
+    expect(console.log).toBeCalled();
+  });
+});
diff --git a/src/cli/providers/viber/__tests__/index.spec.js b/src/cli/providers/viber/__tests__/index.spec.js
new file mode 100644
index 00000000..9dc679fb
--- /dev/null
+++ b/src/cli/providers/viber/__tests__/index.spec.js
@@ -0,0 +1,20 @@
+import sh from '..';
+
+jest.mock('../webhook');
+jest.mock('../help');
+
+describe('telegram cli', () => {
+  it('should exist', () => {
+    expect(sh).toBeDefined();
+  });
+
+  it('should return webhook module', () => {
+    const webhook = require('../webhook').default;
+    expect(sh.webhook).toEqual(webhook);
+  });
+
+  it('should return help module', () => {
+    const help = require('../help').default;
+    expect(sh.help).toEqual(help);
+  });
+});
diff --git a/src/cli/providers/viber/__tests__/setWebhook.spec.js b/src/cli/providers/viber/__tests__/setWebhook.spec.js
new file mode 100644
index 00000000..0fec486a
--- /dev/null
+++ b/src/cli/providers/viber/__tests__/setWebhook.spec.js
@@ -0,0 +1,125 @@
+import MockAdapter from 'axios-mock-adapter';
+
+import { setWebhook, localClient as _localClient } from '../webhook';
+
+jest.mock('messaging-api-viber');
+jest.mock('../../../shared/log');
+jest.mock('../../../shared/getConfig');
+
+const { ViberClient } = require('messaging-api-viber');
+
+const log = require('../../../shared/log');
+const getConfig = require('../../../shared/getConfig');
+
+const MOCK_FILE_WITH_PLATFORM = {
+  viber: {
+    accessToken: '__accessToken__',
+  },
+};
+const _exit = process.exit;
+
+let localClient;
+
+const setup = (
+  { webhook = 'http://example.com/webhook', eventTypes = [] } = {
+    webhook: 'http://example.com/webhook',
+    eventTypes: [],
+  }
+) => ({
+  webhook,
+  eventTypes,
+});
+
+beforeEach(() => {
+  process.exit = jest.fn();
+  getConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.viber);
+
+  localClient = new MockAdapter(_localClient);
+  localClient.onGet('/api/tunnels').reply(200, {
+    tunnels: [
+      {
+        public_url: 'http://fakeDomain.ngrok.io',
+      },
+      {
+        public_url: 'https://fakeDomain.ngrok.io',
+      },
+    ],
+  });
+
+  ViberClient.connect.mockReturnValue({
+    setWebhook: jest.fn(() => ({
+      data: {
+        status: 0,
+        status_message: 'ok',
+        event_types: [
+          'delivered',
+          'seen',
+          'failed',
+          'subscribed',
+          'unsubscribed',
+          'conversation_started',
+        ],
+      },
+    })),
+  });
+});
+
+afterEach(() => {
+  process.exit = _exit;
+});
+
+it('be defined', () => {
+  expect(setWebhook).toBeDefined();
+});
+
+describe('resolve', () => {
+  it('successfully set webhook', async () => {
+    const { webhook, eventTypes } = setup({
+      eventTypes: ['delivered', 'seen'],
+    });
+
+    await setWebhook(webhook, eventTypes);
+
+    expect(ViberClient.connect().setWebhook).toBeCalledWith(
+      webhook,
+      eventTypes
+    );
+    expect(log.print).toHaveBeenCalledTimes(1);
+    expect(log.print.mock.calls[0][0]).toMatch(/Successfully/);
+  });
+
+  it('get ngrok webhook to setup', async () => {
+    await setWebhook(undefined);
+
+    expect(ViberClient.connect().setWebhook).toBeCalledWith(
+      'https://fakeDomain.ngrok.io',
+      []
+    );
+    expect(log.print).toHaveBeenCalledTimes(1);
+    expect(log.print.mock.calls[0][0]).toMatch(/Successfully/);
+  });
+});
+
+describe('reject', () => {
+  it('reject when `accessToken` not found in config file', async () => {
+    const { webhook } = setup();
+    getConfig.mockReturnValue({});
+    process.exit = jest.fn();
+
+    await setWebhook(webhook);
+
+    expect(log.error).toBeCalledWith(
+      '`accessToken` is not found in config file'
+    );
+    expect(process.exit).toBeCalled();
+  });
+
+  it('reject when viber return not success', () => {
+    const { webhook } = setup();
+    ViberClient.connect().setWebhook.mockReturnValueOnce(() =>
+      Promise.reject()
+    );
+
+    expect(setWebhook(webhook).then).toThrow();
+  });
+});