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;
24 using System.ComponentModel;
26 using System.Linq.Expressions;
27 using System.Runtime.CompilerServices;
29 using System.Text.RegularExpressions;
30 using System.Xml.Serialization;
32 namespace OpenTween.Models
35 /// タブで使用する振り分けルールを表すクラス
37 [XmlType("FiltersClass")]
38 public class PostFilterRule : NotifyPropertyChangedBase, IEquatable<PostFilterRule>
41 /// Compile() メソッドの呼び出しが必要な状態か否か
44 public bool IsDirty { get; protected set; }
50 protected Func<PostClass, MyCommon.HITRESULT> FilterDelegate;
56 public string SummaryText
58 get { return this.MakeSummary(); }
62 /// ExecFilter() メソッドの実行時に自動でコンパイルを実行する
65 /// テスト用途以外では AutoCompile に頼らず事前に Compile() メソッドを呼び出すこと
67 internal static bool AutoCompile { get; set; }
72 set => this.SetProperty(ref this._enabled, value);
74 private bool _enabled;
76 [XmlElement("NameFilter")]
77 public string FilterName
79 get => this._FilterName;
80 set => this.SetProperty(ref this._FilterName, value);
82 private string _FilterName;
84 [XmlElement("ExNameFilter")]
85 public string ExFilterName
87 get => this._ExFilterName;
88 set => this.SetProperty(ref this._ExFilterName, value);
90 private string _ExFilterName;
92 [XmlArray("BodyFilterArray")]
93 public string[] FilterBody
95 get => this._FilterBody;
96 set => this.SetProperty(ref this._FilterBody, value ?? throw new ArgumentNullException(nameof(value)));
98 private string[] _FilterBody = new string[0];
100 [XmlArray("ExBodyFilterArray")]
101 public string[] ExFilterBody
103 get => this._ExFilterBody;
104 set => this.SetProperty(ref this._ExFilterBody, value ?? throw new ArgumentNullException(nameof(value)));
106 private string[] _ExFilterBody = new string[0];
108 [XmlElement("SearchBoth")]
109 public bool UseNameField
111 get => this._UseNameField;
112 set => this.SetProperty(ref this._UseNameField, value);
114 private bool _UseNameField;
116 [XmlElement("ExSearchBoth")]
117 public bool ExUseNameField
119 get => this._ExUseNameField;
120 set => this.SetProperty(ref this._ExUseNameField, value);
122 private bool _ExUseNameField;
124 [XmlElement("MoveFrom")]
125 public bool MoveMatches
127 get => this._MoveMatches;
128 set => this.SetProperty(ref this._MoveMatches, value);
130 private bool _MoveMatches;
132 [XmlElement("SetMark")]
133 public bool MarkMatches
135 get => this._MarkMatches;
136 set => this.SetProperty(ref this._MarkMatches, value);
138 private bool _MarkMatches;
140 [XmlElement("SearchUrl")]
141 public bool FilterByUrl
143 get => this._FilterByUrl;
144 set => this.SetProperty(ref this._FilterByUrl, value);
146 private bool _FilterByUrl;
148 [XmlElement("ExSearchUrl")]
149 public bool ExFilterByUrl
151 get => this._ExFilterByUrl;
152 set => this.SetProperty(ref this._ExFilterByUrl, value);
154 private bool _ExFilterByUrl;
156 public bool CaseSensitive
158 get => this._CaseSensitive;
159 set => this.SetProperty(ref this._CaseSensitive, value);
161 private bool _CaseSensitive;
163 public bool ExCaseSensitive
165 get => this._ExCaseSensitive;
166 set => this.SetProperty(ref this._ExCaseSensitive, value);
168 private bool _ExCaseSensitive;
170 public bool UseLambda
172 get => this._UseLambda;
173 set => this.SetProperty(ref this._UseLambda, value);
175 private bool _UseLambda;
177 public bool ExUseLambda
179 get => this._ExUseLambda;
180 set => this.SetProperty(ref this._ExUseLambda, value);
182 private bool _ExUseLambda;
186 get => this._UseRegex;
187 set => this.SetProperty(ref this._UseRegex, value);
189 private bool _UseRegex;
191 public bool ExUseRegex
193 get => this._ExUseRegex;
194 set => this.SetProperty(ref this._ExUseRegex, value);
196 private bool _ExUseRegex;
201 get => this._FilterRt;
202 set => this.SetProperty(ref this._FilterRt, value);
204 private bool _FilterRt;
206 [XmlElement("IsExRt")]
207 public bool ExFilterRt
209 get => this._ExFilterRt;
210 set => this.SetProperty(ref this._ExFilterRt, value);
212 private bool _ExFilterRt;
214 [XmlElement("Source")]
215 public string FilterSource
217 get => this._FilterSource;
218 set => this.SetProperty(ref this._FilterSource, value);
220 private string _FilterSource;
222 [XmlElement("ExSource")]
223 public string ExFilterSource
225 get => this._ExFilterSource;
226 set => this.SetProperty(ref this._ExFilterSource, value);
228 private string _ExFilterSource;
230 public PostFilterRule()
235 this.MarkMatches = true;
236 this.UseNameField = true;
237 this.ExUseNameField = true;
240 static PostFilterRule()
242 // TODO: TabsClass とかの改修が終わるまでデフォルト有効
243 PostFilterRule.AutoCompile = true;
249 public void Compile()
253 this.FilterDelegate = x => MyCommon.HITRESULT.None;
254 this.IsDirty = false;
258 var postParam = Expression.Parameter(typeof(PostClass), "x");
260 var matchExpr = this.MakeFiltersExpr(
262 this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt,
263 this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl);
265 var excludeExpr = this.MakeFiltersExpr(
267 this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt,
268 this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl);
270 Expression<Func<PostClass, MyCommon.HITRESULT>> filterExpr;
272 if (matchExpr != null)
274 MyCommon.HITRESULT hitResult;
276 if (this.MoveMatches)
277 hitResult = MyCommon.HITRESULT.Move;
278 else if (this.MarkMatches)
279 hitResult = MyCommon.HITRESULT.CopyAndMark;
281 hitResult = MyCommon.HITRESULT.Copy;
283 if (excludeExpr != null)
285 // x => matchExpr ? (!excludeExpr ? hitResult : None) : None
287 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
288 Expression.Condition(
290 Expression.Condition(
291 Expression.Not(excludeExpr),
292 Expression.Constant(hitResult),
293 Expression.Constant(MyCommon.HITRESULT.None)),
294 Expression.Constant(MyCommon.HITRESULT.None)),
299 // x => matchExpr ? hitResult : None
301 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
302 Expression.Condition(
304 Expression.Constant(hitResult),
305 Expression.Constant(MyCommon.HITRESULT.None)),
309 else if (excludeExpr != null)
311 // x => excludeExpr ? Exclude : None
313 Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
314 Expression.Condition(
316 Expression.Constant(MyCommon.HITRESULT.Exclude),
317 Expression.Constant(MyCommon.HITRESULT.None)),
320 else // matchExpr == null && excludeExpr == null
322 filterExpr = x => MyCommon.HITRESULT.None;
325 this.FilterDelegate = filterExpr.Compile();
326 this.IsDirty = false;
329 protected virtual Expression MakeFiltersExpr(
330 ParameterExpression postParam,
331 string filterName, string[] filterBody, string filterSource, bool filterRt,
332 bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl)
334 var filterExprs = new List<Expression>();
336 if (useNameField && !string.IsNullOrEmpty(filterName))
338 filterExprs.Add(Expression.OrElse(
339 this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true),
340 this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true)));
342 foreach (var body in filterBody)
344 if (string.IsNullOrEmpty(body))
350 // TODO DynamicQuery相当のGPLv3互換なライブラリで置換する
351 Expression<Func<PostClass, bool>> lambdaExpr = x => false;
352 bodyExpr = lambdaExpr.Body;
357 bodyExpr = this.MakeGenericFilter(postParam, "Text", body, useRegex, caseSensitive);
359 bodyExpr = this.MakeGenericFilter(postParam, "TextFromApi", body, useRegex, caseSensitive);
361 // useNameField = false の場合は ScreenName と RetweetedBy も filterBody のマッチ対象となる
364 bodyExpr = Expression.OrElse(
366 this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true));
368 // bodyExpr || x.RetweetedBy != null && <MakeGenericFilter()>
369 bodyExpr = Expression.OrElse(
375 typeof(PostClass).GetProperty("RetweetedBy")),
376 Expression.Constant(null)),
377 this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true)));
381 filterExprs.Add(bodyExpr);
383 if (!string.IsNullOrEmpty(filterSource))
386 filterExprs.Add(this.MakeGenericFilter(postParam, "SourceHtml", filterSource, useRegex, caseSensitive));
388 filterExprs.Add(this.MakeGenericFilter(postParam, "Source", filterSource, useRegex, caseSensitive, exactMatch: true));
392 // x.RetweetedId != null
393 filterExprs.Add(Expression.NotEqual(
396 typeof(PostClass).GetProperty("RetweetedId")),
397 Expression.Constant(null)));
400 if (filterExprs.Count == 0)
406 // filterExpr[0] && filterExpr[1] && ...
407 var filterExpr = filterExprs[0];
408 foreach (var expr in filterExprs.Skip(1))
410 filterExpr = Expression.AndAlso(filterExpr, expr);
417 protected Expression MakeGenericFilter(
418 ParameterExpression postParam, string targetFieldName, string pattern,
419 bool useRegex, bool caseSensitive, bool exactMatch = false)
421 // x.<targetFieldName>
422 var targetField = Expression.Property(
424 typeof(PostClass).GetProperty(targetFieldName));
427 var targetValue = Expression.Coalesce(targetField, Expression.Constant(string.Empty));
431 var regex = new Regex(pattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
433 // regex.IsMatch(targetField)
434 return Expression.Call(
435 Expression.Constant(regex),
436 typeof(Regex).GetMethod("IsMatch", new[] { typeof(string) }),
441 var compOpt = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
446 // pattern.Equals(targetField, compOpt)
447 return Expression.Call(
448 Expression.Constant(pattern),
449 typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }),
451 Expression.Constant(compOpt));
457 // targetField.IndexOf(pattern, compOpt) != -1
458 return Expression.NotEqual(
461 typeof(string).GetMethod("IndexOf", new[] { typeof(string), typeof(StringComparison) }),
462 Expression.Constant(pattern),
463 Expression.Constant(compOpt)),
464 Expression.Constant(-1));
472 /// <param name="post">振り分けるツイート</param>
473 /// <returns>振り分け結果</returns>
474 public MyCommon.HITRESULT ExecFilter(PostClass post)
478 if (!PostFilterRule.AutoCompile)
479 throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");
484 return this.FilterDelegate(post);
487 public PostFilterRule Clone()
489 return new PostFilterRule
491 FilterBody = this.FilterBody,
492 FilterName = this.FilterName,
493 FilterSource = this.FilterSource,
494 FilterRt = this.FilterRt,
495 FilterByUrl = this.FilterByUrl,
496 UseLambda = this.UseLambda,
497 UseNameField = this.UseNameField,
498 UseRegex = this.UseRegex,
499 CaseSensitive = this.CaseSensitive,
500 ExFilterBody = this.ExFilterBody,
501 ExFilterName = this.ExFilterName,
502 ExFilterSource = this.ExFilterSource,
503 ExFilterRt = this.ExFilterRt,
504 ExFilterByUrl = this.ExFilterByUrl,
505 ExUseLambda = this.ExUseLambda,
506 ExUseNameField = this.ExUseNameField,
507 ExUseRegex = this.ExUseRegex,
508 ExCaseSensitive = this.ExCaseSensitive,
509 MoveMatches = this.MoveMatches,
510 MarkMatches = this.MarkMatches,
515 /// 振り分けルールの文字列表現を返します
517 /// <returns>振り分けルールの概要</returns>
518 public override string ToString()
520 return this.SummaryText;
523 protected override void OnPropertyChanged(PropertyChangedEventArgs e)
526 base.OnPropertyChanged(e);
529 #region from Tween v1.1.0.0
531 // The code in this region block is based on code written by the following authors:
532 // (C) 2007 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
533 // (C) 2008 Moz (@syo68k)
538 protected virtual string MakeSummary()
540 var fs = new StringBuilder();
544 fs.Append(Properties.Resources.Disabled);
547 if (this.HasMatchConditions())
549 if (this.UseNameField)
551 if (!string.IsNullOrEmpty(this.FilterName))
553 fs.AppendFormat(Properties.Resources.SetFiltersText1, this.FilterName);
557 fs.Append(Properties.Resources.SetFiltersText2);
560 if (this.FilterBody.Length > 0)
562 fs.Append(Properties.Resources.SetFiltersText3);
563 foreach (var bf in this.FilterBody)
569 fs.Append(Properties.Resources.SetFiltersText4);
572 if (this.UseNameField)
574 fs.Append(Properties.Resources.SetFiltersText5);
578 fs.Append(Properties.Resources.SetFiltersText6);
582 fs.Append(Properties.Resources.SetFiltersText7);
584 if (this.FilterByUrl)
586 fs.Append(Properties.Resources.SetFiltersText8);
588 if (this.CaseSensitive)
590 fs.Append(Properties.Resources.SetFiltersText13);
598 fs.Append("LambdaExp/");
600 if (!string.IsNullOrEmpty(this.FilterSource))
602 fs.AppendFormat("Src…{0}/", this.FilterSource);
607 if (this.HasExcludeConditions())
610 fs.Append(Properties.Resources.SetFiltersText12);
611 if (this.ExUseNameField)
613 if (!string.IsNullOrEmpty(this.ExFilterName))
615 fs.AppendFormat(Properties.Resources.SetFiltersText1, this.ExFilterName);
619 fs.Append(Properties.Resources.SetFiltersText2);
622 if (this.ExFilterBody.Length > 0)
624 fs.Append(Properties.Resources.SetFiltersText3);
625 foreach (var bf in this.ExFilterBody)
631 fs.Append(Properties.Resources.SetFiltersText4);
634 if (this.ExUseNameField)
636 fs.Append(Properties.Resources.SetFiltersText5);
640 fs.Append(Properties.Resources.SetFiltersText6);
644 fs.Append(Properties.Resources.SetFiltersText7);
646 if (this.ExFilterByUrl)
648 fs.Append(Properties.Resources.SetFiltersText8);
650 if (this.ExCaseSensitive)
652 fs.Append(Properties.Resources.SetFiltersText13);
658 if (this.ExUseLambda)
660 fs.Append("LambdaExp/");
662 if (!string.IsNullOrEmpty(this.ExFilterSource))
664 fs.AppendFormat("Src…{0}/", this.ExFilterSource);
671 if (this.MoveMatches)
673 fs.Append(Properties.Resources.SetFiltersText9);
677 fs.Append(Properties.Resources.SetFiltersText11);
679 if (!this.MoveMatches && this.MarkMatches)
681 fs.Append(Properties.Resources.SetFiltersText10);
683 else if (!this.MoveMatches)
690 return fs.ToString();
695 /// この振り分けルールにマッチ条件が含まれているかを返します
697 public bool HasMatchConditions()
699 return !string.IsNullOrEmpty(this.FilterName) ||
700 this.FilterBody.Any(x => !string.IsNullOrEmpty(x)) ||
701 !string.IsNullOrEmpty(this.FilterSource) ||
706 /// この振り分けルールに除外条件が含まれているかを返します
708 public bool HasExcludeConditions()
710 return !string.IsNullOrEmpty(this.ExFilterName) ||
711 this.ExFilterBody.Any(x => !string.IsNullOrEmpty(x)) ||
712 !string.IsNullOrEmpty(this.ExFilterSource) ||
716 public override bool Equals(object obj)
718 return this.Equals(obj as PostFilterRule);
721 public bool Equals(PostFilterRule other)
726 if (other.HasMatchConditions() || this.HasMatchConditions())
728 if (other.FilterName != this.FilterName ||
729 !other.FilterBody.SequenceEqual(this.FilterBody) ||
730 other.FilterSource != this.FilterSource ||
731 other.FilterRt != this.FilterRt ||
732 other.FilterByUrl != this.FilterByUrl ||
733 other.CaseSensitive != this.CaseSensitive ||
734 other.UseNameField != this.UseNameField ||
735 other.UseLambda != this.UseLambda ||
736 other.UseRegex != this.UseRegex)
742 if (other.HasExcludeConditions() || this.HasExcludeConditions())
744 if (other.ExFilterName != this.ExFilterName ||
745 !other.ExFilterBody.SequenceEqual(this.ExFilterBody) ||
746 other.ExFilterSource != this.ExFilterSource ||
747 other.ExFilterRt != this.ExFilterRt ||
748 other.ExFilterByUrl != this.ExFilterByUrl ||
749 other.ExCaseSensitive != this.ExCaseSensitive ||
750 other.ExUseNameField != this.ExUseNameField ||
751 other.ExUseLambda != this.ExUseLambda ||
752 other.ExUseRegex != this.ExUseRegex)
761 public override int GetHashCode()
763 return this.FilterName?.GetHashCode() ?? 0 ^
764 this.FilterSource?.GetHashCode() ?? 0 ^
765 this.FilterBody.Select(x => x?.GetHashCode() ?? 0).Sum() ^
766 this.ExFilterName?.GetHashCode() ?? 0 ^
767 this.ExFilterSource?.GetHashCode() ?? 0 ^
768 this.ExFilterBody.Select(x => x?.GetHashCode() ?? 0).Sum();