GASで金曜ロードショーを通知するLINE botを作ってみた

<h1>作ろうと思ったきっかけ</h1>
金曜ロードショーで放送する映画って自分では見ないような映画を知れるいい機会だと思っていて、見逃したり途中から見てしまうのはなんだかもったいない。
かといって録画するほどでもないし、わざわざ放送日や作品名を調べるのも面倒なので毎週金曜日にラインで通知してくれたらいいなと思って作りました。

<h1>仕様</h1>
<ul>
<li>毎週金曜日の18時に作品名と放送日を通知する。休みの場合は、次回の作品名と放送日を通知する。</li><br>
<li>ユーザーが「次回の放送内容」とメッセージを送信したら、次回の作品名と放送日を返信する。</li><br>
<li>ユーザーが「過去の放送内容」とメッセージを送信したら、過去の放送の作品名と放送日を返信する。</li><br>
<li>ユーザーがメッセージを打ち込まなくていいようにラインのリッチメニューを準備する。</li><br>
</ul>
botから通知するタイミングについては、前日や金曜日の早い時間帯に通知しても忘れてしまう可能性があるため、金曜日の18時がちょうどいいと考えました。

<h1>LINE Developersでアカウントの作成</h1>
下記のページからログインしてMessaging APIを選択してアカウントを作成します。

https://developers.line.biz/ja/services/messaging-api/

アカウントを作成したら<strong>トップ>プロバイダー名>アカウント名>Messaging API設定</strong>に移動して下記の設定をします。
<ul>
<li>チャネルアクセストークン（長期）を発行します。</li>
<li>Webhookの利用をオンにする。</li>
<li>WebhookURLにはGASでデプロイした時のURLが入るのであとで入力します。</li>
</ul>

それから<strong>トップ>プロバイダー名>アカウント名>チャネル基本設定</strong>の下のほうにある「あなたのユーザーID」というのがあります。
このユーザーIDはGASで開発するときのテストで使用するので覚えておいてください。

ここまで出来たらとりあえずライン側は準備完了です。

<h1>GASでプロジェクトの作成</h1>
Googleドライブにアクセスして左上の<strong>新規→その他→Google Apps Script</strong>を選んでプロジェクトを作成をします。
ここにコードを書いていきます。
トリガーを毎週金曜日午後6時～7時に設定します。

<h1>今回使用したライブラリ</h1>
<ul>
<li>Parser<br>
htmlの要素を取得するのが楽になります。</li>
<br>
<li>Day.js<br>
日付操作が簡単にできます。
他にはmoment.jsというのもありますが、調べてみるとmoment.jsはプロジェクト終了しているため使い方が似ているDay.jsを使用することにしました。
</li>
</ul>

<h1>スクリプトプロパティにアクセストークンをセットする</h1>

```javascript:Config.gs
function setProperty() {
  PropertiesService.getScriptProperties().setProperties({
    'ACCESS_TOCEN':'チャネルアクセストークン',
    'USERID': 'ユーザーID'
  });
}
```
以前はGUIでスクリプトプロパティが設定できていたようですが、新エディタになってからはそれができなくなったらしいので、こんな感じでプロパティをセットするためのファイルを作って、ここにトークンとかをセットするようにしました。
setProperty()を実行することでスクリプトプロパティに値がセットされます。

そして実際の処理を書く方のファイルでスクリプトプロパティの値を取得します。他にもラインでメッセージを送信するときに使うURLもここで定義しました。

```javascript:Script.gs
const ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN');
const USERID = PropertiesService.getScriptProperties().getProperty('USERID');
const URL_MESSAGE_PUSH = 'https://api.line.me/v2/bot/message/push';
const URL_MESSAGE_BROADCAST = 'https://api.line.me/v2/bot/message/broadcast';
const URL_MESSAGE_REPLY = 'https://api.line.me/v2/bot/message/reply';
```
<ul>
<li>
https://api.line.me/v2/bot/message/push<br>
ユーザーIDを指定してメッセージを送信します。主にテストで使用してました。
</li>
<br>
<li>
https://api.line.me/v2/bot/message/broadcast<br>
アカウントと友達になっている人全員にメッセージを送信します。
</li>
<br>
<li>
https://api.line.me/v2/bot/message/reply<br>
ユーザーからのメッセージに返信します。
</li>
</ul>

