CoreTweetでフォローを全てコピーしたリストを作る

TwitterのUserStreamの廃止が8/16に迫ってきましたが皆さんいかがお過ごしでしょうか。ここでは、応急処置としてフォローを全てコピーしたリストを作り、それを読み込ませることで既存のサードパーティークライアントで使う方法を解説したいと思います。

**※公式アプリや、公式ブラウザで閲覧する場合この処置は不要です**
※大量に（1000、2000単位）でリストを一気に追加すると、Twitterからスパム認定（？）されて一時的にリスト登録が制限されるケースがあります。自分はなりました。手加減しながらやってみてください。

## できること・できないこと
### できること
* リストの仕様が変更されない限り、従来のような時系列のタイムラインを維持できる
* リストを1分、30秒など定期的に更新することで擬似的（ここ重要）なUserStreamを再現できる
* 直感的にはUserStream導入前のTwitterのようなタイムラインになる

### できないこと
* UserStreamのような完全リアルタイムの更新はできない
* いいね、RTされた、リプライをもらったのようなイベント通知は受け取れない
* DMの仕様変更には対応できない

## なぜリストなの？
### APIのレート制限
UserStreamでなくホームタイムラインをAPIから取得する場合、「GET statuses/home_timeline」を使います。ただし、これはレート制限がえらく少なく[15分で15回しか呼び出せません](https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline.html)。つまりツイ廃のように15秒、30秒単位でホームを更新してるとすぐレート切れを起こします。

一方でリストの読み込みは「GET lists/statuses」ですが、こちらのAPI上限は潤沢で[15分で900回](https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses)呼び出せます。

（注意）いくら15分900回読み込めるといっても、リストを1秒単位で更新とかするとDoS攻撃と誤認されるおそれが高いので、**常識的な頻度に**するようにしてください。また、リストの呼び出し回数はリスト単位ではなく、ユーザー単位・アプリケーション単位で900/15minなので、マルチタブにする場合は他のリストの呼び出し回数も考慮するようにしてください。

### 5000人まで登録できる
リストはTwitterの仕様上、1リストに5000人まで登録できます。未だにUserStream使っている人よりも、フォローが5000人越えている人のほうが（おそらく）少ないと思われるので、フォローが5000人までのUserStreamなくなったら困るという人にはかなりコストパフォーマンスの高い方法だと思います。

### 注意点
リストの追加には「POST lists/members/create_all」というAPIを使うのですが、このドキュメントによると、

> Please note that there can be issues with lists that rapidly remove and add memberships. Take care when using these methods such that you are not too rapidly switching between removals and adds on the same list.

https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create_all.html

という注意書きがあります（要は短期間に大量の人を同一のリストに入れたり削除したりするな）。自分も一気に2100人ぐらいリストに突っ込もうとしたら、1900人突っ込んだ後から突っ込めなくなりました。こうなるとブラウザから手動で突っ込もうとしても蹴られます（「その操作は許可されていない」とか怒られます）。丸1日経ったら解除されましたが、この基準が公開されていなくよくわからないので**フォローが多い方は手加減しながらやってください**。

## 事前準備
### TwitterAppの作成
[Twitter Application Manager](https://apps.twitter.com/)から権限を「Read/Write」でアプリケーションを作成し、CosumerKey、ComsumerSecretを取得します。電話番号認証が必要な場合もあります。

今回は自分で使うだけのアプリケーションなので、ページの下にある「Your Access Token」のAccessToken、AccessTokenSecretをそのまま利用します。違うアカウントを認証させたい、不特定多数に利用させたい場合は、oAuthで認証させてください。oAuthのやり方は[CoreTweetの公式Wiki](https://github.com/CoreTweet/CoreTweet/wiki/%E8%AA%8D%E8%A8%BC)を参考にしてください。

### CoreTweetのダウンロード
NuGetから「[CoreTweet](https://github.com/CoreTweet/CoreTweet)」をダウンロードしてきます。以下コンソールアプリで作ります。

### リストの作成
CoreTweet側からやってもいいのですが、面倒なのでブラウザで新しいリストを作っておきます。ここでは名前を「timeline」としておきましょう。

### リストIDの取得
他の方法でも取得できるかもしれませんが、先程作ったtimelineのリストIDを取得します。

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CoreTweet;

namespace TimelineList
{
    class Program
    {
        static void Main(string[] args)
        {
            var token = CoreTweet.Tokens.Create(Settings.ConsumerKey, Settings.ConsumerSecret, Settings.AccessToken, Settings.AccessTokenSecret);
            GetListIds(token).Wait();
        }

        public static async Task GetListIds(Tokens token)
        {
            foreach(var list in token.Lists.List(screen_name => "ここに@以下のTwitterユーザー名を入れる"))
            {
                Console.WriteLine($"{list.Id}\t{list.Slug}");
            }
        }
    }
}
```

自分で使うアプリなので、トークンの類はSettingsのクラスにハードコーディングさせました。**これをgithubに公開するとTwitter垢乗っ取られるから気をつけてね**。

出力ではリストIDとリスト名の対応が出てきます。名前が「timeline」のIDをコピーしておきます。

## メインコード
これがメインコードです。listidのところに先程抜いたtimelineのリストIDを代入します。フォロー数が多い場合はループを分割するか、Delayの値を増やすかで調整してください。

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CoreTweet;

namespace TimelineList
{
    class Program
    {
        static void Main(string[] args)
        {
            var token = CoreTweet.Tokens.Create(Settings.ConsumerKey, Settings.ConsumerSecret, Settings.AccessToken, Settings.AccessTokenSecret);
            Command(token).Wait();
        }

        public static async Task Command(Tokens token)
        {
            //Following取得
            var following = (await token.Friends.IdsAsync()).ToArray();

            //ListId(ぶち抜いたListIdを代入)
            var listid = 5554560721;//値変えてね

            //100人ずつ追加
            var n = 100;
            foreach(var i in Enumerable.Range(0, following.Length / n +1))
            {
                Console.WriteLine(i);
                var followSlice = following.Skip(n * i).Take(n).ToArray();
                var response = await token.Lists.Members.CreateAllAsync(list_id => listid, user_id => followSlice);
                //2秒間隔で実行。フォローが多い場合はこの値を調整して手加減する
                await Task.Delay(2000);
            }
        }
    }
}
```

## 結果
フォローをすべてコピーしたリストが出来上がりました。自分の場合は途中怒られて丸一日かかってしまいましたが、フォロー数が少なかったり、ちゃんと手加減しながらやればすぐできるはずです。

![timeline.png](https://qiita-image-store.s3.amazonaws.com/0/232088/c6027707-532a-3041-d05e-4de6f6181702.png)

自分はしばらくこれで様子を見ようと思います。クライアントアプリのソースをいじることなくできるので、ユーザー側の対応としてはコストパフォーマンスいいんじゃないのかな？と思います。