GASとDialogflowでLINE botを作った話

## はじめに
GAS(Google Apps Script)でLINEのbotを作り、そのbotが受け取ったメッセージをDialogflowで自然言語解析して応答させてみた、というお話です。

## GASでLINEのbotを作る
ひとまずDialogflowはおいておいて、オウム返しするだけのbotを作ります。

### 環境構築
まずは以下の記事を参考にしつつ、GASのローカル開発環境を構築しました。
[Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
](https://qiita.com/howdy39/items/0e799a9bfc1d3bccf6e5)[GAS を npm パッケージ + Webpack + TypeScript で開発する](https://qiita.com/shohei_ot/items/7b26461359068a192b96)

### 実装
LINEのbotの実装については非常に多くの情報があるのでここでは深くは触れないことにします。

```typescript:index.ts
import { reply } from "./reply";

global.doPost = (e: any): void => {
  reply(e);
};
```

```typescript:reply.ts
export const reply = (e: any) => {
  const accessToken: string = PropertiesService.getScriptProperties().getProperty(
    "LINE_ACCESS_TOKEN"
  );
  const json = JSON.parse(e.postData.contents).events[0];
  const replyToken: string = json.replyToken;
  const userMessage: string = json.message.text;
  const url: string = "https://api.line.me/v2/bot/message/reply";

  const headers = {
    Authorization: "Bearer " + accessToken,
    "Content-Type": "application/json; charset=UTF-8",
  };

  const postDatas = {
    messages: [
      {
        text: userMessage,
        type: "text",
      },
    ],
    replyToken,
  };

  const options: any = {
    headers,
    method: "POST",
    payload: JSON.stringify(postDatas),
  };

  UrlFetchApp.fetch(url, options);
  return ContentService.createTextOutput(
    JSON.stringify({ content: "post ok" })
  ).setMimeType(ContentService.MimeType.JSON);
};
```

これをデプロイしてLINEのチャネルのWebhookURLにしてあげればオウム返しbotの完成です。

## Dialogflow
ここからが本題

### Dialogflowって何？
[公式ドキュメント](https://cloud.google.com/dialogflow/docs)
[Dialogflow入門](https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313)

私の言葉で説明するよりも上記を読んで頂くほうがいいと思いますが、つまりは**自然言語の解析をGoogle先生のパワーで簡単にしてくれた優れもの**という感じです。

### 実装
まずユーザーに応答する`reply`関数を修正して、`createReply`関数によって作られたテキストを応答するようにします。

```typescript:reply.ts
import { createReply } from "./messages/createReply";

export const reply = (e: any) => {
  
  // 略

  const postDatas = {
    messages: [
      {
        text: createReply(userMessage),
        type: "text",
      },
    ],
    replyToken,
  };

  // 略

};
```

次に、リプライの内容を作る`createReply`を書きます。
私の実装が最適かどうかはさておき、今回は`Dialogflow`というクラスを作り、会話のたびにインスタンスを生成します。`Dialogflow`の実装は後ほど。

```typescript:createReply.ts
import { Dialogflow } from "./dialogflow";

export const createReply = (userMessage: string): string => {
  const queryResult = new Dialogflow(userMessage).postQuery();
  const intent: string = queryResult.intent.displayName;
  
  switch (intent) {
    case "foo":
      return "intentはfooでした";
    case "bar":
      return "intentはbarでした";
    default:
      return userMessage;
  }
};
```
`createReply`は、ユーザーからのメッセージを引数にとり、そのメッセージをDialogflowのAPIに渡してintentを受け取ります。
ここではintentに応じて返答する内容を変えるような関数にしています。

最後に`Dialogflow`クラスを実装します。

```typescript:dialogflow.ts
const scriptProperties = PropertiesService.getScriptProperties();
const dfUrlFormat: string = scriptProperties.getProperty("DF_URL_FORMAT");

export class Dialogflow {
  private sessionID: string;
  private message: string;

  public constructor(message: string) {
    this.sessionID = Math.random().toString(32).substring(2);
    this.message = message;
  }

  public postQuery() {
    const body: any = {
      queryInput: {
        text: {
          languageCode: "ja",
          text: this.message,
        },
      },
      queryParams: {
        timeZone: "Asia/Tokyo",
      },
    };

    const options: any = {
      contentType: "application/json; charset=utf-8",
      headers: {
        Authorization: "Bearer " + getAccessToken(),
      },
      method: "POST",
      payload: JSON.stringify(body),
    };

    const response = UrlFetchApp.fetch(
      dfUrlFormat.replace(/{{sessionID}}/g, this.sessionID),
      options
    );
    return JSON.parse(response.getContentText()).queryResult;
  }
}

const getAccessToken = () => {
  const jsonKey = JSON.parse(
    scriptProperties.getProperty("GOOGLE_APPLICATION_CREDENTIALS")
  );

  const serverToken = new GSApp.init(
    jsonKey.private_key,
    ["https://www.googleapis.com/auth/cloud-platform"],
    jsonKey.client_email
  );

  const tokens = serverToken
    .addUser(jsonKey.client_email)
    .requestToken()
    .getTokens();
  return tokens[jsonKey.client_email].token;
};
```
Dialogflowは、エンドユーザーとの会話をセッションと呼んでいます。それぞれのセッションにセッションIDがあり、このIDはAPIの呼び出し元(つまり私たち)の責任で適切に選択しなければなりません。
今回実装した`Dialogflow`クラスはインスタンスごとにセッションIDをランダムに生成します。(だったら`Session`というクラス名にすればよかった)

また、`postQuery`メソッドを持っていて、detectIntentを取得します。
公式リファレンス: [Method: projects.agent.sessions.detectIntent](https://cloud.google.com/dialogflow/docs/reference/rest/v2/projects.agent.sessions/detectIntent)

OAuth認証は以下の記事を参考に実装しました。
ここでかなり悩んでいたんですが、先人の偉大な知恵に助けられました。
[Hangouts Chatbotをdialogflowを利用してもっとbotらしくする
](https://qiita.com/h-r-k-matsumoto/items/306f02c30529e7aa43d2)

これでDialogflowによる言語解析をLINEbotが出来上がりました。
Dialogflowで簡単に言語解析を体験できるのでぜひお試しあれ。

## おわりに
自然言語解析と聞くと難しそうですがDialogflowを使ってみるとかなり簡単にできますね:grin: