初めに

 こんばんは。今回から実際にOpenTweenの機能を拡張した際の話について、体験談的にまとめていこうと思います。なお、前回も書いた通り、このOpenTween編集はとあるセミナーのようなもので行った演習によるものなのですが、セミナーの内容に開発側へのプルリクエストは含まれていなかったため、ここで開発した機能が搭載されたOpenTweenは残念ながら一般にダウンロードすることはできません。予めご了承ください。

問題意識とソリューション

 皆さんはパクリツイートというものをご存知でしょうか。名前の通り、ツイートをパクる行為です。Twitterには140文字のツイートを短文芸術として作成しいいね(お気に入り登録)数やRT数を競い合うという文化が存在し、筆者もその文化圏に所属していました(というか今でもやっています)。しかし、その文化圏全体に対する脅威として現れたのがパクリツイート問題でした。早い話が、他人の作品を自分のものとして投稿してしまう人が増えてきたということですね。これは立派な剽窃であり日本国内であれば著作権法により罰せられる対象となる行為なのですが、残念ながら2016年現在になってもパクリツイートは蔓延しているのが現状です。
 この現状を知っている読者の一部に、パクリツイートではなくオリジナルツイートを探し出していいねやRTをしようというポリシーを持っている人がいます。しかし、Twitterでこれを検索するのは少なからず手間がかかります。そこで、Twitterクライアントとしてこの活動を支援するために、簡単な操作で原文検索ができないかと考えたのがパクツイ検索機能の開発動機です。
 ちなみになのですが、セミナーに去年参加したあるプログラマはOpenTweenではない別のTwitterクライアントにパクツイ支援機能を搭載したそうです。何やってるんだよ。

実装した仕様

右クリックでオリジナル検索ボタンをクリック

新しいタブを開く

ツイート内容で公式検索した結果を表示

分析と実装の手順

 実際にこの機能を実装した際の実装手順を示します。OpenTweenに機能を追加しようと考えている方は参考になどしてください。そんな人そうそういないと思いますけど。

1.全体概観の把握

 前回の記事でも書きましたが、OpenTweenのソースコードをダウンロードした時点ではC#は全くわからない状況でした。その後基本文法だけはさらっと学んだ(というかほとんどがC/C++と同じであることを確認した)ので、とりあえずMain関数(Mainメソッド?)から実行が始まることは理解しました。ということでまずはMainに向かいます。Ctrl+Shift+Fで「Main」を全文検索です。
 と、MainがApplicationEvents.csに見つかりました。ブレークポイントを設定して実行すると生まれそこなった雛みたいなウィンドウが表示されて確かに止まるのでこれがMainで間違いないようです。で、Mainをよく見ると

                (前略)
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new TweenMain());

                mt.ReleaseMutex();

                return 0;
とありました。つまり、TweenMainが本当のメイン処理となる部分ということですね。ということでTweenMainに進んでブレークポイントを設定して……ということを繰り返しやっていくと、前回の記事で書いた「ApplicationEvents.cs」「Tween.cs」あたりの解析結果を得ることができます。

2.ツイート処理の分析

 今度はより詳細にどの部分でどの処理が行われているかの分析。この時点では検索の仕様が確定していなかったため、とりあえず概観を得るために処理が各種処理が行われているであろう場所を特定しました。
 具体的な方法としては前項と同じく全文検索であたりをつけた後ブレークポイントを設定して実際にその処理を行ったときに止まるのかを確認する、という手法を取ります。これで「ツイートしたとき」「タブを開いたとき」などなんとなくの実行タイミングが分かるので、そこから先はソースコードを実際に読み込みます。と言ってもOpenTweenはコメントが充実してるため、コメントに基づいてある程度の処理内容を予想して読めばそこまで苦労せずに内容が理解できるはずです。
 ここまでの解析で、前回記事の解析内容のうち「PostFilterRule.cs」の詳細を除く部分に関してはざっくりとつかめました。ここから先の具体的処理については実装時にコピペ改変が必要となった際にじっくり読むということになります。

3. デザインの作成

 実装する処理は「右クリックメニューに新たな項目を追加する」と「項目がクリックされたときに検索処理を行う」の二種類に分けられます。まずはうち前者について実装を試みました。
 Visual Studioの仕様が分かっていればまず空のメニューボタンをTween.csのデザイン画面に作ることを思いつくわけですが、残念ながらC#初学者のためその辺りが思い至らず、とりあえず「右クリック」で全文検索。残念ながらこれで見つかったものには所望の箇所がなかったので「Retweet」「Quote」など右クリックしたときに実際に表示される文章で全文検索。するとTween.resxが引っかかりました。ここはただのデザイン部分ですが、

  <data name="ReTweetStripMenuItem.Text" xml:space="preserve">
    <value>Re&amp;tweet</value>
  </data>
  <data name="ReTweetUnofficialStripMenuItem.Size" type="System.Drawing.Size, System.Drawing">
    <value>224, 22</value>
  </data>
  <data name="ReTweetUnofficialStripMenuItem.Text" xml:space="preserve">
    <value>Retweet(U&amp;nofficial)</value>
  </data>
  <data name="QuoteStripMenuItem.Size" type="System.Drawing.Size, System.Drawing">
    <value>224, 22</value>
  </data>
  <data name="QuoteStripMenuItem.Text" xml:space="preserve">
    <value>&amp;Quote</value>
  </data>

