Help us understand the problem. What is going on with this article?

誰かの誕生日に自動でお祝いメッセージを送る人としての倫理がクソなアプリを作ろうとしたら対応APIがぜんぜん無かったけどどうにかした

こんにちは。@ampersand_xyzと申します。
今年はアドベントカレンダーを書く余裕がほとんどなく、当日にアプリを作りながらこの記事を書いています。

Twitterで誕生日に自動でお祝いメッセージを送れたらいいんじゃないか

自分はTwitterを結構な頻度でやっている方だとは思うのですが、お誕生日の人を見逃してうっかり当日をスルーしてしまうということがしばしばあります。
そこでお祝いメッセージ自動化を思い立ったのですがいかんせん自動でお祝いメッセージを送るというのはいかがなものかと感じるわけです。
というのも5年ほど前にFacebookで自動お祝いメッセージを送る試みを実際にやってみてめちゃくちゃ心が傷んだ経験があるためです。
であれば、クソアプリを作るこの時期にクソだからと言い張ればいいのではと着手し始めました。

スクリーンショット 2019-12-21 8.45.01.png

よし。あとは自動でメンションツイートするだけだ。

しかし、そこに至るにはTwitterの制限という高い壁がそびえ立っていたのです。

APIの自動化ポリシーに則りたい

