// OpenTween - Client of Twitter
// Copyright (c) 2013 kim_upsilon (@kim_upsilon)
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see , or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
namespace OpenTween
{
///
/// タブで使用する振り分けルールを表すクラス
///
[XmlType("FiltersClass")]
public class PostFilterRule
{
///
/// Compile() メソッドの呼び出しが必要な状態か否か
///
[XmlIgnore]
public bool IsDirty { get; protected set; }
///
/// コンパイルされた振り分けルール
///
[XmlIgnore]
protected Func FilterDelegate;
///
/// 振り分けルールの概要
///
[XmlIgnore]
public string SummaryText
{
get { return this.MakeSummary(); }
}
///
/// ExecFilter() メソッドの実行時に自動でコンパイルを実行する
///
///
/// テスト用途以外では AutoCompile に頼らず事前に Compile() メソッドを呼び出すこと
///
internal static bool AutoCompile { get; set; }
[XmlElement("NameFilter")]
public string FilterName
{
get { return this._FilterName; }
set
{
this.IsDirty = true;
this._FilterName = value;
}
}
private string _FilterName;
[XmlElement("ExNameFilter")]
public string ExFilterName
{
get { return this._ExFilterName; }
set
{
this.IsDirty = true;
this._ExFilterName = value;
}
}
private string _ExFilterName;
[XmlArray("BodyFilterArray")]
public string[] FilterBody
{
get { return this._FilterBody; }
set
{
if (value == null)
throw new ArgumentNullException("value");
this.IsDirty = true;
this._FilterBody = value;
}
}
private string[] _FilterBody = new string[0];
[XmlArray("ExBodyFilterArray")]
public string[] ExFilterBody
{
get { return this._ExFilterBody; }
set
{
if (value == null)
throw new ArgumentNullException("value");
this.IsDirty = true;
this._ExFilterBody = value;
}
}
private string[] _ExFilterBody = new string[0];
[XmlElement("SearchBoth")]
public bool UseNameField
{
get { return this._UseNameField; }
set
{
this.IsDirty = true;
this._UseNameField = value;
}
}
private bool _UseNameField;
[XmlElement("ExSearchBoth")]
public bool ExUseNameField
{
get { return this._ExUseNameField; }
set
{
this.IsDirty = true;
this._ExUseNameField = value;
}
}
private bool _ExUseNameField;
[XmlElement("MoveFrom")]
public bool MoveMatches
{
get { return this._MoveMatches; }
set
{
this.IsDirty = true;
this._MoveMatches = value;
}
}
private bool _MoveMatches;
[XmlElement("SetMark")]
public bool MarkMatches
{
get { return this._MarkMatches; }
set
{
this.IsDirty = true;
this._MarkMatches = value;
}
}
private bool _MarkMatches;
[XmlElement("SearchUrl")]
public bool FilterByUrl
{
get { return this._FilterByUrl; }
set
{
this.IsDirty = true;
this._FilterByUrl = value;
}
}
private bool _FilterByUrl;
[XmlElement("ExSearchUrl")]
public bool ExFilterByUrl
{
get { return this._ExFilterByUrl; }
set
{
this.IsDirty = true;
this._ExFilterByUrl = value;
}
}
private bool _ExFilterByUrl;
public bool CaseSensitive
{
get { return this._CaseSensitive; }
set
{
this.IsDirty = true;
this._CaseSensitive = value;
}
}
private bool _CaseSensitive;
public bool ExCaseSensitive
{
get { return this._ExCaseSensitive; }
set
{
this.IsDirty = true;
this._ExCaseSensitive = value;
}
}
private bool _ExCaseSensitive;
public bool UseLambda
{
get { return this._UseLambda; }
set
{
this.IsDirty = true;
this._UseLambda = value;
}
}
private bool _UseLambda;
public bool ExUseLambda
{
get { return this._ExUseLambda; }
set
{
this.IsDirty = true;
this._ExUseLambda = value;
}
}
private bool _ExUseLambda;
public bool UseRegex
{
get { return this._UseRegex; }
set
{
this.IsDirty = true;
this._UseRegex = value;
}
}
private bool _UseRegex;
public bool ExUseRegex
{
get { return this._ExUseRegex; }
set
{
this.IsDirty = true;
this._ExUseRegex = value;
}
}
private bool _ExUseRegex;
[XmlElement("IsRt")]
public bool FilterRt
{
get { return this._FilterRt; }
set
{
this.IsDirty = true;
this._FilterRt = value;
}
}
private bool _FilterRt;
[XmlElement("IsExRt")]
public bool ExFilterRt
{
get { return this._ExFilterRt; }
set
{
this.IsDirty = true;
this._ExFilterRt = value;
}
}
private bool _ExFilterRt;
[XmlElement("Source")]
public string FilterSource
{
get { return this._FilterSource; }
set
{
this.IsDirty = true;
this._FilterSource = value;
}
}
private string _FilterSource;
[XmlElement("ExSource")]
public string ExFilterSource
{
get { return this._ExFilterSource; }
set
{
this.IsDirty = true;
this._ExFilterSource = value;
}
}
private string _ExFilterSource;
public PostFilterRule()
{
this.IsDirty = true;
this.MarkMatches = true;
this.UseNameField = true;
this.ExUseNameField = true;
}
static PostFilterRule()
{
// TODO: TabsClass とかの改修が終わるまでデフォルト有効
PostFilterRule.AutoCompile = true;
}
///
/// 振り分けルールをコンパイルします
///
public void Compile()
{
var postParam = Expression.Parameter(typeof(PostClass), "x");
var matchExpr = this.MakeFiltersExpr(
postParam,
this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt,
this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl);
var excludeExpr = this.MakeFiltersExpr(
postParam,
this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt,
this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl);
Expression> filterExpr;
if (matchExpr != null)
{
MyCommon.HITRESULT hitResult;
if (this.MoveMatches)
hitResult = MyCommon.HITRESULT.Move;
else if (this.MarkMatches)
hitResult = MyCommon.HITRESULT.CopyAndMark;
else
hitResult = MyCommon.HITRESULT.Copy;
if (excludeExpr != null)
{
// x => matchExpr ? (!excludeExpr ? hitResult : None) : None
filterExpr =
Expression.Lambda>(
Expression.Condition(
matchExpr,
Expression.Condition(
Expression.Not(excludeExpr),
Expression.Constant(hitResult),
Expression.Constant(MyCommon.HITRESULT.None)),
Expression.Constant(MyCommon.HITRESULT.None)),
postParam);
}
else
{
// x => matchExpr ? hitResult : None
filterExpr =
Expression.Lambda>(
Expression.Condition(
matchExpr,
Expression.Constant(hitResult),
Expression.Constant(MyCommon.HITRESULT.None)),
postParam);
}
}
else if (excludeExpr != null)
{
// x => excludeExpr ? Exclude : None
filterExpr =
Expression.Lambda>(
Expression.Condition(
excludeExpr,
Expression.Constant(MyCommon.HITRESULT.Exclude),
Expression.Constant(MyCommon.HITRESULT.None)),
postParam);
}
else // matchExpr == null && excludeExpr == null
{
filterExpr = x => MyCommon.HITRESULT.None;
}
this.FilterDelegate = filterExpr.Compile();
this.IsDirty = false;
}
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 &&
bodyExpr = Expression.OrElse(
bodyExpr,
Expression.AndAlso(
Expression.NotEqual(
Expression.Property(
postParam,
typeof(PostClass).GetProperty("RetweetedBy")),
Expression.Constant(null)),
this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true)));
}
}
filterExprs.Add(bodyExpr);
}
if (!string.IsNullOrEmpty(filterSource))
{
if (filterByUrl)
filterExprs.Add(this.MakeGenericFilter(postParam, "SourceHtml", filterSource, useRegex, caseSensitive));
else
filterExprs.Add(this.MakeGenericFilter(postParam, "Source", filterSource, useRegex, caseSensitive, exactMatch: true));
}
if (filterRt)
{
// x.RetweetedId != null
filterExprs.Add(Expression.NotEqual(
Expression.Property(
postParam,
typeof(PostClass).GetProperty("RetweetedId")),
Expression.Constant(null)));
}
if (filterExprs.Count == 0)
{
return null;
}
else
{
// filterExpr[0] && filterExpr[1] && ...
var filterExpr = filterExprs[0];
foreach (var expr in filterExprs.Skip(1))
{
filterExpr = Expression.AndAlso(filterExpr, expr);
}
return filterExpr;
}
}
protected Expression MakeGenericFilter(
ParameterExpression postParam, string targetFieldName, string pattern,
bool useRegex, bool caseSensitive, bool exactMatch = false)
{
// x.
var targetField = Expression.Property(
postParam,
typeof(PostClass).GetProperty(targetFieldName));
if (useRegex)
{
var regex = new Regex(pattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
// regex.IsMatch(targetField)
return Expression.Call(
Expression.Constant(regex),
typeof(Regex).GetMethod("IsMatch", new[] { typeof(string) }),
targetField);
}
else
{
var compOpt = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
if (exactMatch)
{
// 完全一致
// pattern.Equals(targetField, compOpt)
return Expression.Call(
Expression.Constant(pattern),
typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }),
targetField,
Expression.Constant(compOpt));
}
else
{
// 部分一致
// targetField.IndexOf(pattern, compOpt) != -1
return Expression.NotEqual(
Expression.Call(
targetField,
typeof(string).GetMethod("IndexOf", new[] { typeof(string), typeof(StringComparison) }),
Expression.Constant(pattern),
Expression.Constant(compOpt)),
Expression.Constant(-1));
}
}
}
///
/// ツイートの振り分けを実行する
///
/// 振り分けるツイート
/// 振り分け結果
public MyCommon.HITRESULT ExecFilter(PostClass post)
{
if (this.IsDirty)
{
if (!PostFilterRule.AutoCompile)
throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");
this.Compile();
}
return this.FilterDelegate(post);
}
public PostFilterRule Clone()
{
return new PostFilterRule
{
FilterBody = this.FilterBody,
FilterName = this.FilterName,
FilterSource = this.FilterSource,
FilterRt = this.FilterRt,
FilterByUrl = this.FilterByUrl,
UseLambda = this.UseLambda,
UseNameField = this.UseNameField,
UseRegex = this.UseRegex,
CaseSensitive = this.CaseSensitive,
ExFilterBody = this.ExFilterBody,
ExFilterName = this.ExFilterName,
ExFilterSource = this.ExFilterSource,
ExFilterRt = this.ExFilterRt,
ExFilterByUrl = this.ExFilterByUrl,
ExUseLambda = this.ExUseLambda,
ExUseNameField = this.ExUseNameField,
ExUseRegex = this.ExUseRegex,
ExCaseSensitive = this.ExCaseSensitive,
MoveMatches = this.MoveMatches,
MarkMatches = this.MarkMatches,
};
}
///
/// 振り分けルールの文字列表現を返します
///
/// 振り分けルールの概要
public override string ToString()
{
return this.SummaryText;
}
#region from Tween v1.1.0.0
// The code in this region block is based on code written by the following authors:
// (C) 2007 kiri_feather (@kiri_feather)
// (C) 2008 Moz (@syo68k)
///
/// フィルタ一覧に表示する文言生成
///
protected virtual string MakeSummary()
{
var fs = new StringBuilder();
if (!string.IsNullOrEmpty(this.FilterName) || this.FilterBody.Length > 0 || this.FilterRt || !string.IsNullOrEmpty(this.FilterSource))
{
if (this.UseNameField)
{
if (!string.IsNullOrEmpty(this.FilterName))
{
fs.AppendFormat(Properties.Resources.SetFiltersText1, this.FilterName);
}
else
{
fs.Append(Properties.Resources.SetFiltersText2);
}
}
if (this.FilterBody.Length > 0)
{
fs.Append(Properties.Resources.SetFiltersText3);
foreach (var bf in this.FilterBody)
{
fs.Append(bf);
fs.Append(" ");
}
fs.Length--;
fs.Append(Properties.Resources.SetFiltersText4);
}
fs.Append("(");
if (this.UseNameField)
{
fs.Append(Properties.Resources.SetFiltersText5);
}
else
{
fs.Append(Properties.Resources.SetFiltersText6);
}
if (this.UseRegex)
{
fs.Append(Properties.Resources.SetFiltersText7);
}
if (this.FilterByUrl)
{
fs.Append(Properties.Resources.SetFiltersText8);
}
if (this.CaseSensitive)
{
fs.Append(Properties.Resources.SetFiltersText13);
}
if (this.FilterRt)
{
fs.Append("RT/");
}
if (this.UseLambda)
{
fs.Append("LambdaExp/");
}
if (!string.IsNullOrEmpty(this.FilterSource))
{
fs.AppendFormat("Src…{0}/", this.FilterSource);
}
fs.Length--;
fs.Append(")");
}
if (!string.IsNullOrEmpty(this.ExFilterName) || this.ExFilterBody.Length > 0 || this.ExFilterRt || !string.IsNullOrEmpty(this.ExFilterSource))
{
//除外
fs.Append(Properties.Resources.SetFiltersText12);
if (this.ExUseNameField)
{
if (!string.IsNullOrEmpty(this.ExFilterName))
{
fs.AppendFormat(Properties.Resources.SetFiltersText1, this.ExFilterName);
}
else
{
fs.Append(Properties.Resources.SetFiltersText2);
}
}
if (this.ExFilterBody.Length > 0)
{
fs.Append(Properties.Resources.SetFiltersText3);
foreach (var bf in this.ExFilterBody)
{
fs.Append(bf);
fs.Append(" ");
}
fs.Length--;
fs.Append(Properties.Resources.SetFiltersText4);
}
fs.Append("(");
if (this.ExUseNameField)
{
fs.Append(Properties.Resources.SetFiltersText5);
}
else
{
fs.Append(Properties.Resources.SetFiltersText6);
}
if (this.ExUseRegex)
{
fs.Append(Properties.Resources.SetFiltersText7);
}
if (this.ExFilterByUrl)
{
fs.Append(Properties.Resources.SetFiltersText8);
}
if (this.ExCaseSensitive)
{
fs.Append(Properties.Resources.SetFiltersText13);
}
if (this.ExFilterRt)
{
fs.Append("RT/");
}
if (this.ExUseLambda)
{
fs.Append("LambdaExp/");
}
if (!string.IsNullOrEmpty(this.ExFilterSource))
{
fs.AppendFormat("Src…{0}/", this.ExFilterSource);
}
fs.Length--;
fs.Append(")");
}
fs.Append("(");
if (this.MoveMatches)
{
fs.Append(Properties.Resources.SetFiltersText9);
}
else
{
fs.Append(Properties.Resources.SetFiltersText11);
}
if (!this.MoveMatches && this.MarkMatches)
{
fs.Append(Properties.Resources.SetFiltersText10);
}
else if (!this.MoveMatches)
{
fs.Length--;
}
fs.Append(")");
return fs.ToString();
}
#endregion
}
}