たんぶろぐ

すぐ忘れる

Go で Twitter のリスト管理を始める (入門編)

Twitter のリスト管理に Go の ChimeraCoder/anaconda というライブラリを使用したので利用例を紹介します。

ながい前置き

最近 Twitter のフォロー数がある程度増えてきて重要な情報を見逃すことが増えてきたので、相互の人・公式アカウント・特定の界隈の人、のようにリスト管理をしたくなってきました。僕の場合、同じ人が複数のリストにまたがって入ることは少ないので「未整理リスト」としてフォローしている全員が入ったリストを作り、そこから別のリストに移動することにしました。

ですが、いざやってみると Twitter のリスト管理って面倒なんですよね。Twitter 公式はユーザーをひとりひとり追加していく必要があるし、icotile3 (サードパーティ製 Web アプリ) は全選択機能があるのですが少し使いにくかったりします。*1

コツコツとリスト整理してきた人はちょっといじるだけなので楽なのですが、新しく作るってなると結構負荷高いんですよね。

そんな経緯でプログラムにやらせようということになりました。

Twitter Developer Account

以下の開発には TwitterDeveloper Account が必要になります。まだ持っていない人は下記のページから申請を行ってください。

去年から少し申請が厳しくなりましたが (UseCase の詳細な記述など)、聞かれたことに答えていれば特に問題はありません。運営の返答は早いので翌日には返信が来て、Approve の通知、または更に詳細な情報を求められます。私の場合は一度詳細情報を求められたあとに Approve されました。申請から Approve までは 1 日くらいでした。

ChimeraCoder/anaconda

ChimeraCoder/anaconda は Go の Twitter API ラッパーライブラリです。シンプルで余計な機能がないところがとても気に入っています。

開発はここらへんを見ながら進めます。Godoc には各関数に対する説明がほとんどありませんが、これは Twitter API に準拠しているためなので、そういう部分は Twitter API Docs の対応するエンドポイントの説明を参照します。

TwitterApi 構造体の取得

全ての API リクエストはここを介して行われます。予め Twitter Developer Portal で取得しておいた API キー (Twitter アプリケーションを区別する認証情報) と自身のアクセストークン (Bearer Token) を渡します。

api = anaconda.NewTwitterApiWithCredentials(
    AccessToken,
    AccessTokenSecret,
    APIKey,
    APISecretKey,
)

今回は自分自身なので直接アクセストークンを取得していますが、他のユーザーを操作する場合は func (*TwitterApi) AuthorizationURL あたりからトークンを取得する必要があります。

Cursor

早速フォロー情報の取得に入りたいところですが、その前に Cursor と anaconda での扱いについて少し話しましょう。CursorTwitter API が持つページネーションの仕組みです。フォロー数の取得など大量のレスポンスが想定される API にはこれが適用されています。

Cursor は実データと前後の Cursor ID を持っている双方向連結リストです。リクエスト時にそのインデックスを渡すことにより対応したページのデータが得られます。-1 は最初のページを示しており、多くの場合これを初期値としてリクエストを開始します。0 はその先がないことを示しており、クライアントはこれにより最後のページだと判断します。簡単な例を下記に示します。

//-> Request to http://example.com&cursor=-1
//<- Response
{
  "ids": [
    "aaa",
    "bbb",
    "ccc"
  ],
  "previous_cursor": 0,
  "next_cursor": 200
}

//-> Request to http://example.com&cursor=200
//<- Response
{
  "ids": [
    "ddd",
    "eee",
    "fff"
  ],
  "previous_cursor": 100,
  "next_cursor": 300
}

//-> Request to http://example.com&cursor=300
//<- Response
{
  "ids": [
    "ggg",
    "hhh",
    "iii"
  ],
  "previous_cursor": 200,
  "next_cursor": 0
}

このように一度のリクエストで完結しないため、anaconda では Channel を用いてユーザーにレスポンスを提供しています。具体的なコードは次のセクションを見てください。

フォロー情報の取得

まず自分がフォローしているユーザー (Friends) と フォロワー (Followers) の ID を取得します。

Cursor を使用した API なのでコードは下記のようになります。

friends := make([]int64, 0, 1000)
friendsChan := api.GetFriendsIdsAll(nil)

friendsLoop:
for {
    select {
    case p, ok := <-friendsChan:
        if ok {
            for _, id := range p.Ids {
                friends = append(friends, id)
            }
        } else {
            break friendsLoop
        }
    }
}