TwitterAPIを使って自動ツイートを行う場合に留意しなくてはいけない事項として、自動化ポリシー( https://help.twitter.com/ja/rules-and-policies/twitter-automation )をよく確認する必要があります。
今回の件についてかいつまんで書くと以下の要項となります。

  • メンションを含んだツイートを行う場合は、送られる対象のユーザーが明確に送ることを許可していることが必要である
  • 明確に送ることを許可とは「宛先の利用者がこちらのアカウントのツイートに返信していた場合や、こちらにダイレクトメッセージを送っていた場合など」を指す
  • フォローを行っているのみでは明確に送ることを許可とは見なさない

どうすればいいのか

上記の自動化ポリシーに則るために、最初にツイート送信許可を明示してもらうためのコミュニケーション用ツイートを行います。
そのツイートに対してリアクション(いいね)を行ったユーザーに対してであれば自動化ポリシーに準拠するはずです。

しかし、TwitterのAPIには現在「ツイートに対していいねした人リスト」を取得する機能がありません。ファッキン。
そのため、「フォロワー限定」として登録アカウントのフォロワーがコミュニケーション用ツイートをいいねしたかどうか、を走査する必要があります。

つまり「フォロー&RTで応募完了!」みたいなキャンペーンをよく見かけるのはこういう制限があるからなんですね。納得〜〜。

対応策と実装

コミュニケーション用ツイートを行うためのボタンを画面内に設置します。ボタンをクリックすると自動でツイートが行われるようにし、行ったツイートのIDを保存しておきます。
スクリーンショット 2019-12-21 7.42.40.png

実装コード
  // ログイン処理時にアクセストークンを取得してユーザ情報を保存しておく。
  // 取り敢えず今回はFirebaseを使用。
  const docRef = db.collection('users').doc(user.uid)
  // ログイン者のアカウントを取得
  docRef.get()
    .then((doc) => {
      const userData = doc.data();
      // ログイン者のトークン情報でツイート
      var Twitter = require('twitter');
      var client = new Twitter({
        consumer_key: APP_CONSUMER_KEY,
        consumer_secret: APP_CONSUMER_SECRET,
        access_token_key: userData.token,
        access_token_secret: userData.secret,
      });
      client.post('statuses/update',
        { status: 'テスト中🎉お誕生日お祝いします🎉🎉🎉🎉\nこのツイートを「いいね」してくれたフォロワーの人にお誕生日にお祝いのメッセージを送ります!(誕生日が公開設定の場合に限ります)' },
        function (error, tweet, response) {
          if (!error) {
            // 告知用ツイートのIDをユーザーに紐付ける形で保存
            docRef.update({
              appealTweetId: tweet.id
            })
            res.status(200).json({ tweetId: tweet.id })
          } else {
            console.log(error);
          }
        })
    })
    .catch((err) => {
      // ユーザ情報取得エラー
      console.log('Error getting documents', err);
    });

誕生日を取得したい

誕生日ぐらいAPIレスポンスに含まれてるでしょとタカをくくっていたのですが、無いんですよ実は。知らなかったんですけど。完全に大誤算でした。クソがーーー!!!!!!!

pappeteerを使ってなんとかする

はい、出ました力こそパワーと言わんばかりのアレです。
pappeteerを使用してページ内の要素を漁って誕生日かどうかをチェックします。

誕生日のユーザーのページには風船が飛ぶ以外にこの部分がお誕生日という表示になっています。
image.jpeg

この要素にはいくつかのClassが付与されています
スクリーンショット 2019-12-21 7.58.46.png

そういう要素があることがわかったらPuppeteerで引っこ抜いてきます。
span.r-qvutc0というセレクタを用いて要素を抽出すると複数個の該当DOMが取得できますが、その中で「Today is their birthday!」という文字列を持った要素が見つかります。お前だ!!!!!

  const browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

  const page = await browser.newPage();
  // mobileTwitter(新しいUI)のURLを指定するように注意
  await page.goto('https://mobile.twitter.com/'+SCREEN_NAME);
  // 
  await page.waitFor(1000);
  await page.screenshot({ path: 'example.png' });

  const isBirthday = await page.evaluate(() => {
    let flag = false;
    const nodeList = document.querySelectorAll("span.r-qvutc0");
    nodeList.forEach(_node => {
      // 今日たんおめだよ〜〜って要素があれば誕生日と判定する
      if (_node.innerText == 'Today is their birthday!') {
        flag = true;
      }
    })
    return flag;
  });
  browser.close()

参考: https://qiita.com/tomi_linka/items/a68cf7840c3da002c6e0

フォロワー情報を取得したい

次はフォロワーを走査するための方法です。
取得した情報を走査してを誕生日かどうかを判別していきます。

フォロワーを取得する方法について

2種類の方法があります。

TwitterIDのリストを取得する

https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids

このエンドポイントを使用すると一度にフォロワーのIDを5000件取得できます。
ただし、IDが分かったところでスクリーンネームがわからないとスクレイピング対象のURLがわかりません。なんなんだよ。
IDからユーザページにリダイレクトするURLもあるにはあるようなのですが 遷移できるユーザとそうでないユーザがいて仕様がよくわかりませんでした。採用見送りです。

Twitterユーザーのリストを取得する

https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-list

ユーザーの詳細情報が含まれるオブジェクトを返却するリストです。詳細情報っつっても誕生日が含まれてませんけどね。ケッ!!!
しかしこちらは一度に取得できる件数は200件までとなります。
なお、1つのユーザーあたりのこのエンドポイントへのリクエスト回数制限は15回/15分です。
1度取得して1ユーザあたり0.3秒以上の処理時間のインターバルを持てばギリギリなんとか制限回避できるはずです。
とはいえ今回の要件では都度puppeteerを走らせてるのでもっと時間がかかりますが…。バカ正直に都度走査せずDBにフォロワーさんの誕生日情報を保持するなどの手段でカバーするのが無難です(今回そこまでやりませんでした)

参考: https://qiita.com/GakuNaitou/items/c20809600dade7770d27

フォロワーさんがツイートをいいねしたかどうかチェックする

https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-favorites-list

前項で取得したフォロワーさん情報を取得したら、前前前項で保存しておいたコミュニケーション用のツイートのIDをいいねしたかどうかチェックし、該当していればお祝いメッセージを送ります。

if (isBirthday) {

    client.get('favorites/list',
      {
        user_id: follower.id,
        since_id: userData.appealTweetId,
      },
      function (error, tweet, response) {
        if (!error) {
          // 対象のツイートがFavされていたら
          if (tweet[0].id == userData.appealTweetId) {
            // お祝いメッセージをメンションする
            client.post('statuses/update',
              { status: '@' + follower.screen_name + ' さん\n' + userData.message },
              function (error, tweet, response) {
                if (!error) {
                  // 正常に処理終了
                } else {
                  console.log(error);
                }
              })
          }
        } else {
          console.log(error);
        }
      })
  }

そして出来上がったものがこちらになります


まあ、自分の誕生日は5月なんですけどね。

なお、API凍結が行われると自分の他のアプリも影響を受けてしまうというおそれがあるため以下に紹介する作成したアプリは現在一般公開していません…。影響を受けて困るアプリの移行申請は済んでいるのですが、完了次第公開できればしたいと思います。puppeteerがガリガリに動くサーバーが楽に用意できるのかは別の話ですが。

余談

Twitterはテストユーザ作成機能がなく、やむなく新規のアカウントを2つ作成したのですが、新規アカウントの制限なのかAPIで情報取得しようとしたさい エラーコード34 'Sorry, that page does not exist.' というエラーが帰ってくるようになりました。なんでさ!robotかどうか確認したじゃん!!!humanだって言ってくれたでしょ!!電話番号だって登録しただろうが!!!
この辺の開発しづらさ、API制限の厳しさ、本当にどうにかしてほしいですが、この数年のAPIをめぐる動向を見る限り期待はできないと思われます。

教訓

クソアプリでどうにかしようとかしないで、お祝いの言葉には真心こめましょう。

ampersand
クソアプリの投稿ばっかりしてるのでContributeの数に惑わされないようにしてください。僕の技術力はしょっぱいです。
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした