GAS × LINE Messaging APIで画像も音声もわかる攻略botを作った話


# この記事何？
[神式一閃カムライトライブ](https://kamurai-tribe.applibot.co.jp/)というアプリのメイン機能「修行」をサポートしてくれるLINE botを作った話です。   
基本的には費用のかからない範囲で制作することを前提としています。   このLINE botを作る際にやったことを備忘録的にまとめていきます。


直前に[この記事](https://qiita.com/akira_/items/c8881dcde93d4cb035c7)がでてびくびくしてます。負けない。

どこかの誰かの役に立つことを願って :wink: 

## 対象
- Google Apps Scriptはなんとなくわかる、もしくは調べればわかる
- LINE Messaging APIってどんなことできんの
- 簡単にbotを作りたい
- 無料でbotを作りたい
- ゲームの攻略botを作りたい

あたりの方向け。

## 成果物
[アメリ@カムトラ](https://zawascript.github.io/kamutora-line-bot/)   
↑ここに載せてある動画がわかりやすいです :movie_camera: 


### 主な機能
- テキストでの検索
- スクショからの検索
- 音声からの検索

### スクショ

![IMG_0102.PNG](https://qiita-image-store.s3.amazonaws.com/0/20650/ba921fcd-c080-8113-16b0-65239470466a.png "スクショ1")![IMG_0103.PNG](https://qiita-image-store.s3.amazonaws.com/0/20650/8e13cb31-d6aa-bcb5-f624-a54738bafe8d.png "スクショ2")![IMG_0104.PNG](https://qiita-image-store.s3.amazonaws.com/0/20650/0e25cb6c-2bb9-4148-262a-130a5f61fba2.png "スクショ3")


#### ついでにアメリの紹介
![character6.png](https://qiita-image-store.s3.amazonaws.com/0/20650/6a5c1124-6f3a-db44-4047-330c4124cff4.png "character6.png")
そのうちプレイアブルキャラクターとして出てほしいです。


# 何を使って？

## イメージ
![アメリ@カムトラ.png](https://qiita-image-store.s3.amazonaws.com/0/20650/1977dccc-3a23-0b0f-2cee-328593cd6033.png "アメリ@カムトラ.png")


##[Google Apps Script](https://developers.google.com/apps-script/)
バックエンド。利用者からのメッセージを受け取り、各APIとの橋渡しを行う。定期実行で攻略情報のスクレイピングも担う。   
Gmailアカウントさえあればすぐにでも使えます。

### 制限
実行制限などの制限が多い。本格的なサービスは厳しい。特にto CはAPIの回数制限的に厳しめ？
無料の範囲での各制限は[ここ](https://docs.google.com/macros/dashboard)。


##[LINE Messaging API](https://developers.line.me/ja/docs/messaging-api/)
メッセージの送受信を行う。テキストメッセージだけでなく、動画や音声などのファイルもしっかり扱える。   
見てくれにこだわる場合、そのjson記述が少しめんどくさい。[Flex Message Simulator](https://developers.line.me/console/fx/)
をうまく使えばそれも簡単。
### 制限
制限は[ここ](https://developers.line.me/ja/reference/messaging-api/)。　　　
botとして利用するのであれば、フリープランで十分かなと言った感じです。


##[Cloud Vision API](https://cloud.google.com/vision/)
Googleのサービス。base64エンコードした画像を解析し、その結果を返してくれる。今回は`Text Detection`（文字検出）を利用。その他にも物体の検出や場所の推定などでもできる。    
正しくImageContextを設定してあげれば、そこそこの精度で判別を行ってくれました。

### 制限
タイトルのリンクの「Cloud Vision API の料金」を参照。


##[Cloud Speech-to-Text](https://cloud.google.com/speech-to-text/?hl=ja)
Cloud Vision APIと同様Googleのサービス。音声をテキストに変換した結果を返してくれる。

### 制限
タイトルのリンクの「Cloud Speech-to-Text API の料金」を参照。

Google Cloudのサービスは基本的にどこも似ていて、一定量まで無料、それ以降は料金がかかります。   


##[CloudConvert API](https://cloudconvert.com/api)
Cloud -> Cloud -> Cloudと来てますがこれはGoogleのものではありません。   
LINE Messaging API <-> Cloud Speech-to-Text 間で音声のフォーマットが違うため、これを変換するために利用。   
豊富な種類のファイルの変換が可能。他にもいろいろ使えそうでした。
### 制限
制限は[ここ](https://cloudconvert.com/pricing)。　　　
無料だと一日25回しか使えない＼(^o^)／   
完全に個人利用の範囲になるので、もし何らかのサービスで利用したい場合はまねーが必要。



# どうやって作ったの？

## 手順

### 1. botの本体[Google Apps Script](https://chrome.google.com/webstore/detail/google-apps-script-github/lfjcgcmkmjjlieihflfhjopckgpelofo?hl=ja)を用意
Google Driveのどこかに適当に用意しましょう。   
とりあえず、LINEからのアクセスが来ていることを簡単に確認するために、[GoogleAppsScriptでdoPost()等でログが使えないときの代替案](https://qiita.com/takanakahiko/items/c8990145605eb4e1452d)を参考にログを見れるようにしておきましょう。   
Spreadsheetはどうせ後で使うことにもなるので、そちらで良いと思います。   
とりあえず適当にファイルを作って下記を記述。

```Javascript:HttpRequestHandler.gs
function doPost(e){
	
	log(JSON.stringify(e));

}

function log(str){
	// なんかログを吐いてくれるようにしておく
}
```

メニュー「公開」から「Webアプリケーションとして導入」しておきましょう。  
 
| 項目 | 設定|
|:-----------|:------------|
|次のユーザーとしてアプリケーションを実行|自分|
|アプリケーションにアクセスできるユーザー:|全員（匿名ユーザーを含む）|


### 2. [LINE Developer](https://developers.line.me/ja/) アカウントを開設

普段LINEを使っているアカウントで作ればよいです。タイトルのリンクからログインを行い作成しましょう。
#### 2-1 プロバイダーの作成
メニューから「プロバイダーリスト」を選択、「新規プロバイダー作成」ボタンを選択。
#####プロバイダー？ :point_left: 
開発者名とでも思っておけばいいのかな。
#### 2-2 チャネルの作成
作成したプロバイダーの画面から、「新規チャネル作成」
#####チャネル？ :point_left: 
一人の開発者は複数のサービスを持てる的な感じで考えておくこととします。   
「チャネル」単位で[LINE@](http://at.line.me/jp/)のアカウントもできる模様。利用者はここで設定した情報を閲覧できます。   
この子がbotになります。   


- アプリタイプは「BOT」
- プランはフリー
で作成しておきましょう。

#### 2-3 チャネル基本設定

##### メッセージ送受信設定

###### アクセストークン
右の（再）発行を押し、トークンを発行します。失効までの時間は、特に理由がなければ0にしておきましょう。

###### Webhook URL ※SSLのみ対応
先程公開したGASのURLを設定します。「接続確認」を押し、「成功しました」が出ることを確認しておきましょう。

##### QRコード
が、表示されているので友達に追加しておきましょう。

##### 参考url
- [LINE Messaging APIでお手軽bot開発！5分でできる作り方をご紹介します](https://ledge.ai/line-messaging-api-exp/)

### 3. LINE <-> GAS間の疎通を確認

上の手順で追加した友達にメッセージを送ってみましょう。SpreadSheetやらStackdriver Loggingやらにその情報が見えるはずです。

コードを少し編集し、今度は返信が行えることを確認します。
今回はLINE Messaging APIを適当にラップするために作成した[LineMessagingApi](https://github.com/zawascript/gas/blob/master/LineMessagingApi/scripts/LineMessagingApi.js)を利用します。
※利用方法はgithubに書いたので割愛します。


```Javascript:HttpRequestHandler.gs
function doPost(e){
	
	// イベントはリストなので、仮でとりあえず１つ目を取得しておく
	var event = JSON.parse(e.postData.contents).events[0];

	var lma = new LineMessagingApi('先程発行したアクセストークン', event.replyToken);
	lma.message('メッセージありがとう！').exec();
}
```

メニュー「公開」から「Webアプリケーションとして導入」、プロジェクトバージョンを「新規作成」にして再度公開します。（この作業を行わないと反映されないので注意）  

この状態で、適当にメッセージを送ってみると、botから返信が返ってきたと思います。


#### `replyToken`? :point_left: 
フリープランでは、bot起因でのメッセージをユーザーに配信することができません。代わりに、ユーザーからのメッセージに対しての返信を行うことができます。一つのメッセージに対して最大5つのメッセージ（吹き出し）を返すことができます。  


`LineMessagingApi`では、

```Javascript

var event = JSON.parse(e.postData.contents).events[0];
var lma = new LineMessagingApi('先程発行したアクセストークン', event.replyToken);
lma.message('あ')
	.message('り')
	.message('が')
	.message('と')
	.message('う')
	.exec();
	
// これ以降、そのトークンを使っても返信はできない
// 原則、一つのreplayTokenに対して一度のみ返信が可能
```

のようにすると複数のメッセージを送信することができます。





### 4. GAS -> Spreadsheetへアクセスし、必要な情報を返す

#### 4-1 必要なデータを保存したSpreadsheetを用意する。

GoogleAppsScriptでSpreadsheetからデータを取得する場合、`getRange()`などで毎回取るのが面倒なため、今回は[SheetAccesser](https://github.com/zawascript/gas/tree/master/SheetAccesser)を使います。   
リンクページのように、一行目にキー、2行目以降にデータの形で検索したいデータを書いておきます。

```Javascript:smple.gs
var sa = new SheetAccesser(SPREAD_SHEET_ID);
objectValues = sa.getObjectValues('character',{id:1});

```

のように直感的にほしい行を取得することができます。※将来的には条件に関数を渡せるようにする予定  


#### 4-2 送られてきたメッセージで検索し、結果を返す


```Javascript:HttpRequestHandler.gs
function doPost(e){
	var event = JSON.parse(e.postData.contents).events[0];
	var lma = new LineMessagingApi('アクセストークン', event.replyToken);
	// 利用者からのメッセージはここに入っている
	var msg = event.message.text; 
	var resultList = searchByName(msg);
	// 文字列としてそのまま返しちゃう
	lma.message(JSON.stringify(resultList)).exec(); 
}


function searchByName(str)
{
	var retList = [];
	var sa = new SheetAccesser(SPREAD_SHEET_ID);
	objectValues = sa.getObjectValues('character'); // 条件指定無しで全取得
	for(var i = 0; i < objectValues.length; ++i){
		if(objectValues[i].name.indexOf(str) !== -1){
			retList.push(objectValues[i]);
		}
	}
	
	return retList
}

```

Spreadsheetにある、送ったメッセージの含まれる行がすべて表示されたかと思います。

##### データはどう集める？ :point_left: 
攻略ボットのようなものを作る際、その元データを手動で全て入力するのは現実的ではありません。そのため、基本的にはどこかのサイトをスクレイピングすることになるかと思います。   
スクレイピングに関しては[それ用のライブラリ等](https://tadaken3.hatenablog.jp/entry/parser-for-gas)を使うか、無難に`UrlFetchApp()`でサイトの文字列を取得し、正規表現で取得することになるかと思います。   
また、スクレイピングをする際に気をつけるべきこともありますので[こちら](https://www.torikun.com/entry/scraping-law)を参考にしてください。



### 5. 表示を整える

ここまでで、とりあえず結果を返すところまで来ました。しかし、文字の羅列が返ってくるだけではとても使いづらいので少しだけ体裁を整えていきます。　  

#### 5-1 [Flex Message](https://developers.line.me/ja/docs/messaging-api/flex-message-elements/)を使う

Flex Messageを利用して、返信メッセージを装飾することができます。（Flex Messageの使用に関してはタイトルのリンク等を参考にしてください）   


先ほど上で紹介した、[Flex Message Simulator](https://developers.line.me/console/fx/)を使い、Flex Messageの表示を確認しながら作成することができます。（タブのRestaurantはデフォルト・・・:blush: ）
![tri.png](https://qiita-image-store.s3.amazonaws.com/0/20650/00c3cdc7-70f3-e366-4dab-f0b6bd05d06d.png "tri.png")

体裁を整え終わったら、それをコードに反映していきます。


#### 5-2 カルーセルを使ってみる
Flex Messageには、carouselという要素があります。先程の様に`type="bubble"`なメッセージを複数含め、横にスワイプして表示することができます。

```Javascript:sample.gs
function doPost(e){
	var event = JSON.parse(e.postData.contents).events[0];
	var lma = new LineMessagingApi('先程発行したアクセストークン', event.replyToken);
	// 利用者からのメッセージはここに入っている
	var msg = event.message.text; 
	var resultList = searchByName(msg);
	
	// carousel()は、bubbleのリストをカルーセル化してくれるやつです
	// カルーセルの最大数は10なので、それを超えた場合にメッセージを分割してくれます。
	lma.carousel(genCharacterBubbleList(resultList)).exec();
	
	// ひとつのbubbleを送るのであればbubble()
	// lma.bubble({type:"bubble"}なjson)
}

// 検索結果をすべてbubbleへ
function genCharacterBubbleList(list){
	var retList = [];
	for(var i = 0; i < list.length; ++i){
		retList.push(genBubbleContent(list[i]));
	}
	return retList;
}


// とりあえずSimulatorのものをそのまま貼り付ける。
function genBubbleContent(character)
{
	return {
    "styles": {
      "footer": {
        "backgroundColor": "#f9f9f9",
        "separator": true,
        "separatorColor": "#888888"
      }
    },
    "type": "bubble",
    "header":{
      "type":"box",
      "layout":"horizontal",
      "contents":[
        {
          "type": "text",
          "text": character.name
        }
      ]
    },
    "body": {
      "type": "box",
      "layout": "vertical",
      "contents": [
        {
          "type": "image",
          // urlを指定して、画像を表示することもできる。
          "url": "https://appmedia.jp/wp-content/themes/appmedia/lib/kamurai/img/chara/"+ character.id+".png",
          "size": "md"
        }
      ]
    },
    "footer": {
      "type": "box",
      "layout": "horizontal",
      "contents": [
        {
          "type": "button",
          "action": {
            "type": "postback",
            "label": "教えてアメリ",
            "data": "このボタンを押したときに、アプリ側で受け取りたいデータを設定できる。"
          }
        },
        {
          "type": "separator",
          "color": "#aaaaaa"
        },
        {
          "type": "button",
          "action": {
            "type": "uri",
            // ボタンを押したときに、リンク先へ飛ばす
            "label": "攻略サイトへ",
            "uri": "https://appmedia.jp/kamurai/"
          }
        }
      ]
    }
  }
}

```

これで、検索に引っかかったキャラクターがスワイプで表示されるようになりました。

この、json記述をどうにかしてくれるGASのライブラリとかあればいいのですが・・・ :persevere: 


##### postbackアクションを行う :point_left: 
先ほど作成した、bubble要素の中の`action.type = "postback"`は、そのボタンを押した際に、`message.type = "postback"`なイベントを送信してきます。   

これを使い、GAS側で利用者の状態を保持せずとも、継続的なやり取りを行うことができます。例えば、

```Javascript:sample.gs
{
  "type": "button",
  "action": {
    "type": "postback",
    "label": "教えてアメリ",
    "data": "{\"action\":\"detail\",\"id\":123456}"
  }
}

```
のようにこのボタンを押した際の値をJSON文字列で設定しておけば、そのメッセージを受け取った際に、キャラクターの詳細を表示する、といったことが可能です。


```Javascript:sample.gs
function doPost(e){

	var event = JSON.parse(e.postData.contents).events[0];
	if(event.type === 'postback'){
		var data = JSON.parse(event.postback.data);
		// data.actionでポストバックの振り分け
		// data.idを使って処理など
		
	} else {
		// その他
	}
}
```

### 6. 画像でも検索したい
検索や体裁を整える術を身に付けたので、Vision APIをつかい、画像での検索を行ってみます。   

#### 6-1 Google Cloud PlatformでVision APIを有効にする

[Google Cloud Visionを使ってみた](https://www.itbook.info/web/2016/11/google-cloud-vision%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F.html)こちらの記事を参考にします。   

Cloud Vision APIの有効化~API Keyの発行までを行います。

#### 6-2 GoogleAppsScriptからVisionAPIを叩く

##### 画像が送られてきた際の振り分け
利用者がbotに画像を送信した際、`message.type = image`で送信されます。   
このメッセージ自体には画像そのものは含まれておらず、送信されてきたメッセージidを使い、コンテンツを取得するAPIを叩きます。  

##### 取得した画像をbase64形式にエンコードしてVisionAPIを叩く
Vision APIにはbase64形式でエンコードされた画像を渡す必要があります。   
GASでは`Utilities.base64Encode()`で簡単に行うことができます。   
こちらも、軽くラップした[GoogleVisionApi]()を使うことにします。　　　

```Javascript:sample.gs
function doPost(e){
	var event = JSON.parse(e.postData.contents).events[0];
	var lma = new LineMessagingApi('アクセストークン', event.replyToken);
	if(event.type === 'image'){
		// getImageBlobは、https://api.line.me/v2/bot/message/{messageId}/contentを叩き、バイナリ形式の画像を返す
		var result = lma.getImageBlob(event.message.id);
		
		// base64形式にエンコードして送信
    	var vision = new GoogleVisionApi("発行したAPIキー");
    	// 文字検出を利用
    	var response = vision.textDetection(Utilities.base64Encode(result));
    	
    	// レスポンスは下記を参照
	}
}
```

あとは、返ってきたデータを見て、必要になる情報を抜き出してあげてください。   
今回は、必要なデータの前後で、特定の文字列がうまく認識されていたので、その間を検索文字列として利用し、先程のように検索のメソッドに投げています。   
レスポンスに関しては[こちら](https://cloud.google.com/vision/docs/ocr)。



### 7. ついでに音声でも検索したい
こちらはもう試したかっただけですが、一応実装はできたので参考までに。

#### 7-1 Google Cloud PlatformでSpeech-to-Textを有効にする
Vision APIを有効にしたのと同じ順序で、Speech APIを有効にし、APIKeyの発行まで行います。

#### 7-2 GoogleAppsScriptからSpeech-to-Text APIを叩く

##### 音声のフォーマットが違う :point_left: 
画像と同様、content取得のAPIを利用して、送られてきた音声を取得します。LINE MessaginApiで取得できる音声形式はm4aですが、Speech-to-Textでは直接利用することができませんでした。そのため、何かしらの手段でSpeech-to-Textで対応しているフォーマットへ変換を行う必要がありました。   
今回は、冒頭の説明通り、[CloudConvert API](https://cloudconvert.com/api)を利用します。[API Console](https://cloudconvert.com/api/console)を使い、形式の指定等が可能で、なかなか使いやすかったです。      
こちらのAPIキーも必要にあるため、アカウントの作成をし、DashBoardでAPIキーの確認を行います。   

また、それぞれのAPIをラップした[CloudConvertApi](https://github.com/zawascript/gas/tree/master/CloudConvertApi)および[GoogleSpeechApi](https://github.com/zawascript/gas/tree/master/GoogleSpeechApi)を利用します。


Vision APIと同様、FLAC形式の音声をbase64にエンコードして送信します。

```Javascript:sample.gs
function doPost(e){
	var event = JSON.parse(e.postData.contents).events[0];
	var lma = new LineMessagingApi('アクセストークン', event.replyToken);
	if(event.type === 'audio'){
  		var result = lma.getAudioBlob(event.message.id);
  
  		// m4a をFLACに変換
  		var cc = new CloudConvert('CloudConvertのAPIキー');
  		var flac = cc.m4aToFlac(result);

  		var speech = new GoogleSpeech('APIキー');
  		var response =  speech.speechToText(Utilities.base64Encode(flac));

    	
    	// レスポンスは下記を参照
	}
}
```

あとは[レスポンス](https://cloud.google.com/speech-to-text/docs/sync-recognize?hl=ja)を参考に、検索するなり、更に変換APIを実行するなりして結果を返すだけです。   

**※繰り返しですが、CloudConvertが無料だと25回/日です** :cry: 

### 8. 完成！
あとは好きなように組み合わせて返してあげるだけ！
めでたい！:laughing: 

## 大変だったこと

##### スクレイピング  
人様の集めた情報を使わせていただいているわけなので何も文句は言えませんが、やはりフォーマットが完全に統一されているわけではないので、かなり苦労しました。これに結構時間を取られます。（ライブラリ等を使わず、正規表現でゴリゴリやっていた。）ただ、後半はパターン化ができてきたため、マシにはなりました。   
やるならライブラリの利用をおすすめします。

##### Flex Message Layout 
html、cssなどを扱っていた身としてはとんでもなく使いづらい。なれるのに時間がかかりました。   
今後のアップデートに期待したい。

##### m4a -> Flac変換
SpeechAPIにわたすためのファイルに関しては、大抵がgcsに上がっている音声を返すものだったりで、このあたりの情報があんまりない。この記事が少しでも参考になればと思います。


# まとめ

## やってよかったと思ったこと

### LINE Messaging API
前からちょいちょいしょうもないbot開発はしていましたが、これぐらい本格的にやったのは初めて。Flex Messageもがっつりさわり、今後もいろいろ触りたい。

### 散財していたコードの整理
GASは使う機会が多かったのですが、簡単にライブラリ化はしているもののかなり色んな所に散財していた。もうgithubに全部あげてしまえということで少しだけ整理ができた。でも途中。。。


## 今後

- 別ゲームでも作る
	- モンスト
	- もしくはコンシューマーとかも
- 今回作ったものの整備
	- 中途半端なやつがいくつもあるので完全対応させる



## 参考URL

### アプリについて
[神式一閃カムライトライブ](https://kamurai-tribe.applibot.co.jp/)
[アメリ@カムトラ](https://zawascript.github.io/kamutora-line-bot/)  

### Google
[Google Apps Script](https://developers.google.com/apps-script/)
[制限](https://docs.google.com/macros/dashboard)
[Cloud Vision API](https://cloud.google.com/vision/)
[Cloud Speech-to-Text](https://cloud.google.com/speech-to-text/?hl=ja)

[GCPでのAPIの有効化](https://www.itbook.info/web/2016/11/google-cloud-vision%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F.html)

### LINE
[LINE Messaging API](https://developers.line.me/ja/docs/messaging-api/)
[制限](https://developers.line.me/ja/reference/messaging-api/)
[LINE Developer](https://developers.line.me/ja/) 
[LINE@](http://at.line.me/jp/)
[Flex Message](https://developers.line.me/ja/docs/messaging-api/flex-message-elements/)

### CloudConvert
[CloudConvert API](https://cloudconvert.com/api)


### zawascript
[APIのラッパーなど](https://github.com/zawascript/gas/)


終わりです。