TwitterAPIを使ってライバロリさんにDMを送るbotを作る【GAS(GoogleAppsScript)】

技術

こんにちは。ざわかける!のざわ(@zw_kakeru)です。
本日はGASからTwitterAPIを叩いて、自動でDMを送るbotを作成します。
DMの送信先は有名なポケモントレーナーであるライバロリさんにしてみました。

ライバロリさんについて

ライバロリさん(@raibarori)は主にニコニコ動画Youtubeで活動している動画投稿者で、
コンテンツはゲーム『ポケットモンスター』シリーズのレート対戦実況がメインとなっています。
実力は相当高く、過去作品も含めて世界ランク瞬間一位を何度も達成されています。
本気の対戦以外にも、進化前のポケモンだけでの対戦や他の動画投稿者とのコラボ企画、時には実写動画も投稿されています。
多い時期には1日2本以上を数日間連続投稿、少ない時期でも3-4日に1本は投稿されます。
投稿頻度は比較的高めで、本人のやる気によって変わるようです。

このライバロリさんは彼のTwitterやInstagramのDMに、自分が動画内で使ってほしいポケモンを送信すると実際にそのポケモンを活躍させた動画を投稿してくれる、という視聴者サービスをしてくれています。
過去にも視聴者からDMで送られてきた要望に応えた動画を上げています。
【ポケモンUSUM】ドダイトスキッズ見てるか……?【ウルトラサン・ウルトラムーン】
【禁忌】”例のサワムラー”使ってみた【ポケモン剣盾】
毎日連絡してくるデンチュラキッズを救いま………

しかし、DMを送ってすぐに動画で使用してくれるわけではありません。
中途半端な志しか持たない視聴者の要望に応えるほどライバロリさんも暇ではないようです。
彼が動画化してくれるその日まで、くる日もくる日もメッセージを送り続けなければなりません。
彼曰く、「たいてい一週間もしたら誰も送って来なくなる」らしく、1年以上毎日送り続けて初めて「根性あるな」と思う程度だということです。

そんなわけで今回はGASプログラミングを用いて、このライバロリさんに私が動画で使ってほしいポケモンのリクエストDMを送るスクリプトを書いていこうと思います。

使っていただきたいポケモン

今回、ライバロリさんに使ってほしいとお願いするポケモンは、「ムシャーナ」にしました。

ムシャーナ。かわいい。

理由は私が初めて厳選、育成したポケモンだからです。
中学生の時に典型的なあくび・めいそう型で育成して対戦していました(ていうかこれ以外の型ってあるのかな)。
あまり強くはありませんでしたが今思い返すととても楽しくプレイしていた記憶ばかりで、ポケモンというコンテンツが末永く続いているのは良いものだなあと思ったりします。

この「ムシャーナを使ってくだい!」というリクエストを毎日1通ずつ彼のTwitterにDMで送りましょう(果たしてこのポケモンで面白い動画が撮れるのかというのは分かりませんが…)。

やりたいこと(仕様)

やりたいこと、実現したいことの整理を行います。

1日1回自動でDMを送る

毎日1回ずつ、自動でライバロリさん宛にTwitterでDMを飛ばします。
これはGAS上の日次トリガーを使えば実現できそうな予感がします。

ランダムな時間にDMを送る

上記のDMを、毎日ランダムな時間に送ります。
毎日決まった時間に送ると怪しまれてしまいそうなので、日ごとに異なる時間に送るように設定することにしました。
ただ、深夜帯などのあまりに非常識な時間帯に送ると迷惑がかかるため、9時から21時の間のどこかとしましょう。

送った回数を表示する

送るDMに、何回目の送信なのかという情報を入れておきます。
ここはDMを送るたびに毎回変更する必要がある部分です。動的コンテンツってやつですね。
少し手間がかかってしまいますが、今まで何回DMを送ったのか分かるようにしておいた方がライバロリさんも見やすいかと考えました。
GASを扱ったことがある方なら分かると思いますが、ここはGASのプロジェクト自体が持てるプロパティを使えば実装できそうです。

やったこと(実装)

ここからは実際の作業について記述していきます。

TwitterAPIの利用申請・連携認証

こちらのnoteを参考に、TwitterAPIの利用申請と連携認証を行いました(「5. アプリで認証を行う」章まで)。
問題なければ最後に「Success」と表示されるようですが、私がやった時は下のエラー画面が表示されました。

「スクリプト関数が見つかりません: authCallBack」とのこと。
よく見ると、CallBackのBが大文字になっていますね。ここは小文字が正しいので修正しましょう。

