初めに

 こんばんは。今回はオリジナルツイート検索に引き続き新たな機能を追加した話です。なお、前回も書いた通り、このOpenTween編集はとあるセミナーのようなもので行った演習によるものなのですが、セミナーの内容に開発側へのプルリクエストは含まれていなかったため、ここで開発した機能が搭載されたOpenTweenは残念ながら一般にダウンロードすることはできません。予めご了承ください。

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

 Twitterのミュート機能は現在でこそ公式で搭載されていますが古くはTwitterクライアントから生まれた機能でした。ミュート機能はリムーブやブロックするほどのことではないがあまり発言を目にしたくないという対象のアカウントに対して使われます。しかし、この「あまり発言を目にしたくない」という感情についてはかなり時間依存性があるのではないでしょうか。
 例えば、実況などを単語単位ではなく人単位でミュートしたいが放送終了後解除するのが手間な場合。例えば一時的に攻撃的な言動をとっているがリムーブするほどのことではないので翌日までミュートしたいという場合。
 このような場合において、現在から何分という時間指定でミュートができれば便利なのではないかと考えたのが時間制限付きミュート機能です。まあ自分で考えたのではなくセミナーで意見をいただいたのですが。

実装した仕様

 まず、前提知識として、Tweenには公式のミュート機能とは違う「ミュートタブ」という独自の振り分け機能がついています。これは「振り分けタブ」と呼ばれるタブの一種で、IDやツイート内容や投稿Twitterクライアントに応じて振り分けを行うことができます。ここに時間制限を新しく追加しようというのが今回の趣旨です。
 実際の仕様としては以下の通りとなります。
タブ振り分け編集「Source」の右に「Limit(min)」を用意 ↓ Limit(min)に相対振り分け時間を分単位で入力 ↓ 現在時刻と比較して相対振り分け時間まで振り分けルールを適用とし、現在時刻が相対振り分け時間から割り出した絶対振り分け時刻を超えていれば振り分け処理を停止
 仕様を見て分かる通り、ミュートのみではなく振り分け全体に応用可能な実装となっています。

分析と実装の手順

1.UIの作成

 まずは振り分けUIの編集を行います。UIでデザインが設定できるファイルを片っ端から見ていったところ、「FilterDIalog.cs」が振り分け画面に対応していました。ということでここを編集します。クライアント振り分けルールのテキストボックス「Source」があったので、それをコピーする形で「Limit(min)」のテキストボックスを作成し、そのままでは画面に収まり切らないので各種要素の場所を適宜移動するなどしました。
 なお、Tweenのタブ振り分けルールは「振り分けルール」と「除外ルール」の二段組になっているため、本機能の実装においてもこの二段組構成を踏襲しました。これにより、「○○分後~○○分後まで」のような振り分けルールの作成が可能となりました。
 その後、テキストボックスに対する初期化処理を記述します。こちらも「Source」を参考にして代入するだけです。ただし、実際の振り分けルールはまだ作っていたいのでそこに関しては後ほど追加という形になります。

2.振り分けルールの作成

 たびたび参考にしてきた「Source」の処理を見ると、ShowDetailというメソッドで

                TextSource.Text = fc.FilterSource;
 となっているのが分かります。この右側がフィルターの本体でしょう。これにマウスカーソルを合わせたところ、PostFilterRule.csのPostFilterRuleクラスだとわかりました。よってそちらに飛びます。すると

        (前略)
        [XmlElement("IsRt")]
        public bool FilterRt
        {
            get { return this._FilterRt; }
            set { this.SetProperty(ref this._FilterRt, value); }
        }
        private bool _FilterRt;

        [XmlElement("IsExRt")]
        public bool ExFilterRt
        {
            get { return this._ExFilterRt; }
            set { this.SetProperty(ref this._ExFilterRt, value); }
        }
        private bool _ExFilte
        [XmlElement("Source")]
        public string FilterSource
        {
            get { return this._FilterSource; }
            set { this.SetProperty(ref this._FilterSource, value); }
        }
        private string _FilterSource;

        [XmlElement("ExSource")]
        public string ExFilterSource
        {
            get { return this._ExFilterSource; }
            set { this.SetProperty(ref this._ExFilterSource, value); }
        }
        private string _ExFilterSource;
        (後略)

という風に振り分け用のデータ変数とアクセッサー(アクセサ?)が定義されているのが分かります。ということでここに

        [XmlElement("Limit")]
        public int FilterLimit
        {
            get
            {
                _FilterLimit = (_LimitTime - DateTime.Now).Minutes;
                return this._FilterLimit;
            }
            set
            {
                this.SetProperty(ref this._FilterLimit, value);
                _LimitTime = DateTime.Now.AddMinutes(_FilterLimit);
            }
        }
        private int _FilterLimit;
        private DateTime _LimitTime;

        [XmlElement("ExLimit")]
        public int ExFilterLimit
        {
            get
            {
                _ExFilterLimit = (_ExLimitTime - DateTime.Now).Minutes;
                return this._ExFilterLimit;
            }
            set
            {
                this.SetProperty(ref this._ExFilterLimit, value);
                _ExLimitTime = DateTime.Now.AddMinutes(_ExFilterLimit);
            }
        }
        private int _ExFilterLimit;
        private DateTime _ExLimitTime;

