OpenTweenを手探ってみた#2

[OpenTweenを手探ってみた#1](http://qiita.com/sabakanation/items/ddbf2d0046024288347f)の続きです。

#フォローフォロワーチェック
フォローフォロワーの変化を記録する機能の実装を試みます。起動時に毎回フォローフォロワーを記録しつつ前回からの変化を調べ、変化があった場合はその時の日付と一緒に変化を記録することにします。C#は簡単にウィンドウの作成ができることを知ったので、ボタンを押したらフォローフォロワーの変化が確認できるウィンドウが出るようにします。

##フォロワーのIDを記録
まずフォロワーチェックを実装します。フォロワーのIDを取得して記録します。

いかにもTwitteAPIを使う感じがするのでAPIについて調べてみます。APIに関してはこちらのサイト( https://syncer.jp/twitter-api-matome )が非常に参考になりました。TwitterAPIのfollowers/idsというエンドポイントでフォロワーのIDが取得できるということなので、「followers/ids」で全体に検索をかけるとTwitterApi.csにFollowersIdsというタスクが見つかります。このタスクを右クリックからFind All Referencesで参照元を調べると、Twitter.csにRefreshFollowerIdsというタスクが見つかります。ここにブレークポイントを立てて実行すると起動時に引っかかるので、起動時にフォロワーのIDを取得しているところだと推測できます。

ここにフォロワーIDをテキストで書き出すような処理と、前回起動時に書き出したフォロワーIDからの変化を調べる処理を加えます。

```C#:Twitter.cs
        public async Task RefreshFollowerIds()
        {
            if (MyCommon._endingFlag) return;

            var cursor = -1L;
            var newFollowerIds = new HashSet<long>();
            do
            {
                var ret = await this.Api.FollowersIds(cursor)
                    .ConfigureAwait(false);

                if (ret.Ids == null)
                    throw new WebApiException("ret.ids == null");

                newFollowerIds.UnionWith(ret.Ids);
                cursor = ret.NextCursor;
            } while (cursor != 0);

            foreach (long Id in newFollowerIds)
            {
                this.followerId.Add(Id);
            }
            TabInformations.GetInstance().RefreshOwl(this.followerId);

            this._GetFollowerResult = true;


            HashSet<long> oldFollowerIds = new HashSet<long>();
            string line;
            try
            {
                using (StreamReader sr = new StreamReader(@"FollowerIds.txt"))
                {
                    while ((line = sr.ReadLine()) != null)
                    {
                        oldFollowerIds.Add(long.Parse(line));
                    }
                }
            }
            catch (Exception error)
            {
                Console.WriteLine(error.Message);
            }
            StreamWriter writer = new StreamWriter(@"FollowerIds.txt", false);
            writer.Write("");
            writer.Close();
            writer = new StreamWriter(@"FollowerIds.txt", true);
            foreach (long Id in this.followerId)
            {
                writer.WriteLine(Id);
                if (oldFollowerIds.Contains(Id))
                {
                    oldFollowerIds.Remove(Id);
                    newFollowerIds.Remove(Id);
                }
            }
            writer.Close();
        }
```
newFollowersIdsに取得したフォロワーIDが記録されています。これに対してoldFollowersIdsというハッシュセットを作り、前回起動時のフォロワーIDをテキストから読み出します。その後テキストをnewFollowersIdsの内容で上書きしつつ、newFollowersIdsにもoldFollowersIdsにも含まれるIDを取り除きます。これでnewFollowersIdsには前回から増えたフォロワーだけが、oldFollowersIdsには前回から減ったフォロワーだけが記録されることになりましたが、まだそれを書き出すような処理は加えていません。

ここでのユーザーのIDは@から始まるアルファベット列ではなく、ユーザーごとに固有の数列です。このIDを人が見ても何も情報を得られないので、名前や@以下のアルファベット列(スクリーンネーム)を取得してからそれを記録するようにしたいと思います。

TwitterAPIを調べると、users/lookupというエンドポイントでIDからユーザー情報を取得できることが分かったので全体に検索をかけてみましたが、それらしいものは見つかりません。このエンドポイントはOpenTweenでは使われていないようです。

ということで先ほど見つけたTwitteApi.csにusers/lookupを使うタスクを追加します。先ほどのサイトを参考にしつつ適当にほかのタスクをパクります。

```C#:TwitterApi.cs
        public Task<TwitterUser[]> UsersLookup(long[] userIds)
        {
            var endpoint = new Uri("users/lookup.json", UriKind.Relative);
            var param = new Dictionary<string, string>
            {
                ["user_id"] = String.Join(",", userIds),
                ["include_entities"] = "true",
            };

            return this.apiConnection.GetAsync<TwitterUser[]>(endpoint, param, "/users/lookup");
        }
```
これを使ってユーザー情報を取得しそれを記録するようにします。同時に日付も記録します。RefreshFollowerIdsの末尾に次のような変更を加えます。

```C#:Twitter.cs
            if (oldFollowerIds.Count != 0 || newFollowerIds.Count != 0)
            {
                writer = new StreamWriter(@"FollowerChages.txt", true);
                writer.WriteLine(DateTime.Now.ToShortDateString());
                
                if (oldFollowerIds.Count != 0)
                {
                    long[] oldArray = (long[])oldFollowerIds.ToArray();
                    long[] oldArray2;
                    for(int i=0; i <= oldArray.Length/100; i++)
                    {
                        if(i != oldArray.Length / 100)
                        {
                            oldArray2 = new long[100];
                            Array.Copy(oldArray, i * 100, oldArray2, 0, 100);
                        }
                        else if (oldArray.Length % 100 == 0)
                        {
                            break;
                        }
                        else
                        {
                            oldArray2 = new long[oldArray.Length % 100];
                            Array.Copy(oldArray, i * 100, oldArray2, 0, oldArray.Length % 100);
                        }
                        var users = await this.Api.UsersLookup(oldArray2)
                            .ConfigureAwait(false);
                        foreach (TwitterUser usr in users)
                        {
                            writer.WriteLine("- {0}@{1}", usr.Name, usr.ScreenName);
                            oldFollowerIds.Remove(usr.Id);
                        }
                    }
                    foreach (long id in oldFollowerIds)
                    {
                        writer.WriteLine("- {0}", id);
                    }
                }
                if (newFollowerIds.Count != 0)
                {
                    long[] newArray = (long[])newFollowerIds.ToArray();
                    long[] newArray2;
                    for (int i = 0; i <= newArray.Length / 100; i++)
                    {
                        if (i != newArray.Length / 100)
                        {
                            newArray2 = new long[100];
                            Array.Copy(newArray, i * 100, newArray2, 0, 100);
                        }
                        else if (newArray.Length % 100 == 0)
                        {
                            break;
                        }
                        else
                        {
                            newArray2 = new long[newArray.Length % 100];
                            Array.Copy(newArray, i * 100, newArray2, 0, newArray.Length % 100);
                        }
                        var users = await this.Api.UsersLookup(newArray2)
                            .ConfigureAwait(false);
                        foreach (TwitterUser usr in users)
                        {
                            writer.WriteLine("+ {0}@{1}", usr.Name, usr.ScreenName);
                            newFollowerIds.Remove(usr.Id);
                        }
                    }
                    foreach (long id in newFollowerIds)
                    {
                        writer.WriteLine("+ {0}", id);
                    }
                }
                writer.Close();
            }
```
users/lookupは一度に100件までしか扱えないので、変化のあったIDが100を超える場合は100ずつ区切ってから投げるようにしてあります。また、ユーザーIDからユーザー情報が得られなかった場合(主にユーザーが削除されていた場合)はユーザーIDのみを表示するようにしてあります。

これで実行すると、OpenTween/bin/Debugの中にフォロワーIDとその変化を記録しているテキストファイルができていることが確認できます。

##ウィンドウに表示する
まずウィンドウを作成します。Visual StudioのソリューションエクスプローラーでOpenTweenのところを右クリックし、AddからWindows Formを選択します。ファイル名はFollowercheck.csとします。

次にソリューションエクスプローラーでTween.csを右クリックしてOpenするとOpenTweenのGUIが表示されます。「その他の機能」をクリックして「片思いユーザーリスト取得」の下を右クリックし、MenuItemをInsertします。「toolStripMenuItem1」と出ているところをクリックして「フォロワーチェック」とします。「フォロワーチェック」をダブルクリックすると「フォロワーチェック」をクリックしたときに呼ばれる関数に飛ぶので、ここにFollowercheck.csのウィンドウを表示する処理を書きます。

```C#:Tween.cs
        private void toolStripMenuItem1_Click(object sender, EventArgs e)
        {
            Followercheck fc = new Followercheck();
            fc.Show();
        }
```
これで実行すると「フォロワーチェック」のボタンからウィンドウが表示されることが確認できます。

Followercheck.csのウィンドウをいじっていきます。体裁を整えるためにPropertiesウィンドウでTextをフォロワーチェックに、FormBorderStyleをFixedDialogに、MaximizeBoxとMinimizeBoxをFalseにします。

フォロワーの変化を表示するのにTreeViewを用いることにします。TreeViewを用いるとこういう感じで表示できます。
![無題3.png](https://qiita-image-store.s3.amazonaws.com/0/146715/2585c838-7edb-0e64-d77e-6701ee2c4d66.png)
ToolboxからTreeViewをドラッグアンドドロップしてウィンドウに追加し、いい感じに配置します。フォロワーチェックのウィンドウをダブルクリックするとFollowercheck.cs内のFollowercheck_Loadというウィンドウが開いたときに呼ばれる関数に飛びます。ここにフォロワーの変化をテキストから読み出してTreeViewに表示する処理を書きます。

```C#:Followercheck.cs
        private void Followercheck_Load(object sender, EventArgs e)
        {
            string line;
            TreeNode rootNode = new TreeNode("");
            try
            {
                using (StreamReader sr = new StreamReader(@"FollowerChages.txt"))
                {
                    while ((line = sr.ReadLine()) != null)
                    {
                        if (line[0] != '-' && line[0] != '+')
                        {
                            rootNode = new TreeNode(line);
                            treeView1.Nodes.Add(rootNode);
                        }
                        else
                        {
                            rootNode.Nodes.Add(new TreeNode(line));
                        }
                    }
                }
            }
            catch (Exception error)
            {
                Console.WriteLine(error.Message);
            }
        }
```
ファイルを順番に見ていって日付があったらそれを親ノードとして設定、アカウントの変化をその日付の子ノードとして追加するようになっています。ファイルの読み込みのところで怒られるので、Followercheck.csの先頭に

```C#:Followercheck.cs
using System.IO;
```
を追加しました。

これでフォロワーの変化が追えるようになりました。

##フォローのIDを記録
次にフォローチェックを実装します。APIのfriends/idsというエンドポイントでフォローしている人のIDを取得できるらしいので、「friends/ids」で全体に検索をかけてみますがそれらしいところは見つかりません。どうやら使われていないようです。

ということなのでTwitterApi.csにfriends/idsを使うタスクを追加します。

```C#:TwitterApi.cs
        public Task<TwitterIds> FriendsIds(long? cursor = null)
        {
            var endpoint = new Uri("friends/ids.json", UriKind.Relative);
            var param = new Dictionary<string, string>();

            if (cursor != null)
                param["cursor"] = cursor.ToString();

            return this.apiConnection.GetAsync<TwitterIds>(endpoint, param, "/friends/ids");
        }
```
このタスクを使っているRefreshFollowerIdsに相当するものもないのでそれも作ります。Twitter.csに以下のRefreshFriendIdsを追加します。やっていることはRefreshFollowerIdsと同じでフォローIDを記録しつつ前回からの変化を調べて変化があればそれも記録します。

```C#:Twitter.cs
        public async Task RefreshFriendIds()
        {
            if (MyCommon._endingFlag) return;

            var cursor = -1L;
            var newFriendIds = new HashSet<long>();
            do
            {
                var ret = await this.Api.FriendsIds(cursor)
                    .ConfigureAwait(false);

                if (ret.Ids == null)
                    throw new WebApiException("ret.ids == null");

                newFriendIds.UnionWith(ret.Ids);
                cursor = ret.NextCursor;
            } while (cursor != 0);

            HashSet<long> oldFriendIds = new HashSet<long>();
            string line;
            try
            {
                using (StreamReader sr = new StreamReader(@"FriendIds.txt"))
                {
                    while ((line = sr.ReadLine()) != null)
                    {
                        oldFriendIds.Add(long.Parse(line));
                    }
                }
            }
            catch (Exception error)
            {
                Console.WriteLine(error.Message);
            }
            StreamWriter writer = new StreamWriter(@"FriendIds.txt", false);
            writer.Write("");
            writer.Close();
            writer = new StreamWriter(@"FriendIds.txt", true);
            HashSet<long> newFriendIds_ = new HashSet<long>();
            foreach (long Id in newFriendIds)
            {
                newFriendIds_.Add(Id);
            }
            foreach (long Id in newFriendIds_)
            {
                writer.WriteLine(Id);
                if (oldFriendIds.Contains(Id))
                {
                    oldFriendIds.Remove(Id);
                    newFriendIds.Remove(Id);
                }
            }
            writer.Close();

            if (oldFriendIds.Count != 0 || newFriendIds.Count != 0)
            {
                writer = new StreamWriter(@"FriendChages.txt", true);
                writer.WriteLine(DateTime.Now.ToShortDateString());
                
                if (oldFriendIds.Count != 0)
                {
                    long[] oldArray = (long[])oldFriendIds.ToArray();
                    long[] oldArray2;
                    for (int i = 0; i <= oldArray.Length / 100; i++)
                    {
                        if (i != oldArray.Length / 100)
                        {
                            oldArray2 = new long[100];
                            Array.Copy(oldArray, i * 100, oldArray2, 0, 100);
                        }
                        else
                        {
                            oldArray2 = new long[oldArray.Length % 100];
                            Array.Copy(oldArray, i * 100, oldArray2, 0, oldArray.Length % 100);
                        }
                        var users = await this.Api.UsersLookup(oldArray2)
                            .ConfigureAwait(false);
                        foreach (TwitterUser usr in users)
                        {
                            writer.WriteLine("- {0}@{1}", usr.Name, usr.ScreenName);
                            oldFriendIds.Remove(usr.Id);
                        }
                        foreach (long id in oldFriendIds)
                        {
                            writer.WriteLine("- {0}", id);
                        }
                    }
                }
                if (newFriendIds.Count != 0)
                {
                    long[] newArray = (long[])newFriendIds.ToArray();
                    long[] newArray2;
                    for (int i = 0; i <= newArray.Length / 100; i++)
                    {
                        if (i != newArray.Length / 100)
                        {
                            newArray2 = new long[100];
                            Array.Copy(newArray, i * 100, newArray2, 0, 100);
                        }
                        else
                        {
                            newArray2 = new long[newArray.Length % 100];
                            Array.Copy(newArray, i * 100, newArray2, 0, newArray.Length % 100);
                        }
                        var users = await this.Api.UsersLookup(newArray2)
                            .ConfigureAwait(false);
                        foreach (TwitterUser usr in users)
                        {
                            writer.WriteLine("+ {0}@{1}", usr.Name, usr.ScreenName);
                            newFriendIds.Remove(usr.Id);
                        }
                        foreach (long id in newFriendIds)
                        {
                            writer.WriteLine("+ {0}", id);
                        }
                    }
                }
                writer.Close();
            }
        }
```
当然このタスクを作っただけでは起動時に呼ばれたりはしません。RefreshFollowerIdsがどうやって起動時に呼ばれているか調べます。全体に「RefreshFollowerIds」で検索をかけるとTween.csでRefreshFollowerIdsAsyncというタスクが見つかります。さらにこれを参照している

```C#:Tween.cs
        private async void TweenMain_Shown(object sender, EventArgs e)
        {
            NotifyIcon1.Visible = true;

            if (this.IsNetworkAvailable())
            {
                StartUserStream();

                var loadTasks = new List<Task>
                {
                    this.RefreshMuteUserIdsAsync(),
                    this.RefreshBlockIdsAsync(),
                    this.RefreshNoRetweetIdsAsync(),
                    this.RefreshTwitterConfigurationAsync(),
                    this.GetHomeTimelineAsync(),
                    this.GetReplyAsync(),
                    this.GetDirectMessagesAsync(),
                    this.GetPublicSearchAllAsync(),
                    this.GetUserTimelineAllAsync(),
                    this.GetListTimelineAllAsync(),
                };

                if (this._cfgCommon.StartupFollowers)
                    loadTasks.Add(this.RefreshFollowerIdsAsync());
                …
```
という部分が見つかります。これはTweenMainのウィンドウが表示されるときに、loadTasksというリストに実行されるべきタスクをまとめているように見えます。

ということで、まずRefreshFollowerIdsAsyncをパクってRefreshFriendIdsAsyncを作ります。必要そうな部分だけ拾うとこんな感じになります。

```C#:Tween.cs
        private async Task RefreshFriendIdsAsync()
        {
            try
            {
                await this.tw.RefreshFriendIds();
            }
            catch (WebApiException ex)
            {
                this.StatusLabel.Text = $"Err:{ex.Message}(RefreshFriendsIds)";
            }
        }
```
これをloadTasksに追加されるようにします。TweenMain_Shownの

```C#:Tween.cs
                var loadTasks = new List<Task>
                {
                    this.RefreshMuteUserIdsAsync(),
                    this.RefreshBlockIdsAsync(),
                    this.RefreshNoRetweetIdsAsync(),
                    this.RefreshTwitterConfigurationAsync(),
                    this.GetHomeTimelineAsync(),
                    this.GetReplyAsync(),
                    this.GetDirectMessagesAsync(),
                    this.GetPublicSearchAllAsync(),
                    this.GetUserTimelineAllAsync(),
                    this.GetListTimelineAllAsync(),
                };
```
の最後に

```C#:Tween.cs
                    this.RefreshFriendIdsAsync()
```
を追加します。

これで実行すると、OpenTween/bin/Debugの中にフォローIDとその変化を記録しているテキストファイルができていることが確認できます。

##ウィンドウに表示する
ウィンドウに表示するのはフォロワーチェックと全く同様です。Friendcheck.csというWindows Formを作り、OpenTweenのGUIにフォローチェックというボタンを追加してFriendcheck.csのウィンドウが呼ばれるようにします。Friendcheck.csのウィンドウにTreeViewを追加してテキストに記録されたIDの変化を表示すれば完成です。