Repository: Yoctol/bottender (ts)

Issue:
[Feature Request] - Get request parameters when the handler callback triggered
I want to get some information like that req.hostname in express when the onEvent() callback triggered. Any easy way?
Proposal: also pass URL query into handler in server adapters
We can merge query and body in `createMiddleware`:

```js
{
  ...query,
  ...body,
}
```

It's useful in custom connectors.


Pull request description:
implement context.requestContext
https://github.com/Yoctol/bottender/issues/285#issuecomment-397600779

Express:

```jd
context.requestContext.req
```

Koa:

```js
context.requestContext.request
```

close #285, close #211

Patch:
diff --git a/src/bot/Bot.js b/src/bot/Bot.js
index 26665bf6..4db3961f 100644
--- a/src/bot/Bot.js
+++ b/src/bot/Bot.js
@@ -21,7 +21,10 @@ function createMemorySessionStore(): SessionStore {
   return new CacheBasedSessionStore(cache, MINUTES_IN_ONE_YEAR);
 }
 
-type RequestHandler = (body: Object) => void | Promise<void>;
+type RequestHandler = (
+  body: Object,
+  requestContext?: ?Object
+) => void | Promise<void>;
 
 export default class Bot {
   _sessions: SessionStore;
@@ -92,7 +95,7 @@ export default class Bot {
       );
     }
 
-    return async body => {
+    return async (body: Object, requestContext?: ?Object) => {
       if (!body) {
         throw new Error('Bot.createRequestHandler: Missing argument.');
       }
@@ -149,6 +152,7 @@ export default class Bot {
             event,
             session: ((session: any): Session),
             initialState: this._initialState,
+            requestContext,
           }),
         {
           concurrency: 5,
diff --git a/src/bot/Connector.js b/src/bot/Connector.js
index d1bcc2e6..547eb50c 100644
--- a/src/bot/Connector.js
+++ b/src/bot/Connector.js
@@ -10,6 +10,7 @@ export interface Connector<B> {
   createContext(params: {
     event: any,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): any;
 }
diff --git a/src/bot/ConsoleConnector.js b/src/bot/ConsoleConnector.js
index 31f2f286..b1e8dc36 100644
--- a/src/bot/ConsoleConnector.js
+++ b/src/bot/ConsoleConnector.js
@@ -64,20 +64,15 @@ export default class ConsoleConnector implements Connector<ConsoleRequestBody> {
     return [new ConsoleEvent(body)];
   }
 
-  createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  createContext(params: {
     event: ConsoleEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }) {
     return new ConsoleContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
       fallbackMethods: this._fallbackMethods,
     });
   }
diff --git a/src/bot/LineConnector.js b/src/bot/LineConnector.js
index 59a93a8b..d49131c5 100644
--- a/src/bot/LineConnector.js
+++ b/src/bot/LineConnector.js
@@ -217,20 +217,15 @@ export default class LineConnector implements Connector<LineRequestBody> {
       .map(e => new LineEvent(e));
   }
 
-  createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  createContext(params: {
     event: LineEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): LineContext {
     return new LineContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
       shouldBatch: this._shouldBatch,
       sendMethod: this._sendMethod,
     });
diff --git a/src/bot/MessengerConnector.js b/src/bot/MessengerConnector.js
index a9e6e19a..24543f5b 100644
--- a/src/bot/MessengerConnector.js
+++ b/src/bot/MessengerConnector.js
@@ -284,18 +284,15 @@ export default class MessengerConnector
     );
   }
 
-  async createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  async createContext(params: {
     event: MessengerEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): Promise<MessengerContext> {
     let customAccessToken;
     if (this._mapPageToAccessToken) {
-      const { rawEvent } = event;
+      const { rawEvent } = params.event;
 
       let pageId = null;
 
@@ -312,10 +309,8 @@ export default class MessengerConnector
       }
     }
     return new MessengerContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
       customAccessToken,
       batchQueue: this._batchQueue,
     });
diff --git a/src/bot/SlackConnector.js b/src/bot/SlackConnector.js
index cb987239..ac897493 100644
--- a/src/bot/SlackConnector.js
+++ b/src/bot/SlackConnector.js
@@ -206,20 +206,15 @@ export default class SlackConnector implements Connector<SlackRequestBody> {
     return [new SlackEvent(rawEvent)];
   }
 
-  createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  createContext(params: {
     event: SlackEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): SlackContext {
     return new SlackContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
     });
   }
 
