PluralKit/PluralKit.Bot/Proxy/ProxyMatcher.cs

114 lines
5.1 KiB
C#

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<ProxyMember> 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<ProxyMember> 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<ProxyMember> members,
string messageContent,
out ProxyMatch match)
{
match = default;
// Skip autoproxy match if we hit the escape character
if (messageContent.StartsWith(AutoproxyEscapeCharacter))
throw new ProxyService.ProxyChecksFailedException(
"This message matches none of your proxy tags, and it was not autoproxied because it starts with a backslash (`\\`).");
// 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 =>
members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value),
_ => null
};
// Throw an error if the member is null, message varies depending on autoproxy mode
if (member == null)
{
if (ctx.AutoproxyMode == AutoproxyMode.Front)
throw new ProxyService.ProxyChecksFailedException(
"You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch <member>` to log a new switch.");
if (ctx.AutoproxyMode == AutoproxyMode.Member)
throw new ProxyService.ProxyChecksFailedException(
"You are using member-specific autoproxy with an invalid member. Was this member deleted?");
if (ctx.AutoproxyMode == AutoproxyMode.Latch)
throw new ProxyService.ProxyChecksFailedException(
"You are using autoproxy latch, but have not sent any messages yet in this server. Please send a message using proxy tags first.");
throw new ProxyService.ProxyChecksFailedException(
"This message matches none of your proxy tags and autoproxy is not enabled.");
}
if (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy)
throw new ProxyService.ProxyChecksFailedException(
"This member has autoproxy disabled. To enable it, use `pk;m <member> autoproxy on`.");
// Moved the IsLatchExpired() check to here, so that an expired latch and a latch without any previous messages throw different errors
if (ctx.AutoproxyMode == AutoproxyMode.Latch && IsLatchExpired(ctx))
throw new ProxyService.ProxyChecksFailedException(
"Latch-mode autoproxy has timed out. Please send a new message using proxy tags.");
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;
}
}