followers := make([]int64, 0, 1000)
followersChan := api.GetFollowersIdsAll(nil)

followersLoop:
for {
    select {
    case p, ok := <-followersChan:
        if ok {
            for _, id := range p.Ids {
                followers = append(followers, id)
            }
        } else {
            break followersLoop
        }
    }
}

ちなみに似たような関数に func (TwitterApi) GetFriendsListAllfunc (TwitterApi) GetFollowersListAll がありますが、リスト追加には ID だけで十分なのと、こちらは Rate Limit の関係で 300 人までしか取得できない (1 Cursor 20 人までで 15 分間に 15 回しかリクエストできない) ため使用しませんでした。

次に相互フォローのユーザーとそれ以外に分割します。Twitter API に相互フォローのユーザーを取得する API は提供されていないため、Friends と Followers の And を取ります。関係性は下記のようになります。

  • 相互フォローの Friends
    • n(Friends) ∩ n(Followers)
  • 相互フォローでない Friends
    • n(Friends) - n(Followers)

ここらへんのスニペットを置いておきました。必要でしたらコピペしてください。

リストの作成

分類をするリストを作成します。

mutual, err := api.CreateList("Mutual", "mutual friends", nil)
if err != nil {
    return err
}

unsorted, err := api.CreateList("Unsorted", "other friends", nil)
if err != nil {
    return err
}

すでにあるリストに追加したい場合は、そのリストの ID を取得して指定する or Slug でリストを指定する必要があります。Slug については次のセクションで少し触れます。

自身のリストを取得するには、自身のユーザー ID が必要なので下記のようにします。

me, err := api.GetSelf(nil)
if err != nil {
    return err
}

lists, err := api.GetListsOwnedBy(me.Id, nil)
if err != nil {
    return err
}

var mutual, unsorted anaconda.List
for _, l := range lists {
    switch l.Name {
    case "mutual":
        mutual = l
    case "unsorted":
        unsorted = l
    default:
    }
}

ユーザーの追加

最後に用意したユーザーをリストに突っ込みましょう。

リストへの追加は Twitter API の仕様により、一度に 100 人までしか追加できないようになっています。そのため、追加するユーザーの配列を 100 人ずつに分割する必要があります。下記に 100 人単位で 2 次元配列に変換するスニペットを置いておきました。必要でしたらコピペしてください。

分割したら API を叩きます。ここのポイントは func (TwitterApi) AddMultipleUsersToList に対して第 1 引数のスクリーンネーム (Twitter のユーザー名) の配列を渡しているのではなく、第 3 引数の url.Value 経由で ID (Twitter 内部の ID) 配列を渡しているところです。これまでに取得したフォロー情報には ID しか含まれていないためこうしています。仮にスクリーンネームでやる場合は各ユーザーの情報を引っ張ってくる必要があります。

// chunk はユーザー配列 (100 人以内)
// e.g.) chunk = []strings{"123", "456", ...}

v := url.Values{}
v.Set("user_id", strings.Join(chunk, ","))

_, err := api.AddMultipleUsersToList(nil, mutual.Id, v)
if err != nil {
    return err
}

このように anaconda ではメインとなるパラメータ以外は url.Value で渡す設計になっています。前セクションで触れた Slug によるリスト指定は下記のように行います。ここでの Slug は ID を指定する代わりに、リスト名とリストを保持するユーザー ID から対象リストを特定するものです。その他パラメータは Twitter API Docs を見ることで知ることができます。

v := url.Values{}
v.Set("user_id", strings.Join(chunk, ","))
v.Set("slug", "Mutual")
v.Set("owner_screen_name", "zuiurs")

_, err := api.AddMultipleUsersToList(nil, 0, v)
if err != nil {
    return err
}

おわりに

anaconda とても使いやすくていい感じでした。

今回は関数を利用して操作するだけでしたが、気が向いたら宣言的にリストを管理するものでも書きたいですね。とはいえほとんどアイコンでユーザーを判断しているので、文字だけで管理するのもなんかなーと思ってます。フロントと組み合わせられたら良いですね。

*1:icotile3 は全選択ができる一方で、一度に追加できるユーザー数に制限 (100 人まで) があり、結局自分の手で 100 人クリックする必要があって使用を断念しました。他に方法があったらごめんなさい。ちなみに 100 人制限は Twitter API の仕様です。