private async Task <(bool success, DiscordMessage message)> EditFilterPropertiesAsync(CommandContext ctx, BotDb db, Piracystring filter) { var interact = ctx.Client.GetInteractivity(); var abort = DiscordEmoji.FromUnicode("🛑"); var lastPage = DiscordEmoji.FromUnicode("↪"); var firstPage = DiscordEmoji.FromUnicode("↩"); var previousPage = DiscordEmoji.FromUnicode("⏪"); var nextPage = DiscordEmoji.FromUnicode("⏩"); var trash = DiscordEmoji.FromUnicode("🗑"); var saveEdit = DiscordEmoji.FromUnicode("💾"); var letterC = DiscordEmoji.FromUnicode("🇨"); var letterL = DiscordEmoji.FromUnicode("🇱"); var letterR = DiscordEmoji.FromUnicode("🇷"); var letterW = DiscordEmoji.FromUnicode("🇼"); var letterM = DiscordEmoji.FromUnicode("🇲"); var letterE = DiscordEmoji.FromUnicode("🇪"); DiscordMessage msg = null; string errorMsg = null; DiscordMessage txt; MessageReactionAddEventArgs emoji; step1: // step 1: define trigger string var embed = FormatFilter(filter, errorMsg, 1) .WithDescription( "Any simple string that is used to flag potential content for a check using Validation regex.\n" + "**Must** be sufficiently long to reduce the number of checks." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify a new **trigger**", embed : embed).ConfigureAwait(false); errorMsg = null; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, lastPage, nextPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == lastPage) { if (filter.Actions.HasFlag(FilterAction.ShowExplain)) { goto step6; } if (filter.Actions.HasFlag(FilterAction.SendMessage)) { goto step5; } goto step4; } } else if (txt?.Content != null) { if (txt.Content.Length < Config.MinimumPiracyTriggerLength) { errorMsg = "Trigger is too short"; goto step1; } filter.String = txt.Content; } else { return(false, msg); } step2: // step 2: context of the filter where it is applicable embed = FormatFilter(filter, errorMsg, 2) .WithDescription( "Context of the filter indicates where it is applicable.\n" + $"**`C`** = **`{FilterContext.Chat}`** will apply it in filtering discord messages.\n" + $"**`L`** = **`{FilterContext.Log}`** will apply it during log parsing.\n" + "Reactions will toggle the context, text message will set the specified flags." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **context(s)**", embed : embed).ConfigureAwait(false); errorMsg = null; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, nextPage, letterC, letterL, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { goto step1; } if (emoji.Emoji == letterC) { filter.Context ^= FilterContext.Chat; goto step2; } if (emoji.Emoji == letterL) { filter.Context ^= FilterContext.Log; goto step2; } } else if (txt != null) { var flagsTxt = txt.Content.Split(Separators, StringSplitOptions.RemoveEmptyEntries); FilterContext newCtx = 0; foreach (var f in flagsTxt) { switch (f.ToUpperInvariant()) { case "C": case "CHAT": newCtx |= FilterContext.Chat; break; case "L": case "LOG": case "LOGS": newCtx |= FilterContext.Log; break; case "ABORT": return(false, msg); case "-": case "SKIP": case "NEXT": break; default: errorMsg = $"Unknown context `{f}`."; goto step2; } } filter.Context = newCtx; } else { return(false, msg); } step3: // step 3: actions that should be performed on match embed = FormatFilter(filter, errorMsg, 3) .WithDescription( "Actions that will be executed on positive match.\n" + $"**`R`** = **`{FilterAction.RemoveContent}`** will remove the message / log.\n" + $"**`W`** = **`{FilterAction.IssueWarning}`** will issue a warning to the user.\n" + $"**`M`** = **`{FilterAction.SendMessage}`** send _a_ message with an explanation of why it was removed.\n" + $"**`E`** = **`{FilterAction.ShowExplain}`** show `explain` for the specified term (**not implemented**).\n" + "Reactions will toggle the action, text message will set the specified flags." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **action(s)**", embed : embed).ConfigureAwait(false); errorMsg = null; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, nextPage, letterR, letterW, letterM, letterE, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { goto step2; } if (emoji.Emoji == letterR) { filter.Actions ^= FilterAction.RemoveContent; goto step3; } if (emoji.Emoji == letterW) { filter.Actions ^= FilterAction.IssueWarning; goto step3; } if (emoji.Emoji == letterM) { filter.Actions ^= FilterAction.SendMessage; goto step3; } if (emoji.Emoji == letterE) { filter.Actions ^= FilterAction.ShowExplain; goto step3; } } else if (txt != null) { var flagsTxt = txt.Content.ToUpperInvariant().Split(Separators, StringSplitOptions.RemoveEmptyEntries); if (flagsTxt.Length == 1 && flagsTxt[0].Length <= Enum.GetValues(typeof(FilterAction)).Length) { flagsTxt = flagsTxt[0].Select(c => c.ToString()).ToArray(); } FilterAction newActions = 0; foreach (var f in flagsTxt) { switch (f) { case "R": case "REMOVE": case "REMOVEMESSAGE": newActions |= FilterAction.RemoveContent; break; case "W": case "WARN": case "WARNING": case "ISSUEWARNING": newActions |= FilterAction.IssueWarning; break; case "M": case "MSG": case "MESSAGE": case "SENDMESSAGE": newActions |= FilterAction.SendMessage; break; case "E": case "X": case "EXPLAIN": case "SHOWEXPLAIN": case "SENDEXPLAIN": newActions |= FilterAction.ShowExplain; break; case "ABORT": return(false, msg); case "-": case "SKIP": case "NEXT": break; default: errorMsg = $"Unknown action `{f.ToLowerInvariant()}`."; goto step2; } } filter.Actions = newActions; } else { return(false, msg); } step4: // step 4: validation regex to filter out false positives of the plaintext triggers embed = FormatFilter(filter, errorMsg, 4) .WithDescription( "Validation [regex](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) to optionally perform more strict trigger check.\n" + "**Please [test](https://regex101.com/) your regex**. Following flags are enabled: Multiline, IgnoreCase.\n" + "Additional validation can help reduce false positives of a plaintext trigger match." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **validation regex**", embed : embed).ConfigureAwait(false); errorMsg = null; var next = (filter.Actions & (FilterAction.SendMessage | FilterAction.ShowExplain)) == 0 ? firstPage : nextPage; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, next, (string.IsNullOrEmpty(filter.ValidatingRegex) ? null : trash), (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { goto step3; } if (emoji.Emoji == firstPage) { goto step1; } if (emoji.Emoji == trash) { filter.ValidatingRegex = null; } } else if (txt != null) { if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-" || txt.Content == ".*") { filter.ValidatingRegex = null; } else { try { Regex.IsMatch("test", txt.Content, RegexOptions.Multiline | RegexOptions.IgnoreCase); } catch (Exception e) { errorMsg = "Invalid regex expression: " + e.Message; goto step4; } filter.ValidatingRegex = txt.Content; } } else { return(false, msg); } if (filter.Actions.HasFlag(FilterAction.SendMessage)) { goto step5; } else if (filter.Actions.HasFlag(FilterAction.ShowExplain)) { goto step6; } else { goto stepConfirm; } step5: // step 5: optional custom message for the user embed = FormatFilter(filter, errorMsg, 5) .WithDescription( "Optional custom message sent to the user.\n" + "If left empty, default piracy warning message will be used." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **validation regex**", embed : embed).ConfigureAwait(false); errorMsg = null; next = (filter.Actions.HasFlag(FilterAction.ShowExplain) ? nextPage : firstPage); (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, next, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { goto step4; } if (emoji.Emoji == firstPage) { goto step1; } } else if (txt != null) { if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-") { filter.CustomMessage = null; } else { filter.CustomMessage = txt.Content; } } else { return(false, msg); } if (filter.Actions.HasFlag(FilterAction.ShowExplain)) { goto step6; } else { goto stepConfirm; } step6: // step 6: show explanation for the term embed = FormatFilter(filter, errorMsg, 6) .WithDescription( "Explanation term that is used to show an explanation.\n" + "**__Currently not implemented__**." ); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **explanation term**", embed : embed).ConfigureAwait(false); errorMsg = null; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, firstPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { if (filter.Actions.HasFlag(FilterAction.SendMessage)) { goto step5; } else { goto step4; } } if (emoji.Emoji == firstPage) { goto step1; } } else if (txt != null) { if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-") { filter.ExplainTerm = null; } else { var existingTerm = await db.Explanation.FirstOrDefaultAsync(exp => exp.Keyword == txt.Content.ToLowerInvariant()).ConfigureAwait(false); if (existingTerm == null) { errorMsg = $"Term `{txt.Content.ToLowerInvariant().Sanitize()}` is not defined."; goto step6; } filter.ExplainTerm = txt.Content; } } else { return(false, msg); } stepConfirm: // last step: confirm if (errorMsg == null && !filter.IsComplete()) { errorMsg = "Some required properties are not defined"; } embed = FormatFilter(filter, errorMsg); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Does this look good? (y/n)", embed : embed.Build()).ConfigureAwait(false); errorMsg = null; (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, firstPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false); if (emoji != null) { if (emoji.Emoji == abort) { return(false, msg); } if (emoji.Emoji == saveEdit) { return(true, msg); } if (emoji.Emoji == previousPage) { if (filter.Actions.HasFlag(FilterAction.ShowExplain)) { goto step6; } if (filter.Actions.HasFlag(FilterAction.SendMessage)) { goto step5; } goto step4; } if (emoji.Emoji == firstPage) { goto step1; } } else if (!string.IsNullOrEmpty(txt?.Content)) { if (!filter.IsComplete()) { goto step5; } switch (txt.Content.ToLowerInvariant()) { case "yes": case "y": case "✅": case "☑": case "✔": case "👌": case "👍": return(true, msg); case "no": case "n": case "❎": case "❌": case "👎": return(false, msg); default: errorMsg = "I don't know what you mean, so I'll just abort"; if (filter.Actions.HasFlag(FilterAction.ShowExplain)) { goto step6; } if (filter.Actions.HasFlag(FilterAction.SendMessage)) { goto step5; } goto step4; } } else { return(false, msg); } return(false, msg); }