2019-04-19 18:48:37 +00:00
using System ;
2019-07-11 20:34:38 +00:00
using System.Collections.Generic ;
2019-12-23 17:36:56 +00:00
using System.Diagnostics ;
2019-04-19 18:48:37 +00:00
using System.Linq ;
2019-12-22 23:31:31 +00:00
using System.Net.Http ;
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
2019-04-19 18:48:37 +00:00
using Dapper ;
using Discord ;
using Discord.WebSocket ;
2019-05-08 18:08:56 +00:00
using Microsoft.Extensions.Configuration ;
2019-04-19 18:48:37 +00:00
using Microsoft.Extensions.DependencyInjection ;
2019-10-05 05:41:00 +00:00
using PluralKit.Bot.Commands ;
using PluralKit.Bot.CommandSystem ;
2019-07-15 19:02:50 +00:00
using Sentry ;
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 ( ) ;
} ;
2019-05-13 20:44:49 +00:00
2019-04-19 18:48:37 +00:00
using ( var services = BuildServiceProvider ( ) )
{
2020-01-11 15:49:20 +00:00
SchemaService . Initialize ( ) ;
2019-07-19 00:29:08 +00:00
var logger = services . GetRequiredService < ILogger > ( ) . ForContext < Initialize > ( ) ;
2019-07-15 19:02:50 +00:00
var coreConfig = services . GetRequiredService < CoreConfig > ( ) ;
var botConfig = services . GetRequiredService < BotConfig > ( ) ;
2019-12-26 20:42:44 +00:00
var schema = services . GetRequiredService < SchemaService > ( ) ;
2019-04-19 18:48:37 +00:00
2019-08-09 10:47:46 +00:00
using ( Sentry . SentrySdk . Init ( coreConfig . SentryUrl ) )
2019-07-15 19:02:50 +00:00
{
2019-07-19 00:29:08 +00:00
logger . Information ( "Connecting to database" ) ;
2019-12-26 20:42:44 +00:00
await schema . ApplyMigrations ( ) ;
2019-07-15 19:02:50 +00:00
2019-07-19 00:29:08 +00:00
logger . Information ( "Connecting to Discord" ) ;
2019-07-15 19:02:50 +00:00
var client = services . GetRequiredService < IDiscordClient > ( ) as DiscordShardedClient ;
await client . LoginAsync ( TokenType . Bot , botConfig . Token ) ;
2019-07-19 00:29:08 +00:00
logger . Information ( "Initializing bot" ) ;
2019-07-15 19:02:50 +00:00
await services . GetRequiredService < Bot > ( ) . Init ( ) ;
2019-07-18 15:13:42 +00:00
await client . StartAsync ( ) ;
2019-07-15 19:02:50 +00:00
2019-07-18 15:13:42 +00:00
try
{
await Task . Delay ( - 1 , token . Token ) ;
}
catch ( TaskCanceledException ) { } // We'll just exit normally
2019-07-19 00:29:08 +00:00
logger . Information ( "Shutting down" ) ;
2019-07-15 19:02:50 +00:00
}
2019-04-19 18:48:37 +00:00
}
}
public ServiceProvider BuildServiceProvider ( ) = > new ServiceCollection ( )
2019-07-18 15:13:42 +00:00
. AddTransient ( _ = > _config . GetSection ( "PluralKit" ) . Get < CoreConfig > ( ) ? ? new CoreConfig ( ) )
. AddTransient ( _ = > _config . GetSection ( "PluralKit" ) . GetSection ( "Bot" ) . Get < BotConfig > ( ) ? ? new BotConfig ( ) )
2019-04-19 18:48:37 +00:00
2019-08-11 20:56:20 +00:00
. AddSingleton < DbConnectionCountHolder > ( )
. AddTransient < DbConnectionFactory > ( )
2019-12-26 20:42:44 +00:00
. AddTransient < SchemaService > ( )
2019-07-18 15:13:42 +00:00
2019-07-20 21:10:26 +00:00
. AddSingleton < IDiscordClient , DiscordShardedClient > ( _ = > new DiscordShardedClient ( new DiscordSocketConfig
{
2019-12-22 00:08:14 +00:00
MessageCacheSize = 0 ,
ConnectionTimeout = 2 * 60 * 1000 ,
2019-10-22 17:31:40 +00:00
ExclusiveBulkDelete = true ,
2019-12-23 00:49:21 +00:00
LargeThreshold = 50 ,
2019-12-23 17:36:56 +00:00
DefaultRetryMode = RetryMode . RetryTimeouts | RetryMode . RetryRatelimit
2019-10-31 16:21:12 +00:00
// Commented this out since Debug actually sends, uh, quite a lot that's not necessary in production
// but leaving it here in case I (or someone else) get[s] confused about why logging isn't working again :p
// LogLevel = LogSeverity.Debug // We filter log levels in Serilog, so just pass everything through (Debug is lower than Verbose)
2019-07-20 21:10:26 +00:00
} ) )
2019-07-18 15:13:42 +00:00
. AddSingleton < Bot > ( )
2019-12-22 23:29:04 +00:00
. AddSingleton ( _ = > new HttpClient { Timeout = TimeSpan . FromSeconds ( 5 ) } )
2019-10-05 05:41:00 +00:00
. AddTransient < CommandTree > ( )
. AddTransient < SystemCommands > ( )
. AddTransient < MemberCommands > ( )
. AddTransient < SwitchCommands > ( )
. AddTransient < LinkCommands > ( )
. AddTransient < APICommands > ( )
. AddTransient < ImportExportCommands > ( )
. AddTransient < HelpCommands > ( )
. AddTransient < ModCommands > ( )
. AddTransient < MiscCommands > ( )
2020-01-23 20:20:22 +00:00
. AddTransient < AutoproxyCommands > ( )
2019-10-05 05:41:00 +00:00
2019-07-18 15:13:42 +00:00
. AddTransient < EmbedService > ( )
. AddTransient < ProxyService > ( )
. AddTransient < LogChannelService > ( )
. AddTransient < DataFileService > ( )
2019-08-12 03:47:55 +00:00
. AddTransient < WebhookExecutorService > ( )
2019-12-22 11:50:47 +00:00
2019-08-12 02:05:22 +00:00
. AddTransient < ProxyCacheService > ( )
2019-07-18 15:13:42 +00:00
. AddSingleton < WebhookCacheService > ( )
2020-01-24 19:28:48 +00:00
. AddSingleton < AutoproxyCacheService > ( )
2019-12-22 11:50:47 +00:00
. AddSingleton < ShardInfoService > ( )
. AddSingleton < CpuStatService > ( )
2019-07-18 15:13:42 +00:00
2019-10-26 17:45:30 +00:00
. AddTransient < IDataStore , PostgresDataStore > ( )
2019-07-18 15:13:42 +00:00
2019-08-12 04:54:28 +00:00
. AddSingleton ( svc = > InitUtils . InitMetrics ( svc . GetRequiredService < CoreConfig > ( ) ) )
2019-07-18 15:13:42 +00:00
. AddSingleton < PeriodicStatCollector > ( )
2019-08-09 10:47:46 +00:00
. AddScoped ( _ = > new Sentry . Scope ( null ) )
. AddTransient < PKEventHandler > ( )
2019-07-18 15:13:42 +00:00
2019-08-11 22:07:29 +00:00
. AddScoped < EventIdProvider > ( )
. AddSingleton ( svc = > new LoggerProvider ( svc . GetRequiredService < CoreConfig > ( ) , "bot" ) )
. AddScoped ( svc = > svc . GetRequiredService < LoggerProvider > ( ) . RootLogger . ForContext ( "EventId" , svc . GetRequiredService < EventIdProvider > ( ) . EventId ) )
2019-08-12 01:48:08 +00:00
. AddMemoryCache ( )
2019-07-18 15:26:06 +00:00
. BuildServiceProvider ( ) ;
2019-04-19 18:48:37 +00:00
}
class Bot
{
private IServiceProvider _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 ;
2019-12-23 13:44:20 +00:00
private PKPerformanceEventListener _pl ;
2019-04-19 18:48:37 +00:00
2019-10-05 05:41:00 +00:00
public Bot ( IServiceProvider services , IDiscordClient client , IMetrics metrics , PeriodicStatCollector collector , ILogger logger )
2019-04-19 18:48:37 +00:00
{
2019-12-23 13:44:20 +00:00
_pl = new PKPerformanceEventListener ( ) ;
2019-08-09 10:47:46 +00:00
_services = services ;
_client = client as DiscordShardedClient ;
2019-07-16 19:59:06 +00:00
_metrics = metrics ;
2019-07-16 21:34:22 +00:00
_collector = collector ;
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
{
2019-12-22 11:08:52 +00:00
_client . ShardDisconnected + = ShardDisconnected ;
2019-07-15 15:16:14 +00:00
_client . ShardReady + = ShardReady ;
2019-07-19 00:29:08 +00:00
_client . Log + = FrameworkLog ;
2019-08-09 10:47:46 +00:00
2019-12-23 00:49:21 +00:00
_client . MessageReceived + = ( msg ) = > HandleEvent ( s = > s . AddMessageBreadcrumb ( msg ) , eh = > eh . HandleMessage ( msg ) ) ;
2019-08-09 10:47:46 +00:00
_client . ReactionAdded + = ( msg , channel , reaction ) = > HandleEvent ( s = > s . AddReactionAddedBreadcrumb ( msg , channel , reaction ) , eh = > eh . HandleReactionAdded ( msg , channel , reaction ) ) ;
_client . MessageDeleted + = ( msg , channel ) = > HandleEvent ( s = > s . AddMessageDeleteBreadcrumb ( msg , channel ) , eh = > eh . HandleMessageDeleted ( msg , channel ) ) ;
_client . MessagesBulkDeleted + = ( msgs , channel ) = > HandleEvent ( s = > s . AddMessageBulkDeleteBreadcrumb ( msgs , channel ) , eh = > eh . HandleMessagesBulkDelete ( msgs , channel ) ) ;
2019-12-22 11:50:47 +00:00
_services . GetService < ShardInfoService > ( ) . Init ( _client ) ;
2019-10-05 05:41:00 +00:00
return Task . CompletedTask ;
2019-07-19 00:29:08 +00:00
}
2019-12-22 11:08:52 +00:00
private Task ShardDisconnected ( Exception ex , DiscordSocketClient shard )
{
_logger . Warning ( ex , $"Shard #{shard.ShardId} disconnected" ) ;
return Task . CompletedTask ;
}
2019-07-19 00:29:08 +00:00
private Task FrameworkLog ( LogMessage msg )
{
// Bridge D.NET logging to Serilog
LogEventLevel level = LogEventLevel . Verbose ;
2019-08-09 10:47:46 +00:00
if ( msg . Severity = = LogSeverity . Critical )
level = LogEventLevel . Fatal ;
else if ( msg . Severity = = LogSeverity . Debug )
level = LogEventLevel . Debug ;
else if ( msg . Severity = = LogSeverity . Error )
level = LogEventLevel . Error ;
else if ( msg . Severity = = LogSeverity . Info )
level = LogEventLevel . Information ;
2019-10-31 16:21:12 +00:00
else if ( msg . Severity = = LogSeverity . Debug ) // D.NET's lowest level is Debug and Verbose is greater, Serilog's is the other way around
2019-08-09 10:47:46 +00:00
level = LogEventLevel . Verbose ;
2019-10-31 16:21:12 +00:00
else if ( msg . Severity = = LogSeverity . Verbose )
level = LogEventLevel . Debug ;
2019-07-19 00:29:08 +00:00
_logger . Write ( level , msg . Exception , "Discord.Net {Source}: {Message}" , msg . Source , msg . Message ) ;
return Task . CompletedTask ;
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
2019-12-23 12:55:43 +00:00
await _client . SetGameAsync ( $"pk;help | in {_client.Guilds.Count} servers" ) ;
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
}
2019-07-21 14:43:28 +00:00
private Task ShardReady ( DiscordSocketClient shardClient )
2019-04-25 16:50:07 +00:00
{
2019-07-18 15:13:42 +00:00
_logger . Information ( "Shard {Shard} connected" , shardClient . ShardId ) ;
2019-07-15 15:16:14 +00:00
Console . WriteLine ( $"Shard #{shardClient.ShardId} connected to {shardClient.Guilds.Sum(g => g.Channels.Count)} channels in {shardClient.Guilds.Count} guilds." ) ;
2019-07-16 21:34:22 +00:00
if ( shardClient . ShardId = = 0 )
{
2019-08-09 10:47:46 +00:00
_updateTimer = new Timer ( ( _ ) = > {
HandleEvent ( s = > s . AddPeriodicBreadcrumb ( ) , __ = > UpdatePeriodic ( ) ) ;
} , null , TimeSpan . Zero , TimeSpan . FromMinutes ( 1 ) ) ;
2019-07-16 21:34:22 +00:00
Console . WriteLine (
$"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})." ) ;
}
2019-07-21 14:43:28 +00:00
return Task . CompletedTask ;
2019-04-20 20:25:03 +00:00
}
2019-08-09 10:47:46 +00:00
private Task HandleEvent ( Action < Scope > breadcrumbFactory , 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 ( ) ;
2019-08-09 10:47:46 +00:00
// Create a DI scope for this event
// and log the breadcrumb to the newly created (in-svc-scope) Sentry scope
using ( var scope = _services . CreateScope ( ) )
2019-07-15 19:02:50 +00:00
{
2019-09-02 18:37:24 +00:00
var evtid = scope . ServiceProvider . GetService < EventIdProvider > ( ) . EventId ;
2019-12-23 00:49:21 +00:00
2019-08-09 10:47:46 +00:00
try
{
await handler ( scope . ServiceProvider . GetRequiredService < PKEventHandler > ( ) ) ;
}
catch ( Exception e )
{
2019-12-23 00:49:21 +00:00
var sentryScope = scope . ServiceProvider . GetRequiredService < Scope > ( ) ;
sentryScope . SetTag ( "evtid" , evtid . ToString ( ) ) ;
breadcrumbFactory ( sentryScope ) ;
2019-08-12 03:56:05 +00:00
HandleRuntimeError ( e , scope . ServiceProvider ) ;
2019-08-09 10:47:46 +00:00
}
2019-07-15 19:02:50 +00:00
}
2019-08-09 10:47:46 +00:00
2019-04-19 18:48:37 +00:00
}
2019-08-09 10:47:46 +00:00
#pragma warning disable 4014
Inner ( ) ;
#pragma warning restore 4014
return Task . CompletedTask ;
2019-04-19 18:48:37 +00:00
}
2019-04-20 20:36:54 +00:00
2019-08-12 03:56:05 +00:00
private void HandleRuntimeError ( Exception e , IServiceProvider services )
2019-04-20 20:36:54 +00:00
{
2019-08-12 03:56:05 +00:00
var logger = services . GetRequiredService < ILogger > ( ) ;
var scope = services . GetRequiredService < Scope > ( ) ;
logger . Error ( e , "Exception in bot event handler" ) ;
2019-08-09 10:47:46 +00:00
var evt = new SentryEvent ( e ) ;
2019-10-22 17:31:40 +00:00
// Don't blow out our Sentry budget on sporadic not-our-problem erorrs
if ( e . IsOurProblem ( ) )
SentrySdk . CaptureEvent ( evt , scope ) ;
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 ;
private IServiceProvider _services ;
2019-10-05 05:41:00 +00:00
private CommandTree _tree ;
2019-11-03 18:15:50 +00:00
private IDataStore _data ;
2019-08-09 10:47:46 +00:00
2019-11-03 18:15:50 +00:00
public PKEventHandler ( ProxyService proxy , ILogger logger , IMetrics metrics , IDiscordClient client , DbConnectionFactory connectionFactory , IServiceProvider services , CommandTree tree , IDataStore data )
2019-08-09 10:47:46 +00:00
{
_proxy = proxy ;
_logger = logger ;
_metrics = metrics ;
_client = ( DiscordShardedClient ) client ;
_connectionFactory = connectionFactory ;
_services = services ;
2019-10-05 05:41:00 +00:00
_tree = tree ;
2019-11-03 18:15:50 +00:00
_data = data ;
2019-08-09 10:47:46 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task HandleMessage ( SocketMessage arg )
2019-08-09 10:47:46 +00:00
{
2019-10-31 16:21:12 +00:00
if ( _client . GetShardFor ( ( arg . Channel as IGuildChannel ) ? . Guild ) . ConnectionState ! = ConnectionState . Connected )
return ; // Discard messages while the bot "catches up" to avoid unnecessary CPU pressure causing timeouts
2019-12-22 22:27:11 +00:00
2019-10-05 05:41:00 +00:00
RegisterMessageMetrics ( arg ) ;
2019-08-11 22:57:23 +00:00
2019-08-09 10:47:46 +00:00
// Ignore system messages (member joined, message pinned, etc)
2019-10-05 05:41:00 +00:00
var msg = arg as SocketUserMessage ;
if ( msg = = null ) return ;
2019-08-09 10:47:46 +00:00
// Ignore bot messages
2019-10-05 05:41:00 +00:00
if ( msg . Author . IsBot | | msg . Author . IsWebhook ) return ;
2019-11-03 18:15:50 +00:00
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 ;
2019-12-23 00:49:00 +00:00
else if ( msg . Content ! = null & & Utils . 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 ;
// If it does, fetch the sender's system (because most commands need that) into the context,
// and start command execution
// Note system may be null if user has no system, hence `OrDefault`
PKSystem system ;
using ( var conn = await _connectionFactory . Obtain ( ) )
system = await conn . QueryFirstOrDefaultAsync < PKSystem > (
"select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system" ,
2019-10-05 05:41:00 +00:00
new { Id = msg . Author . Id } ) ;
2019-10-22 17:31:40 +00:00
try
{
await _tree . ExecuteCommand ( new Context ( _services , msg , argPos , system ) ) ;
}
catch ( Exception e )
{
await HandleCommandError ( msg , e ) ;
// HandleCommandError only *reports* the error, we gotta pass it through to the parent
// error handler by rethrowing:
throw ;
}
2019-08-09 10:47:46 +00:00
}
else
{
// If not, try proxying anyway
2019-08-14 05:16:48 +00:00
try
{
2019-10-05 05:41:00 +00:00
await _proxy . HandleMessageAsync ( msg ) ;
2019-08-14 05:16:48 +00:00
}
catch ( PKError e )
{
2019-10-05 05:41:00 +00:00
await arg . Channel . SendMessageAsync ( $"{Emojis.Error} {e.Message}" ) ;
2019-08-14 05:16:48 +00:00
}
2019-08-09 10:47:46 +00:00
}
}
2019-10-22 17:31:40 +00:00
private async Task HandleCommandError ( SocketUserMessage msg , Exception exception )
{
// 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 :)
if ( exception . IsOurProblem ( ) )
{
var eid = _services . GetService < EventIdProvider > ( ) . EventId ;
await msg . 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.
}
2019-08-11 22:57:23 +00:00
private void RegisterMessageMetrics ( SocketMessage msg )
{
_metrics . Measure . Meter . Mark ( BotMetrics . MessagesReceived ) ;
2019-08-12 02:05:22 +00:00
var gatewayLatency = DateTimeOffset . Now - msg . CreatedAt ;
2019-10-27 22:44:27 +00:00
_logger . Verbose ( "Message received with latency {Latency}" , gatewayLatency ) ;
2019-08-11 22:57:23 +00:00
}
2019-08-09 10:47:46 +00:00
public Task HandleReactionAdded ( Cacheable < IUserMessage , ulong > message , ISocketMessageChannel channel ,
SocketReaction reaction ) = > _proxy . HandleReactionAddedAsync ( message , channel , reaction ) ;
public Task HandleMessageDeleted ( Cacheable < IMessage , ulong > message , ISocketMessageChannel channel ) = >
_proxy . HandleMessageDeletedAsync ( message , channel ) ;
public Task HandleMessagesBulkDelete ( IReadOnlyCollection < Cacheable < IMessage , ulong > > messages ,
IMessageChannel channel ) = > _proxy . HandleMessageBulkDeleteAsync ( messages , channel ) ;
}
2019-12-01 00:41:04 +00:00
}