Cloud Function で、Retweetした人全員(無料プランなので1週間前まで)を取得してみた

概要

とあるツイートをリツイートした人を取得したかった。
今回は、全部Firebaseで行こう!ってなってたので、Cloud Functions でやってみました。

参考

Firebase Cloud Functions で Twitter API と連携してみる - Qiita

Firebaseを駆使してTwitterをbot化してみた - Qiita

[Twitter API] リツイートを100件より多く取得する方法 – プログラミング生放送

やること

twitterAPIの準備

Twitterのアクセスは、twitを利用しました。
GitHub - ttezel/twit: Twitter API Client for node (REST & Streaming API)

twitterの情報は、上の記事にあるように、configに入れて使ってます。

TweetIdからテキストを取得

こちらのAPIを利用すれば、TweetIdから取得できるのですが、最新100件までしか取得できません。
GET statuses/retweeters/ids — Twitter Developers

そのため、今回は、下記を使ってます。
Standard search API — Twitter Developers

テキストを用いて、Retweetしたユーザーを取得

だいたい先頭100文字が一致してれば、対象のTweetRetweetしたTweetだと判断して、先頭100文字で検索しています。
上の記事では、filter:retweets 条件を追加していましたが、下記の理由で外しました。

search API は、100件取得できなくても、それより古いRetweetがある可能性があります。
そのため、100件以下しかRetweetが取得できなくても、もう一回叩いて、古いRetweetがないかを確認しないといけません。
そうすると、2倍呼び出すことになり、APIの制限にかかる可能性が上がります。
一方、filter:retweets を外すと、対象のTweetが取得できることになります。
対象のTweetが取得できたということは、それより古いRetweetが存在しないということなので、APIの呼び出しが1度で済みます。
そのため、対象のTweetを取得するために、filterを外しました。

検索条件には、since_idとmax_idを入れていますが、これは上の記事に説明があります。

また、計算時、BigIntに型変換しているのは、twitterIdは下記記事にあるように、通常のInt型ではおさまらないためです。 TwitterIDが33bitになったため、値をINT型に入れているとエラーに。

こんな感じで、Retweetした全てのユーザーを取得できると思います!
下に実際のプログラムを記載してますが、記事に載せるように変更した部分あるので、うまく動かなかったらすいません...

プログラム

import * as functions from 'firebase-functions';
import * as twit from 'twit';

const twitter = new twit(
  {
    consumer_key: functions.config().credential.twitter.consumer_key,
    consumer_secret: functions.config().credential.twitter.consumer_secret,
    access_token: functions.config().credential.twitter.access_token,
    access_token_secret: functions.config().credential.twitter.access_token_secret,
  }
);

// TweetIdから、Retweetされた文字を取得する。
const fetchRetweetList = async (tweetId: String, latestTweetId: String): Promise<any[]> => {

  // TweetIdから、TweetのTextを取得する。
  const text = await twitter.get('statuses/show/:id', { id: tweetId, include_entities: false }).catch(err => {
    console.log('caught error', err.stack)
  })
  .then((result: any) => {
    if (result === undefined) return '';
    return result.data.text;
  });

  if (text === '') return [];

  // since_id に前回最新だったTweetIdを取得し、1引くことで、前回最新だったTweetを含む、新しいTweetを取得するようにする。
  // TweetIdの桁数が大きいので、BigInt型を利用する。
  const params = { q: `${text.slice(0, 100)}`, count: 100, since_id: String(BigInt(latestTweetId) - BigInt(1)) };

  let isSearchContinue = true;
  let maxId = '';
  let results: any[] = [];

  // 取得したことがない全てのReTweetを取得する。
  // 一度に100件までしか取得できないので、max_idを更新しながら、whileを続けていく。
  while (isSearchContinue) {
    // max_id にIDを指定すると、それより古いTweetを取得できる。(指定したIDを含むっぽい)
    const queryParams = maxId ? Object.assign(params, { max_id: maxId }) : params;

    // 100件取得できない場合が多い...鍵付きユーザーのためか?
    const tmp = await twitter.get('search/tweets', queryParams)
      .catch(err => {
        console.log('caught error', err.stack)
      })
      .then((result: any) => {
        const searchList = result.data.statuses;
        // 50件以下の検索結果の場合は、流石にこれ以上古い結果はないだろうということで、次の検索を行わない。
        // もっといい方法あるかも。もしかしたら、無限ループ陥る可能性もあるかもしれないので、もしコピーするときは注意してください。
        if (searchList.length < 50) {
          isSearchContinue = false;
        }

        // 次の検索の時に、基準とするIDは、一番古いTweetIDより、1古いIDとする。
        maxId = searchList[searchList.length - 1].id_str
        maxId = String(BigInt(maxId) - BigInt(1));

        return searchList.map((tweet: any) => {
          const tweetId = tweet.id_str;

          // もし前回既に取得したものがあったら、次の検索を行わない。
          if (tweetId == latestTweetId) {
            isSearchContinue = false;
          }

          const user = tweet.user;
          const retweet = tweet.retweeted_status;
          return {
            tweetId: tweetId,
            userId: user.id_str,
            retweetId: retweet ? retweet.id_str : ''
          }
        });
      });

    results = results.concat(tmp);
  }

  // retweetIdが、指定したTweetIdと異なる可能性があるので、除外する。
  return results.filter((result: any) => result.retweetId == tweetId);
}

exports.retweeter = functions.https.onRequest(async (request, response) => {
  const result = await fetchRetweetList(1, 3);

  response.send({ data: result });
});

終わり

初めてcloud functionsとか、firestoreとか触りましたが、すごく便利でした!