この記事について
Qiita初投稿のうえ、プログラミング初心者です。至らぬ点がたくさんあるとは思いますが暖かく見守ってください…
Google Apps Scriptを用いて毎朝LINEでその日のいろいろを教えてくれるBotを作ったので解説も兼ねてご紹介します。
第1回, 第2回ではセットアップの説明を、第3回以降ではコードの詳細な解説をしていきます。
この記事で取り扱う内容
- Google Apps Script (GAS)の基本
 - JavaScriptの基本~発展
 - LINE Messaging API
 - Flex Message
 - TimeTree API(予定取得)
 - Googleカレンダー(祝日取得)
 
実際どんな感じ?
表示されるもの
- 日付
 - 祝日
 - 収集があるごみ
 - 天気(気温)
 - 予定(TimeTreeから)
 
GASプロジェクトを作成
こちらから新規のGASプロジェクトを作成します。
アドレスバーに
script.newで新規プロジェクトを作成できます。
.new ドメインを利用したショートカット一覧(英語)
注意すること
V8 ランタイムを搭載したプロジェクトでのみ機能します(新規プロジェクトは最初から搭載しています)。
既存のプロジェクトを使う場合は、それがV8 ランタイムを搭載したプロジェクトであることを確認してください。
プロジェクトを開いた際にこのプロジェクトは Chrome V8 を搭載した新しい Apps Script ランタイムで実行しています。という表示があれば問題ありません(詳細)。
ええい、これが完成したコードだ!
このコードをコピーしてGASプロジェクトにドーン!!
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);
}
もちろん、これだけでは機能しません。
まだまだ設定することがありますが、長くなりましたので、その手順は次の記事にてご紹介します。
