Refactor and simplify the main bot classes
This commit is contained in:
		@@ -1,10 +1,8 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Data;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.WebSockets;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using App.Metrics;
 | 
			
		||||
 | 
			
		||||
using Autofac;
 | 
			
		||||
@@ -13,8 +11,6 @@ using DSharpPlus;
 | 
			
		||||
using DSharpPlus.Entities;
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
@@ -26,130 +22,89 @@ using Serilog.Events;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    class Initialize
 | 
			
		||||
    public class Bot
 | 
			
		||||
    {
 | 
			
		||||
        private IConfiguration _config;
 | 
			
		||||
        
 | 
			
		||||
        static void Main(string[] args) => new Initialize { _config = InitUtils.BuildConfiguration(args).Build()}.MainAsync().GetAwaiter().GetResult();
 | 
			
		||||
        private readonly DiscordShardedClient _client;
 | 
			
		||||
        private readonly ILogger _logger;
 | 
			
		||||
        private readonly ILifetimeScope _services;
 | 
			
		||||
        private readonly PeriodicStatCollector _collector;
 | 
			
		||||
        private readonly IMetrics _metrics;
 | 
			
		||||
 | 
			
		||||
        private async Task MainAsync()
 | 
			
		||||
        private Task _periodicTask; // Never read, just kept here for GC reasons
 | 
			
		||||
 | 
			
		||||
        public Bot(DiscordShardedClient client, ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics)
 | 
			
		||||
        {
 | 
			
		||||
            ThreadPool.SetMinThreads(32, 32);
 | 
			
		||||
            ThreadPool.SetMaxThreads(128, 128);
 | 
			
		||||
            
 | 
			
		||||
            Console.WriteLine("Starting PluralKit...");
 | 
			
		||||
            
 | 
			
		||||
            InitUtils.Init();
 | 
			
		||||
 | 
			
		||||
            // 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();
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            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>();
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                SchemaService.Initialize();
 | 
			
		||||
 | 
			
		||||
                var coreConfig = services.Resolve<CoreConfig>();
 | 
			
		||||
                var schema = services.Resolve<SchemaService>();
 | 
			
		||||
 | 
			
		||||
                using var _ = Sentry.SentrySdk.Init(coreConfig.SentryUrl);
 | 
			
		||||
                
 | 
			
		||||
                logger.Information("Connecting to database");
 | 
			
		||||
                await schema.ApplyMigrations();
 | 
			
		||||
 | 
			
		||||
                logger.Information("Connecting to Discord");
 | 
			
		||||
                var client = services.Resolve<DiscordShardedClient>();
 | 
			
		||||
                await client.StartAsync();
 | 
			
		||||
                
 | 
			
		||||
                logger.Information("Initializing bot");
 | 
			
		||||
                await services.Resolve<Bot>().Init();
 | 
			
		||||
                
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(-1, token.Token);
 | 
			
		||||
                }
 | 
			
		||||
                catch (TaskCanceledException) { } // We'll just exit normally
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception e)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Fatal(e, "Unrecoverable error while initializing bot");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Information("Shutting down");
 | 
			
		||||
            
 | 
			
		||||
            // Allow the log buffer to flush properly before exiting (needed for fatal errors)
 | 
			
		||||
            await Task.Delay(1000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    class Bot
 | 
			
		||||
    {
 | 
			
		||||
        private ILifetimeScope _services;
 | 
			
		||||
        private DiscordShardedClient _client;
 | 
			
		||||
        private IMetrics _metrics;
 | 
			
		||||
        private PeriodicStatCollector _collector;
 | 
			
		||||
        private ILogger _logger;
 | 
			
		||||
        private Task _periodicWorker;
 | 
			
		||||
        
 | 
			
		||||
        public Bot(ILifetimeScope services, DiscordShardedClient client, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
 | 
			
		||||
        {
 | 
			
		||||
            _services = services;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _metrics = metrics;
 | 
			
		||||
            _services = services;
 | 
			
		||||
            _collector = collector;
 | 
			
		||||
            _metrics = metrics;
 | 
			
		||||
            _logger = logger.ForContext<Bot>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task Init()
 | 
			
		||||
        public void Init()
 | 
			
		||||
        {
 | 
			
		||||
            // Attach the handlers we need
 | 
			
		||||
            _client.DebugLogger.LogMessageReceived += FrameworkLog;
 | 
			
		||||
            
 | 
			
		||||
            _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)); 
 | 
			
		||||
            // HandleEvent takes a type parameter, automatically inferred by the event type
 | 
			
		||||
            _client.MessageCreated += HandleEvent;
 | 
			
		||||
            _client.MessageDeleted += HandleEvent;
 | 
			
		||||
            _client.MessageUpdated += HandleEvent;
 | 
			
		||||
            _client.MessagesBulkDeleted += HandleEvent;
 | 
			
		||||
            _client.MessageReactionAdded += HandleEvent;
 | 
			
		||||
            
 | 
			
		||||
            // Init the shard stuff
 | 
			
		||||
            _services.Resolve<ShardInfoService>().Init(_client);
 | 
			
		||||
            
 | 
			
		||||
            // Will not be awaited, just runs in the background
 | 
			
		||||
            _periodicWorker = UpdatePeriodic();
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
            // Not awaited, just needs to run in the background
 | 
			
		||||
            _periodicTask = UpdatePeriodic();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void FrameworkLog(object sender, DebugLogMessageEventArgs args)
 | 
			
		||||
        private Task HandleEvent<T>(T evt) where T: DiscordEventArgs
 | 
			
		||||
        {
 | 
			
		||||
            // Bridge D#+ logging to Serilog
 | 
			
		||||
            LogEventLevel level = LogEventLevel.Verbose;
 | 
			
		||||
            if (args.Level == LogLevel.Critical)
 | 
			
		||||
                level = LogEventLevel.Fatal;
 | 
			
		||||
            else if (args.Level == LogLevel.Debug)
 | 
			
		||||
                level = LogEventLevel.Debug;
 | 
			
		||||
            else if (args.Level == LogLevel.Error)
 | 
			
		||||
                level = LogEventLevel.Error;
 | 
			
		||||
            else if (args.Level == LogLevel.Info)
 | 
			
		||||
                level = LogEventLevel.Information;
 | 
			
		||||
            else if (args.Level == LogLevel.Warning)
 | 
			
		||||
                level = LogEventLevel.Warning;
 | 
			
		||||
            // We don't want to stall the event pipeline, so we'll "fork" inside here
 | 
			
		||||
            var _ = HandleEventInner();
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            _logger.Write(level, args.Exception, "D#+ {Source}: {Message}", args.Application, args.Message);
 | 
			
		||||
            async Task HandleEventInner()
 | 
			
		||||
            {
 | 
			
		||||
                var serviceScope = _services.BeginLifetimeScope();
 | 
			
		||||
                var handler = serviceScope.Resolve<IEventHandler<T>>();
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await handler.Handle(evt);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception exc)
 | 
			
		||||
                {
 | 
			
		||||
                    await HandleError(handler, evt, serviceScope, exc);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task HandleError<T>(IEventHandler<T> handler, T evt, ILifetimeScope serviceScope, Exception exc)
 | 
			
		||||
            where T: DiscordEventArgs
 | 
			
		||||
        {
 | 
			
		||||
            _logger.Error(exc, "Exception in bot event handler");
 | 
			
		||||
 | 
			
		||||
            var shouldReport = exc.IsOurProblem();
 | 
			
		||||
            if (shouldReport)
 | 
			
		||||
            {
 | 
			
		||||
                // Report error to Sentry
 | 
			
		||||
                // This will just no-op if there's no URL set
 | 
			
		||||
                var sentryEvent = new SentryEvent();
 | 
			
		||||
                var sentryScope = serviceScope.Resolve<Scope>();
 | 
			
		||||
                SentrySdk.CaptureEvent(sentryEvent, sentryScope);
 | 
			
		||||
 | 
			
		||||
                // Once we've sent it to Sentry, report it to the user (if we have permission to)
 | 
			
		||||
                var reportChannel = handler.ErrorChannelFor(evt);
 | 
			
		||||
                if (reportChannel != null && reportChannel.BotHasPermission(Permissions.SendMessages))
 | 
			
		||||
                {
 | 
			
		||||
                    var eid = sentryEvent.EventId;
 | 
			
		||||
                    await reportChannel.SendMessageAsync(
 | 
			
		||||
                        $"{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.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task UpdatePeriodic()
 | 
			
		||||
@@ -169,275 +124,28 @@ namespace PluralKit.Bot
 | 
			
		||||
                }
 | 
			
		||||
                catch (WebSocketException) { }
 | 
			
		||||
 | 
			
		||||
                // Collect some stats, submit them to the metrics backend
 | 
			
		||||
                await _collector.CollectStats();
 | 
			
		||||
 | 
			
		||||
                _logger.Information("Submitted metrics to backend");
 | 
			
		||||
                await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync());
 | 
			
		||||
                _logger.Information("Submitted metrics to backend");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task HandleEvent(Func<PKEventHandler, Task> handler)
 | 
			
		||||
        private void FrameworkLog(object sender, DebugLogMessageEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            // Inner function so we can await the handler without stalling the entire pipeline
 | 
			
		||||
            async Task Inner()
 | 
			
		||||
            {
 | 
			
		||||
                // "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();
 | 
			
		||||
                
 | 
			
		||||
                using var containerScope = _services.BeginLifetimeScope();
 | 
			
		||||
                var sentryScope = containerScope.Resolve<Scope>();
 | 
			
		||||
                var eventHandler = containerScope.Resolve<PKEventHandler>();
 | 
			
		||||
            // Bridge D#+ logging to Serilog
 | 
			
		||||
            LogEventLevel level = LogEventLevel.Verbose;
 | 
			
		||||
            if (args.Level == LogLevel.Critical)
 | 
			
		||||
                level = LogEventLevel.Fatal;
 | 
			
		||||
            else if (args.Level == LogLevel.Debug)
 | 
			
		||||
                level = LogEventLevel.Debug;
 | 
			
		||||
            else if (args.Level == LogLevel.Error)
 | 
			
		||||
                level = LogEventLevel.Error;
 | 
			
		||||
            else if (args.Level == LogLevel.Info)
 | 
			
		||||
                level = LogEventLevel.Information;
 | 
			
		||||
            else if (args.Level == LogLevel.Warning)
 | 
			
		||||
                level = LogEventLevel.Warning;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await handler(eventHandler);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception e)
 | 
			
		||||
                {
 | 
			
		||||
                    await HandleRuntimeError(eventHandler, e, sentryScope);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var _ = Inner();
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task HandleRuntimeError(PKEventHandler eventHandler, Exception exc, Scope scope)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.Error(exc, "Exception in bot event handler");
 | 
			
		||||
            
 | 
			
		||||
            var evt = new SentryEvent(exc);
 | 
			
		||||
            
 | 
			
		||||
            // Don't blow out our Sentry budget on sporadic not-our-problem erorrs
 | 
			
		||||
            if (exc.IsOurProblem())
 | 
			
		||||
                SentrySdk.CaptureEvent(evt, scope);
 | 
			
		||||
            
 | 
			
		||||
            // Once we've sent it to Sentry, report it to the user
 | 
			
		||||
            await eventHandler.ReportError(evt, exc);
 | 
			
		||||
            _logger.Write(level, args.Exception, "D#+ {Source}: {Message}", args.Application, args.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class PKEventHandler {
 | 
			
		||||
        private ProxyService _proxy;
 | 
			
		||||
        private ILogger _logger;
 | 
			
		||||
        private IMetrics _metrics;
 | 
			
		||||
        private DiscordShardedClient _client;
 | 
			
		||||
        private DbConnectionFactory _connectionFactory;
 | 
			
		||||
        private ILifetimeScope _services;
 | 
			
		||||
        private CommandTree _tree;
 | 
			
		||||
        private Scope _sentryScope;
 | 
			
		||||
        private ProxyCache _cache;
 | 
			
		||||
        private LastMessageCacheService _lastMessageCache;
 | 
			
		||||
        private LoggerCleanService _loggerClean;
 | 
			
		||||
 | 
			
		||||
        // 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.
 | 
			
		||||
        private DiscordMessage _currentlyHandlingMessage = null;
 | 
			
		||||
 | 
			
		||||
        public PKEventHandler(ProxyService proxy, ILogger logger, IMetrics metrics, DiscordShardedClient client, DbConnectionFactory connectionFactory, ILifetimeScope services, CommandTree tree, Scope sentryScope, ProxyCache cache, LastMessageCacheService lastMessageCache, LoggerCleanService loggerClean)
 | 
			
		||||
        {
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _metrics = metrics;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _connectionFactory = connectionFactory;
 | 
			
		||||
            _services = services;
 | 
			
		||||
            _tree = tree;
 | 
			
		||||
            _sentryScope = sentryScope;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
            _lastMessageCache = lastMessageCache;
 | 
			
		||||
            _loggerClean = loggerClean;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task HandleMessage(MessageCreateEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            // TODO
 | 
			
		||||
            /*var shard = _client.GetShardFor((arg.Channel as IGuildChannel)?.Guild);
 | 
			
		||||
            if (shard.ConnectionState != ConnectionState.Connected || _client.CurrentUser == null)
 | 
			
		||||
                return; // Discard messages while the bot "catches up" to avoid unnecessary CPU pressure causing timeouts*/
 | 
			
		||||
 | 
			
		||||
            RegisterMessageMetrics(args);
 | 
			
		||||
 | 
			
		||||
            // Ignore system messages (member joined, message pinned, etc)
 | 
			
		||||
            var msg = args.Message;
 | 
			
		||||
            if (msg.MessageType != MessageType.Default) return;
 | 
			
		||||
            
 | 
			
		||||
            // Fetch information about the guild early, as we need it for the logger cleanup
 | 
			
		||||
            GuildConfig cachedGuild = default;
 | 
			
		||||
            if (msg.Channel.Type == ChannelType.Text) cachedGuild = await _cache.GetGuildDataCached(msg.Channel.GuildId);
 | 
			
		||||
            
 | 
			
		||||
            // Pass guild bot/WH messages onto the logger cleanup service, but otherwise ignore
 | 
			
		||||
            if (msg.Author.IsBot && msg.Channel.Type == ChannelType.Text)
 | 
			
		||||
            {
 | 
			
		||||
                await _loggerClean.HandleLoggerBotCleanup(msg, cachedGuild);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _currentlyHandlingMessage = msg;
 | 
			
		||||
            
 | 
			
		||||
            // 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()},
 | 
			
		||||
                {"guild", msg.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", msg.Id.ToString()},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", args.Client.ShardId.ToString());
 | 
			
		||||
            
 | 
			
		||||
            // Add to last message cache
 | 
			
		||||
            _lastMessageCache.AddMessage(msg.Channel.Id, msg.Id);
 | 
			
		||||
            
 | 
			
		||||
            // We fetch information about the sending account from the cache
 | 
			
		||||
            var cachedAccount = await _cache.GetAccountDataCached(msg.Author.Id);
 | 
			
		||||
            // this ^ may be null, do remember that down the line
 | 
			
		||||
 | 
			
		||||
            int argPos = -1;
 | 
			
		||||
            // Check if message starts with the command prefix
 | 
			
		||||
            if (msg.Content.StartsWith("pk;", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
 | 
			
		||||
            else if (msg.Content.StartsWith("pk!", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
 | 
			
		||||
            else if (msg.Content != null && StringUtils.HasMentionPrefix(msg.Content, ref argPos, out var id)) // Set argPos to the proper value
 | 
			
		||||
                if (id != _client.CurrentUser.Id) // But undo it if it's someone else's ping
 | 
			
		||||
                    argPos = -1;
 | 
			
		||||
            
 | 
			
		||||
            // If it does, try executing a command
 | 
			
		||||
            if (argPos > -1)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.Verbose("Parsing command {Command} from message {Channel}-{Message}", msg.Content, msg.Channel.Id, msg.Id);
 | 
			
		||||
                
 | 
			
		||||
                // Essentially move the argPos pointer by however much whitespace is at the start of the post-argPos string
 | 
			
		||||
                var trimStartLengthDiff = msg.Content.Substring(argPos).Length -
 | 
			
		||||
                                          msg.Content.Substring(argPos).TrimStart().Length;
 | 
			
		||||
                argPos += trimStartLengthDiff;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await _tree.ExecuteCommand(new Context(_services, args.Client, msg, argPos, cachedAccount?.System));
 | 
			
		||||
                }
 | 
			
		||||
                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.
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (cachedAccount != null)
 | 
			
		||||
            {
 | 
			
		||||
                // If not, try proxying anyway
 | 
			
		||||
                // but only if the account data we got before is present
 | 
			
		||||
                // no data = no account = no system = no proxy!
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await _proxy.HandleMessageAsync(args.Client, cachedGuild, cachedAccount, msg, doAutoProxy: true);
 | 
			
		||||
                }
 | 
			
		||||
                catch (PKError e)
 | 
			
		||||
                {
 | 
			
		||||
                    if (msg.Channel.Guild == null || msg.Channel.BotHasPermission(Permissions.SendMessages))
 | 
			
		||||
                        await msg.Channel.SendMessageAsync($"{Emojis.Error} {e.Message}");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task ReportError(SentryEvent evt, Exception exc)
 | 
			
		||||
        {
 | 
			
		||||
            // If we don't have a "trigger message", bail
 | 
			
		||||
            if (_currentlyHandlingMessage == null) return;
 | 
			
		||||
            
 | 
			
		||||
            // 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 (exc.IsOurProblem() && _currentlyHandlingMessage.Channel.BotHasPermission(Permissions.SendMessages))
 | 
			
		||||
            {
 | 
			
		||||
                var eid = evt.EventId;
 | 
			
		||||
                await _currentlyHandlingMessage.Channel.SendMessageAsync(
 | 
			
		||||
                    $"{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.");
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // If not, don't care. lol.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RegisterMessageMetrics(MessageCreateEventArgs msg)
 | 
			
		||||
        {
 | 
			
		||||
            _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
 | 
			
		||||
 | 
			
		||||
            var gatewayLatency = DateTimeOffset.Now - msg.Message.Timestamp;
 | 
			
		||||
            _logger.Verbose("Message received with latency {Latency}", gatewayLatency);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task HandleReactionAdded(MessageReactionAddEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.reaction", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"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}
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", args.Client.ShardId.ToString());
 | 
			
		||||
            return _proxy.HandleReactionAddedAsync(args);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task HandleMessageDeleted(MessageDeleteEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", args.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", args.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", args.Message.Id.ToString()},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", args.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            return _proxy.HandleMessageDeletedAsync(args);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task HandleMessagesBulkDelete(MessageBulkDeleteEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", args.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", args.Channel.Id.ToString()},
 | 
			
		||||
                {"messages", string.Join(",", args.Messages.Select(m => m.Id))},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", args.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            return _proxy.HandleMessageBulkDeleteAsync(args);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task HandleMessageEdited(MessageUpdateEventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            // Sometimes edit message events arrive for other reasons (eg. an embed gets updated server-side)
 | 
			
		||||
            // If this wasn't a *content change* (ie. there's message contents to read), bail
 | 
			
		||||
            // It'll also sometimes arrive with no *author*, so we'll go ahead and ignore those messages too
 | 
			
		||||
            if (args.Message.Content == null) return;
 | 
			
		||||
            if (args.Author == null) return;
 | 
			
		||||
            
 | 
			
		||||
            _sentryScope.AddBreadcrumb(args.Message.Content ?? "<unknown>", "event.messageEdit", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", args.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", args.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", args.Message.Id.ToString()}
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", args.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            // If this isn't a guild, bail
 | 
			
		||||
            if (args.Channel.Guild == null) return;
 | 
			
		||||
            
 | 
			
		||||
            // If this isn't the last message in the channel, don't do anything
 | 
			
		||||
            if (_lastMessageCache.GetLastMessage(args.Channel.Id) != args.Message.Id) return;
 | 
			
		||||
            
 | 
			
		||||
            // Fetch account from cache if there is any
 | 
			
		||||
            var account = await _cache.GetAccountDataCached(args.Author.Id);
 | 
			
		||||
            if (account == null) return; // Again: no cache = no account = no system = no proxy
 | 
			
		||||
            
 | 
			
		||||
            // Also fetch guild cache
 | 
			
		||||
            var guild = await _cache.GetGuildDataCached(args.Channel.GuildId);
 | 
			
		||||
 | 
			
		||||
            // Just run the normal message handling stuff
 | 
			
		||||
            await _proxy.HandleMessageAsync(args.Client, guild, account, args.Message, doAutoProxy: false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								PluralKit.Bot/Handlers/IEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								PluralKit.Bot/Handlers/IEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus.Entities;
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    public interface IEventHandler<in T> where T: DiscordEventArgs
 | 
			
		||||
    {
 | 
			
		||||
        Task Handle(T evt);
 | 
			
		||||
 | 
			
		||||
        DiscordChannel ErrorChannelFor(T evt) => null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										144
									
								
								PluralKit.Bot/Handlers/MessageCreated.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								PluralKit.Bot/Handlers/MessageCreated.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using App.Metrics;
 | 
			
		||||
 | 
			
		||||
using Autofac;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus;
 | 
			
		||||
using DSharpPlus.Entities;
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
 | 
			
		||||
using Sentry;
 | 
			
		||||
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    public class MessageCreated: IEventHandler<MessageCreateEventArgs>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly CommandTree _tree;
 | 
			
		||||
        private readonly DiscordShardedClient _client;
 | 
			
		||||
        private readonly LastMessageCacheService _lastMessageCache;
 | 
			
		||||
        private readonly ILogger _logger;
 | 
			
		||||
        private readonly LoggerCleanService _loggerClean;
 | 
			
		||||
        private readonly IMetrics _metrics;
 | 
			
		||||
        private readonly ProxyService _proxy;
 | 
			
		||||
        private readonly ProxyCache _proxyCache;
 | 
			
		||||
        private readonly Scope _sentryScope;
 | 
			
		||||
        private readonly ILifetimeScope _services;
 | 
			
		||||
        
 | 
			
		||||
        public MessageCreated(LastMessageCacheService lastMessageCache, ILogger logger, LoggerCleanService loggerClean, IMetrics metrics, ProxyService proxy, ProxyCache proxyCache, Scope sentryScope, DiscordShardedClient client, CommandTree tree, ILifetimeScope services)
 | 
			
		||||
        {
 | 
			
		||||
            _lastMessageCache = lastMessageCache;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _loggerClean = loggerClean;
 | 
			
		||||
            _metrics = metrics;
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
            _proxyCache = proxyCache;
 | 
			
		||||
            _sentryScope = sentryScope;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _tree = tree;
 | 
			
		||||
            _services = services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public DiscordChannel ErrorChannelFor(MessageCreateEventArgs evt) => evt.Channel;
 | 
			
		||||
 | 
			
		||||
        public async Task Handle(MessageCreateEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            RegisterMessageMetrics(evt);
 | 
			
		||||
 | 
			
		||||
            // Ignore system messages (member joined, message pinned, etc)
 | 
			
		||||
            var msg = evt.Message;
 | 
			
		||||
            if (msg.MessageType != MessageType.Default) return;
 | 
			
		||||
 | 
			
		||||
            var cachedGuild = await _proxyCache.GetGuildDataCached(msg.Channel.GuildId);
 | 
			
		||||
            var cachedAccount = await _proxyCache.GetAccountDataCached(msg.Author.Id);
 | 
			
		||||
            // this ^ may be null, do remember that down the line
 | 
			
		||||
            
 | 
			
		||||
            // Pass guild bot/WH messages onto the logger cleanup service
 | 
			
		||||
            if (msg.Author.IsBot && msg.Channel.Type == ChannelType.Text)
 | 
			
		||||
            {
 | 
			
		||||
                await _loggerClean.HandleLoggerBotCleanup(msg, cachedGuild);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // First try parsing a command, then try proxying
 | 
			
		||||
            if (await TryHandleCommand(evt, cachedGuild, cachedAccount)) return;
 | 
			
		||||
            await TryHandleProxy(evt, cachedGuild, cachedAccount);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<bool> TryHandleCommand(MessageCreateEventArgs evt, GuildConfig cachedGuild, CachedAccount cachedAccount)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = evt.Message;
 | 
			
		||||
            
 | 
			
		||||
            int argPos = -1;
 | 
			
		||||
            // Check if message starts with the command prefix
 | 
			
		||||
            if (msg.Content.StartsWith("pk;", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
 | 
			
		||||
            else if (msg.Content.StartsWith("pk!", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
 | 
			
		||||
            else if (msg.Content != null && StringUtils.HasMentionPrefix(msg.Content, ref argPos, out var id)) // Set argPos to the proper value
 | 
			
		||||
                if (id != _client.CurrentUser.Id) // But undo it if it's someone else's ping
 | 
			
		||||
                    argPos = -1;
 | 
			
		||||
            
 | 
			
		||||
            // If we didn't find a prefix, give up handling commands
 | 
			
		||||
            if (argPos == -1) return false;
 | 
			
		||||
            
 | 
			
		||||
            // Trim leading whitespace from command without actually modifying the wring
 | 
			
		||||
            // This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string
 | 
			
		||||
            var trimStartLengthDiff = msg.Content.Substring(argPos).Length - msg.Content.Substring(argPos).TrimStart().Length;
 | 
			
		||||
            argPos += trimStartLengthDiff;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _tree.ExecuteCommand(new Context(_services, evt.Client, msg, argPos, cachedAccount?.System));
 | 
			
		||||
            }
 | 
			
		||||
            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.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<bool> TryHandleProxy(MessageCreateEventArgs evt, GuildConfig cachedGuild, CachedAccount cachedAccount)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = evt.Message;
 | 
			
		||||
            
 | 
			
		||||
            // If we don't have any cached account data, this means no member in the account has a proxy tag set
 | 
			
		||||
            if (cachedAccount == null) return false;
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _proxy.HandleMessageAsync(evt.Client, cachedGuild, cachedAccount, msg, doAutoProxy: true);
 | 
			
		||||
            }
 | 
			
		||||
            catch (PKError e)
 | 
			
		||||
            {
 | 
			
		||||
                // User-facing errors, print to the channel properly formatted
 | 
			
		||||
                if (msg.Channel.Guild == null || msg.Channel.BotHasPermission(Permissions.SendMessages))
 | 
			
		||||
                    await msg.Channel.SendMessageAsync($"{Emojis.Error} {e.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RegisterMessageMetrics(MessageCreateEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
 | 
			
		||||
            _lastMessageCache.AddMessage(evt.Channel.Id, evt.Message.Id);
 | 
			
		||||
            
 | 
			
		||||
            // Add message info as Sentry breadcrumb
 | 
			
		||||
            _sentryScope.AddBreadcrumb(evt.Message.Content, "event.message", data: new Dictionary<string, string>
 | 
			
		||||
            {
 | 
			
		||||
                {"user", evt.Author.Id.ToString()},
 | 
			
		||||
                {"channel", evt.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", evt.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", evt.Message.Id.ToString()},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", evt.Client.ShardId.ToString());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								PluralKit.Bot/Handlers/MessageDeleted.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								PluralKit.Bot/Handlers/MessageDeleted.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using Sentry;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    // Double duty :)
 | 
			
		||||
    public class MessageDeleted: IEventHandler<MessageDeleteEventArgs>, IEventHandler<MessageBulkDeleteEventArgs>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ProxyService _proxy;
 | 
			
		||||
        private readonly Scope _sentryScope;
 | 
			
		||||
 | 
			
		||||
        public MessageDeleted(Scope sentryScope, ProxyService proxy)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope = sentryScope;
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public Task Handle(MessageDeleteEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", evt.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", evt.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", evt.Message.Id.ToString()},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", evt.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            return _proxy.HandleMessageDeletedAsync(evt);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task Handle(MessageBulkDeleteEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", evt.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", evt.Channel.Id.ToString()},
 | 
			
		||||
                {"messages", string.Join(",", evt.Messages.Select(m => m.Id))},
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", evt.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            return _proxy.HandleMessageBulkDeleteAsync(evt);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								PluralKit.Bot/Handlers/MessageEdited.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								PluralKit.Bot/Handlers/MessageEdited.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
 | 
			
		||||
using Sentry;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    public class MessageEdited: IEventHandler<MessageUpdateEventArgs>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly LastMessageCacheService _lastMessageCache;
 | 
			
		||||
        private readonly ProxyService _proxy;
 | 
			
		||||
        private readonly ProxyCache _proxyCache;
 | 
			
		||||
        private readonly Scope _sentryScope;
 | 
			
		||||
 | 
			
		||||
        public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, ProxyCache proxyCache, Scope sentryScope)
 | 
			
		||||
        {
 | 
			
		||||
            _lastMessageCache = lastMessageCache;
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
            _proxyCache = proxyCache;
 | 
			
		||||
            _sentryScope = sentryScope;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task Handle(MessageUpdateEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            // Sometimes edit message events arrive for other reasons (eg. an embed gets updated server-side)
 | 
			
		||||
            // If this wasn't a *content change* (ie. there's message contents to read), bail
 | 
			
		||||
            // It'll also sometimes arrive with no *author*, so we'll go ahead and ignore those messages too
 | 
			
		||||
            if (evt.Message.Content == null) return;
 | 
			
		||||
            if (evt.Author == null) return;
 | 
			
		||||
            
 | 
			
		||||
            // Also, if this is in DMs don't bother either
 | 
			
		||||
            if (evt.Channel.Guild == null) return;
 | 
			
		||||
            
 | 
			
		||||
            _sentryScope.AddBreadcrumb(evt.Message.Content ?? "<unknown>", "event.messageEdit", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"channel", evt.Channel.Id.ToString()},
 | 
			
		||||
                {"guild", evt.Channel.GuildId.ToString()},
 | 
			
		||||
                {"message", evt.Message.Id.ToString()}
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", evt.Client.ShardId.ToString());
 | 
			
		||||
 | 
			
		||||
            // If this isn't the last message in the channel, don't do anything
 | 
			
		||||
            if (_lastMessageCache.GetLastMessage(evt.Channel.Id) != evt.Message.Id) return;
 | 
			
		||||
            
 | 
			
		||||
            // Fetch account and guild info from cache if there is any
 | 
			
		||||
            var account = await _proxyCache.GetAccountDataCached(evt.Author.Id);
 | 
			
		||||
            if (account == null) return; // Again: no cache = no account = no system = no proxy
 | 
			
		||||
            var guild = await _proxyCache.GetGuildDataCached(evt.Channel.GuildId);
 | 
			
		||||
            
 | 
			
		||||
            // Just run the normal message handling stuff, with a flag to disable autoproxying
 | 
			
		||||
            await _proxy.HandleMessageAsync(evt.Client, guild, account, evt.Message, doAutoProxy: false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								PluralKit.Bot/Handlers/ReactionAdded.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								PluralKit.Bot/Handlers/ReactionAdded.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using Sentry;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    public class ReactionAdded: IEventHandler<MessageReactionAddEventArgs>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ProxyService _proxy;
 | 
			
		||||
        private readonly Scope _sentryScope;
 | 
			
		||||
 | 
			
		||||
        public ReactionAdded(ProxyService proxy, Scope sentryScope)
 | 
			
		||||
        {
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
            _sentryScope = sentryScope;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task Handle(MessageReactionAddEventArgs evt)
 | 
			
		||||
        {
 | 
			
		||||
            _sentryScope.AddBreadcrumb("", "event.reaction", data: new Dictionary<string, string>()
 | 
			
		||||
            {
 | 
			
		||||
                {"user", evt.User.Id.ToString()},
 | 
			
		||||
                {"channel", (evt.Channel?.Id ?? 0).ToString()},
 | 
			
		||||
                {"guild", (evt.Channel?.GuildId ?? 0).ToString()},
 | 
			
		||||
                {"message", evt.Message.Id.ToString()},
 | 
			
		||||
                {"reaction", evt.Emoji.Name}
 | 
			
		||||
            });
 | 
			
		||||
            _sentryScope.SetTag("shard", evt.Client.ShardId.ToString());
 | 
			
		||||
            return _proxy.HandleReactionAddedAsync(evt);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								PluralKit.Bot/Init.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								PluralKit.Bot/Init.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
using Autofac;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus;
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot
 | 
			
		||||
{
 | 
			
		||||
    public class Init
 | 
			
		||||
    {
 | 
			
		||||
        static Task Main(string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            // Load configuration and run global init stuff
 | 
			
		||||
            var config = InitUtils.BuildConfiguration(args).Build();
 | 
			
		||||
            InitUtils.Init();
 | 
			
		||||
            
 | 
			
		||||
            // Set up DI container and modules
 | 
			
		||||
            var services = BuildContainer(config);
 | 
			
		||||
            
 | 
			
		||||
            return RunWrapper(services, async ct =>
 | 
			
		||||
            {
 | 
			
		||||
                var logger = services.Resolve<ILogger>().ForContext<Init>();
 | 
			
		||||
                
 | 
			
		||||
                // Initialize Sentry SDK, and make sure it gets dropped at the end
 | 
			
		||||
                using var _ = Sentry.SentrySdk.Init(services.Resolve<CoreConfig>().SentryUrl);
 | 
			
		||||
 | 
			
		||||
                // "Connect to the database" (ie. set off database migrations and ensure state)
 | 
			
		||||
                logger.Information("Connecting to database");
 | 
			
		||||
                await services.Resolve<SchemaService>().ApplyMigrations();
 | 
			
		||||
                
 | 
			
		||||
                // Start the Discord client; StartAsync returns once shard instances are *created* (not necessarily connected)
 | 
			
		||||
                logger.Information("Connecting to Discord");
 | 
			
		||||
                await services.Resolve<DiscordShardedClient>().StartAsync();
 | 
			
		||||
                
 | 
			
		||||
                // Start the bot stuff and let it register things
 | 
			
		||||
                services.Resolve<Bot>().Init();
 | 
			
		||||
                
 | 
			
		||||
                // Lastly, we just... wait. Everything else is handled in the DiscordClient event loop
 | 
			
		||||
                await Task.Delay(-1, ct);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static async Task RunWrapper(IContainer services, Func<CancellationToken, Task> taskFunc)
 | 
			
		||||
        {
 | 
			
		||||
            // This function does a couple things: 
 | 
			
		||||
            // - Creates a CancellationToken that'll cancel tasks once we get a Ctrl-C / SIGINT
 | 
			
		||||
            // - Wraps the given function in an exception handler that properly logs errors
 | 
			
		||||
            var logger = services.Resolve<ILogger>().ForContext<Init>();
 | 
			
		||||
            
 | 
			
		||||
            var cts = new CancellationTokenSource();
 | 
			
		||||
            Console.CancelKeyPress += delegate { cts.Cancel(); };
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await taskFunc(cts.Token);
 | 
			
		||||
            }
 | 
			
		||||
            catch (TaskCanceledException e) when (e.CancellationToken == cts.Token)
 | 
			
		||||
            {
 | 
			
		||||
                // The CancellationToken we made got triggered - this is normal!
 | 
			
		||||
                // Therefore, exception handler is empty.
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception e)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Fatal(e, "Error while running bot");
 | 
			
		||||
                
 | 
			
		||||
                // Allow the log buffer to flush properly before exiting
 | 
			
		||||
                await Task.Delay(1000, cts.Token);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static IContainer BuildContainer(IConfiguration config)
 | 
			
		||||
        {
 | 
			
		||||
            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>();
 | 
			
		||||
            return builder.Build();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ using System.Net.Http;
 | 
			
		||||
using Autofac;
 | 
			
		||||
 | 
			
		||||
using DSharpPlus;
 | 
			
		||||
using DSharpPlus.EventArgs;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +49,10 @@ namespace PluralKit.Bot
 | 
			
		||||
            
 | 
			
		||||
            // Bot core
 | 
			
		||||
            builder.RegisterType<Bot>().AsSelf().SingleInstance();
 | 
			
		||||
            builder.RegisterType<PKEventHandler>().AsSelf();
 | 
			
		||||
            builder.RegisterType<MessageCreated>().As<IEventHandler<MessageCreateEventArgs>>();
 | 
			
		||||
            builder.RegisterType<MessageDeleted>().As<IEventHandler<MessageDeleteEventArgs>>().As<IEventHandler<MessageBulkDeleteEventArgs>>();
 | 
			
		||||
            builder.RegisterType<MessageEdited>().As<IEventHandler<MessageUpdateEventArgs>>();
 | 
			
		||||
            builder.RegisterType<ReactionAdded>().As<IEventHandler<MessageReactionAddEventArgs>>();
 | 
			
		||||
            
 | 
			
		||||
            // Bot services
 | 
			
		||||
            builder.RegisterType<EmbedService>().AsSelf().SingleInstance();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 | 
			
		||||
	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=commands/@EntryIndexedValue">True</s:Boolean>
 | 
			
		||||
	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=commandsystem/@EntryIndexedValue">True</s:Boolean>
 | 
			
		||||
	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=handlers/@EntryIndexedValue">True</s:Boolean>
 | 
			
		||||
	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services/@EntryIndexedValue">True</s:Boolean>
 | 
			
		||||
	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utils/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
 | 
			
		||||
@@ -23,7 +23,7 @@ namespace PluralKit.Bot
 | 
			
		||||
        public string InnerText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class ProxyService {
 | 
			
		||||
    public class ProxyService {
 | 
			
		||||
        private DiscordShardedClient _client;
 | 
			
		||||
        private LogChannelService _logChannel;
 | 
			
		||||
        private IDataStore _data;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user