diff --git a/src/bot/TelegramConnector.js b/src/bot/TelegramConnector.js
index 8c22688a..79c378a9 100644
--- a/src/bot/TelegramConnector.js
+++ b/src/bot/TelegramConnector.js
@@ -147,20 +147,15 @@ export default class TelegramConnector
     return [new TelegramEvent(rawEvent)];
   }
 
-  createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  createContext(params: {
     event: TelegramEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): TelegramContext {
     return new TelegramContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
     });
   }
 }
diff --git a/src/bot/ViberConnector.js b/src/bot/ViberConnector.js
index 4d996409..e60f572e 100644
--- a/src/bot/ViberConnector.js
+++ b/src/bot/ViberConnector.js
@@ -100,20 +100,15 @@ export default class ViberConnector implements Connector<ViberRequestBody> {
     return [new ViberEvent(rawEvent)];
   }
 
-  createContext({
-    event,
-    session,
-    initialState,
-  }: {
+  createContext(params: {
     event: ViberEvent,
     session: ?Session,
-    initialState: Object,
+    initialState: ?Object,
+    requestContext: ?Object,
   }): ViberContext {
     return new ViberContext({
+      ...params,
       client: this._client,
-      event,
-      session,
-      initialState,
     });
   }
 
diff --git a/src/context/ConsoleContext.js b/src/context/ConsoleContext.js
index 789bf4ad..3502fb7f 100644
--- a/src/context/ConsoleContext.js
+++ b/src/context/ConsoleContext.js
@@ -13,6 +13,7 @@ type Options = {|
   event: ConsoleEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
   fallbackMethods: boolean,
 |};
 
@@ -31,9 +32,10 @@ export default class ConsoleContext extends Context implements PlatformContext {
     event,
     session,
     initialState,
+    requestContext,
     fallbackMethods,
   }: Options) {
-    super({ client, event, session, initialState });
+    super({ client, event, session, initialState, requestContext });
     if (fallbackMethods) {
       // $FlowExpectedError
       return new Proxy(this, {
diff --git a/src/context/Context.js b/src/context/Context.js
index b0021ee8..967f5387 100644
--- a/src/context/Context.js
+++ b/src/context/Context.js
@@ -11,6 +11,7 @@ type Options = {|
   event: any,
   session: ?any,
   initialState: ?Object,
+  requestContext: ?Object,
 |};
 
 type Response = {
@@ -27,14 +28,22 @@ export default class Context {
   _event: Object;
   _session: ?Object;
   _initialState: ?Object;
+  _requestContext: ?Object;
 
   response: Response;
 
-  constructor({ client, event, session, initialState }: Options) {
+  constructor({
+    client,
+    event,
+    session,
+    initialState,
+    requestContext,
+  }: Options) {
     this._client = client;
     this._event = event;
     this._session = session;
     this._initialState = initialState || {};
+    this._requestContext = requestContext;
 
     debug('bottender: context created with rawEvent');
     debug(JSON.stringify(this._event.rawEvent, null, 2));
@@ -67,6 +76,14 @@ export default class Context {
     return this._event;
   }
 
+  /**
+   * The context of request.
+   *
+   */
+  get requestContext(): ?Object {
+    return this._requestContext;
+  }
+
   /**
    * The session state of the context.
    *
diff --git a/src/context/LineContext.js b/src/context/LineContext.js
index 1d4ad1b0..decd8f6a 100644
--- a/src/context/LineContext.js
+++ b/src/context/LineContext.js
@@ -17,6 +17,7 @@ type Options = {|
   event: LineEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
   shouldBatch: ?boolean,
   sendMethod: ?string,
 |};
@@ -39,10 +40,11 @@ class LineContext extends Context implements PlatformContext {
     event,
     session,
     initialState,
+    requestContext,
     shouldBatch,
     sendMethod,
   }: Options) {
-    super({ client, event, session, initialState });
+    super({ client, event, session, initialState, requestContext });
 
     this._shouldBatch = shouldBatch || false;
     this._sendMethod = sendMethod || 'push';
diff --git a/src/context/MessengerContext.js b/src/context/MessengerContext.js
index abcc7847..5d35afc6 100644
--- a/src/context/MessengerContext.js
+++ b/src/context/MessengerContext.js
@@ -15,6 +15,7 @@ type Options = {|
   event: MessengerEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
   customAccessToken: ?string,
   batchQueue: ?Object,
 |};
@@ -31,10 +32,11 @@ class MessengerContext extends Context implements PlatformContext {
     event,
     session,
     initialState,
+    requestContext,
     customAccessToken,
     batchQueue,
   }: Options) {
-    super({ client, event, session, initialState });
+    super({ client, event, session, initialState, requestContext });
     this._customAccessToken = customAccessToken;
     this._batchQueue = batchQueue;
   }
diff --git a/src/context/SlackContext.js b/src/context/SlackContext.js
index 36dba971..d9e63970 100644
--- a/src/context/SlackContext.js
+++ b/src/context/SlackContext.js
@@ -15,6 +15,7 @@ type Options = {|
   event: SlackEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
 |};
 
 export default class SlackContext extends Context implements PlatformContext {
@@ -22,8 +23,14 @@ export default class SlackContext extends Context implements PlatformContext {
   _event: SlackEvent;
   _session: ?Session;
 
-  constructor({ client, event, session, initialState }: Options) {
-    super({ client, event, session, initialState });
+  constructor({
+    client,
+    event,
+    session,
+    initialState,
+    requestContext,
+  }: Options) {
+    super({ client, event, session, initialState, requestContext });
   }
 
   /**
diff --git a/src/context/TelegramContext.js b/src/context/TelegramContext.js
index f4a5ebaf..b081d92e 100644
--- a/src/context/TelegramContext.js
+++ b/src/context/TelegramContext.js
@@ -15,6 +15,7 @@ type Options = {|
   event: TelegramEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
 |};
 
 class TelegramContext extends Context implements PlatformContext {
@@ -22,8 +23,14 @@ class TelegramContext extends Context implements PlatformContext {
   _event: TelegramEvent;
   _session: ?Session;
 
-  constructor({ client, event, session, initialState }: Options) {
-    super({ client, event, session, initialState });
+  constructor({
+    client,
+    event,
+    session,
+    initialState,
+    requestContext,
+  }: Options) {
+    super({ client, event, session, initialState, requestContext });
   }
 
   /**
diff --git a/src/context/ViberContext.js b/src/context/ViberContext.js
index 12c4f271..38b031c2 100644
--- a/src/context/ViberContext.js
+++ b/src/context/ViberContext.js
@@ -15,6 +15,7 @@ type Options = {|
   event: ViberEvent,
   session: ?Session,
   initialState: ?Object,
+  requestContext: ?Object,
 |};
 
 class ViberContext extends Context implements PlatformContext {
@@ -22,8 +23,14 @@ class ViberContext extends Context implements PlatformContext {
   _event: ViberEvent;
   _session: ?Session;
 
-  constructor({ client, event, session, initialState }: Options) {
-    super({ client, event, session, initialState });
+  constructor({
+    client,
+    event,
+    session,
+    initialState,
+    requestContext,
+  }: Options) {
+    super({ client, event, session, initialState, requestContext });
   }
 
   /**
diff --git a/src/express/createMiddleware.js b/src/express/createMiddleware.js
index 690f9fc0..635eda67 100644
--- a/src/express/createMiddleware.js
+++ b/src/express/createMiddleware.js
@@ -12,10 +12,16 @@ function createMiddleware(bot) {
         'createMiddleware(): Missing query and body, you may need a body parser. Use `body-parser` or other similar package before this middleware.'
       );
     }
-    const response = await requestHandler({
-      ...req.query,
-      ...req.body,
-    });
+    const response = await requestHandler(
+      {
+        ...req.query,
+        ...req.body,
+      },
+      {
+        req,
+        res,
+      }
+    );
     if (response) {
       res.set(response.headers || {});
       res.status(response.status || 200);
diff --git a/src/koa/createMiddleware.js b/src/koa/createMiddleware.js
index 2669750a..c7d12adc 100644
--- a/src/koa/createMiddleware.js
+++ b/src/koa/createMiddleware.js
@@ -2,17 +2,21 @@ import isEmpty from 'lodash/isEmpty';
 
 function createMiddleware(bot) {
   const requestHandler = bot.createRequestHandler();
-  return async ({ request, response }) => {
+  return async ctx => {
+    const { request, response } = ctx;
     if (isEmpty(request.query) && !request.body) {
       throw new Error(
         'createMiddleware(): Missing query and body, you may need a body parser. Use `koa-bodyparser` or other similar package before this middleware.'
       );
     }
 
-    const res = await requestHandler({
-      ...request.query,
-      ...request.body,
-    });
+    const res = await requestHandler(
+      {
+        ...request.query,
+        ...request.body,
+      },
+      ctx
+    );
     if (res) {
       response.set(res.headers || {});
       response.status = res.status || 200;
diff --git a/src/micro/createRequestHandler.js b/src/micro/createRequestHandler.js
index 7c0d56eb..13f31e29 100644
--- a/src/micro/createRequestHandler.js
+++ b/src/micro/createRequestHandler.js
@@ -56,10 +56,16 @@ function createRequestHandler(bot, config = {}) {
         }
         const { query } = url.parse(req.url, true);
 
-        const response = await requestHandler({
-          ...query,
-          ...body,
-        });
+        const response = await requestHandler(
+          {
+            ...query,
+            ...body,
+          },
+          {
+            req,
+            res,
+          }
+        );
         if (response) {
           Object.keys(response.headers).forEach(key => {
             res.setHeader(key, response.headers[key]);
diff --git a/src/restify/createMiddleware.js b/src/restify/createMiddleware.js
index 0c57e409..4bae6171 100644
--- a/src/restify/createMiddleware.js
+++ b/src/restify/createMiddleware.js
@@ -9,10 +9,16 @@ function createMiddleware(bot) {
       );
     }
 
-    const response = await requestHandler({
-      ...req.query,
-      ...req.body,
-    });
+    const response = await requestHandler(
+      {
+        ...req.query,
+        ...req.body,
+      },
+      {
+        req,
+        res,
+      }
+    );
     if (response) {
       res.send(response.status || 200, response.body || '', response.headers);
     } else {


Test patch:
diff --git a/src/bot/__tests__/Bot.spec.js b/src/bot/__tests__/Bot.spec.js
index 334e1467..5f1f3338 100644
--- a/src/bot/__tests__/Bot.spec.js
+++ b/src/bot/__tests__/Bot.spec.js
@@ -441,7 +441,7 @@ describe('#setInitialState', () => {
     expect(bot.setInitialState({})).toBe(bot);
   });
 
-  it('initialState should be pass to createContext', async () => {
+  it('initialState should be passed to createContext', async () => {
     const event = {};
     const connector = {
       platform: 'any',
@@ -483,6 +483,38 @@ describe('#setInitialState', () => {
   });
 });
 
+describe('request context', () => {
+  it('requestContext should be passed to createContext', async () => {
+    const event = {};
+    const connector = {
+      platform: 'any',
+      getUniqueSessionKey: jest.fn(),
+      shouldSessionUpdate: jest.fn(),
+      updateSession: jest.fn(),
+      mapRequestToEvents: jest.fn(() => [event]),
+      createContext: jest.fn(() => ({ event, session: undefined })),
+    };
+
+    const { bot } = setup({ connector });
+
+    connector.getUniqueSessionKey.mockReturnValue('__id__');
+
+    bot.onEvent(() => {});
+
+    const requestHandler = bot.createRequestHandler();
+
+    const body = {};
+    const requestContext = { req: {}, res: {} };
+    await requestHandler(body, requestContext);
+
+    expect(connector.createContext).toBeCalledWith(
+      expect.objectContaining({
+        requestContext,
+      })
+    );
+  });
+});
+
 describe('context lifecycle', () => {
   it('should call context.handlerDidEnd when it exists', async () => {
     const event = {};
diff --git a/src/context/__tests__/Context.spec.js b/src/context/__tests__/Context.spec.js
index 0a4b5417..2cc623c2 100644
--- a/src/context/__tests__/Context.spec.js
+++ b/src/context/__tests__/Context.spec.js
@@ -15,12 +15,14 @@ function setup({
   event = {},
   session = {},
   initialState = {},
+  requestContext,
 } = {}) {
   const context = new Context({
     client,
     event,
     session,
     initialState,
+    requestContext,
   });
   return {
     context,
@@ -157,3 +159,21 @@ describe('state', () => {
     });
   });
 });
+
+describe('request context', () => {
+  describe('#requestContext', () => {
+    it('should be empty when no requestContext provided', () => {
+      const { context } = setup({ requestContext: undefined });
+
+      expect(context.requestContext).toBeUndefined();
+    });
+
+    it('should support get requestContext', () => {
+      const requestContext = { req: {}, res: {} };
+
+      const { context } = setup({ requestContext });
+
+      expect(context.requestContext).toBe(requestContext);
+    });
+  });
+});
diff --git a/src/context/__tests__/LineContext.spec.js b/src/context/__tests__/LineContext.spec.js
index 14f1a29f..6d6e70b4 100644
--- a/src/context/__tests__/LineContext.spec.js
+++ b/src/context/__tests__/LineContext.spec.js
@@ -64,6 +64,7 @@ const groupSession = {
 
 const setup = ({
   session = userSession,
+  requestContext,
   shouldBatch = false,
   sendMethod,
 } = {}) => {
@@ -72,6 +73,7 @@ const setup = ({
     client,
     event: new LineEvent(rawEvent),
     session,
+    requestContext,
     shouldBatch,
     sendMethod,
   });
diff --git a/src/express/__tests__/createMiddleware.spec.js b/src/express/__tests__/createMiddleware.spec.js
index 86426f01..588a8576 100644
--- a/src/express/__tests__/createMiddleware.spec.js
+++ b/src/express/__tests__/createMiddleware.spec.js
@@ -49,7 +49,7 @@ it('should merge query and body then pass into requestHandler', async () => {
 
   await middleware(req, res, next);
 
-  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 });
+  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 }, { req, res });
 });
 
 it('should overwrite query value if this key exists in body', async () => {
@@ -70,7 +70,7 @@ it('should overwrite query value if this key exists in body', async () => {
 
   await middleware(req, res, next);
 
-  expect(requestHandler).toBeCalledWith({ a: 2 });
+  expect(requestHandler).toBeCalledWith({ a: 2 }, { req, res });
 });
 
 it('should response 200 if there is response return from requestHandler', async () => {
diff --git a/src/koa/__tests__/createMiddleware.spec.js b/src/koa/__tests__/createMiddleware.spec.js
index 1988af3b..2e515730 100644
--- a/src/koa/__tests__/createMiddleware.spec.js
+++ b/src/koa/__tests__/createMiddleware.spec.js
@@ -36,10 +36,11 @@ it('should merge query and body then pass into requestHandler', async () => {
     body: { b: 2 },
   };
   const response = {};
+  const ctx = { request, response };
 
-  await middleware({ request, response });
+  await middleware(ctx);
 
-  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 });
+  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 }, ctx);
 });
 
 it('should overwrite query value if this key exists in body', async () => {
@@ -53,10 +54,11 @@ it('should overwrite query value if this key exists in body', async () => {
     body: { a: 2 },
   };
   const response = {};
+  const ctx = { request, response };
 
-  await middleware({ request, response });
+  await middleware(ctx);
 
-  expect(requestHandler).toBeCalledWith({ a: 2 });
+  expect(requestHandler).toBeCalledWith({ a: 2 }, ctx);
 });
 
 it('should response 200 if there is response return from requestHandler', async () => {
diff --git a/src/micro/__tests__/createRequestHandler.spec.js b/src/micro/__tests__/createRequestHandler.spec.js
index bb8f8dfd..fba3b317 100644
--- a/src/micro/__tests__/createRequestHandler.spec.js
+++ b/src/micro/__tests__/createRequestHandler.spec.js
@@ -251,7 +251,7 @@ it('should merge query and body then pass into requestHandler', async () => {
 
   await microRequestHandler(req, res);
 
-  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 });
+  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 }, { req, res });
 });
 
 it('should overwrite query value if this key exists in body', async () => {
@@ -269,7 +269,7 @@ it('should overwrite query value if this key exists in body', async () => {
 
   await microRequestHandler(req, res);
 
-  expect(requestHandler).toBeCalledWith({ a: 2 });
+  expect(requestHandler).toBeCalledWith({ a: 2 }, { req, res });
 });
 
 it('should response 200 if there is response return from requestHandler', async () => {
diff --git a/src/restify/__tests__/createMiddleware.spec.js b/src/restify/__tests__/createMiddleware.spec.js
index ea14fb62..e1aa0c7c 100644
--- a/src/restify/__tests__/createMiddleware.spec.js
+++ b/src/restify/__tests__/createMiddleware.spec.js
@@ -46,7 +46,7 @@ it('should merge query and body then pass into requestHandler', async () => {
 
   await middleware(req, res, next);
 
-  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 });
+  expect(requestHandler).toBeCalledWith({ a: '1', b: 2 }, { req, res });
 });
 
 it('should overwrite query value if this key exists in body', async () => {
@@ -66,7 +66,7 @@ it('should overwrite query value if this key exists in body', async () => {
 
   await middleware(req, res, next);
 
-  expect(requestHandler).toBeCalledWith({ a: 2 });
+  expect(requestHandler).toBeCalledWith({ a: 2 }, { req, res });
 });
 
 it('should response 200 if there is response return from requestHandler', async () => {
diff --git a/src/test-utils/ContextSimulator.js b/src/test-utils/ContextSimulator.js
index 53c4d223..d3952be5 100644
--- a/src/test-utils/ContextSimulator.js
+++ b/src/test-utils/ContextSimulator.js
@@ -120,6 +120,7 @@ class ContextSimulator {
         _state: state || this._initialState,
       },
       initialState: this._initialState,
+      requestContext: {},
     });
 
     context.platform = 'other';