<h1>金曜ロードショー公式サイトから情報を取得する</h1>

```javascript:Script.gs
// 放送予定日と放送予定作品を取得
function getKinroSchedule(){
    let result = '';
    let today = dayjs.dayjs().format('YYYYMMDD');
    let day = dayjs.dayjs(today).day();
    let friday = 5;
    
    // 金曜ではない場合、日付を加算して金曜日に補正
    if(day > friday){
      today = dayjs.dayjs(today).add(6,'day').format('YYYYMMDD');
    }
    else{
      day = friday - day;
      today = dayjs.dayjs(today).add(day,'day').format('YYYYMMDD');
    }
    
    // 金曜ロードショー公式サイトから放送予定作品タイトルを取得
    let html = UrlFetchApp.fetch('https://kinro.ntv.co.jp/lineup/' + today).getContentText('UTF-8');
    let lineupTitle = Parser.data(html).from('<h1 class="ico">').to('</h1>').build();

    // 放送予定作品のページが見つからない場合、放送予定作品のページが見つかるまで1週間ずつ加算する
    while(lineupTitle == '今後の放送ラインナップ'){
      today = dayjs.dayjs(today).add(1,'week').format('YYYYMMDD');

      html = UrlFetchApp.fetch('https://kinro.ntv.co.jp/lineup/' + today).getContentText('UTF-8');
      lineupTitle = Parser.data(html).from('<h1 class="ico">').to('</h1>').build();

      // 20週間以上放送予定日が見つからない場合抜ける
      if(dayjs.dayjs(today) >= dayjs.dayjs().add(20, 'week')){
        result = '金曜ロードショーはしばらくお休みです。公式サイトを確認してください。\r\nhttps://kinro.ntv.co.jp/lineup';
        return result;
      }
    }

    // 放送予定日と作品名を取得
    let mainLineup = Parser.data(html).from('<section id="main_lineup">').to('</section>').build();
    let date = Parser.data(mainLineup).from('<div class="date">').to('</div>').build();
    let title = Parser.data(mainLineup).from('<h1 class="title">').to('</h1>').build();

    if(today == dayjs.dayjs().format('YYYYMMDD')){
      result = '🎬今夜の金曜ロードショーは\r\n' + '「' + title + '」です！✨\r\n' + date + '\r\nhttps://kinro.ntv.co.jp/lineup/' + today;
    }
    else{
      result = '🎬今夜の金曜ロードショーはお休みです。😔\r\n次回の金曜ロードショーは\r\n' + date + '\r\n「' + title + '」です！✨\r\nhttps://kinro.ntv.co.jp/lineup/' + today;
    }
    return result;
}
```
今回の肝となる部分です。
金曜ロードショーの公式サイトのラインナップのURLが https://kinro.ntv.co.jp/lineup で一番後ろに/放送日を付け加えると放送される作品の詳細ページのURLになります。https://kinro.ntv.co.jp/lineup/20210903

この放送日の部分が放送日じゃない場合だと、ラインナップのページに戻されるようになっていました。
なので作品の詳細ページに移動できずラインナップのページに戻される場合は、1週間ずつ足して作品の詳細ページに移動できるまでループして情報を取得するようにしました。

<h1>アカウントと友達になっている人全員にメッセージを配信する</h1>

```javascript:Script.gs
// 金曜ロードショーの内容を友達全員に配信
function PushMessage() {
    let message = getKinroSchedule();
    let options = {
      'method' : 'POST',
      'headers' : {
        'Content-Type' : 'application/json; charset=UTF-8',
        'Authorization' : 'Bearer ' + ACCESS_TOKEN
      },
      'payload': JSON.stringify({
        //'to': USERID,
        'messages': [{
            'type': 'text',
            'text': message,
        }]
      })
    };
    //UrlFetchApp.fetch(URL_MESSAGE_PUSH, options) // テスト用
    UrlFetchApp.fetch(URL_MESSAGE_BROADCAST, options)
}
```
コメントアウトしてる部分をコメントアウト解除して UrlFetchApp.fetch(URL_MESSAGE_BROADCAST, options) の部分をコメントアウトすると、自分だけにメッセージを送ることができます。

