Repository: Yoctol/bottender
Pull request #606 (2019-12-26 11:05:34)

feat(router): add slack routes
Add slack routes:

```js
const { router, slack } = require('bottender/router');

function Action() {
  // ...
}

function App() {
  return router([
    slack.message(Action),
    slack.event('pin_added', Action),
    slack.event('star_added', Action),
    slack(Action),
  ]);
}
```

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

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

```diff
@@               Coverage Diff               @@
##           release/1.1     #606      +/-   ##
===============================================
+ Coverage        80.77%   80.82%   +0.05%     
===============================================
  Files               94       95       +1     
  Lines             4239     4251      +12     
  Branches          1041     1032       -9     
===============================================
+ Hits              3424     3436      +12     
- Misses             775      779       +4     
+ Partials            40       36       -4
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/606?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/bottender/src/slack/SlackConnector.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zbGFjay9TbGFja0Nvbm5lY3Rvci50cw==) | `92.15% <ø> (ø)` | :arrow_up: |
| [packages/bottender/src/slack/SlackContext.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zbGFjay9TbGFja0NvbnRleHQudHM=) | `88.73% <ø> (ø)` | :arrow_up: |
| [packages/bottender/src/slack/SlackEvent.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zbGFjay9TbGFja0V2ZW50LnRz) | `97.36% <ø> (ø)` | :arrow_up: |
| [packages/bottender/src/slack/routes.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zbGFjay9yb3V0ZXMudHM=) | `100% <100%> (ø)` | |
| [packages/bottender/src/router/index.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9yb3V0ZXIvaW5kZXgudHM=) | `90.54% <100%> (+0.26%)` | :arrow_up: |
| [packages/create-bottender-app/src/shared/log.ts](https://codecov.io/gh/Yoctol/bottender/pull/606/diff?src=pr&el=tree#diff-cGFja2FnZXMvY3JlYXRlLWJvdHRlbmRlci1hcHAvc3JjL3NoYXJlZC9sb2cudHM=) | `0% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/606?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/606?src=pr&el=footer). Last update [62e81ee...525d822](https://codecov.io/gh/Yoctol/bottender/pull/606?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/bottender/src/router/index.ts b/packages/bottender/src/router/index.ts
index 4088f659..da4daf1a 100644
--- a/packages/bottender/src/router/index.ts
+++ b/packages/bottender/src/router/index.ts
@@ -1,6 +1,7 @@
 import Context from '../context/Context';
 import line from '../line/routes';
 import messenger from '../messenger/routes';
+import slack from '../slack/routes';
 import telegram from '../telegram/routes';
 import viber from '../viber/routes';
 import { Action, Client, Event, Props } from '../types';
@@ -203,6 +204,7 @@ export {
   platform,
   line,
   messenger,
+  slack,
   telegram,
   viber,
 };
diff --git a/packages/bottender/src/slack/SlackConnector.ts b/packages/bottender/src/slack/SlackConnector.ts
index 3624ecbc..b2e9555e 100644
--- a/packages/bottender/src/slack/SlackConnector.ts
+++ b/packages/bottender/src/slack/SlackConnector.ts
@@ -12,7 +12,7 @@ import { RequestContext } from '../types';
 import SlackContext from './SlackContext';
 import SlackEvent, {
   BlockActionEvent,
-  EventAPITypes,
+  EventTypes,
   InteractiveMessageEvent,
   Message,
   SlackRawEvent,
@@ -27,7 +27,7 @@ type EventsAPIBody = {
   token: string;
   teamId: string;
   apiAppId: string;
-  type: EventAPITypes;
+  type: EventTypes;
   event: Message;
   authedUsers: string[];
   eventId: string;
diff --git a/packages/bottender/src/slack/SlackContext.ts b/packages/bottender/src/slack/SlackContext.ts
index f8c73425..4fcaca7a 100644
--- a/packages/bottender/src/slack/SlackContext.ts
+++ b/packages/bottender/src/slack/SlackContext.ts
@@ -13,7 +13,7 @@ import SlackEvent from './SlackEvent';
 type Options = {
   client: SlackOAuthClient;
   event: SlackEvent;
-  session: Session | null;
+  session?: Session | null;
   initialState?: Record<string, any> | null;
   requestContext?: RequestContext;
   emitter?: EventEmitter | null;
diff --git a/packages/bottender/src/slack/SlackEvent.ts b/packages/bottender/src/slack/SlackEvent.ts
index afdb3280..63c76001 100644
--- a/packages/bottender/src/slack/SlackEvent.ts
+++ b/packages/bottender/src/slack/SlackEvent.ts
@@ -2,7 +2,7 @@ import pascalCase from 'pascal-case';
 
 import { Event } from '../context/Event';
 
-export type EventAPITypes =
+export type EventTypes =
   | 'message'
   | 'app_uninstalled'
   | 'channel_archive'
@@ -58,7 +58,7 @@ export type EventAPITypes =
   | 'user_change';
 
 export type Message = {
-  type: EventAPITypes;
+  type: EventTypes;
   subtype?: string;
   channel: string;
   user: string;
diff --git a/packages/bottender/src/slack/routes.ts b/packages/bottender/src/slack/routes.ts
new file mode 100644
index 00000000..c27c7bb1
--- /dev/null
+++ b/packages/bottender/src/slack/routes.ts
@@ -0,0 +1,58 @@
+import Context from '../context/Context';
+import { Action, Client, Event } from '../types';
+import { RoutePredicate, route } from '../router';
+
+import { EventTypes } from './SlackEvent';
+
+type Route = <C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) => {
+  predicate: RoutePredicate<C, E>;
+  action: Action<C, E>;
+};
+
+type Slack = Route & {
+  message: Route;
+  event: <C extends Client = any, E extends Event = any>(
+    eventType: EventTypes,
+    action: Action<C, E>
+  ) => {
+    predicate: RoutePredicate<C, E>;
+    action: Action<C, E>;
+  };
+};
+
+const slack: Slack = <C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) => {
+  return route(context => context.platform === 'slack', action);
+};
+
+function message<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'slack' && context.event.isMessage,
+    action
+  );
+}
+
+slack.message = message;
+
+function event<C extends Client = any, E extends Event = any>(
+  eventType: EventTypes,
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'slack' && context.event.rawEvent.type === eventType,
+    action
+  );
+}
+
+slack.event = event;
+
+// TODO: support slack.command
+
+export default slack;


