2019-04-19 18:48:37 +00:00
using System ;
2019-07-11 20:34:38 +00:00
using System.Collections.Generic ;
2020-04-17 21:10:01 +00:00
using System.Data ;
2019-04-19 18:48:37 +00:00
using System.Linq ;
2020-04-24 21:47:35 +00:00
using System.Net.WebSockets ;
2019-04-25 16:50:07 +00:00
using System.Threading ;
2019-04-19 18:48:37 +00:00
using System.Threading.Tasks ;
2019-07-16 19:59:06 +00:00
using App.Metrics ;
2019-12-23 00:49:21 +00:00
2020-01-26 00:27:45 +00:00
using Autofac ;
2020-04-17 21:10:01 +00:00
using DSharpPlus ;
using DSharpPlus.Entities ;
using DSharpPlus.EventArgs ;
2019-05-08 18:08:56 +00:00
using Microsoft.Extensions.Configuration ;
2019-10-05 05:41:00 +00:00
2020-01-26 00:27:45 +00:00
using PluralKit.Core ;
2019-10-05 05:41:00 +00:00
2019-07-15 19:02:50 +00:00
using Sentry ;
2020-01-26 00:27:45 +00:00
2019-07-18 15:13:42 +00:00
using Serilog ;
2019-07-19 00:29:08 +00:00
using Serilog.Events ;
2019-04-19 18:48:37 +00:00
2019-04-21 13:33:22 +00:00
namespace PluralKit.Bot
2019-04-19 18:48:37 +00:00
{
class Initialize
{
2019-05-08 18:08:56 +00:00
private IConfiguration _config ;
2019-07-09 18:39:29 +00:00
static void Main ( string [ ] args ) = > new Initialize { _config = InitUtils . BuildConfiguration ( args ) . Build ( ) } . MainAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
2019-04-19 18:48:37 +00:00
private async Task MainAsync ( )
{
2019-12-22 23:28:19 +00:00
ThreadPool . SetMinThreads ( 32 , 32 ) ;
ThreadPool . SetMaxThreads ( 128 , 128 ) ;
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "Starting PluralKit..." ) ;
2019-05-19 20:03:28 +00:00
2019-07-09 18:39:29 +00:00
InitUtils . Init ( ) ;
2019-07-18 15:13:42 +00:00
// Set up a CancellationToken and a SIGINT hook to properly dispose of things when the app is closed
// The Task.Delay line will throw/exit (forgot which) and the stack and using statements will properly unwind
var token = new CancellationTokenSource ( ) ;
Console . CancelKeyPress + = delegate ( object e , ConsoleCancelEventArgs args )
{
args . Cancel = true ;
token . Cancel ( ) ;
} ;
2020-01-26 00:27:45 +00:00
var builder = new ContainerBuilder ( ) ;
builder . RegisterInstance ( _config ) ;
builder . RegisterModule ( new ConfigModule < BotConfig > ( "Bot" ) ) ;
builder . RegisterModule ( new LoggingModule ( "bot" ) ) ;
builder . RegisterModule ( new MetricsModule ( ) ) ;
builder . RegisterModule < DataStoreModule > ( ) ;
builder . RegisterModule < BotModule > ( ) ;
using var services = builder . Build ( ) ;
var logger = services . Resolve < ILogger > ( ) . ForContext < Initialize > ( ) ;
2019-05-13 20:44:49 +00:00
2020-01-26 00:27:45 +00:00
try
2019-04-19 18:48:37 +00:00
{
2020-01-11 15:49:20 +00:00
SchemaService . Initialize ( ) ;
2020-01-26 00:27:45 +00:00
var coreConfig = services . Resolve < CoreConfig > ( ) ;
var schema = services . Resolve < SchemaService > ( ) ;
using var _ = Sentry . SentrySdk . Init ( coreConfig . SentryUrl ) ;
2020-01-11 15:49:20 +00:00
2020-01-26 00:27:45 +00:00
logger . Information ( "Connecting to database" ) ;
await schema . ApplyMigrations ( ) ;
2019-04-19 18:48:37 +00:00
2020-01-26 00:27:45 +00:00
logger . Information ( "Connecting to Discord" ) ;
var client = services . Resolve < DiscordShardedClient > ( ) ;
await client . StartAsync ( ) ;
2020-04-17 21:10:01 +00:00
logger . Information ( "Initializing bot" ) ;
2020-01-26 00:27:45 +00:00
await services . Resolve < Bot > ( ) . Init ( ) ;
try
2019-07-15 19:02:50 +00:00
{
2020-01-26 00:27:45 +00:00
await Task . Delay ( - 1 , token . Token ) ;
2019-07-15 19:02:50 +00:00
}
2020-01-26 00:27:45 +00:00
catch ( TaskCanceledException ) { } // We'll just exit normally
2019-04-19 18:48:37 +00:00
}
2020-01-26 00:27:45 +00:00
catch ( Exception e )
2019-07-20 21:10:26 +00:00
{
2020-01-26 00:27:45 +00:00
logger . Fatal ( e , "Unrecoverable error while initializing bot" ) ;
}
2019-07-18 15:13:42 +00:00
2020-01-26 00:27:45 +00:00
logger . Information ( "Shutting down" ) ;
2020-03-26 23:02:56 +00:00
// Allow the log buffer to flush properly before exiting (needed for fatal errors)
await Task . Delay ( 1000 ) ;
2020-01-26 00:27:45 +00:00
}
2019-04-19 18:48:37 +00:00
}
class Bot
{
2020-01-26 00:27:45 +00:00
private ILifetimeScope _services ;
2019-07-15 15:16:14 +00:00
private DiscordShardedClient _client ;
2019-04-25 16:50:07 +00:00
private Timer _updateTimer ;
2019-07-16 19:59:06 +00:00
private IMetrics _metrics ;
2019-07-16 21:34:22 +00:00
private PeriodicStatCollector _collector ;
2019-07-18 15:13:42 +00:00
private ILogger _logger ;
2020-04-16 16:18:08 +00:00
private WebhookRateLimitService _webhookRateLimit ;
private int _periodicUpdateCount ;
2019-04-19 18:48:37 +00:00
2020-04-17 21:10:01 +00:00
public Bot ( ILifetimeScope services , DiscordShardedClient client , IMetrics metrics , PeriodicStatCollector collector , ILogger logger , WebhookRateLimitService webhookRateLimit )
2019-04-19 18:48:37 +00:00
{
2019-08-09 10:47:46 +00:00
_services = services ;
2020-04-17 21:10:01 +00:00
_client = client ;
2019-07-16 19:59:06 +00:00
_metrics = metrics ;
2019-07-16 21:34:22 +00:00
_collector = collector ;
2020-04-16 16:18:08 +00:00
_webhookRateLimit = webhookRateLimit ;
2019-07-18 15:13:42 +00:00
_logger = logger . ForContext < Bot > ( ) ;
2019-04-19 18:48:37 +00:00
}
2019-10-05 05:41:00 +00:00
public Task Init ( )
2019-04-19 18:48:37 +00:00
{
2020-04-17 21:10:01 +00:00
// _client.ShardDisconnected += ShardDisconnected;
// _client.ShardReady += ShardReady;
_client . DebugLogger . LogMessageReceived + = FrameworkLog ;
2019-08-09 10:47:46 +00:00
2020-04-17 21:10:01 +00:00
_client . MessageCreated + = args = > HandleEvent ( eh = > eh . HandleMessage ( args ) ) ;
_client . MessageReactionAdded + = args = > HandleEvent ( eh = > eh . HandleReactionAdded ( args ) ) ;
_client . MessageDeleted + = args = > HandleEvent ( eh = > eh . HandleMessageDeleted ( args ) ) ;
_client . MessagesBulkDeleted + = args = > HandleEvent ( eh = > eh . HandleMessagesBulkDelete ( args ) ) ;
_client . MessageUpdated + = args = > HandleEvent ( eh = > eh . HandleMessageEdited ( args ) ) ;
2019-12-22 11:50:47 +00:00
2020-01-26 00:27:45 +00:00
_services . Resolve < ShardInfoService > ( ) . Init ( _client ) ;
2019-10-05 05:41:00 +00:00
return Task . CompletedTask ;
2019-07-19 00:29:08 +00:00
}
2020-04-17 21:10:01 +00:00
/ * private Task ShardDisconnected ( Exception ex , DiscordSocketClient shard )
2019-12-22 11:08:52 +00:00
{
_logger . Warning ( ex , $"Shard #{shard.ShardId} disconnected" ) ;
return Task . CompletedTask ;
2020-04-17 21:10:01 +00:00
} * /
2019-12-22 11:08:52 +00:00
2020-04-17 21:10:01 +00:00
private void FrameworkLog ( object sender , DebugLogMessageEventArgs args )
2019-07-19 00:29:08 +00:00
{
2020-04-17 21:10:01 +00:00
// Bridge D#+ logging to Serilog
2019-07-19 00:29:08 +00:00
LogEventLevel level = LogEventLevel . Verbose ;
2020-04-17 21:10:01 +00:00
if ( args . Level = = LogLevel . Critical )
2019-08-09 10:47:46 +00:00
level = LogEventLevel . Fatal ;
2020-04-17 21:10:01 +00:00
else if ( args . Level = = LogLevel . Debug )
2019-08-09 10:47:46 +00:00
level = LogEventLevel . Debug ;
2020-04-17 21:10:01 +00:00
else if ( args . Level = = LogLevel . Error )
2019-08-09 10:47:46 +00:00
level = LogEventLevel . Error ;
2020-04-17 21:10:01 +00:00
else if ( args . Level = = LogLevel . Info )
2019-08-09 10:47:46 +00:00
level = LogEventLevel . Information ;
2020-04-17 21:10:01 +00:00
else if ( args . Level = = LogLevel . Warning )
level = LogEventLevel . Warning ;
2019-07-19 00:29:08 +00:00
2020-04-17 21:10:01 +00:00
_logger . Write ( level , args . Exception , "D#+ {Source}: {Message}" , args . Application , args . Message ) ;
2019-04-19 18:48:37 +00:00
}
2019-07-16 21:34:22 +00:00
// Method called every 60 seconds
2019-04-25 16:50:07 +00:00
private async Task UpdatePeriodic ( )
2019-04-20 20:25:03 +00:00
{
2019-07-16 21:34:22 +00:00
// Change bot status
2020-04-17 21:10:01 +00:00
var totalGuilds = _client . ShardClients . Values . Sum ( c = > c . Guilds . Count ) ;
2020-04-24 21:47:35 +00:00
try // DiscordClient may throw an exception if an update is attempted while reconnecting (e.g just after OP 7 received)
{
await _client . UpdateStatusAsync ( new DiscordActivity ( $"pk;help | in {totalGuilds} servers" ) ) ;
}
catch ( WebSocketException ) { }
2020-04-16 16:18:08 +00:00
// Run webhook rate limit GC every 10 minutes
if ( _periodicUpdateCount + + % 10 = = 0 )
{
var _ = Task . Run ( ( ) = > _webhookRateLimit . GarbageCollect ( ) ) ;
}
2019-07-16 21:34:22 +00:00
await _collector . CollectStats ( ) ;
2019-07-18 15:13:42 +00:00
_logger . Information ( "Submitted metrics to backend" ) ;
2019-07-16 21:34:22 +00:00
await Task . WhenAll ( ( ( IMetricsRoot ) _metrics ) . ReportRunner . RunAllAsync ( ) ) ;
2019-04-25 16:50:07 +00:00
}
2020-04-24 21:47:35 +00:00
private Task ShardReady ( DiscordClient shardClient )
2019-04-25 16:50:07 +00:00
{
2020-04-24 21:47:35 +00:00
_logger . Information ( "Shard {Shard} connected to {ChannelCount} channels in {GuildCount} guilds" , shardClient . ShardId , shardClient . Guilds . Sum ( g = > g . Value . Channels . Count ) , shardClient . Guilds . Count ) ;
2019-07-16 21:34:22 +00:00
if ( shardClient . ShardId = = 0 )
{
2019-08-09 10:47:46 +00:00
_updateTimer = new Timer ( ( _ ) = > {
2020-01-26 00:27:45 +00:00
HandleEvent ( _ = > UpdatePeriodic ( ) ) ;
2019-08-09 10:47:46 +00:00
} , null , TimeSpan . Zero , TimeSpan . FromMinutes ( 1 ) ) ;
2019-07-16 21:34:22 +00:00
2020-01-26 00:27:45 +00:00
_logger . Information ( "PluralKit started as {Username}#{Discriminator} ({Id})" , _client . CurrentUser . Username , _client . CurrentUser . Discriminator , _client . CurrentUser . Id ) ;
}
2019-07-21 14:43:28 +00:00
return Task . CompletedTask ;
2020-04-24 21:47:35 +00:00
}
2019-04-20 20:25:03 +00:00
2020-01-26 00:27:45 +00:00
private Task HandleEvent ( Func < PKEventHandler , Task > handler )
2019-04-19 18:48:37 +00:00
{
2019-08-09 10:47:46 +00:00
// Inner function so we can await the handler without stalling the entire pipeline
async Task Inner ( )
2019-04-29 15:42:09 +00:00
{
2019-08-11 23:15:55 +00:00
// "Fork" this task off by ~~yeeting~~ yielding it at the back of the task queue
// This prevents any synchronous nonsense from also stalling the pipeline before the first await point
await Task . Yield ( ) ;
2020-01-26 00:27:45 +00:00
using var containerScope = _services . BeginLifetimeScope ( ) ;
var sentryScope = containerScope . Resolve < Scope > ( ) ;
var eventHandler = containerScope . Resolve < PKEventHandler > ( ) ;
try
2019-07-15 19:02:50 +00:00
{
2020-01-26 00:27:45 +00:00
await handler ( eventHandler ) ;
}
catch ( Exception e )
{
await HandleRuntimeError ( eventHandler , e , sentryScope ) ;
2019-07-15 19:02:50 +00:00
}
2019-04-19 18:48:37 +00:00
}
2019-08-09 10:47:46 +00:00
2020-01-26 00:27:45 +00:00
var _ = Inner ( ) ;
2019-08-09 10:47:46 +00:00
return Task . CompletedTask ;
2019-04-19 18:48:37 +00:00
}
2019-04-20 20:36:54 +00:00
2020-01-26 00:27:45 +00:00
private async Task HandleRuntimeError ( PKEventHandler eventHandler , Exception exc , Scope scope )
2019-04-20 20:36:54 +00:00
{
2020-01-26 00:27:45 +00:00
_logger . Error ( exc , "Exception in bot event handler" ) ;
2019-08-09 10:47:46 +00:00
2020-01-26 00:27:45 +00:00
var evt = new SentryEvent ( exc ) ;
2019-08-09 10:47:46 +00:00
2019-10-22 17:31:40 +00:00
// Don't blow out our Sentry budget on sporadic not-our-problem erorrs
2020-01-26 00:27:45 +00:00
if ( exc . IsOurProblem ( ) )
2019-10-22 17:31:40 +00:00
SentrySdk . CaptureEvent ( evt , scope ) ;
2020-01-26 00:27:45 +00:00
// Once we've sent it to Sentry, report it to the user
await eventHandler . ReportError ( evt , exc ) ;
2019-04-20 20:36:54 +00:00
}
2019-04-19 18:48:37 +00:00
}
2019-08-09 10:47:46 +00:00
class PKEventHandler {
private ProxyService _proxy ;
private ILogger _logger ;
private IMetrics _metrics ;
private DiscordShardedClient _client ;
private DbConnectionFactory _connectionFactory ;
2020-01-26 00:27:45 +00:00
private ILifetimeScope _services ;
2019-10-05 05:41:00 +00:00
private CommandTree _tree ;
2020-01-26 00:27:45 +00:00
private Scope _sentryScope ;
2020-02-01 13:40:57 +00:00
private ProxyCache _cache ;
2020-02-12 13:21:48 +00:00
private LastMessageCacheService _lastMessageCache ;
2020-02-14 23:12:03 +00:00
private LoggerCleanService _loggerClean ;
2020-01-26 00:27:45 +00:00
// We're defining in the Autofac module that this class is instantiated with one instance per event
// This means that the HandleMessage function will either be called once, or not at all
// The ReportError function will be called on an error, and needs to refer back to the "trigger message"
// hence, we just store it in a local variable, ignoring it entirely if it's null.
2020-04-17 21:10:01 +00:00
private DiscordMessage _currentlyHandlingMessage = null ;
2019-08-09 10:47:46 +00:00
2020-02-14 23:12:03 +00:00
public PKEventHandler ( ProxyService proxy , ILogger logger , IMetrics metrics , DiscordShardedClient client , DbConnectionFactory connectionFactory , ILifetimeScope services , CommandTree tree , Scope sentryScope , ProxyCache cache , LastMessageCacheService lastMessageCache , LoggerCleanService loggerClean )
2019-08-09 10:47:46 +00:00
{
_proxy = proxy ;
_logger = logger ;
_metrics = metrics ;
2020-01-26 00:27:45 +00:00
_client = client ;
2019-08-09 10:47:46 +00:00
_connectionFactory = connectionFactory ;
_services = services ;
2019-10-05 05:41:00 +00:00
_tree = tree ;
2020-01-26 00:27:45 +00:00
_sentryScope = sentryScope ;
2020-02-01 13:40:57 +00:00
_cache = cache ;
2020-02-12 13:21:48 +00:00
_lastMessageCache = lastMessageCache ;
2020-02-14 23:12:03 +00:00
_loggerClean = loggerClean ;
2019-08-09 10:47:46 +00:00
}
2020-04-17 21:10:01 +00:00
public async Task HandleMessage ( MessageCreateEventArgs args )
2019-08-09 10:47:46 +00:00
{
2020-04-17 21:10:01 +00:00
// TODO
/ * var shard = _client . GetShardFor ( ( arg . Channel as IGuildChannel ) ? . Guild ) ;
2020-03-08 10:10:05 +00:00
if ( shard . ConnectionState ! = ConnectionState . Connected | | _client . CurrentUser = = null )
2020-04-17 21:10:01 +00:00
return ; // Discard messages while the bot "catches up" to avoid unnecessary CPU pressure causing timeouts*/
2020-01-26 00:27:45 +00:00
2020-04-17 21:10:01 +00:00
RegisterMessageMetrics ( args ) ;
2019-08-11 22:57:23 +00:00
2019-08-09 10:47:46 +00:00
// Ignore system messages (member joined, message pinned, etc)
2020-04-17 21:10:01 +00:00
var msg = args . Message ;
if ( msg . MessageType ! = MessageType . Default ) return ;
2020-02-14 23:12:03 +00:00
// Fetch information about the guild early, as we need it for the logger cleanup
2020-04-17 21:10:01 +00:00
GuildConfig cachedGuild = default ;
2020-04-24 21:20:34 +00:00
if ( msg . Channel . Type = = ChannelType . Text ) cachedGuild = await _cache . GetGuildDataCached ( msg . Channel . GuildId ) ;
2020-02-14 23:12:03 +00:00
// Pass guild bot/WH messages onto the logger cleanup service, but otherwise ignore
2020-04-17 21:10:01 +00:00
if ( msg . Author . IsBot & & msg . Channel . Type = = ChannelType . Text )
2020-02-14 23:12:03 +00:00
{
2020-04-17 21:10:01 +00:00
await _loggerClean . HandleLoggerBotCleanup ( msg , cachedGuild ) ;
2020-02-14 23:12:03 +00:00
return ;
}
2020-04-17 21:10:01 +00:00
_currentlyHandlingMessage = msg ;
2019-11-03 18:15:50 +00:00
2020-01-26 00:27:45 +00:00
// Add message info as Sentry breadcrumb
_sentryScope . AddBreadcrumb ( msg . Content , "event.message" , data : new Dictionary < string , string >
{
{ "user" , msg . Author . Id . ToString ( ) } ,
{ "channel" , msg . Channel . Id . ToString ( ) } ,
2020-04-17 21:10:01 +00:00
{ "guild" , msg . Channel . GuildId . ToString ( ) } ,
2020-01-26 00:27:45 +00:00
{ "message" , msg . Id . ToString ( ) } ,
} ) ;
2020-04-17 21:10:01 +00:00
_sentryScope . SetTag ( "shard" , args . Client . ShardId . ToString ( ) ) ;
2020-01-26 00:27:45 +00:00
2020-02-12 13:21:48 +00:00
// Add to last message cache
2020-04-17 21:10:01 +00:00
_lastMessageCache . AddMessage ( msg . Channel . Id , msg . Id ) ;
2020-02-12 13:21:48 +00:00
2020-02-14 23:12:03 +00:00
// We fetch information about the sending account from the cache
2020-02-01 13:40:57 +00:00
var cachedAccount = await _cache . GetAccountDataCached ( msg . Author . Id ) ;
// this ^ may be null, do remember that down the line
2019-10-05 05:41:00 +00:00
int argPos = - 1 ;
2019-08-09 10:47:46 +00:00
// Check if message starts with the command prefix
2019-10-27 14:08:33 +00:00
if ( msg . Content . StartsWith ( "pk;" , StringComparison . InvariantCultureIgnoreCase ) ) argPos = 3 ;
else if ( msg . Content . StartsWith ( "pk!" , StringComparison . InvariantCultureIgnoreCase ) ) argPos = 3 ;
2020-02-12 14:16:19 +00:00
else if ( msg . Content ! = null & & StringUtils . HasMentionPrefix ( msg . Content , ref argPos , out var id ) ) // Set argPos to the proper value
2019-10-05 05:41:00 +00:00
if ( id ! = _client . CurrentUser . Id ) // But undo it if it's someone else's ping
argPos = - 1 ;
2019-11-03 18:15:50 +00:00
// If it does, try executing a command
2019-10-05 05:41:00 +00:00
if ( argPos > - 1 )
2019-08-09 10:47:46 +00:00
{
2019-10-27 22:44:27 +00:00
_logger . Verbose ( "Parsing command {Command} from message {Channel}-{Message}" , msg . Content , msg . Channel . Id , msg . Id ) ;
2019-08-11 22:07:29 +00:00
2019-08-09 10:47:46 +00:00
// Essentially move the argPos pointer by however much whitespace is at the start of the post-argPos string
2019-10-05 05:41:00 +00:00
var trimStartLengthDiff = msg . Content . Substring ( argPos ) . Length -
msg . Content . Substring ( argPos ) . TrimStart ( ) . Length ;
2019-08-09 10:47:46 +00:00
argPos + = trimStartLengthDiff ;
2020-02-24 08:57:16 +00:00
try
{
2020-04-17 21:10:01 +00:00
await _tree . ExecuteCommand ( new Context ( _services , args . Client , msg , argPos , cachedAccount ? . System ) ) ;
2020-02-24 08:57:16 +00:00
}
catch ( PKError )
{
// Only permission errors will ever bubble this far and be caught here instead of Context.Execute
// so we just catch and ignore these. TODO: this may need to change.
}
2019-08-09 10:47:46 +00:00
}
2020-02-01 13:40:57 +00:00
else if ( cachedAccount ! = null )
2019-08-09 10:47:46 +00:00
{
// If not, try proxying anyway
2020-02-01 13:40:57 +00:00
// but only if the account data we got before is present
// no data = no account = no system = no proxy!
2019-08-14 05:16:48 +00:00
try
{
2020-04-17 21:10:01 +00:00
await _proxy . HandleMessageAsync ( args . Client , cachedGuild , cachedAccount , msg , doAutoProxy : true ) ;
2019-08-14 05:16:48 +00:00
}
catch ( PKError e )
{
2020-04-17 21:10:01 +00:00
if ( msg . Channel . Guild = = null | | msg . Channel . BotHasPermission ( Permissions . SendMessages ) )
await msg . Channel . SendMessageAsync ( $"{Emojis.Error} {e.Message}" ) ;
2019-08-14 05:16:48 +00:00
}
2019-08-09 10:47:46 +00:00
}
}
2020-01-26 00:27:45 +00:00
public async Task ReportError ( SentryEvent evt , Exception exc )
2019-10-22 17:31:40 +00:00
{
2020-01-26 00:27:45 +00:00
// If we don't have a "trigger message", bail
2020-04-17 21:10:01 +00:00
if ( _currentlyHandlingMessage = = null ) return ;
2020-01-26 00:27:45 +00:00
2019-10-22 17:31:40 +00:00
// This function *specifically* handles reporting a command execution error to the user.
// We'll fetch the event ID and send a user-facing error message.
// ONLY IF this error's actually our problem. As for what defines an error as "our problem",
// check the extension method :)
2020-04-17 21:10:01 +00:00
if ( exc . IsOurProblem ( ) & & _currentlyHandlingMessage . Channel . BotHasPermission ( Permissions . SendMessages ) )
2019-10-22 17:31:40 +00:00
{
2020-01-26 00:27:45 +00:00
var eid = evt . EventId ;
2020-04-17 21:10:01 +00:00
await _currentlyHandlingMessage . Channel . SendMessageAsync (
2019-12-01 00:41:04 +00:00
$"{Emojis.Error} Internal error occurred. Please join the support server (<https://discord.gg/PczBt78>), and send the developer this ID: `{eid}`\nBe sure to include a description of what you were doing to make the error occur." ) ;
2019-10-22 17:31:40 +00:00
}
// If not, don't care. lol.
}
2020-04-17 21:10:01 +00:00
private void RegisterMessageMetrics ( MessageCreateEventArgs msg )
2019-08-11 22:57:23 +00:00
{
_metrics . Measure . Meter . Mark ( BotMetrics . MessagesReceived ) ;
2019-08-12 02:05:22 +00:00
2020-04-17 21:10:01 +00:00
var gatewayLatency = DateTimeOffset . Now - msg . Message . Timestamp ;
2019-10-27 22:44:27 +00:00
_logger . Verbose ( "Message received with latency {Latency}" , gatewayLatency ) ;
2019-08-11 22:57:23 +00:00
}
2020-04-17 21:10:01 +00:00
public Task HandleReactionAdded ( MessageReactionAddEventArgs args )
2020-01-26 00:27:45 +00:00
{
_sentryScope . AddBreadcrumb ( "" , "event.reaction" , data : new Dictionary < string , string > ( )
{
2020-04-17 21:10:01 +00:00
{ "user" , args . User . Id . ToString ( ) } ,
{ "channel" , ( args . Channel ? . Id ? ? 0 ) . ToString ( ) } ,
{ "guild" , ( args . Channel ? . GuildId ? ? 0 ) . ToString ( ) } ,
{ "message" , args . Message . Id . ToString ( ) } ,
{ "reaction" , args . Emoji . Name }
2020-01-26 00:27:45 +00:00
} ) ;
2020-04-17 21:10:01 +00:00
_sentryScope . SetTag ( "shard" , args . Client . ShardId . ToString ( ) ) ;
return _proxy . HandleReactionAddedAsync ( args ) ;
2020-01-26 00:27:45 +00:00
}
2019-08-09 10:47:46 +00:00
2020-04-17 21:10:01 +00:00
public Task HandleMessageDeleted ( MessageDeleteEventArgs args )
2020-01-26 00:27:45 +00:00
{
_sentryScope . AddBreadcrumb ( "" , "event.messageDelete" , data : new Dictionary < string , string > ( )
{
2020-04-17 21:10:01 +00:00
{ "channel" , args . Channel . Id . ToString ( ) } ,
{ "guild" , args . Channel . GuildId . ToString ( ) } ,
{ "message" , args . Message . Id . ToString ( ) } ,
2020-01-26 00:27:45 +00:00
} ) ;
2020-04-17 21:10:01 +00:00
_sentryScope . SetTag ( "shard" , args . Client . ShardId . ToString ( ) ) ;
2020-02-12 13:37:11 +00:00
2020-04-17 21:10:01 +00:00
return _proxy . HandleMessageDeletedAsync ( args ) ;
2020-01-26 00:27:45 +00:00
}
2019-08-09 10:47:46 +00:00
2020-04-17 21:10:01 +00:00
public Task HandleMessagesBulkDelete ( MessageBulkDeleteEventArgs args )
2020-01-26 00:27:45 +00:00
{
_sentryScope . AddBreadcrumb ( "" , "event.messageDelete" , data : new Dictionary < string , string > ( )
{
2020-04-17 21:10:01 +00:00
{ "channel" , args . Channel . Id . ToString ( ) } ,
{ "guild" , args . Channel . Id . ToString ( ) } ,
{ "messages" , string . Join ( "," , args . Messages . Select ( m = > m . Id ) ) } ,
2020-01-26 00:27:45 +00:00
} ) ;
2020-04-17 21:10:01 +00:00
_sentryScope . SetTag ( "shard" , args . Client . ShardId . ToString ( ) ) ;
2020-02-12 13:37:11 +00:00
2020-04-17 21:10:01 +00:00
return _proxy . HandleMessageBulkDeleteAsync ( args ) ;
2020-01-26 00:27:45 +00:00
}
2020-02-12 13:21:48 +00:00
2020-04-17 21:10:01 +00:00
public async Task HandleMessageEdited ( MessageUpdateEventArgs args )
2020-02-12 13:21:48 +00:00
{
2020-04-17 21:10:01 +00:00
_sentryScope . AddBreadcrumb ( args . Message . Content ? ? "<unknown>" , "event.messageEdit" , data : new Dictionary < string , string > ( )
2020-02-12 13:21:48 +00:00
{
2020-04-17 21:10:01 +00:00
{ "channel" , args . Channel . Id . ToString ( ) } ,
{ "guild" , args . Channel . GuildId . ToString ( ) } ,
{ "message" , args . Message . Id . ToString ( ) }
2020-02-12 13:21:48 +00:00
} ) ;
2020-04-17 21:10:01 +00:00
_sentryScope . SetTag ( "shard" , args . Client . ShardId . ToString ( ) ) ;
2020-02-12 13:37:11 +00:00
2020-02-12 13:21:48 +00:00
// If this isn't a guild, bail
2020-04-17 21:10:01 +00:00
if ( args . Channel . Guild = = null ) return ;
2020-02-12 13:21:48 +00:00
// If this isn't the last message in the channel, don't do anything
2020-04-17 21:10:01 +00:00
if ( _lastMessageCache . GetLastMessage ( args . Channel . Id ) ! = args . Message . Id ) return ;
2020-02-12 13:21:48 +00:00
// Fetch account from cache if there is any
2020-04-17 21:10:01 +00:00
var account = await _cache . GetAccountDataCached ( args . Author . Id ) ;
2020-02-12 13:21:48 +00:00
if ( account = = null ) return ; // Again: no cache = no account = no system = no proxy
// Also fetch guild cache
2020-04-17 21:10:01 +00:00
var guild = await _cache . GetGuildDataCached ( args . Channel . GuildId ) ;
2020-02-12 13:21:48 +00:00
// Just run the normal message handling stuff
2020-04-17 21:10:01 +00:00
await _proxy . HandleMessageAsync ( args . Client , guild , account , args . Message , doAutoProxy : false ) ;
2020-02-12 13:21:48 +00:00
}
2019-08-09 10:47:46 +00:00
}
2019-12-01 00:41:04 +00:00
}