<h1>ユーザーからのメッセージに返信をする</h1>

```javascript:Script.gs
// ユーザーからのメッセージに返信
function doPost(e) {
  // Webhookイベントオブジェクトを取得
  let contents = e.postData.contents;
  let obj = JSON.parse(contents);
  let events = obj['events'];
  let message = '';
  
  // 連続して複数メッセージが送られてきた場合、最初のメッセージのみ返信する
  if(events.length == 1){
    switch(events[0].message.text){
      case '次回の放送内容':
        message = getKinroSchedule().replace('今夜の金曜ロードショーはお休みです。😔\r\n','');
        break;
      case '過去の放送内容':
        message = get_PastLineup();
        break;
      default:
        return;
    }
    let options = {
      'method' : 'POST',
      'headers' : {
        'Content-Type' : 'application/json',
        'Authorization' : 'Bearer ' + ACCESS_TOKEN
      },
      'payload': JSON.stringify({
      'replyToken' : events[0].replyToken,
          'messages': [{
              'type': 'text',
              'text': message,
          }]
      })
    };
    UrlFetchApp.fetch(URL_MESSAGE_REPLY, options);
  }
}
```
GASで用意されているdoPost()関数を使ってラインからのWebhookイベントを受け取ります。
ユーザーが連続でメッセージを送信した場合、最初に送信したイベント配列と残りの連続したメッセージのイベント配列に分けられます。
下の画像を見るとわかりやすいです。返ってきてるのはイベントオブジェクトのlengthプロパティの値です。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/f7a02438-5ea1-4d98-e4dd-d028c063b622.png)
全てのメッセージに返信する必要はないので最初のメッセージ（events.lengthが1）だけに返信するようにしました。

<h1>デプロイする</h1>
完成したらウェブアプリを選択してアクセスできるユーザーを全員に設定してデプロイします。
するとウェブアプリのURLが生成されるので、そのURLをラインのWebhookURLに入力すると動くようになります。

<h1>ソースコード全体</h1>

