Twitter で 自動フォロワー獲得ツール を作った話 ②

# まえがき
本記事は、[Twitter で 自動フォロワー獲得ツール を作った話](https://note.com/masanaosuzuki/n/n48fd7289537e) の続編です。
前編をご覧になっていない方は、まずは上記のリンクへどうぞ。
また、本記事に記載されている全体のソースコードは、[Github](https://github.com/masagao/twitter_automation) からご覧いただけます。

# あらすじ
Twitter のフォロワーを増やすための法則を確立したものの、
「[GAOGAO](https://gaogao.asia/) エンジニアたるもの、プログラミングで実装してみてはどうか」
との [@tejitak](https://twitter.com/tejitak) のお言葉を受け、Node.js で自動化を試みるのでした。
当時は開発経験がまるでなかったため、初のアプリケーション作成になりました。

# おさらい
今回の実装内容
「**どんなコンテンツでも、あるフォーマットに沿ってツイートすれば、ほぼ確実にライクとフォローが期待できる法則**」
>① **毎日定時に決まったフォーマットでツイートする**
② **自身のタイムラインの全ツイートを可能な限り早くライクする**
③ **自身のフォロワーで、フォロワー数の多いユーザーの、最新のツイートにライクしている全ユーザーをフォローする**
④ **③ でフォローしたユーザーの中から、フォローバックされていないユーザーのプロフィールに行き、最新のツイート 3件 にライク + ミュートする**
⑤ **一定期間後、フォローされていない、且つミュートになっているユーザーをリムる**

# ⓪Twtter API の 利用
まず、[Twitter Developers](https://developer.twitter.com/apps) から API を登録しました。すると、

 - `Consumer Key`
 - `Consumer Secret`
 - `Access Token`
 - `Access Token Secret`

が発行されるので、環境変数に保存します。
これらを利用して、実際に Twitter にアクセスします。
以前は審査等なかったのですが、現在は英語での利用申請が必要です。

# ① 毎日定時に決まったフォーマットでツイートする
こちらは、手動でツイートしていたので、今回は割愛します。

# ② 自身のタイムラインの全ツイートを可能な限り早くライクする
こちらは、`twitterFav`メソッドにて実装しました。

```masanao.js
async function twitterFav() {

  const tweetsCount = 10
  const favouritesCount = 0
  const myId = process.env.my_id

  try {
    const tweets = await twitterApi.getHtimeline(client, tweetsCount)
    let arr = []

    for(let i = 0; i < tweets.length; i++) {
      if(tweets[i].favorited !== true && tweets[i].user.id_str !== myId) {
        arr.push(tweets[i].id_str)
      }
    }

    let checkedTweets = arr.join(',')
    const favTweets = await twitterApi.getLookup(client, checkedTweets)

    for(var i = 0; i < favTweets.length; i++) {
      if(favTweets[i].favorite_count >= favouritesCount) {
        await twitterApi.postFavoriting(client, favTweets[i].id_str)
      }
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterFav()
```

- `tweetsCount`：タイムラインから取得するツイート数
- `favouritesCount`：対象ツイートのライク数
- `process.env.my_id`：実行者の TwitterID

上記を設定し、エンドポイントをまとめた`twitterApi`ファイルから適宜メソッドを呼び出しています。

まず、自身のタイムラインから`tweetsCount`分のツイートを取得。
繰り返し処理にて、ライク済みのツイートと、自分のツイート以外を配列に格納。
配列内の`favouritesCount`で設定したライク数を超えているツイートにライクを実行。
という流れになっています。

`favouritesCount`があることで、誰もライクしていないツイートには触れないといった、ややテクニカルなプレーが可能になります。

# ③ 自身のフォロワーで、フォロワー数の多いユーザーの、最新のツイートにライクしている全ユーザーをフォローする
こちらは、`twitterBeg`メソッドにて実装しました。

```hajime.js
const asyncFunc = (targetTweet) => {
  return new Promise((resolve, reject) => {
    request(`https://twitter.com/i/activity/favorited_popup?id=${targetTweet}`, (err, response, body) => {
      resolve(body)
    })
  })
}

