Repository: Yoctol/bottender
Pull request #718 (2020-04-05 15:24:07)

feat(messenger): support reaction event and routing
https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/message-reactions/

Support `event.isReaction` & `event.react`:

```js
function App(context) {
  if (context.event.isReaction) {
    console.log(context.event.reaction);
    // {
    //   reaction: 'smile',
    //   emoji: '\u{2764}\u{FE0F}',
    //   action: 'react',
    //   mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
    //  }
  }
}
```

Support detect events in routers:

```js
const { router, messenger } = require('bottender/router');

function App() {
  return router([
    messenger.reaction.react(HandleReactionReact),
    messenger.reaction.unreact(HandleReactionUnreact),
    messenger.reaction(HandleReaction),
  ]);
}

async function HandleReactionReact(context) {}
async function HandleReactionUnreact(context) {}
async function HandleReaction(context) {}
```

codecov-io: # [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=h1) Report
> Merging [#718](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=desc) into [release/1.4](https://codecov.io/gh/Yoctol/bottender/commit/239adbfa882d31a985c6d70070b49dfd451fcc62&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/Yoctol/bottender/pull/718/graphs/tree.svg?width=650&height=150&src=pr&token=JX3EGtxYH3)](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=tree)

```diff
@@               Coverage Diff               @@
##           release/1.4     #718      +/-   ##
===============================================
+ Coverage        80.82%   80.88%   +0.05%     
===============================================
  Files              108      108              
  Lines             4491     4505      +14     
  Branches          1078     1127      +49     
===============================================
+ Hits              3630     3644      +14     
+ Misses             815      808       -7     
- Partials            46       53       +7     
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/bottender/src/messenger/MessengerEvent.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9tZXNzZW5nZXIvTWVzc2VuZ2VyRXZlbnQudHM=) | `98.47% <100.00%> (+0.06%)` | :arrow_up: |
| [packages/bottender/src/messenger/routes.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9tZXNzZW5nZXIvcm91dGVzLnRz) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/chain.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9jaGFpbi50cw==) | `76.92% <0.00%> (ø)` | |
| [packages/bottender/src/getClient.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9nZXRDbGllbnQudHM=) | `55.55% <0.00%> (ø)` | |
| [packages/bottender/src/router/index.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9yb3V0ZXIvaW5kZXgudHM=) | `90.66% <0.00%> (ø)` | |
| [packages/create-bottender-app/src/shared/log.ts](https://codecov.io/gh/Yoctol/bottender/pull/718/diff?src=pr&el=tree#diff-cGFja2FnZXMvY3JlYXRlLWJvdHRlbmRlci1hcHAvc3JjL3NoYXJlZC9sb2cudHM=) | `0.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=footer). Last update [239adbf...a9ec7af](https://codecov.io/gh/Yoctol/bottender/pull/718?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/channel-line-routing.md b/docs/channel-line-routing.md
index 9894b8b1..f6c3750f 100644
--- a/docs/channel-line-routing.md
+++ b/docs/channel-line-routing.md
@@ -25,7 +25,7 @@ function App() {
     line.things.link(HandleThingsLink),
     line.things.unlink(HandleThingsUnlink),
     line.things.scenarioResult(HandleThingsScenarioResult),
-    line(HandleLine),
+    line.any(HandleLine),
   ]);
 }
 
@@ -50,7 +50,7 @@ async function HandleLine(context) {}
 
 All available routes in `line` that recognize different kind of events:
 
-- `line` - triggers the action when receiving any LINE events.
+- `line.any` - triggers the action when receiving any LINE events.
 - `line.message` - triggers the action when receiving LINE `message` events.
 - `line.follow` - triggers the action when receiving LINE `follow` events.
 - `line.unfollow` - triggers the action when receiving LINE `unfollow` events.
