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