と追加します。ちなみにアクセッサーの時点で相対時間を絶対時間に変更して保存する処理になっています。
 さて、データの追加が完了したので実際の振り分けを行っている部分を確認して必要な処理を追加します。複雑に入り組んでいるため詳細は省きますが、以下のコードが振り分けルールを生成している部分と考えられます。

        protected virtual Expression MakeFiltersExpr(
            ParameterExpression postParam,
            string filterName, string[] filterBody, string filterSource, bool filterRt,
            bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl)
        {
            var filterExprs = new List();

            if (useNameField && !string.IsNullOrEmpty(filterName))
            {
                filterExprs.Add(Expression.OrElse(
                    this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true),
                    this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true)));
            }
            foreach (var body in filterBody)
            {
                if (string.IsNullOrEmpty(body))
                    continue;

                Expression bodyExpr;
                if (useLambda)
                {
                    // TODO DynamicQuery相当のGPLv3互換なライブラリで置換する
                    Expression> lambdaExpr = x => false;
                    bodyExpr = lambdaExpr.Body;
                }
                else
                {
                    if (filterByUrl)
                        bodyExpr = this.MakeGenericFilter(postParam, "Text", body, useRegex, caseSensitive);
                    else
                        bodyExpr = this.MakeGenericFilter(postParam, "TextFromApi", body, useRegex, caseSensitive);

                    // useNameField = false の場合は ScreenName と RetweetedBy も filterBody のマッチ対象となる
                    if (!useNameField)
                    {
                        bodyExpr = Expression.OrElse(
                            bodyExpr,
                            this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true));

                        // bodyExpr || x.RetweetedBy != null && 
 長い。読めない。どうやらExpression型という条件文をデータとして保存する型を使っているようなのですが、C#経験のなさも相まって全く読めません。しかしこの部分が実際に振り分けを行っているコードなのでここを変更しないと振り分けはできない。もはや万事休すかと思われたのですが、

/// <summary> /// ツイートの振り分けを実行する /// </summary> /// <param name="post">振り分けるツイート</param> /// <returns>振り分け結果</returns> public MyCommon.HITRESULT ExecFilter(PostClass post) { if (this.IsDirty) { if (!PostFilterRule.AutoCompile) throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります"); this.Compile(); } }
 どうやらこのメソッドが振り分け時に必ず呼ばれている模様。そして、PostFilterRuleクラスを解析したときに、Enableという変数を持っていたことが確認できていたので、

        public MyCommon.HITRESULT ExecFilter(PostClass post)
        {
            if (this.IsDirty)
            {
                if (!PostFilterRule.AutoCompile)
                    throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");

                this.Compile();
            }

            //時間消滅機能
            if (DateTime.Now < _ExLimitTime)return MyCommon.HITRESULT.None;
            if (DateTime.Now > _LimitTime)this.Enabled = false;

            return this.FilterDelegate(post);
        }

こうしました。Expression型の伝統を無視してif文で無理やり実装しましたが動かすだけならこれで動くはずです。

3.振り分けルールとUIの結びつけ

 流れとしては必要なのでわざわざ項目建てしましたが、UIを作って初期化したときと同じように「Source」を参考にコピペ改変すれば終了です。単にPostFilterRuleにデータを渡すようにすれば後は自動的にExecFilterメソッドが呼ばれて振り分けが行われます。デバッガによる観察的分析の結果どうやらツイートを読み込んだタイミングで必ずExecFilterメソッドが呼ばれるようになっているようです。

実装結果

 タブの振り分けフィルタを作成し、振り分けルールのLimitに終了時刻、除外ルールのLimitに開始時刻をそれぞれ現在からの相対分数で記述すると、その期間だけ振り分けが行われるようになります。終了時刻を超えると振り分けルールが自動で「無効」の表示となり、振り分けが停止します。この無効となった振り分けルールはユーザーが各自削除しなければならないという欠点があります。
 また、実装手順でも書いた通り、本来はExpression型による振り分けが標準的に行われているのに対し、今回の実装ではEnabled変数を直接変更しています。機能的には問題はありませんが、今後の拡張性などを考えるとExpression型による処理に変更したほうがより良いでしょう。

終わりに

 前回のパクツイ検索は実用上で難のある実装となってしまいしたが、今回は利便性の高い機能が追加でき、セミナーの実演発表でも概ね好評でした。セミナーで実装した部分はこれで終了ですが、今後も可能であればOpenTweenを触っていけたらと思っているところです。
 とりえあえず今回の時間制限タブはなかなか上手くできたのでGitHubにプルリクエストだけでもしてみようかと思っているのですが、どうでしょうかね……。実装が怪しいので採用されるかどうかは不明ですが進展があった場合はまた記事を書きます。

参考文献

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