You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
5.2 KiB
134 lines
5.2 KiB
using DiscordChatExporter.Core.Utils.Extensions;
|
|
using Superpower;
|
|
using Superpower.Parsers;
|
|
|
|
namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing;
|
|
|
|
internal static class FilterGrammar
|
|
{
|
|
private static readonly TextParser<char> EscapedCharacter = Character
|
|
.EqualTo('\\')
|
|
.IgnoreThen(Character.AnyChar);
|
|
|
|
private static readonly TextParser<string> QuotedString =
|
|
from open in Character.In('"', '\'')
|
|
from value in Parse.OneOf(EscapedCharacter, Character.Except(open)).Many().Text()
|
|
from close in Character.EqualTo(open)
|
|
select value;
|
|
|
|
private static readonly TextParser<string> UnquotedString = Parse
|
|
.OneOf(
|
|
EscapedCharacter,
|
|
// Avoid whitespace as it's treated as an implicit 'and' operator.
|
|
// Also avoid all special tokens used by other parsers.
|
|
Character.ExceptIn(' ', '(', ')', '"', '\'', '-', '~', '|', '&')
|
|
)
|
|
.AtLeastOnce()
|
|
.Text();
|
|
|
|
private static readonly TextParser<string> String = Parse
|
|
.OneOf(QuotedString, UnquotedString)
|
|
.Named("text string");
|
|
|
|
private static readonly TextParser<MessageFilter> ContainsFilter = String.Select(v =>
|
|
(MessageFilter)new ContainsMessageFilter(v)
|
|
);
|
|
|
|
private static readonly TextParser<MessageFilter> FromFilter = Span.EqualToIgnoreCase("from:")
|
|
.Try()
|
|
.IgnoreThen(String)
|
|
.Select(v => (MessageFilter)new FromMessageFilter(v))
|
|
.Named("from:<value>");
|
|
|
|
private static readonly TextParser<MessageFilter> MentionsFilter = Span.EqualToIgnoreCase(
|
|
"mentions:"
|
|
)
|
|
.Try()
|
|
.IgnoreThen(String)
|
|
.Select(v => (MessageFilter)new MentionsMessageFilter(v))
|
|
.Named("mentions:<value>");
|
|
|
|
private static readonly TextParser<MessageFilter> ReactionFilter = Span.EqualToIgnoreCase(
|
|
"reaction:"
|
|
)
|
|
.Try()
|
|
.IgnoreThen(String)
|
|
.Select(v => (MessageFilter)new ReactionMessageFilter(v))
|
|
.Named("reaction:<value>");
|
|
|
|
private static readonly TextParser<MessageFilter> HasFilter = Span.EqualToIgnoreCase("has:")
|
|
.Try()
|
|
.IgnoreThen(
|
|
Parse.OneOf(
|
|
Span.EqualToIgnoreCase("link")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Link))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("embed")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Embed))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("file")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.File))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("video")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Video))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("image")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Image))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("sound")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
|
Span.EqualToIgnoreCase("pin")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
|
.Try(),
|
|
Span.EqualToIgnoreCase("invite")
|
|
.IgnoreThen(Parse.Return(MessageContentMatchKind.Invite))
|
|
.Try()
|
|
)
|
|
)
|
|
.Select(k => (MessageFilter)new HasMessageFilter(k))
|
|
.Named("has:<value>");
|
|
|
|
// Make sure that property-based filters like 'has:link' don't prevent text like 'hello' from being parsed.
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/909#issuecomment-1227575455
|
|
private static readonly TextParser<MessageFilter> PrimitiveFilter = Parse.OneOf(
|
|
FromFilter,
|
|
MentionsFilter,
|
|
ReactionFilter,
|
|
HasFilter,
|
|
ContainsFilter
|
|
);
|
|
|
|
private static readonly TextParser<MessageFilter> GroupedFilter =
|
|
from open in Character.EqualTo('(')
|
|
from content in Parse.Ref(() => ChainedFilter!).Token()
|
|
from close in Character.EqualTo(')')
|
|
select content;
|
|
|
|
private static readonly TextParser<MessageFilter> NegatedFilter = Character
|
|
// Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias
|
|
.In('-', '~')
|
|
.IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter))
|
|
.Select(f => (MessageFilter)new NegatedMessageFilter(f));
|
|
|
|
private static readonly TextParser<MessageFilter> ChainedFilter = Parse.Chain(
|
|
// Operator
|
|
Parse.OneOf(
|
|
// Explicit operator
|
|
Character.In('|', '&').Token().Try(),
|
|
// Implicit operator (resolves to 'and')
|
|
Character.EqualTo(' ').AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
|
),
|
|
// Operand
|
|
Parse.OneOf(NegatedFilter, GroupedFilter, PrimitiveFilter),
|
|
// Reducer
|
|
(op, left, right) =>
|
|
op switch
|
|
{
|
|
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
|
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
|
}
|
|
);
|
|
|
|
public static readonly TextParser<MessageFilter> Filter = ChainedFilter.Token().AtEnd();
|
|
}
|