【GAS × OCR × LINE API】やる気がどんどん無くなるダイエット管理Bot（汎用性ゼロ）

# はじめに
こんにちはーーーー！
[DMM WEBCAMP Advent Calendar 2021](https://qiita.com/advent-calendar/2021/infratop) 3日目を担当します、[@ayumu-1212](https://qiita.com/ayumu-1212)です！

今回書くのは、タイトルにある通り、「**やる気がどんどん無くなるダイエット管理Bot**」です。
うちのチームのアドベントカレンダーのテーマが
「**プログラミングを始めたばかりの方もわかる、ハンズオンや言語基礎**」
なのにも関わらず<font color="red">汎用性ゼロ！</font> :stuck_out_tongue_winking_eye:
( ...ごめんなさい、これに関しては話を聞いてませんでした。。テーマ昨日知りました。。)
( LINE Botの作り方などは苦し紛れに詳しく書いたので、、そこがハンズオンということで。。)
「あーーくそアプリだなー」って思いながら何となく見てください！

途中ソースコードの改変が多発しますが、最後に[全体ソースコード](#全体ソースコード)を乗っけてます！

またどんなアプリか先に知りたいって方は、[最後](#ついに完成)に乗っけてますのでご参照を！

# 背景
実は私、体重がこの1年半で20kgも太ってしまいました。
そろそろダイエットしないとやばいと思った私は近くのジムに通い始めました。
そのジムには筋トレ器具やランニングマシンはもちろんなのですが、**TANITAの体組成計**が置いてありました。
おおお、、これはいい。。と思い、体重と体組成を図りました。
※その結果が以下↓↓
<img width="400" alt="S__19562545.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/72df5a68-3846-4e1b-e282-d44e05aeff8c.jpeg">

すげぇ！！めっちゃ詳しく結果がでる。
けど、、**管理できないのがめんどくさいなぁ。。**
前回測ったときからどのくらい痩せたのかとか、筋肉が増えたのかとかがわからないと、意味がない。
体重計からクラウドにデータが上がる機能はないし。

そうだ！LINEBotを作ろう！
結果の写真を送れば、自動でグラフ化してくれるやつ！
(これで終わればいいのですが、あとで何かもの足りないといって、~~クソ~~ 機能を追加します。)

# 全体像
<img width="80%" alt="S__19562545.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/10bd998f-5361-4cc3-8cc9-61302770c365.png">

# 開発準備
LINE Bot・GASを使ったことある人は展開せずに進めて大丈夫です～～

### LINE Botの導入 ※展開すると詳しく見ることができます。
<details><summary>1. LINE Developerアカウントの登録 ※初回登録のみ</summary>
以下のURLでサインアップ (※LINEアカウントでもビジネスアカウントでも可能)
https://account.line.biz/signup
</details>

<details><summary>2. プロバイダーを新規で作成</summary>
<img width="80%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/c3d8b6e6-1e70-f008-02c3-bfcea09a4864.png">
<img width="50%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/223bd814-bd3c-1f2b-a6bc-480a0ed0e9a2.png">
</details>

<details><summary>3. チャネルを新規で作成</summary>
2で作成したプロバイダ内にて、新規のチャネルを作成します。
<img width="80%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/8b6c4cdd-d323-cfda-5a80-e7dcb6e1a727.png">
次に遷移するページで以下の情報を入力
| 項目 | 入力情報 | 備考 |
|:-----------|:------------|:------------|
| **チャネルの種類** | Messaging API | デフォルトで入力されています |
| **プロバイダー** | (2.のプロバイダー名) | デフォルトで入力されています |
| **チャネルアイコン** | 任意 | Botのトップ画像になります |
| **チャネル名** | 任意 | Botの名前になります |
| **チャネル説明** | 任意 | 今回は特にユーザーに表示はされません |
| **大業種** | 任意 | 当てはまると思うものを選びましょう |
| **中業種** | 任意 | 当てはまると思うものを選びましょう |
| **メールアドレス** | 任意 | チャネルに関するお知らせが届きます |
| **プライバシーポリシーURL** | 任意 | もしあれば |
| **サービス利用規約URL** | 任意 | もしあれば |
| **LINE公式アカウント利用規約** | チェック |  |
| **LINE公式アカウントAPI利用規約** | チェック |  |
最後に[作成]をおしてチャネルは完成です！簡単ですね。
試しにBotを友達追加してメッセージ送ってみましょう！
<img width="70%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/a5d08267-5061-5e76-f30f-828ee985e61d.png">
</details>

### GASの導入 ※展開すると詳しく見ることができます。
今回は開発環境・本番環境としてGoogle App Script(通称GAS)を用います。様々なアプリケーションとの互換性の高さ、デプロイの容易性などが長所としてあるので、これを使っていきます。

1. [GoogleDrive](https://drive.google.com/drive/my-drive)を開く

<details><summary>2. GASのEditorを開く</summary>
<img width="40%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/72b545d2-38fa-c283-661c-b111b090c5b1.png">
<img width="70%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/4c387748-7e31-5129-dc4a-0b0bf338b21c.png">
</details>

<details><summary>3. プロジェクト名をつける</summary>
<img width="70%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/d439dc5a-1499-f35b-f420-51621b9dafef.png">
<img width="40%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/3a270076-6233-7567-b7c7-777f34eb1663.png">
これでGASの環境導入も完了です
</details>


# 開発
いくつかのセクションに分けて、実装していきます。

1. GASでLINEから送られた画像を受信して、OCR(文字認識)する
1. スプレッドシートにユーザーの体組成を記録する
1. 結果をユーザーに送信する

また、それぞれのファイルは以下の意味を持たせてます。

| ファイル名 | 記述内容 |
|:-|:-:|
| main.gs | メインの処理 |
| line_api_scripts.gs | LINE API関連の処理  |
| drive_scripts.gs | OCR関連の処理 |
| spreadsheet_scripts.gs | スプレッドシート関連の処理 |
それではやっていきましょ～～！

## 1. GASでLINEから送られた画像を受信して、OCR(文字認識)する
### GASソースコード
:pencil2:新規作成

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {
  // LINEのメッセージから必要なデータだけ抜粋
  var json = JSON.parse(event.postData.contents);

  // 返信token, message_type取得
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;

  // 画像取得ための、message_id取得
  var messageId = json.events[0].message.id;
    
  //投稿が画像の時 画像のURLを生成し画像取得
  var imageURL = "https://api-data.line.me/v2/bot/message/" + messageId + "/content/";
  var image = getImage(imageURL);

  //取得した画像を文字に起こす
  var ocrText = getText(image);
}
```
:pencil2:新規作成

```javascript:line_api_scripts.gs
// トークン
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");

//投稿から画像を取得
function getImage(imageURL) {

  //取得と同時にBlobしておく
  var image = UrlFetchApp.fetch(imageURL, {
       "headers": {
         "Content-Type": "application/json; charset=UTF-8",
         "Authorization": "Bearer " + TOKEN,
       },
       "method": "get"
  }).getBlob();

  return image;

}
```
:pencil2:新規作成

```javascript:drive_scripts.gs
//画像を文字に起こす
function getText(image) {

  //画像のタイトルとタイプを取得
  var title = image.getName();
  var mimeType = image.getContentType();

  //ドキュメントを作成し画像を挿入（DriveAPIの設定が必要）
  var resource = {title: title, mimeType: mimeType};
  var fileId = Drive.Files.insert(resource, image, {ocr: true}).id;

  //ドキュメントからテキストを取得したらドキュメントを削除
  var document = DocumentApp.openById(fileId);
  var ocrText = document.getBody().getText().replace("\n", "");
  Drive.Files.remove(fileId);

  return ocrText;

}
```

### コードの簡単な解説
#### 1-1. LINEからデータを受信するために
LINEからデータを受信するのに、**webhook**というものを使います。
webhookとは何なのかは今回割愛します。

まぁ簡単に言うと、「データを送受信する仕組み」です。
※詳しくはこちらで → [Webhookとは？]( https://qiita.com/soarflat/items/ed970f6dc59b2ab76169 )

<details><summary>こちらを開くとwebhookの設定方法が載っています。</summary>
LINEからのメッセージの受信先を指定するために、GASをデプロイ(サーバー上にアップ)します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/97916558-38d8-1ada-be89-713dc30000ba.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/7844a726-ed06-fc9d-f01e-184d348f8682.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/ec24118d-dfdf-f2a7-f89e-8b8bd30c6463.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/1b38b4e3-8107-073c-87cd-20007c699ebc.png)
先ほどコピーしたURLをLINE Developers に登録します。
ご自身のLINEチャネルまで入り、以下の手順で登録してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/5e243e00-62d3-6875-2dbe-41d33d4ddc16.png)
これで設定が完了です。
LINEから送られるメッセージが、登録したURLの**doPost()**に届くようになります。
</details>

#### 1-2. LINEのAPIを使うために
LINEのAPIを使うためには、必ず**トークン**が必要になってきます。鍵みたいなものですね。

<details><summary>以下の手順で取得しておきましょう。</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/d32c6f19-141d-d922-4d19-a0ab19a8feb3.png)
</details>

#### 1-3. GASにおける"大事なカギ"の扱い方
シークレットキーや、先ほど取得したトークンなどはコードにベタ打ちするのはよくないです。
もしソースコードを見られたり、Githubに上げようとすると、それが簡単に他の人に知られてしまうからです。
そのため、「大事なカギ」はプロパティに登録しておきましょう。(railsでいう.envファイルですね。)

<details><summary>こちらを開くとプロパティの設定方法が載っています。</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/2ca140ae-b211-416c-f186-003bec229d61.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/939523a9-96ee-e527-4f95-925833407abd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/aba0604c-e439-50f8-ec7e-647b8d997449.png)
</details>

設定すると、大事なカギが、

```javascript
PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
```
などで取得できます！

#### 1-4. OCRを使うために
OCRはGoogle Drive APIを使えば簡単に使うことができます。
Google Driveではpdfファイルや画像をGoogle Documentの形に変換したらOCRしてくれる見たいで、それを使ってるっぽいです。
<details><summary>以下の手順でDrive APIを導入しましょう。</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/a8e07c6b-9edd-9825-1e9f-017312211d33.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/02fc76a7-4bc1-4227-d060-5858f4f19bc2.png)
</details>

## 2. スプレッドシートにユーザーの体組成を記録する
### スプレッドシートの作成
先ほどOCRしたデータを入れるスプレッドシートを作りましょう！
体重管理としてしたかったのは、
- 日付ごとの体組成の管理
- グラフ化
の2つだったので、そのためのテンプレを作りました。
...特に説明することがない。。
作ったスプレッドシートのテンプレを乗っけときますね！

https://docs.google.com/spreadsheets/d/13INW3_MwdHojUsl-jmGJW6Az0_YjINXSyDR48AXR0pE/edit?usp=sharing

### GASソースコード
:pencil2:追記

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {
　
(省略)
　・
　・
　・
　// 以下を追記
  // 送信者専用のSSを取得
  var ss = getMySpreadSheet(user_id);

  // 今回の結果をスプレッドシートに入力
  varinputResultForSS(ss, ocrText);
}
```
:pencil2:新規作成

```javascript:spreadsheet_scripts.gs
// 自分のスプレッドシートがあれば取得、なければ新規作成
function getMySpreadSheet(user_id) {
  // ユーザーID一覧データを取得
  var ss_id = PropertiesService.getScriptProperties().getProperty("USER_LIST_SS_ID");
  var ss = SpreadsheetApp.openById(ss_id);
  var sh = ss.getActiveSheet();
  const shLastRow = sh.getLastRow();
  var ss_values = sh.getRange(1,1, shLastRow, 3).getValues();

  //  ユーザーIDが登録済みかどうか調査
  for(let i = 1; i < shLastRow; i++) {
    this_row = ss_values[i];
    if (this_row[0] === user_id) {
      // 登録済みの場合、既存のspreadsheetを返す
      var subscribed_ss = SpreadsheetApp.openById(this_row[2]);
      return subscribed_ss;
    }
  }

  // 初回登録だった場合、テンプレのスプレッドシートをコピーして返す
  // テンプレSSID
  var temp_ss_id = PropertiesService.getScriptProperties().getProperty("RECORDING_TEMPLATE_SS_ID");
  var temp_ss = SpreadsheetApp.openById(temp_ss_id);
  
  // メッセージ送信主のLINE名取得
  var user_name = getDisplayName(user_id);

  // テンプレをコピーして、タイトルにLINE名を入れる
  var new_ss_name = user_name + "さんの体組成シート";
  var new_ss = temp_ss.copy(new_ss_name);

  // スプレッドシート共有権限付与
  var drive_ss = DriveApp.getFileById(new_ss.getId());
  var access = DriveApp.Access.ANYONE_WITH_LINK;
  var permission = DriveApp.Permission.EDIT;
  drive_ss.setSharing(access, permission);

  // ユーザー一覧データに新規登録
  var input_row = [[user_id, user_name, new_ss.getId()]];
  sh.getRange(shLastRow + 1, 1, 1, 3).setValues(input_row);

  return new_ss;
}

// スプレッドシートに体組成データを入力
function inputResultForSS(ss, ocrText) {
  // 体組成データを、スプレッドシートに入力できる形に変換
  var inputData = extractInputData(ocrText);

  
  // 性別, 年齢, 身長代入
  var sh = ss.getActiveSheet();
  var lastCol = sh.getRange(7, sh.getMaxColumns()).getNextDataCell(SpreadsheetApp.Direction.PREVIOUS).getColumn();
  var input_row = [[inputData.shift(), inputData.shift(), inputData.shift()]];
  sh.getRange(4, 2, 1, 3).setValues(input_row);

  // その他データを以下に代入
  for (let i = 7; i <= 22; i++) {
    sh.getRange(i, lastCol + 1).setValue(inputData.shift());
  }
}

// 体組成データをスプレッドシートに入力できる形に変換
function extractInputData(ocrText) {
  // 取得したデータの空白、改行、タブをすべて取り除く
  var preSplitText = ocrText.split(/\t|\s|\n/);

  // さらにデータの数字と文字列を分断(あとでデータ抽出しやすい)
  var splitText = [];
  for (let i = 0; i < preSplitText.length; i++) {
    var ssi = 0;
    var is_str = Boolean(preSplitText[i][0].match(/[^0-9.+]/g));
    for (let j = 0; j < preSplitText[i].length; j++) {
      // console.log("is_str = " + String(is_str) + ", pre_str = " + String(Boolean(preSplitText[i][j].match(/[^0-9.+]/g))));
      if (Boolean(preSplitText[i][j].match(/[^0-9.+]/g)) ^ is_str) {
        splitText.push(preSplitText[i].slice(ssi, j));
        ssi = j;
        is_str = Boolean(preSplitText[i][j].match(/[^0-9.+]/g));
      }
    }
    splitText.push(preSplitText[i].slice(ssi, preSplitText[i].length + 1));
  }
  // 最後に返す配列
  var outputData = [];

  // 性別年齢身長抽出
  var sex = splitText[splitText.indexOf("性別") + 1];
  var age = splitText[splitText.indexOf("身長") - 1];
  var height = splitText[splitText.indexOf("身長") + 1];
  outputData.push(sex);
  outputData.push(age);
  outputData.push(height);

  // その他データ抽出
  // // 全身
  var index_before_the_day = splitText.indexOf("E");
  var the_day = new Date("20" + splitText[index_before_the_day + 2] + "/" +
                         splitText[index_before_the_day + 3] + "/" +
                         splitText[index_before_the_day + 4]);
  var weight = Number(splitText[splitText.indexOf("体重") + 1]);
  var prev_index_rate_of_fat = splitText.indexOf("体脂肪率") + 1;
  var rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var prev_index_muscle_mass = splitText.indexOf("筋肉量") + 1;
  var muscle_mass = Number(splitText[prev_index_muscle_mass]);
  var basal_metabolic_rate = Number(splitText[splitText.indexOf("基礎代謝量") + 1]);
  var bmi = Number(splitText[splitText.indexOf("BMI") + 1]);
  outputData.push(the_day);
  outputData.push(weight);
  outputData.push(rate_of_fat);
  outputData.push(muscle_mass);
  outputData.push(basal_metabolic_rate);
  outputData.push(bmi);

  // // 右足
  var right_foot_index = splitText.indexOf("右足");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", right_foot_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", right_foot_index) + 1;
  var right_foot_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var right_foot_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(right_foot_rate_of_fat);
  outputData.push(right_foot_muscle_mass);

  // // 左足
  var left_foot_index = splitText.indexOf("左足");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", left_foot_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", left_foot_index) + 1;
  var left_foot_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var left_foot_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(left_foot_rate_of_fat);
  outputData.push(left_foot_muscle_mass);

  // // 右腕
  var right_arm_index = splitText.indexOf("右腕");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", right_arm_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", right_arm_index) + 1;
  var right_arm_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var right_arm_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(right_arm_rate_of_fat);
  outputData.push(right_arm_muscle_mass);

  // // 左腕
  var left_arm_index = splitText.indexOf("左腕");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", left_arm_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", left_arm_index) + 1;
  var left_arm_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var left_arm_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(left_arm_rate_of_fat);
  outputData.push(left_arm_muscle_mass);

  // // 体幹部
  var trunk_index = splitText.indexOf("体幹部");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", trunk_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", trunk_index) + 1;
  var trunk_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var trunk_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(trunk_rate_of_fat);
  outputData.push(trunk_muscle_mass);

  return outputData;
}
```
:pencil2:追記

```javascript:line_api_scripts.gs
(省略)
　・
　・
　・
// 以下を追記
// LINE名の取得
function getDisplayName(user_id) {
  const endPoint = "https://api.line.me/v2/bot/profile/" + user_id;
  const res = UrlFetchApp.fetch(endPoint, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + TOKEN,
    },
    method: "GET",
  });

  const userDisplayName = JSON.parse(res.getContentText()).displayName;
  return userDisplayName;
}
```
### コードの簡単な解説
#### 2-1. 2回目以降のGASコードデプロイの注意点
webhookのときに初回デプロイをしたと思うのですが、2回目以降のデプロイは少し注意が必要です。
デプロイするたびにURLが変わってしまうと、毎回毎回URLを再設定しないといけなくなります。
なので、以下のようなデプロイ方法にしてください～～

<details><summary>デプロイ設定方法2回目以降</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/99c9f61a-2930-4d38-8e8a-66c5bd87d7dc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/a7cff34f-b976-3c37-dda1-d1740c4ce5de.png)
</details>

#### 2-2. ユーザー一覧データの扱い
ユーザー一覧データはスプレッドシートに格納しています。
そこにデータを出し入れして管理しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/40a840d2-d7f4-baa8-9492-96e7e5c352e9.png)

#### 2-3. テキストの分解
`spreadsheet_scripts.gs > extractInputData()`にて、先ほどOCRしたテキストをスプレッドに入力できる形にしています。
ここに関しては正規化を使って、ゴリゴリに分解してます

1. ログでOCRされたテキストを確認
1. スプレッドシートで欲しいデータの形を確認
1. どう変換すればいいのか考えて、正規化を用いて空白・改行を消したり、文字列と数字を分けたり
1. できた配列データから欲しいデータを抽出

という手順で考えて、コードを組みました。

## 3. 結果をユーザーに送信する
### GASソースコード
:tools:中間の下記部分を変更

```javascript:main.gs(変更前)
  // 返信token, message_type取得
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;
```

```javascript:main.gs(変更後)
  // user_id, 返信token, message_type取得
  var user_id = json.events[0].source.userId;
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;
```
:pencil2:追記

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {
　
(省略)
　・
　・
　・
　// 以下を追記
  // 結果のスプレッドシートのURLを返信
　replyText("内容を反映しました！\nいままでの記録は以下のURLで確認しましょう。\n" + ss.getUrl(), replyToken);
}
```
:tools:先頭の2行を変更

```javascript:line_api_scripts.gs(変更前)
// トークン
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
```

```javascript:line_api_scripts.gs(変更後)
// トークンとか、RESTapiとか、
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
var replyURL = "https://api.line.me/v2/bot/message/reply";
```
:pencil2:追記

```javascript:line_api_scripts.gs
(省略)
　・
　・
　・
//テキストで返信
function replyText(text,replyToken) {

  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": text
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}
```

### コードの簡単な解説
。。。。。。特にない...:sweat_smile:
書きましょう！

# 完成！！....のはずが？
さぁ！これで完成です！
テストも行って、無事動くことが確認できました！

...。
..........。
けどなんかもの足りない。。
ということで、どうせアドベントカレンダーを出すのであれば、**クソ機能** を追加しちゃいましょう！:upside_down:

# クソ機能要件定義
### ①飯テロ機能
ダイエットをする上で7-8割方が行うであろう**食事制限**、**糖質制限**
それはダイエットをする上で、1、2を争う過酷なものである。

いままで**食べることが好き**で、自分の欲求を満たすように食ってきた人たちの成れの果てで行うのが**ダイエット**である。(言い過ぎ)
そう思うとその趣味、人生の楽しみを奪われるのだから、食事制限の過酷さは想像に難くない。

よって一般に普及しているダイエットアプリに飯テロ機能なつくわけがない。

**だからつけよう**(強気)

### ②「それ言わなくてもよくない？」機能
ダイエットするのに大事なのは何だと思いますか？
そう、目標設定である。目標がより明確に見えれば見えるほど、人はそこに向かって頑張れるのだ。

そしてダイエットでよく耳にする目標設定が、
「女優の○○さんみたいにスタイルよくなりたい～～」
「俳優の○○さんみたいにシュッとしたいな。」

ここでミソなのが、カッコイイ人を目標におくことである。
たとえ、**スタイルが完璧でもカッコよくはない人は目標におかないのである。**
ダイエットした先が、**カッコよくはない人**になりたくないのである。
...お分かりだろうか。

**そこを気付かせる機能をつけます**(さらに強気)

# 追加開発
2つのセクションを追加して実装していきます。
4. 飯テロ機能
5. 「それ言わなくてもよくない？」機能
追加の機能は以下のファイルに入れておきます。

| ファイル名 | 記述内容 |
|:-|:-:|
| terro.gs | ダイエットに対してのテロ行為 |

## 4. 飯テロ機能
- Botに画像以外を送ったら、飯テロ画像が帰ってくる。
- BotがOCRしている間の待ち時間で飯テロ画像が送らてくる。
- 定期的に飯テロ画像が送られてくる。

### GASソースコード
:tools:mainコードを書き換え

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {

  // LINEのメッセージから必要なデータだけ抜粋
  var json = JSON.parse(event.postData.contents);

  // user_id, 返信token, message_type取得
  var user_id = json.events[0].source.userId;
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;

  // 画像取得ための、message_id取得
  var messageId = json.events[0].message.id;

  // 投稿が画像以外のとき
  if (type != "image"){
    // 画像以外を受信したときの処理
    // 飯テロ画像取得
    var image_urls = getImageAndThumbnail();
    replyText("体組成計結果の画像を送ってね！", replyToken);
    sendImage(image_urls[0], image_urls[1], user_id);

  } else {
    // 「入力中」と送信
    replyText("いま入力中です。\n下の画像を見ながらちょっとまってね！", replyToken);

    // 飯テロ画像取得
    var image_urls = getImageAndThumbnail();
    sendImage(image_urls[0], image_urls[1], user_id);
    
    //投稿が画像の時 画像のURLを生成し画像取得
    var imageURL = "https://api-data.line.me/v2/bot/message/" + messageId + "/content/";
    var image = getImage(imageURL);

    //取得した画像を文字に起こす
    var ocrText = getText(image);

    // 送信者専用のSSを取得
    var ss = getMySpreadSheet(user_id);

    // 今回の結果をスプレッドシートに入力
    inputResultForSS(ss, ocrText);

    //起こした文字をLINEで返信する
　  sendText("内容を反映しました！\nいままでの記録は以下のURLで確認しましょう。\n" + ss.getUrl(), user_id);
  }
}

```
:pencil2:追記

```javascript:spreadsheet_scripts.gs
(省略)
　・
　・
　・
// 登録されているユーザーIDをすべて取得
function subscribedUserIds() {
  var ss_id = PropertiesService.getScriptProperties().getProperty("USER_LIST_SS_ID");
  var ss = SpreadsheetApp.openById(ss_id);
  var sh = ss.getActiveSheet();
  const shLastRow = sh.getLastRow();
  var ss_values = sh.getRange(1,1, shLastRow, 1).getValues();

  // 先頭1行は見出しなので削除
  ss_values.shift();
  return ss_values;
}
```
:tools:先頭の2行を変更

```javascript:line_api_scripts.gs(変更前)
// トークンとか、RESTapiとか、
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
var replyURL = "https://api.line.me/v2/bot/message/reply";
```

```javascript:line_api_scripts.gs(変更後)
// トークンとか、RESTapiとか、
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
var replyURL = "https://api.line.me/v2/bot/message/reply";
var pushURL = "https://api.line.me/v2/bot/message/push";
```
:pencil2:追記

```javascript:line_api_scripts.gs
(省略)
　・
　・
　・
//テキストで送信
function sendText(text,user_id) {

  //メッセージ作成
  var payload = JSON.stringify({
      "to": user_id,
      "messages": [{
        "type": "text",
        "text": text
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(pushURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

//画像で返信
function replyImage(main_image_url, thumbnail_image_url,replyToken) {

  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "image",
        "originalContentUrl": main_image_url,
        "previewImageUrl": thumbnail_image_url
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

//画像で送信
function sendImage(main_image_url, thumbnail_image_url, user_id) {

  //メッセージ作成
  var payload = JSON.stringify({
      "to": user_id,
      "messages": [{
        "type": "image",
        "originalContentUrl": main_image_url,
        "previewImageUrl": thumbnail_image_url
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(pushURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}
```
:pencil2:新規作成

```javascript:terro.gs
// ユーザー全員に飯テロの画像を送りつける
function meshiterro() {
  var image_urls = getImageAndThumbnail();
  var user_ids = subscribedUserIds();
  for (let i = 0; i < user_ids.length; i++) {
    sendImage(image_urls[0], image_urls[1], user_ids[i][0]);
  }
}

// 飯テロ画像を取得
function getImageAndThumbnail() {
  // エンドポイント
  var FLICKR_ENDPOINT_URI = 'https://api.flickr.com/services/rest/';

  // 何枚取得して、その中から画像を選ぶか
  var NUM_OF_PHOTOS = 20;

  // メシテロワード
  var MESHI_TERO_WORDS = [
    'カレー',
    '焼きおにぎり',
    'ラーメン',
    'チャーハン',
    '焼肉',
    'フォンダンショコラ',
    'ブリュレ',
    'パンケーキ',
    'ハンバーグ',
    'ハンバーガー',
    'お茶漬け',
    '卵かけごはん',
    '寿司',
    'pizza',
    '焼鳥',
    '豚汁',
    '麻婆豆腐',
    'からあげ',
  ];
  // リクエストを送るパラメータ
  params = {
      'method': 'flickr.photos.search',
      'api_key': PropertiesService.getScriptProperties().getProperty("FLICKR_API_KEY"),
      'text': MESHI_TERO_WORDS[Math.floor(Math.random()*MESHI_TERO_WORDS.length)],
      'license': '1,2,3,4,5,6', // Creative Commons Lisense
      'per_page': NUM_OF_PHOTOS,
      'format': 'json',
      'nojsoncallback': '1',
      'privacy_filter': '1', // 1 public photos
      'content_type': '1', // 1 for photos only
      'sort': 'relevance'
  };
  // リクエスト送受信
  var resp = UrlFetchApp.fetch(FLICKR_ENDPOINT_URI, {
    'method': 'post',
    'payload': params
  });

  // jsonを変換して、画像をURLにする
  var resp_json = JSON.parse(resp);
  photo_info = resp_json.photos.photo[Math.floor(Math.random()*NUM_OF_PHOTOS)];
  tmp_url = 'https://farm' + photo_info.farm + '.staticflickr.com/' + photo_info.server + '/' + photo_info.id + '_' + photo_info.secret;
  image_url = tmp_url + '.jpg'
  thumbnail_url = tmp_url + '_m.jpg'
  return [image_url, thumbnail_url];
}
```

### コードの簡単な解説
#### 4-1. 飯テロ画像の持ってき方
「飯テロ画像か持ってこれるapiないかな～～、あるわけないけど。」
と思ってGoogleで検索してみた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/72011fcc-e619-196a-a0c6-dd6133709cb1.png)
**あんのかいwwwwwwwww**
ということでありがたく使わせていただきます。

https://qiita.com/muk-ai/items/0c41b1bd35560effc211

この記事にある**[Flickr API](https://www.flickr.com/services/api/)**は、なんと著作権問題も大丈夫らしい。素晴らしすぎる。

#### 4-2. コードを定期実行するには
GASの機能でトリガーというものがあります。
それを使えば指定したコードを時間指定で実行してくれます。
今回だと飯テロ画像を定期的にユーザー全員に送信したいので、`terro.gs > meshiterro()` にトリガーを設定します。

<details><summary>トリガーの設定方法はこちら</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/b2e10c5c-67bf-800b-ae67-b3210d600c81.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/0074783c-b428-431a-cd87-9b9b069bdd2f.png)
</details>

## 5. 「それ言わなくてもよくない？」機能
- ユーザーのbmiとほぼ同じ**芸人**をユーザーに伝える
- もうちょっと痩せたらほぼ同じbmiになる**芸人**をユーザーに伝える

(今回使用させていただいた芸人の皆様、申し訳ございません。)

### GASソースコード
:tools:中間の下記部分を変更

```javascript:main.gs(変更前)
    // 今回の結果をスプレッドシートに入力
    inputResultForSS(ss, ocrText);
```

```javascript:main.gs(変更後)
    // 今回の結果をスプレッドシートに入力
    var bmi = inputResultForSS(ss, ocrText);
```

:pencil2:追記

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {
　
(省略)
　・
　・
　・
    // BMIが似ている芸能人を取得
    var now_similary_person = getBmiPerson(bmi);
    var next_similary_person = getBmiPerson(bmi - 1);

    //起こした文字をLINEで返信する
    sendText("内容を反映しました！\nいまのあなたの体型は\n" + now_similary_person + "さん\nくらいですね。\nあとすこしで\n" + next_similary_person + "さん\nくらいのの体型になれます！頑張りましょう！！\n\nいままでの記録は以下のURLで確認しましょう。\n" + ss.getUrl(),user_id);
  }
}
```
:tools:変更

```javascript:spreadsheet_scripts.gs(変更前)
// スプレッドシートに体組成データを入力
function inputResultForSS(ss, ocrText) {
  // 体組成データを、スプレッドシートに入力できる形に変換
  var inputData = extractInputData(ocrText);
```

```javascript:spreadsheet_scripts.gs(変更後)
// スプレッドシートに体組成データを入力
function inputResultForSS(ss, ocrText) {
  // 体組成データを、スプレッドシートに入力できる形に変換
  var inputData = extractInputData(ocrText);

  // BMI取得
  var bmi = inputData[8];
```

:pencil2:追記

```javascript:spreadsheet_scripts.gs
// スプレッドシートに体組成データを入力
function inputResultForSS(ss, ocrText) {
(省略)
　・
　・
　・
  return bmi;
}
```
:pencil2:追記

```javascript:terro.gs
(省略)
　・
　・
　・
// BMI似てる人を返すw
function getBmiPerson(bmi) {
  if (bmi < 16) {
    return "カラ〇カ矢部";
  } else if (bmi >= 16 && bmi < 17) {
    return "アン〇ールズ山根";
  } else if (bmi >= 17 && bmi < 18) {
    return "アン〇ールズ田中";
  } else if (bmi >= 18 && bmi < 19) {
    return "千〇ジュニア";
  } else if (bmi >= 19 && bmi < 20) {
    return "コ〇リコ田中";
  } else if (bmi >= 20 && bmi < 21) {
    return "ミッツ〇ングローブ";
  } else if (bmi >= 21 && bmi < 22) {
    return "千〇せいじ";
  } else if (bmi >= 22 && bmi < 23) {
    return "トータル〇ンボス藤田";
  } else if (bmi >= 23 && bmi < 24) {
    return "楽〇んご";
  } else if (bmi >= 24 && bmi < 25) {
    return "野性〇弾くっきー";
  } else if (bmi >= 25 && bmi < 26) {
    return "ケン〇ーコバヤシ";
  } else if (bmi >= 26 && bmi < 27) {
    return "出〇哲郎";
  } else if (bmi >= 27 && bmi < 28) {
    return "かま〇たち山内";
  } else if (bmi >= 28 && bmi < 29) {
    return "とろ〇ーモンくぼた";
  } else if (bmi >= 29 && bmi < 30) {
    return "バナ〇マン日村";
  } else if (bmi >= 30 && bmi < 31) {
    return "カン〇ング竹山";
  } else if (bmi >= 31 && bmi < 32) {
    return "ロ〇ート秋山";
  } else if (bmi >= 32 && bmi < 33) {
    return "長〇小力";
  } else if (bmi >= 33) {
    return "TK〇木下";
  }
}
```

### コードの簡単な解説
特になし！

# ついに完成
### LINE Botの挙動
![ezgif-4-769bc221fbd7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/a6d8e8c5-2f16-171e-e8e1-4ef5299371b6.gif)

### SpreadSheet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/d1acc49a-dcf8-ce73-5859-703c3b8e4c1f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437352/ce05c54a-1730-2d9d-f675-f06588c9998b.png)

# さいごに
ここまで読んでいただきありがとうございます！

最初はちょっとだけコード書こうと思ったら、最後には結構大掛かりになってしまいました。:zipper_mouth:
けど、最近こういうように書きたいコードを気ままに書くことをしていなかったので、とても楽しかったです～

ところどころにナレッジを入れ込んだので、お役に立てていたら幸いです。
エンジニアに幸あれ。

# 全体ソースコード

<details>
<summary>main.gs</summary><div>

```javascript:main.gs
//LINEに投稿があったら実行
function doPost(event) {

  // LINEのメッセージから必要なデータだけ抜粋
  var json = JSON.parse(event.postData.contents);

  // user_id, 返信token, message_type取得
  var user_id = json.events[0].source.userId;
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;

  // 画像取得ための、message_id取得
  var messageId = json.events[0].message.id;

  // 投稿が画像以外のとき
  if (type != "image"){
    // 画像以外を受信したときの処理
    // 飯テロ画像取得
    var image_urls = getImageAndThumbnail();
    replyText("体組成計結果の画像を送ってね！", replyToken);
    sendImage(image_urls[0], image_urls[1], user_id);

  } else {
    // 「入力中」と送信
    replyText("いま入力中です。\n下の画像を見ながらちょっとまってね！", replyToken);

    // 飯テロ画像取得
    var image_urls = getImageAndThumbnail();
    sendImage(image_urls[0], image_urls[1], user_id);
    
    //投稿が画像の時 画像のURLを生成し画像取得
    var imageURL = "https://api-data.line.me/v2/bot/message/" + messageId + "/content/";
    var image = getImage(imageURL);

    //取得した画像を文字に起こす
    var ocrText = getText(image);

    // 送信者専用のSSを取得
    var ss = getMySpreadSheet(user_id);

    // 今回の結果をスプレッドシートに入力
    var bmi = inputResultForSS(ss, ocrText);

    // BMIが似ている芸能人を取得
    var now_similary_person = getBmiPerson(bmi);
    var next_similary_person = getBmiPerson(bmi - 1);

    //起こした文字をLINEで返信する
    sendText("内容を反映しました！\nいまのあなたの体型は\n" + now_similary_person + "さん\nくらいですね。\nあとすこしで\n" + next_similary_person + "さん\nくらいのの体型になれます！頑張りましょう！！\n\nいままでの記録は以下のURLで確認しましょう。\n" + ss.getUrl(),user_id);

  }
}
```
</div></details>

<details>
<summary>spreadsheet_scripts.gs</summary><div>

```javascript:spreadsheet_scripts.gs
// 自分のスプレッドシートがあれば取得、なければ新規作成
function getMySpreadSheet(user_id) {
  // ユーザーID一覧データを取得
  var ss_id = PropertiesService.getScriptProperties().getProperty("USER_LIST_SS_ID");
  var ss = SpreadsheetApp.openById(ss_id);
  var sh = ss.getActiveSheet();
  const shLastRow = sh.getLastRow();
  var ss_values = sh.getRange(1,1, shLastRow, 3).getValues();

  //  ユーザーIDが登録済みかどうか調査
  for(let i = 1; i < shLastRow; i++) {
    this_row = ss_values[i];
    if (this_row[0] === user_id) {
      // 登録済みの場合、既存のspreadsheetを返す
      var subscribed_ss = SpreadsheetApp.openById(this_row[2]);
      return subscribed_ss;
    }
  }

  // 初回登録だった場合、テンプレのスプレッドシートをコピーして返す
  // テンプレSSID
  var temp_ss_id = PropertiesService.getScriptProperties().getProperty("RECORDING_TEMPLATE_SS_ID");
  var temp_ss = SpreadsheetApp.openById(temp_ss_id);
  
  // メッセージ送信主のLINE名取得
  var user_name = getDisplayName(user_id);

  // テンプレをコピーして、タイトルにLINE名を入れる
  var new_ss_name = user_name + "さんの体組成シート";
  var new_ss = temp_ss.copy(new_ss_name);

  // スプレッドシート共有権限付与
  var drive_ss = DriveApp.getFileById(new_ss.getId());
  var access = DriveApp.Access.ANYONE_WITH_LINK;
  var permission = DriveApp.Permission.EDIT;
  drive_ss.setSharing(access, permission);

  // ユーザー一覧データに新規登録
  var input_row = [[user_id, user_name, new_ss.getId()]];
  sh.getRange(shLastRow + 1, 1, 1, 3).setValues(input_row);

  return new_ss;
}

// スプレッドシートに体組成データを入力
function inputResultForSS(ss, ocrText) {
  // 体組成データを、スプレッドシートに入力できる形に変換
  var inputData = extractInputData(ocrText);

  // BMI取得
  var bmi = inputData[8];

  
  // 性別, 年齢, 身長代入
  var sh = ss.getActiveSheet();
  var lastCol = sh.getRange(7, sh.getMaxColumns()).getNextDataCell(SpreadsheetApp.Direction.PREVIOUS).getColumn();
  var input_row = [[inputData.shift(), inputData.shift(), inputData.shift()]];
  sh.getRange(4, 2, 1, 3).setValues(input_row);

  // その他データを以下に代入
  for (let i = 7; i <= 22; i++) {
    sh.getRange(i, lastCol + 1).setValue(inputData.shift());
  }
  return bmi;
}

// 体組成データをスプレッドシートに入力できる形に変換
function extractInputData(ocrText) {
  // 取得したデータの空白、改行、タブをすべて取り除く
  var preSplitText = ocrText.split(/\t|\s|\n/);

  // さらにデータの数字と文字列を分断(あとでデータ抽出しやすい)
  var splitText = [];
  for (let i = 0; i < preSplitText.length; i++) {
    var ssi = 0;
    var is_str = Boolean(preSplitText[i][0].match(/[^0-9.+]/g));
    for (let j = 0; j < preSplitText[i].length; j++) {
      // console.log("is_str = " + String(is_str) + ", pre_str = " + String(Boolean(preSplitText[i][j].match(/[^0-9.+]/g))));
      if (Boolean(preSplitText[i][j].match(/[^0-9.+]/g)) ^ is_str) {
        splitText.push(preSplitText[i].slice(ssi, j));
        ssi = j;
        is_str = Boolean(preSplitText[i][j].match(/[^0-9.+]/g));
      }
    }
    splitText.push(preSplitText[i].slice(ssi, preSplitText[i].length + 1));
  }
  // 最後に返す配列
  var outputData = [];

  // 性別年齢身長抽出
  var sex = splitText[splitText.indexOf("性別") + 1];
  var age = splitText[splitText.indexOf("身長") - 1];
  var height = splitText[splitText.indexOf("身長") + 1];
  outputData.push(sex);
  outputData.push(age);
  outputData.push(height);

  // その他データ抽出
  // // 全身
  var index_before_the_day = splitText.indexOf("E");
  var the_day = new Date("20" + splitText[index_before_the_day + 2] + "/" +
                         splitText[index_before_the_day + 3] + "/" +
                         splitText[index_before_the_day + 4]);
  var weight = Number(splitText[splitText.indexOf("体重") + 1]);
  var prev_index_rate_of_fat = splitText.indexOf("体脂肪率") + 1;
  var rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var prev_index_muscle_mass = splitText.indexOf("筋肉量") + 1;
  var muscle_mass = Number(splitText[prev_index_muscle_mass]);
  var basal_metabolic_rate = Number(splitText[splitText.indexOf("基礎代謝量") + 1]);
  var bmi = Number(splitText[splitText.indexOf("BMI") + 1]);
  outputData.push(the_day);
  outputData.push(weight);
  outputData.push(rate_of_fat);
  outputData.push(muscle_mass);
  outputData.push(basal_metabolic_rate);
  outputData.push(bmi);

  // // 右足
  var right_foot_index = splitText.indexOf("右足");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", right_foot_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", right_foot_index) + 1;
  var right_foot_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var right_foot_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(right_foot_rate_of_fat);
  outputData.push(right_foot_muscle_mass);

  // // 左足
  var left_foot_index = splitText.indexOf("左足");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", left_foot_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", left_foot_index) + 1;
  var left_foot_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var left_foot_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(left_foot_rate_of_fat);
  outputData.push(left_foot_muscle_mass);

  // // 右腕
  var right_arm_index = splitText.indexOf("右腕");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", right_arm_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", right_arm_index) + 1;
  var right_arm_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var right_arm_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(right_arm_rate_of_fat);
  outputData.push(right_arm_muscle_mass);

  // // 左腕
  var left_arm_index = splitText.indexOf("左腕");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", left_arm_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", left_arm_index) + 1;
  var left_arm_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var left_arm_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(left_arm_rate_of_fat);
  outputData.push(left_arm_muscle_mass);

  // // 体幹部
  var trunk_index = splitText.indexOf("体幹部");
  prev_index_rate_of_fat = splitText.indexOf("体脂肪率", trunk_index) + 1;
  prev_index_muscle_mass = splitText.indexOf("筋肉量", trunk_index) + 1;
  var trunk_rate_of_fat = Number(splitText[prev_index_rate_of_fat]);
  var trunk_muscle_mass = Number(splitText[prev_index_muscle_mass]);
  outputData.push(trunk_rate_of_fat);
  outputData.push(trunk_muscle_mass);

  return outputData;
}