function authCallback(request) {
 return twitter.authCallback(request);
}

実行すると、無事にエラーが消えて「Success」の文字が表示されました。
逆にこの人はエラー出ずに通ったのかな…謎です。
写し間違えたのかもしれませんね。

ユーザーIDの取得

これでTwitterAPIを使用できるようになりました。
さっそく具体的なコーディングに入っていきます。

まず、送信先(ライバロリさん)を設定しなければなりません。
ユーザー名(@raibarori)から内部で保持されているIDを取得します。

function getUserId(userName) {
  var service = twitter.getService();
  var requestURL = "https://api.twitter.com/1.1/users/lookup.json?screen_name=" + userName
  var response = service.fetch(requestURL, {
    method: "get",
    contentType: 'application/json'
  });

  return JSON.parse(response.getContentText())[0].id_str
}

TwitterAPIを叩く基本的な操作です。
レスポンスとして返ってきたIDに向けてDMを送信していきます。

DMを作成して送信する

APIを使うことでライバロリさんのIDを取得できるようになったので、実際のDMを作成して送信します。

function sendDM(_, userName, text){
  if (userName == undefined) { userName = "zw_kakeru" }
  if (text == undefined) {
    prop = PropertiesService.getScriptProperties()
    counter = prop.getProperty("counter")
    if (counter == undefined) { counter = 0 }
    counter++
    text = "ムシャーナを動画で使っていただけませんか。(" + counter + "回目)"
    prop.setProperty("counter", counter)
  }
  userId = getUserId(userName)

  try{
    var service = twitter.getService();
    var payload = JSON.stringify({
      event: {
        type: 'message_create',
        message_create: {
          target: {
            recipient_id: String(userId)  
          },
          message_data: { text: text }  
        }
      }
    });
    var response = service.fetch('https://api.twitter.com/1.1/direct_messages/events/new.json',{
      method: 'POST',
      contentType: 'application/json',
      payload: payload
    });
    return response;
  } catch(e) {
    Logger.log('Exception:'+e);
  }
}

先程の章で説明したgetUserIdメソッドを内部で呼び出し、返ってきたIDを送信先に設定してメッセージを作成、送信を行っています。
日次実行するのはこのsendDMメソッドだけです。
汎用性を持たせるために(ライバロリさん以外の人にもDMを送りたくなった時にすぐに送れるように)、このような引数設定と実装を行いました。
ID自体は毎日変わるものではないのでプロパティに保持しておけば事足りるのですが、別に毎日問い合わせても大した負荷にもならないと思うのでこのままいきます。
ちなみに、トリガー実行メソッドとしての引数設定でハマった部分をTipsとして別記事に記載しておきました。


また、メッセージに関してもcounterプロパティを使用し、毎日インクリメントしてからメッセージの作成、保存を行うことで「何回DMを送ったのか」をメッセージに含めることができるようになっています。

ランダム実行トリガーをセットする

ここまでで、当初想定していた要件を満たしたDMをライバロリさんに送信することができるようになりました。
あとは毎日ランダムな時間に送る、という部分を実装する必要があります。

GASのエディタ上でのトリガー設定では、毎日同じ時間に起動させる設定は行えるのですが、ランダムな時間での設定は行えません。
そのため、「ランダムな時間を自動生成してその時間にトリガーをセットする」という作業を毎日同じ時間に行う、という実装にしましょう。

function setTrigger(_, funcName) {
  if (funcName == undefined) { funcName = "sendDM" }

  const time = new Date();
  const rh = [9, 21]
  const rm = [0, 60]

  triggerHour = Math.floor(Math.random() * (rh[1] - rh[0])) + rh[0]
  triggerMinite = Math.floor(Math.random() * (rm[1] - rm[0])) + rm[0]

  time.setHours(triggerHour); 
  time.setMinutes(triggerMinite);
  time.setSeconds(0);
  ScriptApp.newTrigger(funcName).timeBased().at(time).create();
}

9時から21時までの間の時間をランダムに生成し、その時間にsendDMメソッドの実行トリガーをセットします。
(範囲指定の乱数生成の記述がなぜこうなるのか、というのは自分で考えてください。)
そして、このsetTriggerメソッドの日次実行GASエディタ上で設定しておきます。
こうすることで、「毎日同じ時間」に「その日のランダムな時間にDMを送るトリガー」を設定でき、その時間になると実際にライバロリさんの元にDMが届くという仕組みになります。

