TwitterAPIを使わずにLINEからTwitterに投稿したい

# バ○スみたいに一斉に叫んでみたい
ここ最近は分からないけれど、一時期某映画のラストで「バル○！」とTwitterに投稿するのが流行りましたよね。
映像を見ながら文字を入力するのは面倒！できればボタン一つでサクッと投稿したい！

そんなワガママを叶えるべく、LINE×GAS×IFTTTで手軽に投稿できるツールを作成しました。

今回は、わたくし宝塚歌劇団:notes:好きなので、劇団への愛を叫ぶ仕様としていますが、言葉を変えれば色々と遊べます！

# 動作イメージ
実際に使ってみたイメージはこんな感じです。

https://twitter.com/momoiku3/status/1503181493832941571

# 環境
●GoogleAppsScript
●LINEmessagingAPI
●IFTTT
●Googleスプレッドシート
●Gmail

英語が得意だったり、回数の制約がなく使いたい方はTwitterAPIを使用すると良いのですが、英語で300字以上の申請理由を書けとか、とてつもなく申請が面倒なので、制約はあるけれど比較的簡単にできるIFTTTで環境を構築しました。

# 環境の構成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/ca3ebbc7-2446-9ebf-0c8c-72024eb178a6.png)

### 処理の流れのポイント
今回のプログラムでは、GoogleスプレッドシートとGmailを使うことで、こちらの2点の問題を解消しています。
 **①IFTTTの実行回数の制限** 
IFTTTの無料版では、IFTTTからTwitterへの投稿は1日25回という制限がついています。（2022年3月現在）
そのため、自分の投稿回数のカウントと、制限回数に達したのかを把握するためのカウント用としてスプレッドシートを使用しました。

 **②IFTTTの処理遅延** 
使用しているIFTTTが無料版につき実行にタイムラグが発生することがあるため、webbookではなくメールの受信をトリガーにして、遅延を防ぐ仕様にしています。
◆参考◆

https://www.pnkts.net/2019/10/06/gas-to-twitter-immediately

※遅延対応以外に、プログラムも参考にさせていただきました！

# 事前準備
### LINE側の準備
LINEmessagingAPIを使用するため、こちらの公式マニュアルを参考に作成しておいてください。
[Messaging APIを始めよう](https://developers.line.biz/ja/docs/messaging-api/getting-started/)
[ボットを作成する](https://developers.line.biz/ja/docs/messaging-api/building-bot/)

### スプレッドシート側の準備
回数のカウントと制限解除のチェック用にこのような形のシートを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/eac1748e-8e09-6e36-8e6e-6ceadffeabd3.png)
①投稿回数
　→プログラムから回数を入力するので、スプレッドシートでは欄さえあればOK
②最終投稿日時
　→プログラムから日時を入力するので、スプレッドシートでは欄さえあればOK
③回数リセット
　→同上
④現在
　→同上
⑤比較
　→回数リセットの日にちと本日が同日か否かを関数で判定
　 関数は「`=IF(D2=C2,"1",if(D2<C2,"2","3"))`」です。

### IFTTTの準備
トリガーにEメールを設定して、Eメールを受信したらTwitterに投稿するシナリオを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/c91f6261-1c33-3d38-0c7e-d52068a368b5.png)
◆トリガーの設定
Emailの「Send IFTTT any email」をトリガーとして設定します。
トリガーとするメールの送信先は、黄色マーカーを引いている`trigger@applet.ifttt.com`です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/1a1dbd90-b9c2-dc4e-015e-dc381d7940cf.png)
◆実行内容の設定
Twitterをアクションとして設定します。
今回は文字のみをツイートするので、「Post a tweet」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/e3ae8c8f-a75d-c7f3-cade-41d3ed2531cd.png)
ツイート内容を設定します。
今回はメール本文にツイートしたい内容を記載しているので、「Body」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/b1db05fb-516f-bdf3-c179-2176a66f17e5.png)

# プログラム
プログラムの全体はこんな感じです。
<details><summary>プログラム全体</summary>