Test patch:
diff --git a/packages/bottender/src/slack/__tests__/routes.spec.ts b/packages/bottender/src/slack/__tests__/routes.spec.ts
new file mode 100644
index 00000000..51916240
--- /dev/null
+++ b/packages/bottender/src/slack/__tests__/routes.spec.ts
@@ -0,0 +1,134 @@
+import Context from '../../context/Context';
+import SlackContext from '../SlackContext';
+import SlackEvent from '../SlackEvent';
+import router from '../../router';
+import slack from '../routes';
+import { run } from '../../bot/Bot';
+
+const slackEventTextMessage = new SlackEvent({
+  type: 'message',
+  channel: 'C2147483705',
+  user: 'U2147483697',
+  text: 'Hello world',
+  ts: '1355517523.000005',
+});
+
+const slackEventPinAdded = new SlackEvent({
+  type: 'pin_added',
+  user: 'U024BE7LH',
+  channelId: 'C02ELGNBH',
+  item: {},
+  eventTs: '1360782804.083113',
+});
+
+async function Action(context) {
+  await context.sendText('hello');
+}
+
+async function expectRouteMatchContext({ route, context }) {
+  const Router = router([route]);
+
+  const app = run(Router);
+
+  context.sendText = jest.fn();
+
+  await app(context);
+
+  expect(context.sendText).toBeCalledWith('hello');
+}
+
+async function expectRouteMatchSlackEvent({ route, event }) {
+  const context = new SlackContext({
+    client: {} as any,
+    event,
+  });
+
+  await expectRouteMatchContext({
+    route,
+    context,
+  });
+}
+
+async function expectRouteNotMatchContext({ route, context }) {
+  const Router = router([route]);
+
+  const app = run(Router);
+
+  context.sendText = jest.fn();
+
+  await app(context);
+
+  expect(context.sendText).not.toBeCalledWith('hello');
+}
+
+async function expectRouteNotMatchSlackEvent({ route, event }) {
+  const context = new SlackContext({
+    client: {} as any,
+    event,
+  });
+
+  await expectRouteNotMatchContext({
+    route,
+    context,
+  });
+}
+
+class TestContext extends Context<any, any> {
+  get platform() {
+    return 'test';
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-empty-function
+  sendText() {}
+}
+
+describe('#slack', () => {
+  it('should call action when it receives a slack event', async () => {
+    await expectRouteMatchSlackEvent({
+      route: slack(Action),
+      event: slackEventTextMessage,
+    });
+  });
+
+  it('should not call action when it receives a non-slack event', async () => {
+    await expectRouteNotMatchContext({
+      route: slack(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({
+        route: slack.message(Action),
+        event: slackEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-message event', async () => {
+      await expectRouteNotMatchSlackEvent({
+        route: slack.message(Action),
+        event: slackEventPinAdded,
+      });
+    });
+  });
+
+  describe('#slack.event', () => {
+    it('should call action when it receives a slack event event', async () => {
+      await expectRouteMatchSlackEvent({
+        route: slack.event('pin_added', Action),
+        event: slackEventPinAdded,
+      });
+    });
+
+    it('should not call action when it receives a non-event event', async () => {
+      await expectRouteNotMatchSlackEvent({
+        route: slack.event('pin_added', Action),
+        event: slackEventTextMessage,
+      });
+    });
+  });
+});