diff --git a/docs/channel-messenger-routing.md b/docs/channel-messenger-routing.md
index 96e6cff4..77b8b1b6 100644
--- a/docs/channel-messenger-routing.md
+++ b/docs/channel-messenger-routing.md
@@ -24,10 +24,13 @@ function App() {
     messenger.optin(HandleOptin),
     messenger.policyEnforcement(HandlePolicyEnforcement),
     messenger.postback(HandlePostback),
+    messenger.reaction.react(HandleReactionReact),
+    messenger.reaction.unreact(HandleReactionUnreact),
+    messenger.reaction(HandleReaction),
     messenger.read(HandleRead),
     messenger.referral(HandleReferral),
     messenger.standby(HandleStandby),
-    messenger(HandleMessenger),
+    messenger.any(HandleMessenger),
   ]);
 }
 
@@ -46,6 +49,9 @@ async function HandleAppRoles(context) {}
 async function HandleOptin(context) {}
 async function HandlePolicyEnforcement(context) {}
 async function HandlePostback(context) {}
+async function HandleReactionReact(context) {}
+async function HandleReactionUnreact(context) {}
+async function HandleReaction(context) {}
 async function HandleRead(context) {}
 async function HandleReferral(context) {}
 async function HandleStandby(context) {}
@@ -54,7 +60,7 @@ async function HandleMessenger(context) {}
 
 All available routes in `messenger` that recognize different kind of events:
 
-- `messenger` - triggers the action when receiving any Messenger events.
+- `messenger.any` - triggers the action when receiving any Messenger events.
 - `messenger.message` - triggers the action when receiving Messenger `message` events.
 - `messenger.accountLinking.linked` - triggers the action when receiving Messenger `account_linking` events with status `linked`.
 - `messenger.accountLinking.unlinked` - triggers the action when receiving Messenger `account_linking` events with status `unlinked`.
@@ -70,6 +76,9 @@ All available routes in `messenger` that recognize different kind of events:
 - `messenger.policyEnforcement` - triggers the action when receiving Messenger `policy_enforcement` events.
 - `messenger.postback` - triggers the action when receiving Messenger `postback` events.
 - `messenger.preCheckout` - triggers the action when receiving Messenger `pre_checkout` events.
+- `messenger.reaction.react` - triggers the action when receiving Messenger `reaction` events with action `react`.
+- `messenger.reaction.unreact` - triggers the action when receiving Messenger `reaction` events with action `unreact`.
+- `messenger.reaction` - triggers the action when receiving Messenger `reaction` events.
 - `messenger.read` - triggers the action when receiving Messenger `read` events.
 - `messenger.referral` - triggers the action when receiving Messenger `referral` events.
 - `messenger.standby` - triggers the action when receiving Messenger `standby` events.
diff --git a/docs/channel-slack-routing.md b/docs/channel-slack-routing.md
index 49f06bb0..2ca03a4d 100644
--- a/docs/channel-slack-routing.md
+++ b/docs/channel-slack-routing.md
@@ -13,7 +13,7 @@ function App() {
     slack.message(HandleMessage),
     slack.event('pin_added', HandlePinAdded),
     slack.event('star_added', HandleStarAdded),
-    slack(HandleSlack),
+    slack.any(HandleSlack),
   ]);
 }
 
@@ -26,6 +26,6 @@ async function HandleSlack(context) {}
 
 All available routes in `slack` that recognize different kind of events:
 
-- `slack` - triggers the action when receiving any Slack 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).
diff --git a/docs/channel-telegram-routing.md b/docs/channel-telegram-routing.md
index c8d09ce0..a7139de7 100644
--- a/docs/channel-telegram-routing.md
+++ b/docs/channel-telegram-routing.md
@@ -20,7 +20,7 @@ function App() {
     telegram.shippingQuery(HandleShippingQuery),
     telegram.preCheckoutQuery(HandlePreCheckoutQuery),
     telegram.poll(HandlePoll),
-    telegram(HandleTelegram),
+    telegram.any(HandleTelegram),
   ]);
 }
 
@@ -40,7 +40,7 @@ async function HandleTelegram(context) {}
 
 All available routes in `telegram` that recognize different kind of events:
 