実行済みのトリガーを削除する

このままではトリガーが毎日1つずつ増えていってしまうので、実行済みのトリガーを削除するメソッドを作成します。

function deleteTrigger(_, funcName) {
  if (funcName == undefined) { funcName = "sendDM" }

  const triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == funcName) {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

設定されているトリガーを検索してsendDMメソッドを指定しているトリガーを全て削除します。
注意点としては、このdeleteTriggerメソッドは先程のsetTriggerメソッドよりも先に実行する必要があるということです。
そうしないとせっかく生成したトリガーが起動する前に削除されてしまいますからね。
私はdeleteTriggerを深夜0~1時の間、setTriggerを深夜2~3時の間と設定しました。

完成したスクリプト

これでやりたいことを全てコードに落とし込めました。
あとはTwitterインスタンスをグローバルで生成しておけば万事うまくいくでしょう。
最終的に完成したスクリプトは以下の通りです。

var twitter = TwitterWebService.getInstance(
 "XXXXXXXXXXXXXXXXXXXXXXXXX",
 "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"
);

function getUserId(userName) {
  var service = twitter.getService();
  var requestURL = "https://api.twitter.com/1.1/users/lookup.json?screen_name=" + userName
  var response = service.fetch(requestURL, {
    method: "get",
    contentType: 'application/json'
  });

  return JSON.parse(response.getContentText())[0].id_str
}

function sendDM(_, userName, text){
  if (userName == undefined) { userName = "zw_kakeru" }
  if (text == undefined) {
    prop = PropertiesService.getScriptProperties()
    counter = prop.getProperty("counter")
    if (counter == undefined) { counter = 0 }
    counter++
    text = "ムシャーナを動画で使っていただけませんか。(" + counter + "回目)"
    prop.setProperty("counter", counter)
  }
  userId = getUserId(userName)

  try{
    var service = twitter.getService();
    var payload = JSON.stringify({
      event: {
        type: 'message_create',
        message_create: {
          target: {
            recipient_id: String(userId)  
          },
          message_data: { text: text }  
        }
      }
    });
    var response = service.fetch('https://api.twitter.com/1.1/direct_messages/events/new.json',{
      method: 'POST',
      contentType: 'application/json',
      payload: payload
    });
    return response;
  } catch(e) {
    Logger.log('Exception:'+e);
  }
}

function setTrigger(_, funcName) {
  if (funcName == undefined) { funcName = "sendDM" }

  const time = new Date();
  const rh = [9, 21]
  const rm = [0, 60]

  triggerHour = Math.floor(Math.random() * (rh[1] - rh[0])) + rh[0]
  triggerMinite = Math.floor(Math.random() * (rm[1] - rm[0])) + rm[0]

  time.setHours(triggerHour); 
  time.setMinutes(triggerMinite);
  time.setSeconds(0);
  ScriptApp.newTrigger(funcName).timeBased().at(time).create();
}

function deleteTrigger(_, funcName) {
  if (funcName == undefined) { funcName = "sendDM" }

  const triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == funcName) {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

実行結果

数日間このスクリプトを運用していますが、問題なく動作しています。

果たして何日目でライバロリさんはムシャーナの動画を上げてくださるでしょうか。
楽しみにしておきましょう。

終わりに

GASからTwitterAPIを叩いて自動でDMを送るbotを作成しました。
利用申請やトリガー生成など、思ったよりも実装に手間がかかりましたね。
勉強になりました。

ところでムシャーナの動画が投稿されるよりも前にこの記事がライバロリさん本人に見つかってしまったらどうなるのでしょうか。
やはり「botなんてけしからん!」と言って動画化していただけなかったりするのでしょうか。
それから、2021年11月19日に『ポケットモンスター ブリリアントダイヤモンド/シャイニングパール』が、2022年1月28日に『Pokémon LEGENDS アルセウス』が発売されることが分かっています。
それらが発売されるとこれまでのような剣盾環境の対戦動画が減ってしまう、あるいはサービス終了によって対戦環境自体が無くなってしまうことも考えられます。
あまり時間がありませんがそれまでにどうにか動画化していただきたいなあとも思っています。

最後に、今回の記事通りにやれば無限に大量のDMを送ることなども可能ですが、一線を越えて他人に迷惑をかけることは絶対にやめましょう。
一応、そこそこの開発技術が無いと最後までたどり着けないように書いたつもりですけど。
はい。今回は以上です。

追記

追記です。
結果が出ました!

タイトルとURLをコピーしました