1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
23 using System.Collections.Generic;
25 using System.Linq.Expressions;
27 using System.Text.RegularExpressions;
28 using System.Xml.Serialization;
33 /// タブで使用する振り分けルールを表すクラス
35 [XmlType("FiltersClass")]
36 public class PostFilterRule
39 /// Compile() メソッドの呼び出しが必要な状態か否か
42 public bool IsDirty { get; protected set; }
48 protected Func<PostClass, MyCommon.HITRESULT> FilterDelegate;
54 public string SummaryText
56 get { return this.MakeSummary(); }
60 /// ExecFilter() メソッドの実行時に自動でコンパイルを実行する
63 /// テスト用途以外では AutoCompile に頼らず事前に Compile() メソッドを呼び出すこと
65 internal static bool AutoCompile { get; set; }
67 [XmlElement("NameFilter")]
68 public string FilterName
70 get { return this._FilterName; }
74 this._FilterName = value;
77 private string _FilterName;
79 [XmlElement("ExNameFilter")]
80 public string ExFilterName
82 get { return this._ExFilterName; }
86 this._ExFilterName = value;
89 private string _ExFilterName;
91 [XmlArray("BodyFilterArray")]
92 public string[] FilterBody
94 get { return this._FilterBody; }
98 throw new ArgumentNullException("value");
101 this._FilterBody = value;
104 private string[] _FilterBody = new string[0];
106 [XmlArray("ExBodyFilterArray")]
107 public string[] ExFilterBody
109 get { return this._ExFilterBody; }
113 throw new ArgumentNullException("value");
116 this._ExFilterBody = value;
119 private string[] _ExFilterBody = new string[0];
121 [XmlElement("SearchBoth")]
122 public bool UseNameField
124 get { return this._UseNameField; }
128 this._UseNameField = value;
131 private bool _UseNameField;
133 [XmlElement("ExSearchBoth")]
134 public bool ExUseNameField
136 get { return this._ExUseNameField; }
140 this._ExUseNameField = value;
143 private bool _ExUseNameField;
145 [XmlElement("MoveFrom")]
146 public bool MoveMatches
148 get { return this._MoveMatches; }
152 this._MoveMatches = value;
155 private bool _MoveMatches;
157 [XmlElement("SetMark")]
158 public bool MarkMatches
160 get { return this._MarkMatches; }
164 this._MarkMatches = value;
167 private bool _MarkMatches;
169 [XmlElement("SearchUrl")]
170 public bool FilterByUrl
172 get { return this._FilterByUrl; }
176 this._FilterByUrl = value;
179 private bool _FilterByUrl;
181 [XmlElement("ExSearchUrl")]
182 public bool ExFilterByUrl
184 get { return this._ExFilterByUrl; }
188 this._ExFilterByUrl = value;
191 private bool _ExFilterByUrl;
193 public bool CaseSensitive
195 get { return this._CaseSensitive; }
199 this._CaseSensitive = value;
202 private bool _CaseSensitive;
204 public bool ExCaseSensitive
206 get { return this._ExCaseSensitive; }
210 this._ExCaseSensitive = value;
213 private bool _ExCaseSensitive;
215 public bool UseLambda
217 get { return this._UseLambda; }
221 this._UseLambda = value;
224 private bool _UseLambda;
226 public bool ExUseLambda
228 get { return this._ExUseLambda; }
232 this._ExUseLambda = value;
235 private bool _ExUseLambda;
239 get { return this._UseRegex; }
243 this._UseRegex = value;
246 private bool _UseRegex;
248 public bool ExUseRegex
250 get { return this._ExUseRegex; }
254 this._ExUseRegex = value;
257 private bool _ExUseRegex;
262 get { return this._FilterRt; }
266 this._FilterRt = value;
269 private bool _FilterRt;
271 [XmlElement("IsExRt")]
272 public bool ExFilterRt
274 get { return this._ExFilterRt; }
278 this._ExFilterRt = value;
281 private bool _ExFilterRt;
283 [XmlElement("Source")]
284 public string FilterSource
286 get { return this._FilterSource; }
290 this._FilterSource = value;
293 private string _FilterSource;
295 [XmlElement("ExSource")]
296 public string ExFilterSource
298 get { return this._ExFilterSource; }
302 this._ExFilterSource = value;
305 private string _ExFilterSource;
307 public PostFilterRule()
311 this.MarkMatches = true;
312 this.UseNameField = true;
313 this.ExUseNameField = true;
316 static PostFilterRule()
318 // TODO: TabsClass とかの改修が終わるまでデフォルト有効
319 PostFilterRule.AutoCompile = true;
325 public void Compile()
327 var postParam = Expression.Parameter(typeof(PostClass), "x");
329 var matchExpr = this.MakeFiltersExpr(
331 this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt,
332 this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl);
334 var excludeExpr = this.MakeFiltersExpr(
336 this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt,
337 this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl);
339 Expression<Func<PostClass, MyCommon.HITRESULT>> filterExpr;
341 if (matchExpr != null)
343 MyCommon.HITRESULT hitResult;
345 if (this.MoveMatches)
346 hitResult = MyCommon.HITRESULT.Move;
347 else if (this.MarkMatches)
348 hitResult = MyCommon.HITRESULT.CopyAndMark;
350 hitResult = MyCommon.HITRESULT.Copy;
352 if (excludeExpr != null)
354 // x => matchExpr ? (!excludeExpr ? hitResult : None) : None
356 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
357 Expression.Condition(
359 Expression.Condition(
360 Expression.Not(excludeExpr),
361 Expression.Constant(hitResult),
362 Expression.Constant(MyCommon.HITRESULT.None)),
363 Expression.Constant(MyCommon.HITRESULT.None)),
368 // x => matchExpr ? hitResult : None
370 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
371 Expression.Condition(
373 Expression.Constant(hitResult),
374 Expression.Constant(MyCommon.HITRESULT.None)),
378 else if (excludeExpr != null)
380 // x => excludeExpr ? Exclude : None
382 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
383 Expression.Condition(
385 Expression.Constant(MyCommon.HITRESULT.Exclude),
386 Expression.Constant(MyCommon.HITRESULT.None)),
389 else // matchExpr == null && excludeExpr == null
391 filterExpr = x => MyCommon.HITRESULT.None;
394 this.FilterDelegate = filterExpr.Compile();
395 this.IsDirty = false;
398 protected virtual Expression MakeFiltersExpr(
399 ParameterExpression postParam,
400 string filterName, string[] filterBody, string filterSource, bool filterRt,
401 bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl)
403 var filterExprs = new List<Expression>();
405 if (useNameField && !string.IsNullOrEmpty(filterName))
407 filterExprs.Add(Expression.OrElse(
408 this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true),
409 this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true)));
411 foreach (var body in filterBody)
413 if (string.IsNullOrEmpty(body))
419 // TODO DynamicQuery相当のGPLv3互換なライブラリで置換する
420 Expression<Func<PostClass, bool>> lambdaExpr = x => false;
421 bodyExpr = lambdaExpr.Body;
426 bodyExpr = this.MakeGenericFilter(postParam, "Text", body, useRegex, caseSensitive);
428 bodyExpr = this.MakeGenericFilter(postParam, "TextFromApi", body, useRegex, caseSensitive);
430 // useNameField = false の場合は ScreenName と RetweetedBy も filterBody のマッチ対象となる
433 bodyExpr = Expression.OrElse(
435 this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true));
437 // bodyExpr || x.RetweetedBy != null && <MakeGenericFilter()>
438 bodyExpr = Expression.OrElse(
444 typeof(PostClass).GetProperty("RetweetedBy")),
445 Expression.Constant(null)),
446 this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true)));
450 filterExprs.Add(bodyExpr);
452 if (!string.IsNullOrEmpty(filterSource))
455 filterExprs.Add(this.MakeGenericFilter(postParam, "SourceHtml", filterSource, useRegex, caseSensitive));
457 filterExprs.Add(this.MakeGenericFilter(postParam, "Source", filterSource, useRegex, caseSensitive, exactMatch: true));
461 // x.RetweetedId != null
462 filterExprs.Add(Expression.NotEqual(
465 typeof(PostClass).GetProperty("RetweetedId")),
466 Expression.Constant(null)));
469 if (filterExprs.Count == 0)
475 // filterExpr[0] && filterExpr[1] && ...
476 var filterExpr = filterExprs[0];
477 foreach (var expr in filterExprs.Skip(1))
479 filterExpr = Expression.AndAlso(filterExpr, expr);
486 protected Expression MakeGenericFilter(
487 ParameterExpression postParam, string targetFieldName, string pattern,
488 bool useRegex, bool caseSensitive, bool exactMatch = false)
490 // x.<targetFieldName>
491 var targetField = Expression.Property(
493 typeof(PostClass).GetProperty(targetFieldName));
497 var regex = new Regex(pattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
499 // regex.IsMatch(targetField)
500 return Expression.Call(
501 Expression.Constant(regex),
502 typeof(Regex).GetMethod("IsMatch", new[] { typeof(string) }),
507 var compOpt = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
512 // pattern.Equals(targetField, compOpt)
513 return Expression.Call(
514 Expression.Constant(pattern),
515 typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }),
517 Expression.Constant(compOpt));
523 // targetField.IndexOf(pattern, compOpt) != -1
524 return Expression.NotEqual(
527 typeof(string).GetMethod("IndexOf", new[] { typeof(string), typeof(StringComparison) }),
528 Expression.Constant(pattern),
529 Expression.Constant(compOpt)),
530 Expression.Constant(-1));
538 /// <param name="post">振り分けるツイート</param>
539 /// <returns>振り分け結果</returns>
540 public MyCommon.HITRESULT ExecFilter(PostClass post)
544 if (!PostFilterRule.AutoCompile)
545 throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");
550 return this.FilterDelegate(post);
553 public PostFilterRule Clone()
555 return new PostFilterRule
557 FilterBody = this.FilterBody,
558 FilterName = this.FilterName,
559 FilterSource = this.FilterSource,
560 FilterRt = this.FilterRt,
561 FilterByUrl = this.FilterByUrl,
562 UseLambda = this.UseLambda,
563 UseNameField = this.UseNameField,
564 UseRegex = this.UseRegex,
565 CaseSensitive = this.CaseSensitive,
566 ExFilterBody = this.ExFilterBody,
567 ExFilterName = this.ExFilterName,
568 ExFilterSource = this.ExFilterSource,
569 ExFilterRt = this.ExFilterRt,
570 ExFilterByUrl = this.ExFilterByUrl,
571 ExUseLambda = this.ExUseLambda,
572 ExUseNameField = this.ExUseNameField,
573 ExUseRegex = this.ExUseRegex,
574 ExCaseSensitive = this.ExCaseSensitive,
575 MoveMatches = this.MoveMatches,
576 MarkMatches = this.MarkMatches,
581 /// 振り分けルールの文字列表現を返します
583 /// <returns>振り分けルールの概要</returns>
584 public override string ToString()
586 return this.SummaryText;
589 #region from Tween v1.1.0.0
591 // The code in this region block is based on code written by the following authors:
592 // (C) 2007 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
593 // (C) 2008 Moz (@syo68k)
598 protected virtual string MakeSummary()
600 var fs = new StringBuilder();
601 if (!string.IsNullOrEmpty(this.FilterName) || this.FilterBody.Length > 0 || this.FilterRt || !string.IsNullOrEmpty(this.FilterSource))
603 if (this.UseNameField)
605 if (!string.IsNullOrEmpty(this.FilterName))
607 fs.AppendFormat(Properties.Resources.SetFiltersText1, this.FilterName);
611 fs.Append(Properties.Resources.SetFiltersText2);
614 if (this.FilterBody.Length > 0)
616 fs.Append(Properties.Resources.SetFiltersText3);
617 foreach (var bf in this.FilterBody)
623 fs.Append(Properties.Resources.SetFiltersText4);
626 if (this.UseNameField)
628 fs.Append(Properties.Resources.SetFiltersText5);
632 fs.Append(Properties.Resources.SetFiltersText6);
636 fs.Append(Properties.Resources.SetFiltersText7);
638 if (this.FilterByUrl)
640 fs.Append(Properties.Resources.SetFiltersText8);
642 if (this.CaseSensitive)
644 fs.Append(Properties.Resources.SetFiltersText13);
652 fs.Append("LambdaExp/");
654 if (!string.IsNullOrEmpty(this.FilterSource))
656 fs.AppendFormat("Src…{0}/", this.FilterSource);
661 if (!string.IsNullOrEmpty(this.ExFilterName) || this.ExFilterBody.Length > 0 || this.ExFilterRt || !string.IsNullOrEmpty(this.ExFilterSource))
664 fs.Append(Properties.Resources.SetFiltersText12);
665 if (this.ExUseNameField)
667 if (!string.IsNullOrEmpty(this.ExFilterName))
669 fs.AppendFormat(Properties.Resources.SetFiltersText1, this.ExFilterName);
673 fs.Append(Properties.Resources.SetFiltersText2);
676 if (this.ExFilterBody.Length > 0)
678 fs.Append(Properties.Resources.SetFiltersText3);
679 foreach (var bf in this.ExFilterBody)
685 fs.Append(Properties.Resources.SetFiltersText4);
688 if (this.ExUseNameField)
690 fs.Append(Properties.Resources.SetFiltersText5);
694 fs.Append(Properties.Resources.SetFiltersText6);
698 fs.Append(Properties.Resources.SetFiltersText7);
700 if (this.ExFilterByUrl)
702 fs.Append(Properties.Resources.SetFiltersText8);
704 if (this.ExCaseSensitive)
706 fs.Append(Properties.Resources.SetFiltersText13);
712 if (this.ExUseLambda)
714 fs.Append("LambdaExp/");
716 if (!string.IsNullOrEmpty(this.ExFilterSource))
718 fs.AppendFormat("Src…{0}/", this.ExFilterSource);
725 if (this.MoveMatches)
727 fs.Append(Properties.Resources.SetFiltersText9);
731 fs.Append(Properties.Resources.SetFiltersText11);
733 if (!this.MoveMatches && this.MarkMatches)
735 fs.Append(Properties.Resources.SetFiltersText10);
737 else if (!this.MoveMatches)
744 return fs.ToString();