```javascript
//メールをキーにIFTTTを実行
const sendMail='trigger@applet.ifttt.com';
//現在時刻の取得
const now = new Date();
//本文重複回避のための日時の取得
const sendTime=Utilities.formatDate(now,'Asia/Tokyo', 'yyyy/MM/dd: HH:mm:ss');

//IFTTTのTwitter投稿回数チェック用
  const id = "スプレッドシートのID";
  const ss = SpreadsheetApp.openById(id);
  const sheet = ss.getActiveSheet();
  const countCell = sheet.getRange("A2"); //数えた回数を入れるところ
  const lastTweet = sheet.getRange("B2"); //最後に投稿された日時を登録するところ
  const resetCell = sheet.getRange("C2"); //回数がリセットされる時間を登録するところ　※制限解除は日本時間16時っぽい
  const todayCell = sheet.getRange("D2"); //現在の日にちを登録するところ
  const chkCell = sheet.getRange("E2"); //リセット日と現在の比較結果を判定したところ

  let resetTime = "";
  let count = "";
  let countMsg = "";
  let chkData = "";
  let replyMsg = "";


//キーとなるLINEからの連動部分
const CHANNEL_ACCESS_TOKEN = "アクセストークン";

function doPost(e) {

    handleMessage(e);
}

function handleMessage(e) {
  const replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  const lineType = JSON.parse(e.postData.contents).events[0].type
  if (typeof replyToken === "undefined" || lineType === "follow") {
    return;
  }
  let userMessage = JSON.parse(e.postData.contents).events[0].message.text;

  //1日25ツイートチェック
  
    count=countCell.getValue();

    //現在日時をシートに登録
    let activeDate = Utilities.formatDate(now,'Asia/Tokyo', 'yyyy/MM/dd');
    todayCell.setValue(activeDate);
    //現在の時間を持っておく
    let activeTime = Utilities.formatDate(now,'Asia/Tokyo', 'HH');
    
    //シートに登録されている比較結果を取得
    chkData=chkCell.getValue();

    if (count === 25){
      //リセット時間を確認※16時頃にリセットされるらしい
      if ((chkData === "1")&&(activeTime<16)){
        countMsg = "投稿可能上限値に達しています。\nリセットまでお待ちください。（16時頃を予定）";
        reply(replyToken,countMsg);

      }else if(chkData === "2"){
        countMsg = "投稿可能上限値に達しています。\nリセットまでお待ちください。（明日16時頃を予定）";
         reply(replyToken,countMsg);

      }else{

        //前回リセット以降にはじめて投稿した場合
        countCell.setValue(0);//回数を初期化
          if (activeTime<16){
            //16時前なら本日を登録
            resetTime = activeDate;
            resetCell.setValue(resetTime);
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

           }else{
            //16時以降なら翌日を登録
            let setDay = now
            setDay.setDate(setDay.getDate()+1)
            resetTime = Utilities.formatDate(setDay,'Asia/Tokyo', 'yyyy/MM/dd');
            resetCell.setValue(resetTime);
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

            }
        }
    
    }else if(count < 25){
      //25回以下の場合で、前回リセット時間以降に初めて投稿した場合
      if(chkData === "1"){
        //リセットも同日のとき
        if (activeTime<16){
        //16時前なら本日を登録かつ数値はリセットしない
          resetTime = activeDate;
          resetCell.setValue(resetTime);
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);


        }else{
          //16時以降なら翌日を登録かつ数値をリセット
          let setDay = now
          setDay.setDate(setDay.getDate()+1)
          resetTime = Utilities.formatDate(setDay,'Asia/Tokyo', 'yyyy/MM/dd');
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);
        }

      }else if(chkData === "3"){
        //リセット日が古い場合
        if (activeTime<16){
        //16時前なら本日を登録かつ数値リセット
          resetTime=activeDate;
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

        }else{
          //16時以降なら翌日を登録かつ数値をリセット
          let setDay = now
          setDay.setDate(setDay.getDate()+1)
          resetTime = Utilities.formatDate(setDay,'Asia/Tokyo', 'yyyy/MM/dd');
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

        }
      }else if(chkData === "2"){
        //リセットが翌日の時は日付などは変更しない
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

      }   

  }
}

function reply(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  UrlFetchApp.fetch(url, {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "method": "post",
    "payload": JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": message,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}

//LINEからのメッセージを加工
//ツイート内容を作成
function tweetAction(lineMsg){

  //投稿回数を取得
  count = countCell.getValue();

  //メインとなる送信メッセージ
  let sendMsg = "";

  //LINEのメッセージから組名を切り出す
  let kw = lineMsg.slice(0,2);
  let listKw = ["花組","月組","雪組","星組","宙組","専科"];

  if(listKw.indexOf(kw) == -1){
    //先頭2文字がlistKwに含まれていない場合
    sendMsg = "宝塚最高！";  
  }else{
    sendMsg = kw+"最高！"
  }

  //投稿するメッセージの生成
  if(count < 20){
    //Twitter投稿回数のカウント
    countCell.setValue(count+1);
    count = count+1;
    countMsg = sendMsg+"\n"+count+"ツイート目です。";
    //IFTTTに投稿内容をメール送信
    MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
    lastTweet.setValue(sendTime);
    
    return countMsg;  

    }else if((count >=20) && (count<24)){
      countCell.setValue(count+1);
      count = count+1;
      countMsg = sendMsg+"\n"+count+"ツイート目です。\n※25ツイートまで投稿できます";
      MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
      lastTweet.setValue(sendTime);

      return countMsg;

    }else{
      //※25ツイート目
      countCell.setValue(count+1);
      count = count+1
      countMsg = sendMsg+"\n"+count+"ツイート目です。\n※上限に達しました。";
      MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
      lastTweet.setValue(sendTime);

      return countMsg
    }

}
```
</details>