こんな感じでnameの部分にメソッド名と思しきものが入っているのが確認できました。このメソッド名で検索するとTween.csが引っかかり、デザインだけでなく具体的な処理もTween.csに書かれていることが分かります。
 そして、この辺りでやっとデザイナーツールについての知見を得たので、Tween.csをGUIで編集して「オリジナル検索」を追加したところ、Tween.csに

        private void OriginalSearchMenuItem_Click(object sender, EventArgs e)
        {
        }
が、Tween.resxに

<data name="OriginalSearchMenuItem.Size" type="System.Drawing.Size, System.Drawing">
    <value>224, 22</value>
  </data>
  <data name="OriginalSearchMenuItem.Text" xml:space="preserve">
    <value>オリジナル検索</value>
  </data>

が追加されました。

4. 検索処理の作成

 最後に実際の検索処理を実装します。まずはGUIで作成したOriginalSearchMenuItem_Clickの中身を書き込みますが、他の右クリック処理を見る限りここに実際の処理を書くのではなく実際の処理を書いたメソッドをここで呼び出すという形式のようです。ということでdoSearchOriginaメソッドを用意しました。
 まずは非公式RT処理を元に処理内容を解析します。

        private void doReTweetUnofficial()
        {
            //RT @id:内容
            if (this.ExistCurrentPost)
            {
                if (_curPost.IsDm || !StatusText.Enabled)
                    return;

                if (_curPost.IsProtect)
                {
                    MessageBox.Show("Protected.");
                    return;
                }
                string rtdata = _curPost.Text;
                rtdata = CreateRetweetUnofficial(rtdata, this.StatusText.Multiline);

                StatusText.Text = " RT @" + _curPost.ScreenName + ": " + rtdata;

                // 投稿時に in_reply_to_status_id を付加する
                var inReplyToStatusId = this._curPost.RetweetedId ?? this._curPost.StatusId;
                var inReplyToScreenName = this._curPost.ScreenName;
                this.inReplyTo = Tuple.Create(inReplyToStatusId, inReplyToScreenName);

                StatusText.SelectionStart = 0;
                StatusText.Focus();
            }
        }

 最初のif文はおそらくツイートが実際に選択されているかの分岐でしょう。次のif文はカーソルを乗せてコメントを読むと何らかの必要なフラグなようです。その次のif文は非公開ツイートかどうかのフラグでしょう。そして重要なのが次の_curPost.Textです。これを格納したrtdataの前に「RT: @ユーザー名」を付けて別のstring型に入れる処理を行っています。ここから推測されるに、_curPost.Textが選択したツイートの内容を示す文字列であると考えられます。これでツイート内容の抽出についてはわかりました。
 次に、検索部分に関して解析します。同一ファイル内でSearch等の単語で検索したところ、以下の検索メソッドが見つかりました。

        public void AddNewTabForSearch(string searchWord)
        {
            //同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了
            foreach (var tb in _statuses.GetTabsByType<publicsearchtabmodel>())
            {
                if (tb.SearchWords == searchWord && string.IsNullOrEmpty(tb.SearchLang))
                {
                    foreach (TabPage tp in ListTab.TabPages)
                    {
                        if (tb.TabName == tp.Text)
                        {
                            ListTab.SelectedTab = tp;
                            return;
                        }
                    }
                }
            }
            //ユニークなタブ名生成
            string tabName = searchWord;
            for (int i = 0; i <= 100; i++)
            {
                if (_statuses.ContainsTab(tabName))
                    tabName += "_";
                else
                    break;
            }
            //タブ追加
            var tab = new PublicSearchTabModel(tabName);
            _statuses.AddTab(tab);
            AddNewTab(tab, startup: false);
            //追加したタブをアクティブに
            ListTab.SelectedIndex = ListTab.TabPages.Count - 1;
            //検索条件の設定
            ComboBox cmb = (ComboBox)ListTab.SelectedTab.Controls["panelSearch"].Controls["comboSearch"];
            cmb.Items.Add(searchWord);
            cmb.Text = searchWord;
            SaveConfigsTabs();
            //検索実行
            this.SearchButton_Click(ListTab.SelectedTab.Controls["panelSearch"].Controls["comboSearch"], null);
        }

 かなり親切なコメントがついているので、コメント通りに理解すれば良いでしょう。オリジナルツイート検索ではこのメソッド自体を呼び出す、またはこのメソッドの内容を抽出して自作のメソッドに搭載する、の二つの実装方法が考えらますが、前者を採用すると検索時のタブ名が検索文字列となってしまい非常に名前の長いタブが生成されてしまうことが確認されたため、後者の方法を利用することとしました。
 以上の知見に基づいて完成したオリジナルツイート検索メソッドが以下の通りとなります。

        private void doSearchOriginal()
        {
            if (this.ExistCurrentPost)
            {
                //ツイートが選択されていなければ実行されない
                if (_curPost.IsDm || !StatusText.Enabled) return;

                //ツイート文字列を受け取る
                string otdata = _curPost.Text;                                      // 生データを受け取る
                otdata = CreateRetweetUnofficial(otdata, this.StatusText.Multiline)
                    + " exclude:retweets";                                          // 整形(本来は非公式RT用だが流用可能)



                /*** タブを開いてツイート内容で検索 ***/
                //タブ追加
                foreach (var tb in _statuses.GetTabsByType<publicsearchtabmodel>())
                {
                    if (tb.SearchWords == otdata && string.IsNullOrEmpty(tb.SearchLang))
                    {
                        foreach (TabPage tp in ListTab.TabPages)
                        {
                            if (tb.TabName == tp.Text)
                            {
                                ListTab.SelectedTab = tp;
                                return;
                            }
                        }
                    }
                }
                string tabName = "パクツイ検索" + DateTime.Now;
                 var tab = new PublicSearchTabModel(tabName);
                _statuses.AddTab(tab);
                AddNewTab(tab, startup: false);
                //追加したタブをアクティブに
                ListTab.SelectedIndex = ListTab.TabPages.Count - 1;
                //検索条件の設定
                ComboBox cmb = (ComboBox)ListTab.SelectedTab.Controls["panelSearch"].Controls["comboSearch"];
                cmb.Items.Add(otdata);
                cmb.Text = otdata;
                SaveConfigsTabs();
                //検索実行
                this.SearchButton_Click(ListTab.SelectedTab.Controls["panelSearch"].Controls["comboSearch"], null);
                StatusText.SelectionStart = 0;
                StatusText.Focus();
            }
        }

 前半部がdoReTweetUnofficialのコピペ改変、後半部がAddNewTabForSearchのコピペ改変で作られていることがよくわかると思います。
 文字列の整形にCreateRetweetUnofficialというメソッドを使っていますが、これは改行文字などが_curPost.Textにはhtmlタグの形で入っているのでそれを整形するためのメソッドです。非公式リツイート用に開発されたものと思われますが、本メソッドでも流用可能なので利用しています。また、末尾に" exclude:retweets"という文字列をを付与していますが、これを付与しないで検索するとRTされたツイートがリツイートソースと共に検索結果に表示されてしまいます。パクツイのオリジナルツイート検索という本機能の趣旨を鑑みると、RTが検索結果に表示されるのは都合が悪いため、末尾に自動で文字列を付与して検索結果から排除しています。

