More updates to event infrastructure
This commit is contained in:
		@@ -103,19 +103,10 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            async Task HandleEventInner()
 | 
					            async Task HandleEventInner()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                using var _ = LogContext.PushProperty("EventId", Guid.NewGuid());
 | 
					                using var _ = LogContext.PushProperty("EventId", Guid.NewGuid());
 | 
				
			||||||
 | 
					                _logger
 | 
				
			||||||
 | 
					                    .ForContext("Elastic", "yes?")
 | 
				
			||||||
 | 
					                    .Debug("Gateway event: {@Event}", evt);
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // Mainly for testing ELK volume atm, no-op unless Elastic is configured
 | 
					 | 
				
			||||||
                if (evt is MessageCreateEventArgs mc)
 | 
					 | 
				
			||||||
                    using (LogContext.PushProperty("Elastic", "yes?"))
 | 
					 | 
				
			||||||
                        _logger.Information("Received event {@Event}", new
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Type = mc.GetType().Name.Replace("EventArgs", ""),
 | 
					 | 
				
			||||||
                            MessageId = mc.Message.Id,
 | 
					 | 
				
			||||||
                            ChannelId = mc.Channel.Id,
 | 
					 | 
				
			||||||
                            GuildId = mc.Guild?.Id ?? 0,
 | 
					 | 
				
			||||||
                            UserId = mc.Author.Id,
 | 
					 | 
				
			||||||
                        });
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                await using var serviceScope = _services.BeginLifetimeScope();
 | 
					                await using var serviceScope = _services.BeginLifetimeScope();
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // Also, find a Sentry enricher for the event type (if one is present), and ask it to put some event data in the Sentry scope
 | 
					                // Also, find a Sentry enricher for the event type (if one is present), and ask it to put some event data in the Sentry scope
 | 
				
			||||||