## プログラム解説
### ツイートは1日25件まで対応（無料版）
無料版のIFTTTでは、Twitter投稿は1日25件までという制限がついているため、こちらの内容が分かるような判定を入れました。
①自分が何件投稿しているのか
②上限値に達したら、制限解除されるまで投稿できないということ
 この部分のプログラムはこちら。

 **①投稿しようとしたタイミングでの回数と時間を取得** 
スプレッドシートに登録されている回数を`count`に保存しておきます。
あと、Twitter投稿しようとした日付情報をスプレッドシートの所定の欄へ登録します。
回数リセットの時間と投稿時間を比較した結果を`chkData`へ保存します。（※この部分の紆余曲折は後述...）
```javascript
  //1日25ツイートチェック
  
    count = countCell.getValue();

    //現在日時をシートに登録
    let activeDate = Utilities.formatDate(now,'Asia/Tokyo', 'yyyy/MM/dd');
    todayCell.setValue(activeDate);
    //現在の時間を持っておく
    let activeTime = Utilities.formatDate(now,'Asia/Tokyo', 'HH');
    
    //シートに登録されている比較結果を取得
    chkData = chkCell.getValue();
```

 **②25件の場合は制限解除まで待つようリプライする**
投稿制限が掛かったら、不要にEメールを送信しないようにします。
制限解除の時間は、日本時間16時頃らしいという情報があるため、一旦16時で制限が解除される想定で作成しています。

https://www.pnkts.net/2019/10/02/ifttt-daily-limit

が、17時頃では？というツイートも目にしたため、もしかしたら解除時間に変動がある可能性があります。

あと、16時より前なのかどうかについては、こちらのプログラムを参考にしています。

https://qiita.com/KazumasaIwazaki/items/9d5a783b36a77ca30c82

```javascript
    if (count === 25){
      //リセット時間を確認※16時頃にリセットされるらしい
      if ((chkData === "1")&&(activeTime<16)){
        countMsg = "投稿可能上限値に達しています。\nリセットまでお待ちください。（16時頃を予定）";
        reply(replyToken,countMsg);

      }else if(chkData === "2"){
        countMsg = "投稿可能上限値に達しています。\nリセットまでお待ちください。（明日16時頃を予定）";
         reply(replyToken,countMsg);

      }
```
 **③25回の投稿制限に引っ掛からない場合（＝Twitter投稿OK）** 
投稿制限に引っ掛からない場合は、条件に応じて回数の初期化と新たな回数リセット時間をスプレッドシートに登録してTwitterに投稿します。
複数判定があるため、こんな感じで分岐させています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/89649ab7-5958-d997-d999-a2814d2f7904.png)

