Repository: Yoctol/bottender
Pull request #758 (2020-05-07 05:20:56)

feat: slack route accept any requests by passing *
improve routes for the following usages:

# handle any events
- slack.event('*', action)

# handle any commands
- slack.command('*', action)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fc3eb46..c3be6c24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 1.4.4 / 2020-05-06
+
+## slack
+
+- [fix] convert slack interactive message event to camelcase (#755).
+
 # 1.4.3 / 2020-04-29
 
 - [type] use string instead enum to compare.
diff --git a/README.md b/README.md
index 825eeed5..416ba7e3 100644
--- a/README.md
+++ b/README.md
@@ -57,16 +57,23 @@ Here is the first one to get you started:
 
 ```js
 // index.js
-module.export = async function App(context) {
-  await context.sendText('Hello World');
-};
-```
+const { router, text } = require('bottender/router');
 
-```sh
-npx bottender start --console
-```
+async function SayHi(context) {
+  await context.sendText('Hi!');
+}
 
-It creates and runs a bot that always replies with "Hello World" in the console.
+async function Unknown(context) {
+  await context.sendText('Sorry, I don’t know what you say.');
+}
+
+module.export = function App(context) {
+  return router([
+    text('hi', SayHi),
+    text('*', Unknown),
+  ]);
+};
+```
 
 ## Notable Features
 
diff --git a/docs/advanced-guides-custom-server.md b/docs/advanced-guides-custom-server.md
index 9a16d0c0..bfb69425 100644
--- a/docs/advanced-guides-custom-server.md
+++ b/docs/advanced-guides-custom-server.md
@@ -9,16 +9,10 @@ title: Running Bottender with Custom Servers
 
 Thus, You can find those lines in your generated `package.json`:
 
-```json
-{
-  ...
+```
   "scripts": {
     "dev": "bottender dev",
     "start": "bottender start",
-    ...
-  },
-  ...
-}
 ```
 
 When executing one of `bottender dev` or `bottender start` command, under the hood, it sets up a default express server for Bottender and load `index.js` from the project root directory as the root action of the chatbot.
@@ -27,7 +21,7 @@ To run Bottender with the custom HTTP server, you need to prepare an HTTP server
 
 ## Express
 
-### Creating A New Project with Custom Express Server
+### Creating a New Project with Custom Express Server
 
 If you want to have a clean project with the custom express, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-express) to develop your project. There are four steps you could follow to create your project:
 
@@ -38,7 +32,7 @@ If you want to have a clean project with the custom express, you could start fro
 
 If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom express server by following the migration instructions below.
 
-### Migrating An Existing Project to Custom Express Server
+### Migrating an Existing Project to Custom Express Server
 
 Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using express server. In this case, you need to write a custom express server and delegate all chatbot's webhook requests to the Bottender app.
 
@@ -64,13 +58,11 @@ const handle = app.getRequestHandler();
 app.prepare().then(() => {
   const server = express();
 
-  server.use(
-    bodyParser.json({
-      verify: (req, _, buf) => {
-        req.rawBody = buf.toString();
-      },
-    })
-  );
+  const verify = (req, _, buf) => {
+    req.rawBody = buf.toString();
+  };
+  server.use(bodyParser.json({ verify }));
+  server.use(bodyParser.urlencoded({ extended: false, verify }));
 
   // your custom route
   server.get('/api', (req, res) => {
@@ -89,25 +81,19 @@ app.prepare().then(() => {
 });
 ```
 
-3. Modify scripts in the `package.json` to `nodemon server.js` and `node server.js` instead:
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
 
-```json
-{
-  ...
+```
   "scripts": {
     "dev": "nodemon server.js",
     "start": "node server.js",
-    ...
-  },
-  ...
-}
 ```
 
 That's all you need to do, and you can have you bot hosting on the custom express server!
 
 ## Koa
 
-### Creating A New Project with Custom Koa Server
+### Creating a New Project with Custom Koa Server
 
 If you want to have a clean project with the custom koa, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-koa) to develop your project. There are four steps you could follow to create your project:
 
@@ -118,7 +104,7 @@ If you want to have a clean project with the custom koa, you could start from [t
 
 If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom koa server by following the migration instructions below.
 
-### Migrating An Existing Project to Custom Koa Server
+### Migrating an Existing Project to Custom Koa Server
 
 Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using koa server. In this case, you need to write a custom koa server and delegate all chatbot's webhook requests to the Bottender app.
 
@@ -143,6 +129,14 @@ const handle = app.getRequestHandler();
 
 app.prepare().then(() => {
   const server = new Koa();
+
+  server.use(bodyParser());
+  server.use((ctx, next) => {
+    ctx.req.body = ctx.request.body;
+    ctx.req.rawBody = ctx.request.rawBody;
+    return next();
+  });
+
   const router = new Router();
 
   router.get('/api', ctx => {
@@ -154,12 +148,6 @@ app.prepare().then(() => {
     ctx.respond = false;
   });
 
-  server.use(bodyParser());
-  server.use((ctx, next) => {
-    ctx.req.body = ctx.request.body;
-    ctx.req.rawBody = ctx.request.rawBody;
-    return next();
-  });
   server.use(router.routes());
 
   server.listen(port, err => {
@@ -169,25 +157,19 @@ app.prepare().then(() => {
 });
 ```
 
-3. Modify scripts in the `package.json` to `nodemon server.js` and `node server.js` instead:
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
 
-```json
-{
-  ...
+```
   "scripts": {
     "dev": "nodemon server.js",
     "start": "node server.js",
-    ...
-  },
-  ...
-}
 ```
 
 That's all you need to do, and you can have you bot hosting on the custom koa server!
 
 ## Restify
 
-### Creating A New Project with Custom Restify Server
+### Creating a New Project with Custom Restify Server
 
 If you want to have a clean project with the custom restify, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-restify) to develop your project. There are four steps you could follow to create your project:
 
@@ -198,7 +180,7 @@ If you want to have a clean project with the custom restify, you could start fro
 
 If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom restify server by following the migration instructions below.
 
-### Migrating An Existing Project to Custom Restify Server
+### Migrating an Existing Project to Custom Restify Server
 
 Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using restify server. In this case, you need to write a custom restify server and delegate all chatbot's webhook requests to the Bottender app.
 
@@ -243,18 +225,12 @@ app.prepare().then(() => {
 });
 ```
 
-3. Modify scripts in the `package.json` to `nodemon server.js` and `node server.js` instead:
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
 
-```json
-{
-  ...
+```
   "scripts": {
     "dev": "nodemon server.js",
     "start": "node server.js",
-    ...
-  },
-  ...
-}
 ```
 
 That's all you need to do, and you can have you bot hosting on the custom restify server!
diff --git a/docs/channel-slack-routing.md b/docs/channel-slack-routing.md
index 2ca03a4d..c7bc37fe 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/examples/custom-server-express/server.js b/examples/custom-server-express/server.js
index 7b7caee9..520bfc68 100644
--- a/examples/custom-server-express/server.js
+++ b/examples/custom-server-express/server.js
@@ -13,13 +13,11 @@ const handle = app.getRequestHandler();
 app.prepare().then(() => {
   const server = express();
 
-  server.use(
-    bodyParser.json({
-      verify: (req, _, buf) => {
-        req.rawBody = buf.toString();
-      },
-    })
-  );
+  const verify = (req, _, buf) => {
+    req.rawBody = buf.toString();
+  };
+  server.use(bodyParser.json({ verify }));
+  server.use(bodyParser.urlencoded({ extended: false, verify }));
 
   server.get('/api', (req, res) => {
     res.json({ ok: true });
diff --git a/examples/custom-server-koa/server.js b/examples/custom-server-koa/server.js
index b341a205..bff637ff 100644
--- a/examples/custom-server-koa/server.js
+++ b/examples/custom-server-koa/server.js
@@ -13,6 +13,14 @@ const handle = app.getRequestHandler();
 
 app.prepare().then(() => {
   const server = new Koa();
+
+  server.use(bodyParser());
+  server.use((ctx, next) => {
+    ctx.req.body = ctx.request.body;
+    ctx.req.rawBody = ctx.request.rawBody;
+    return next();
+  });
+
   const router = new Router();
 
   router.get('/api', ctx => {
@@ -24,12 +32,6 @@ app.prepare().then(() => {
     ctx.respond = false;
   });
 
-  server.use(bodyParser());
-  server.use((ctx, next) => {
-    ctx.req.body = ctx.request.body;
-    ctx.req.rawBody = ctx.request.rawBody;
-    return next();
-  });
   server.use(router.routes());
 
   server.listen(port, err => {
diff --git a/examples/messenger-persistent-menu/README.md b/examples/messenger-persistent-menu/README.md
index eda4af4e..798087a1 100644
--- a/examples/messenger-persistent-menu/README.md
+++ b/examples/messenger-persistent-menu/README.md
@@ -40,7 +40,7 @@ If you successfully start the server, you get a webhook URL in the format of `ht
 While the server is running, you can run following command with `bottender` to set up the webhook with the webhook url you get from running `npm run dev`:
 
 ```sh
-npx bottender messenger webhook set -w <YOUR_WEBHOOK_URL>
+npx bottender messenger webhook set
 ```
 
 > **Note:** You must fill `APP_ID`, `APP_SECRET` and `VERIFY_TOKEN` in your `.env` file before running this command.
diff --git a/examples/telegram-game/.env b/examples/telegram-game/.env
new file mode 100644
index 00000000..c8b6f8bb
--- /dev/null
+++ b/examples/telegram-game/.env
@@ -0,0 +1,3 @@
+TELEGRAM_ACCESS_TOKEN=
+GAME_NAME=
+ROOT_PATH=
diff --git a/examples/telegram-game/README.md b/examples/telegram-game/README.md
new file mode 100644
index 00000000..7adaa297
--- /dev/null
+++ b/examples/telegram-game/README.md
@@ -0,0 +1,63 @@
+# Telegram Game
+
+## 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/telegram-game
+cd telegram-game
+```
+
+Install dependencies:
+
+```sh
+npm install
+```
+
+You must fill `TELEGRAM_ACCESS_TOKEN`, `GAME_NAME` and `ROOT_PATH` in your `.env` file.
+
+If you are not familiar with Telegram Bot, you may refer to Bottender's doc, [Telegram Setup](https://bottender.js.org/docs/channel-telegram-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.
+
+To set up ngrok for your server, run:
+
+```sh
+npx ngrok http 5000
+```
+
+If you successfully start the server, you get a webhook URL in the format of `https://xxxxxxxx.ngrok.io/webhooks/telegram` from your terminal.
+
+## Set Webhook
+
+While the server running, you can run following command with `bottender` to set up the webhook with the webhook URL you get from running `npm run dev`:
+
+```sh
+npx bottender telegram webhook set
+```
+
+> **Note:** You must fill in your `.env` file before running this command.
+
+## Idea of This Example
+
+This example is a game bot running on [Telegram](https://telegram.org/).
+
+This example contains the following topics:
+
+- send the game to the user
+- set game score of the user
+- share the game to a different chat
+
+You must create a new Telegram game by following [Telegram Bot API#Games](https://core.telegram.org/bots/api#games).
+
+## Related Examples
+
+- [telegram-hello-world](../telegram-hello-world)
+- [telegram-poll](../telegram-poll)
diff --git a/examples/telegram-game/bottender.config.js b/examples/telegram-game/bottender.config.js
new file mode 100644
index 00000000..c269cd70
--- /dev/null
+++ b/examples/telegram-game/bottender.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  channels: {
+    telegram: {
+      enabled: true,
+      path: '/webhooks/telegram',
+      accessToken: process.env.TELEGRAM_ACCESS_TOKEN,
+    },
+  },
+};
diff --git a/examples/telegram-game/game.html b/examples/telegram-game/game.html
new file mode 100644
index 00000000..8cb0a977
--- /dev/null
+++ b/examples/telegram-game/game.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Demo Game</title>
+</head>
+<body>
+  your score is <input type="text" id="text" />
+  <button id="sendScore">
+    Send Score
+  </button>
+  <button onclick="TelegramGameProxy.shareScore()">Share score</button>
+  <script src="https://telegram-js.azureedge.net/js/games.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+  <script>
+    const text = document.getElementById("text");
+    const button = document.getElementById("sendScore");
+    button.addEventListener("click", () => {
+      const score = Number(text.value);
+      const key = location.search.split("=")[1];
+      axios.post('/setScore', {
+        score,
+        key
+      })
+      .then(function (response) {
+        alert(`Game over, your score is ${score}.`);
+      })
+      .catch(function (error) {
+        alert(error);
+      });
+    })
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/telegram-game/gameSession.js b/examples/telegram-game/gameSession.js
new file mode 100644
index 00000000..d1798a49
--- /dev/null
+++ b/examples/telegram-game/gameSession.js
@@ -0,0 +1,18 @@
+const session = {};
+
+function createSession(data) {
+  const key = Math.random()
+    .toString(36)
+    .substr(2, 10);
+  session[key] = data;
+  return key;
+}
+
+function getSession(key) {
+  return session[key];
+}
+
+module.exports = {
+  getSession,
+  createSession,
+};
diff --git a/examples/telegram-game/index.js b/examples/telegram-game/index.js
new file mode 100644
index 00000000..1b4ab744
--- /dev/null
+++ b/examples/telegram-game/index.js
@@ -0,0 +1,35 @@
+const { router, telegram, text, route } = require('bottender/router');
+
+const { createSession } = require('./gameSession');
+
+async function DefaultAction(context) {
+  await context.sendText('please type /game to create a new demo game.');
+}
+
+async function SendGame(context) {
+  await context.sendGame(process.env.GAME_NAME);
+}
+
+async function HandleCallbackQuery(context) {
+  const callbackQuery = context.event.callbackQuery;
+  const callbackQueryId = callbackQuery.id;
+  const userId = callbackQuery.from.id;
+  const messageId = callbackQuery.message.messageId;
+  const chatId = callbackQuery.message.chat.id;
+  const key = createSession({
+    userId,
+    messageId,
+    chatId,
+  });
+  await context.client.answerCallbackQuery(callbackQueryId, {
+    url: `${process.env.ROOT_PATH}/game?key=${key}`,
+  });
+}
+
+module.exports = async function App(context) {
+  return router([
+    text(/\/game/, SendGame),
+    telegram.callbackQuery(HandleCallbackQuery),
+    telegram.any(DefaultAction),
+  ]);
+};
diff --git a/examples/telegram-game/package.json b/examples/telegram-game/package.json
new file mode 100644
index 00000000..6beecde8
--- /dev/null
+++ b/examples/telegram-game/package.json
@@ -0,0 +1,16 @@
+{
+  "scripts": {
+    "dev": "nodemon server.js",
+    "start": "cross-env NODE_ENV=production node server.js"
+  },
+  "dependencies": {
+    "body-parser": "^1.19.0",
+    "bottender": "latest",
+    "cross-env": "^6.0.3",
+    "express": "^4.17.1",
+    "messaging-api-telegram": "^0.8.2"
+  },
+  "devDependencies": {
+    "nodemon": "^2.0.1"
+  }
+}
diff --git a/examples/telegram-game/server.js b/examples/telegram-game/server.js
new file mode 100644
index 00000000..374e0905
--- /dev/null
+++ b/examples/telegram-game/server.js
@@ -0,0 +1,55 @@
+const path = require('path');
+
+const bodyParser = require('body-parser');
+const express = require('express');
+const { bottender, getClient } = require('bottender');
+
+const { getSession } = require('./gameSession');
+
+const client = getClient('telegram');
+
+const app = bottender({
+  dev: process.env.NODE_ENV !== 'production',
+});
+
+const port = Number(process.env.PORT) || 5000;
+
+// the request handler of the bottender app
+const handle = app.getRequestHandler();
+
+app.prepare().then(() => {
+  const server = express();
+
+  server.use(
+    bodyParser.json({
+      verify: (req, _, buf) => {
+        req.rawBody = buf.toString();
+      },
+    })
+  );
+
+  server.get('/game', (req, res) => {
+    const filename = path.join(`${__dirname}/game.html`);
+    res.sendFile(filename);
+  });
+
+  server.post('/setScore', async (req, res) => {
+    const data = getSession(req.body.key);
+    const score = req.body.score;
+    await client.setGameScore(data.userId, score, {
+      force: true,
+      ...data,
+    });
+    res.send({ ok: true });
+  });
+
+  // route for webhook request
+  server.all('*', (req, res) => {
+    return handle(req, res);
+  });
+
+  server.listen(port, err => {
+    if (err) throw err;
+    console.log(`> Ready on http://localhost:${port}`);
+  });
+});
diff --git a/examples/telegram-hello-world/README.md b/examples/telegram-hello-world/README.md
index 9d4501da..81b67c0d 100644
--- a/examples/telegram-hello-world/README.md
+++ b/examples/telegram-hello-world/README.md
@@ -34,7 +34,7 @@ If you successfully start the server, you get a webhook URL in the format of `ht
 While the server is running, you can run the following command with `bottender` to set up the webhook with the webhook URL you got from running `npm run dev`:
 
 ```sh
-npx bottender telegram webhook set -w <YOUR_WEBHOOK_URL>
+npx bottender telegram webhook set
 ```
 
 > **Note:** You must fill in your `.env` file before running this command.
diff --git a/examples/telegram-inline-keyboard/.env b/examples/telegram-inline-keyboard/.env
new file mode 100644
index 00000000..cf07abc9
--- /dev/null
+++ b/examples/telegram-inline-keyboard/.env
@@ -0,0 +1,1 @@
+TELEGRAM_ACCESS_TOKEN=
diff --git a/examples/telegram-inline-keyboard/README.md b/examples/telegram-inline-keyboard/README.md
new file mode 100644
index 00000000..9dff04cf
--- /dev/null
+++ b/examples/telegram-inline-keyboard/README.md
@@ -0,0 +1,52 @@
+# Telegram Inline Keyboard
+
+## 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/telegram-inline-keyboard
+cd telegram-inline-keyboard
+```
+
+Install dependencies:
+
+```sh
+npm install
+```
+
+You must fill `TELEGRAM_ACCESS_TOKEN` in your `.env` file.
+
+If you are not familiar with Telegram Bot, you may refer to Bottender's doc, [Telegram Setup](https://bottender.js.org/docs/channel-telegram-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/telegram` from your terminal.
+
+## Set Webhook
+
+While the server is running, you can run the following command with `bottender` to set up the webhook with the webhook URL you got from running `npm run dev`:
+
+```sh
+npx bottender telegram webhook set
+```
+
+> **Note:** You must fill in your `.env` file before running this command.
+
+## Idea of This Example
+
+This example is a bot about inline keyboard running on [Telegram](https://telegram.org/).
+
+This example contains the following topics:
+
+- show the inline keyboard to the user
+- build a multiple layer inline keyboard
+
+For more information, check [Telegram Bots#Inline Keyboards](https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating)
+
diff --git a/examples/telegram-inline-keyboard/bottender.config.js b/examples/telegram-inline-keyboard/bottender.config.js
new file mode 100644
index 00000000..c269cd70
--- /dev/null
+++ b/examples/telegram-inline-keyboard/bottender.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  channels: {
+    telegram: {
+      enabled: true,
+      path: '/webhooks/telegram',
+      accessToken: process.env.TELEGRAM_ACCESS_TOKEN,
+    },
+  },
+};
diff --git a/examples/telegram-inline-keyboard/index.js b/examples/telegram-inline-keyboard/index.js
new file mode 100644
index 00000000..3000fca7
--- /dev/null
+++ b/examples/telegram-inline-keyboard/index.js
@@ -0,0 +1,94 @@
+const { router, telegram, text } = require('bottender/router');
+
+function generateInlineKeyboard(table) {
+  return {
+    inlineKeyboard: table.map(row => {
+      return row.map(cell => {
+        return {
+          text: cell,
+          callbackData: cell,
+        };
+      });
+    }),
+  };
+}
+
+const mainMenu = {
+  text: 'This is main menu, please click an option.',
+  replyMarkup: generateInlineKeyboard([
+    ['A', 'B'],
+    ['C', 'D'],
+  ]),
+};
+
+const submenuA = {
+  text: 'This is submenu A.',
+  replyMarkup: generateInlineKeyboard([
+    ['A1', 'A2'],
+    ['A3', 'A4'],
+    ['< back to main menu'],
+  ]),
+};
+
+const submenuB = {
+  text: 'This is submenu B.',
+  replyMarkup: generateInlineKeyboard([
+    ['B1', 'B2'],
+    ['B3', 'B4'],
+    ['< back to main menu'],
+  ]),
+};
+
+const submenuC = {
+  text: 'This is submenu C.',
+  replyMarkup: generateInlineKeyboard([
+    ['C1', 'C2'],
+    ['C3', 'C4'],
+    ['< back to main menu'],
+  ]),
+};
+
+const submenuD = {
+  text: 'This is submenu D.',
+  replyMarkup: generateInlineKeyboard([
+    ['D1', 'D2'],
+    ['D3', 'D4'],
+    ['< back to main menu'],
+  ]),
+};
+
+const menuMapping = {
+  '< back to main menu': mainMenu,
+  A: submenuA,
+  B: submenuB,
+  C: submenuC,
+  D: submenuD,
+};
+
+async function DefaultAction(context) {
+  await context.sendText('Please type "show keyboard" to show the keyboard.');
+}
+
+async function ShowKeyboard(context) {
+  await context.sendText(mainMenu.text, { replyMarkup: mainMenu.replyMarkup });
+}
+
+async function AnswerKeyboard(context) {
+  const callbackQuery = context.event.callbackQuery;
+  const messageId = callbackQuery.message.messageId;
+  const data = callbackQuery.data;
+  const menu = menuMapping[data];
+  if (menu) {
+    context.editMessageText(messageId, menu.text, { replyMarkup: menu.menu });
+  } else {
+    context.editMessageText(messageId, `Your final choice is ${data}.`);
+  }
+}
+
+module.exports = async function App(context) {
+  return router([
+    text('show keyboard', ShowKeyboard),
+    telegram.callbackQuery(AnswerKeyboard),
+    telegram.any(DefaultAction),
+  ]);
+};
diff --git a/examples/telegram-inline-keyboard/package.json b/examples/telegram-inline-keyboard/package.json
new file mode 100644
index 00000000..451d8d52
--- /dev/null
+++ b/examples/telegram-inline-keyboard/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "dev": "bottender dev",
+    "start": "bottender start"
+  },
+  "dependencies": {
+    "bottender": "latest"
+  }
+}
diff --git a/examples/telegram-inline-query/.env b/examples/telegram-inline-query/.env
new file mode 100644
index 00000000..cf07abc9
--- /dev/null
+++ b/examples/telegram-inline-query/.env
@@ -0,0 +1,1 @@
+TELEGRAM_ACCESS_TOKEN=
diff --git a/examples/telegram-inline-query/README.md b/examples/telegram-inline-query/README.md
new file mode 100644
index 00000000..d044c66d
--- /dev/null
+++ b/examples/telegram-inline-query/README.md
@@ -0,0 +1,52 @@
+# Telegram Inline Query
+
+## 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/telegram-inline-query
+cd telegram-inline-query
+```
+
+Install dependencies:
+
+```sh
+npm install
+```
+
+You must fill `TELEGRAM_ACCESS_TOKEN` in your `.env` file.
+
+If you are not familiar with Telegram Bot, you may refer to Bottender's doc, [Telegram Setup](https://bottender.js.org/docs/channel-telegram-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/telegram` from your terminal.
+
+## Set Webhook
+
+While the server is running, you can run the following command with `bottender` to set up the webhook with the webhook URL you got from running `npm run dev`:
+
+```sh
+npx bottender telegram webhook set
+```
+
+> **Note:** You must fill in your `.env` file before running this command.
+
+## Idea of This Example
+
+This example is a bot about inline query running on [Telegram](https://telegram.org/).
+
+This example contains the following topics:
+
+- receive the inline query from the user
+- answer the inline query with results
+- collect the result chosen by the user
+
+You must enable `inline` and `inlinefeedback` by following [Telegram Inline Bot](https://core.telegram.org/bots/inline) and [Telegram Inline Bot#Collecting Feedback](https://core.telegram.org/bots/inline#collecting-feedback)
\ No newline at end of file
diff --git a/examples/telegram-inline-query/bottender.config.js b/examples/telegram-inline-query/bottender.config.js
new file mode 100644
index 00000000..c269cd70
--- /dev/null
+++ b/examples/telegram-inline-query/bottender.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  channels: {
+    telegram: {
+      enabled: true,
+      path: '/webhooks/telegram',
+      accessToken: process.env.TELEGRAM_ACCESS_TOKEN,
+    },
+  },
+};
diff --git a/examples/telegram-inline-query/index.js b/examples/telegram-inline-query/index.js
new file mode 100644
index 00000000..6432a6e0
--- /dev/null
+++ b/examples/telegram-inline-query/index.js
@@ -0,0 +1,47 @@
+const { router, telegram } = require('bottender/router');
+
+const inlineQueryResultArticle = {
+  type: 'article',
+  id: 'article1',
+  title: 'article',
+  inputMessageContent: {
+    messageText: 'article content',
+  },
+};
+
+const inlineQueryResultPhoto = {
+  type: 'photo',
+  id: 'photo1',
+  photoUrl: 'https://picsum.photos/200',
+  thumbUrl: 'https://picsum.photos/200',
+};
+
+async function DefaultAction(context) {
+  await context.sendText('Please mention me to send inline query to me.');
+}
+
+async function AnswerInlineQuery(context) {
+  const { query } = context.event.inlineQuery;
+  await context.sendText(`You are trying to query ${query}.`);
+  await context.answerInlineQuery(
+    [inlineQueryResultArticle, inlineQueryResultPhoto],
+    {
+      cacheTime: 0,
+    }
+  );
+}
+
+async function CollectFeedback(context) {
+  const { query, resultId } = context.event.chosenInlineResult;
+  await context.sendText(
+    `Your query is ${query} and the result you picked is ${resultId}.`
+  );
+}
+
+module.exports = async function App(context) {
+  return router([
+    telegram.inlineQuery(AnswerInlineQuery),
+    telegram.chosenInlineResult(CollectFeedback),
+    telegram.any(DefaultAction),
+  ]);
+};
diff --git a/examples/telegram-inline-query/package.json b/examples/telegram-inline-query/package.json
new file mode 100644
index 00000000..451d8d52
--- /dev/null
+++ b/examples/telegram-inline-query/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "dev": "bottender dev",
+    "start": "bottender start"
+  },
+  "dependencies": {
+    "bottender": "latest"
+  }
+}
diff --git a/examples/telegram-poll/README.md b/examples/telegram-poll/README.md
index 90657164..de4f7ae3 100644
--- a/examples/telegram-poll/README.md
+++ b/examples/telegram-poll/README.md
@@ -34,7 +34,7 @@ If you successfully start the server, you get a webhook URL in the format of `ht
 While the server running, you can run following command with `bottender` to set up the webhook with the webhook URL you get from running `npm run dev`:
 
 ```sh
-npx bottender telegram webhook set -w <YOUR_WEBHOOK_URL>
+npx bottender telegram webhook set
 ```
 
 > **Note:** You must fill in your `.env` file before running this command.
diff --git a/examples/telegram-reply-keyboard/.env b/examples/telegram-reply-keyboard/.env
new file mode 100644
index 00000000..cf07abc9
--- /dev/null
+++ b/examples/telegram-reply-keyboard/.env
@@ -0,0 +1,1 @@
+TELEGRAM_ACCESS_TOKEN=
diff --git a/examples/telegram-reply-keyboard/README.md b/examples/telegram-reply-keyboard/README.md
new file mode 100644
index 00000000..5fda3020
--- /dev/null
+++ b/examples/telegram-reply-keyboard/README.md
@@ -0,0 +1,52 @@
+# Telegram Reply Keyboard
+
+## 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/telegram-reply-keyboard
+cd telegram-reply-keyboard
+```
+
+Install dependencies:
+
+```sh
+npm install
+```
+
+You must fill `TELEGRAM_ACCESS_TOKEN` in your `.env` file.
+
+If you are not familiar with Telegram Bot, you may refer to Bottender's doc, [Telegram Setup](https://bottender.js.org/docs/channel-telegram-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/telegram` from your terminal.
+
+## Set Webhook
+
+While the server is running, you can run the following command with `bottender` to set up the webhook with the webhook URL you got from running `npm run dev`:
+
+```sh
+npx bottender telegram webhook set
+```
+
+> **Note:** You must fill in your `.env` file before running this command.
+
+## Idea of This Example
+
+This example is a bot about reply keyboard running on [Telegram](https://telegram.org/).
+
+This example contains the following topics:
+
+- show the custom keyboard to the user
+- remove the custom keyboard
+
+For more information, check [Telegram Bots#Keyboards](https://core.telegram.org/bots#keyboards)
+
diff --git a/examples/telegram-reply-keyboard/bottender.config.js b/examples/telegram-reply-keyboard/bottender.config.js
new file mode 100644
index 00000000..c269cd70
--- /dev/null
+++ b/examples/telegram-reply-keyboard/bottender.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  channels: {
+    telegram: {
+      enabled: true,
+      path: '/webhooks/telegram',
+      accessToken: process.env.TELEGRAM_ACCESS_TOKEN,
+    },
+  },
+};
diff --git a/examples/telegram-reply-keyboard/index.js b/examples/telegram-reply-keyboard/index.js
new file mode 100644
index 00000000..44c63807
--- /dev/null
+++ b/examples/telegram-reply-keyboard/index.js
@@ -0,0 +1,47 @@
+const { router, telegram, text } = require('bottender/router');
+
+async function DefaultAction(context) {
+  await context.sendText('Please type "show keyboard" to show the keyboard.');
+}
+
+async function ShowKeyboard(context) {
+  const replyMarkup = {
+    keyboard: [
+      [
+        {
+          text: '🍔',
+        },
+        {
+          text: '🍕',
+        },
+      ],
+      [
+        {
+          text: '🌮',
+        },
+        {
+          text: '🍱',
+        },
+      ],
+    ],
+    oneTimeKeyboard: true,
+  };
+  await context.sendText('Which one is your favorite food?', { replyMarkup });
+}
+
+async function AnswerKeyboard(context) {
+  const replyMarkup = {
+    removeKeyboard: true,
+  };
+  await context.sendText(`Your favorite food is ${context.event.text}.`, {
+    replyMarkup,
+  });
+}
+
+module.exports = async function App(context) {
+  return router([
+    text('show keyboard', ShowKeyboard),
+    text(/[🍔🍕🌮🍱]/u, AnswerKeyboard),
+    telegram.any(DefaultAction),
+  ]);
+};
diff --git a/examples/telegram-reply-keyboard/package.json b/examples/telegram-reply-keyboard/package.json
new file mode 100644
index 00000000..451d8d52
--- /dev/null
+++ b/examples/telegram-reply-keyboard/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "dev": "bottender dev",
+    "start": "bottender start"
+  },
+  "dependencies": {
+    "bottender": "latest"
+  }
+}
diff --git a/examples/viber-hello-world/README.md b/examples/viber-hello-world/README.md
index 2e97a65c..841ef68c 100644
--- a/examples/viber-hello-world/README.md
+++ b/examples/viber-hello-world/README.md
@@ -32,7 +32,7 @@ If you successfully start the server, you get a webhook URL in the format of `ht
 While the server is running, you can run the following command with `bottender` to set up the webhook with the webhook URL you got from running `npm run dev`:
 
 ```sh
-npx bottender viber webhook set -w <YOUR_WEBHOOK_URL>
+npx bottender viber webhook set
 ```
 
 > **Note:** You must fill in your `.env` file before running this command.
diff --git a/lerna.json b/lerna.json
index 87ef4c5f..5ee9bb80 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,7 +2,7 @@
   "lerna": "3.4.0",
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "1.4.3",
+  "version": "1.4.4",
   "command": {
     "publish": {
       "ignoreChanges": [
diff --git a/packages/bottender-dialogflow/package.json b/packages/bottender-dialogflow/package.json
index d1ee4ef8..55b7c75c 100644
--- a/packages/bottender-dialogflow/package.json
+++ b/packages/bottender-dialogflow/package.json
@@ -7,7 +7,7 @@
     "type": "git",
     "url": "https://github.com/Yoctol/bottender.git"
   },
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "dist/index.js",
   "files": [
     "dist"
@@ -22,7 +22,7 @@
     "bottender": ">= 1.2.0-0"
   },
   "devDependencies": {
-    "bottender": "^1.4.3"
+    "bottender": "^1.4.4"
   },
   "keywords": [
     "bot",
diff --git a/packages/bottender-luis/package.json b/packages/bottender-luis/package.json
index 248ca242..197b6725 100644
--- a/packages/bottender-luis/package.json
+++ b/packages/bottender-luis/package.json
@@ -7,7 +7,7 @@
     "type": "git",
     "url": "https://github.com/Yoctol/bottender.git"
   },
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "dist/index.js",
   "files": [
     "dist"
@@ -21,7 +21,7 @@
     "bottender": ">= 1.2.0-0"
   },
   "devDependencies": {
-    "bottender": "^1.4.3"
+    "bottender": "^1.4.4"
   },
   "keywords": [
     "bot",
diff --git a/packages/bottender-qna-maker/package.json b/packages/bottender-qna-maker/package.json
index 2fb0f667..f4b5952b 100644
--- a/packages/bottender-qna-maker/package.json
+++ b/packages/bottender-qna-maker/package.json
@@ -7,7 +7,7 @@
     "type": "git",
     "url": "https://github.com/Yoctol/bottender.git"
   },
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "dist/index.js",
   "files": [
     "dist"
@@ -21,7 +21,7 @@
     "bottender": ">= 1.2.0-0"
   },
   "devDependencies": {
-    "bottender": "^1.4.3"
+    "bottender": "^1.4.4"
   },
   "keywords": [
     "bot",
diff --git a/packages/bottender-rasa/package.json b/packages/bottender-rasa/package.json
index b1f59f4e..4a6ef7ea 100644
--- a/packages/bottender-rasa/package.json
+++ b/packages/bottender-rasa/package.json
@@ -7,7 +7,7 @@
     "type": "git",
     "url": "https://github.com/Yoctol/bottender.git"
   },
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "dist/index.js",
   "files": [
     "dist"
@@ -22,7 +22,7 @@
     "bottender": ">= 1.2.0-0"
   },
   "devDependencies": {
-    "bottender": "^1.4.3"
+    "bottender": "^1.4.4"
   },
   "keywords": [
     "bot",
diff --git a/packages/bottender/README.md b/packages/bottender/README.md
index 825eeed5..81283d11 100644
--- a/packages/bottender/README.md
+++ b/packages/bottender/README.md
@@ -57,16 +57,20 @@ Here is the first one to get you started:
 
 ```js
 // index.js
-module.export = async function App(context) {
-  await context.sendText('Hello World');
-};
-```
+const { router, text } = require('bottender/router');
 
-```sh
-npx bottender start --console
-```
+async function SayHi(context) {
+  await context.sendText('Hi!');
+}
 
-It creates and runs a bot that always replies with "Hello World" in the console.
+async function Unknown(context) {
+  await context.sendText('Sorry, I don’t know what you say.');
+}
+
+module.export = function App(context) {
+  return router([text('hi', SayHi), text('*', Unknown)]);
+};
+```
 
 ## Notable Features
 
diff --git a/packages/bottender/package.json b/packages/bottender/package.json
index d5daa74b..97e11c85 100644
--- a/packages/bottender/package.json
+++ b/packages/bottender/package.json
@@ -7,7 +7,7 @@
     "type": "git",
     "url": "https://github.com/Yoctol/bottender.git"
   },
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "dist/index.js",
   "browser": "dist/browser.js",
   "bin": {
diff --git a/packages/bottender/src/slack/SlackConnector.ts b/packages/bottender/src/slack/SlackConnector.ts
index 66b8421b..8f7aa146 100644
--- a/packages/bottender/src/slack/SlackConnector.ts
+++ b/packages/bottender/src/slack/SlackConnector.ts
@@ -5,6 +5,7 @@ import invariant from 'invariant';
 import pProps from 'p-props';
 import warning from 'warning';
 import { SlackOAuthClient } from 'messaging-api-slack';
+import { camelcaseKeysDeep } from 'messaging-api-common';
 
 import Session from '../session/Session';
 import { Connector } from '../bot/Connector';
@@ -122,7 +123,8 @@ export default class SlackConnector
     }
 
     if (body.payload && typeof body.payload === 'string') {
-      const payload = JSON.parse(body.payload);
+      const payload = camelcaseKeysDeep(JSON.parse(body.payload));
+
       if (payload.type === 'interactive_message') {
         return payload as InteractiveMessageEvent;
       }
diff --git a/packages/bottender/src/slack/SlackEvent.ts b/packages/bottender/src/slack/SlackEvent.ts
index 2157fdf0..23ff0767 100644
--- a/packages/bottender/src/slack/SlackEvent.ts
+++ b/packages/bottender/src/slack/SlackEvent.ts
@@ -3,6 +3,7 @@ import { pascalCase } from 'pascal-case';
 import { Event } from '../context/Event';
 
 export type EventTypes =
+  | '*'
   | 'message'
   | 'app_uninstalled'
   | 'channel_archive'
diff --git a/packages/bottender/src/slack/routes.ts b/packages/bottender/src/slack/routes.ts
index 85427f10..c35ff2aa 100644
--- a/packages/bottender/src/slack/routes.ts
+++ b/packages/bottender/src/slack/routes.ts
@@ -53,7 +53,9 @@ function event<C extends AnyContext>(
 ) {
   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
   );
 }
@@ -68,7 +70,7 @@ function command<C extends AnyContext>(
     (context: C) =>
       context.platform === 'slack' &&
       context.event.command &&
-      context.event.command === commandText,
+      (commandText === '*' || context.event.command === commandText),
     action
   );
 }
diff --git a/website/i18n/en.json b/website/i18n/en.json
index 42c53179..92ef3a12 100644
--- a/website/i18n/en.json
+++ b/website/i18n/en.json
@@ -692,12 +692,27 @@
       "version-1.3.1/version-1.3.1-the-basics-session": {
         "title": "Session"
       },
+      "version-1.4/version-1.4-advanced-guides-custom-server": {
+        "title": "Running Bottender with Custom Servers"
+      },
+      "version-1.4/version-1.4-advanced-guides-deployment": {
+        "title": "Deployment"
+      },
+      "version-1.4/version-1.4-advanced-guides-multi-channel": {
+        "title": "Multi-Channel Support"
+      },
       "version-1.4/version-1.4-advanced-guides-nlu": {
         "title": "Natural Language Understanding"
       },
+      "version-1.4/version-1.4-api-console-context": {
+        "title": "ConsoleContext"
+      },
       "version-1.4/version-1.4-api-context": {
         "title": "Context"
       },
+      "version-1.4/version-1.4-api-event": {
+        "title": "Event"
+      },
       "version-1.4/version-1.4-api-line-client": {
         "title": "LineClient"
       },
@@ -716,6 +731,9 @@
       "version-1.4/version-1.4-channel-line-errors": {
         "title": "Error Handling in LINE"
       },
+      "version-1.4/version-1.4-channel-line-liff": {
+        "title": "LINE Front-end Framework (LIFF)"
+      },
       "version-1.4/version-1.4-channel-line-notify": {
         "title": "LINE Notify"
       },
@@ -725,6 +743,12 @@
       "version-1.4/version-1.4-channel-line-sending-messages": {
         "title": "Sending LINE Messages"
       },
+      "version-1.4/version-1.4-channel-messenger-handover-protocol": {
+        "title": "Messenger Handover Protocol"
+      },
+      "version-1.4/version-1.4-channel-messenger-multi-page": {
+        "title": "Messenger Multi-Page Support"
+      },
       "version-1.4/version-1.4-channel-messenger-routing": {
         "title": "Messenger Routing"
       },
@@ -734,6 +758,9 @@
       "version-1.4/version-1.4-channel-slack-routing": {
         "title": "Slack Routing"
       },
+      "version-1.4/version-1.4-channel-slack-sending-messages": {
+        "title": "Sending Slack Messages"
+      },
       "version-1.4/version-1.4-channel-telegram-routing": {
         "title": "Telegram Routings"
       },
diff --git a/website/versioned_docs/version-1.4/advanced-guides-custom-server.md b/website/versioned_docs/version-1.4/advanced-guides-custom-server.md
new file mode 100644
index 00000000..cf5f560a
--- /dev/null
+++ b/website/versioned_docs/version-1.4/advanced-guides-custom-server.md
@@ -0,0 +1,237 @@
+---
+id: version-1.4-advanced-guides-custom-server
+title: Running Bottender with Custom Servers
+original_id: advanced-guides-custom-server
+---
+
+## The Concept
+
+`bottender dev` and `bottender start` are the default npm scripts generated by [create-bottender-app](getting-started.md#create-a-new-bottender-app).
+
+Thus, You can find those lines in your generated `package.json`:
+
+```
+  "scripts": {
+    "dev": "bottender dev",
+    "start": "bottender start",
+```
+
+When executing one of `bottender dev` or `bottender start` command, under the hood, it sets up a default express server for Bottender and load `index.js` from the project root directory as the root action of the chatbot.
+
+To run Bottender with the custom HTTP server, you need to prepare an HTTP server, delegate the webhook routes to Bottender, and modify the setting of `package.json`.
+
+## Express
+
+### Creating a New Project with Custom Express Server
+
+If you want to have a clean project with the custom express, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-express) to develop your project. There are four steps you could follow to create your project:
+
+1. Download the code from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-express).
+2. Run `yarn` or `npm install` command to install all dependencies.
+3. Fill the `.env` file with correct value.
+4. Run `yarn dev` or `npm run dev` to start the dev server.
+
+If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom express server by following the migration instructions below.
+
+### Migrating an Existing Project to Custom Express Server
+
+Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using express server. In this case, you need to write a custom express server and delegate all chatbot's webhook requests to the Bottender app.
+
+To achieve that, you could follow the instructions, as shown below:
+
+1. Run `yarn add express body-parser nodemon` or `npm install express body-parser nodemon` command to install dependencies we need.
+2. Create a `server.js` file in the root directory of the project and copy the following code into it.
+
+```js
+const bodyParser = require('body-parser');
+const express = require('express');
+const { bottender } = require('bottender');
+
+const app = bottender({
+  dev: process.env.NODE_ENV !== 'production',
+});
+
+const port = Number(process.env.PORT) || 5000;
+
+// the request handler of the bottender app
+const handle = app.getRequestHandler();
+
+app.prepare().then(() => {
+  const server = express();
+
+  const verify = (req, _, buf) => {
+    req.rawBody = buf.toString();
+  };
+  server.use(bodyParser.json({ verify }));
+  server.use(bodyParser.urlencoded({ extended: false, verify }));
+
+  // your custom route
+  server.get('/api', (req, res) => {
+    res.json({ ok: true });
+  });
+
+  // route for webhook request
+  server.all('*', (req, res) => {
+    return handle(req, res);
+  });
+
+  server.listen(port, err => {
+    if (err) throw err;
+    console.log(`> Ready on http://localhost:${port}`);
+  });
+});
+```
+
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
+
+```
+  "scripts": {
+    "dev": "nodemon server.js",
+    "start": "node server.js",
+```
+
+That's all you need to do, and you can have you bot hosting on the custom express server!
+
+## Koa
+
+### Creating a New Project with Custom Koa Server
+
+If you want to have a clean project with the custom koa, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-koa) to develop your project. There are four steps you could follow to create your project:
+
+1. Download the code from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-koa).
+2. Run `yarn install` command to install dependencies.
+3. Fill the `.env` file with correct value.
+4. Run `yarn dev` to start server.
+
+If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom koa server by following the migration instructions below.
+
+### Migrating an Existing Project to Custom Koa Server
+
+Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using koa server. In this case, you need to write a custom koa server and delegate all chatbot's webhook requests to the Bottender app.
+
+To achieve that, you could follow the instructions, as shown below:
+
+1. Run `yarn add koa koa-router koa-bodyparser nodemon` or `npm install koa koa-router koa-bodyparser nodemon` command to install dependencies we need.
+2. Create a `server.js` file in the root directory of the project and copy the following code into it.
+
+```js
+const Koa = require('koa');
+const Router = require('koa-router');
+const bodyParser = require('koa-bodyparser');
+const { bottender } = require('bottender');
+
+const app = bottender({
+  dev: process.env.NODE_ENV !== 'production',
+});
+
+const port = Number(process.env.PORT) || 5000;
+
+const handle = app.getRequestHandler();
+
+app.prepare().then(() => {
+  const server = new Koa();
+
+  server.use(bodyParser());
+  server.use((ctx, next) => {
+    ctx.req.body = ctx.request.body;
+    ctx.req.rawBody = ctx.request.rawBody;
+    return next();
+  });
+
+  const router = new Router();
+
+  router.get('/api', ctx => {
+    ctx.response.body = { ok: true };
+  });
+
+  router.all('*', async ctx => {
+    await handle(ctx.req, ctx.res);
+    ctx.respond = false;
+  });
+
+  server.use(router.routes());
+
+  server.listen(port, err => {
+    if (err) throw err;
+    console.log(`> Ready on http://localhost:${port}`);
+  });
+});
+```
+
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
+
+```
+  "scripts": {
+    "dev": "nodemon server.js",
+    "start": "node server.js",
+```
+
+That's all you need to do, and you can have you bot hosting on the custom koa server!
+
+## Restify
+
+### Creating a New Project with Custom Restify Server
+
+If you want to have a clean project with the custom restify, you could start from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-restify) to develop your project. There are four steps you could follow to create your project:
+
+1. Download the code from [this example](https://github.com/Yoctol/bottender/tree/master/examples/custom-server-restify).
+2. Run `yarn install` command to install dependencies.
+3. Fill the `.env` file with correct value.
+4. Run `yarn dev` to start server.
+
+If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it to the custom restify server by following the migration instructions below.
+
+### Migrating an Existing Project to Custom Restify Server
+
+Suppose that you already have a project built from [create-bottender-app](getting-started.md#create-a-new-bottender-app), and you want to develop some additional APIs using restify server. In this case, you need to write a custom restify server and delegate all chatbot's webhook requests to the Bottender app.
+
+To achieve that, you could follow the instructions, as shown below:
+
+1. Run `yarn add restify nodemon` or `npm install restify nodemon` command to install dependencies we need.
+2. Create a `server.js` file in the root directory of the project and copy the following code into it.
+
+```js
+const restify = require('restify');
+const { bottender } = require('bottender');
+
+const app = bottender({
+  dev: process.env.NODE_ENV !== 'production',
+});
+
+const port = Number(process.env.PORT) || 5000;
+
+const handle = app.getRequestHandler();
+
+app.prepare().then(() => {
+  const server = restify.createServer();
+
+  server.use(restify.plugins.queryParser());
+  server.use(restify.plugins.bodyParser());
+
+  server.get('/api', (req, res) => {
+    res.send({ ok: true });
+  });
+
+  server.get('*', (req, res) => {
+    return handle(req, res);
+  });
+  server.post('*', (req, res) => {
+    return handle(req, res);
+  });
+
+  server.listen(port, err => {
+    if (err) throw err;
+    console.log(`> Ready on http://localhost:${port}`);
+  });
+});
+```
+
+3. Modify `scripts` in the `package.json` to `nodemon server.js` and `node server.js` instead:
+
+```
+  "scripts": {
+    "dev": "nodemon server.js",
+    "start": "node server.js",
+```
+
+That's all you need to do, and you can have you bot hosting on the custom restify server!
diff --git a/website/versioned_docs/version-1.4/advanced-guides-deployment.md b/website/versioned_docs/version-1.4/advanced-guides-deployment.md
new file mode 100644
index 00000000..0fbed2cf
--- /dev/null
+++ b/website/versioned_docs/version-1.4/advanced-guides-deployment.md
@@ -0,0 +1,342 @@
+---
+id: version-1.4-advanced-guides-deployment
+title: Deployment
+original_id: advanced-guides-deployment
+---
+
+Once you finished your bot in development, the next step is to deploy your bot to a hosting service.
+
+### Before Going Further
+
+We assumed that you already built at least one basic Bottender app in development. If you haven't, you may check [Getting Started](getting-started.md) to create your first Bottender app in a few minutes, then jump to the setup doc of your favorite chat channel:
+
+- [Messenger Setup](channel-messenger-setup.md)
+- [WhatsApp Setup](channel-whatsapp-setup.md)
+- [LINE Setup](channel-line-setup.md)
+- [Slack Setup](channel-slack-setup.md)
+- [Telegram Setup](channel-telegram-setup.md)
+- [Viber Setup](channel-viber-setup.md)
+
+## Heroku
+
+<p><img width="300" src="https://user-images.githubusercontent.com/662387/72130857-a8a98300-33b5-11ea-9ec6-10c8aac37230.jpg"></p>
+
+Heroku is one of the most popular hosting services. Not only the clear document, ease of scalability, using Git for deployment, but also the friendly [free pricing plan](https://www.heroku.com/pricing) for experiment purpose.
+
+In the following, you can see the necessary steps of Heroku Deployment:
+
+### Step 1: Create a Heroku Account and Download Heroku CLI
+
+First, [sign up](https://www.heroku.com/) a Heroku account if you haven't, then download and install [Heroku CLI](https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up).
+
+> **Note:** For the full command list, please refer to Heroku's doc, [Heroku CLI Commands](https://devcenter.heroku.com/articles/heroku-cli).
+
+### Step 2: Heroku Login and Create a Heroku App
+
+Before going further, make sure you have login your Heroku account by:
+
+```sh
+heroku login
+```
+
+Then, you can create a Heroku app by the below command.
+
+```sh
+heroku create <your-heroku-app-name>
+```
+
+> **Note:** You may see some app name regulation if you don't meet it. For example: `Name must start with a letter, end with a letter or digit and can only contain lowercase letters, digits, and dashes`
+
+Once you created your Heroku app successfully, you could see a deployment address for your app like `https://<your-heroku-app-name>.herokuapp.com/`. You can note it down for the coming webhook setting.
+
+### Step 3: Fill in Environment Variables to Heroku
+
+Config the environment variables of your Heroku app with the following commands: `heroku config:set -a <your-heroku-app-name> <ENV_KEY_01>=<ENV_VALUE_01>`.
+
+For chat channels require multiple environment variables, you may use commands like `heroku config:set -a <your-heroku-app-name> <ENV_KEY_01>=<ENV_VALUE_01> <ENV_KEY_02>=<ENV_VALUE_02>`.
+
+For example:
+
+```sh
+heroku config:set -a <your-heroku-app-name> MESSENGER_PAGE_ID=xxxxxx MESSENGER_ACCESS_TOKEN=xxxxxx MESSENGER_APP_ID=xxxxxx MESSENGER_APP_SECRET=xxxxxx MESSENGER_VERIFY_TOKEN=xxxxxx
+```
+
+### Step 4: Using Git in Your Bottender App
+
+Deployment of Heroku depends on Git. Make sure you have run `git init` and make the first commit in your Bottender app.
+
+For example:
+
+```sh
+git init
+git add .
+git commit -am "first commit"
+```
+
+### Step 5: Deploy Your Bot to Heroku and Set Up Webhook
+
+When you try to set up the webhook, some chat channels (e.g., Messenger) might ask for an immediate bot server verification. So, we recommend you to set up the webhook after your Bottender app server running.
+
+There are two basic types of webhook setup:
+
+1. Set up webhook by Developer Console UI, e.g., Messenger, LINE, Slack
+2. Set up webhook by CLI, e.g., Messenger, Telegram, Viber
+
+#### Step 5a: Set Up Webhook by UI of Developer Console
+
+Use Heroku CLI by Git push to complete the deployment.
+
+```sh
+heroku git:remote -a <your-heroku-app-name>
+git push heroku master
+```
+
+Then fill in your webhook URL on the developer console of the chat channel.
+
+> **Note:** If you are not familiar with webhook setup, you may refer to Bottender docs, [Messenger Setup](channel-messenger-setup.md), [LINE Setup](channel-line-setup.md), and [Slack Setup](channel-slack-setup.md).
+
+> **Note:** If you haven't changed your webhook path in `bottender.config.js`, by default, your Messenger Bot webhook is `https://<your-heroku-app-name>.herokuapp.com/webhooks/messenger`; your LINE Bot webhook is `https://<your-heroku-app-name>.herokuapp.com/webhooks/line`, etc.
+
+#### Step 5b: Set Up Webhook by CLI
+
+You can benefit from the `Procfile` feature of Heroku, which specifies the commands executed by the app on startup. We are going to use two process types of `Procfile`:
+
+- `web` process type: tell Heroku to run your bot server for every `dyno`
+- `release` process type: set up webhook before a new release is deployed to production
+
+> **Note:** For more information about `Procfile`, see [The Procfile](https://devcenter.heroku.com/articles/procfile).
+
+Using a Messenger Bot as an example, the `Procfile` looks like the below with default webhook path settings:
+
+```js
+// Procfile
+
+web: npm start
+release: echo "Y" | npx bottender messenger webhook set -w https://<your-heroku-app-name>.com/webhooks/messenger
+```
+
+> **Note:**
+>
+> - The `echo "Y"` aims to answer the first interactive CLI prompt
+> - If you haven't changed your webhook path in `bottender.config.js`, by default, your Messenger Bot webhook is `https://<your-heroku-app-name>.herokuapp.com/webhooks/messenger`; your LINE Bot webhook is `https://<your-heroku-app-name>.herokuapp.com/webhooks/line`, etc.
+
+Finally, You can use Heroku CLI by Git push to complete the deployment and let Heroku runs the `Procfile` to help you finish the webhook setup.
+
+```sh
+heroku git:remote -a <your-heroku-app-name>
+git push heroku master
+```
+
+### Step 6: Completed!
+
+Congratulations! You have made your Bottender bot production-ready. Share your fantastic bot with your friends!
+
+> **Note:** If you would like to share your Bottender Bot with the community, please feel free to add your project to [Bottender Users](https://bottender.js.org/users).
+
+## ZEIT Now 2.0
+
+<p><img width="1000" src="https://user-images.githubusercontent.com/662387/72130872-acd5a080-33b5-11ea-8c6c-ae06dd250be6.png"></p>
+
+ZEIT Now 2.0 is a super developer-friendly hosting service. ZEIT Now is famous for its one-word magic command `now` in the deployment of static web hosting.
+
+However, it needs a bit configuration to deploy a Bottender app and make it works serverless. In the following sections, you can see the necessary steps of ZEIT Now 2.0 deployment:
+
+### Step 1: Create a ZEIT Account
+
+First, [create](https://ZEIT.co/signup) a ZEIT Account if you haven't.
+
+### Step 2: Install ZEIT Now CLI
+
+We love to deploy with CLI! Install ZEIT Now CLI with npm by:
+
+```sh
+npm install -g now
+```
+
+### Step 3: Create a `server.js`
+
+ZEIT Now 2.0 doesn't support `npm` scripts, so we need a `server.js` as the entry point.
+
+```js
+// server.js
+
+const bodyParser = require('body-parser');
+const express = require('express');
+const { bottender } = require('bottender');
+
+const app = bottender({
+  dev: process.env.NODE_ENV !== 'production',
+});
+
+const port = Number(process.env.PORT) || 5000;
+
+const handle = app.getRequestHandler();
+
+app.prepare().then(() => {
+  const server = express();
+
+  server.use(
+    bodyParser.json({
+      verify: (req, _, buf) => {
+        req.rawBody = buf.toString();
+      },
+    })
+  );
+
+  server.all('*', (req, res) => {
+    return handle(req, res);
+  });
+
+  server.listen(port, err => {
+    if (err) throw err;
+    console.log(`> Ready on http://localhost:${port}`);
+  });
+});
+```
+
+### Step 4: Update Your `now.json`
+
+In the following configuration, you will make a few settings:
+
+- Set ZEIT Now to version 2.0
+- Use `@now/node` to bundle `server.js`
+- Put `bottender.config.js` and `index.js` into `includeFiles`. ZEIT Now uses `ncc` and `webpack bundler` under the hood, so you need to tell them to put those two files into the bundle)
+- Route all (/.\*) to `server.js`
+- Add your chat channel specific environment variables from `.env` to env. The number of environment variables various from the chat channel you use.
+- (optional) If you are debugging your app, you may set `DEBUG` env to `bottender*,messaging-api*`
+
+Using a Messenger Bot as an example. your `now.json` is like:
+
+```js
+// now.json
+
+{
+  "version": 2,
+  "builds": [
+    {
+      "src": "server.js",
+      "use": "@now/node",
+      "config": {
+        "includeFiles": [
+          "bottender.config.js",
+          "index.js"
+        ],
+        "bundle": true
+      }
+    }
+  ],
+  "routes": [
+    {
+      "src": "/.*",
+      "dest": "/server.js"
+    }
+  ],
+  "env": {
+    "MESSENGER_PAGE_ID": "xxxxxx",
+    "MESSENGER_ACCESS_TOKEN": "xxxxxx",
+    "MESSENGER_APP_ID": "xxxxxx",
+    "MESSENGER_APP_SECRET": "xxxxxx",
+    "MESSENGER_VERIFY_TOKEN": "xxxxxx",
+    "DEBUG": "bottender*,messaging-api*"
+  }
+}
+```
+
+### Step 5: Update Your `bottender.config.js`
+
+ZEIT Now 2.0 is a serverless hosting service, and serverless functions terminated right after HTTP response. So you have to make Bottender work synchronously, i.e., respond to chat channel right after received an event.
+
+to make the Bottender app executes in the synchronous mode, you must configure chat channels with `sync: true` in `bottender.config.js`.
+
+```js
+// bottender.config.js
+
+module.exports = {
+  // ... skip
+
+  channels: {
+    messenger: {
+      enabled: true,
+      sync: true,
+      // ...skip
+    },
+    line: {
+      enabled: true,
+      sync: true,
+      // ...skip
+    },
+    telegram: {
+      enabled: false,
+      sync: true,
+      // ...skip
+    },
+    slack: {
+      enabled: false,
+      sync: true,
+      // ...skip
+    },
+    viber: {
+      enabled: false,
+      sync: true,
+      // ...skip
+    },
+  },
+};
+```
+
+### Step 6: Update Your `package.json`
+
+Set up Node.js version to `10.x` or `12.x` in the `engines` env to avoid warnings for deprecated node 8.
+
+```js
+// package.json
+{
+ // ...skip
+ "engines": { "node": "12.x" }
+}
+```
+
+### Step 7: Deploy with `now`
+
+After all the settings, you are ready to use the magic word, `now`, for ZEIT Now 2.0 deployment.
+
+```sh
+now
+```
+
+Then you see something like the screenshot below.
+
+![](https://user-images.githubusercontent.com/662387/72136431-c382f400-33c3-11ea-9745-839c212c1b2e.png)
+
+> **Note:** Your deployed URL would be like `https://<your-app-name>.<your-user-name>.now.sh`
+
+### Step 8: Webhook Setup
+
+If you haven't changed your webhook path in `bottender.config.js`, by default, your Messenger Bot webhook is `https://<your-app-name>.<your-user-name>.now.sh/webhooks/messenger`; your LINE Bot webhook is `https://<your-app-name>.<your-user-name>.now.sh/webhooks/line`, etc.
+
+There are two basic types of webhook setup:
+
+1. Set up webhook by Developer Console UI, e.g., Messenger, LINE, Slack
+2. Set up webhook by CLI, e.g., Messenger, Telegram, Viber
+
+#### Step 8a: Set Up Webhook by Developer Console UI
+
+Fill in your webhook URL on the developer console of the chat channel.
+
+> **Note:** If you are not familiar with webhook setup, you may refer to Bottender docs, [Messenger Setup](channel-messenger-setup.md), [LINE Setup](channel-line-setup.md), [Slack Setup](channel-slack-setup.md).
+
+#### Step 8b: Set Up Webhook by CLI
+
+Make sure you have the same environment variable settings in your local `.env` file and `now.js`. Then you can use the following command to set up the webhook.
+
+Using a Messenger Bot as an example, your command is like:
+
+```sh
+npx bottender messenger webhook set -w https://<your-app-name>.<your-user-name>.now.sh/webhooks/messenger
+```
+
+### Step 9: Completed!
+
+Congratulations! You have made your Bottender bot production-ready. Share your fantastic bot with your friends!
+
+> **Note:** If you would like to share your Bottender Bot with the community, please feel free to add your project to [Bottender Users](https://bottender.js.org/users).
diff --git a/website/versioned_docs/version-1.4/advanced-guides-multi-channel.md b/website/versioned_docs/version-1.4/advanced-guides-multi-channel.md
new file mode 100644
index 00000000..a5b6afe0
--- /dev/null
+++ b/website/versioned_docs/version-1.4/advanced-guides-multi-channel.md
@@ -0,0 +1,193 @@
+---
+id: version-1.4-advanced-guides-multi-channel
+title: Multi-Channel Support
+original_id: advanced-guides-multi-channel
+---
+
+Bottender intends to meet enterprise project needs. So, Bottender supports multiple chat channels in the very early stage. For example, you can put the environment variables of each chat channel in one config file; you can use [platform specific routes](the-basics-routing.md#platform-specific-routes) to organize user events from various platforms.
+
+Plus, Bottender aims to support the full features of each chat channel, e.g., Block Kit of Slack, rich menu of LINE, Handover Protocol of Messenger. And you don't have to learn extra generic chat UIs and worry about if these still apply to the latest chat UI of each chat channel.
+
+The primary three steps to build a basic cross-platform bot are as follows:
+
+1. Set up each chat channel
+2. Enable each chat channel
+3. Develop cross-platform bot actions
+
+## Set Up Each Chat Channel
+
+To build a cross-platform bot, you have to set up each platform first. In short, you have to fill in environment variables in your `.env` for the chat channels supported by your bot.
+
+```
+# .env
+
+MESSENGER_PAGE_ID=
+MESSENGER_ACCESS_TOKEN=
+MESSENGER_APP_ID=
+MESSENGER_APP_SECRET=
+MESSENGER_VERIFY_TOKEN=
+
+WHATSAPP_ACCOUNT_SID=
+WHATSAPP_AUTH_TOKEN=
+WHATSAPP_PHONE_NUMBER=
+
+LINE_ACCESS_TOKEN=
+LINE_CHANNEL_SECRET=
+
+TELEGRAM_ACCESS_TOKEN=
+
+SLACK_ACCESS_TOKEN=
+SLACK_VERIFICATION_TOKEN=
+
+VIBER_ACCESS_TOKEN=
+```
+
+> **Note:** If you are not familiar with setting up chat channels, you may refer to the following Bottender docs:
+>
+> - [Messenger Setup](channel-messenger-setup.md)
+> - [WhatsApp Setup](channel-whatsapp-setup.md)
+> - [LINE Setup](channel-line-setup.md)
+> - [Slack Setup](channel-slack-setup.md)
+> - [Telegram Setup](channel-telegram-setup.md)
+> - [Viber Setup](channel-viber-setup.md)
+
+## Enable Each Chat Channel
+
+Next, you have to make sure you have enable the channel you want to support in `bottender.config.js`.
+
+For example, if you want to enable your bot to support Messenger, you have to set `channels.messenger.enabled` as `true`.
+
+```js
+// bottender.config.js
+
+module.exports = {
+  channels: {
+    messenger: {
+      enabled: true,
+      path: '/webhooks/messenger',
+      pageId: process.env.MESSENGER_PAGE_ID,
+      accessToken: process.env.MESSENGER_ACCESS_TOKEN,
+      appId: process.env.MESSENGER_APP_ID,
+      appSecret: process.env.MESSENGER_APP_SECRET,
+      verifyToken: process.env.MESSENGER_VERIFY_TOKEN,
+    },
+    whatsapp: {
+      enabled: true,
+      path: '/webhooks/whatsapp',
+      accountSid: process.env.WHATSAPP_ACCOUNT_SID,
+      authToken: process.env.WHATSAPP_AUTH_TOKEN,
+      phoneNumber: process.env.WHATSAPP_PHONE_NUMBER,
+    },
+    line: {
+      enabled: true,
+      path: '/webhooks/line',
+      accessToken: process.env.LINE_ACCESS_TOKEN,
+      channelSecret: process.env.LINE_CHANNEL_SECRET,
+    },
+    telegram: {
+      enabled: true,
+      path: '/webhooks/telegram',
+      accessToken: process.env.TELEGRAM_ACCESS_TOKEN,
+    },
+    slack: {
+      enabled: true,
+      path: '/webhooks/slack',
+      accessToken: process.env.SLACK_ACCESS_TOKEN,
+      signingSecret: process.env.SLACK_SIGNING_SECRET,
+    },
+    viber: {
+      enabled: true,
+      path: '/webhooks/viber',
+      accessToken: process.env.VIBER_ACCESS_TOKEN,
+      sender: {
+        name: 'xxxx',
+      },
+    },
+  },
+};
+```
+
+## Develop Cross-Platform Bot Actions
+
+Now, you can develop actions to handle messages from different platforms.
+
+A right bot action relates to the current user status with the incoming event. In the following section, you can see various approaches to cross-platform event handling.
+
+### Using `context.platform` to Manage Cross-Platform Events
+
+You can use [`context.platform`](api-context.md#platform) to check that each message belongs to which platform.
+
+In the following code, you can see a simple action that replies to the users with the platform they are using.
+
+```js
+module.exports = async function App(context) {
+  await context.sendText(`Hello World. Platform: ${context.platform}`);
+};
+```
+
+You can check out the repository of the above example [here](https://github.com/Yoctol/bottender/tree/master/examples/multiple-channels).
+
+### Using `router` and `platform` to Manage Cross-Platform Events
+
+Different platforms provide different advanced events for better user experience. For example, Messenger supports Button Template, while LINE supports Flex Message. Therefore, you might want to define some platform-specific actions to handle these advanced events.
+
+To better organize bot actions, you can use `router` and `platform` functions from `bottender/router` to handle routing among different platforms.
+
+In the example app, you can see two additional actions to leverage the platform-specific events.
+
+```js
+import { platform, router } from 'bottender/router';
+
+async function LineAction(context) {
+  await context.sendFlex('This is a line-specific message!', {
+    type: 'bubble',
+    body: {
+      type: 'box',
+      layout: 'horizontal',
+      contents: [
+        {
+          type: 'text',
+          text: 'Hello,',
+        },
+        {
+          type: 'text',
+          text: 'World',
+        },
+      ],
+    },
+  });
+}
+
+async function MessengerAction(context) {
+  await context.sendButtonTemplate('This is a messenger-specific message!', [
+    {
+      type: 'postback',
+      title: 'Hello',
+      payload: 'Hello',
+    },
+    {
+      type: 'postback',
+      title: 'World',
+      payload: 'World',
+    },
+  ]);
+}
+
+module.exports = async function App(context) {
+  return router([
+    platform('line', LineAction),
+    platform('messenger', MessengerAction),
+  ]);
+};
+```
+
+### Using Platform Specific Routes to Manage Cross-Platform Events
+
+By Bottender 1.1+, you can use platform specific routes as an alternative to organize events from various chat channels. To learn more about the details of those specific routes, check out their documentation:
+
+- [Messenger Routes](channel-messenger-routing.md)
+- [WhatsApp Routes](channel-whatsapp-routing.md)
+- [LINE Routes](channel-line-routing.md)
+- [Slack Routes](channel-slack-routing.md)
+- [Telegram Routes](channel-telegram-routing.md)
+- [Viber Routes](channel-viber-routing.md)
diff --git a/website/versioned_docs/version-1.4/api-console-context.md b/website/versioned_docs/version-1.4/api-console-context.md
new file mode 100644
index 00000000..de2e2457
--- /dev/null
+++ b/website/versioned_docs/version-1.4/api-console-context.md
@@ -0,0 +1,25 @@
+---
+id: version-1.4-api-console-context
+title: ConsoleContext
+original_id: api-console-context
+---
+
+Extends from [Context](api-context.md).
+
+#### `platform`
+
+Example:
+
+```js
+context.platform; // 'console'
+```
+
+#### `sendText(text)`
+
+Send text to the owner of the session.
+
+Example:
+
+```js
+context.sendText('Hello');
+```
diff --git a/website/versioned_docs/version-1.4/api-context.md b/website/versioned_docs/version-1.4/api-context.md
index db2baf5e..ded69ede 100644
--- a/website/versioned_docs/version-1.4/api-context.md
+++ b/website/versioned_docs/version-1.4/api-context.md
@@ -8,14 +8,14 @@ Bottender context encapsulates many helpful methods and exports session, client,
 
 For platform specific methods, please check out following links:
 
-| Platform                                | Doc                                                    |
-| --------------------------------------- | ------------------------------------------------------ |
-| [Messenger](https://www.messenger.com/) | [APIReference-MessengerContext](api-messenger-context) |
-| [LINE](https://line.me/)                | [APIReference-LineContext](api-line-context)           |
-| [Slack](https://slack.com/)             | [APIReference-SlackContext](api-slack-context)         |
-| [Telegram](https://telegram.org/)       | [APIReference-TelegramContext](api-telegram-context)   |
-| [Viber](https://www.viber.com/)         | [APIReference-ViberContext](api-viber-context)         |
-| Console                                 | [APIReference-ConsoleContext](api-console-context)     |
+| Platform                                | Doc                                                       |
+| --------------------------------------- | --------------------------------------------------------- |
+| [Messenger](https://www.messenger.com/) | [APIReference-MessengerContext](api-messenger-context.md) |
+| [LINE](https://line.me/)                | [APIReference-LineContext](api-line-context.md)           |
+| [Slack](https://slack.com/)             | [APIReference-SlackContext](api-slack-context.md)         |
+| [Telegram](https://telegram.org/)       | [APIReference-TelegramContext](api-telegram-context.md)   |
+| [Viber](https://www.viber.com/)         | [APIReference-ViberContext](api-viber-context.md)         |
+| Console                                 | [APIReference-ConsoleContext](api-console-context.md)     |
 
 #### `platform`
 
diff --git a/website/versioned_docs/version-1.4/api-event.md b/website/versioned_docs/version-1.4/api-event.md
new file mode 100644
index 00000000..cf746337
--- /dev/null
+++ b/website/versioned_docs/version-1.4/api-event.md
@@ -0,0 +1,87 @@
+---
+id: version-1.4-api-event
+title: Event
+original_id: api-event
+---
+
+Bottender event helps you determine what kind of message is sent from user.
+
+You can access `context.event` object within your handlers as the following example shows.
+
+```js
+async function MyAction(context) {
+  if (context.event.isText) {
+    await context.sendText('I know you sent text message.');
+  } else {
+    await context.sendText('I know you did not send text message.');
+  }
+}
+```
+
+For platform specific events, please check out following links:
+
+| Platform                                | Doc                                                   |
+| --------------------------------------- | ----------------------------------------------------- |
+| Console                                 | [APIReference-ConsoleEvent](api-console-event.md)     |
+| [Messenger](https://www.messenger.com/) | [APIReference-MessengerEvent](api-messenger-event.md) |
+| [LINE](https://line.me/)                | [APIReference-LineEvent](api-line-event.md)           |
+| [Slack](https://slack.com/)             | [APIReference-SlackEvent](api-slack-event.md)         |
+| [Telegram](https://telegram.org/)       | [APIReference-TelegramEvent](api-telegram-event.md)   |
+| [Viber](https://www.viber.com/)         | [APIReference-ViberEvent](api-viber-event.md)         |
+
+#### `rawEvent`
+
+Underlying raw event.
+
+Example:
+
+```js
+event.rawEvent;
+// {
+//   ...
+// }
+```
+
+#### `isMessage`
+
+Determine if the event is a message event.
+
+Example:
+
+```js
+event.isMessage; // true
+```
+
+#### `message`
+
+The message object from Messenger raw event.
+
+Example:
+
+```js
+event.message;
+// {
+//   ...
+//   text: 'Awesome.',
+// }
+```
+
+#### `isText`
+
+Determine if the event is a message event which includes text.
+
+Example:
+
+```js
+event.isText; // true
+```
+
+#### `text`
+
+The text string from Messenger raw event.
+
+Example:
+
+```js
+event.text; // 'Awesome.'
+```
diff --git a/website/versioned_docs/version-1.4/channel-line-liff.md b/website/versioned_docs/version-1.4/channel-line-liff.md
new file mode 100644
index 00000000..042bd4b8
--- /dev/null
+++ b/website/versioned_docs/version-1.4/channel-line-liff.md
@@ -0,0 +1,223 @@
+---
+id: version-1.4-channel-line-liff
+title: LINE Front-end Framework (LIFF)
+original_id: channel-line-liff
+---
+
+**LINE Front-end Framework (LIFF)** is the webview solution in LINE Apps. LIFF provides three different sizes of embedded webviews. You may use those sizes to enrich your users' experience:
+
+![different sizes of liff](https://user-images.githubusercontent.com/563929/73712876-6a01b100-4746-11ea-8f85-22b8026882d5.png)
+
+Besides, LIFF gives you more chances to improve interactions between users and the chatbot. One of the common scenarios is submitting a form in LINE Bots.
+
+### Features in LIFF
+
+LIFF lets you access a lot of different APIs on your web pages. Before getting started, you may want to grab some basic knowledge from LINE official docs:
+
+- [Getting user profile](https://developers.line.biz/en/reference/liff/#get-profile)
+- [Sending messages to LINE](https://developers.line.biz/en/reference/liff/#send-messages)
+- [Opening link in external browser](https://developers.line.biz/en/reference/liff/#open-window)
+- [Scanning QR Code](https://developers.line.biz/en/reference/liff/#scan-code)
+- [Connecting to Bluetooth devices](https://developers.line.biz/en/reference/liff/#bluetooth-request-device)
+- Others features in [LIFF playground](https://playground-for-liff.linecorp.com/)
+
+## LIFF V2
+
+LIFF v2 introduces some new features to developers, e.g., scanning QR codes, allowing users to open LIFF pages on the Desktop version of the LINE app.
+
+For catching up quickly, this is a minimal implementation - [Bottender example with LIFF v2](https://github.com/Yoctol/bottender/tree/master/examples/line-liff-v2).
+
+The following sections are step by step tutorials about how to send messages with LIFF in a Bottender project created by [Create Bottender App](getting-started.md#create-a-new-bottender-app).
+
+> **Note:** LIFF v1 is not recommended anymore. If you're using it, you should migrate to LIFF v2. However, you could still find an example for [LIFF v1 here](https://github.com/Yoctol/bottender/tree/master/examples/line-liff-v1).
+
+## Workflow
+
+For making your LIFF pages work, your typical workflow should look like this:
+
+- Creating a LINE Login channel in [LINE Developers console](https://developers.line.biz/console/)
+- Adding a LIFF app in the LINE Login channel
+- Developing the LIFF page
+
+### Creating LINE Login Channel
+
+First of all, you need to open [LINE Developers Console](https://developers.line.biz/console/) to create a LINE Login channel.
+
+For more details about creating a LINE Login channel, please checkout [Creating A Provider and Channel](https://developers.line.biz/en/docs/liff/getting-started/#creating-a-provider-and-channel).
+
+### Adding LIFF in the LINE Login Channel
+
+- Click the "LIFF" tab in the LINE Login Channel you just created.
+- Click the "Add" button
+- Fill in the "Endpoint URL" in this format: `https://{your domain}.ngrok.io/liff`
+- Fill in other values
+
+For more details about registering a LIFF app, please checkout [Registering LIFF App](https://developers.line.biz/en/docs/liff/registering-liff-apps/#registering-liff-app).
+
+> **Note:** [ngrok](https://ngrok.com/) is a well-known service that provides public HTTPS URLs for your local server using the tunnel. When you develop your bot, you may want to use ngrok to get a temporary HTTPS URL.
+
+### Environment Variables Setting
+
+If you are familiar with any official Bottender example, you may already know about how to use the `.env` file to manage your environment variables in your local project.
+
+In this case, you need to add another `LINE_LIFF_ID` env to `.env` file, so there are at least those three LINE related environment variables in your file:
+
+```
+LINE_ACCESS_TOKEN={your LINE access token from LINE Messaging API channel}
+LINE_CHANNEL_SECRET={your LINE channel secret from LINE Messaging API channel}
+LINE_LIFF_ID={your LIFF id from LINE Login channel}
+```
+
+You could find your LIFF ID in the LIFF URL. The format of LIFF URL looks like `line://app/{your LIFF ID}`.
+
+For more details about LIFF ID, please checkout [Registering LIFF App (in step 4)](https://developers.line.biz/en/docs/liff/registering-liff-apps/#registering-liff-app).
+
+### Adding Custom Routes to HTTP Server for LIFF Pages
+
+To serve LIFF webpages, we need to add additional routes to the server. Fortunately, [custom server](advanced-guides-custom-server.md#the-concept) come to the rescue!
+
+You could use express, koa, restify, or whatever you like, but we are going to use express in this guide. Before going down, make sure that you set up correctly according to [this guide](advanced-guides-custom-server.md#express).
+
+After having a custom server, you could add the following two routes into `server.js` to handle requests from LIFF.
+
+```js
+server.get('/send-id', (req, res) => {
+  res.json({ id: process.env.LINE_LIFF_ID });
+});
+
+server.get('/liff', (req, res) => {
+  const filename = path.join(`${__dirname}/liff.html`);
+  res.sendFile(filename);
+});
+```
+
+### Initializing the LIFF Page
+
+Before starting using any feature provided by LIFF, you need to create a `liff.html` file in the root directory of the project and copy the following code into it for LIFF initialization:
+
+```html
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>BOTTENDER LINE LIFF V2 DEMO</title>
+  </head>
+
+  <body>
+    <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
+    <script>
+      function initializeLiff(myLiffId) {
+        liff
+          .init({
+            liffId: myLiffId,
+          })
+          .then(() => {
+            alert('LIFF init success!');
+          })
+          .catch(err => {
+            alert(`error: ${JSON.stringify(err)}`);
+          });
+      }
+
+      document.addEventListener('DOMContentLoaded', () => {
+        fetch(`/send-id`)
+          .then(reqResponse => reqResponse.json())
+          .then(jsonResponse => {
+            let myLiffId = jsonResponse.id;
+            initializeLiff(myLiffId);
+          })
+          .catch(err => {
+            alert(`error: ${JSON.stringify(err)}`);
+          });
+      });
+    </script>
+  </body>
+</html>
+```
+
+It is worth mentioning that `liff.init()` and some other `liff` methods return `Promise` as results, so you should handle those asynchronous code carefully.
+
+### Sending Messages from LIFF page
+
+After initializing the LIFF page, we can call `liff.sendMessages` to send messages in the thread imperatively. For example:
+
+```js
+liff.sendMessages([
+  {
+    type: 'text',
+    text: 'Hello, LIFF!',
+  },
+]);
+```
+
+Up to five texts, images, videos, audios, locations, templates (only a URI action can be set as an action), and flex messages are acceptable.
+
+Let's add a click event listener to send messages on click. You could replace the body tag in `liff.html` with the following implementation:
+
+```html
+<body>
+  <button id="button">send test message</button>
+  <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
+  <script>
+    function initializeLiff(myLiffId) {
+      liff
+        .init({
+          liffId: myLiffId,
+        })
+        .then(() => {
+          setButtonHandler();
+        })
+        .catch(err => {
+          alert(`error: ${JSON.stringify(err)}`);
+        });
+    }
+
+    function setButtonHandler() {
+      let button = document.getElementById('button');
+      button.addEventListener('click', () => {
+        window.alert('clicked: sendMessages');
+        liff
+          .sendMessages([
+            {
+              type: 'text',
+              text: 'Hello, LIFF!',
+            },
+          ])
+          .then(() => {
+            alert('message sent');
+            liff.closeWindow();
+          })
+          .catch(err => {
+            window.alert('Error sending message: ' + err);
+          });
+      });
+    }
+
+    document.addEventListener('DOMContentLoaded', () => {
+      fetch(`/send-id`)
+        .then(reqResponse => reqResponse.json())
+        .then(jsonResponse => {
+          let myLiffId = jsonResponse.id;
+          initializeLiff(myLiffId);
+        })
+        .catch(err => {
+          alert(`error: ${JSON.stringify(err)}`);
+        });
+    });
+  </script>
+</body>
+```
+
+### Sending LIFF LINK to the User
+
+To test the LIFF page, you could replace the code in `index.js` to send the LIFF URL to the user:
+
+```js
+module.exports = async function App(context) {
+  const liffUrl = `https://liff.line.me/${process.env.LINE_LIFF_ID}`;
+  await context.sendText(liffUrl);
+};
+```
+
+Now, you could execute `yarn dev` or `npm run dev` to start the server, and you have a LINE Bot with a simple LIFF page.
diff --git a/website/versioned_docs/version-1.4/channel-line-notify.md b/website/versioned_docs/version-1.4/channel-line-notify.md
index 05cb3e3b..2c028adc 100644
--- a/website/versioned_docs/version-1.4/channel-line-notify.md
+++ b/website/versioned_docs/version-1.4/channel-line-notify.md
@@ -17,7 +17,7 @@ If you want to have a clean project with LINE Notify, you could start from [this
 3. Fill the `.env` file with the correct values.
 4. Run `yarn dev` or `npm run dev` to start the dev server.
 
-If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started#create-a-new-bottender-app) command and migrate it by following the migration instructions below.
+If you want to have the folder structure we recommend, you could start with [create-bottender-app](getting-started.md#create-a-new-bottender-app) command and migrate it by following the migration instructions below.
 
 ### Creating a LINE Notify Service
 
@@ -71,9 +71,9 @@ export default new LineNotify({
 
 ### Guiding Users to the LINE Notify Authorization Page
 
-To serve webpages, we need to add additional routes to the server. Fortunately, [custom server](advanced-guides-custom-server#the-concept) come to the rescue!
+To serve webpages, we need to add additional routes to the server. Fortunately, [custom server](advanced-guides-custom-server.md#the-concept) come to the rescue!
 
-You could use express, koa, restify, or whatever you like, but we are going to use express in this guide. Before going down, make sure that you set up correctly according to [this guide](advanced-guides-custom-server#express).
+You could use express, koa, restify, or whatever you like, but we are going to use express in this guide. Before going down, make sure that you set up correctly according to [this guide](advanced-guides-custom-server.md#express).
 
 Modify `src/index.js` to send the authorization link to your users:
 
diff --git a/website/versioned_docs/version-1.4/channel-messenger-handover-protocol.md b/website/versioned_docs/version-1.4/channel-messenger-handover-protocol.md
new file mode 100644
index 00000000..b87b1757
--- /dev/null
+++ b/website/versioned_docs/version-1.4/channel-messenger-handover-protocol.md
@@ -0,0 +1,196 @@
+---
+id: version-1.4-channel-messenger-handover-protocol
+title: Messenger Handover Protocol
+original_id: channel-messenger-handover-protocol
+---
+
+<p><img width="360" src="https://user-images.githubusercontent.com/662387/71710521-a5534c00-2e37-11ea-8bb4-c3c0ea6656d9.jpg"></p>
+
+Since Messenger has become a critical customer support channel between brands and customers, the cooperation between Messenger Bots and human agents creates a complete customer journey on messaging business.
+
+Here comes the Messenger handover protocol, which aims to make a smooth transition between Messenger Bots and human agents in the compact Messenger window. Technically speaking, this protocol allows a Facebook Page simultaneously owns multiple Facebook Apps, e.g., a Facebook app for auto-reply, and another Facebook app for human agents support.
+
+> **Note:**
+>
+> - If you are curious how to change the profile image between Messenger Bot and human agents, please check Bottender's doc, [Using Persona](channel-messenger-persona.md).
+> - For more information about handover protocol, please check Facebook's official doc, [Handover Protocol](https://developers.facebook.com/docs/messenger-platform/handover-protocol/)
+
+### `Thread Control`: Who is Answering the Customer
+
+Before going further, you have to know the concept of `Thread Control`. Assume that there are multiple Facebook Apps behind a single Facebook page. Only one Facebook owns the `Thread Control` of a specific customer at a time, which means only one Facebook App can reply to the customer.
+
+### `Primary Receiver`: The Privilege App Role
+
+Among all the apps behind the Facebook Page, only a single app could be `Primary Receiver`, the rest are `Secondary Receivers`.
+
+You may treat `Primary Receiver` as the boss. S/he is the only one who owns the privilege to allow other apps to reply. S/he can also take back `Thread Control` from `Secondary Receivers` at any moment. By default, `Primary Receiver` owns the thread control, i.e., all messages sent to the `Primary Receiver.`
+
+You, or the Facebook Page admin, can change the app role, i,e, which app is the `Primary Receiver` at the Page Settings.
+
+The default setting of App roles is a bit mysterious. It is like no app is `Primary Receiver`. We would suggest you check your app role settings and assign Bottender's app as the `Primary Receiver` to manage the `Thread Control.`
+
+![](https://user-images.githubusercontent.com/662387/71799800-34a76c00-3091-11ea-9c15-c6ed8069eda2.png)
+
+![](https://user-images.githubusercontent.com/662387/71704543-010cdd80-2e16-11ea-8300-3e2f21291ce3.png)
+
+In the following table, you can see the difference in capability between `Primary Receiver` and `Secondary Receiver`.
+
+![](https://user-images.githubusercontent.com/662387/71662894-0aeafe00-2d8e-11ea-9de8-df807e7cfdd9.png)
+
+> **Note:**
+>
+> - If you can't wait to begin with an example, you can jump to Bottender's example, [Messenger Handover](https://github.com/Yoctol/bottender/tree/master/examples/messenger-handover).
+> - For more information, you can refer to Facebook's official doc, [Assigning App Roles](https://developers.facebook.com/docs/messenger-platform/handover-protocol/assign-app-roles)
+
+## Passing Thread Control
+
+The `Pass Thread Control` API allows one app to pass the control of a conversation to another. The calling app can pass optional metadata to the receiving app in the API request. By the `pass_thread_control` webhook, an app can know if it takes the thread control.
+
+```js
+async function App(context) {
+  await context.passThreadControl('target-app-id');
+}
+```
+
+`Page Inbox` is a unique official app of Facebook. It is the default Facebook app for every Facebook Page to reply to messages by human agents.
+
+![](https://user-images.githubusercontent.com/662387/71801051-bc42aa00-3094-11ea-9137-e0cffc2a5bf6.png)
+
+`Page Inbox` has three default folders: `Main`, `Done`, `Spam`. When you set `Page Inbox` as `Secondary Receiver`, all messages sent to your bot appears in the `Done` folder by default.
+
+When the Page inbox has control of the conversation, all messages from the conversation move to the `Inbox` folder and wait for a human agent to respond. When you move a specific customer to the `Done` folder, you indirectly pass the `Thread Control` of the customer to `Primary Receiver`, which is usually the Facebook App of your Bottender project.
+
+![](https://user-images.githubusercontent.com/662387/71801472-ffe9e380-3095-11ea-97d3-1b8f449b0b61.png)
+
+```js
+async function App(context) {
+  await context.passThreadControlToPageInbox();
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Passing Thread Control](https://developers.facebook.com/docs/messenger-platform/handover-protocol/pass-thread-control)
+
+## Taking Thread Control and Requesting Thread Control
+
+Take Thread Control API allows the `Primary Receiver` to take control of the conversation from `Secondary Receivers`. It is useful when `Secondary Receiver` is not responding. An optional metadata string may also be sent in the request.
+
+The `Secondary Receiver` app receives a `messaging_handovers` webhook event when it loses control of the conversation. The event contains the metadata string.
+
+```js
+async function App(context) {
+  await context.takeThreadControl();
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Take Thread Control](https://developers.facebook.com/docs/messenger-platform/handover-protocol/take-thread-control)
+
+The Request Thread Control API allows a `Secondary Receiver` to notify the `Primary Receiver` that it wants control of the chat. The `Primary Receiver` can then take control of the chat if necessary, then pass control to the `Secondary Receiver` that sent the request. An optional metadata string may also be sent in the request.
+
+The `Primary Receiver` may also ignore the request and do nothing.
+
+```js
+async function App(context) {
+  await context.requestThreadControl();
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Request Thread Control](https://developers.facebook.com/docs/messenger-platform/handover-protocol/request-thread-control)
+
+## Getting the Thread Owner
+
+The following function helps you check which Facebook app owns the `Thread Control`. It is useful for bots that have complex implementations of the handover protocol, where an app's actions are dependent on whether it currently has thread control.
+
+The Thread Owner API may be called by `Primary Receiver` and `Secondary Receivers`.
+
+```js
+async function App(context) {
+  const threadOwner = await context.getThreadOwner();
+  console.log(threadOwner); // { appId: "thread-owner-app-id" }
+}
+```
+
+Bottender offers a handy function, `isThreadOwner`, to check whether the Facebook App of Bottender code owns the thread. This function uses the `appId` in `bottender.config.js` to compare with the current App Id from `getThreadOwner`. So please make sure you have your `appId` appropriately set.
+
+```js
+async function App(context) {
+  const isThreadOwner = await context.isThreadOwner();
+  console.log(isThreadOwner); // true or false
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Get Thread Owner](https://developers.facebook.com/docs/messenger-platform/handover-protocol/get-thread-owner)
+
+## Retrieving the List of Secondary Receivers
+
+This API allows `Primary Receiver` to get the list of `Secondary Receiver` apps for a page.
+
+By default, no app has been set explicitly as a Primary Receiver. We recommend you to config your Bottender app as the `Primary Receiver` to avoid any unexpected situations.
+
+```js
+async function App(context) {
+  const secondaryReceivers = await context.client.getSecondaryReceivers();
+  console.log(secondaryReceivers);
+  // [
+  //   {
+  //     id: "12345678910",
+  //     name: "David's Composer"
+  //   },
+  //   {
+  //     id: "23456789101",
+  //     name: "Messenger Rocks"
+  //   }
+  // ]
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Secondary Receivers List API Reference](https://developers.facebook.com/docs/messenger-platform/reference/handover-protocol/secondary-receivers/)
+
+## Webhook Events
+
+In the following section, we would like to highlight a few highly related webhook events. Please make sure you have enable corresponding `Page Subscription Fields` in your Facebook App Settings.
+
+![](https://user-images.githubusercontent.com/662387/71803615-3a567f00-309c-11ea-959f-9c3ba16fae89.png)
+
+### app_role
+
+Once a role is assigned, either `Primary Receiver` or `Secondary Receiver` app gets an `app_role` webhook.
+
+```js
+{
+  recipient: {
+    id: '<PSID>'
+  },
+  timestamp: 1458692752478,
+  appRoles: {
+    123456789: ['primary_receiver'],
+  },
+}
+```
+
+> - For more information, you can refer to Facebook's official doc, [Assign App Roles.](https://developers.facebook.com/docs/messenger-platform/handover-protocol/assign-app-roles)
+
+### messaging_handovers
+
+The messaging_handovers webhook event notifies an app's webhook when the following actions happen:
+
+- Thread control is passed to the app
+- Thread control is taken from the app
+- App role is changed
+
+> - For more information, you can refer to Facebook's official doc, [messaging_handovers](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_handovers)
+
+### standby
+
+For bots using the handover protocol, this callback occurs when a message has been sent to your page, but your app is not the current thread owner.
+
+> - For more information, you can refer to Facebook's official doc, [standby](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/standby)
+
+### echo
+
+This callback occurs when a message has been sent by your page.
+
+It is helpful when your `Primary Receiver` app, e.g., Bottender code, has passed `Thread Control` to a `Secondary Receiver app`, e.g., `Page Inbox`. However, you want to make sure your bot takes `Thread Control` back after a certain amount of time.
+
+You can subscribe to this callback by selecting the message_echoes field when setting up your webhook.
+
+> - For more information, you can refer to Facebook's official doc, [echo](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/message-echoes)
diff --git a/website/versioned_docs/version-1.4/channel-messenger-multi-page.md b/website/versioned_docs/version-1.4/channel-messenger-multi-page.md
new file mode 100644
index 00000000..b2683285
--- /dev/null
+++ b/website/versioned_docs/version-1.4/channel-messenger-multi-page.md
@@ -0,0 +1,127 @@
+---
+id: version-1.4-channel-messenger-multi-page
+title: Messenger Multi-Page Support
+original_id: channel-messenger-multi-page
+---
+
+![](https://user-images.githubusercontent.com/662387/71957687-66533b00-3229-11ea-9914-7e5919dd9d61.png)
+
+This doc outlines how to build a single Bottender app to serve multiple Facebook Pages. For example, you can create a single Bottender app of dental appointments for multiple clinics with individual clinic info.
+
+In our case, we use Bottender to build our online chatbot builder, [Creator](https://yoctol.ai/creator/). Every user can use [Creator](https://yoctol.ai/creator/) to build his/her bots. Technically speaking, each user's outcome of Creator is `bot configs` followed by a predefined structure. Finally, we use a single Bottender app with different `bot configs` to provider various Messenger Bots for each Facebook Page.
+
+The core concept that makes one Bottender app serves multiple Facebook Pages is `context`. It covers not only the user's message, Page Id, but also the Page token for bot response. For more information, you may check out Bottender's doc, [Handling Messenger Events](channel-messenger-handling-events.md).
+
+In the following sections, you can see three essential steps:
+
+- Enable `mapPageToAccessToken` feature of Bottender
+- Prepare `Facebook Page Access Token` for each Facebook Page
+- Manage `Facebook Page Subscriptions Fields` your bot needs
+
+> **Note:** If you can't wait to begin with an example code, please visit [Messenger Multi-pages Example](https://github.com/Yoctol/bottender/tree/master/examples/messenger-multi-pages).
+
+## Enable `mapPageToAccessToken` feature of Bottender
+
+You have to tell Bottender you are developing a Bottender app to serve for multiple Facebook Pages by fill `channels.messenger.mapPageToAccessToken` in `bottender.config.js`.
+
+```js
+// bottender.config.js
+module.exports = {
+  channels: {
+    messenger: {
+      enabled: true,
+      path: '/webhooks/messenger',
+      pageId: process.env.MESSENGER_PAGE_ID,
+      accessToken: process.env.MESSENGER_ACCESS_TOKEN,
+      appId: process.env.MESSENGER_APP_ID,
+      appSecret: process.env.MESSENGER_APP_SECRET,
+      verifyToken: process.env.MESSENGER_VERIFY_TOKEN,
+      mapPageToAccessToken: pageId => {
+        // resolve corresponding access token
+      },
+    },
+  },
+};
+```
+
+## Prepare `Facebook Page Access Token` for Each Facebook Page
+
+To make your bot to reply to users, you need `Facebook Page Access Token` for each Facebook Page. So, you have to implement your `mapPageToAccessToken` function to tell Bottender how to get the corresponding access token of each Facebook Page.
+
+> **Note:** All operations require a `Facebook Page Access Token` with [`manage_pages`](https://developers.facebook.com/docs/facebook-login/permissions/#reference-manage_pages) permission.
+
+### Fetching Token Statically
+
+If you only need your bot to apply to a fixed set of Facebook Pages, you may check the sample code below.
+
+```js
+function mapPageToAccessToken(pageId) {
+  switch (pageId) {
+    case PAGE_1_PAGE_ID:
+      return PAGE_1_ACCESS_TOKEN;
+    case PAGE_2_PAGE_ID:
+      return PAGE_2_ACCESS_TOKEN;
+  }
+}
+```
+
+For more information, you can check the Bottender example, [Messenger Multi-pages Example](https://github.com/Yoctol/bottender/tree/master/examples/messenger-multi-pages).
+
+### Fetching Token Dynamically
+
+We assume that an advanced developer like you might need to support more Facebook Pages dynamically. Plus, you don't want to modify the `mapPageToAccessToken` function every time you add your bot to a new Page.
+
+In this case, you may dynamically load tokens from SQL Database, MongoDB, Redis, REST API, or whatever you want:
+
+```js
+async function mapPageToAccessToken(pageId) {
+  return fetchTokenFromSomewhere(pageId);
+}
+```
+
+## Manage `Facebook Page Subscriptions Fields` Your Bot Needs
+
+Facebook has dedicated permission controls; please remember to subscribe `Facebook Page Subscriptions Fields` you need.
+
+We recommend you at least enable the following fields:
+
+- `messages`
+- `messaging_postbacks`
+- `messaging_optins`
+- `messaging_referrals`
+- `messaging_policy_enforcement`
+
+### Subscribe `Facebook Page Subscriptions Fields`
+
+We assume that you might need to add your bot to a Facebook Pages dynamically. In the following code, you can see how to subscribe `Facebook Page Subscriptions Fields` your bot needs.
+
+```js
+const { getClient } = require('bottender');
+
+const messenger = getClient('messenger');
+
+// subscribe app for page
+await messenger.axios.post(
+  `/${pageId}/subscribed_apps?access_token=${accessToken}`,
+  {
+    subscribedFields: [
+      'messages',
+      'messaging_postbacks',
+      'messaging_optins',
+      'messaging_referrals',
+      'messaging_policy_enforcement',
+    ].join(','),
+  }
+);
+```
+
+### Unsubscribe `Facebook Page Subscriptions Fields`
+
+We assume that you might need to remove your bot from Facebook Pages dynamically. In the following code, you can see how to unsubscribe `Facebook Page Subscriptions Fields` your bot have subscribed.
+
+```js
+// unsubscribe app for page
+await messenger.axios.delete(
+  `/${pageId}/subscribed_apps?access_token=${accessToken}`
+);
+```
diff --git a/website/versioned_docs/version-1.4/channel-slack-sending-messages.md b/website/versioned_docs/version-1.4/channel-slack-sending-messages.md
new file mode 100644
index 00000000..f1158912
--- /dev/null
+++ b/website/versioned_docs/version-1.4/channel-slack-sending-messages.md
@@ -0,0 +1,172 @@
+---
+id: version-1.4-channel-slack-sending-messages
+title: Sending Slack Messages
+original_id: channel-slack-sending-messages
+---
+
+Since Slack is a messaging channel specialized for team communication, Slack bots are usually expected to work either inside a channel or 1 on 1 chat.
+
+Chatting in the channel makes collecting individual preferences efficiently. For example, we have a Slack bot to collect colleagues' afternoon tea orders.
+
+Plus, Slack bot is especially good at notification scenarios, e.g., monitoring the health status of online service, or current stars of [Bottender GitHub repo](https://github.com/Yoctol/bottender)(Star it if you like it!). It is because Slack apps can post messages to channels, groups, or users without any extra fee.
+
+While developing with Official Slack API, developers have to manage token, channel id, user id of each dialog. Bottender helps developers manage those parameters by `context.` Bot developers can always make a bot respond to its current context, no matter the context is `Public Channels,` `Private Channels,` `Private Groups,` or `Direct Messaging.`
+
+> **Note:**
+>
+> - The idea of `Bot` connects to Slack's `APP.` It is important when you are cross-referencing Bottender doc with Slack's official doc.
+> - Since Slack is one of the sophisticated chat channels, in this doc, we only cover the most frequent use of methods. To access the full list of Slack API support of Bottender, you may refer to [Bottender's API](api-slack-context.md).
+>   If you are interested in Slack's complete methods, you may refer to Slack's official document, "[API Methods.](https://api.slack.com/methods)". Don't miss the `Test` tab next to each method; it offers a handy way to try native Slack API better.
+
+## Sending Text Messages
+
+![Screen Shot 2019-12-02 at 6 38 26 PM](https://user-images.githubusercontent.com/662387/69952866-0f27bf80-1533-11ea-8cbc-41b68e2f733e.png)
+
+`Text message` is the most frequent and common message types. It also offers a minimal medium carrying out dynamic data, e.g., stock price and weather info.
+
+```js
+async function SendHi(context) {
+  await context.sendText('Hi!');
+}
+```
+
+## Posting Messages
+
+![Screen Shot 2019-12-06 at 2 52 03 PM](https://user-images.githubusercontent.com/662387/70302447-1b17c800-1838-11ea-8c9a-affe2820fb2f.png)
+
+This method posts a message to a public channel, private channel, or direct message. To fully support Slack's chat UI features, Bottender passes message parameters to Slack's API.
+
+```js
+await context.chat.postMessage({
+  text: 'Hello world!',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.postMessage`](https://api.slack.com/methods/chat.postMessage)
+
+## Updating Messages
+
+![image](https://user-images.githubusercontent.com/662387/70304195-5e743580-183c-11ea-8859-fab3b2d1b55b.png)
+
+Updating existing messages is one of the unique features of Slack. It is useful when your bot displays a real-time date, and you don't want to have a frequently scrolling window.
+
+In the above image, the second message is updated by Bottender. You may notice that while a user updated his message, a little annotation `(edited)` is shown next to the message, a bot update its existing message without `(edited)` annotation.
+
+```js
+await context.chat.update({
+  text: 'Hello world!',
+  ts: '1405894322.002768',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.update`](https://api.slack.com/methods/chat.update)
+
+> **Note:**
+>
+> - The `ts` parameter in the code above, refers to the timestamp of the message, which is a (per-channel) unique id of each message,
+> - Ephemeral messages created by chat.postEphemeral or otherwise cannot be updated with this method.
+
+## Deleting Messages
+
+This method deletes a message posted by the bot from a channel.
+
+```js
+await context.chat.delete({
+  ts: '1405894322.002768',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.delete`](https://api.slack.com/methods/chat.delete)
+
+> **Note:** The `ts` parameter in the code above, refers to the timestamp of the message, which is a (per-channel) unique id of each message,
+
+## Posting Ephemeral Messages
+
+![image](https://user-images.githubusercontent.com/662387/70307764-da727b80-1844-11ea-9c33-488cf477e6a4.png)
+
+Bots can post `Ephemeral Messages` only visible to the assigned user in a specific public channel, private channel, or private conversation. In short, `Ephemeral Messages` allows bots to replies context-sensitive information to the user under current Bottender `context.` For example, when a user inputs `check-in`, the bot could post an `Ephemeral Message` to the user check-in time.
+
+For more information, please refer to Slack's official doc, [`chat.postEphemeral`](https://api.slack.com/methods/chat.postEphemeral)
+
+> **Note:** `Ephemeral Message` delivery is not guaranteed — the user must be currently active in Slack and a member of the specified channel.
+
+```js
+await context.chat.postEphemeral({
+  text: 'Hello world!',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.postEphemeral`](https://api.slack.com/methods/chat.postEphemeral)
+
+## Sharing Me Messages
+
+![image](https://user-images.githubusercontent.com/662387/70309070-9e8ce580-1847-11ea-97b3-c4d5bfaf6996.png)
+
+Share a me message into a channel.
+
+```js
+await context.chat.meMessage({
+  text: 'Hello world!',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.meMessage`](https://api.slack.com/methods/chat.meMessage)
+
+## Retrieving A Permalink URL for Specific Extant Messages
+
+![image](https://user-images.githubusercontent.com/662387/70309356-4efae980-1848-11ea-815e-dfdf7ae49cc2.png)
+
+Retrieve a permalink URL for a specific extant message
+
+```js
+await context.chat.getPermalink({
+  messageTs: '1234567890.123456',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.getPermalink`](https://api.slack.com/methods/chat.getPermalink)
+
+> **Note:** The `messageTs` parameter in the code above, refers to the timestamp of the message, which is a (per-channel) unique id of each message,
+
+## Scheduling Messages
+
+Slack owns a built-in message scheduling system. For other chat channels, you probably have to run a `cronjob` to schedule messages.
+
+### Scheduling Messages
+
+![image](https://user-images.githubusercontent.com/662387/70310573-efeaa400-184a-11ea-8f24-2cae4b644d08.png)
+
+Schedules a message to be sent to a channel.
+
+```js
+await context.chat.scheduleMessage({
+  text: 'Hello world!',
+  postAt: '299876400',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.scheduleMessage`](https://api.slack.com/methods/chat.scheduleMessage)
+
+> **Note:** The `postAt` parameter in the code above follows Unix EPOCH timestamp.
+
+### Getting a List of Scheduled Messages
+
+You can get a list of scheduled messages by the following code.
+
+```js
+await context.chat.scheduledMessages.list();
+```
+
+For more information, please refer to Slack's official doc, [`chat.scheduledMessages.list`](https://api.slack.com/methods/chat.scheduledMessages.list)
+
+### Deleting Pending Scheduled Messages
+
+First, you have to use `chat.scheduledMessages.list` to figure out the `<SCHEDULED_MESSAGE_ID>` you want to delete. Then you can use the below method to delete a scheduled message.
+
+```js
+await context.chat.deleteScheduledMessage({
+  scheduledMessageId: '<SCHEDULED_MESSAGE_ID>',
+});
+```
+
+For more information, please refer to Slack's official doc, [`chat.deleteScheduledMessage`](https://api.slack.com/methods/chat.deleteScheduledMessage)


Test patch:
diff --git a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
index d7ad2700..5f379297 100644
--- a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
+++ b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
@@ -445,6 +445,15 @@ describe('#mapRequestToEvents', () => {
     expect(events).toHaveLength(1);
     expect(events[0]).toBeInstanceOf(SlackEvent);
   });
+
+  it('should include callbackId when request is a interactiveMessageRequest', () => {
+    const { connector } = setup();
+    const events = connector.mapRequestToEvents(interactiveMessageRequest.body);
+
+    expect(events).toHaveLength(1);
+    expect(events[0]).toBeInstanceOf(SlackEvent);
+    expect(events[0].callbackId).toBe('wopr_game');
+  });
 });
 
 describe('#createContext', () => {
diff --git a/packages/bottender/src/slack/__tests__/routes.spec.ts b/packages/bottender/src/slack/__tests__/routes.spec.ts
index e714a140..96aa2c64 100644
--- a/packages/bottender/src/slack/__tests__/routes.spec.ts
+++ b/packages/bottender/src/slack/__tests__/routes.spec.ts
@@ -152,6 +152,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),
@@ -165,14 +183,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,
+      });
+    });
   });
 });