2020-06-12 18:29:50 +00:00
using System.Collections.Generic ;
using System.Linq ;
using NodaTime ;
using PluralKit.Core ;
namespace PluralKit.Bot
{
public class ProxyMatcher
{
2020-06-24 14:48:55 +00:00
private static readonly char AutoproxyEscapeCharacter = '\\' ;
2020-12-08 11:57:17 +00:00
public static readonly Duration DefaultLatchExpiryTime = Duration . FromHours ( 6 ) ;
2020-06-12 18:29:50 +00:00
2020-06-13 20:20:24 +00:00
private readonly IClock _clock ;
private readonly ProxyTagParser _parser ;
2020-06-12 18:29:50 +00:00
public ProxyMatcher ( ProxyTagParser parser , IClock clock )
{
_parser = parser ;
_clock = clock ;
}
2020-06-12 21:13:21 +00:00
public bool TryMatch ( MessageContext ctx , IReadOnlyCollection < ProxyMember > members , out ProxyMatch match , string messageContent ,
2020-06-12 18:29:50 +00:00
bool hasAttachments , bool allowAutoproxy )
{
if ( TryMatchTags ( members , messageContent , hasAttachments , out match ) ) return true ;
2020-06-12 21:13:21 +00:00
if ( allowAutoproxy & & TryMatchAutoproxy ( ctx , members , messageContent , out match ) ) return true ;
2020-06-12 18:29:50 +00:00
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
2020-07-05 11:26:49 +00:00
return hasAttachments | | match . Content . Trim ( ) . Length > 0 ;
2020-06-12 18:29:50 +00:00
}
2020-06-12 21:13:21 +00:00
private bool TryMatchAutoproxy ( MessageContext ctx , IReadOnlyCollection < ProxyMember > members , string messageContent ,
2020-06-12 18:29:50 +00:00
out ProxyMatch match )
{
match = default ;
2020-06-24 14:48:55 +00:00
// Skip autoproxy match if we hit the escape character
if ( messageContent . StartsWith ( AutoproxyEscapeCharacter ) )
2021-08-04 01:06:14 +00:00
throw new ProxyService . ProxyChecksFailedException ( "This message matches none of your proxy tags, and it was not autoproxied because it starts with a backslash (`\\`)." ) ;
2020-06-24 14:48:55 +00:00
2020-06-12 21:13:21 +00:00
// Find the member we should autoproxy (null if none)
var member = ctx . AutoproxyMode switch
2020-06-12 18:29:50 +00:00
{
2020-06-12 21:13:21 +00:00
AutoproxyMode . Member when ctx . AutoproxyMember ! = null = >
members . FirstOrDefault ( m = > m . Id = = ctx . AutoproxyMember ) ,
2021-08-04 01:06:14 +00:00
2020-06-14 19:37:04 +00:00
AutoproxyMode . Front when ctx . LastSwitchMembers . Length > 0 = >
2020-06-12 21:13:21 +00:00
members . FirstOrDefault ( m = > m . Id = = ctx . LastSwitchMembers [ 0 ] ) ,
2021-08-04 01:06:14 +00:00
AutoproxyMode . Latch when ctx . LastMessageMember ! = null = >
2020-06-12 21:13:21 +00:00
members . FirstOrDefault ( m = > m . Id = = ctx . LastMessageMember . Value ) ,
_ = > null
} ;
2021-08-04 01:06:14 +00:00
// 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." ) ;
else if ( ctx . AutoproxyMode = = AutoproxyMode . Member )
throw new ProxyService . ProxyChecksFailedException ( "You are using member-specific autoproxy with an invalid member. Was this member deleted?" ) ;
else 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." ) ;
2020-06-12 18:29:50 +00:00
match = new ProxyMatch
{
Content = messageContent ,
2020-06-12 21:13:21 +00:00
Member = member ,
2020-06-12 18:29:50 +00:00
// We're autoproxying, so not using any proxy tags here
// we just find the first pair of tags (if any), otherwise null
2020-06-12 21:13:21 +00:00
ProxyTags = member . ProxyTags . FirstOrDefault ( )
2020-06-12 18:29:50 +00:00
} ;
return true ;
}
2020-06-12 21:13:21 +00:00
2020-11-21 00:44:15 +00:00
private bool IsLatchExpired ( MessageContext ctx )
2020-06-12 21:13:21 +00:00
{
2020-11-21 00:44:15 +00:00
if ( ctx . LastMessage = = null ) return true ;
if ( ctx . LatchTimeout = = 0 ) return false ;
2020-12-08 11:57:17 +00:00
var timeout = ctx . LatchTimeout . HasValue
? Duration . FromSeconds ( ctx . LatchTimeout . Value )
: DefaultLatchExpiryTime ;
2020-11-21 00:44:15 +00:00
var timestamp = DiscordUtils . SnowflakeToInstant ( ctx . LastMessage . Value ) ;
return _clock . GetCurrentInstant ( ) - timestamp > timeout ;
2020-06-12 21:13:21 +00:00
}
2020-06-12 18:29:50 +00:00
}
}