-- `telegram` - triggers the action when receiving any Telegram events.
+- `telegram.any` - triggers the action when receiving any Telegram events.
 - `telegram.message` - triggers the action when receiving Telegram `message` events.
 - `telegram.editedMessage` - triggers the action when receiving Telegram `editedMessage` events.
 - `telegram.channelPost` - triggers the action when receiving Telegram `channelPost` events.
diff --git a/docs/channel-viber-routing.md b/docs/channel-viber-routing.md
index 62a7292a..975be2a7 100644
--- a/docs/channel-viber-routing.md
+++ b/docs/channel-viber-routing.md
@@ -17,7 +17,7 @@ function App() {
     viber.delivered(HandleDelivered),
     viber.seen(HandleSeen),
     viber.failed(HandleFailed),
-    viber(HandleViber),
+    viber.any(HandleViber),
   ]);
 }
 
@@ -34,7 +34,7 @@ async function HandleViber(context) {}
 
 All available routes in `viber` that recognize different kind of events:
 
-- `viber` - triggers the action when receiving any Viber events.
+- `viber.any` - triggers the action when receiving any Viber events.
 - `viber.message` - triggers the action when receiving Viber `message` events.
 - `viber.subscribed` - triggers the action when receiving Viber `subscribed` events.
 - `viber.unsubscribed` - triggers the action when receiving Viber `unsubscribed` events.
diff --git a/docs/channel-whatsapp-routing.md b/docs/channel-whatsapp-routing.md
index 54170b5a..d97bfc63 100644
--- a/docs/channel-whatsapp-routing.md
+++ b/docs/channel-whatsapp-routing.md
@@ -16,7 +16,7 @@ function App() {
     whatsapp.sent(HandleSent),
     whatsapp.delivered(HandleDelivered),
     whatsapp.read(HandleRead),
-    whatsapp(HandleWhatsapp),
+    whatsapp.any(HandleWhatsapp),
   ]);
 }
 
@@ -32,7 +32,7 @@ async function HandleWhatsapp(context) {}
 
 All available routes in `whatsapp` that recognize different kind of events:
 
-- `whatsapp` - triggers the action when receiving any WhatsApp events.
+- `whatsapp.any` - triggers the action when receiving any WhatsApp events.
 - `whatsapp.message` - triggers the action when receiving WhatsApp `received` events. Alias: `whatsapp.received`.
 - `whatsapp.media` - triggers the action when receiving WhatsApp `received` events includes media.
 - `whatsapp.received` - triggers the action when receiving WhatsApp `received` events.
