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-12-20 10:38:26 +00:00
using System.Text ;
2020-12-21 02:16:48 +00:00
using System.Text.RegularExpressions ;
2020-06-11 21:20:46 +00:00
using System.Threading.Tasks ;
2020-06-14 20:19:12 +00:00
using App.Metrics ;
2020-12-22 12:15:26 +00:00
using Myriad.Cache ;
using Myriad.Extensions ;
using Myriad.Gateway ;
using Myriad.Rest ;
using Myriad.Rest.Exceptions ;
using Myriad.Rest.Types.Requests ;
using Myriad.Types ;
2020-06-11 21:20:46 +00:00
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-08-29 11:46:27 +00:00
private readonly ModelRepository _repo ;
2020-06-12 21:13:21 +00:00
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-12-22 12:15:26 +00:00
private readonly IDiscordCache _cache ;
2021-08-03 17:44:22 +00:00
private readonly LastMessageCacheService _lastMessage ;
2020-12-22 12:15:26 +00:00
private readonly DiscordApiClient _rest ;
2020-06-12 18:29:50 +00:00
2021-08-03 17:44:22 +00:00
public ProxyService ( LogChannelService logChannel , ILogger logger , WebhookExecutorService webhookExecutor , IDatabase db ,
ProxyMatcher matcher , IMetrics metrics , ModelRepository repo , IDiscordCache cache , DiscordApiClient rest , LastMessageCacheService lastMessage )
2020-06-11 21:20:46 +00:00
{
_logChannel = logChannel ;
_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-08-29 11:46:27 +00:00
_repo = repo ;
2020-12-22 12:15:26 +00:00
_cache = cache ;
2021-08-03 17:44:22 +00:00
_lastMessage = lastMessage ;
2020-12-22 12:15:26 +00:00
_rest = rest ;
2020-06-11 21:20:46 +00:00
_logger = logger . ForContext < ProxyService > ( ) ;
}
2020-12-22 12:15:26 +00:00
public async Task < bool > HandleIncomingMessage ( Shard shard , MessageCreateEvent message , MessageContext ctx , Guild guild , Channel channel , bool allowAutoproxy , PermissionSet botPermissions )
2020-06-11 21:20:46 +00:00
{
2021-08-27 15:03:47 +00:00
if ( ! ShouldProxy ( channel , message , ctx ) )
2020-12-22 12:15:26 +00:00
return false ;
2021-09-22 17:48:34 +00:00
2021-07-15 10:41:19 +00:00
var rootChannel = _cache . GetRootChannel ( message . ChannelId ) ;
2020-06-14 20:19:12 +00:00
List < ProxyMember > members ;
2021-09-03 20:20:07 +00:00
// Fetch members and try to match to a specific member
2020-06-14 20:19:12 +00:00
using ( _metrics . Measure . Timer . Time ( BotMetrics . ProxyMembersQueryTime ) )
2021-09-30 01:51:38 +00:00
members = ( await _repo . GetProxyMembers ( message . Author . Id , message . GuildId ! . Value ) ) . ToList ( ) ;
2021-08-27 15:03:47 +00:00
2020-12-22 12:15:26 +00:00
if ( ! _matcher . TryMatch ( ctx , members , out var match , message . Content , message . Attachments . Length > 0 ,
2020-06-12 21:13:21 +00:00
allowAutoproxy ) ) return false ;
2020-06-12 18:29:50 +00:00
2021-06-14 15:31:14 +00:00
// this is hopefully temporary, so not putting it into a separate method
if ( message . Content ! = null & & message . Content . Length > 2000 ) throw new PKError ( "PluralKit cannot proxy messages over 2000 characters in length." ) ;
2020-06-12 21:13:21 +00:00
// Permission check after proxy match so we don't get spammed when not actually proxying
2021-08-27 15:03:47 +00:00
if ( ! await CheckBotPermissionsOrError ( botPermissions , rootChannel . Id ) )
2020-12-22 12:15:26 +00:00
return false ;
2020-11-26 05:04:40 +00:00
// this method throws, so no need to wrap it in an if statement
CheckProxyNameBoundsOrError ( match . Member . ProxyName ( ctx ) ) ;
2021-08-27 15:03:47 +00:00
2020-07-10 14:35:52 +00:00
// Check if the sender account can mention everyone/here + embed links
// we need to "mirror" these permissions when proxying to prevent exploits
2021-07-15 10:41:19 +00:00
var senderPermissions = PermissionExtensions . PermissionsFor ( guild , rootChannel , message ) ;
2020-12-22 12:15:26 +00:00
var allowEveryone = senderPermissions . HasFlag ( PermissionSet . MentionEveryone ) ;
var allowEmbeds = senderPermissions . HasFlag ( PermissionSet . EmbedLinks ) ;
2020-06-12 18:29:50 +00:00
// Everything's in order, we can execute the proxy!
2021-09-03 20:20:07 +00:00
await ExecuteProxy ( shard , message , ctx , match , allowEveryone , allowEmbeds ) ;
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
2021-08-04 01:06:14 +00:00
public bool ShouldProxy ( Channel channel , Message msg , MessageContext ctx )
2020-06-12 21:13:21 +00:00
{
// Make sure author has a system
2021-08-04 01:06:14 +00:00
if ( ctx . SystemId = = null )
throw new ProxyChecksFailedException ( Errors . NoSystemError . Message ) ;
2021-08-27 15:03:47 +00:00
2020-06-12 21:13:21 +00:00
// Make sure channel is a guild text channel and this is a normal message
2021-08-04 01:06:14 +00:00
if ( ! DiscordUtils . IsValidGuildChannel ( channel ) )
throw new ProxyChecksFailedException ( "This channel is not a text channel." ) ;
if ( msg . Type ! = Message . MessageType . Default & & msg . Type ! = Message . MessageType . Reply )
throw new ProxyChecksFailedException ( "This message is not a normal message." ) ;
2021-08-27 15:03:47 +00:00
2020-06-12 21:13:21 +00:00
// Make sure author is a normal user
2021-08-04 01:06:14 +00:00
if ( msg . Author . System = = true | | msg . Author . Bot | | msg . WebhookId ! = null )
throw new ProxyChecksFailedException ( "This message was not sent by a normal user." ) ;
2021-08-27 15:03:47 +00:00
2020-06-12 21:13:21 +00:00
// Make sure proxying is enabled here
2021-08-04 01:06:14 +00:00
if ( ctx . InBlacklist )
throw new ProxyChecksFailedException ( $"Proxying was disabled in this channel by a server administrator (via the proxy blacklist)." ) ;
// Make sure the system has proxying enabled in the server
if ( ! ctx . ProxyEnabled )
throw new ProxyChecksFailedException ( "Your system has proxying disabled in this server. Type `pk;proxy on` to enable it." ) ;
2021-08-27 15:03:47 +00:00
2020-06-12 21:13:21 +00:00
// Make sure we have either an attachment or message content
var isMessageBlank = msg . Content = = null | | msg . Content . Trim ( ) . Length = = 0 ;
2021-08-04 01:06:14 +00:00
if ( isMessageBlank & & msg . Attachments . Length = = 0 )
throw new ProxyChecksFailedException ( "Message cannot be blank." ) ;
2021-08-27 15:03:47 +00:00
2020-06-12 21:13:21 +00:00
// All good!
return true ;
}
2021-09-03 20:20:07 +00:00
private async Task ExecuteProxy ( Shard shard , Message trigger , MessageContext ctx ,
2020-07-10 14:35:52 +00:00
ProxyMatch match , bool allowEveryone , bool allowEmbeds )
2020-06-11 21:20:46 +00:00
{
2020-12-20 10:38:26 +00:00
// Create reply embed
2020-12-22 12:15:26 +00:00
var embeds = new List < Embed > ( ) ;
2021-01-31 15:02:34 +00:00
if ( trigger . Type = = Message . MessageType . Reply & & trigger . MessageReference ? . ChannelId = = trigger . ChannelId )
2020-12-20 10:38:26 +00:00
{
2021-01-31 15:02:34 +00:00
var repliedTo = trigger . ReferencedMessage . Value ;
2021-01-14 02:21:56 +00:00
if ( repliedTo ! = null )
{
2021-08-15 04:50:31 +00:00
var ( nickname , avatar ) = await FetchReferencedMessageAuthorInfo ( trigger , repliedTo ) ;
var embed = CreateReplyEmbed ( match , trigger , repliedTo , nickname , avatar ) ;
2021-01-14 02:21:56 +00:00
if ( embed ! = null )
embeds . Add ( embed ) ;
}
2021-08-27 15:03:47 +00:00
2021-01-14 02:21:56 +00:00
// TODO: have a clean error for when message can't be fetched instead of just being silent
2020-12-20 10:38:26 +00:00
}
2021-08-27 15:03:47 +00:00
2020-06-12 18:29:50 +00:00
// Send the webhook
2020-07-10 14:35:52 +00:00
var content = match . ProxyContent ;
if ( ! allowEmbeds ) content = content . BreakLinkEmbeds ( ) ;
2020-08-29 11:46:27 +00:00
2021-07-15 10:41:19 +00:00
var messageChannel = _cache . GetChannel ( trigger . ChannelId ) ;
var rootChannel = _cache . GetRootChannel ( trigger . ChannelId ) ;
2021-08-27 15:03:47 +00:00
var threadId = messageChannel . IsThread ( ) ? messageChannel . Id : ( ulong? ) null ;
2021-11-04 18:13:43 +00:00
var guild = _cache . GetGuild ( trigger . GuildId . Value ) ;
2021-07-15 10:41:19 +00:00
2020-12-22 12:15:26 +00:00
var proxyMessage = await _webhookExecutor . ExecuteWebhook ( new ProxyRequest
{
GuildId = trigger . GuildId ! . Value ,
2021-07-15 10:41:19 +00:00
ChannelId = rootChannel . Id ,
ThreadId = threadId ,
2021-08-03 17:44:22 +00:00
Name = await FixSameName ( messageChannel . Id , ctx , match . Member ) ,
2021-08-01 16:51:54 +00:00
AvatarUrl = AvatarUtils . TryRewriteCdnUrl ( match . Member . ProxyAvatar ( ctx ) ) ,
2020-12-22 12:15:26 +00:00
Content = content ,
Attachments = trigger . Attachments ,
2021-11-04 18:13:43 +00:00
FileSizeLimit = guild . FileSizeLimit ( ) ,
2020-12-22 12:15:26 +00:00
Embeds = embeds . ToArray ( ) ,
AllowEveryone = allowEveryone ,
} ) ;
2021-09-03 20:20:07 +00:00
await HandleProxyExecutedActions ( shard , ctx , trigger , proxyMessage , match ) ;
2020-11-15 13:34:49 +00:00
}
2021-08-15 04:50:31 +00:00
private async Task < ( string? , string? ) > FetchReferencedMessageAuthorInfo ( Message trigger , Message referenced )
2020-12-20 10:38:26 +00:00
{
2021-01-31 15:02:34 +00:00
if ( referenced . WebhookId ! = null )
2021-08-15 04:50:31 +00:00
return ( null , null ) ;
2021-01-31 15:02:34 +00:00
2020-12-20 10:38:26 +00:00
try
{
2021-01-31 15:02:34 +00:00
var member = await _rest . GetGuildMember ( trigger . GuildId ! . Value , referenced . Author . Id ) ;
2021-08-15 04:50:31 +00:00
return ( member ? . Nick , member ? . Avatar ) ;
2020-12-20 10:38:26 +00:00
}
2021-01-31 15:02:34 +00:00
catch ( ForbiddenException )
2020-12-20 10:38:26 +00:00
{
2021-01-31 15:02:34 +00:00
_logger . Warning ( "Failed to fetch member {UserId} in guild {GuildId} when getting reply nickname, falling back to username" ,
referenced . Author . Id , trigger . GuildId ! . Value ) ;
2021-08-15 04:50:31 +00:00
return ( null , null ) ;
2020-12-20 10:38:26 +00:00
}
2020-12-20 15:58:52 +00:00
}
2021-08-15 04:50:31 +00:00
private Embed CreateReplyEmbed ( ProxyMatch match , Message trigger , Message repliedTo , string? nickname , string? avatar )
2020-12-20 15:58:52 +00:00
{
2021-01-31 15:02:34 +00:00
// repliedTo doesn't have a GuildId field :/
var jumpLink = $"https://discord.com/channels/{trigger.GuildId}/{repliedTo.ChannelId}/{repliedTo.Id}" ;
2021-08-27 15:03:47 +00:00
2020-12-20 10:38:26 +00:00
var content = new StringBuilder ( ) ;
2021-01-31 15:02:34 +00:00
var hasContent = ! string . IsNullOrWhiteSpace ( repliedTo . Content ) ;
2020-12-20 15:58:52 +00:00
if ( hasContent )
{
2021-01-31 15:02:34 +00:00
var msg = repliedTo . Content ;
2020-12-21 02:16:48 +00:00
if ( msg . Length > 100 )
{
2021-01-31 15:02:34 +00:00
msg = repliedTo . Content . Substring ( 0 , 100 ) ;
2021-06-25 10:54:49 +00:00
var endsWithOpenMention = Regex . IsMatch ( msg , @"<[at]?[@#:][!&]?(\w+:)?(\d+)?(:[tTdDfFR])?$" ) ;
if ( endsWithOpenMention )
2021-06-25 10:34:44 +00:00
{
2021-06-25 10:54:49 +00:00
var mentionTail = repliedTo . Content . Substring ( 100 ) . Split ( ">" ) [ 0 ] ;
2021-06-25 11:15:25 +00:00
if ( repliedTo . Content . Contains ( msg + mentionTail + ">" ) )
2021-06-25 10:54:49 +00:00
msg + = mentionTail + ">" ;
2021-06-25 10:34:44 +00:00
}
2021-06-25 13:05:25 +00:00
var endsWithUrl = Regex . IsMatch ( msg ,
2021-06-25 13:14:30 +00:00
@"(http|https)(:\/\/)?(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256})?\.?([a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$" ) ;
2021-06-25 13:05:25 +00:00
if ( endsWithUrl )
{
var urlTail = repliedTo . Content . Substring ( 100 ) . Split ( " " ) [ 0 ] ;
msg + = urlTail + " " ;
}
2021-08-27 15:03:47 +00:00
2021-01-31 15:02:34 +00:00
var spoilersInOriginalString = Regex . Matches ( repliedTo . Content , @"\|\|" ) . Count ;
2020-12-21 02:16:48 +00:00
var spoilersInTruncatedString = Regex . Matches ( msg , @"\|\|" ) . Count ;
if ( spoilersInTruncatedString % 2 = = 1 & & spoilersInOriginalString % 2 = = 0 )
msg + = "||" ;
2021-06-25 13:05:25 +00:00
if ( msg ! = repliedTo . Content )
msg + = "…" ;
2020-12-21 02:16:48 +00:00
}
2021-08-27 15:03:47 +00:00
2020-12-22 12:15:26 +00:00
content . Append ( $"**[Reply to:]({jumpLink})** " ) ;
2020-12-21 02:16:48 +00:00
content . Append ( msg ) ;
2021-01-31 15:02:34 +00:00
if ( repliedTo . Attachments . Length > 0 )
2020-12-20 15:58:52 +00:00
content . Append ( $" {Emojis.Paperclip}" ) ;
}
else
{
2020-12-22 12:15:26 +00:00
content . Append ( $"*[(click to see attachment)]({jumpLink})*" ) ;
2020-12-20 15:58:52 +00:00
}
2021-08-27 15:03:47 +00:00
2021-01-31 15:02:34 +00:00
var username = nickname ? ? repliedTo . Author . Username ;
2021-08-15 04:50:31 +00:00
var avatarUrl = avatar ! = null
2021-09-30 19:36:01 +00:00
? $"https://cdn.discordapp.com/guilds/{trigger.GuildId}/users/{repliedTo.Author.Id}/avatars/{avatar}.png"
2021-08-15 04:50:31 +00:00
: $"https://cdn.discordapp.com/avatars/{repliedTo.Author.Id}/{repliedTo.Author.Avatar}.png" ;
2020-12-22 12:15:26 +00:00
return new Embed
{
2020-12-20 15:58:52 +00:00
// unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation]
2020-12-22 12:15:26 +00:00
Author = new ( $"{username}\u2004\u21a9\ufe0f" , IconUrl : avatarUrl ) ,
2021-05-01 18:20:00 +00:00
Description = content . ToString ( ) ,
Color = match . Member . Color ? . ToDiscordColor ( ) ,
2020-12-22 12:15:26 +00:00
} ;
2020-12-20 10:38:26 +00:00
}
2021-08-08 19:56:24 +00:00
private async Task < string > FixSameName ( ulong channelId , MessageContext ctx , ProxyMember member )
2021-08-03 17:44:22 +00:00
{
var proxyName = member . ProxyName ( ctx ) ;
2021-08-27 15:03:47 +00:00
2021-08-08 19:56:24 +00:00
var lastMessage = _lastMessage . GetLastMessage ( channelId ) ? . Previous ;
2021-08-03 17:44:22 +00:00
if ( lastMessage = = null )
2021-08-08 19:56:24 +00:00
// cache is out of date or channel is empty.
2021-08-03 17:44:22 +00:00
return proxyName ;
2021-08-27 15:03:47 +00:00
2021-09-18 23:14:08 +00:00
var pkMessage = await _db . Execute ( conn = > _repo . GetMessage ( conn , lastMessage . Id ) ) ;
2021-08-03 17:44:22 +00:00
2021-08-08 19:56:24 +00:00
if ( lastMessage . AuthorUsername = = proxyName )
2021-08-03 17:44:22 +00:00
{
// last message wasn't proxied by us, but somehow has the same name
// it's probably from a different webhook (Tupperbox?) but let's fix it anyway!
2021-08-08 19:56:24 +00:00
if ( pkMessage = = null )
2021-08-03 17:44:22 +00:00
return FixSameNameInner ( proxyName ) ;
// last message was proxied by a different member
2021-08-08 19:56:24 +00:00
if ( pkMessage . Member . Id ! = member . Id )
2021-08-03 17:44:22 +00:00
return FixSameNameInner ( proxyName ) ;
}
// if we fixed the name last message and it's the same member proxying, we want to fix it again
2021-08-08 19:56:24 +00:00
if ( lastMessage . AuthorUsername = = FixSameNameInner ( proxyName ) & & pkMessage ? . Member . Id = = member . Id )
2021-08-03 17:44:22 +00:00
return FixSameNameInner ( proxyName ) ;
// No issues found, current proxy name is fine.
return proxyName ;
}
private string FixSameNameInner ( string name )
= > $"{name}\u17b5" ;
2021-09-03 20:20:07 +00:00
private async Task HandleProxyExecutedActions ( Shard shard , MessageContext ctx ,
2020-12-22 12:15:26 +00:00
Message triggerMessage , Message proxyMessage ,
2020-11-15 13:34:49 +00:00
ProxyMatch match )
{
2021-08-04 04:41:51 +00:00
var sentMessage = new PKMessage
2020-08-29 11:46:27 +00:00
{
2020-11-15 13:34:49 +00:00
Channel = triggerMessage . ChannelId ,
2020-12-22 12:15:26 +00:00
Guild = triggerMessage . GuildId ,
2020-08-29 11:46:27 +00:00
Member = match . Member . Id ,
2020-11-15 13:34:49 +00:00
Mid = proxyMessage . Id ,
OriginalMid = triggerMessage . Id ,
Sender = triggerMessage . Author . Id
2021-08-04 04:41:51 +00:00
} ;
2021-09-30 01:51:38 +00:00
Task SaveMessageInDatabase ( )
= > _repo . AddMessage ( sentMessage ) ;
2021-08-27 15:03:47 +00:00
2021-08-04 04:41:51 +00:00
Task LogMessageToChannel ( ) = > _logChannel . LogMessage ( ctx , sentMessage , triggerMessage , proxyMessage ) . AsTask ( ) ;
2021-08-27 15:03:47 +00:00
2020-11-15 13:34:49 +00:00
async Task DeleteProxyTriggerMessage ( )
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
{
2020-12-22 12:15:26 +00:00
await _rest . DeleteMessage ( triggerMessage . ChannelId , triggerMessage . Id ) ;
2020-06-13 16:31:20 +00:00
}
catch ( NotFoundException )
{
2021-08-27 15:03:47 +00:00
_logger . Debug ( "Trigger message {TriggerMessageId} was already deleted when we attempted to; deleting proxy message {ProxyMessageId} also" ,
2020-11-15 13:34:49 +00:00
triggerMessage . Id , proxyMessage . Id ) ;
await HandleTriggerAlreadyDeleted ( proxyMessage ) ;
// Swallow the exception, we don't need it
2020-06-13 16:31:20 +00:00
}
2020-06-11 21:20:46 +00:00
}
2021-08-27 15:03:47 +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 (
2020-11-15 13:34:49 +00:00
DeleteProxyTriggerMessage ( ) ,
SaveMessageInDatabase ( ) ,
LogMessageToChannel ( )
2020-06-13 16:31:20 +00:00
) ;
2020-06-11 21:20:46 +00:00
}
2020-11-15 13:34:49 +00:00
2020-12-22 12:15:26 +00:00
private async Task HandleTriggerAlreadyDeleted ( Message proxyMessage )
2020-11-15 13:34:49 +00:00
{
// If a trigger message is deleted before we get to delete it, we can assume a mod bot or similar got to it
// In this case we should also delete the now-proxied message.
// This is going to hit the message delete event handler also, so that'll do the cleanup for us
try
{
2020-12-22 12:15:26 +00:00
await _rest . DeleteMessage ( proxyMessage . ChannelId , proxyMessage . Id ) ;
2020-11-15 13:34:49 +00:00
}
catch ( NotFoundException ) { }
catch ( UnauthorizedException ) { }
}
2020-12-22 12:15:26 +00:00
private async Task < bool > CheckBotPermissionsOrError ( PermissionSet permissions , ulong responseChannel )
2020-06-11 21:20:46 +00:00
{
// If we can't send messages at all, just bail immediately.
// 2020-04-22: Manage Messages does *not* override a lack of Send Messages.
2021-08-27 15:03:47 +00:00
if ( ! permissions . HasFlag ( PermissionSet . SendMessages ) )
2020-12-22 12:15:26 +00:00
return false ;
2020-06-11 21:20:46 +00:00
2020-12-22 12:15:26 +00:00
if ( ! permissions . HasFlag ( PermissionSet . ManageWebhooks ) )
2020-06-11 21:20:46 +00:00
{
// todo: PKError-ify these
2020-12-22 12:15:26 +00:00
await _rest . CreateMessage ( responseChannel , new MessageRequest
{
Content = $"{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."
} ) ;
2020-06-11 21:20:46 +00:00
return false ;
}
2020-12-22 12:15:26 +00:00
if ( ! permissions . HasFlag ( PermissionSet . ManageMessages ) )
2020-06-11 21:20:46 +00:00
{
2020-12-22 12:15:26 +00:00
await _rest . CreateMessage ( responseChannel , new MessageRequest
{
Content = $"{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."
} ) ;
2020-06-11 21:20:46 +00:00
return false ;
}
return true ;
}
2020-06-12 21:13:21 +00:00
2020-11-26 05:04:40 +00:00
private void CheckProxyNameBoundsOrError ( string proxyName )
2020-06-12 18:29:50 +00:00
{
if ( proxyName . Length > Limits . MaxProxyNameLength ) throw Errors . ProxyNameTooLong ( proxyName ) ;
}
2021-08-27 15:03:47 +00:00
public class ProxyChecksFailedException : Exception
2021-08-04 01:06:14 +00:00
{
2021-08-27 15:03:47 +00:00
public ProxyChecksFailedException ( string message ) : base ( message ) { }
2021-08-04 01:06:14 +00:00
}
2020-06-11 21:20:46 +00:00
}
2021-08-27 15:03:47 +00:00
}