BotFramework "WebChat" の歩き方

# はじめに
Microsoftの提供する [BotFramework](https://dev.botframework.com/)では、Bot Builder SDKを使ってBotサーバを書くと、Bot Connector Serviceが仲立ちして各Channel (Facebook Messenger, Slack, Skype etc) を通してbotと会話できるようになるのですが、そのChannelの1つに 「WebChat」(webサイトに導入できるwidget)があります。  
本記事では、WebChatの導入方法とカスタマイズ方法について書きます。 
(Botサーバ側の開発については他の記事を参照下さい) 
<img width="834" alt="architecture-resize.png" src="https://qiita-image-store.s3.amazonaws.com/0/81874/b7fb0308-7e02-ef02-44a3-621d047059a8.png">
(https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png)

# WebChatを導入する方法
- WebChat自体はReact-Redux製のOSSとして[こちら](https://github.com/Microsoft/BotFramework-WebChat) で公開されているのですが、以下に書くように自前でソースをbuild & ホスティングせず、Microsoftがホスティングするものを使って導入することもできます。

- __導入法1: iframeをサイトに埋め込む__
    - [Botの管理ページ](https://dev.botframework.com/bots/) に「WebChat」Channelを追加し、表示される「Embed code」をコピーしてサイトに埋め込む
    - Embed codeの例 (シークレットはBotの管理ページから確認できます）

```html
<iframe src='https://webchat.botframework.com/embed/{Bot名}?s={シークレット}></iframe>
```

- __導入法2: iframeは使いたくない場合__
    - 以下のように、WebChatの描画領域のDOMを渡す方法があります。

```html
<!DOCTYPE html>
<html>
  <head>
    <link href="https://cdn.botframework.com/botframework-webchat/latest/botchat.css" rel="stylesheet" />
  </head>
  <body>
    <div id="bot"/>
    <script src="https://cdn.botframework.com/botframework-webchat/latest/botchat.js"></script>
    <script>
      BotChat.App({
        directLine: { secret: 'シークレット' },
        user: { id: 'userid' },
        bot: { id: 'botid' },
        resize: 'detect'
      }, document.getElementById("bot")); // ★chatを描画させたい領域のDOMを渡す
    </script>
  </body>
</html>

```

- __導入法3: Reactサイトに導入する__
    - `npm install botframework-webchat` した上で、以下のようにComponentとして追加することが出来ます

```javascript
import { Chat } from 'botframework-webchat';

...

const YourApp = () => {
    <div>
        <YourComponent />
        <Chat directLine={{ secret: 'シークレット' }} user={{ id: 'user_id', name: 'user_name' }}/>
        <YourOtherComponent />
    </div>
}
...
```
その他詳細は [README](https://github.com/Microsoft/BotFramework-WebChat/blob/master/README.md) をご覧ください。

# WebChatのカスタマイズ方法

## 開発フロー

```bash
# まずは環境構築
git clone https://github.com/Microsoft/BotFramework-WebChat.git
npm install

# => ソースを修正する

npm run build # => ./botchat.js, ./botchat.cssが吐き出される
npm run start # 開発サーバ起動

# => ブラウザで http://localhost:8000/samples/sidebar/index.html?s={シークレット} 
#    にアクセスしすると、 ./botchat.js, ./botchat.css を読み込んでchatが表示される
```

#### localのwidgetをlocalのBotサーバに接続させたい場合は？

以下のように、Bot Connector Serviceが参照するBotを[ngrok](https://qiita.com/kitaro729/items/44214f9f81d3ebda58bd)でlocalにポートフォワードすることで可能になります

```bash
# 上記手順でwidgetのサーバは起動した状態で

# 1. Botサーバを起動(Node.jsの場合)
npm run start # => port:3978でBotサーバが起動する

# 2. 別ターミナルからngrokを起動
ngrok http 3978 # => 「Forwarding  https://aaabbbccc999.ngrok.io -> localhost:3978」をメモっておく

# 3. https://dev.botframework.com/bots　から対象のBotを選択 -> 
#    右上の「SETTINGS」 -> 「Messaging endpoint」を 2のURLに変更して保存
#    【※注意】2018年3月頃にMS Bot Frameworkは Azure Bot Serviceに統合されることが
#     アナウンスされているため、ここの手順は将来変わると思われます。

# 4. http://localhost:8000/... にアクセスしてBotと会話すると、localのBotサーバにつながります

```

## 見た目を変えたい場合
- ヘッダーの色を変えたい、文字サイズを変えたい 程度であれば [src/scss/botchat.scss](https://github.com/Microsoft/BotFramework-WebChat/blob/master/src/scss/botchat.scss) を修正 & buildしたものをホスティングして読み込ませればokです

## 機能を修正したい場合
- WebChat本体のソースコードを修正することになりますが、Bot Connector Serviceを介してBotサーバとやりとりをするという関係上、通信部分には手を出せない ( [Direct Line API](https://docs.microsoft.com/en-us/bot-framework/rest-api/bot-framework-rest-direct-line-3-0-concepts) の制約を受ける) ことに注意が必要です。
    - Bot Connector Serviceを介さず、直接BotサーバのAPIを叩くんや！となれば話は別ですが、WebChat以外のChannelとの機能差異が生まれることになります。
    - DirectLineAPIとのやりとりは、[BotFramework-DirectLineJS](https://github.com/microsoft/botframework-directlinejs)というライブラリとして提供されています。
- ソースを読むにあたっては、[TypeScript](http://www.typescriptlang.org/),[React](https://facebook.github.io/react/),[Redux](http://redux.js.org/),[redux-observable](https://redux-observable.js.org/), [RxJS](http://reactivex.io/rxjs/)あたりの知識が必要になります。(多い・・)

以下では、WebChat本体を修正するのに必要な知識についてざっくりまとめます。

### ファイル構成
```bash
.
├── package-lock.json
├── package.json
├── samples # 動作確認用のHTML等が入っている
│   ├── backchannel
│   ├── fullwindow
│   ├── react
│   ├── sidebar
│   └── speech
├── src # コンポーネントの各ファイルは、ここに1階層(フラット)に配置されている
│   ├── ActivityView.tsx
│   :         :
│   ├── SpeechOptions.ts
│   ├── Store.ts   # store
│   ├── Strings.ts # localize文言はここ
│   ├── Timestamp.tsx
│   ├── Types.ts
│   ├── adaptivecards-hostconfig.d.ts
│   ├── getTabIndex.ts
│   └── scss # 見た目を変えたい場合はこの中のscssをいじる
├── test
├── tsconfig.json
├── webpack.config.js
└── webpack.production.config.js
```

### ストア構成

action creator, state, reducerは src/Store.tsに定義

|state名|役割|
|---|---|
|shell|ユーザの入力したテキストを保持|
|format|locale情報、localize用のテキストマスタを保持|
|size|widgetのwidth, heightを保持|
|connection|Bot Connector Serviceへのconnectionや、ユーザ/Botのidentity情報を保持|
|history|Bot-ユーザ間でやりとりしたActivity(=メッセージや、タイピング中であることを通知するイベントetc)を保持する|

createStoreは以下のようになっており、副作用を伴うものはEpicとして定義してMiddlewareにして混ぜ込む。
(Epic: actionのストリーム(=Observable)を受け取ってaction(Observable)を返す関数。例えば、ユーザがメッセージを入力して送信ボタンを押すと、Shell Actionの `Send_Message`が発火 → それをsendMessageEpicでhookして `state.history.botConnection.postActivity` でBotConnector Serviceに投げる → 成功したら `Send_Message_Succeed` actionを返す という感じで使われる）

```typescript
export const createStore = () =>
    reduxCreateStore(
        combineReducers<ChatState>({
            shell,
            format,
            size,
            connection,
            history
        }),
        applyMiddleware(createEpicMiddleware(combineEpics(
            updateSelectedActivityEpic,
            sendMessageEpic,    # <- 上記に書いたメッセージの送信は
            trySendMessageEpic, # <- これらで行う
            retrySendMessageEpic,
            showTypingEpic,
            sendTypingEpic,
            speakSSMLEpic,
            speakOnMessageReceivedEpic,
            startListeningEpic,
            stopListeningEpic,
            stopSpeakingEpic,
            listeningSilenceTimeoutEpic
        )))
    );
```

### コンポーネント構成
- src/App.tsxがReactアプリケーションを表現、src/Chat.tsxがTopレベルのコンポーネント
- 見た目上はざっくり以下のような対応になる
![quiz2.png](https://qiita-image-store.s3.amazonaws.com/0/81874/2f97fa9f-fcc5-6a98-1c4c-7419023ca48b.png)

### ○○したい時はこうしよう

#### ページ遷移しても同じ会話を継続したい

BotChat.Appのコンストラクタに渡す directLineオプションに `conversationId` というものがあり、これにページ遷移前のconversationIdを渡してchatを起動すると、会話を継続することができます。

```javascript
BotChat.App({
    directLine: {
        secret: params['s'] || envToSecret[env],
        token: params['t'],
        domain: params['domain'],
        webSocket: isWebSocket,
        conversationId: 'hogehoge', // ★ここに渡す
    },
    user: user,
    bot: bot,
        :
```

(遷移前のページでの)conversationIdはcookie等に保存すると良いかと思います。
保存するタイミングとしては、BotからのActivityを受け取る箇所で行えば良いでしょう。

```diff
# https://github.com/Microsoft/BotFramework-WebChat/blob/master/src/Chat.tsx

diff --git a/src/Chat.tsx b/src/Chat.tsx
index 8e6a8c9..af17109 100644
--- a/src/Chat.tsx
+++ b/src/Chat.tsx
@@ -78,7 +78,14 @@ export class Chat extends React.Component<ChatProps, {}> {
         let state = this.store.getState();
         switch (activity.type) {
             case "message":
-                this.store.dispatch<ChatActions>({ type: activity.from.id === state.connection.user.id ? 'Receive_Sent_Message' : 'Receive_Message', activity });
+                let is_sent_message = activity.from.id === state.connection.user.id ? true : false;
+                this.store.dispatch<ChatActions>({ type: is_sent_message ? 'Receive_Sent_Message' : 'Receive_Message', activity });
+                if (is_sent_message) {
+                    break;
+                }
+                // 例えばこんな感じでconversationIdを保存
+                document.cookie = 'conversationId=' + activity.conversation.id;
+
                 break;

             case "typing":
```


#### Bot-Widget間で独自のメッセージ交換やイベント通知をしたい
- 両者で交わされるActivityオブジェクト(メッセージやTyping中であることのイベント通知)に `entity`というプロパティ(anyの配列)があり、そこは自由に使えるようです。
    - 使うシーンとしては、entity経由でwidgetからBotに`location.href`を通知して、chatが起動されたページによってBotの応答を変える、など色々考えられそうです。
    - entityはBot Connector Serviceを経由する、かつキャッシュされる(上に書いたconversationIdを渡してchatを起動した際は、キャッシュされたactivityがConnector Serviceから送られてくる)ため、外部サービスに渡して良い情報なのか？という点は考慮が必要かと思います。
- Bot側(Node.js SDK)のReferenceは [これ](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.imessage.html#entities) と [これ](https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.message.html#addentity)です。  以下のような感じでentityを参照/送信できます。

```javascript
var builder = require('botbuilder');

function(session, args){
    // ユーザから送られたentitiesを参照
    var entities = session.messsage.entities;
                 :
    // ユーザにentitiesを送る
    var entity = {hoge: 'fuga'};
    var message = new builder.Message(session)
        .text('hoge')
        .addEntity(entity)
    session.send(message)
```  
- widget側では以下のようにして参照します。

```typescript
// 例. ActivityView
this.props.activity.entities

```
- widget側からentityを送る方法は未実装のようですが、以下の様な改修すると送ることができます。(本家へのPR検討しよう・・）

```diff
diff --git a/src/Store.ts b/src/Store.ts
index 13da314..56963cf 100644
--- a/src/Store.ts
+++ b/src/Store.ts
@@ -9,7 +9,7 @@ import * as konsole from './Konsole';

 import { Reducer } from 'redux';

-export const sendMessage = (text: string, from: User, locale: string) => ({
+export const sendMessage = (text: string, from: User, locale: string, entities: any[] = []) => ({
     type: 'Send_Message',
     activity: {
         type: "message",
@@ -17,7 +17,8 @@ export const sendMessage = (text: string, from: User, locale: string) => ({
         from,
         locale,
         textFormat: 'plain',
-        timestamp: (new Date()).toISOString()
+        timestamp: (new Date()).toISOString(),
+        entities: entities
     }} as ChatActions);

 export const sendFiles = (files: FileList, from: User, locale: string) => ({
```
- sendMessageを使う側

```typescript
import 'sendMessage' from 'path/to/Store';

const entities = [{hoge: 'fuga'}];
sendMessage('message', user, locale, entities);
```

以上です。素敵なchatbot生活をお過ごし下さい :innocent: