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

# この記事について

Qiita初投稿のうえ、プログラミング初心者です。至らぬ点がたくさんあるとは思いますが暖かく見守ってください…
Google Apps Scriptを用いて毎朝LINEでその日のいろいろを教えてくれるBotを作ったので解説も兼ねてご紹介します。
第1回, 第2回ではセットアップの説明を、第3回以降ではコードの詳細な解説をしていきます。

+ 第1回
+ [第2回](//qiita.com/items/5d5925c295b88d79f882)
+ [第3回](//qiita.com/items/5a2fbdbf11b4be6f8808)
+ [第4回](//qiita.com/items/64315d311d44275465af)

### この記事で取り扱う内容

+ Google Apps Script (GAS)の基本
+ JavaScriptの基本～発展
+ LINE Messaging API
+ Flex Message
+ TimeTree API（予定取得）
+ Googleカレンダー（祝日取得）

## 実際どんな感じ？

下の画像のような通知が届きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687929/0e03cd89-42d3-b7ff-f4ed-b82968758213.jpeg" width="400px">

#### 表示されるもの
+ 日付
+ 祝日
+ 収集があるごみ
+ 天気（気温）
+ 予定（TimeTreeから）

# GASプロジェクトを作成

[こちら](https://script.new)から新規のGASプロジェクトを作成します。
> アドレスバーに`script.new`で新規プロジェクトを作成できます。
[.new ドメインを利用したショートカット一覧（英語）](https://whats.new/shortcuts/)

#### 注意すること

**V8 ランタイムを搭載したプロジェクトでのみ機能します**（新規プロジェクトは最初から搭載しています）**。**
既存のプロジェクトを使う場合は、それが**V8 ランタイムを搭載したプロジェクト**であることを確認してください。
プロジェクトを開いた際に`このプロジェクトは Chrome V8 を搭載した新しい Apps Script ランタイムで実行しています。`という表示があれば問題ありません（[詳細](https://developers.google.com/apps-script/guides/v8-runtime)）。

# ええい、これが完成したコードだ！

このコードをコピーしてGASプロジェクトにドーン！！

```javascript:コード.gs
function notify() {
  
  // Change these variables fit to your condition
  const garbage = [0, '資源ごみ', '可燃ごみ', 'カン・ビン', '不燃ごみ', '可燃ごみ', 'プラスチックごみ'];
  const area = '13101';

  
  const d = new Date();
  const today = (d.getMonth() + 1) + '月' + d.getDate() + '日（' + '日月火水木金土'[d.getDay()] + '）';
  
  
  const [event_holiday] = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com').getEventsForDay(d);
  const holiday = event_holiday ? event_holiday.getTitle() : '';
  
  
  let content = UrlFetchApp.fetch('https://static.tenki.jp/static-api/history/forecast/' + area + '.js').getContentText();
  content = JSON.parse(content.substring(content.indexOf('(') + 1, content.indexOf(');')));
  let {max_t: temp_h = "不明", min_t: temp_l = "不明", t: weather = "不明"} = content;
  
  const words = {
    '時々': '|',
    '一時': '|',
    'のち': '»',
    '晴': '☀',
    '曇': '☁',
    '雨': '☔',
    '雪': '⛄'
  };
  for (let key in words) {
    weather = weather.replace(key, words[key]);
  }
  
  
  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'
        }
      ]
    }
  };
  
  
  if (holiday != '') {
    flex.body.contents[1].contents.splice(0, 0, {
      'type': 'text',
      'text': holiday,
      'size': 'md',
      'color': '#808080',
      'flex': 0,
      'gravity': 'center'
    });
  }
  
  
  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'
    });
  }
  
  
  const props = PropertiesService.getScriptProperties();
  
  const TIMETREE_TOKEN = props.getProperty('TIMETREE_TOKEN');
  const opt = {
    'headers': {
      'Authorization': 'Bearer ' + TIMETREE_TOKEN
    },
    'method': 'get'
  };
  const calendars = JSON.parse(UrlFetchApp.fetch('https://timetreeapis.com/calendars', opt).getContentText()).data;
  const z = (t) => ('0' + t).slice(-2);
  
  let ev_exists = false;
  for (let events 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 events) {
      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;
    }
  }
  
  if (!ev_exists) {
    flex.body.contents.splice(3, 2);
  }
  
  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);
}
```

#### もちろん、これだけでは機能しません。
まだまだ設定することがありますが、長くなりましたので、その手順は次の記事にてご紹介します。