Azure Bot ServiceのBot Framework SDKのkoa対応

最近はBot Framework v4を利用して、Botアプリケーションを作成しています。
Botアプリケーションを作成するには、SDKを利用するのですが、expressrestifyくらいしかWebアプリケーションのフレームワークが対応してないような気がします。

koaの場合

例えば、BotFrameworkAdapter.processActivity の要求にレスポンスオブジェクトを渡す必要があるのですが、 koacontext.response を渡すと status、send、end あたりのメソッドがない為に、ランタイムエラーが発生します。

これは、expressrestifyのレスポンスオブジェクトのインターフェースをBotFrameworkAdapterが期待しているからです。

また、koaの実装の場合、ミドルウェアを一通り処理した後に最終的な応答を返すので、サンプルでよくある実装をすると応答を正常に返せません。

// よくみるサンプルコード
// express/restifyの場合
app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await bot.run(context);
  });
});

Bot Emulatorを使用してデバッグすると、404を返した後に200を返したりします。

f:id:holyshared:20200802185533p:plain
Emulatorのログ

// 素直にkoaでサンプルのコードを実装した例
// routerはkoa-routerのインスタンス
router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const { request: req, response: res } = ctx;
  adaptor.processActivity(req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

この現象の理由は、processActivityをawaitしてない為、コールバック関数を抜けた後に、404を直ぐに返し、その後にBotのメッセージ応答が正常に終了し、200などの応答を返す為だと思います。

express/restifyの場合はawaitしなくてもレスポンスオブジェクトのendを実行したタイミングで応答が完了するので問題ないのですが、koaの場合はそうではなく、最終的な応答を返すのがフレームワーク側 なのでうまく行きません。

サンプルのコードとかだと、ただのコールバック関数で処理しているだけに見えたので若干困りました。

最終的な対応

原因がわかったので、適当なexpress/restifyのレスポンスオブジェクトを渡しつつ、processActivityをawaitして対応することにしました。

スマートな解決方法ではないですが、テキストメッセージをユーザーに送るくらいだったらこれで良さそうです。
また、WebResponseのインターフェース仕様を読む限り、end、send、statusだけ実装されてれば良いぽいのでこれであってそうではあります。

import Koa from 'koa';
import Router, { RouterContext } from 'koa-router';
import { BotFrameworkAdapter, TurnContext, WebResponse } from 'botbuilder';
import { Bot } from './bot';

const app = new Koa();
const router = new Router();

const adaptor = new BotFrameworkAdapter({
  appId: process.env.BOT_APP_CLIENT_ID,
  appPassword: process.env.BOT_APP_CLIENT_PASSWORD
});

const bot = new Bot();

const createWebResponse = (ctx: RouterContext<{}, {}>): WebResponse => ({
  status(code: number): void {
    ctx.status = code;
  },
  send(body: string): void {
    ctx.body = body;
  },
  end(): void {
  }
});

router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const res = createWebResponse(ctx);
  await adaptor.processActivity(ctx.req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

app.use(router.routes())
  .use(router.allowedMethods())
  .listen(3000);