Twitter APIを使わずにTwitterの画像付き投稿をDiscordに流すbotを作った話

#はじめに
こんにちは、[いたて](https://twitter.com/i_tatte)と申します。情報系の大学1回生です。今回は、表題にもある通りTwitterの画像付き投稿をDiscordに自動で流すbotを作ってみました。
#経緯
私は高校で仲のよかった文化部グループのDiscordサーバーに参加しています。このサーバーには食事にまつわるチャンネルがあり、日々メンバーたちによる飯テロや自炊晒しなどが行われています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/d3ee4f6e-a545-608c-25dd-e8c5f7d7b4ed.png)
ところで、私はTwitterで[#いたてめし](https://twitter.com/hashtag/%E3%81%84%E3%81%9F%E3%81%A6%E3%82%81%E3%81%97?f=live)というハッシュタグを作り、毎日の食事を投稿することにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/a3423c6e-ffd7-8ae9-4510-1d6b4633a1f4.png)
Twitterに投稿した内容をDiscordサーバーにまた上げなおすのも面倒なのでDiscordのチャンネルには自炊写真を上げていなかったのですが、メンバーの一人にDiscordにも上げてほしいとの要望をもらったので、せっかくなのでハッシュタグ[#いたてめし](https://twitter.com/hashtag/%E3%81%84%E3%81%9F%E3%81%A6%E3%82%81%E3%81%97?f=live)のついた投稿を自動でDiscordに投げるbotを作ってみました。
しかし、私はTwitter APIの利用ができません。あのAPIは利用申請をしてから実際に利用が可能になるまで少し時間がかかるのです（数時間ぐらい）。その間待つのもなんだか嫌だったので、APIを叩かずにbotを構築する方法を模索してみました。
#実装
botの実現には主にGASとIFTTT（イフト、と読むらしいです）というサービスを用いました。GASはGoogle Apps Scriptの略称で、Google Drive上に置けるサーバーサイドスクリプト環境です。詳しくは後述しますが、こういったWebアプリを作るのに非常に適しています。
![image](https://amadoc-insight.com/wp-content/uploads/2020/10/GAS001-1.jpg)
IFTTTは簡単に言うと条件に合わせて発火するトリガーを簡単に作れるWebサービスです。今回は特定ユーザーのハッシュタグ付きツイートを検知してGoogle spreadsheetに書き込むトリガーを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/c73bab8e-9c0c-6a9d-5835-fb2e9f064dbf.png)
#素朴な方法
まずは一番簡単な方法で実装してみます。IFTTTで投稿を検知してGoogle spreadsheetに投げ、それをGASから検知して、整形してDiscordに投げる方法です。この方法はとても簡単ですが、**取得できるのはツイートへのリンクだけで、画像は取得できない**という欠点があります。DiscordはTwitterリンクを貼ると自動的にembedをいい感じにやってくれるので、それで済ませるのなら簡単ですね。
まあ、とりあえずやってみましょう。
##IFTTTの設定
IFTTTの設定はとても簡単ですので、方法については省略します。適当にググったら無限に情報が出てくるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/d2130280-9a53-0ca5-b012-51ebee1e8c59.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/a3686ecd-92d8-f474-94d6-c49ab158cb4c.png)
このように設定してみました。この設定を行った時点で、[#いたてめし](https://twitter.com/hashtag/%E3%81%84%E3%81%9F%E3%81%A6%E3%82%81%E3%81%97?f=live)のついたツイートが自動的に指定したスプレッドシートに書き込まれるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/826f6e0f-94b7-3c0a-3bd4-0f21abf7f2c9.png)
これでIFTTTの設定はおしまいです。か、簡単すぎる…………
##GASでスクリプトを組む
あとはGASからスプレッドシートの変更を検知してDiscordにWebhookを投げるだけですね。Google spreadsheetの「ツール」からスクリプトエディタを開き、コーディングしていきます。WebhookのところにはDiscordのWebhook URLを指定します。

```javascript:itatemeshi-crawler.gs
function getSheetUpdate() {
  var mySheet = SpreadsheetApp.getActiveSheet();
  for(var i = 1; i <= mySheet.getDataRange().getLastRow(); i++) {
    if(mySheet.getRange(i, 5).getValue()==""){
      postToDiscord(mySheet.getRange(i, 2).getValue());
      mySheet.getRange(i, 5).setValue("done");
    }
  }
}

function postToDiscord(message) {
  var webhookURL = "Your Webhook URL";
  var options = {
    "content" : message
  };
  var response = UrlFetchApp.fetch(
    webhookURL,
    {
      method: "POST",
      contentType: "application/json",
      payload: JSON.stringify(options),
      muteHttpExceptions: true,
    }
  );
}
```

これだけです。ね？簡単でしょ？
プログラムとしては、スプレッドシートを全部舐めて未マークの行があればDiscordに投げてマークを付ける、というだけです。
さて、ここからがGASのマジですごいところなんですが、GASにはかなり優秀な**トリガー実行機能**が搭載されています。この機能を使うことで1分ごとに実行するだとか、カレンダー更新時に実行するだとか、**スプレッドシート更新時に実行**するだとかといった条件に応じた実行が可能となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/50ae8aa9-f164-2404-faff-e3c27bf5564f.png)
こんな風に設定してやるだけで、スプレッドシートの更新時に自動で```getSheetUpdate()```がコールされます。神か？？？？？

……はい。これだけでbotが完成してしまいました。こんな風に動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/23bd141c-594f-aa1b-2988-bdb2b34dd3d7.png)
やったー！！完成！！！
#芸がない、そう思いませんか？
私は思います。

