using System.Collections.Generic; using System.Linq; using NodaTime; using PluralKit.Core; namespace PluralKit.Bot { public class ProxyMatcher { private static readonly char AutoproxyEscapeCharacter = '\\'; public static readonly Duration DefaultLatchExpiryTime = Duration.FromHours(6); private readonly IClock _clock; private readonly ProxyTagParser _parser; public ProxyMatcher(ProxyTagParser parser, IClock clock) { _parser = parser; _clock = clock; } public bool TryMatch(MessageContext ctx, IReadOnlyCollection members, out ProxyMatch match, string messageContent, bool hasAttachments, bool allowAutoproxy) { if (TryMatchTags(members, messageContent, hasAttachments, out match)) return true; if (allowAutoproxy && TryMatchAutoproxy(ctx, members, messageContent, out match)) return true; return false; } private bool TryMatchTags(IReadOnlyCollection members, string messageContent, bool hasAttachments, out ProxyMatch match) { if (!_parser.TryMatch(members, messageContent, out match)) return false; // Edge case: If we got a match with blank inner text, we'd normally just send w/ attachments // However, if there are no attachments, the user probably intended something else, so we "un-match" and proceed to autoproxy return hasAttachments || match.Content.Trim().Length > 0; } private bool TryMatchAutoproxy(MessageContext ctx, IReadOnlyCollection members, string messageContent, out ProxyMatch match) { match = default; // Skip autoproxy match if we hit the escape character if (messageContent.StartsWith(AutoproxyEscapeCharacter)) return false; // Find the member we should autoproxy (null if none) var member = ctx.AutoproxyMode switch { AutoproxyMode.Member when ctx.AutoproxyMember != null => members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx) => members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value), _ => null }; if (member == null || (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy)) return false; match = new ProxyMatch { Content = messageContent, Member = member, // We're autoproxying, so not using any proxy tags here // we just find the first pair of tags (if any), otherwise null ProxyTags = member.ProxyTags.FirstOrDefault() }; return true; } private bool IsLatchExpired(MessageContext ctx) { if (ctx.LastMessage == null) return true; if (ctx.LatchTimeout == 0) return false; var timeout = ctx.LatchTimeout.HasValue ? Duration.FromSeconds(ctx.LatchTimeout.Value) : DefaultLatchExpiryTime; var timestamp = DiscordUtils.SnowflakeToInstant(ctx.LastMessage.Value); return _clock.GetCurrentInstant() - timestamp > timeout; } } }