```javascript
else if(count < 25){
      //25回以下の場合で、前回リセット時間以降に初めて投稿した場合
      if(chkData === "1"){
        //リセットも同日のとき
        if (activeTime<16){
        //16時前なら本日を登録かつ数値はリセットしない
          resetTime = activeDate;
          resetCell.setValue(resetTime);
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);


        }else{
          //16時以降なら翌日を登録かつ数値をリセット
          let setDay = now
          setDay.setDate(setDay.getDate()+1)
          resetTime = Utilities.formatDate(setDay,'Asia/Tokyo', 'yyyy/MM/dd');
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);
        }

      }else if(chkData === "3"){
        //リセット日が古い場合
        if (activeTime<16){
        //16時前なら本日を登録かつ数値リセット
          resetTime = activeDate;
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

        }else{
          //16時以降なら翌日を登録かつ数値をリセット
          let setDay = now
          setDay.setDate(setDay.getDate()+1)
          resetTime = Utilities.formatDate(setDay,'Asia/Tokyo', 'yyyy/MM/dd');
          resetCell.setValue(resetTime);
          countCell.setValue(0);//回数を初期化
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

        }
      }else if(chkData === "2"){
        //リセットが翌日の時は日付などは変更しない
            //Twitter投稿＆リプメッセージ作る
            replyMsg = tweetAction(userMessage);
            reply(replyToken,replyMsg);

      }
```
### ツイート内容作成
LINEのから定型のメッセージが送信されるので、そのメッセージを生かしがプログラムにします。
定型メッセージの送信は、LINEのリッチメニューを使うと便利なので、作成しておきます。

https://qiita.com/bow_arrow/items/32ac5d2b4c67bd0c1dc2

 **①メインのメッセージを作成**
LINEから送信されたメッセージの先頭2文字が`listKw`に含まれているか否かを`indexOf()`を使って判定し、判定ごとに送信する文言を作成します。

```javascript

//LINEからのメッセージを加工
//ツイート内容を作成
function tweetAction(lineMsg){

  //投稿回数を取得
  count = countCell.getValue();

  //メインとなる送信メッセージ
  let sendMsg = "";

  //LINEのメッセージから組名を切り出す
  let kw = lineMsg.slice(0,2);
  let listKw = ["花組","月組","雪組","星組","宙組","専科"];

  if(listKw.indexOf(kw) == -1){
    //先頭2文字がlistKwに含まれていない場合
    sendMsg = "宝塚最高！";  
  }else{
    sendMsg = kw+"最高！"
  }

```
 **②Twitterへ投稿＆投稿回数のリプライ**
何回Twitterに投稿したかが分かるように、スプレッドシートの投稿回数を更新＋リプライに投稿回数を表示します。
併せて、メール本文に投稿内容を記載したEメールを送信してトリガー実行させます。

なお、Twitterは同じ内容を投稿しようとすると投稿できないようなので、ツイート本文に送信日時を付けて重複チェックに引っ掛からないようにしています。

```javascript
  //投稿するメッセージの生成
  if(count < 20){
    //Twitter投稿回数のカウント
    countCell.setValue(count+1);
    count = count+1;
    countMsg = sendMsg+"\n"+count+"ツイート目です。";
    //IFTTTに投稿内容をメール送信
    MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
    lastTweet.setValue(sendTime);
    
    return countMsg;  

    }else if((count >=20) && (count<24)){
      countCell.setValue(count+1);
      count = count+1;
      countMsg = sendMsg+"\n"+count+"ツイート目です。\n※25ツイートまで投稿できます";
      MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
      lastTweet.setValue(sendTime);

      return countMsg;

    }else{
      //※25ツイート目
      countCell.setValue(count+1);
      count = count+1
      countMsg = sendMsg+"\n"+count+"ツイート目です。\n※上限に達しました。";
      MailApp.sendEmail(sendMail, '件名', sendMsg+'\n'+sendTime);
      lastTweet.setValue(sendTime);

      return countMsg
    }
```

# 作成で苦労したポイント
## TwitterAPIの承認を得るまでの工程がハード過ぎる
当初はTwitterのAPIを取得して回数制限なく使用できるようにしたかったのですが、英語で300文字以上の申請理由を書いたり、翻訳機能を使ってどうにか申請理由を書いて申請してもすぐに承認されず、やっと返信が来たと思ったら「もうちょっと詳しく申請理由を聞きたいんだけど？」みたいな内容で **心が折れました。** 
本格的なツールを作る場合は、APIを取得したほうが良いと思いますが、 **とりあえず作成してみよう！** という場合はIFTTTで充分でした。

## GASでの日付判定がうまくいかない
当初、スプレッドシートに登録した回数リセットの日にちと、現在時刻をプログラム上で比較する仕様を予定していましが、何度やってもエラーや正しく判定されない事象に陥りました…。
スプレッドシートに登録されている日付との比較を行っている事例をいくつか試しましたが、どうにもうまくいかず。
プログラムでの判定がうまくできないのなら、スプレッドシート上で判定した方が早い！という事で、関数で判定した結果を用いる方法に落ち着きました。