diff --git a/packages/bottender/src/line/routes.ts b/packages/bottender/src/line/routes.ts
index 85fcde64..59c66117 100644
--- a/packages/bottender/src/line/routes.ts
+++ b/packages/bottender/src/line/routes.ts
@@ -10,6 +10,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Line = Route & {
+  any: Route;
   message: Route;
   follow: Route;
   unfollow: Route;
@@ -35,6 +36,8 @@ const line: Line = <C extends Context<any, any>>(action: Action<C, any>) => {
   return route(context => context.platform === 'line', action);
 };
 
+line.any = line;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'line' && context.event.isMessage,
diff --git a/packages/bottender/src/messenger/MessengerEvent.ts b/packages/bottender/src/messenger/MessengerEvent.ts
index 1862461c..9a9e8131 100644
--- a/packages/bottender/src/messenger/MessengerEvent.ts
+++ b/packages/bottender/src/messenger/MessengerEvent.ts
@@ -175,6 +175,21 @@ export type AccountLinking =
   | { status: 'linked'; authorizationCode: string }
   | { status: 'unlinked' };
 
+export type Reaction = {
+  reaction:
+    | 'smile'
+    | 'angry'
+    | 'sad'
+    | 'wow'
+    | 'love'
+    | 'like'
+    | 'dislike'
+    | 'other';
+  emoji: string;
+  action: 'react' | 'unreact';
+  mid: string;
+};
+
 export type MessengerRawEvent = {
   sender?: Sender;
   recipient?: Recipient;
@@ -196,6 +211,7 @@ export type MessengerRawEvent = {
   referral?: Referral;
   brandedCamera?: BrandedCamera;
   accountLinking?: AccountLinking;
+  reaction?: Reaction;
 };
 
 type MessengerEventOptions = {
@@ -864,4 +880,25 @@ export default class MessengerEvent implements Event<MessengerRawEvent> {
     }
     return (this._rawEvent as any).accountLinking;
   }
+
+  /**
+   * Determine if the event is a reaction event.
+   *
+   */
+  get isReaction(): boolean {
+    return (
+      !!this._rawEvent.reaction && typeof this._rawEvent.reaction === 'object'
+    );
+  }
+
+  /**
+   * The reaction object from Messenger event.
+   *
+   */
+  get reaction(): Reaction | null {
+    if (!this.isReaction) {
+      return null;
+    }
+    return (this._rawEvent as any).reaction;
+  }
 }
diff --git a/packages/bottender/src/messenger/routes.ts b/packages/bottender/src/messenger/routes.ts
index 3af8dc55..f92fb2d6 100644
--- a/packages/bottender/src/messenger/routes.ts
+++ b/packages/bottender/src/messenger/routes.ts
@@ -10,6 +10,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Messenger = Route & {
+  any: Route;
   message: Route;
   accountLinking: Route & {
     linked: Route;
@@ -31,6 +32,10 @@ type Messenger = Route & {
   read: Route;
   referral: Route;
   standby: Route;
+  reaction: Route & {
+    react: Route;
+    unreact: Route;
+  };
 };
 
 const messenger: Messenger = <C extends Context<any, any>>(
@@ -39,6 +44,8 @@ const messenger: Messenger = <C extends Context<any, any>>(
   return route(context => context.platform === 'messenger', action);
 };
 
+messenger.any = messenger;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'messenger' && context.event.isMessage,
@@ -249,4 +256,38 @@ function standby<C extends Context<any, any>>(action: Action<C, any>) {
 
 messenger.standby = standby;
 
+function reaction<C extends Context<any, any>>(action: Action<C, any>) {
+  return route(
+    (context: C) =>
+      context.platform === 'messenger' && context.event.isReaction,
+    action
+  );
+}
+
+messenger.reaction = reaction;
+
+function reactionReact<C extends Context<any, any>>(action: Action<C, any>) {
+  return route(
+    (context: C) =>
+      context.platform === 'messenger' &&
+      context.event.isReaction &&
+      context.event.reaction.action === 'react',
+    action
+  );
+}
+
+reaction.react = reactionReact;
+
+function reactionUnreact<C extends Context<any, any>>(action: Action<C, any>) {
+  return route(
+    (context: C) =>
+      context.platform === 'messenger' &&
+      context.event.isReaction &&
+      context.event.reaction.action === 'unreact',
+    action
+  );
+}
+
+reaction.unreact = reactionUnreact;
+
 export default messenger;
diff --git a/packages/bottender/src/slack/routes.ts b/packages/bottender/src/slack/routes.ts
index 53ad60c8..d6b1f47e 100644
--- a/packages/bottender/src/slack/routes.ts
+++ b/packages/bottender/src/slack/routes.ts
@@ -12,6 +12,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Slack = Route & {
+  any: Route;
   message: Route;
   event: <C extends Context<any, any>>(
     eventType: EventTypes,
@@ -33,6 +34,8 @@ const slack: Slack = <C extends Context<any, any>>(action: Action<C, any>) => {
   return route((context: C) => context.platform === 'slack', action);
 };
 
+slack.any = slack;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'slack' && context.event.isMessage,
diff --git a/packages/bottender/src/telegram/routes.ts b/packages/bottender/src/telegram/routes.ts
index 4032382e..b7029d3c 100644
--- a/packages/bottender/src/telegram/routes.ts
+++ b/packages/bottender/src/telegram/routes.ts
@@ -10,6 +10,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Telegram = Route & {
+  any: Route;
   message: Route;
   editedMessage: Route;
   channelPost: Route;
@@ -28,6 +29,8 @@ const telegram: Telegram = <C extends Context<any, any>>(
   return route(context => context.platform === 'telegram', action);
 };
 
+telegram.any = telegram;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'telegram' && context.event.isMessage,
diff --git a/packages/bottender/src/viber/routes.ts b/packages/bottender/src/viber/routes.ts
index c176650b..41d6e505 100644
--- a/packages/bottender/src/viber/routes.ts
+++ b/packages/bottender/src/viber/routes.ts
@@ -10,6 +10,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Viber = Route & {
+  any: Route;
   message: Route;
   subscribed: Route;
   unsubscribed: Route;
@@ -23,6 +24,8 @@ const viber: Viber = <C extends Context<any, any>>(action: Action<C, any>) => {
   return route(context => context.platform === 'viber', action);
 };
 
+viber.any = viber;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'viber' && context.event.isMessage,
diff --git a/packages/bottender/src/whatsapp/routes.ts b/packages/bottender/src/whatsapp/routes.ts
index bd763a27..2ae73e04 100644
--- a/packages/bottender/src/whatsapp/routes.ts
+++ b/packages/bottender/src/whatsapp/routes.ts
@@ -10,6 +10,7 @@ type Route = <C extends Context<any, any>>(
 };
 
 type Whatsapp = Route & {
+  any: Route;
   message: Route;
   media: Route;
   received: Route;
@@ -24,6 +25,8 @@ const whatsapp: Whatsapp = <C extends Context<any, any>>(
   return route(context => context.platform === 'whatsapp', action);
 };
 
+whatsapp.any = whatsapp;
+
 function message<C extends Context<any, any>>(action: Action<C, any>) {
   return route(
     (context: C) => context.platform === 'whatsapp' && context.event.isMessage,


Test patch:
diff --git a/packages/bottender/src/line/__tests__/routes.spec.ts b/packages/bottender/src/line/__tests__/routes.spec.ts
index 691c3831..672b9d47 100644
--- a/packages/bottender/src/line/__tests__/routes.spec.ts
+++ b/packages/bottender/src/line/__tests__/routes.spec.ts
@@ -323,6 +323,25 @@ describe('#line', () => {
     });
   });
 
+  describe('#line.any', () => {
+    it('should call action when it receives a line event', async () => {
+      await expectRouteMatchLineEvent({
+        route: line.any(Action),
+        event: lineEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-line event', async () => {
+      await expectRouteNotMatchContext({
+        route: line.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#line.message', () => {
     it('should call action when it receives a line message event', async () => {
       await expectRouteMatchLineEvent({
diff --git a/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts b/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts
index ffff315c..fae2b0ff 100644
--- a/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts
+++ b/packages/bottender/src/messenger/__tests__/MessengerEvent.spec.ts
@@ -610,6 +610,38 @@ const accountLinkingUnlinked = {
   },
 };
 
+const reactionReact = {
+  sender: {
+    id: '1476077422222289',
+  },
+  recipient: {
+    id: '707356222221168',
+  },
+  timestamp: 1469111400000,
+  reaction: {
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'react',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  },
+};
+
+const reactionUnreact = {
+  sender: {
+    id: '1476077422222289',
+  },
+  recipient: {
+    id: '707356222221168',
+  },
+  timestamp: 1469111400000,
+  reaction: {
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'unreact',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  },
+};
+
 it('#rawEvent', () => {
   expect(new MessengerEvent(textMessage).rawEvent).toEqual(textMessage);
   expect(new MessengerEvent(imageMessage).rawEvent).toEqual(imageMessage);
@@ -1455,3 +1487,27 @@ it('#accountLinking', () => {
     status: 'unlinked',
   });
 });
+
+it('#isReaction', () => {
+  expect(new MessengerEvent(textMessage).isReaction).toEqual(false);
+  expect(new MessengerEvent(postback).isReaction).toEqual(false);
+  expect(new MessengerEvent(reactionReact).isReaction).toEqual(true);
+  expect(new MessengerEvent(reactionUnreact).isReaction).toEqual(true);
+});
+
+it('#reaction', () => {
+  expect(new MessengerEvent(textMessage).reaction).toEqual(null);
+  expect(new MessengerEvent(postback).reaction).toEqual(null);
+  expect(new MessengerEvent(reactionReact).reaction).toEqual({
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'react',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  });
+  expect(new MessengerEvent(reactionUnreact).reaction).toEqual({
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'unreact',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  });
+});
diff --git a/packages/bottender/src/messenger/__tests__/routes.spec.ts b/packages/bottender/src/messenger/__tests__/routes.spec.ts
index bc9bdf90..56f80ed1 100644
--- a/packages/bottender/src/messenger/__tests__/routes.spec.ts
+++ b/packages/bottender/src/messenger/__tests__/routes.spec.ts
@@ -322,6 +322,38 @@ const messengerEventStandby = new MessengerEvent(
   { isStandby: true }
 );
 
+const messengerEventReactionReact = new MessengerEvent({
+  sender: {
+    id: '1476077422222289',
+  },
+  recipient: {
+    id: '707356222221168',
+  },
+  timestamp: 1469111400000,
+  reaction: {
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'react',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  },
+});
+
+const messengerEventReactionUnreact = new MessengerEvent({
+  sender: {
+    id: '1476077422222289',
+  },
+  recipient: {
+    id: '707356222221168',
+  },
+  timestamp: 1469111400000,
+  reaction: {
+    reaction: 'smile',
+    emoji: '\u{2764}\u{FE0F}',
+    action: 'unreact',
+    mid: 'mid.$cAAE1UUyiiwthh0NPrVbVf4HFNDGl',
+  },
+});
+
 async function Action(context) {
   await context.sendText('hello');
 }
@@ -401,6 +433,25 @@ describe('#messenger', () => {
     });
   });
 
+  describe('#messenger.any', () => {
+    it('should call action when it receives a messenger event', async () => {
+      await expectRouteMatchMessengerEvent({
+        route: messenger.any(Action),
+        event: messengerEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-messenger event', async () => {
+      await expectRouteNotMatchContext({
+        route: messenger.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#messenger.message', () => {
     it('should call action when it receives a messenger message event', async () => {
       await expectRouteMatchMessengerEvent({
@@ -720,4 +771,52 @@ describe('#messenger', () => {
       });
     });
   });
+
+  describe('#messenger.reaction', () => {
+    it('should call action when it receives a messenger reaction event', async () => {
+      await expectRouteMatchMessengerEvent({
+        route: messenger.reaction(Action),
+        event: messengerEventReactionReact,
+      });
+    });
+
+    it('should not call action when it receives a non-reaction event', async () => {
+      await expectRouteNotMatchMessengerEvent({
+        route: messenger.reaction(Action),
+        event: messengerEventTextMessage,
+      });
+    });
+
+    describe('#messenger.reaction.react', () => {
+      it('should call action when it receives a messenger reaction.react event', async () => {
+        await expectRouteMatchMessengerEvent({
+          route: messenger.reaction.react(Action),
+          event: messengerEventReactionReact,
+        });
+      });
+
+      it('should not call action when it receives a non-reaction.react event', async () => {
+        await expectRouteNotMatchMessengerEvent({
+          route: messenger.reaction.react(Action),
+          event: messengerEventReactionUnreact,
+        });
+      });
+    });
+
+    describe('#messenger.reaction.unreact', () => {
+      it('should call action when it receives a messenger reaction.unreact event', async () => {
+        await expectRouteMatchMessengerEvent({
+          route: messenger.reaction.unreact(Action),
+          event: messengerEventReactionUnreact,
+        });
+      });
+
+      it('should not call action when it receives a non-reaction.unreact event', async () => {
+        await expectRouteNotMatchMessengerEvent({
+          route: messenger.reaction.unreact(Action),
+          event: messengerEventReactionReact,
+        });
+      });
+    });
+  });
 });
diff --git a/packages/bottender/src/slack/__tests__/routes.spec.ts b/packages/bottender/src/slack/__tests__/routes.spec.ts
index 29918f3b..e714a140 100644
--- a/packages/bottender/src/slack/__tests__/routes.spec.ts
+++ b/packages/bottender/src/slack/__tests__/routes.spec.ts
@@ -116,6 +116,25 @@ describe('#slack', () => {
     });
   });
 
+  describe('#slack.any', () => {
+    it('should call action when it receives a slack event', async () => {
+      await expectRouteMatchSlackEvent({
+        route: slack.any(Action),
+        event: slackEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-slack event', async () => {
+      await expectRouteNotMatchContext({
+        route: slack.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#slack.message', () => {
     it('should call action when it receives a slack message event', async () => {
       await expectRouteMatchSlackEvent({
diff --git a/packages/bottender/src/telegram/__tests__/routes.spec.ts b/packages/bottender/src/telegram/__tests__/routes.spec.ts
index 112dea19..34c582d4 100644
--- a/packages/bottender/src/telegram/__tests__/routes.spec.ts
+++ b/packages/bottender/src/telegram/__tests__/routes.spec.ts
@@ -273,6 +273,25 @@ describe('#telegram', () => {
     });
   });
 
+  describe('#telegram.any', () => {
+    it('should call action when it receives a telegram event', async () => {
+      await expectRouteMatchTelegramEvent({
+        route: telegram.any(Action),
+        event: telegramEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-telegram event', async () => {
+      await expectRouteNotMatchContext({
+        route: telegram.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#telegram.message', () => {
     it('should call action when it receives a telegram message event', async () => {
       await expectRouteMatchTelegramEvent({
diff --git a/packages/bottender/src/viber/__tests__/routes.spec.ts b/packages/bottender/src/viber/__tests__/routes.spec.ts
index cce094a1..587fae13 100644
--- a/packages/bottender/src/viber/__tests__/routes.spec.ts
+++ b/packages/bottender/src/viber/__tests__/routes.spec.ts
@@ -163,6 +163,25 @@ describe('#viber', () => {
     });
   });
 
+  describe('#viber.any', () => {
+    it('should call action when it receives a viber event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.any(Action),
+        event: viberEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-viber event', async () => {
+      await expectRouteNotMatchContext({
+        route: viber.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#viber.message', () => {
     it('should call action when it receives a viber message event', async () => {
       await expectRouteMatchViberEvent({
diff --git a/packages/bottender/src/whatsapp/__tests__/routes.spec.ts b/packages/bottender/src/whatsapp/__tests__/routes.spec.ts
index c0c32f24..877de22e 100644
--- a/packages/bottender/src/whatsapp/__tests__/routes.spec.ts
+++ b/packages/bottender/src/whatsapp/__tests__/routes.spec.ts
@@ -160,6 +160,25 @@ describe('#whatsapp', () => {
     });
   });
 
+  describe('#whatsapp.any', () => {
+    it('should call action when it receives a whatsapp event', async () => {
+      await expectRouteMatchWhatsappEvent({
+        route: whatsapp.any(Action),
+        event: whatsappEventTextMessageReceived,
+      });
+    });
+
+    it('should not call action when it receives a non-whatsapp event', async () => {
+      await expectRouteNotMatchContext({
+        route: whatsapp.any(Action),
+        context: new TestContext({
+          client: {} as any,
+          event: {},
+        }),
+      });
+    });
+  });
+
   describe('#whatsapp.message', () => {
     it('should call action when it receives a whatsapp message event', async () => {
       await expectRouteMatchWhatsappEvent({