最近はBot Framework v4を利用して、Botアプリケーションを作成しています。
Botアプリケーションを作成するには、SDKを利用するのですが、express、restifyくらいしかWebアプリケーションのフレームワークが対応してないような気がします。
koaの場合
例えば、BotFrameworkAdapter.processActivity の要求にレスポンスオブジェクトを渡す必要があるのですが、 koaの context.response を渡すと status、send、end あたりのメソッドがない為に、ランタイムエラーが発生します。
これは、expressやrestifyのレスポンスオブジェクトのインターフェースをBotFrameworkAdapterが期待しているからです。
また、koaの実装の場合、ミドルウェアを一通り処理した後に最終的な応答を返すので、サンプルでよくある実装をすると応答を正常に返せません。
// よくみるサンプルコード // express/restifyの場合 app.post('/api/messages', (req, res) => { adapter.processActivity(req, res, async (context) => { await bot.run(context); }); });
Bot Emulatorを使用してデバッグすると、404を返した後に200を返したりします。
// 素直に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);