Repository: Yoctol/bottender (ts)

Issue:
way to interact with bot_message subtype
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

I'm trying to write a solution for clients where it is actually a feature if bots can communicate directly with each other. At the moment, this is not possible, since 'messages' with the 'bot_message' subtype and/or a 'bot_id' are filtered out entirely. 

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

I would like to have a solution where 'bot_message' is its own event type. Messages that are sent by this particular bot itself should be filtered out entirely, of course. But for the others, a user should have at the option to handle them in a way that suits their purpose

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

I tried to modify the rawEvent on the request object and also to override the _isBotRequest method on the connector. Both lead to very messy solutions.

**Additional context**
Add any other context or screenshots about the feature request here.


Pull request description:
feat(slack): add includeBotMessages option for interacting with bot_message
#625

Implement the following API to interact with `bot_message`:

Set `includeBotMessages` to `true`

```js
// bottender.config.js

module.exports = {
  // ...
  slack: {
    enabled: true,
    path: '/webhooks/slack',
    accessToken: process.env.SLACK_ACCESS_TOKEN,
    verificationToken: process.env.SLACK_VERIFICATION_TOKEN,
    includeBotMessages: true, // add this line
  },
};
```

Use `context.event.isBotMessage` to determine if the event is a bot message event:

```js
module.exports = function App(context) {
  if (context.event.isBotMessage) {
    console.log(context.event.rawEvent.botId);
  }
};
```

Patch:
diff --git a/packages/bottender/src/slack/SlackBot.ts b/packages/bottender/src/slack/SlackBot.ts
index 487bce2a..d4cb3590 100644
--- a/packages/bottender/src/slack/SlackBot.ts
+++ b/packages/bottender/src/slack/SlackBot.ts
@@ -21,19 +21,22 @@ export default class SlackBot extends Bot<
     verificationToken,
     origin,
     skipLegacyProfile,
+    includeBotMessages,
   }: {
     accessToken: string;
     sessionStore?: SessionStore;
     sync?: boolean;
     verificationToken?: string;
     origin?: string;
-    skipLegacyProfile?: boolean | null;
+    skipLegacyProfile?: boolean;
+    includeBotMessages?: boolean;
   }) {
     const connector = new SlackConnector({
       accessToken,
       verificationToken,
       origin,
       skipLegacyProfile,
+      includeBotMessages,
     });
     super({ connector, sessionStore, sync });
     this._accessToken = accessToken;
diff --git a/packages/bottender/src/slack/SlackConnector.ts b/packages/bottender/src/slack/SlackConnector.ts
index 470d2c38..c8c458d0 100644
--- a/packages/bottender/src/slack/SlackConnector.ts
+++ b/packages/bottender/src/slack/SlackConnector.ts
@@ -38,8 +38,9 @@ type EventsAPIBody = {
 export type SlackRequestBody = EventsAPIBody | { payload: string };
 
 type CommonConstructorOptions = {
-  skipLegacyProfile?: boolean | null;
+  skipLegacyProfile?: boolean;
   verificationToken?: string;
+  includeBotMessages?: boolean;
 };
 
 type ConstructorOptionsWithoutClient = {
@@ -63,8 +64,14 @@ export default class SlackConnector
 
   _skipLegacyProfile: boolean;
 
+  _includeBotMessages: boolean;
+
   constructor(options: ConstructorOptions) {
-    const { verificationToken, skipLegacyProfile } = options;
+    const {
+      verificationToken,
+      skipLegacyProfile,
+      includeBotMessages,
+    } = options;
     if ('client' in options) {
       this._client = options.client;
     } else {
@@ -83,15 +90,17 @@ export default class SlackConnector
 
     this._verificationToken = verificationToken || '';
 
-    this._skipLegacyProfile =
-      typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true;
-
     if (!this._verificationToken) {
       warning(
         false,
         '`verificationToken` is not set. Will bypass Slack event verification.\nPass in `verificationToken` to perform Slack event verification.'
       );
     }
+
+    this._skipLegacyProfile =
+      typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true;
+
+    this._includeBotMessages = includeBotMessages || false;
   }
 
   _getRawEventFromRequest(body: SlackRequestBody): SlackRawEvent {
@@ -304,8 +313,8 @@ export default class SlackConnector
   mapRequestToEvents(body: SlackRequestBody): SlackEvent[] {
     const rawEvent = this._getRawEventFromRequest(body);
 
-    if (this._isBotEventRequest(body)) {
-      return []; // FIXME
+    if (!this._includeBotMessages && this._isBotEventRequest(body)) {
+      return [];
     }
 
     return [new SlackEvent(rawEvent)];
diff --git a/packages/bottender/src/slack/SlackEvent.ts b/packages/bottender/src/slack/SlackEvent.ts
index 3602eb0e..68e48cfb 100644
--- a/packages/bottender/src/slack/SlackEvent.ts
+++ b/packages/bottender/src/slack/SlackEvent.ts
@@ -278,6 +278,14 @@ export default class SlackEvent implements Event<SlackRawEvent> {
     }
     return null;
   }
+
+  /**
+   * Determine if the event is a bot message event.
+   *
+   */
+  get isBotMessage(): boolean {
+    return (this._rawEvent as any).subtype === 'bot_message';
+  }
 }
 
 // https://api.slack.com/events


Test patch:
diff --git a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
index 318027e8..44f0a4c1 100644
--- a/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
+++ b/packages/bottender/src/slack/__tests__/SlackConnector.spec.ts
@@ -150,6 +150,7 @@ const RtmMessage = {
 function setup({
   verificationToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
   skipLegacyProfile,
+  includeBotMessages,
 } = {}) {
   const mockSlackOAuthClient = {
     getUserInfo: jest.fn(),
@@ -164,6 +165,7 @@ function setup({
       accessToken,
       verificationToken,
       skipLegacyProfile,
+      includeBotMessages,
     }),
     mockSlackOAuthClient,
   };
@@ -396,6 +398,23 @@ describe('#mapRequestToEvents', () => {
     expect(events).toHaveLength(1);
     expect(events[0]).toBeInstanceOf(SlackEvent);
   });
+
+  it('should not include bot message by default', () => {
+    const { connector } = setup();
+    const events = connector.mapRequestToEvents(botRequest.body);
+
+    expect(events).toHaveLength(0);
+  });
+
+  it('should include bot message when includeBotMessages is true', () => {
+    const { connector } = setup({
+      includeBotMessages: true,
+    });
+    const events = connector.mapRequestToEvents(botRequest.body);
+
+    expect(events).toHaveLength(1);
+    expect(events[0]).toBeInstanceOf(SlackEvent);
+  });
 });
 
 describe('#createContext', () => {
diff --git a/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts b/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts
index 1016b6a7..6f6fa0ad 100644
--- a/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts
+++ b/packages/bottender/src/slack/__tests__/SlackEvent.spec.ts
@@ -272,6 +272,16 @@ const message = {
   ts: '1355517523.000005',
 };
 
+const botMessage = {
+  type: 'message',
+  subtype: 'bot_message',
+  ts: '1358877455.000010',
+  text: 'Pushing is the answer',
+  botId: 'BB12033',
+  username: 'github',
+  icons: {},
+};
+
 const channelsMessage = {
   type: 'message',
   channel: 'C2147483705',
@@ -894,3 +904,8 @@ describe('interactive message event', () => {
     expect(new SlackEvent(message).action).toEqual(null);
   });
 });
+
+it('#isBotMessage', () => {
+  expect(new SlackEvent(message).isBotMessage).toEqual(false);
+  expect(new SlackEvent(botMessage).isBotMessage).toEqual(true);
+});