@@ -146,7 +137,13 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            // Make this beforehand so we can access the event ID for logging
 | 
					            // Make this beforehand so we can access the event ID for logging
 | 
				
			||||||
            var sentryEvent = new SentryEvent(exc);
 | 
					            var sentryEvent = new SentryEvent(exc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _logger.Error(exc, "Exception in bot event handler (Sentry ID: {SentryEventId})", sentryEvent.EventId);
 | 
					            _logger
 | 
				
			||||||
 | 
					                .ForContext("Elastic", "yes?")
 | 
				
			||||||
 | 
					                .Error(exc, "Exception in event handler: {{SentryEventId}}", sentryEvent.EventId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If the event is us responding to our own error messages, don't bother logging
 | 
				
			||||||
 | 
					            if (evt is MessageCreateEventArgs mc && mc.Author.Id == _client.CurrentUser.Id)
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var shouldReport = exc.IsOurProblem();
 | 
					            var shouldReport = exc.IsOurProblem();
 | 
				
			||||||
            if (shouldReport)
 | 
					            if (shouldReport)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -116,7 +116,10 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            var builder = new ContainerBuilder();
 | 
					            var builder = new ContainerBuilder();
 | 
				
			||||||
            builder.RegisterInstance(config);
 | 
					            builder.RegisterInstance(config);
 | 
				
			||||||
            builder.RegisterModule(new ConfigModule<BotConfig>("Bot"));
 | 
					            builder.RegisterModule(new ConfigModule<BotConfig>("Bot"));
 | 
				
			||||||
            builder.RegisterModule(new LoggingModule("bot"));
 | 
					            builder.RegisterModule(new LoggingModule("bot", cfg =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cfg.Destructure.With<EventDestructuring>();
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
            builder.RegisterModule(new MetricsModule());
 | 
					            builder.RegisterModule(new MetricsModule());
 | 
				
			||||||
            builder.RegisterModule<DataStoreModule>();
 | 
					            builder.RegisterModule<DataStoreModule>();
 | 
				
			||||||
            builder.RegisterModule<BotModule>();
 | 
					            builder.RegisterModule<BotModule>();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public void OnError(Exception error) { }
 | 
					        public void OnError(Exception error) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string NormalizeRoutePath(string url)
 | 
					        private string NormalizeRoutePath(string url)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            url = Regex.Replace(url, @"/channels/\d{17,19}", "/channels/{channel_id}");
 | 
					            url = Regex.Replace(url, @"/channels/\d{17,19}", "/channels/{channel_id}");
 | 
				
			||||||
            url = Regex.Replace(url, @"/messages/\d{17,19}", "/messages/{message_id}");
 | 
					            url = Regex.Replace(url, @"/messages/\d{17,19}", "/messages/{message_id}");
 | 
				
			||||||
@@ -51,13 +51,25 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            return url;
 | 
					            return url;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task HandleResponse(HttpResponseMessage response, Activity activity)
 | 
					        private string Endpoint(HttpRequestMessage req)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var routePath = NormalizeRoutePath(req.RequestUri.LocalPath.Replace("/api/v7", ""));
 | 
				
			||||||
 | 
					            return $"{req.Method} {routePath}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        private void HandleException(Exception exc, HttpRequestMessage req)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _logger
 | 
				
			||||||
 | 
					                .ForContext("RequestUrlRoute", Endpoint(req))
 | 
				
			||||||
 | 
					                .Error(exc, "HTTP error: {RequestMethod} {RequestUrl}", req.Method, req.RequestUri);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private async Task HandleResponse(HttpResponseMessage response, Activity activity)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (response.RequestMessage.RequestUri.Host != "discord.com")
 | 
					            if (response.RequestMessage.RequestUri.Host != "discord.com")
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            var routePath = NormalizeRoutePath(response.RequestMessage.RequestUri.LocalPath.Replace("/api/v7", ""));
 | 
					            var endpoint = Endpoint(response.RequestMessage);
 | 
				
			||||||
            var route = $"{response.RequestMessage.Method} {routePath}";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using (LogContext.PushProperty("Elastic", "yes?"))
 | 
					            using (LogContext.PushProperty("Elastic", "yes?"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -67,10 +79,10 @@ namespace PluralKit.Bot
 | 
				
			|||||||
                    LogContext.PushProperty("ResponseBody", content);
 | 
					                    LogContext.PushProperty("ResponseBody", content);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                LogContext.PushProperty("RequestUrlRoute", route);
 | 
					                _logger
 | 
				
			||||||
                
 | 
					                    .ForContext("RequestUrlRoute", endpoint)
 | 
				
			||||||
                _logger.Information(
 | 
					                    .Information(
 | 
				
			||||||
                    "HTTP {RequestMethod} {RequestUrl} -> {ResponseStatusCode} {ResponseStatusString} (in {RequestDurationMs:F1} ms)",
 | 
					                    "HTTP: {RequestMethod} {RequestUrl} -> {ResponseStatusCode} {ResponseStatusString} (in {RequestDurationMs:F1} ms)",
 | 
				
			||||||
                    response.RequestMessage.Method,
 | 
					                    response.RequestMessage.Method,
 | 
				
			||||||
                    response.RequestMessage.RequestUri,
 | 
					                    response.RequestMessage.RequestUri,
 | 
				
			||||||
                    (int) response.StatusCode,
 | 
					                    (int) response.StatusCode,
 | 
				
			||||||
@@ -80,32 +92,55 @@ namespace PluralKit.Bot
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            var timer = _metrics.Provider.Timer.Instance(BotMetrics.DiscordApiRequests, new MetricTags(
 | 
					            var timer = _metrics.Provider.Timer.Instance(BotMetrics.DiscordApiRequests, new MetricTags(
 | 
				
			||||||
                new[] {"endpoint", "status_code"},
 | 
					                new[] {"endpoint", "status_code"},
 | 
				
			||||||
                new[] {route, ((int) response.StatusCode).ToString()}
 | 
					                new[] {endpoint, ((int) response.StatusCode).ToString()}
 | 
				
			||||||
            ));
 | 
					            ));
 | 
				
			||||||
            timer.Record(activity.Duration.Ticks / 10, TimeUnit.Microseconds);
 | 
					            timer.Record(activity.Duration.Ticks / 10, TimeUnit.Microseconds);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public void OnNext(KeyValuePair<string, object> value)
 | 
					        public void OnNext(KeyValuePair<string, object> value)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (value.Key == "System.Net.Http.HttpRequestOut.Stop")
 | 
					            switch (value.Key)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var data = Unsafe.As<TypedData>(value.Value);
 | 
					                case "System.Net.Http.HttpRequestOut.Stop":
 | 
				
			||||||
                var _ = HandleResponse(data.Response, Activity.Current);
 | 
					                {
 | 
				
			||||||
 | 
					                    var data = Unsafe.As<ActivityStopData>(value.Value);
 | 
				
			||||||
 | 
					                    if (data.Response != null)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        var _ = HandleResponse(data.Response, Activity.Current);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                case "System.Net.Http.Exception":
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    var data = Unsafe.As<ExceptionData>(value.Value);
 | 
				
			||||||
 | 
					                    HandleException(data.Exception, data.Request);
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static void Install(IComponentContext services)
 | 
					        public static void Install(IComponentContext services)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            DiagnosticListener.AllListeners.Subscribe(new ListenerObserver(services));
 | 
					            DiagnosticListener.AllListeners.Subscribe(new ListenerObserver(services));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        private class TypedData
 | 
					#pragma warning disable 649
 | 
				
			||||||
 | 
					        private class ActivityStopData
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Field order here matters!
 | 
					            // Field order here matters!
 | 
				
			||||||
            public HttpResponseMessage Response;
 | 
					            public HttpResponseMessage Response;
 | 
				
			||||||
            public HttpRequestMessage Request;
 | 
					            public HttpRequestMessage Request;
 | 
				
			||||||
            public TaskStatus RequestTaskStatus;
 | 
					            public TaskStatus RequestTaskStatus;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        private class ExceptionData
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Field order here matters!
 | 
				
			||||||
 | 
					            public Exception Exception;
 | 
				
			||||||
 | 
					            public HttpRequestMessage Request;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					#pragma warning restore 649
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public class ListenerObserver: IObserver<DiagnosticListener>
 | 
					        public class ListenerObserver: IObserver<DiagnosticListener>
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								PluralKit.Bot/Tracing/EventDestructuring.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								PluralKit.Bot/Tracing/EventDestructuring.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using DSharpPlus.Entities;
 | 
				
			||||||
 | 
					using DSharpPlus.EventArgs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using Serilog.Core;
 | 
				
			||||||
 | 
					using Serilog.Events;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace PluralKit.Bot
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public class EventDestructuring: IDestructuringPolicy
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory,
 | 
				
			||||||
 | 
					                                   out LogEventPropertyValue result)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (!(value is DiscordEventArgs dea))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                result = null;
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var props = new List<LogEventProperty>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                new LogEventProperty("Type", new ScalarValue(dea.EventType())),
 | 
				
			||||||
 | 
					                new LogEventProperty("Shard", new ScalarValue(dea.Client.ShardId))
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            void AddMessage(DiscordMessage msg)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                props.Add(new LogEventProperty("MessageId", new ScalarValue(msg.Id)));
 | 
				
			||||||
 | 
					                props.Add(new LogEventProperty("ChannelId", new ScalarValue(msg.ChannelId)));
 | 
				
			||||||
 | 
					                props.Add(new LogEventProperty("GuildId", new ScalarValue(msg.Channel.GuildId)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (msg.Author != null)
 | 
				
			||||||
 | 
					                    props.Add(new LogEventProperty("AuthorId", new ScalarValue(msg.Author.Id)));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (value is MessageCreateEventArgs mc)
 | 
				
			||||||
 | 
					                AddMessage(mc.Message);
 | 
				
			||||||
 | 
					            else if (value is MessageUpdateEventArgs mu)
 | 
				
			||||||
 | 
					                AddMessage(mu.Message);
 | 
				
			||||||
 | 
					            else if (value is MessageDeleteEventArgs md)
 | 
				
			||||||
 | 
					                AddMessage(md.Message);
 | 
				
			||||||
 | 
					            else if (value is MessageReactionAddEventArgs mra)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                AddMessage(mra.Message);
 | 
				
			||||||
 | 
					                props.Add(new LogEventProperty("ReactingUserId", new ScalarValue(mra.User.Id)));
 | 
				
			||||||
 | 
					                props.Add(new LogEventProperty("Emoji", new ScalarValue(mra.Emoji.GetDiscordName())));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            result = new StructureValue(props);
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
using DSharpPlus;
 | 
					using DSharpPlus;
 | 
				
			||||||
using DSharpPlus.Entities;
 | 
					using DSharpPlus.Entities;
 | 
				
			||||||
 | 
					using DSharpPlus.EventArgs;
 | 
				
			||||||
using DSharpPlus.Exceptions;
 | 
					using DSharpPlus.Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
@@ -23,13 +24,13 @@ namespace PluralKit.Bot
 | 
				
			|||||||
        public static DiscordColor Green = new DiscordColor(0x00cc78);
 | 
					        public static DiscordColor Green = new DiscordColor(0x00cc78);
 | 
				
			||||||
        public static DiscordColor Red = new DiscordColor(0xef4b3d);
 | 
					        public static DiscordColor Red = new DiscordColor(0xef4b3d);
 | 
				
			||||||
        public static DiscordColor Gray = new DiscordColor(0x979c9f);
 | 
					        public static DiscordColor Gray = new DiscordColor(0x979c9f);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Permissions DM_PERMISSIONS = (Permissions) 0b00000_1000110_1011100110000_000000;
 | 
					        public static Permissions DM_PERMISSIONS = (Permissions) 0b00000_1000110_1011100110000_000000;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        private static readonly Regex USER_MENTION = new Regex("<@!?(\\d{17,19})>");
 | 
					        private static readonly Regex USER_MENTION = new Regex("<@!?(\\d{17,19})>");
 | 
				
			||||||
        private static readonly Regex ROLE_MENTION = new Regex("<@&(\\d{17,19})>");
 | 
					        private static readonly Regex ROLE_MENTION = new Regex("<@&(\\d{17,19})>");
 | 
				
			||||||
        private static readonly Regex EVERYONE_HERE_MENTION = new Regex("@(everyone|here)");
 | 
					        private static readonly Regex EVERYONE_HERE_MENTION = new Regex("@(everyone|here)");
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Discord uses Khan Academy's simple-markdown library for parsing Markdown,
 | 
					        // Discord uses Khan Academy's simple-markdown library for parsing Markdown,
 | 
				
			||||||
        // which uses the following regex for link detection: 
 | 
					        // which uses the following regex for link detection: 
 | 
				
			||||||
        // ^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])
 | 
					        // ^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])
 | 
				
			||||||
@@ -38,9 +39,11 @@ namespace PluralKit.Bot
 | 
				
			|||||||
        // I added <? and >? at the start/end; they need to be handled specially later...
 | 
					        // I added <? and >? at the start/end; they need to be handled specially later...
 | 
				
			||||||
        private static readonly Regex UNBROKEN_LINK_REGEX = new Regex("<?(https?:\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])>?");
 | 
					        private static readonly Regex UNBROKEN_LINK_REGEX = new Regex("<?(https?:\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])>?");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static readonly FieldInfo _roleIdsField = typeof(DiscordMember).GetField("_role_ids", BindingFlags.NonPublic | BindingFlags.Instance);
 | 
					        private static readonly FieldInfo _roleIdsField =
 | 
				
			||||||
 | 
					            typeof(DiscordMember).GetField("_role_ids", BindingFlags.NonPublic | BindingFlags.Instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static string NameAndMention(this DiscordUser user) {
 | 
					        public static string NameAndMention(this DiscordUser user)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            return $"{user.Username}#{user.Discriminator} ({user.Mention})";
 | 
					            return $"{user.Username}#{user.Discriminator} ({user.Mention})";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,12 +53,12 @@ namespace PluralKit.Bot
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            ValidateCachedRoles(member);
 | 
					            ValidateCachedRoles(member);
 | 
				
			||||||
            var permissions = channel.PermissionsFor(member);
 | 
					            var permissions = channel.PermissionsFor(member);
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // This method doesn't account for channels without read permissions
 | 
					            // This method doesn't account for channels without read permissions
 | 
				
			||||||
            // If we don't have read permissions in the channel, we don't have *any* permissions
 | 
					            // If we don't have read permissions in the channel, we don't have *any* permissions
 | 
				
			||||||
            if ((permissions & Permissions.AccessChannels) != Permissions.AccessChannels)
 | 
					            if ((permissions & Permissions.AccessChannels) != Permissions.AccessChannels)
 | 
				
			||||||
                return Permissions.None;
 | 
					                return Permissions.None;
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            return permissions;
 | 
					            return permissions;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,16 +81,16 @@ namespace PluralKit.Bot
 | 
				
			|||||||
                if (member != null)
 | 
					                if (member != null)
 | 
				
			||||||
                    return PermissionsInSync(channel, member);
 | 
					                    return PermissionsInSync(channel, member);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            return PermissionsInSync(channel, user);
 | 
					            return PermissionsInSync(channel, user);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Same as PermissionsIn, but always synchronous. DiscordUser must be a DiscordMember if channel is in guild.
 | 
					        // Same as PermissionsIn, but always synchronous. DiscordUser must be a DiscordMember if channel is in guild.
 | 
				
			||||||
        public static Permissions PermissionsInSync(this DiscordChannel channel, DiscordUser user)
 | 
					        public static Permissions PermissionsInSync(this DiscordChannel channel, DiscordUser user)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (channel.Guild != null && !(user is DiscordMember))
 | 
					            if (channel.Guild != null && !(user is DiscordMember))
 | 
				
			||||||
                throw new ArgumentException("Function was passed a guild channel but a non-member DiscordUser");
 | 
					                throw new ArgumentException("Function was passed a guild channel but a non-member DiscordUser");
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            if (user is DiscordMember m) return PermissionsInGuild(channel, m);
 | 
					            if (user is DiscordMember m) return PermissionsInGuild(channel, m);
 | 
				
			||||||
            if (channel.Type == ChannelType.Private) return DM_PERMISSIONS;
 | 
					            if (channel.Type == ChannelType.Private) return DM_PERMISSIONS;
 | 
				
			||||||
            return Permissions.None;
 | 
					            return Permissions.None;
 | 
				
			||||||
@@ -106,7 +109,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
        public static bool BotHasAllPermissions(this DiscordChannel channel, Permissions permissionSet) =>
 | 
					        public static bool BotHasAllPermissions(this DiscordChannel channel, Permissions permissionSet) =>
 | 
				
			||||||
            (BotPermissions(channel) & permissionSet) == permissionSet;
 | 
					            (BotPermissions(channel) & permissionSet) == permissionSet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Instant SnowflakeToInstant(ulong snowflake) => 
 | 
					        public static Instant SnowflakeToInstant(ulong snowflake) =>
 | 
				
			||||||
            Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
 | 
					            Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static ulong InstantToSnowflake(Instant time) =>
 | 
					        public static ulong InstantToSnowflake(Instant time) =>
 | 
				
			||||||
@@ -128,42 +131,46 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            // Workaround for https://github.com/DSharpPlus/DSharpPlus/issues/565
 | 
					            // Workaround for https://github.com/DSharpPlus/DSharpPlus/issues/565
 | 
				
			||||||
            return input?.Replace("%20", "+");
 | 
					            return input?.Replace("%20", "+");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Task<DiscordMessage> SendMessageFixedAsync(this DiscordChannel channel, string content = null, DiscordEmbed embed = null, IEnumerable<IMention> mentions = null) =>
 | 
					        public static Task<DiscordMessage> SendMessageFixedAsync(this DiscordChannel channel, string content = null,
 | 
				
			||||||
 | 
					                                                                 DiscordEmbed embed = null,
 | 
				
			||||||
 | 
					                                                                 IEnumerable<IMention> mentions = null) =>
 | 
				
			||||||
            // Passing an empty list blocks all mentions by default (null allows all through)
 | 
					            // Passing an empty list blocks all mentions by default (null allows all through)
 | 
				
			||||||
            channel.SendMessageAsync(content, embed: embed, mentions: mentions ?? new IMention[0]);
 | 
					            channel.SendMessageAsync(content, embed: embed, mentions: mentions ?? new IMention[0]);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // This doesn't do anything by itself (DiscordMember.SendMessageAsync doesn't take a mentions argument)
 | 
					        // This doesn't do anything by itself (DiscordMember.SendMessageAsync doesn't take a mentions argument)
 | 
				
			||||||
        // It's just here for consistency so we don't use the standard SendMessageAsync method >.>
 | 
					        // It's just here for consistency so we don't use the standard SendMessageAsync method >.>
 | 
				
			||||||
        public static Task<DiscordMessage> SendMessageFixedAsync(this DiscordMember member, string content = null, DiscordEmbed embed = null) =>
 | 
					        public static Task<DiscordMessage> SendMessageFixedAsync(this DiscordMember member, string content = null,
 | 
				
			||||||
 | 
					                                                                 DiscordEmbed embed = null) =>
 | 
				
			||||||
            member.SendMessageAsync(content, embed: embed);
 | 
					            member.SendMessageAsync(content, embed: embed);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static bool TryGetCachedUser(this DiscordClient client, ulong id, out DiscordUser user)
 | 
					        public static bool TryGetCachedUser(this DiscordClient client, ulong id, out DiscordUser user)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            user = null;
 | 
					            user = null;
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            var cache = (ConcurrentDictionary<ulong, DiscordUser>) typeof(BaseDiscordClient)
 | 
					            var cache = (ConcurrentDictionary<ulong, DiscordUser>) typeof(BaseDiscordClient)
 | 
				
			||||||
                .GetProperty("UserCache", BindingFlags.Instance | BindingFlags.NonPublic)
 | 
					                .GetProperty("UserCache", BindingFlags.Instance | BindingFlags.NonPublic)
 | 
				
			||||||
                ?.GetValue(client);
 | 
					                ?.GetValue(client);
 | 
				
			||||||
            return cache != null && cache.TryGetValue(id, out user);
 | 
					            return cache != null && cache.TryGetValue(id, out user);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static DiscordColor? ToDiscordColor(this string color)
 | 
					        public static DiscordColor? ToDiscordColor(this string color)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (int.TryParse(color, NumberStyles.HexNumber, null, out var colorInt))
 | 
					            if (int.TryParse(color, NumberStyles.HexNumber, null, out var colorInt))
 | 
				
			||||||
                return new DiscordColor(colorInt);
 | 
					                return new DiscordColor(colorInt);
 | 
				
			||||||
            throw new ArgumentException($"Invalid color string '{color}'.");
 | 
					            throw new ArgumentException($"Invalid color string '{color}'.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId)
 | 
					        public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            mentionId = 0;
 | 
					            mentionId = 0;
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix
 | 
					            // Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix
 | 
				
			||||||
            if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@'))
 | 
					            if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@'))
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            int num = content.IndexOf('>');
 | 
					            int num = content.IndexOf('>');
 | 
				
			||||||
            if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !TryParseMention(content.Substring(0, num + 1), out mentionId))
 | 
					            if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' ||
 | 
				
			||||||
 | 
					                !TryParseMention(content.Substring(0, num + 1), out mentionId))
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            argPos = num + 2;
 | 
					            argPos = num + 2;
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
@@ -174,7 +181,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            if (ulong.TryParse(potentialMention, out id)) return true;
 | 
					            if (ulong.TryParse(potentialMention, out id)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var match = USER_MENTION.Match(potentialMention);
 | 
					            var match = USER_MENTION.Match(potentialMention);
 | 
				
			||||||
            if (match.Success && match.Index == 0 && match.Length == potentialMention.Length) 
 | 
					            if (match.Success && match.Index == 0 && match.Length == potentialMention.Length)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                id = ulong.Parse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture);
 | 
					                id = ulong.Parse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture);
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
@@ -183,12 +190,13 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static IEnumerable<IMention> ParseAllMentions(this string input, bool allowEveryone = false, DiscordGuild guild = null)
 | 
					        public static IEnumerable<IMention> ParseAllMentions(this string input, bool allowEveryone = false,
 | 
				
			||||||
 | 
					                                                             DiscordGuild guild = null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var mentions = new List<IMention>();
 | 
					            var mentions = new List<IMention>();
 | 
				
			||||||
            mentions.AddRange(USER_MENTION.Matches(input)
 | 
					            mentions.AddRange(USER_MENTION.Matches(input)
 | 
				
			||||||
                .Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention));
 | 
					                .Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention));
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // Only allow role mentions through where the role is actually listed as *mentionable*
 | 
					            // Only allow role mentions through where the role is actually listed as *mentionable*
 | 
				
			||||||
            // (ie. any user can @ them, regardless of permissions)
 | 
					            // (ie. any user can @ them, regardless of permissions)
 | 
				
			||||||
            // Still let the allowEveryone flag override this though (privileged users can @ *any* role)
 | 
					            // Still let the allowEveryone flag override this though (privileged users can @ *any* role)
 | 
				
			||||||
@@ -213,7 +221,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            if (input == null)
 | 
					            if (input == null)
 | 
				
			||||||
                return null;
 | 
					                return null;
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // Break all pairs of backticks by placing a ZWNBSP (U+FEFF) between them.
 | 
					            // Break all pairs of backticks by placing a ZWNBSP (U+FEFF) between them.
 | 
				
			||||||
            // Run twice to catch any pairs that are created from the first pass
 | 
					            // Run twice to catch any pairs that are created from the first pass
 | 
				
			||||||
            var escaped = input
 | 
					            var escaped = input
 | 
				
			||||||
@@ -223,7 +231,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            // Escape the start/end of the string if necessary to better "connect" with other things
 | 
					            // Escape the start/end of the string if necessary to better "connect" with other things
 | 
				
			||||||
            if (escaped.StartsWith("`")) escaped = "\ufeff" + escaped;
 | 
					            if (escaped.StartsWith("`")) escaped = "\ufeff" + escaped;
 | 
				
			||||||
            if (escaped.EndsWith("`")) escaped = escaped + "\ufeff";
 | 
					            if (escaped.EndsWith("`")) escaped = escaped + "\ufeff";
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            return escaped;
 | 
					            return escaped;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -234,38 +242,41 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            return $"``{EscapeBacktickPair(input)}``";
 | 
					            return $"``{EscapeBacktickPair(input)}``";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Task<DiscordUser> GetUser(this DiscordRestClient client, ulong id) => 
 | 
					        public static Task<DiscordUser> GetUser(this DiscordRestClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetUserAsync(id));
 | 
					            WrapDiscordCall(client.GetUserAsync(id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Task<DiscordUser> GetUser(this DiscordClient client, ulong id) => 
 | 
					        public static Task<DiscordUser> GetUser(this DiscordClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetUserAsync(id));
 | 
					            WrapDiscordCall(client.GetUserAsync(id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Task<DiscordChannel> GetChannel(this DiscordRestClient client, ulong id) => 
 | 
					        public static Task<DiscordChannel> GetChannel(this DiscordRestClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetChannelAsync(id));
 | 
					            WrapDiscordCall(client.GetChannelAsync(id));
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Task<DiscordChannel> GetChannel(this DiscordClient client, ulong id) => 
 | 
					        public static Task<DiscordChannel> GetChannel(this DiscordClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetChannelAsync(id));
 | 
					            WrapDiscordCall(client.GetChannelAsync(id));
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Task<DiscordGuild> GetGuild(this DiscordRestClient client, ulong id) => 
 | 
					        public static Task<DiscordGuild> GetGuild(this DiscordRestClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetGuildAsync(id));
 | 
					            WrapDiscordCall(client.GetGuildAsync(id));
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Task<DiscordGuild> GetGuild(this DiscordClient client, ulong id) => 
 | 
					        public static Task<DiscordGuild> GetGuild(this DiscordClient client, ulong id) =>
 | 
				
			||||||
            WrapDiscordCall(client.GetGuildAsync(id));
 | 
					            WrapDiscordCall(client.GetGuildAsync(id));
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        public static Task<DiscordMember> GetMember(this DiscordRestClient client, ulong guild, ulong user)
 | 
					        public static Task<DiscordMember> GetMember(this DiscordRestClient client, ulong guild, ulong user)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            async Task<DiscordMember> Inner() => 
 | 
					            async Task<DiscordMember> Inner() =>
 | 
				
			||||||
                await (await client.GetGuildAsync(guild)).GetMemberAsync(user);
 | 
					 | 
				
			||||||
            return WrapDiscordCall(Inner());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        public static Task<DiscordMember> GetMember(this DiscordClient client, ulong guild, ulong user)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            async Task<DiscordMember> Inner() => 
 | 
					 | 
				
			||||||
                await (await client.GetGuildAsync(guild)).GetMemberAsync(user);
 | 
					                await (await client.GetGuildAsync(guild)).GetMemberAsync(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return WrapDiscordCall(Inner());
 | 
					            return WrapDiscordCall(Inner());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Task<DiscordMember> GetMember(this DiscordGuild guild, ulong user) => 
 | 
					        public static Task<DiscordMember> GetMember(this DiscordClient client, ulong guild, ulong user)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            async Task<DiscordMember> Inner() =>
 | 
				
			||||||
 | 
					                await (await client.GetGuildAsync(guild)).GetMemberAsync(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return WrapDiscordCall(Inner());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static Task<DiscordMember> GetMember(this DiscordGuild guild, ulong user) =>
 | 
				
			||||||
            WrapDiscordCall(guild.GetMemberAsync(user));
 | 
					            WrapDiscordCall(guild.GetMemberAsync(user));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Task<DiscordMessage> GetMessage(this DiscordChannel channel, ulong id) =>
 | 
					        public static Task<DiscordMessage> GetMessage(this DiscordChannel channel, ulong id) =>
 | 
				
			||||||
@@ -282,17 +293,21 @@ namespace PluralKit.Bot
 | 
				
			|||||||
                shard.Guilds.TryGetValue(id, out guild);
 | 
					                shard.Guilds.TryGetValue(id, out guild);
 | 
				
			||||||
                if (guild != null) return guild;
 | 
					                if (guild != null) return guild;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static async Task<DiscordChannel> GetChannel(this DiscordShardedClient client, ulong id, ulong? guildId = null)
 | 
					        public static async Task<DiscordChannel> GetChannel(this DiscordShardedClient client, ulong id,
 | 
				
			||||||
 | 
					                                                            ulong? guildId = null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // we need to know the channel's guild ID to get the cached guild object, so we grab it from the API
 | 
					            // we need to know the channel's guild ID to get the cached guild object, so we grab it from the API
 | 
				
			||||||
            if (guildId == null) {
 | 
					            if (guildId == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
                var channel = await WrapDiscordCall(client.ShardClients.Values.FirstOrDefault().GetChannelAsync(id));
 | 
					                var channel = await WrapDiscordCall(client.ShardClients.Values.FirstOrDefault().GetChannelAsync(id));
 | 
				
			||||||
                if (channel != null) guildId = channel.GuildId;
 | 
					                if (channel != null) guildId = channel.GuildId;
 | 
				
			||||||
                else return null; // we probably don't have the guild in cache if the API doesn't give it to us
 | 
					                else return null; // we probably don't have the guild in cache if the API doesn't give it to us
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return client.GetGuild(guildId.Value).GetChannel(id);
 | 
					            return client.GetGuild(guildId.Value).GetChannel(id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -325,7 +340,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
            // Add the first page to the embed description
 | 
					            // Add the first page to the embed description
 | 
				
			||||||
            if (pages.Count > 0)
 | 
					            if (pages.Count > 0)
 | 
				
			||||||
                eb.WithDescription(pages[0]);
 | 
					                eb.WithDescription(pages[0]);
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // Add the rest to blank-named (\u200B) fields
 | 
					            // Add the rest to blank-named (\u200B) fields
 | 
				
			||||||
            for (var i = 1; i < pages.Count; i++)
 | 
					            for (var i = 1; i < pages.Count; i++)
 | 
				
			||||||
                eb.AddField("\u200B", pages[i]);
 | 
					                eb.AddField("\u200B", pages[i]);
 | 
				
			||||||
@@ -343,5 +358,8 @@ namespace PluralKit.Bot
 | 
				
			|||||||
                    return match.Value;
 | 
					                    return match.Value;
 | 
				
			||||||
                return $"<{match.Value}>";
 | 
					                return $"<{match.Value}>";
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static string EventType(this DiscordEventArgs evt) => 
 | 
				
			||||||
 | 
					            evt.GetType().Name.Replace("EventArgs", "");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,10 +82,12 @@ namespace PluralKit.Core
 | 
				
			|||||||
    public class LoggingModule: Module
 | 
					    public class LoggingModule: Module
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly string _component;
 | 
					        private readonly string _component;
 | 
				
			||||||
 | 
					        private readonly Action<LoggerConfiguration> _fn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public LoggingModule(string component)
 | 
					        public LoggingModule(string component, Action<LoggerConfiguration> fn = null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _component = component;
 | 
					            _component = component;
 | 
				
			||||||
 | 
					            _fn = fn ?? (_ => { });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override void Load(ContainerBuilder builder)
 | 
					        protected override void Load(ContainerBuilder builder)
 | 
				
			||||||
@@ -143,6 +145,7 @@ namespace PluralKit.Core
 | 
				
			|||||||
                        c => c.Elasticsearch(elasticConfig));
 | 
					                        c => c.Elasticsearch(elasticConfig));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _fn.Invoke(logger);
 | 
				
			||||||
            return logger.CreateLogger();
 | 
					            return logger.CreateLogger();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user