実装結果

 上記の通りに実装したところ、右クリックメニューに「オリジナル検索」が表示されるようになり、それをクリックすると新しく検索用タブが開きツイート内容で検索されるようになりました。そのため、当初の使用案の実装は正しく行われたと言えるでしょう。
 しかし、ツイート内容で検索してもそのツイートを含め一つも検索結果が引っかからないということがたびたび観測されました。調査のため、Twitterのウェブサイトで同じ内容で検索してみたところ、こちらでも同様に検索結果がみつからないとの表示が。つまり、これはTwitterの検索機能そのものの仕様ということでしょう。今回の実装方法を採用している以上、この問題は解決不能です。
 先行研究ではGoogle検索を利用して原文を発見しており、その技術がbotとしての実用化に至っているため、原理的に実装不能ということではないようです。Google APIなどTwitterクライアントとはまた別の技術が必要になると思われますが、可能であれば挑戦してみたいところではありますね。

終わりに

 今回のパクツイ検索については残念な結果に終わってしまいました。ただ、上手く検索が成功した際の利便性はやはり高いと感じられました。なので方向性自体は悪くないと思われるため、時間があるときに改良アルゴリズムが実装できたらと思っている次第であります。
 次回はまた別の機能を実装します。そちらは個人的にもなかなかよくできたのではないかと感じているので、よろしかったら是非とも読んでいただけるとありがたいです。

参考文献

金城俊哉(2016)『Visual C# 2015 パーフェクトマスター』 秀和システム.