async function twitterBeg() {
  const myId = process.env.my_id;
  const influencerId = ''
  tweetCount = 1

  try
  {
    const tweets = await twitterApi.getUtimeline(client, influencerId, tweetCount)
    const arr = []

    for (let i = 0; i < tweets.length; i++) {
      arr.push({
        id: tweets[i].id_str,
        favorite_count: tweets[i].favorite_count
      })
    }

    arr.sort((a, b) => {
      const countA = a.favorite_count
      const countB = b.favorite_count
      if (countA > countB) {
        return -1
      } else if (countA < countB) {
        return 1
      }
      return 0
    })

    const targetTweet = arr[0].id
    const body = await asyncFunc(targetTweet)
    const json = JSON.parse(body)
    const content = json.htmlUsers
    const regex = /data-user-id=\"(\d+)\"/g
    let set = new Set()

    while((m = regex.exec(content)) !== null) {
      set.add(m[1])
    }
    const userIds = Array.from(set)

    for (var i = 0; i < userIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, myId, userIds[i])
        if (friendships.relationship.source.following !== true && friendships.relationship.source.muting !== true) {
          await twitterApi.postFollowing(client, userIds[i])
        }
      } catch (err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(5000)
    }
  } catch (err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterBeg()
```
- `myId`：実行者の TwitterID
- `influencerId`：対象ユーザーの TwitterID
- `tweetCount`：ユーザータイムラインから取得するツイート数

まず、`influencerId`のユーザータイムラインから、`tweetCount`分のツイートを取得。
その上でツイートのIDと、ライク数を抽出し、配列に格納。ライク数順にソート。
最もライク数の多いツイートの、`https://twitter.com/i/activity/favorited_popup?id=${targetTweet}`にアクセスし、ライクしている全ユーザーのIDをスクレイピングで取得。(*現在、Twitter の仕様が変わってしまった様で、上記サンプルコード実行できませんでした。折を見て修正予定です。)
繰り返し処理にて、取得したユーザーの中からフォロー中、ミュート中のユーザーを省き、フォローを実行。
という流れになっています。

ミュート中のユーザーを省く意図については、以前⑤でフォロー解除したユーザーへの再フォローを防止する施策になります。

# ④ ③ でフォローしたユーザーの中から、フォローバックされていないユーザーのプロフィールに行き、最新のツイート 3件 にライク + ミュートする
こちらは、`twitterMute`メソッドにて実装しました。

```tejitak.js
async function twitterMute() {

  const userId = process.env.my_id
  const friendsCount = 1
  const tweetsCount = 20
  const favoritedlimit = 3

  try {
    const friends = await twitterApi.getFriendsIds(client, userId, friendsCount)
    const targetIds = friends.ids

    for(var i = 0; i < targetIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, userId, targetIds[i])

        if(friendships.relationship.target.following !== true && friendships.relationship.source.muting !== true) {
          const friendsTweets = await twitterApi.getFriendstimeline(client, targetIds[i], tweetsCount)
          let favoritedCount = 0

          for(var j = 0; j < friendsTweets.length; j++) {
            if(friendsTweets[j].favorited !== true && favoritedCount < favoritedlimit) {
              await twitterApi.postFavoriting(client, friendsTweets[j].id_str)
              await twitterApi.sleep(1000)
              favoritedCount++
            }
          }
          await twitterApi.sleep(1000)
          await twitterApi.postMute(client, targetIds[i])
        }
      } catch(err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(2000)
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterMute()
```
- `userId`：実行者の TwitterID
- `friendsCount`：取得するフォロー中のユーザー数
- `tweetsCount`：ユーザータイムラインから取得するツイート数
- `favoritedlimit`：ライクするツイート数

まず、実行者のフォロー中のユーザーを`friendsCount`分取得。
繰り返し処理にて、フォローされておらず、未だミュートしていないユーザーを抽出。
該当ユーザーのユーザータイムラインから`tweetsCount`分のツイートを取得。
`favoritedlimit`に達するまで、未だライクしていないツイートにライクを実行。
リクエスト制限対策として`sleep関数`にて実行を停止した後に、フラグ用のミュートを実行。
という流れになっています。

我ながら、ミュート大好き過ぎますね。。

# ⑤ 一定期間後、フォローされていない、且つミュートになっているユーザーをリムる
最後に、`twitterUnfollow`メソッドの実装になります。

```hideyoshi.js
async function twitterUnfollow() {

  const userId = process.env.my_id
  const friendsCount = 25

  try {
    const friends = await twitterApi.getFriendsIds(client, userId, friendsCount)
    const targetIds = friends.ids

    for(var i = 0; i < targetIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, userId, targetIds[i])

        if(friendships.relationship.target.following !== true && friendships.relationship.source.muting === true) {
          try {
            await twitterApi.postUnfollowing(client, targetIds[i])
          } catch(err) {
            outLogger.warn(err)
            taskLogger.warn(err)
          }
        }
      } catch(err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(3000)
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterUnfollow()
```
- `userId`：実行者の TwitterID
- `friendsCount`：取得するフォロー中のユーザー数

まず、実行者のフォロー中のユーザーを`friendsCount`分取得。
繰り返し処理にて、フォローされておらず、既にミュート済みのユーザーを抽出。
フォロー解除を実行。
という流れになっています。

以上 ① - ⑤ をターミナルから定期実行していました。

# まとめ

じきに [@masumi_sugae](https://twitter.com/masumi_sugae) くんと一緒にプロダクト化してみようという話にもなったのですが、程なくして Twitter API 規制があり、頓挫してしまいました。
機会があれば調整して実用化したいとも思うので、ご興味のある方はぜひ。

また無知により、リーダブルコード信仰の代わりに、アニミズム信仰を持ち出して、ファイル名に自身や、身近な人の名前をつけていました。(ex. `masanao.js`) 
今となっては完全に謎です。

今回、Twitter のフォロワーを増やす方法と題して、二週にわたって執筆して参りましたが、
この方法で獲得したフォロワーは、数字上の意味しか持たないことをご留意ください。
よって、FF比率のカモフラージュを意図した徒らなフォロー解除を実施すると、ほぼ100%相手もリムってきます。

結論：フォロワー数を増やしたいのであれば、インフルエンサーに絡もう

-次回 (8/10)
「[Webサイト案件における初学者のジレンマ](https://note.com/masanaosuzuki/n/n07f52747e683)」