GASで毎朝その日の予定とかを通知するLINE Bot [第4回]

# この記事について

前回の続きです。

+ [第1回](//qiita.com/items/634bf26f7d1cb30e8944)
+ [第2回](//qiita.com/items/5d5925c295b88d79f882)
+ [第3回](//qiita.com/items/5a2fbdbf11b4be6f8808)
+ 第4回
 + [Flex Message](#flex-message)
 + [祝日・ごみ収集日の表示](#祝日ごみ収集日の表示)
 + [予定の表示](#予定の表示)
 + [Messaging APIに送信](#messaging-apiに送信)

# コード解説

前回の続きをどんどん解説していきます。まずはこれ。

### Flex Message

```js
let flex = {
  'type': 'bubble',
  'size': 'giga',
  'body': {
    'type': 'box',
    'layout': 'vertical',
    'contents': [
      {
        'type': 'text',
        'text': today,
        'weight': 'bold',
        'size': 'xxl',
        'flex': 0
      }, {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'filler'
          }, {
            'type': 'text',
            'text': weather,
            'size': 'lg',
            'color': '#444444',
            'flex': 0,
            'gravity': 'center'
          }
        ]
      }, {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'filler'
          }, {
            'type': 'text',
            'text': temp_l + '℃',
            'size': 'lg',
            'color': '#3f51b5',
            'flex': 0
          }, {
            'type': 'text',
            'text': '/',
            'size': 'lg',
            'color': '#444444',
            'margin': 'xs',
            'flex': 0
          }, {
            'type': 'text',
            'text': temp_h + '℃',
            'size': 'lg',
            'color': '#f44336',
            'margin': 'xs',
            'flex': 0,
            'gravity': 'center'
          }
        ]
      }, {
        'type': 'separator',
        'margin': 'xl',
        'color': '#808080'
      }, {
        'type': 'box',
        'layout': 'vertical',
        'contents': [
          //EVENTS
        ],
        'margin': 'xl'
      }
    ]
  }
};
```

これがBotの送信するデータの中核になるわけです。これは**Flex Message**というもので、LINEのメッセージをCSS Flexboxライクのフォーマットで表示するものなのだそう。
無の状態からこのデータを作ったわけではなく、LINEが[シミュレータ](https://developers.line.biz/flex-simulator/)を公開してくれている。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687929/02c39d51-2aa1-7eee-2b4f-627c2e45f7b9.png" height="400px">
右上の`View as JSON`でJSONデータを見ることができます。これをコピペして変数に置き換えるなどの処理をするだけでFlex Messageのデータを作れます。

### 祝日・ごみ収集日の表示

```js
if (holiday != '') {
  flex.body.contents[1].contents.splice(0, 0, {
    'type': 'text',
    'text': holiday,
    'size': 'md',
    'color': '#808080',
    'flex': 0,
    'gravity': 'center'
  });
}
```
`holiday`の中身があるときに実行されます。
`splice(start, count, data)`でJSONデータに割り込む形でデータを挿入します。`start`で割り込む位置、`count`で割り込む際に元データから消去する`start`からの要素の数を、`data`に挿入するデータを指定します。
`flex.body.contents[1].contents[0]`の位置にデータを割り込ませ、`count`は0なので元のデータは1ずつずれて更新されます。
<br>

```js
if (garbage[d.getDay()]) {
  let line = holiday == '' ? 1 : 2;
  flex.body.contents[line].contents.splice(0, 0, {
    'type': 'text',
    'text': garbage[d.getDay()],
    'size': 'md',
    'color': '#808080',
    'flex': 0,
    'gravity': 'center'
  });
}
```

ごみ収集日が0（＝false）以外である時に実行されます。
その日が祝日なら2行目、祝日でないなら1行目に表示させるため、三項演算子を用いて`line`を指定しています。
こちらも同じく`splice(start, count, data)`で挿入。

### 予定の表示

予定にはTimeTreeに登録したデータを利用します。

```js
const props = PropertiesService.getScriptProperties();

const TIMETREE_TOKEN = props.getProperty('TIMETREE_TOKEN');
const opt = {
  'headers': {
    'Authorization': 'Bearer ' + TIMETREE_TOKEN
  },
  'method': 'get'
};
```

`props`には、GAS拡張サービスの`PropertiesService`で`getScriptProperties()`を実行し、プロジェクトのプロパティに保存したAPIのトークンの配列を読み込んで代入しています。
`getProperty(key)`で指定したキーに対応する値を返します。

`opt`には、`UrlFetchApp.fetch(url, option)`のオプションを設定しています。
主にデータを取得する際に用いられるGETリクエストでは、オプションには基本的に以下のような内容を利用できます。

```js
{
  "method": "get",
  "headers": {
    /* header */
  }
}
```

`"method"`は`"get"`，`"headers"`にはヘッダというものを入れます。

今回のHTTPリクエストではAPIが要求するリクエストタイプ（`'method': 'get'`）とAPIキー（トークン）をAPI側に送信する必要があるため、ヘッダに認証情報を入れています。

> このトークンは、IDやパスワードなしにAPIが利用可能なので、Bearerトークンとよばれており、`'Authorization': 'Bearer XXXXXXXX`という記法で認証を受けるのが一般的です。

```js
const calendars = JSON.parse(UrlFetchApp.fetch('https://timetreeapis.com/calendars', opt).getContentText()).data;
const z = (t) => ('0' + t).slice(-2);
```

>TimeTree API > カレンダー一覧の取得
`https://timetreeapis.com/calendars`
[TimeTree API ドキュメント](//developers.timetreeapp.com/ja/docs/api)

`calendars`にはリクエストして返ってきたカレンダー一覧のJSONデータが入ります。
<br>
`const z = (t) => ('0' + t).slice(-2);`
ここでは、`t`が1文字の`'X'`だった時、`'0X'`となる「ゼロ埋め」の処理になっています。
`slice(n, m)`は、配列のn番目からm番目まで（m番目は含まない）をコピーする関数です。
`m`を省略し、`n`に負の数を入れると、最後からn番目までをコピーします。
第3回でも書きましたが、文字列は配列の一種なのでこの関数を利用できます。最後から2文字分を取得できるわけです。

**アロー関数**<sup>V8</sup> という記法で、`z`に関数を定義しています。

```js:アロー関数
let x = (a, b) => {
  return a * b;
}

//1行にすると、その計算結果がそのまま返される
let x = (a, b) => a * b;
```

<br>

```js
let ev_exists = false;
for (let calendar of calendars) {
  let cal = JSON.parse(UrlFetchApp
                       .fetch('https://timetreeapis.com/calendars/' + calendar.id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                       .getContentText()).data;
  for (let event of cal) {
    let {title, start_at, end_at, all_day} = event.attributes;
    start_at = new Date(start_at);
    end_at = new Date(end_at);
    let time = all_day ? '終日' : z(start_at.getHours()) + ':' + z(start_at.getMinutes()) + '-' + 
      z(end_at.getHours()) + ':' + z(end_at.getMinutes());
    let schedule = {
      'type': 'box',
      'layout': 'horizontal',
      'contents': [
        {
          'type': 'text',
          'text': time,
          'flex': 0,
          'color': '#808080',
          'gravity': 'center',
          'size': 'md'
        }, {
          'type': 'text',
          'text': title,
          'size': 'lg',
          'weight': 'bold',
          'color': '#606060',
          'flex': 0,
          'gravity': 'center',
          'margin': 'lg'
        }
      ],
      'margin': 'sm'
    };
    flex.body.contents[4].contents.push(schedule);
    ev_exists = true;
  }
}
```

第3回では`for-in`関数を紹介しましたが、今回は**`for-of`関数**<sup>V8</sup> です。

```js:for-of
let json = {a: 1, b: 3, c: 5};
let x = 0;
for (let data of json) {
  x += data;
}
// x => 9;
```

配列（連想配列含む）の要素ひとつひとつに対して処理を行う反復処理です。
配列の要素の数だけ実行されます。`data`には要素のデータが入ります。

```js
let cal = JSON.parse(UrlFetchApp
                       .fetch('https://timetreeapis.com/calendars/' + calendar.id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                       .getContentText()).data;
```
ここでは、取得した全種類のカレンダーひとつひとつのその日の予定を取得します。
そこから先は、

+ 予定をひとつひとつ取り出して
+ 分割代入で時間とタイトルと終日かどうかのデータを取得して
+ 三項演算子で`time`に終日なら`'終日'`、そうでないなら時間をゼロ埋めして代入して
+ JSONデータ作って
+ Flex Messageに突っ込む

だけです。<br>
もし予定が何もないなら、`ev_exists`は`false`となり、以下のコードが実行されます。

```js
if (!ev_exists) {
  flex.body.contents.splice(3, 2);
}
```

`splice(start, count, data)`についてはすでにご紹介しましたが、ここでは、`data`を省略すると、`start`から`count`の分だけデータが削除されるという仕様を使っています。
予定の上に引かれている罫線と、予定を格納するボックスを削除しています。

### Messaging APIに送信

さあ、いよいよ大詰めです。

```js
const LINE_TOKEN = props.getProperty('LINE_TOKEN');
const payload = {
  'messages': [
    {
      'type': 'flex',
      'altText': today,
      'contents': flex
    }
  ]
};

const opt_line = {
  'headers': {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + LINE_TOKEN
  },
  'method': 'post',
  'payload': JSON.stringify(payload)
};
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/broadcast', opt_line);
```

`PropertiesService`から、保存しているMessaging APIのトークンを`LINE_TOKEN`に代入します。
今回のHTTPリクエストの種類は`POST`なので、GAS側からデータを送信します。
`payload`にデータを代入します。
`opt_line`に`UrlFetchApp.fetch(url, option)`のオプションを設定します。
データを送信するので、今回はヘッダに`'Content-Type'`を設定します。データの形式です。
また、TimeTree APIと同様に認証情報を設定します。
`POST`のリクエストの場合は、`'method': 'post'`となります。
`'payload'`にデータを入れることで、Massaging APIにメッセージのデータが送信されます。

# まとめ
いかがでしたでしょうか。記事を書いている途中から力尽きそうになっていましたが、書ききることができました。
分割代入やアロー関数など、まだあまり使用例が多くないものをご紹介できたかと思います。
ご感想やご指摘などありましたらコメントいただけますと幸いです。
それでは、よいプログラミングライフを！