```javascript:Script.gs
const ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN');
const USERID = PropertiesService.getScriptProperties().getProperty('USERID');
const URL_MESSAGE_PUSH = 'https://api.line.me/v2/bot/message/push';
const URL_MESSAGE_BROADCAST = 'https://api.line.me/v2/bot/message/broadcast';
const URL_MESSAGE_REPLY = 'https://api.line.me/v2/bot/message/reply';

// 放送予定日と放送予定作品を取得
function getKinroSchedule(){
    let result = '';
    let today = dayjs.dayjs().format('YYYYMMDD');
    let day = dayjs.dayjs(today).day();
    let friday = 5;
    
    // 金曜ではない場合、日付を加算して金曜日に補正
    if(day > friday){
      today = dayjs.dayjs(today).add(6,'day').format('YYYYMMDD');
    }
    else{
      day = friday - day;
      today = dayjs.dayjs(today).add(day,'day').format('YYYYMMDD');
    }
    
    // 金曜ロードショー公式サイトから放送予定作品タイトルを取得
    let html = UrlFetchApp.fetch('https://kinro.ntv.co.jp/lineup/' + today).getContentText('UTF-8');
    let lineupTitle = Parser.data(html).from('<h1 class="ico">').to('</h1>').build();

    // 放送予定作品のページが見つからない場合、放送予定作品のページが見つかるまで1週間ずつ加算する
    while(lineupTitle == '今後の放送ラインナップ'){
      today = dayjs.dayjs(today).add(1,'week').format('YYYYMMDD');

      html = UrlFetchApp.fetch('https://kinro.ntv.co.jp/lineup/' + today).getContentText('UTF-8');
      lineupTitle = Parser.data(html).from('<h1 class="ico">').to('</h1>').build();

      // 20週間以上放送予定日が見つからない場合抜ける
      if(dayjs.dayjs(today) >= dayjs.dayjs().add(20, 'week')){
        result = '金曜ロードショーはしばらくお休みです。公式サイトを確認してください。\r\nhttps://kinro.ntv.co.jp/lineup';
        return result;
      }
    }

    // 放送予定日と作品名を取得
    let mainLineup = Parser.data(html).from('<section id="main_lineup">').to('</section>').build();
    let date = Parser.data(mainLineup).from('<div class="date">').to('</div>').build();
    let title = Parser.data(mainLineup).from('<h1 class="title">').to('</h1>').build();

    if(today == dayjs.dayjs().format('YYYYMMDD')){
      result = '🎬今夜の金曜ロードショーは\r\n' + '「' + title + '」です！✨\r\n' + date + '\r\nhttps://kinro.ntv.co.jp/lineup/' + today;
    }
    else{
      result = '🎬今夜の金曜ロードショーはお休みです。😔\r\n次回の金曜ロードショーは\r\n' + date + '\r\n「' + title + '」です！✨\r\nhttps://kinro.ntv.co.jp/lineup/' + today;
    }
    return result;
}

// 金曜ロードショーの内容を友達全員に配信
function PushMessage() {
    let message = getKinroSchedule();
    let options = {
      'method' : 'POST',
      'headers' : {
        'Content-Type' : 'application/json; charset=UTF-8',
        'Authorization' : 'Bearer ' + ACCESS_TOKEN
      },
      'payload': JSON.stringify({
        //'to': USERID,
        'messages': [{
            'type': 'text',
            'text': message,
        }]
      })
    };
    //UrlFetchApp.fetch(URL_MESSAGE_PUSH, options) // テスト用
    UrlFetchApp.fetch(URL_MESSAGE_BROADCAST, options)
}

// ユーザーからのメッセージに返信
function doPost(e) {
  // Webhookイベントオブジェクトを取得
  let contents = e.postData.contents;
  let obj = JSON.parse(contents);
  let events = obj['events'];
  let message = '';
  
  // 連続して複数メッセージが送られてきた場合、最初のメッセージのみ返信する
  if(events.length == 1){
    switch(events[0].message.text){
      case '次回の放送内容':
        message = getKinroSchedule().replace('今夜の金曜ロードショーはお休みです。😔\r\n','');
        break;
      case '過去の放送内容':
        message = get_PastLineup();
        break;
      default:
        return;
    }
    let options = {
      'method' : 'POST',
      'headers' : {
        'Content-Type' : 'application/json',
        'Authorization' : 'Bearer ' + ACCESS_TOKEN
      },
      'payload': JSON.stringify({
      'replyToken' : events[0].replyToken,
          'messages': [{
              'type': 'text',
              'text': message,
          }]
      })
    };
    UrlFetchApp.fetch(URL_MESSAGE_REPLY, options);
  }
}

// 過去の放送ラインナップを取得
function get_PastLineup(){
  let result = '📌過去の放送ラインナップ📌';
  let html = UrlFetchApp.fetch('https://kinro.ntv.co.jp/lineup/').getContentText('UTF-8'); 
  let past_lineup = Parser.data(html).from('<section id="past_lineup">').to('</section>').build();
  let date = Parser.data(past_lineup).from('<div class="date">').to('</div>').iterate();
  let title = Parser.data(past_lineup).from('<div class="title">').to('</div>').iterate();
  
  for(let i = 0; i < date.length; i++){
    result += '\r\n' + date[i] + '  ' + title[i];
  }
  return result;
}
```

<h1>所感</h1>
金曜ロードショーのサイトの作りが意外と単純だっため割と簡単に実装することができました。
GASはTypeScriptでも開発ができるようなので今度はTypeScriptで書いてみたいと思いました。
初めて個人で開発をしたのですが自分の思うように開発できるのは仕事とは違った楽しさがあってよかったです。

<h1>さいごに</h1>
せっかく作ったので今回作成したアカウントを公開します。
友達登録して使ってもらえると嬉しいです！
IDとQRはこちらです。よい金曜日を。

@449durir

![449durir.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/6cac5249-f38f-c194-53b7-2ee0f30434c5.png)