#nullable enable using System.Text.RegularExpressions; using PluralKit.Core; namespace PluralKit.Bot; public class ProxyTagParser { private readonly Regex prefixPattern = new(@"^<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>"); private readonly Regex suffixPattern = new(@"<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>$"); public bool TryMatch(IEnumerable members, string? input, out ProxyMatch result) { result = default; // Null input is valid and is equivalent to empty string if (input == null) return false; // If the message starts with a @mention, and then proceeds to have proxy tags, // extract the mention and place it inside the inner message // eg. @Ske [text] => [@Ske text] var leadingMention = ExtractLeadingMention(ref input); // "Flatten" list of members to a list of tag-member pairs // Then order them by "tag specificity" // (prefix+suffix length desc = inner message asc = more specific proxy first) var tags = members .SelectMany(member => member.ProxyTags.Select(tag => (tag, member))) .OrderByDescending(p => p.tag.ProxyString.Length); // Iterate now-ordered list of tags and try matching each one foreach (var (tag, member) in tags) { result.ProxyTags = tag; result.Member = member; // Skip blank tags (shouldn't ever happen in practice) if (tag.Prefix == null && tag.Suffix == null) continue; if (tag.Prefix == "<" && prefixPattern.IsMatch(input)) continue; if (tag.Suffix == ">" && suffixPattern.IsMatch(input)) continue; // Can we match with these tags? if (TryMatchTagsInner(input, tag, out result.Content)) { // If we extracted a leading mention before, add that back now if (leadingMention != null) result.Content = $"{leadingMention} {result.Content}"; return true; } // (if not, keep going) } // We couldn't match anything :( return false; } private bool TryMatchTagsInner(string input, ProxyTag tag, out string inner) { inner = ""; // Normalize null tags to empty strings var prefix = tag.Prefix ?? ""; var suffix = tag.Suffix ?? ""; // Check if our input starts/ends with the tags var isMatch = input.Length >= prefix.Length + suffix.Length && input.StartsWith(prefix) && input.EndsWith(suffix); // Special case: image-only proxies + proxy tags with spaces // Trim everything, then see if we have a "contentless tag pair" (normally disallowed, but OK if we have an attachment) // Note `input` is still "", even if there are spaces between if (!isMatch && input.Trim() == prefix.TrimEnd() + suffix.TrimStart()) return true; if (!isMatch) return false; // We got a match, extract inner text inner = input.Substring(prefix.Length, input.Length - prefix.Length - suffix.Length); // (see https://github.com/PluralKit/PluralKit/pull/181) return inner.Trim() != "\U0000fe0f"; } private string? ExtractLeadingMention(ref string input) { var mentionPos = 0; if (!DiscordUtils.HasMentionPrefix(input, ref mentionPos, out _)) return null; var leadingMention = input.Substring(0, mentionPos); input = input.Substring(mentionPos); return leadingMention; } }