というのも、このbot、ただURL投げるだけなんですよ。表示のカスタマイズとかはできないんですね。それに、余計な情報(TwitterロゴやTwitterユーザー名、ツイートへのリンク文字列など)がついているのも気に食わないですね。ということで、改良していきましょう。
#改良
やはりただリンクを投げるだけではあまりにも味気ないので、embedを使っていい感じに装飾していくことにしましょう。メッセージ本文にはツイート内容を、embed内の画像にはツイートされた画像を埋め込んでDiscordに投げたらよりいい感じになりそうな気がします。

……と、ここで問題発生です。embedに画像を埋め込むためには生の画像データが必要ですが、ツイートから生の画像データを手に入れるのは実はそう簡単ではありません。Twitterはページが動的に生成されるためです。人力でやるのであればちょっと右クリックするだけだし、Twitter APIを使えば一瞬でとってこられるのですが、Twitter APIを使わずにGASからとなると一筋縄ではいきません。そこで、**PhantomJsCloud**を利用しました。

[PhantomJsCloud](https://phantomjscloud.com/)とは、動的なWebページをスクレイピングできるサービスです。スクレイピングしたいURLを渡すと、JavaScriptが走った後の静的なHTMLドキュメントを返してくれます。使い方はググったりドキュメント見たりすればわかると思います。
![image](https://phantomjscloud.com/img/logo-600.png)
これを使って、ツイートのページをカリカリとスクレイピングしちゃいましょう！

PhantomJsCloudにTwitterのURLを投げるような関数を上のGASコードに実装していきましょう。

```javascript:itatemeshi-crawler.gs
function scrape(TwitterURL) {
  var key = 'Your PhantomJsCloud APIKey';
  var PJSURL = 'https://phantomjscloud.com/api/browser/v2/'+ key +'/?request=%7Burl:%22' + TwitterURL + '%22,renderType:%27HTML%27,outputAsJson:true%7D';

  return UrlFetchApp.fetch(PJSURL).getContentText();
}
```

これで`scrape`関数にTwitterのURLを入れたら、静的なHTMLドキュメントが返される関数が実装できました。URLがキモい形をしているのは文字列を直接打ち込んでいるからですね。ふつうはJSON形式の`payload`とかを用意すると思います。めんどかったので。

あとは、このHTMLドキュメントから目当ての画像を掘り出すだけですね。いろいろ方法はありますが、めんどくさいので今回は文字列の検索とスライス処理でごり押すことにしました。~~経験的に合わせてるのでいつか事故りそう~~

それに合わせて`PostToDiscord`関数も適当に改造してembedを埋め込み、ほかにも文字列処理の関数を適当に作って組み込むと、こんな感じになりました。

```javascript:itatemeshi-crawler.gs
function getSheetUpdate() {
  var mySheet = SpreadsheetApp.getActiveSheet();
  var maxRow = mySheet.getDataRange().getLastRow();
  for(var i = 1; i <= maxRow; i++) {
    if(mySheet.getRange(i, 5).getValue()==""){
      var content = scrape(mySheet.getRange(i, 2).getValue());
      var TwitterPhotoURL = content.substring(content.indexOf("&quot;https://pbs.twimg.com/media/") + 6, content.indexOf("&amp;", content.indexOf("&quot;https://pbs.twimg.com/media/")));
      postToDiscord(cropString(mySheet.getRange(i, 1).getValue()), mySheet.getRange(i, 2).getValue(), TwitterPhotoURL);
      mySheet.getRange(i, 5).setValue("done");
    }
  }
}

function postToDiscord(message, tweetURL, photoURL) {
  var webhookURL = "Your Webhook URL";
  var options = {
    "content" : message,
    "embeds":[
    {
      "title": "今日のいたてめし",
      "url": tweetURL,
      "color": 5620992,
      "image": {
        "url": photoURL
      }
    }
    ]
  };
  var response = UrlFetchApp.fetch(
    webhookURL,
    {
      method: "POST",
      contentType: "application/json",
      payload: JSON.stringify(options),
      muteHttpExceptions: true,
    }
  );
}

function scrape(TwitterURL) {
  var key = 'Your PhantomJsCloud APIKey';

  var PJSURL = 'https://phantomjscloud.com/api/browser/v2/'+ key +'/?request=%7Burl:%22' + TwitterURL + '%22,renderType:%27HTML%27,outputAsJson:true%7D';

  return UrlFetchApp.fetch(PJSURL).getContentText();
}

function cropString(string) {
  return string.substring(0,string.indexOf("#"));
}
```
なんか突然湧いた`cropString`関数は処理を見ればわかる通り、ただハッシュタグ以前の文字列を切り出して返しているだけですね。

これで完成です！！こんな感じで動作します↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554644/2698e968-003d-6e00-1af8-b648d339d098.png)
いやー良い、いいですね。シンプルかつ必要最低限の情報に抑えられています。青字の「今日のいたてめし」にはツイートへのリンクが埋め込まれているので、先ほどの形式と比べても情報量は一切変わっていません。好みに合わせて自由にカスタマイズできるのもいいですね（説明文を追加したりもできたりします）。

……え？違いが微妙すぎる？どこに拘ってるんだって？

おっしゃる通りだと思います。はい。
#おわりに
当初はTwitter APIを使わないとどうにもならないものだと思っていたのでAPIの申請までしたのですが、APIの利用が可能になるまでにしばらくかかりそうだったので足掻いてみたらなんかできてしまいました。bot自体は大したものではないですが、技術的に面白い点が多く、非常に勉強になりました。ニッチすぎて需要は無だと思いますが、せっかく作ったので記事にまとめてみました。

ではでは皆さんごきげんよう。また別の記事で会いましょう～～