// 登録されているユーザーIDをすべて取得
function subscribedUserIds() {
  var ss_id = PropertiesService.getScriptProperties().getProperty("USER_LIST_SS_ID");
  var ss = SpreadsheetApp.openById(ss_id);
  var sh = ss.getActiveSheet();
  const shLastRow = sh.getLastRow();
  var ss_values = sh.getRange(1,1, shLastRow, 1).getValues();

  // 先頭1行は見出しなので削除
  ss_values.shift();
  return ss_values;
}
```
</div></details>

<details>
<summary>line_api_scripts.gs</summary><div>

```javascript:line_api_scripts.gs
// トークンとか、RESTapiとか、
var TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_API_TOKEN");
var replyURL = "https://api.line.me/v2/bot/message/reply";
var pushURL = "https://api.line.me/v2/bot/message/push";

//投稿から画像を取得
function getImage(imageURL) {

  //取得と同時にBlobしておく
  var image = UrlFetchApp.fetch(imageURL, {
       "headers": {
         "Content-Type": "application/json; charset=UTF-8",
         "Authorization": "Bearer " + TOKEN,
       },
       "method": "get"
  }).getBlob();

  return image;

}

//テキストで返信
function replyText(text,replyToken) {

  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": text
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

//テキストで送信
function sendText(text,user_id) {

  //メッセージ作成
  var payload = JSON.stringify({
      "to": user_id,
      "messages": [{
        "type": "text",
        "text": text
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(pushURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

//画像で返信
function replyImage(main_image_url, thumbnail_image_url,replyToken) {

  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "image",
        "originalContentUrl": main_image_url,
        "previewImageUrl": thumbnail_image_url
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

//画像で送信
function sendImage(main_image_url, thumbnail_image_url, user_id) {

  //メッセージ作成
  var payload = JSON.stringify({
      "to": user_id,
      "messages": [{
        "type": "image",
        "originalContentUrl": main_image_url,
        "previewImageUrl": thumbnail_image_url
      }]
  });

  //メッセージ送信
  UrlFetchApp.fetch(pushURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;
}

// LINE名の取得
function getDisplayName(user_id) {
  const endPoint = "https://api.line.me/v2/bot/profile/" + user_id;
  const res = UrlFetchApp.fetch(endPoint, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + TOKEN,
    },
    method: "GET",
  });

  const userDisplayName = JSON.parse(res.getContentText()).displayName;
  return userDisplayName;
}
```
</div></details>

<details>
<summary>drive_scripts.gs</summary><div>

```javascript:drive_scripts.gs
//画像を文字に起こす
function getText(image) {

  //画像のタイトルとタイプを取得
  var title = image.getName();
  var mimeType = image.getContentType();

  //ドキュメントを作成し画像を挿入（DriveAPIの設定が必要）
  var resource = {title: title, mimeType: mimeType};
  var fileId = Drive.Files.insert(resource, image, {ocr: true}).id;

  //ドキュメントからテキストを取得したらドキュメントを削除
  var document = DocumentApp.openById(fileId);
  var ocrText = document.getBody().getText().replace("\n", "");
  Drive.Files.remove(fileId);

  return ocrText;

}
```
</div></details>

<details>
<summary>terro.gs</summary><div>

```javascript:terro.gs
// ユーザー全員に飯テロの画像を送りつける
function meshiterro() {
  var image_urls = getImageAndThumbnail();
  var user_ids = subscribedUserIds();
  for (let i = 0; i < user_ids.length; i++) {
    sendImage(image_urls[0], image_urls[1], user_ids[i][0]);
  }
}

// 飯テロ画像を取得
function getImageAndThumbnail() {
  // エンドポイント
  var FLICKR_ENDPOINT_URI = 'https://api.flickr.com/services/rest/';

  // 何枚取得して、その中から画像を選ぶか
  var NUM_OF_PHOTOS = 20;

  // メシテロワード
  var MESHI_TERO_WORDS = [
    'カレー',
    '焼きおにぎり',
    'ラーメン',
    'チャーハン',
    '焼肉',
    'フォンダンショコラ',
    'ブリュレ',
    'パンケーキ',
    'ハンバーグ',
    'ハンバーガー',
    'お茶漬け',
    '卵かけごはん',
    '寿司',
    'pizza',
    '焼鳥',
    '豚汁',
    '麻婆豆腐',
    'からあげ',
  ];
  // リクエストを送るパラメータ
  params = {
      'method': 'flickr.photos.search',
      'api_key': PropertiesService.getScriptProperties().getProperty("FLICKR_API_KEY"),
      'text': MESHI_TERO_WORDS[Math.floor(Math.random()*MESHI_TERO_WORDS.length)],
      'license': '1,2,3,4,5,6', // Creative Commons Lisense
      'per_page': NUM_OF_PHOTOS,
      'format': 'json',
      'nojsoncallback': '1',
      'privacy_filter': '1', // 1 public photos
      'content_type': '1', // 1 for photos only
      'sort': 'relevance'
  };
  // リクエスト送受信
  var resp = UrlFetchApp.fetch(FLICKR_ENDPOINT_URI, {
    'method': 'post',
    'payload': params
  });

  // jsonを変換して、画像をURLにする
  var resp_json = JSON.parse(resp);
  photo_info = resp_json.photos.photo[Math.floor(Math.random()*NUM_OF_PHOTOS)];
  tmp_url = 'https://farm' + photo_info.farm + '.staticflickr.com/' + photo_info.server + '/' + photo_info.id + '_' + photo_info.secret;
  image_url = tmp_url + '.jpg'
  thumbnail_url = tmp_url + '_m.jpg'
  return [image_url, thumbnail_url];
}

// BMI似てる人を返すw
function getBmiPerson(bmi) {
  if (bmi < 16) {
    return "カラ〇カ矢部";
  } else if (bmi >= 16 && bmi < 17) {
    return "アン〇ールズ山根";
  } else if (bmi >= 17 && bmi < 18) {
    return "アン〇ールズ田中";
  } else if (bmi >= 18 && bmi < 19) {
    return "千〇ジュニア";
  } else if (bmi >= 19 && bmi < 20) {
    return "コ〇リコ田中";
  } else if (bmi >= 20 && bmi < 21) {
    return "ミッツ〇ングローブ";
  } else if (bmi >= 21 && bmi < 22) {
    return "千〇せいじ";
  } else if (bmi >= 22 && bmi < 23) {
    return "トータル〇ンボス藤田";
  } else if (bmi >= 23 && bmi < 24) {
    return "楽〇んご";
  } else if (bmi >= 24 && bmi < 25) {
    return "野性〇弾くっきー";
  } else if (bmi >= 25 && bmi < 26) {
    return "ケン〇ーコバヤシ";
  } else if (bmi >= 26 && bmi < 27) {
    return "出〇哲郎";
  } else if (bmi >= 27 && bmi < 28) {
    return "かま〇たち山内";
  } else if (bmi >= 28 && bmi < 29) {
    return "とろ〇ーモンくぼた";
  } else if (bmi >= 29 && bmi < 30) {
    return "バナ〇マン日村";
  } else if (bmi >= 30 && bmi < 31) {
    return "カン〇ング竹山";
  } else if (bmi >= 31 && bmi < 32) {
    return "ロ〇ート秋山";
  } else if (bmi >= 32 && bmi < 33) {
    return "長〇小力";
  } else if (bmi >= 33) {
    return "TK〇木下";
  }
}
```
</div></details>
# 引用
- メインで参考
https://qiita.com/YasumiYasumi/items/49479bfaa683fefd09c5

- messaging apiとGASの連携
https://qiita.com/MxShun/items/7a563a795d41cdc0f1dc

- 本番環境でGASのログを出す
https://www.hidetoshl.com/gas-gcp/

- Google Cloud Platform環境下でGoogle Drive APIを有効にする
https://www.virment.com/google-drive-api-activate/

- Messaging APIをつかってプロフィール情報取得
https://developers.line.biz/ja/reference/messaging-api/#get-profile

- GASでTOKEN直接打ちたくない。。
https://qiita.com/massa-potato/items/2209ff367d65c5dd6181