2020-06-11 21:20:46 +00:00
using System ;
2020-06-14 20:19:12 +00:00
using System.Collections.Generic ;
2020-06-12 18:29:50 +00:00
using System.Linq ;
2020-06-11 21:20:46 +00:00
using System.Threading.Tasks ;
2020-06-14 20:19:12 +00:00
using App.Metrics ;
2020-06-11 21:20:46 +00:00
using DSharpPlus ;
using DSharpPlus.Entities ;
using DSharpPlus.Exceptions ;
using PluralKit.Core ;
using Serilog ;
namespace PluralKit.Bot
{
2020-06-12 18:29:50 +00:00
public class ProxyService
{
2020-06-13 20:20:24 +00:00
private static readonly TimeSpan MessageDeletionDelay = TimeSpan . FromMilliseconds ( 1000 ) ;
2020-06-12 18:29:50 +00:00
2020-06-12 21:13:21 +00:00
private readonly LogChannelService _logChannel ;
2020-06-13 17:36:43 +00:00
private readonly IDatabase _db ;
2020-06-12 21:13:21 +00:00
private readonly IDataStore _data ;
private readonly ILogger _logger ;
private readonly WebhookExecutorService _webhookExecutor ;
2020-06-12 18:29:50 +00:00
private readonly ProxyMatcher _matcher ;
2020-06-14 20:19:12 +00:00
private readonly IMetrics _metrics ;
2020-06-12 18:29:50 +00:00
public ProxyService ( LogChannelService logChannel , IDataStore data , ILogger logger ,
2020-06-14 20:19:12 +00:00
WebhookExecutorService webhookExecutor , IDatabase db , ProxyMatcher matcher , IMetrics metrics )
2020-06-11 21:20:46 +00:00
{
_logChannel = logChannel ;
_data = data ;
_webhookExecutor = webhookExecutor ;
2020-06-12 18:29:50 +00:00
_db = db ;
_matcher = matcher ;
2020-06-14 20:19:12 +00:00
_metrics = metrics ;
2020-06-11 21:20:46 +00:00
_logger = logger . ForContext < ProxyService > ( ) ;
}
2020-06-12 21:13:21 +00:00
public async Task < bool > HandleIncomingMessage ( DiscordMessage message , MessageContext ctx , bool allowAutoproxy )
2020-06-11 21:20:46 +00:00
{
2020-06-12 21:13:21 +00:00
if ( ! ShouldProxy ( message , ctx ) ) return false ;
2020-06-12 18:29:50 +00:00
// Fetch members and try to match to a specific member
2020-06-13 16:31:20 +00:00
await using var conn = await _db . Obtain ( ) ;
2020-06-14 20:19:12 +00:00
List < ProxyMember > members ;
using ( _metrics . Measure . Timer . Time ( BotMetrics . ProxyMembersQueryTime ) )
members = ( await conn . QueryProxyMembers ( message . Author . Id , message . Channel . GuildId ) ) . ToList ( ) ;
2020-06-12 21:13:21 +00:00
if ( ! _matcher . TryMatch ( ctx , members , out var match , message . Content , message . Attachments . Count > 0 ,
allowAutoproxy ) ) return false ;
2020-06-12 18:29:50 +00:00
2020-06-12 21:13:21 +00:00
// Permission check after proxy match so we don't get spammed when not actually proxying
if ( ! await CheckBotPermissionsOrError ( message . Channel ) ) return false ;
2020-06-12 21:30:10 +00:00
if ( ! CheckProxyNameBoundsOrError ( match . Member . ProxyName ( ctx ) ) ) return false ;
2020-06-12 18:29:50 +00:00
// Everything's in order, we can execute the proxy!
2020-06-13 16:31:20 +00:00
await ExecuteProxy ( conn , message , ctx , match ) ;
2020-06-12 21:13:21 +00:00
return true ;
2020-06-11 21:20:46 +00:00
}
2020-06-12 21:13:21 +00:00
private bool ShouldProxy ( DiscordMessage msg , MessageContext ctx )
{
// Make sure author has a system
if ( ctx . SystemId = = null ) return false ;
// Make sure channel is a guild text channel and this is a normal message
if ( msg . Channel . Type ! = ChannelType . Text | | msg . MessageType ! = MessageType . Default ) return false ;
// Make sure author is a normal user
if ( msg . Author . IsSystem = = true | | msg . Author . IsBot | | msg . WebhookMessage ) return false ;
// Make sure proxying is enabled here
if ( ! ctx . ProxyEnabled | | ctx . InBlacklist ) return false ;
// Make sure we have either an attachment or message content
var isMessageBlank = msg . Content = = null | | msg . Content . Trim ( ) . Length = = 0 ;
if ( isMessageBlank & & msg . Attachments . Count = = 0 ) return false ;
// All good!
return true ;
}
2020-06-13 16:31:20 +00:00
private async Task ExecuteProxy ( IPKConnection conn , DiscordMessage trigger , MessageContext ctx ,
ProxyMatch match )
2020-06-11 21:20:46 +00:00
{
2020-06-12 18:29:50 +00:00
// Send the webhook
2020-06-12 21:30:10 +00:00
var id = await _webhookExecutor . ExecuteWebhook ( trigger . Channel , match . Member . ProxyName ( ctx ) ,
match . Member . ProxyAvatar ( ctx ) ,
2020-06-13 20:16:04 +00:00
match . ProxyContent , trigger . Attachments ) ;
2020-06-12 21:13:21 +00:00
2020-06-13 16:31:20 +00:00
Task SaveMessage ( ) = > _data . AddMessage ( conn , trigger . Author . Id , trigger . Channel . GuildId , trigger . Channel . Id , id , trigger . Id , match . Member . Id ) ;
Task LogMessage ( ) = > _logChannel . LogMessage ( ctx , match , trigger , id ) . AsTask ( ) ;
async Task DeleteMessage ( )
2020-06-11 21:20:46 +00:00
{
2020-06-13 16:31:20 +00:00
// Wait a second or so before deleting the original message
await Task . Delay ( MessageDeletionDelay ) ;
try
{
await trigger . DeleteAsync ( ) ;
}
catch ( NotFoundException )
{
// If it's already deleted, we just log and swallow the exception
_logger . Warning ( "Attempted to delete already deleted proxy trigger message {Message}" , trigger . Id ) ;
}
2020-06-11 21:20:46 +00:00
}
2020-06-13 16:31:20 +00:00
// Run post-proxy actions (simultaneously; order doesn't matter)
// Note that only AddMessage is using our passed-in connection, careful not to pass it elsewhere and run into conflicts
await Task . WhenAll (
DeleteMessage ( ) ,
SaveMessage ( ) ,
LogMessage ( )
) ;
2020-06-11 21:20:46 +00:00
}
2020-06-12 18:29:50 +00:00
private async Task < bool > CheckBotPermissionsOrError ( DiscordChannel channel )
2020-06-11 21:20:46 +00:00
{
var permissions = channel . BotPermissions ( ) ;
// If we can't send messages at all, just bail immediately.
// 2020-04-22: Manage Messages does *not* override a lack of Send Messages.
if ( ( permissions & Permissions . SendMessages ) = = 0 ) return false ;
if ( ( permissions & Permissions . ManageWebhooks ) = = 0 )
{
// todo: PKError-ify these
2020-06-20 15:33:10 +00:00
await channel . SendMessageFixedAsync (
2020-06-11 21:20:46 +00:00
$"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this." ) ;
return false ;
}
if ( ( permissions & Permissions . ManageMessages ) = = 0 )
{
2020-06-20 15:33:10 +00:00
await channel . SendMessageFixedAsync (
2020-06-11 21:20:46 +00:00
$"{Emojis.Error} PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the original trigger message. Please contact a server administrator to remedy this." ) ;
return false ;
}
return true ;
}
2020-06-12 21:13:21 +00:00
2020-06-12 21:30:10 +00:00
private bool CheckProxyNameBoundsOrError ( string proxyName )
2020-06-12 18:29:50 +00:00
{
if ( proxyName . Length < 2 ) throw Errors . ProxyNameTooShort ( proxyName ) ;
if ( proxyName . Length > Limits . MaxProxyNameLength ) throw Errors . ProxyNameTooLong ( proxyName ) ;
2020-06-12 21:13:21 +00:00
2020-06-12 18:29:50 +00:00
// TODO: this never returns false as it throws instead, should this happen?
return true ;
}
2020-06-11 21:20:46 +00:00
}
2020-06-12 18:29:50 +00:00
}