OpenTweenを手探ってみた
大学の実験「大規模ソフトウェアを手探る」でオープンソースのツイッタークライアントOpenTweenを手探っていじってみたときの記録です。
やりたかったこと
古いツイートをふぁぼると新しいツイートに埋もれてふぁぼ一覧で確認できない! のでふぁぼったツイートをふぁぼった順にソートする機能を追加しようとしました。
また、フォローフォロワーの数が減っているといなくなったのが誰か気になってしょうがないので、フォローフォロワーの変化を記録する機能を追加しようとしました。
環境
今回手探ったのはこの時点で最新のOpenTween v1.3.5。OpenTweenはC#で開発されているのでOSはWindows10。Visual Studioを使いました。
とりあえずダウンロードして実行
ソースコードは以下から入手できます。git cloneするかエクスポート:zipでダウンロードします。
https://ja.osdn.net/projects/opentween/scm/git/open-tween/tree/master/
フォルダ内にあるOpenTween.slnでVisual Studioが開きます。上のstartを押せばあっさり実行できました。
ふぁぼ順ふぁぼ
最初にふぁぼ順ふぁぼの実装を試みます。OpenTweenはRecent, Reply, Direct, Favoriteなどのタブごとにツイートが振り分けられるようになっているので、ここにFavorites2タブを新しく追加してそこでのソート基準をふぁぼった時間にすることにします。ツイート自体にはふぁぼられた時間は記録されていないので、クライアント側で記録するようにします。
Favorites2タブの作成
OpenTweenはこんな感じでタブにツイートが振り分けられてます。
最初からあるFavoritesタブをそのまま複製してFavorites2タブを作ってみます。コード全体の構造を把握するなどできるわけもないので、適当に検索をかけて手探っていきます。Visual StudioではCtrl+Fでファイル内の検索、Ctrl+Shift+Fで全体の検索ができます。
タブにFavoritesと表示されているので、これにあたりをつけて「”Favorites”」(文字列としてのFavoritesを探したいのでダブルクオーテーションまで含めて)で全体を検索します。するとMyCommon.cs内の以下の部分で引っかかります。
public static class DEFAULTTAB
{
public const string RECENT = "Recent";
public const string REPLY = "Reply";
public const string DM = "Direct";
public const string FAV = "Favorites";
public static readonly string MUTE = Properties.Resources.MuteTabName;
}
デフォルトのタブを設定してるところみたいですね。ここに
public const string FAV2 = "Favorites2";
を追加してみますがタブが増えることはありません。
ならばということでDEFAULTTABのところで右クリックしてFind All Referencesします。するとFavoritesTabModel.csというそれっぽいファイルが出てきました。これがFavoritesタブの本体のようなものでしょう。同じModelsディレクトリに新しくFavoritesTabModel2.csというファイルを作成して内容をほぼパクります。以下の部分だけ変更します。
…
public class FavoritesTabModel2 : TabModel
{
public override MyCommon.TabUsageType TabType
=> MyCommon.TabUsageType.Favorites2;
public FavoritesTabModel2() : this(MyCommon.DEFAULTTAB.FAV2)
{
}
public FavoritesTabModel2(string tabName) : base(tabName)
{
}
…
当たり前ですがMyCommon.TabUsageType.Favorites2のところで怒られます。TabUsageTypeのところで右クリックしてGo To Definitionします。するとMyCommon.cs内の
[Flags]
public enum TabUsageType
{
Undefined = 0,
Home = 1, //Unique
Mentions = 2, //Unique
DirectMessage = 4, //Unique
Favorites = 8, //Unique
UserDefined = 16,
…
SearchResults = 4096,
}
というところに飛ばされます。なんだかよくわからないので適当に
Favorites2 = 8192,
を追加してみるとエラーはなくなりましたが、タブが増えてはくれません。
さて、いったんFavoritesTabModel.csにもどって考えてみます。FavoritesTabModelというクラスがどのように扱われているのか調べるために「FavoritesTabModel」で全体を検索します。するとTween.csなる怪しげなファイルに出くわしました。どうやらこのファイルがメインの処理を行っているようです。Tween.cs内で検索に引っかかったところを順番に見ていくと、まず
//デフォルトタブの存在チェック、ない場合には追加
…
if (this._statuses.GetTabByType<FavoritesTabModel>() == null)
this._statuses.AddTab(new FavoritesTabModel());
…
ここはコメントに書いてある通りデフォルトタブを追加しているところなので
if (this._statuses.GetTabByType<FavoritesTabModel2>() == null)
this._statuses.AddTab(new FavoritesTabModel2());
を追加します。
つぎを見ると
switch (tabSetting.TabType)
{
…
case MyCommon.TabUsageType.Favorites:
tab = new FavoritesTabModel(tabSetting.TabName);
break;
…
}
どうやらタブごとに色々設定しているらしいので
case MyCommon.TabUsageType.Favorites2:
tab = new FavoritesTabModel2(tabSetting.TabName);
break;
を追加します。
次は
if (_statuses.Tabs.Count == 0)
{
…
_statuses.AddTab(new FavoritesTabModel());
}
タブがない場合はデフォルトタブを追加、といった感じでしょうか。ここにも
_statuses.AddTab(new FavoritesTabModel2());
を追加します。
もう一か所GetFavoritesAsyncというタスクで検索が引っ掛かりましたが、どうもAPIを叩いてふぁぼったツイートを取得しているところのようなのでとりあえず無視します。
これで実行するとようやくFavorites2タブが表示されました。
ふぁぼった時間の記録
ふぁぼった時間を記録するために、ふぁぼったときに呼ばれる場所を探します。先ほど見つけたTween.csなるメインの処理を行っていそうなところに「favorite」で検索をかけます。いくつか引っかかりますが順番に見ていくとFavoriteChangeというタスクが見つかります。
//trueでFavAdd,falseでFavRemove
というコメントがあるので間違いないでしょう。
ここに処理を加える前にふぁぼったツイートのIDと時間を記録するためのDictionaryを作っておきます。FavoritesTabModel2のクラス内に
public static Dictionary<long, String> favorites2_dict;
static FavoritesTabModel2()
{
favorites2_dict = new Dictionary<long, string>();
}
を追加します。
Tween.csのFavoriteChangeに戻ります。Dictionaryに追加すると同時にテキストにも書き出すので以下のように変更します。コメントにあったようにFavAddがtrueのときにふぁぼったときの処理、falseの時にふぁぼを解除した時の処理を書きます。
private async Task FavoriteChange(bool FavAdd, bool multiFavoriteChangeDialogEnable = true)
{
…
if (FavAdd)
{
var selectedPost = this.GetCurTabPost(_curList.SelectedIndices[0]);
if (selectedPost.IsFav)
{
this.StatusLabel.Text = Properties.Resources.FavAddToolStripMenuItem_ClickText4;
return;
}
await this.FavAddAsync(selectedPost.StatusId, tab); String time_now = DateTime.Now.ToString(@"M/d/yyyy HH:mm:ss");
StreamWriter writer = new StreamWriter(@"Favorites.txt", true);
writer.Write("{0} ", selectedPost.StatusId);
writer.WriteLine(time_now);
writer.Close();
FavoritesTabModel2.favorites2_dict[selectedPost.StatusId] = time_now;
}
else
{
…
foreach (long Id in statusIds)
{
if (FavoritesTabModel2.favorites2_dict.ContainsKey(Id))
{
FavoritesTabModel2.favorites2_dict.Remove(Id);
StreamWriter writer = new StreamWriter(@"Favorites.txt", true);
writer.WriteLine("{0} -", Id);
writer.Close();
}
}
}
}
ふぁぼを解除した時、テキストにはツイートのIDとハイフンを記録しておきます。
OpenTween起動時にテキストから読みだしてDictionaryに移し替えます。起動時に呼ばれる場所を探すためにTween.cs内を「起動」で検索するとTweenMain_Loadなる関数が見つかりました。ブレークポイントを立ててみると起動時に止まるのが確認できます。この関数の中の適当な位置に以下を追加します。
string line;
Boolean flg = false;
try
{
using (StreamReader sr = new StreamReader(@"Favorites.txt"))
{
while ((line = sr.ReadLine()) != null)
{
if (line.Substring(19) == "-")
{
FavoritesTabModel2.favorites2_dict.Remove(long.Parse(line.Substring(0, 18)));
flg = true;
}
else
{
FavoritesTabModel2.favorites2_dict[long.Parse(line.Substring(0, 18))] = line.Substring(19);
}
}
}
}
catch (Exception error)
{
Console.WriteLine(error.Message);
}
if (flg)
{
StreamWriter writer = new StreamWriter(@"Favorites.txt", false);
writer.Write("");
writer.Close();
writer = new StreamWriter(@"Favorites.txt", true);
foreach (long Id in FavoritesTabModel2.favorites2_dict.Keys)
{
writer.Write("{0} ", Id);
writer.WriteLine(FavoritesTabModel2.favorites2_dict[Id]);
}
writer.Close();
}
ハイフンがあったらそのIDはDictionaryから削除するようにしてあります。
これでふぁぼった時間の記録ができるようになりました。
Favorites2タブにふぁぼったツイートを表示
Favorites2タブにふぁぼったツイートを表示させます。ふぁぼったツイートがどうやってFavoritesタブに表示されるのか調べるために、先ほど見つけた「TabUsageType.Favorites」で全体に検索をかけます。するとTableInformations.csにDistributePostsという関数が見つかります。ツイートを各タブに振り分けているみたいなので、以下のように変更を加えてFavorites2タブに振り分けられるようにします。まず
var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
の後ろに
var fav2Tab = this.GetTabByType(MyCommon.TabUsageType.Favorites2);
を加えます。次に
// Fav済み発言だったらFavoritesタブに追加
if (post.IsFav)
favTab.AddPostQueue(post);
の後ろに
if (FavoritesTabModel2.favorites2_dict.ContainsKey(post.StatusId))
fav2Tab.AddPostQueue(post);
を加えます。OpenTweenでふぁぼったものだけがFavorites2タブに追加されるようにしてあります。
これで実行してみますが、ふぁぼってもFavorites2タブには振り分けられません。OpenTweenを再起動しないと反映されないようです。ふぁぼを削除した時も同様です。
もう1度「TabUsageType.Favorites」で全体に検索をかけます。するとFavAddAsyncInternalとFavRemoveAsyncInternalという2つのタスクの中で引っかかります。OpenTweenの実行中にはここで振り分けを行っているようです。favTabをパクって、FavAddAsyncInternalの方は
var favTab = this._statuses.GetTabByType(MyCommon.TabUsageType.Favorites);
favTab.AddPostQueue(postTl);
の後ろに
var fav2Tab = this._statuses.GetTabByType(MyCommon.TabUsageType.Favorites2);
fav2Tab.AddPostQueue(postTl);
を追加します。FavRemoveAsyncInternalの方は
var favTab = this._statuses.GetTabByType(MyCommon.TabUsageType.Favorites);
の後ろに
var fav2Tab = this._statuses.GetTabByType(MyCommon.TabUsageType.Favorites2);
を、また
favTab.EnqueueRemovePost(statusId, setIsDeleted: false);
の後ろに
fav2Tab.EnqueueRemovePost(statusId, setIsDeleted: false);
を加えます。
これでふぁぼったツイートがFavorites2タブに振り分けられるようになりました。
Favorites2タブのソート基準を変更
Favorites2タブのソート基準をふぁぼった時間に変更します。適当に「sort」で全体に検索をかけるとTabModel.csにApplySortModeなる関数を発見しました。
if (this.SortMode == ComparerMode.Id)
{
comparison = (x, y) => sign * x.CompareTo(y);
}
これがおそらく時間順(実際はツイートID順ですがツイートIDは時間順に付けられるみたいなので)にソートするよう設定しているところでしょう。ここをFavorites2タブの時だけふぁぼった時間を基準にソートされるように変更します。
if (this.SortMode == ComparerMode.Id)
{
if (this.TabName == "Favorites2")
{
comparison = (x, y) =>
{
if (FavoritesTabModel2.favorites2_dict.ContainsKey(x) && FavoritesTabModel2.favorites2_dict.ContainsKey(y))
{
DateTime x_datetime = DateTime.Parse(FavoritesTabModel2.favorites2_dict[x]);
DateTime y_datetime = DateTime.Parse(FavoritesTabModel2.favorites2_dict[y]);
return sign * x_datetime.CompareTo(y_datetime);
}
else
{
return sign * x.CompareTo(y);
}
};
}
else
{
comparison = (x, y) => sign * x.CompareTo(y);
}
}
これで実行すると新しくふぁぼったツイートがうまくソートされません。OpenTweenを再起動しないとちゃんと反映されないようです。
これはおそらくソート基準がFavorites2タブだけ違うためでしょう。本来どのタブも同じソート基準で並んでいるはずなので、新しくふぁぼを追加してもRecentタブと同じ基準でFavorites2タブに振り分けているだけで、いちいちそのタブでの基準でソートし直してはいないように思えます。ApplySortModeの参照元をたどっていってもそういう感じに見えます。
なのでなんとかしていいタイミングでApplySortModeを呼び出させましょう。ふぁぼる度に毎回呼び出してもいいですが、例えばFavorites2タブを選択したタイミングで呼び出せれば無駄は少ないです。適当に「select」等で検索していくと「tabselect」で検索したときにTween.csでListTabSelectという関数を発見しました。ここにブレークポイントを立てて実行すると、タブを切り替えるたびに止まります。ここでApplySortModeを呼び出したいところですが、ApplySortModeはprivateな関数なので、代わりにこのApplySortModeを呼び出しているSetSortModeを呼び出すことにします。以下の内容をListTabSelectに追加します。
if (this._curTab.Text == "Favorites2")
curTabModel.SetSortMode(curTabModel.SortMode, curTabModel.SortOrder);
これでようやくふぁぼ順ふぁぼが完成しました。下の画像でもツイートの順番が日時(ツイート自体の日時)の順番でないことが確認できます。
OpenTweenを手探ってみた#2に続きます。