From a6fbd869be4e9dd931cb48f4970f697f516030e5 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 22 Dec 2020 13:15:26 +0100 Subject: [PATCH 001/608] Initial commit, basic proxying working --- Myriad/Cache/DiscordCacheExtensions.cs | 50 +++ Myriad/Cache/IDiscordCache.cs | 28 ++ Myriad/Cache/MemoryDiscordCache.cs | 143 ++++++++ Myriad/Extensions/ChannelExtensions.cs | 7 + Myriad/Extensions/MessageExtensions.cs | 7 + Myriad/Extensions/PermissionExtensions.cs | 126 +++++++ Myriad/Extensions/UserExtensions.cs | 10 + Myriad/Gateway/Cluster.cs | 88 +++++ Myriad/Gateway/ClusterSessionState.cs | 15 + Myriad/Gateway/Events/ChannelCreateEvent.cs | 6 + Myriad/Gateway/Events/ChannelDeleteEvent.cs | 6 + Myriad/Gateway/Events/ChannelUpdateEvent.cs | 6 + Myriad/Gateway/Events/GuildCreateEvent.cs | 12 + Myriad/Gateway/Events/GuildDeleteEvent.cs | 4 + Myriad/Gateway/Events/GuildMemberAddEvent.cs | 9 + .../Gateway/Events/GuildMemberRemoveEvent.cs | 10 + .../Gateway/Events/GuildMemberUpdateEvent.cs | 9 + Myriad/Gateway/Events/GuildRoleCreateEvent.cs | 6 + Myriad/Gateway/Events/GuildRoleDeleteEvent.cs | 4 + Myriad/Gateway/Events/GuildRoleUpdateEvent.cs | 6 + Myriad/Gateway/Events/GuildUpdateEvent.cs | 6 + Myriad/Gateway/Events/IGatewayEvent.cs | 35 ++ .../Gateway/Events/InteractionCreateEvent.cs | 6 + Myriad/Gateway/Events/MessageCreateEvent.cs | 9 + .../Gateway/Events/MessageDeleteBulkEvent.cs | 4 + Myriad/Gateway/Events/MessageDeleteEvent.cs | 4 + .../Gateway/Events/MessageReactionAddEvent.cs | 8 + .../Events/MessageReactionRemoveAllEvent.cs | 4 + .../Events/MessageReactionRemoveEmojiEvent.cs | 7 + .../Events/MessageReactionRemoveEvent.cs | 7 + Myriad/Gateway/Events/MessageUpdateEvent.cs | 7 + Myriad/Gateway/Events/ReadyEvent.cs | 15 + Myriad/Gateway/Events/ResumedEvent.cs | 4 + Myriad/Gateway/GatewayCloseException.cs | 35 ++ Myriad/Gateway/GatewayIntent.cs | 24 ++ Myriad/Gateway/GatewayPacket.cs | 31 ++ Myriad/Gateway/GatewaySettings.cs | 8 + Myriad/Gateway/Payloads/GatewayHello.cs | 4 + Myriad/Gateway/Payloads/GatewayIdentify.cs | 28 ++ Myriad/Gateway/Payloads/GatewayResume.cs | 4 + .../Gateway/Payloads/GatewayStatusUpdate.cs | 23 ++ Myriad/Gateway/Shard.cs | 328 ++++++++++++++++++ Myriad/Gateway/ShardConnection.cs | 118 +++++++ Myriad/Gateway/ShardInfo.cs | 4 + Myriad/Gateway/ShardSessionInfo.cs | 8 + Myriad/Myriad.csproj | 19 + Myriad/Rest/BaseRestClient.cs | 240 +++++++++++++ Myriad/Rest/DiscordApiClient.cs | 120 +++++++ Myriad/Rest/DiscordApiError.cs | 9 + .../Exceptions/DiscordRequestException.cs | 71 ++++ Myriad/Rest/Exceptions/RatelimitException.cs | 29 ++ Myriad/Rest/Ratelimit/Bucket.cs | 152 ++++++++ Myriad/Rest/Ratelimit/BucketManager.cs | 79 +++++ .../Rest/Ratelimit/DiscordRateLimitPolicy.cs | 46 +++ Myriad/Rest/Ratelimit/RatelimitHeaders.cs | 46 +++ Myriad/Rest/Ratelimit/Ratelimiter.cs | 86 +++++ Myriad/Rest/Types/AllowedMentions.cs | 19 + Myriad/Rest/Types/MultipartFile.cs | 6 + Myriad/Rest/Types/Requests/CommandRequest.cs | 13 + .../Types/Requests/CreateWebhookRequest.cs | 4 + .../Types/Requests/ExecuteWebhookRequest.cs | 13 + .../Rest/Types/Requests/MessageEditRequest.cs | 10 + Myriad/Rest/Types/Requests/MessageRequest.cs | 13 + .../Requests/ModifyGuildMemberRequest.cs | 7 + .../JsonSerializerOptionsExtensions.cs | 20 ++ .../JsonSnakeCaseNamingPolicy.cs | 88 +++++ Myriad/Serialization/JsonStringConverter.cs | 22 ++ .../PermissionSetJsonConverter.cs | 24 ++ .../Serialization/ShardInfoJsonConverter.cs | 28 ++ Myriad/Types/Activity.cs | 22 ++ Myriad/Types/Application/Application.cs | 27 ++ .../Types/Application/ApplicationCommand.cs | 13 + .../ApplicationCommandInteractionData.cs | 9 + ...ApplicationCommandInteractionDataOption.cs | 9 + .../Application/ApplicationCommandOption.cs | 24 ++ Myriad/Types/Application/Interaction.cs | 19 + ...teractionApplicationCommandCallbackData.cs | 15 + .../Types/Application/InteractionResponse.cs | 17 + Myriad/Types/Channel.cs | 40 +++ Myriad/Types/Embed.cs | 64 ++++ Myriad/Types/Emoji.cs | 9 + Myriad/Types/Gateway/GatewayInfo.cs | 13 + Myriad/Types/Gateway/SessionStartLimit.cs | 9 + Myriad/Types/Guild.cs | 24 ++ Myriad/Types/GuildMember.cs | 14 + Myriad/Types/Message.cs | 85 +++++ Myriad/Types/PermissionSet.cs | 47 +++ Myriad/Types/Permissions.cs | 6 + Myriad/Types/Role.cs | 14 + Myriad/Types/User.cs | 38 ++ Myriad/Types/Webhook.cs | 21 ++ PluralKit.Bot/Bot.cs | 171 ++++++--- PluralKit.Bot/CommandSystem/Context.cs | 41 ++- PluralKit.Bot/Handlers/IEventHandler.cs | 10 +- PluralKit.Bot/Handlers/MessageCreated.cs | 87 +++-- PluralKit.Bot/Handlers/MessageDeleted.cs | 18 +- PluralKit.Bot/Handlers/MessageEdited.cs | 36 +- PluralKit.Bot/Handlers/ReactionAdded.cs | 8 +- PluralKit.Bot/Init.cs | 8 +- PluralKit.Bot/Modules.cs | 44 ++- PluralKit.Bot/PluralKit.Bot.csproj | 1 + PluralKit.Bot/Proxy/ProxyService.cs | 145 ++++---- PluralKit.Bot/Services/LogChannelService.cs | 61 ++-- PluralKit.Bot/Services/LoggerCleanService.cs | 10 +- PluralKit.Bot/Services/WebhookCacheService.cs | 95 ++--- .../Services/WebhookExecutorService.cs | 123 ++++--- PluralKit.Bot/Utils/DiscordUtils.cs | 9 +- PluralKit.Bot/Utils/SentryUtils.cs | 39 ++- PluralKit.sln | 6 + 109 files changed, 3539 insertions(+), 359 deletions(-) create mode 100644 Myriad/Cache/DiscordCacheExtensions.cs create mode 100644 Myriad/Cache/IDiscordCache.cs create mode 100644 Myriad/Cache/MemoryDiscordCache.cs create mode 100644 Myriad/Extensions/ChannelExtensions.cs create mode 100644 Myriad/Extensions/MessageExtensions.cs create mode 100644 Myriad/Extensions/PermissionExtensions.cs create mode 100644 Myriad/Extensions/UserExtensions.cs create mode 100644 Myriad/Gateway/Cluster.cs create mode 100644 Myriad/Gateway/ClusterSessionState.cs create mode 100644 Myriad/Gateway/Events/ChannelCreateEvent.cs create mode 100644 Myriad/Gateway/Events/ChannelDeleteEvent.cs create mode 100644 Myriad/Gateway/Events/ChannelUpdateEvent.cs create mode 100644 Myriad/Gateway/Events/GuildCreateEvent.cs create mode 100644 Myriad/Gateway/Events/GuildDeleteEvent.cs create mode 100644 Myriad/Gateway/Events/GuildMemberAddEvent.cs create mode 100644 Myriad/Gateway/Events/GuildMemberRemoveEvent.cs create mode 100644 Myriad/Gateway/Events/GuildMemberUpdateEvent.cs create mode 100644 Myriad/Gateway/Events/GuildRoleCreateEvent.cs create mode 100644 Myriad/Gateway/Events/GuildRoleDeleteEvent.cs create mode 100644 Myriad/Gateway/Events/GuildRoleUpdateEvent.cs create mode 100644 Myriad/Gateway/Events/GuildUpdateEvent.cs create mode 100644 Myriad/Gateway/Events/IGatewayEvent.cs create mode 100644 Myriad/Gateway/Events/InteractionCreateEvent.cs create mode 100644 Myriad/Gateway/Events/MessageCreateEvent.cs create mode 100644 Myriad/Gateway/Events/MessageDeleteBulkEvent.cs create mode 100644 Myriad/Gateway/Events/MessageDeleteEvent.cs create mode 100644 Myriad/Gateway/Events/MessageReactionAddEvent.cs create mode 100644 Myriad/Gateway/Events/MessageReactionRemoveAllEvent.cs create mode 100644 Myriad/Gateway/Events/MessageReactionRemoveEmojiEvent.cs create mode 100644 Myriad/Gateway/Events/MessageReactionRemoveEvent.cs create mode 100644 Myriad/Gateway/Events/MessageUpdateEvent.cs create mode 100644 Myriad/Gateway/Events/ReadyEvent.cs create mode 100644 Myriad/Gateway/Events/ResumedEvent.cs create mode 100644 Myriad/Gateway/GatewayCloseException.cs create mode 100644 Myriad/Gateway/GatewayIntent.cs create mode 100644 Myriad/Gateway/GatewayPacket.cs create mode 100644 Myriad/Gateway/GatewaySettings.cs create mode 100644 Myriad/Gateway/Payloads/GatewayHello.cs create mode 100644 Myriad/Gateway/Payloads/GatewayIdentify.cs create mode 100644 Myriad/Gateway/Payloads/GatewayResume.cs create mode 100644 Myriad/Gateway/Payloads/GatewayStatusUpdate.cs create mode 100644 Myriad/Gateway/Shard.cs create mode 100644 Myriad/Gateway/ShardConnection.cs create mode 100644 Myriad/Gateway/ShardInfo.cs create mode 100644 Myriad/Gateway/ShardSessionInfo.cs create mode 100644 Myriad/Myriad.csproj create mode 100644 Myriad/Rest/BaseRestClient.cs create mode 100644 Myriad/Rest/DiscordApiClient.cs create mode 100644 Myriad/Rest/DiscordApiError.cs create mode 100644 Myriad/Rest/Exceptions/DiscordRequestException.cs create mode 100644 Myriad/Rest/Exceptions/RatelimitException.cs create mode 100644 Myriad/Rest/Ratelimit/Bucket.cs create mode 100644 Myriad/Rest/Ratelimit/BucketManager.cs create mode 100644 Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs create mode 100644 Myriad/Rest/Ratelimit/RatelimitHeaders.cs create mode 100644 Myriad/Rest/Ratelimit/Ratelimiter.cs create mode 100644 Myriad/Rest/Types/AllowedMentions.cs create mode 100644 Myriad/Rest/Types/MultipartFile.cs create mode 100644 Myriad/Rest/Types/Requests/CommandRequest.cs create mode 100644 Myriad/Rest/Types/Requests/CreateWebhookRequest.cs create mode 100644 Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs create mode 100644 Myriad/Rest/Types/Requests/MessageEditRequest.cs create mode 100644 Myriad/Rest/Types/Requests/MessageRequest.cs create mode 100644 Myriad/Rest/Types/Requests/ModifyGuildMemberRequest.cs create mode 100644 Myriad/Serialization/JsonSerializerOptionsExtensions.cs create mode 100644 Myriad/Serialization/JsonSnakeCaseNamingPolicy.cs create mode 100644 Myriad/Serialization/JsonStringConverter.cs create mode 100644 Myriad/Serialization/PermissionSetJsonConverter.cs create mode 100644 Myriad/Serialization/ShardInfoJsonConverter.cs create mode 100644 Myriad/Types/Activity.cs create mode 100644 Myriad/Types/Application/Application.cs create mode 100644 Myriad/Types/Application/ApplicationCommand.cs create mode 100644 Myriad/Types/Application/ApplicationCommandInteractionData.cs create mode 100644 Myriad/Types/Application/ApplicationCommandInteractionDataOption.cs create mode 100644 Myriad/Types/Application/ApplicationCommandOption.cs create mode 100644 Myriad/Types/Application/Interaction.cs create mode 100644 Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs create mode 100644 Myriad/Types/Application/InteractionResponse.cs create mode 100644 Myriad/Types/Channel.cs create mode 100644 Myriad/Types/Embed.cs create mode 100644 Myriad/Types/Emoji.cs create mode 100644 Myriad/Types/Gateway/GatewayInfo.cs create mode 100644 Myriad/Types/Gateway/SessionStartLimit.cs create mode 100644 Myriad/Types/Guild.cs create mode 100644 Myriad/Types/GuildMember.cs create mode 100644 Myriad/Types/Message.cs create mode 100644 Myriad/Types/PermissionSet.cs create mode 100644 Myriad/Types/Permissions.cs create mode 100644 Myriad/Types/Role.cs create mode 100644 Myriad/Types/User.cs create mode 100644 Myriad/Types/Webhook.cs diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs new file mode 100644 index 00000000..ff9a251f --- /dev/null +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; + +using Myriad.Gateway; + +namespace Myriad.Cache +{ + public static class DiscordCacheExtensions + { + public static ValueTask HandleGatewayEvent(this IDiscordCache cache, IGatewayEvent evt) + { + switch (evt) + { + case GuildCreateEvent gc: + return cache.SaveGuildCreate(gc); + case GuildUpdateEvent gu: + return cache.SaveGuild(gu); + case GuildDeleteEvent gd: + return cache.RemoveGuild(gd.Id); + case ChannelCreateEvent cc: + return cache.SaveChannel(cc); + case ChannelUpdateEvent cu: + return cache.SaveChannel(cu); + case ChannelDeleteEvent cd: + return cache.RemoveChannel(cd.Id); + case GuildRoleCreateEvent grc: + return cache.SaveRole(grc.GuildId, grc.Role); + case GuildRoleUpdateEvent gru: + return cache.SaveRole(gru.GuildId, gru.Role); + case GuildRoleDeleteEvent grd: + return cache.RemoveRole(grd.GuildId, grd.RoleId); + case MessageCreateEvent mc: + return cache.SaveUser(mc.Author); + } + + return default; + } + + private static async ValueTask SaveGuildCreate(this IDiscordCache cache, GuildCreateEvent guildCreate) + { + await cache.SaveGuild(guildCreate); + + foreach (var channel in guildCreate.Channels) + // The channel object does not include GuildId for some reason... + await cache.SaveChannel(channel with { GuildId = guildCreate.Id }); + + foreach (var member in guildCreate.Members) + await cache.SaveUser(member.User); + } + } +} \ No newline at end of file diff --git a/Myriad/Cache/IDiscordCache.cs b/Myriad/Cache/IDiscordCache.cs new file mode 100644 index 00000000..fdc348c6 --- /dev/null +++ b/Myriad/Cache/IDiscordCache.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Myriad.Types; + +namespace Myriad.Cache +{ + public interface IDiscordCache + { + public ValueTask SaveGuild(Guild guild); + public ValueTask SaveChannel(Channel channel); + public ValueTask SaveUser(User user); + public ValueTask SaveRole(ulong guildId, Role role); + + public ValueTask RemoveGuild(ulong guildId); + public ValueTask RemoveChannel(ulong channelId); + public ValueTask RemoveUser(ulong userId); + public ValueTask RemoveRole(ulong guildId, ulong roleId); + + public ValueTask GetGuild(ulong guildId); + public ValueTask GetChannel(ulong channelId); + public ValueTask GetUser(ulong userId); + public ValueTask GetRole(ulong roleId); + + public IAsyncEnumerable GetAllGuilds(); + public ValueTask> GetGuildChannels(ulong guildId); + } +} \ No newline at end of file diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs new file mode 100644 index 00000000..8ba50366 --- /dev/null +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Myriad.Types; + +namespace Myriad.Cache +{ + public class MemoryDiscordCache: IDiscordCache + { + private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _guilds; + private readonly ConcurrentDictionary _roles; + private readonly ConcurrentDictionary _users; + + public MemoryDiscordCache() + { + _guilds = new ConcurrentDictionary(); + _channels = new ConcurrentDictionary(); + _users = new ConcurrentDictionary(); + _roles = new ConcurrentDictionary(); + } + + public ValueTask SaveGuild(Guild guild) + { + SaveGuildRaw(guild); + + foreach (var role in guild.Roles) + // Don't call SaveRole because that updates guild state + // and we just got a brand new one :) + _roles[role.Id] = role; + + return default; + } + + public ValueTask SaveChannel(Channel channel) + { + _channels[channel.Id] = channel; + + if (channel.GuildId != null && _guilds.TryGetValue(channel.GuildId.Value, out var guild)) + guild.Channels.TryAdd(channel.Id, true); + + return default; + } + + public ValueTask SaveUser(User user) + { + _users[user.Id] = user; + return default; + } + + public ValueTask SaveRole(ulong guildId, Role role) + { + _roles[role.Id] = role; + + if (_guilds.TryGetValue(guildId, out var guild)) + { + // TODO: this code is stinky + var found = false; + for (var i = 0; i < guild.Guild.Roles.Length; i++) + { + if (guild.Guild.Roles[i].Id != role.Id) + continue; + + guild.Guild.Roles[i] = role; + found = true; + } + + if (!found) + { + _guilds[guildId] = guild with { + Guild = guild.Guild with { + Roles = guild.Guild.Roles.Concat(new[] { role}).ToArray() + } + }; + } + } + + return default; + } + + public ValueTask RemoveGuild(ulong guildId) + { + _guilds.TryRemove(guildId, out _); + return default; + } + + public ValueTask RemoveChannel(ulong channelId) + { + if (!_channels.TryRemove(channelId, out var channel)) + return default; + + if (channel.GuildId != null && _guilds.TryGetValue(channel.GuildId.Value, out var guild)) + guild.Channels.TryRemove(channel.Id, out _); + + return default; + } + + public ValueTask RemoveUser(ulong userId) + { + _users.TryRemove(userId, out _); + return default; + } + + public ValueTask RemoveRole(ulong guildId, ulong roleId) + { + _roles.TryRemove(roleId, out _); + return default; + } + + public ValueTask GetGuild(ulong guildId) => new(_guilds.GetValueOrDefault(guildId)?.Guild); + + public ValueTask GetChannel(ulong channelId) => new(_channels.GetValueOrDefault(channelId)); + + public ValueTask GetUser(ulong userId) => new(_users.GetValueOrDefault(userId)); + + public ValueTask GetRole(ulong roleId) => new(_roles.GetValueOrDefault(roleId)); + + public async IAsyncEnumerable GetAllGuilds() + { + foreach (var guild in _guilds.Values) + yield return guild.Guild; + } + + public ValueTask> GetGuildChannels(ulong guildId) + { + if (!_guilds.TryGetValue(guildId, out var guild)) + throw new ArgumentException("Guild not found", nameof(guildId)); + + return new ValueTask>(guild.Channels.Keys.Select(c => _channels[c])); + } + + private CachedGuild SaveGuildRaw(Guild guild) => + _guilds.GetOrAdd(guild.Id, (_, g) => new CachedGuild(g), guild); + + private record CachedGuild(Guild Guild) + { + public readonly ConcurrentDictionary Channels = new(); + } + } +} \ No newline at end of file diff --git a/Myriad/Extensions/ChannelExtensions.cs b/Myriad/Extensions/ChannelExtensions.cs new file mode 100644 index 00000000..99344138 --- /dev/null +++ b/Myriad/Extensions/ChannelExtensions.cs @@ -0,0 +1,7 @@ +namespace Myriad.Extensions +{ + public static class ChannelExtensions + { + + } +} \ No newline at end of file diff --git a/Myriad/Extensions/MessageExtensions.cs b/Myriad/Extensions/MessageExtensions.cs new file mode 100644 index 00000000..ef999fc0 --- /dev/null +++ b/Myriad/Extensions/MessageExtensions.cs @@ -0,0 +1,7 @@ +namespace Myriad.Extensions +{ + public class MessageExtensions + { + + } +} \ No newline at end of file diff --git a/Myriad/Extensions/PermissionExtensions.cs b/Myriad/Extensions/PermissionExtensions.cs new file mode 100644 index 00000000..02fd3292 --- /dev/null +++ b/Myriad/Extensions/PermissionExtensions.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Myriad.Gateway; +using Myriad.Types; + +namespace Myriad.Extensions +{ + public static class PermissionExtensions + { + public static PermissionSet EveryonePermissions(this Guild guild) => + guild.Roles.FirstOrDefault(r => r.Id == guild.Id)?.Permissions ?? PermissionSet.Dm; + + public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg) => + PermissionsFor(guild, channel, msg.Author.Id, msg.Member!.Roles); + + public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId, + ICollection roleIds) + { + if (channel.Type == Channel.ChannelType.Dm) + return PermissionSet.Dm; + + var perms = GuildPermissions(guild, userId, roleIds); + perms = ApplyChannelOverwrites(perms, channel, userId, roleIds); + + if ((perms & PermissionSet.Administrator) == PermissionSet.Administrator) + return PermissionSet.All; + + if ((perms & PermissionSet.ViewChannel) == 0) + perms &= ~NeedsViewChannel; + + if ((perms & PermissionSet.SendMessages) == 0) + perms &= ~NeedsSendMessages; + + return perms; + } + + public static bool Has(this PermissionSet value, PermissionSet flag) => + (value & flag) == flag; + + public static PermissionSet GuildPermissions(this Guild guild, ulong userId, ICollection roleIds) + { + if (guild.OwnerId == userId) + return PermissionSet.All; + + var perms = PermissionSet.None; + foreach (var role in guild.Roles) + { + if (role.Id == guild.Id || roleIds.Contains(role.Id)) + perms |= role.Permissions; + } + + if (perms.Has(PermissionSet.Administrator)) + return PermissionSet.All; + + return perms; + } + + public static PermissionSet ApplyChannelOverwrites(PermissionSet perms, Channel channel, ulong userId, + ICollection roleIds) + { + if (channel.PermissionOverwrites == null) + return perms; + + var everyoneDeny = PermissionSet.None; + var everyoneAllow = PermissionSet.None; + var roleDeny = PermissionSet.None; + var roleAllow = PermissionSet.None; + var userDeny = PermissionSet.None; + var userAllow = PermissionSet.None; + + foreach (var overwrite in channel.PermissionOverwrites) + { + switch (overwrite.Type) + { + case Channel.OverwriteType.Role when overwrite.Id == channel.GuildId: + everyoneDeny |= overwrite.Deny; + everyoneAllow |= overwrite.Allow; + break; + case Channel.OverwriteType.Role when roleIds.Contains(overwrite.Id): + roleDeny |= overwrite.Deny; + roleAllow |= overwrite.Allow; + break; + case Channel.OverwriteType.Member when overwrite.Id == userId: + userDeny |= overwrite.Deny; + userAllow |= overwrite.Allow; + break; + } + } + + perms &= ~everyoneDeny; + perms |= everyoneAllow; + perms &= ~roleDeny; + perms |= roleAllow; + perms &= ~userDeny; + perms |= userAllow; + return perms; + } + + private const PermissionSet NeedsViewChannel = + PermissionSet.SendMessages | + PermissionSet.SendTtsMessages | + PermissionSet.ManageMessages | + PermissionSet.EmbedLinks | + PermissionSet.AttachFiles | + PermissionSet.ReadMessageHistory | + PermissionSet.MentionEveryone | + PermissionSet.UseExternalEmojis | + PermissionSet.AddReactions | + PermissionSet.Connect | + PermissionSet.Speak | + PermissionSet.MuteMembers | + PermissionSet.DeafenMembers | + PermissionSet.MoveMembers | + PermissionSet.UseVad | + PermissionSet.Stream | + PermissionSet.PrioritySpeaker; + + private const PermissionSet NeedsSendMessages = + PermissionSet.MentionEveryone | + PermissionSet.SendTtsMessages | + PermissionSet.AttachFiles | + PermissionSet.EmbedLinks; + } +} \ No newline at end of file diff --git a/Myriad/Extensions/UserExtensions.cs b/Myriad/Extensions/UserExtensions.cs new file mode 100644 index 00000000..1f31b231 --- /dev/null +++ b/Myriad/Extensions/UserExtensions.cs @@ -0,0 +1,10 @@ +using Myriad.Types; + +namespace Myriad.Extensions +{ + public static class UserExtensions + { + public static string AvatarUrl(this User user) => + $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png"; + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Cluster.cs b/Myriad/Gateway/Cluster.cs new file mode 100644 index 00000000..304cfb8a --- /dev/null +++ b/Myriad/Gateway/Cluster.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Myriad.Types; + +using Serilog; + +namespace Myriad.Gateway +{ + public class Cluster + { + private readonly GatewaySettings _gatewaySettings; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _shards = new(); + + public Cluster(GatewaySettings gatewaySettings, ILogger logger) + { + _gatewaySettings = gatewaySettings; + _logger = logger; + } + + public Func? EventReceived { get; set; } + + public IReadOnlyDictionary Shards => _shards; + public ClusterSessionState SessionState => GetClusterState(); + public User? User => _shards.Values.Select(s => s.User).FirstOrDefault(s => s != null); + + private ClusterSessionState GetClusterState() + { + var shards = new List(); + foreach (var (id, shard) in _shards) + shards.Add(new ClusterSessionState.ShardState + { + Shard = shard.ShardInfo ?? new ShardInfo(id, _shards.Count), Session = shard.SessionInfo + }); + + return new ClusterSessionState {Shards = shards}; + } + + public async Task Start(GatewayInfo.Bot info, ClusterSessionState? lastState = null) + { + if (lastState != null && lastState.Shards.Count == info.Shards) + await Resume(info.Url, lastState); + else + await Start(info.Url, info.Shards); + } + + public async Task Resume(string url, ClusterSessionState sessionState) + { + _logger.Information("Resuming session with {ShardCount} shards at {Url}", sessionState.Shards.Count, url); + foreach (var shardState in sessionState.Shards) + CreateAndAddShard(url, shardState.Shard, shardState.Session); + + await StartShards(); + } + + public async Task Start(string url, int shardCount) + { + _logger.Information("Starting {ShardCount} shards at {Url}", shardCount, url); + for (var i = 0; i < shardCount; i++) + CreateAndAddShard(url, new ShardInfo(i, shardCount), null); + + await StartShards(); + } + + private async Task StartShards() + { + _logger.Information("Connecting shards..."); + await Task.WhenAll(_shards.Values.Select(s => s.Start())); + } + + private void CreateAndAddShard(string url, ShardInfo shardInfo, ShardSessionInfo? session) + { + var shard = new Shard(_logger, new Uri(url), _gatewaySettings, shardInfo, session); + shard.OnEventReceived += evt => OnShardEventReceived(shard, evt); + _shards[shardInfo.ShardId] = shard; + } + + private async Task OnShardEventReceived(Shard shard, IGatewayEvent evt) + { + if (EventReceived != null) + await EventReceived(shard, evt); + } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/ClusterSessionState.cs b/Myriad/Gateway/ClusterSessionState.cs new file mode 100644 index 00000000..aafb14be --- /dev/null +++ b/Myriad/Gateway/ClusterSessionState.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Myriad.Gateway +{ + public record ClusterSessionState + { + public List Shards { get; init; } + + public record ShardState + { + public ShardInfo Shard { get; init; } + public ShardSessionInfo Session { get; init; } + } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/ChannelCreateEvent.cs b/Myriad/Gateway/Events/ChannelCreateEvent.cs new file mode 100644 index 00000000..08c7f4aa --- /dev/null +++ b/Myriad/Gateway/Events/ChannelCreateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record ChannelCreateEvent: Channel, IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/ChannelDeleteEvent.cs b/Myriad/Gateway/Events/ChannelDeleteEvent.cs new file mode 100644 index 00000000..7a3907b9 --- /dev/null +++ b/Myriad/Gateway/Events/ChannelDeleteEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record ChannelDeleteEvent: Channel, IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/ChannelUpdateEvent.cs b/Myriad/Gateway/Events/ChannelUpdateEvent.cs new file mode 100644 index 00000000..95b675ac --- /dev/null +++ b/Myriad/Gateway/Events/ChannelUpdateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record ChannelUpdateEvent: Channel, IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildCreateEvent.cs b/Myriad/Gateway/Events/GuildCreateEvent.cs new file mode 100644 index 00000000..acfc9132 --- /dev/null +++ b/Myriad/Gateway/Events/GuildCreateEvent.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildCreateEvent: Guild, IGatewayEvent + { + public Channel[] Channels { get; init; } + public GuildMember[] Members { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildDeleteEvent.cs b/Myriad/Gateway/Events/GuildDeleteEvent.cs new file mode 100644 index 00000000..a46be10b --- /dev/null +++ b/Myriad/Gateway/Events/GuildDeleteEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record GuildDeleteEvent(ulong Id, bool Unavailable): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildMemberAddEvent.cs b/Myriad/Gateway/Events/GuildMemberAddEvent.cs new file mode 100644 index 00000000..33bcb057 --- /dev/null +++ b/Myriad/Gateway/Events/GuildMemberAddEvent.cs @@ -0,0 +1,9 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildMemberAddEvent: GuildMember, IGatewayEvent + { + public ulong GuildId { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildMemberRemoveEvent.cs b/Myriad/Gateway/Events/GuildMemberRemoveEvent.cs new file mode 100644 index 00000000..713dd85a --- /dev/null +++ b/Myriad/Gateway/Events/GuildMemberRemoveEvent.cs @@ -0,0 +1,10 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public class GuildMemberRemoveEvent: IGatewayEvent + { + public ulong GuildId { get; init; } + public User User { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildMemberUpdateEvent.cs b/Myriad/Gateway/Events/GuildMemberUpdateEvent.cs new file mode 100644 index 00000000..61f5b828 --- /dev/null +++ b/Myriad/Gateway/Events/GuildMemberUpdateEvent.cs @@ -0,0 +1,9 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildMemberUpdateEvent: GuildMember, IGatewayEvent + { + public ulong GuildId { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildRoleCreateEvent.cs b/Myriad/Gateway/Events/GuildRoleCreateEvent.cs new file mode 100644 index 00000000..4c5079fc --- /dev/null +++ b/Myriad/Gateway/Events/GuildRoleCreateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildRoleCreateEvent(ulong GuildId, Role Role): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildRoleDeleteEvent.cs b/Myriad/Gateway/Events/GuildRoleDeleteEvent.cs new file mode 100644 index 00000000..082c56df --- /dev/null +++ b/Myriad/Gateway/Events/GuildRoleDeleteEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record GuildRoleDeleteEvent(ulong GuildId, ulong RoleId): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildRoleUpdateEvent.cs b/Myriad/Gateway/Events/GuildRoleUpdateEvent.cs new file mode 100644 index 00000000..298769ca --- /dev/null +++ b/Myriad/Gateway/Events/GuildRoleUpdateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildRoleUpdateEvent(ulong GuildId, Role Role): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/GuildUpdateEvent.cs b/Myriad/Gateway/Events/GuildUpdateEvent.cs new file mode 100644 index 00000000..5d4695db --- /dev/null +++ b/Myriad/Gateway/Events/GuildUpdateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GuildUpdateEvent: Guild, IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/IGatewayEvent.cs b/Myriad/Gateway/Events/IGatewayEvent.cs new file mode 100644 index 00000000..17c5068b --- /dev/null +++ b/Myriad/Gateway/Events/IGatewayEvent.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace Myriad.Gateway +{ + public interface IGatewayEvent + { + public static readonly Dictionary EventTypes = new() + { + {"READY", typeof(ReadyEvent)}, + {"RESUMED", typeof(ResumedEvent)}, + {"GUILD_CREATE", typeof(GuildCreateEvent)}, + {"GUILD_UPDATE", typeof(GuildUpdateEvent)}, + {"GUILD_DELETE", typeof(GuildDeleteEvent)}, + {"GUILD_MEMBER_ADD", typeof(GuildMemberAddEvent)}, + {"GUILD_MEMBER_REMOVE", typeof(GuildMemberRemoveEvent)}, + {"GUILD_MEMBER_UPDATE", typeof(GuildMemberUpdateEvent)}, + {"GUILD_ROLE_CREATE", typeof(GuildRoleCreateEvent)}, + {"GUILD_ROLE_UPDATE", typeof(GuildRoleUpdateEvent)}, + {"GUILD_ROLE_DELETE", typeof(GuildRoleDeleteEvent)}, + {"CHANNEL_CREATE", typeof(ChannelCreateEvent)}, + {"CHANNEL_UPDATE", typeof(ChannelUpdateEvent)}, + {"CHANNEL_DELETE", typeof(ChannelDeleteEvent)}, + {"MESSAGE_CREATE", typeof(MessageCreateEvent)}, + {"MESSAGE_UPDATE", typeof(MessageUpdateEvent)}, + {"MESSAGE_DELETE", typeof(MessageDeleteEvent)}, + {"MESSAGE_DELETE_BULK", typeof(MessageDeleteBulkEvent)}, + {"MESSAGE_REACTION_ADD", typeof(MessageReactionAddEvent)}, + {"MESSAGE_REACTION_REMOVE", typeof(MessageReactionRemoveEvent)}, + {"MESSAGE_REACTION_REMOVE_ALL", typeof(MessageReactionRemoveAllEvent)}, + {"MESSAGE_REACTION_REMOVE_EMOJI", typeof(MessageReactionRemoveEmojiEvent)}, + {"INTERACTION_CREATE", typeof(InteractionCreateEvent)} + }; + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/InteractionCreateEvent.cs b/Myriad/Gateway/Events/InteractionCreateEvent.cs new file mode 100644 index 00000000..5ffccabc --- /dev/null +++ b/Myriad/Gateway/Events/InteractionCreateEvent.cs @@ -0,0 +1,6 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record InteractionCreateEvent: Interaction, IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageCreateEvent.cs b/Myriad/Gateway/Events/MessageCreateEvent.cs new file mode 100644 index 00000000..6df58ad4 --- /dev/null +++ b/Myriad/Gateway/Events/MessageCreateEvent.cs @@ -0,0 +1,9 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record MessageCreateEvent: Message, IGatewayEvent + { + public GuildMemberPartial? Member { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageDeleteBulkEvent.cs b/Myriad/Gateway/Events/MessageDeleteBulkEvent.cs new file mode 100644 index 00000000..b4b88601 --- /dev/null +++ b/Myriad/Gateway/Events/MessageDeleteBulkEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record MessageDeleteBulkEvent(ulong[] Ids, ulong ChannelId, ulong? GuildId): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageDeleteEvent.cs b/Myriad/Gateway/Events/MessageDeleteEvent.cs new file mode 100644 index 00000000..e7a05e04 --- /dev/null +++ b/Myriad/Gateway/Events/MessageDeleteEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record MessageDeleteEvent(ulong Id, ulong ChannelId, ulong? GuildId): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageReactionAddEvent.cs b/Myriad/Gateway/Events/MessageReactionAddEvent.cs new file mode 100644 index 00000000..c7545bea --- /dev/null +++ b/Myriad/Gateway/Events/MessageReactionAddEvent.cs @@ -0,0 +1,8 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record MessageReactionAddEvent(ulong UserId, ulong ChannelId, ulong MessageId, ulong? GuildId, + GuildMember? Member, + Emoji Emoji): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageReactionRemoveAllEvent.cs b/Myriad/Gateway/Events/MessageReactionRemoveAllEvent.cs new file mode 100644 index 00000000..1ef0dab0 --- /dev/null +++ b/Myriad/Gateway/Events/MessageReactionRemoveAllEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record MessageReactionRemoveAllEvent(ulong ChannelId, ulong MessageId, ulong? GuildId): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageReactionRemoveEmojiEvent.cs b/Myriad/Gateway/Events/MessageReactionRemoveEmojiEvent.cs new file mode 100644 index 00000000..ff4a5dad --- /dev/null +++ b/Myriad/Gateway/Events/MessageReactionRemoveEmojiEvent.cs @@ -0,0 +1,7 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record MessageReactionRemoveEmojiEvent + (ulong ChannelId, ulong MessageId, ulong? GuildId, Emoji Emoji): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageReactionRemoveEvent.cs b/Myriad/Gateway/Events/MessageReactionRemoveEvent.cs new file mode 100644 index 00000000..392e2cf9 --- /dev/null +++ b/Myriad/Gateway/Events/MessageReactionRemoveEvent.cs @@ -0,0 +1,7 @@ +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record MessageReactionRemoveEvent + (ulong UserId, ulong ChannelId, ulong MessageId, ulong? GuildId, Emoji Emoji): IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/MessageUpdateEvent.cs b/Myriad/Gateway/Events/MessageUpdateEvent.cs new file mode 100644 index 00000000..9e77d076 --- /dev/null +++ b/Myriad/Gateway/Events/MessageUpdateEvent.cs @@ -0,0 +1,7 @@ +namespace Myriad.Gateway +{ + public record MessageUpdateEvent(ulong Id, ulong ChannelId): IGatewayEvent + { + // TODO: lots of partials + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/ReadyEvent.cs b/Myriad/Gateway/Events/ReadyEvent.cs new file mode 100644 index 00000000..7dad1ee7 --- /dev/null +++ b/Myriad/Gateway/Events/ReadyEvent.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record ReadyEvent: IGatewayEvent + { + [JsonPropertyName("v")] public int Version { get; init; } + public User User { get; init; } + public string SessionId { get; init; } + public ShardInfo? Shard { get; init; } + public ApplicationPartial Application { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Events/ResumedEvent.cs b/Myriad/Gateway/Events/ResumedEvent.cs new file mode 100644 index 00000000..de8ecfe1 --- /dev/null +++ b/Myriad/Gateway/Events/ResumedEvent.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record ResumedEvent: IGatewayEvent; +} \ No newline at end of file diff --git a/Myriad/Gateway/GatewayCloseException.cs b/Myriad/Gateway/GatewayCloseException.cs new file mode 100644 index 00000000..02f83159 --- /dev/null +++ b/Myriad/Gateway/GatewayCloseException.cs @@ -0,0 +1,35 @@ +using System; + +namespace Myriad.Gateway +{ + // TODO: unused? + public class GatewayCloseException: Exception + { + public GatewayCloseException(int closeCode, string closeReason): base($"{closeCode}: {closeReason}") + { + CloseCode = closeCode; + CloseReason = closeReason; + } + + public int CloseCode { get; } + public string CloseReason { get; } + } + + public class GatewayCloseCode + { + public const int UnknownError = 4000; + public const int UnknownOpcode = 4001; + public const int DecodeError = 4002; + public const int NotAuthenticated = 4003; + public const int AuthenticationFailed = 4004; + public const int AlreadyAuthenticated = 4005; + public const int InvalidSeq = 4007; + public const int RateLimited = 4008; + public const int SessionTimedOut = 4009; + public const int InvalidShard = 4010; + public const int ShardingRequired = 4011; + public const int InvalidApiVersion = 4012; + public const int InvalidIntent = 4013; + public const int DisallowedIntent = 4014; + } +} \ No newline at end of file diff --git a/Myriad/Gateway/GatewayIntent.cs b/Myriad/Gateway/GatewayIntent.cs new file mode 100644 index 00000000..1a2c7c7d --- /dev/null +++ b/Myriad/Gateway/GatewayIntent.cs @@ -0,0 +1,24 @@ +using System; + +namespace Myriad.Gateway +{ + [Flags] + public enum GatewayIntent + { + Guilds = 1 << 0, + GuildMembers = 1 << 1, + GuildBans = 1 << 2, + GuildEmojis = 1 << 3, + GuildIntegrations = 1 << 4, + GuildWebhooks = 1 << 5, + GuildInvites = 1 << 6, + GuildVoiceStates = 1 << 7, + GuildPresences = 1 << 8, + GuildMessages = 1 << 9, + GuildMessageReactions = 1 << 10, + GuildMessageTyping = 1 << 11, + DirectMessages = 1 << 12, + DirectMessageReactions = 1 << 13, + DirectMessageTyping = 1 << 14 + } +} \ No newline at end of file diff --git a/Myriad/Gateway/GatewayPacket.cs b/Myriad/Gateway/GatewayPacket.cs new file mode 100644 index 00000000..1cf7e26d --- /dev/null +++ b/Myriad/Gateway/GatewayPacket.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace Myriad.Gateway +{ + public record GatewayPacket + { + [JsonPropertyName("op")] public GatewayOpcode Opcode { get; init; } + [JsonPropertyName("d")] public object? Payload { get; init; } + + [JsonPropertyName("s")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Sequence { get; init; } + + [JsonPropertyName("t")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? EventType { get; init; } + } + + public enum GatewayOpcode + { + Dispatch = 0, + Heartbeat = 1, + Identify = 2, + PresenceUpdate = 3, + VoiceStateUpdate = 4, + Resume = 6, + Reconnect = 7, + RequestGuildMembers = 8, + InvalidSession = 9, + Hello = 10, + HeartbeatAck = 11 + } +} \ No newline at end of file diff --git a/Myriad/Gateway/GatewaySettings.cs b/Myriad/Gateway/GatewaySettings.cs new file mode 100644 index 00000000..fdaf13ea --- /dev/null +++ b/Myriad/Gateway/GatewaySettings.cs @@ -0,0 +1,8 @@ +namespace Myriad.Gateway +{ + public record GatewaySettings + { + public string Token { get; init; } + public GatewayIntent Intents { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Payloads/GatewayHello.cs b/Myriad/Gateway/Payloads/GatewayHello.cs new file mode 100644 index 00000000..f8593bb9 --- /dev/null +++ b/Myriad/Gateway/Payloads/GatewayHello.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record GatewayHello(int HeartbeatInterval); +} \ No newline at end of file diff --git a/Myriad/Gateway/Payloads/GatewayIdentify.cs b/Myriad/Gateway/Payloads/GatewayIdentify.cs new file mode 100644 index 00000000..bc6d1931 --- /dev/null +++ b/Myriad/Gateway/Payloads/GatewayIdentify.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace Myriad.Gateway +{ + public record GatewayIdentify + { + public string Token { get; init; } + public ConnectionProperties Properties { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Compress { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? LargeThreshold { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ShardInfo? Shard { get; init; } + + public GatewayIntent Intents { get; init; } + + public record ConnectionProperties + { + [JsonPropertyName("$os")] public string Os { get; init; } + [JsonPropertyName("$browser")] public string Browser { get; init; } + [JsonPropertyName("$device")] public string Device { get; init; } + } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Payloads/GatewayResume.cs b/Myriad/Gateway/Payloads/GatewayResume.cs new file mode 100644 index 00000000..fd386889 --- /dev/null +++ b/Myriad/Gateway/Payloads/GatewayResume.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record GatewayResume(string Token, string SessionId, int Seq); +} \ No newline at end of file diff --git a/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs b/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs new file mode 100644 index 00000000..23ad01b7 --- /dev/null +++ b/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +using Myriad.Types; + +namespace Myriad.Gateway +{ + public record GatewayStatusUpdate + { + public enum UserStatus + { + Online, + Dnd, + Idle, + Invisible, + Offline + } + + public ulong? Since { get; init; } + public ActivityPartial[]? Activities { get; init; } + public UserStatus Status { get; init; } + public bool Afk { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Shard.cs b/Myriad/Gateway/Shard.cs new file mode 100644 index 00000000..a4b65592 --- /dev/null +++ b/Myriad/Gateway/Shard.cs @@ -0,0 +1,328 @@ +using System; +using System.Net.WebSockets; +using System.Text.Json; +using System.Threading.Tasks; + +using Myriad.Serialization; +using Myriad.Types; + +using Serilog; + +namespace Myriad.Gateway +{ + public class Shard: IAsyncDisposable + { + private const string LibraryName = "Newcord Test"; + + private readonly JsonSerializerOptions _jsonSerializerOptions = + new JsonSerializerOptions().ConfigureForNewcord(); + + private readonly ILogger _logger; + private readonly Uri _uri; + + private ShardConnection? _conn; + private TimeSpan? _currentHeartbeatInterval; + private bool _hasReceivedAck; + private DateTimeOffset? _lastHeartbeatSent; + private Task _worker; + + public ShardInfo? ShardInfo { get; private set; } + public GatewaySettings Settings { get; } + public ShardSessionInfo SessionInfo { get; private set; } + public ShardState State { get; private set; } + public TimeSpan? Latency { get; private set; } + public User? User { get; private set; } + + public Func? OnEventReceived { get; set; } + + public Shard(ILogger logger, Uri uri, GatewaySettings settings, ShardInfo? info = null, + ShardSessionInfo? sessionInfo = null) + { + _logger = logger; + _uri = uri; + + Settings = settings; + ShardInfo = info; + SessionInfo = sessionInfo ?? new ShardSessionInfo(); + } + + public async ValueTask DisposeAsync() + { + if (_conn != null) + await _conn.DisposeAsync(); + } + + public Task Start() + { + _worker = MainLoop(); + return Task.CompletedTask; + } + + public async Task UpdateStatus(GatewayStatusUpdate payload) + { + if (_conn != null && _conn.State == WebSocketState.Open) + await _conn!.Send(new GatewayPacket {Opcode = GatewayOpcode.PresenceUpdate, Payload = payload}); + } + + private async Task MainLoop() + { + while (true) + try + { + _logger.Information("Connecting..."); + + State = ShardState.Connecting; + await Connect(); + + _logger.Information("Connected. Entering main loop..."); + + // Tick returns false if we need to stop and reconnect + while (await Tick(_conn!)) + await Task.Delay(TimeSpan.FromMilliseconds(1000)); + + _logger.Information("Connection closed, reconnecting..."); + State = ShardState.Closed; + } + catch (Exception e) + { + _logger.Error(e, "Error in shard state handler"); + } + } + + private async Task Tick(ShardConnection conn) + { + if (conn.State != WebSocketState.Connecting && conn.State != WebSocketState.Open) + return false; + + if (!await TickHeartbeat(conn)) + // TickHeartbeat returns false if we're disconnecting + return false; + + return true; + } + + private async Task TickHeartbeat(ShardConnection conn) + { + // If we don't need to heartbeat, do nothing + if (_lastHeartbeatSent == null || _currentHeartbeatInterval == null) + return true; + + if (DateTimeOffset.UtcNow - _lastHeartbeatSent < _currentHeartbeatInterval) + return true; + + // If we haven't received the ack in time, close w/ error + if (!_hasReceivedAck) + { + _logger.Warning( + "Did not receive heartbeat Ack from gateway within interval ({HeartbeatInterval})", + _currentHeartbeatInterval); + State = ShardState.Closing; + await conn.Disconnect(WebSocketCloseStatus.ProtocolError, "Did not receive ACK in time"); + return false; + } + + // Otherwise just send it :) + await SendHeartbeat(conn); + _hasReceivedAck = false; + return true; + } + + private async Task SendHeartbeat(ShardConnection conn) + { + _logger.Debug("Sending heartbeat"); + + await conn.Send(new GatewayPacket {Opcode = GatewayOpcode.Heartbeat, Payload = SessionInfo.LastSequence}); + _lastHeartbeatSent = DateTimeOffset.UtcNow; + } + + private async Task Connect() + { + if (_conn != null) + await _conn.DisposeAsync(); + + _currentHeartbeatInterval = null; + + _conn = new ShardConnection(_uri, _logger, _jsonSerializerOptions) {OnReceive = OnReceive}; + } + + private async Task OnReceive(GatewayPacket packet) + { + switch (packet.Opcode) + { + case GatewayOpcode.Hello: + { + await HandleHello((JsonElement) packet.Payload!); + break; + } + case GatewayOpcode.Heartbeat: + { + _logger.Debug("Received heartbeat request from shard, sending Ack"); + await _conn!.Send(new GatewayPacket {Opcode = GatewayOpcode.HeartbeatAck}); + break; + } + case GatewayOpcode.HeartbeatAck: + { + Latency = DateTimeOffset.UtcNow - _lastHeartbeatSent; + _logger.Debug("Received heartbeat Ack (latency {Latency})", Latency); + + _hasReceivedAck = true; + break; + } + case GatewayOpcode.Reconnect: + { + _logger.Information("Received Reconnect, closing and reconnecting"); + await _conn!.Disconnect(WebSocketCloseStatus.Empty, null); + break; + } + case GatewayOpcode.InvalidSession: + { + var canResume = ((JsonElement) packet.Payload!).GetBoolean(); + + // Clear session info before DCing + if (!canResume) + SessionInfo = SessionInfo with { Session = null }; + + var delay = TimeSpan.FromMilliseconds(new Random().Next(1000, 5000)); + + _logger.Information( + "Received Invalid Session (can resume? {CanResume}), reconnecting after {ReconnectDelay}", + canResume, delay); + await _conn!.Disconnect(WebSocketCloseStatus.Empty, null); + + // Will reconnect after exiting this "loop" + await Task.Delay(delay); + break; + } + case GatewayOpcode.Dispatch: + { + SessionInfo = SessionInfo with { LastSequence = packet.Sequence }; + var evt = DeserializeEvent(packet.EventType!, (JsonElement) packet.Payload!)!; + + if (evt is ReadyEvent rdy) + { + if (State == ShardState.Connecting) + await HandleReady(rdy); + else + _logger.Warning("Received Ready event in unexpected state {ShardState}, ignoring?", State); + } + else if (evt is ResumedEvent) + { + if (State == ShardState.Connecting) + await HandleResumed(); + else + _logger.Warning("Received Resumed event in unexpected state {ShardState}, ignoring?", + State); + } + + await HandleEvent(evt); + break; + } + default: + { + _logger.Debug("Received unknown gateway opcode {Opcode}", packet.Opcode); + break; + } + } + } + + private async Task HandleEvent(IGatewayEvent evt) + { + if (OnEventReceived != null) + await OnEventReceived.Invoke(evt); + } + + + private IGatewayEvent? DeserializeEvent(string eventType, JsonElement data) + { + if (!IGatewayEvent.EventTypes.TryGetValue(eventType, out var clrType)) + { + _logger.Information("Received unknown event type {EventType}", eventType); + return null; + } + + try + { + _logger.Verbose("Deserializing {EventType} to {ClrType}", eventType, clrType); + return JsonSerializer.Deserialize(data.GetRawText(), clrType, _jsonSerializerOptions) + as IGatewayEvent; + } + catch (JsonException e) + { + _logger.Error(e, "Error deserializing event {EventType} to {ClrType}", eventType, clrType); + return null; + } + } + + private Task HandleReady(ReadyEvent ready) + { + ShardInfo = ready.Shard; + SessionInfo = SessionInfo with { Session = ready.SessionId }; + User = ready.User; + State = ShardState.Open; + + return Task.CompletedTask; + } + + private Task HandleResumed() + { + State = ShardState.Open; + return Task.CompletedTask; + } + + private async Task HandleHello(JsonElement json) + { + var hello = JsonSerializer.Deserialize(json.GetRawText(), _jsonSerializerOptions)!; + _logger.Debug("Received Hello with interval {Interval} ms", hello.HeartbeatInterval); + _currentHeartbeatInterval = TimeSpan.FromMilliseconds(hello.HeartbeatInterval); + + await SendHeartbeat(_conn!); + + await SendIdentifyOrResume(); + } + + private async Task SendIdentifyOrResume() + { + if (SessionInfo.Session != null && SessionInfo.LastSequence != null) + await SendResume(SessionInfo.Session, SessionInfo.LastSequence!.Value); + else + await SendIdentify(); + } + + private async Task SendIdentify() + { + _logger.Information("Sending gateway Identify for shard {@ShardInfo}", SessionInfo); + await _conn!.Send(new GatewayPacket + { + Opcode = GatewayOpcode.Identify, + Payload = new GatewayIdentify + { + Token = Settings.Token, + Properties = new GatewayIdentify.ConnectionProperties + { + Browser = LibraryName, Device = LibraryName, Os = Environment.OSVersion.ToString() + }, + Intents = Settings.Intents, + Shard = ShardInfo + } + }); + } + + private async Task SendResume(string session, int lastSequence) + { + _logger.Information("Sending gateway Resume for session {@SessionInfo}", ShardInfo, + SessionInfo); + await _conn!.Send(new GatewayPacket + { + Opcode = GatewayOpcode.Resume, Payload = new GatewayResume(Settings.Token, session, lastSequence) + }); + } + + public enum ShardState + { + Closed, + Connecting, + Open, + Closing + } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/ShardConnection.cs b/Myriad/Gateway/ShardConnection.cs new file mode 100644 index 00000000..77453de2 --- /dev/null +++ b/Myriad/Gateway/ShardConnection.cs @@ -0,0 +1,118 @@ +using System; +using System.Buffers; +using System.IO; +using System.Net.WebSockets; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using Serilog; + +namespace Myriad.Gateway +{ + public class ShardConnection: IAsyncDisposable + { + private readonly MemoryStream _bufStream = new(); + + private readonly ClientWebSocket _client = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly ILogger _logger; + private readonly Task _worker; + + public ShardConnection(Uri uri, ILogger logger, JsonSerializerOptions jsonSerializerOptions) + { + _logger = logger; + _jsonSerializerOptions = jsonSerializerOptions; + + _worker = Worker(uri); + } + + public Func? OnReceive { get; set; } + + public WebSocketState State => _client.State; + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + await _worker; + + _client.Dispose(); + await _bufStream.DisposeAsync(); + _cts.Dispose(); + } + + private async Task Worker(Uri uri) + { + var realUrl = new UriBuilder(uri) + { + Query = "v=8&encoding=json" + }.Uri; + _logger.Debug("Connecting to gateway WebSocket at {GatewayUrl}", realUrl); + await _client.ConnectAsync(realUrl, default); + + while (!_cts.IsCancellationRequested && _client.State == WebSocketState.Open) + try + { + await HandleReceive(); + } + catch (Exception e) + { + _logger.Error(e, "Error in WebSocket receive worker"); + } + } + + private async Task HandleReceive() + { + _bufStream.SetLength(0); + var result = await ReadData(_bufStream); + var data = _bufStream.GetBuffer().AsMemory(0, (int) _bufStream.Position); + + if (result.MessageType == WebSocketMessageType.Text) + await HandleReceiveData(data); + else if (result.MessageType == WebSocketMessageType.Close) + _logger.Information("WebSocket closed by server: {StatusCode} {Reason}", _client.CloseStatus, + _client.CloseStatusDescription); + } + + private async Task HandleReceiveData(Memory data) + { + var packet = JsonSerializer.Deserialize(data.Span, _jsonSerializerOptions)!; + + try + { + if (OnReceive != null) + await OnReceive.Invoke(packet); + } + catch (Exception e) + { + _logger.Error(e, "Error in gateway handler for {OpcodeType}", packet.Opcode); + } + } + + private async Task ReadData(MemoryStream stream) + { + using var buf = MemoryPool.Shared.Rent(); + ValueWebSocketReceiveResult result; + do + { + result = await _client.ReceiveAsync(buf.Memory, _cts.Token); + stream.Write(buf.Memory.Span.Slice(0, result.Count)); + } while (!result.EndOfMessage); + + return result; + } + + public async Task Send(GatewayPacket packet) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(packet, _jsonSerializerOptions); + await _client.SendAsync(bytes.AsMemory(), WebSocketMessageType.Text, true, default); + } + + public async Task Disconnect(WebSocketCloseStatus status, string? description) + { + await _client.CloseAsync(status, description, default); + _cts.Cancel(); + } + } +} \ No newline at end of file diff --git a/Myriad/Gateway/ShardInfo.cs b/Myriad/Gateway/ShardInfo.cs new file mode 100644 index 00000000..07a096f6 --- /dev/null +++ b/Myriad/Gateway/ShardInfo.cs @@ -0,0 +1,4 @@ +namespace Myriad.Gateway +{ + public record ShardInfo(int ShardId, int NumShards); +} \ No newline at end of file diff --git a/Myriad/Gateway/ShardSessionInfo.cs b/Myriad/Gateway/ShardSessionInfo.cs new file mode 100644 index 00000000..81d6ee5f --- /dev/null +++ b/Myriad/Gateway/ShardSessionInfo.cs @@ -0,0 +1,8 @@ +namespace Myriad.Gateway +{ + public record ShardSessionInfo + { + public string? Session { get; init; } + public int? LastSequence { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Myriad.csproj b/Myriad/Myriad.csproj new file mode 100644 index 00000000..2b027a76 --- /dev/null +++ b/Myriad/Myriad.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + enable + + + + true + full + + + + + + + + + diff --git a/Myriad/Rest/BaseRestClient.cs b/Myriad/Rest/BaseRestClient.cs new file mode 100644 index 00000000..ad35cc0a --- /dev/null +++ b/Myriad/Rest/BaseRestClient.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +using Myriad.Rest.Exceptions; +using Myriad.Rest.Ratelimit; +using Myriad.Rest.Types; +using Myriad.Serialization; + +using Polly; + +using Serilog; + +namespace Myriad.Rest +{ + public class BaseRestClient: IAsyncDisposable + { + private const string ApiBaseUrl = "https://discord.com/api/v8"; + + private readonly Version _httpVersion = new(2, 0); + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly ILogger _logger; + private readonly Ratelimiter _ratelimiter; + private readonly AsyncPolicy _retryPolicy; + + public BaseRestClient(string userAgent, string token, ILogger logger) + { + _logger = logger.ForContext(); + + if (!token.StartsWith("Bot ")) + token = "Bot " + token; + + Client = new HttpClient(); + Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", userAgent); + Client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", token); + + _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForNewcord(); + + _ratelimiter = new Ratelimiter(logger); + var discordPolicy = new DiscordRateLimitPolicy(_ratelimiter); + + // todo: why doesn't the timeout work? o.o + var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10)); + + var waitPolicy = Policy + .Handle() + .WaitAndRetryAsync(3, + (_, e, _) => ((RatelimitBucketExhaustedException) e).RetryAfter, + (_, _, _, _) => Task.CompletedTask) + .AsAsyncPolicy(); + + _retryPolicy = Policy.WrapAsync(timeoutPolicy, waitPolicy, discordPolicy); + } + + public HttpClient Client { get; } + + public ValueTask DisposeAsync() + { + _ratelimiter.Dispose(); + Client.Dispose(); + return default; + } + + public async Task Get(string path, (string endpointName, ulong major) ratelimitParams) where T: class + { + var request = new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + path); + var response = await Send(request, ratelimitParams, true); + + // GET-only special case: 404s are nulls and not exceptions + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + + return await ReadResponse(response); + } + + public async Task Post(string path, (string endpointName, ulong major) ratelimitParams, object? body) + where T: class + { + var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + + var response = await Send(request, ratelimitParams); + return await ReadResponse(response); + } + + public async Task PostMultipart(string path, (string endpointName, ulong major) ratelimitParams, object? payload, MultipartFile[]? files) + where T: class + { + var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); + SetRequestFormDataBody(request, payload, files); + + var response = await Send(request, ratelimitParams); + return await ReadResponse(response); + } + + public async Task Patch(string path, (string endpointName, ulong major) ratelimitParams, object? body) + where T: class + { + var request = new HttpRequestMessage(HttpMethod.Patch, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + + var response = await Send(request, ratelimitParams); + return await ReadResponse(response); + } + + public async Task Put(string path, (string endpointName, ulong major) ratelimitParams, object? body) + where T: class + { + var request = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + + var response = await Send(request, ratelimitParams); + return await ReadResponse(response); + } + + public async Task Delete(string path, (string endpointName, ulong major) ratelimitParams) + { + var request = new HttpRequestMessage(HttpMethod.Delete, ApiBaseUrl + path); + await Send(request, ratelimitParams); + } + + private void SetRequestJsonBody(HttpRequestMessage request, object? body) + { + if (body == null) return; + request.Content = + new ReadOnlyMemoryContent(JsonSerializer.SerializeToUtf8Bytes(body, _jsonSerializerOptions)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + } + + private void SetRequestFormDataBody(HttpRequestMessage request, object? payload, MultipartFile[]? files) + { + var bodyJson = JsonSerializer.SerializeToUtf8Bytes(payload, _jsonSerializerOptions); + + var mfd = new MultipartFormDataContent(); + mfd.Add(new ByteArrayContent(bodyJson), "payload_json"); + + if (files != null) + { + for (var i = 0; i < files.Length; i++) + { + var (filename, stream) = files[i]; + mfd.Add(new StreamContent(stream), $"file{i}", filename); + } + } + + request.Content = mfd; + } + + private async Task ReadResponse(HttpResponseMessage response) where T: class + { + if (response.StatusCode == HttpStatusCode.NoContent) + return null; + return await response.Content.ReadFromJsonAsync(_jsonSerializerOptions); + } + + private async Task Send(HttpRequestMessage request, + (string endpointName, ulong major) ratelimitParams, + bool ignoreNotFound = false) + { + return await _retryPolicy.ExecuteAsync(async _ => + { + _logger.Debug("Sending request: {RequestMethod} {RequestPath}", + request.Method, request.RequestUri); + + request.Version = _httpVersion; + request.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + stopwatch.Stop(); + + _logger.Debug( + "Received response in {ResponseDurationMs} ms: {RequestMethod} {RequestPath} -> {StatusCode} {ReasonPhrase}", + stopwatch.ElapsedMilliseconds, request.Method, request.RequestUri, (int) response.StatusCode, + response.ReasonPhrase); + + await HandleApiError(response, ignoreNotFound); + + return response; + }, + new Dictionary + { + {DiscordRateLimitPolicy.EndpointContextKey, ratelimitParams.endpointName}, + {DiscordRateLimitPolicy.MajorContextKey, ratelimitParams.major} + }); + } + + private async ValueTask HandleApiError(HttpResponseMessage response, bool ignoreNotFound) + { + if (response.IsSuccessStatusCode) + return; + + if (response.StatusCode == HttpStatusCode.NotFound && ignoreNotFound) + return; + + throw await CreateDiscordException(response); + } + + private async ValueTask CreateDiscordException(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + var apiError = TryParseApiError(body); + + return response.StatusCode switch + { + HttpStatusCode.BadRequest => new BadRequestException(response, body, apiError), + HttpStatusCode.Forbidden => new ForbiddenException(response, body, apiError), + HttpStatusCode.Unauthorized => new UnauthorizedException(response, body, apiError), + HttpStatusCode.NotFound => new NotFoundException(response, body, apiError), + HttpStatusCode.Conflict => new ConflictException(response, body, apiError), + HttpStatusCode.TooManyRequests => new TooManyRequestsException(response, body, apiError), + _ => new UnknownDiscordRequestException(response, body, apiError) + }; + } + + private DiscordApiError? TryParseApiError(string responseBody) + { + if (string.IsNullOrWhiteSpace(responseBody)) + return null; + + try + { + return JsonSerializer.Deserialize(responseBody, _jsonSerializerOptions); + } + catch (JsonException e) + { + _logger.Verbose(e, "Error deserializing API error"); + } + + return null; + } + } +} \ No newline at end of file diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs new file mode 100644 index 00000000..27588b51 --- /dev/null +++ b/Myriad/Rest/DiscordApiClient.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; +using Myriad.Types; + +using Serilog; + +namespace Myriad.Rest +{ + public class DiscordApiClient + { + private const string UserAgent = "Test Discord Library by @Ske#6201"; + private readonly BaseRestClient _client; + + public DiscordApiClient(string token, ILogger logger) + { + _client = new BaseRestClient(UserAgent, token, logger); + } + + public Task GetGateway() => + _client.Get("/gateway", ("GetGateway", default))!; + + public Task GetGatewayBot() => + _client.Get("/gateway/bot", ("GetGatewayBot", default))!; + + public Task GetChannel(ulong channelId) => + _client.Get($"/channels/{channelId}", ("GetChannel", channelId)); + + public Task GetMessage(ulong channelId, ulong messageId) => + _client.Get($"/channels/{channelId}/messages/{messageId}", ("GetMessage", channelId)); + + public Task GetGuild(ulong id) => + _client.Get($"/guilds/{id}", ("GetGuild", id)); + + public Task GetUser(ulong id) => + _client.Get($"/users/{id}", ("GetUser", default)); + + public Task CreateMessage(ulong channelId, MessageRequest request) => + _client.Post($"/channels/{channelId}/messages", ("CreateMessage", channelId), request)!; + + public Task EditMessage(ulong channelId, ulong messageId, MessageEditRequest request) => + _client.Patch($"/channels/{channelId}/messages/{messageId}", ("EditMessage", channelId), request)!; + + public Task DeleteMessage(ulong channelId, ulong messageId) => + _client.Delete($"/channels/{channelId}/messages/{messageId}", ("DeleteMessage", channelId)); + + public Task CreateReaction(ulong channelId, ulong messageId, Emoji emoji) => + _client.Put($"/channels/{channelId}/messages/{messageId}/reactions/{EncodeEmoji(emoji)}/@me", + ("CreateReaction", channelId), null); + + public Task DeleteOwnReaction(ulong channelId, ulong messageId, Emoji emoji) => + _client.Delete($"/channels/{channelId}/messages/{messageId}/reactions/{EncodeEmoji(emoji)}/@me", + ("DeleteOwnReaction", channelId)); + + public Task DeleteUserReaction(ulong channelId, ulong messageId, Emoji emoji, ulong userId) => + _client.Delete($"/channels/{channelId}/messages/{messageId}/reactions/{EncodeEmoji(emoji)}/{userId}", + ("DeleteUserReaction", channelId)); + + public Task DeleteAllReactions(ulong channelId, ulong messageId) => + _client.Delete($"/channels/{channelId}/messages/{messageId}/reactions", + ("DeleteAllReactions", channelId)); + + public Task DeleteAllReactionsForEmoji(ulong channelId, ulong messageId, Emoji emoji) => + _client.Delete($"/channels/{channelId}/messages/{messageId}/reactions/{EncodeEmoji(emoji)}", + ("DeleteAllReactionsForEmoji", channelId)); + + public Task CreateGlobalApplicationCommand(ulong applicationId, + ApplicationCommandRequest request) => + _client.Post($"/applications/{applicationId}/commands", + ("CreateGlobalApplicationCommand", applicationId), request)!; + + public Task GetGuildApplicationCommands(ulong applicationId, ulong guildId) => + _client.Get($"/applications/{applicationId}/guilds/{guildId}/commands", + ("GetGuildApplicationCommands", applicationId))!; + + public Task CreateGuildApplicationCommand(ulong applicationId, ulong guildId, + ApplicationCommandRequest request) => + _client.Post($"/applications/{applicationId}/guilds/{guildId}/commands", + ("CreateGuildApplicationCommand", applicationId), request)!; + + public Task EditGuildApplicationCommand(ulong applicationId, ulong guildId, + ApplicationCommandRequest request) => + _client.Patch($"/applications/{applicationId}/guilds/{guildId}/commands", + ("EditGuildApplicationCommand", applicationId), request)!; + + public Task DeleteGuildApplicationCommand(ulong applicationId, ulong commandId) => + _client.Delete($"/applications/{applicationId}/commands/{commandId}", + ("DeleteGuildApplicationCommand", applicationId)); + + public Task CreateInteractionResponse(ulong interactionId, string token, InteractionResponse response) => + _client.Post($"/interactions/{interactionId}/{token}/callback", + ("CreateInteractionResponse", interactionId), response); + + public Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberRequest request) => + _client.Patch($"/guilds/{guildId}/members/{userId}", + ("ModifyGuildMember", guildId), request); + + public Task CreateWebhook(ulong channelId, CreateWebhookRequest request) => + _client.Post($"/channels/{channelId}/webhooks", ("CreateWebhook", channelId), request)!; + + public Task GetWebhook(ulong webhookId) => + _client.Get($"/webhooks/{webhookId}/webhooks", ("GetWebhook", webhookId))!; + + public Task GetChannelWebhooks(ulong channelId) => + _client.Get($"/channels/{channelId}/webhooks", ("GetChannelWebhooks", channelId))!; + + public Task ExecuteWebhook(ulong webhookId, string webhookToken, ExecuteWebhookRequest request, + MultipartFile[]? files = null) => + _client.PostMultipart($"/webhooks/{webhookId}/{webhookToken}", + ("ExecuteWebhook", webhookId), request, files)!; + + private static string EncodeEmoji(Emoji emoji) => + WebUtility.UrlEncode(emoji.Name) ?? emoji.Id?.ToString() ?? + throw new ArgumentException("Could not encode emoji"); + } +} \ No newline at end of file diff --git a/Myriad/Rest/DiscordApiError.cs b/Myriad/Rest/DiscordApiError.cs new file mode 100644 index 00000000..59cce46b --- /dev/null +++ b/Myriad/Rest/DiscordApiError.cs @@ -0,0 +1,9 @@ +using System.Text.Json; + +namespace Myriad.Rest +{ + public record DiscordApiError(string Message, int Code) + { + public JsonElement? Errors { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Exceptions/DiscordRequestException.cs b/Myriad/Rest/Exceptions/DiscordRequestException.cs new file mode 100644 index 00000000..6570ad81 --- /dev/null +++ b/Myriad/Rest/Exceptions/DiscordRequestException.cs @@ -0,0 +1,71 @@ +using System; +using System.Net; +using System.Net.Http; + +namespace Myriad.Rest.Exceptions +{ + public class DiscordRequestException: Exception + { + public DiscordRequestException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError) + { + RequestBody = requestBody; + Response = response; + ApiError = apiError; + } + + public string RequestBody { get; init; } = null!; + public HttpResponseMessage Response { get; init; } = null!; + + public HttpStatusCode StatusCode => Response.StatusCode; + public int? ErrorCode => ApiError?.Code; + + internal DiscordApiError? ApiError { get; init; } + + public override string Message => + (ApiError?.Message ?? Response.ReasonPhrase ?? "") + (FormError != null ? $": {FormError}" : ""); + + public string? FormError => ApiError?.Errors?.ToString(); + } + + public class NotFoundException: DiscordRequestException + { + public NotFoundException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( + response, requestBody, apiError) { } + } + + public class UnauthorizedException: DiscordRequestException + { + public UnauthorizedException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( + response, requestBody, apiError) { } + } + + public class ForbiddenException: DiscordRequestException + { + public ForbiddenException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( + response, requestBody, apiError) { } + } + + public class ConflictException: DiscordRequestException + { + public ConflictException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( + response, requestBody, apiError) { } + } + + public class BadRequestException: DiscordRequestException + { + public BadRequestException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( + response, requestBody, apiError) { } + } + + public class TooManyRequestsException: DiscordRequestException + { + public TooManyRequestsException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): + base(response, requestBody, apiError) { } + } + + public class UnknownDiscordRequestException: DiscordRequestException + { + public UnknownDiscordRequestException(HttpResponseMessage response, string requestBody, + DiscordApiError? apiError): base(response, requestBody, apiError) { } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Exceptions/RatelimitException.cs b/Myriad/Rest/Exceptions/RatelimitException.cs new file mode 100644 index 00000000..780f45ea --- /dev/null +++ b/Myriad/Rest/Exceptions/RatelimitException.cs @@ -0,0 +1,29 @@ +using System; + +using Myriad.Rest.Ratelimit; + +namespace Myriad.Rest.Exceptions +{ + public class RatelimitException: Exception + { + public RatelimitException(string? message): base(message) { } + } + + public class RatelimitBucketExhaustedException: RatelimitException + { + public RatelimitBucketExhaustedException(Bucket bucket, TimeSpan retryAfter): base( + "Rate limit bucket exhausted, request blocked") + { + Bucket = bucket; + RetryAfter = retryAfter; + } + + public Bucket Bucket { get; } + public TimeSpan RetryAfter { get; } + } + + public class GloballyRatelimitedException: RatelimitException + { + public GloballyRatelimitedException(): base("Global rate limit hit") { } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Ratelimit/Bucket.cs b/Myriad/Rest/Ratelimit/Bucket.cs new file mode 100644 index 00000000..31e7ea24 --- /dev/null +++ b/Myriad/Rest/Ratelimit/Bucket.cs @@ -0,0 +1,152 @@ +using System; +using System.Threading; + +using Serilog; + +namespace Myriad.Rest.Ratelimit +{ + public class Bucket + { + private static readonly TimeSpan Epsilon = TimeSpan.FromMilliseconds(10); + private static readonly TimeSpan FallbackDelay = TimeSpan.FromMilliseconds(200); + + private static readonly TimeSpan StaleTimeout = TimeSpan.FromSeconds(5); + + private readonly ILogger _logger; + private readonly SemaphoreSlim _semaphore = new(1, 1); + + private DateTimeOffset _nextReset; + private bool _resetTimeValid; + + public Bucket(ILogger logger, string key, ulong major, int limit) + { + _logger = logger.ForContext(); + + Key = key; + Major = major; + + Limit = limit; + Remaining = limit; + _resetTimeValid = false; + } + + public string Key { get; } + public ulong Major { get; } + + public int Remaining { get; private set; } + + public int Limit { get; private set; } + + public DateTimeOffset LastUsed { get; private set; } = DateTimeOffset.UtcNow; + + public bool TryAcquire() + { + LastUsed = DateTimeOffset.Now; + + try + { + _semaphore.Wait(); + + if (Remaining > 0) + { + _logger.Debug( + "{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit} left], allowing through", + Key, Major, Remaining, Limit); + Remaining--; + return true; + } + + _logger.Debug("{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit}] left, denying", + Key, Major, Remaining, Limit); + return false; + } + finally + { + _semaphore.Release(); + } + } + + public void HandleResponse(RatelimitHeaders headers) + { + try + { + _semaphore.Wait(); + + if (headers.ResetAfter != null) + { + var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time + if (headerNextReset > _nextReset) + { + _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server", + Key, Major, _nextReset); + + _nextReset = headerNextReset; + _resetTimeValid = true; + } + } + + if (headers.Limit != null) + Limit = headers.Limit.Value; + } + finally + { + _semaphore.Release(); + } + } + + public void Tick(DateTimeOffset now) + { + try + { + _semaphore.Wait(); + + // If we're past the reset time *and* we haven't reset already, do that + var timeSinceReset = _nextReset - now; + var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero; + if (shouldReset) + { + _logger.Debug("{BucketKey}/{BucketMajor}: Bucket timed out, refreshing with {BucketLimit} requests", + Key, Major, Limit); + Remaining = Limit; + _resetTimeValid = false; + return; + } + + // We've run out of requests without having any new reset time, + // *and* it's been longer than a set amount - add one request back to the pool and hope that one returns + var isBucketStale = !_resetTimeValid && Remaining <= 0 && timeSinceReset > StaleTimeout; + if (isBucketStale) + { + _logger.Warning( + "{BucketKey}/{BucketMajor}: Bucket is stale ({StaleTimeout} passed with no rate limit info), allowing one request through", + Key, Major, StaleTimeout); + + Remaining = 1; + + // Reset the (still-invalid) reset time to now, so we don't keep hitting this conditional over and over... + _nextReset = now; + } + } + finally + { + _semaphore.Release(); + } + } + + public TimeSpan GetResetDelay(DateTimeOffset now) + { + // If we don't have a valid reset time, return the fallback delay always + // (so it'll keep spinning until we hopefully have one...) + if (!_resetTimeValid) + return FallbackDelay; + + var delay = _nextReset - now; + + // If we have a really small (or negative) value, return a fallback delay too + if (delay < Epsilon) + return FallbackDelay; + + return delay; + } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Ratelimit/BucketManager.cs b/Myriad/Rest/Ratelimit/BucketManager.cs new file mode 100644 index 00000000..b5326903 --- /dev/null +++ b/Myriad/Rest/Ratelimit/BucketManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +using Serilog; + +namespace Myriad.Rest.Ratelimit +{ + public class BucketManager: IDisposable + { + private static readonly TimeSpan StaleBucketTimeout = TimeSpan.FromMinutes(5); + private static readonly TimeSpan PruneWorkerInterval = TimeSpan.FromMinutes(1); + private readonly ConcurrentDictionary<(string key, ulong major), Bucket> _buckets = new(); + + private readonly ConcurrentDictionary _endpointKeyMap = new(); + private readonly ConcurrentDictionary _knownKeyLimits = new(); + + private readonly ILogger _logger; + + private readonly Task _worker; + private readonly CancellationTokenSource _workerCts = new(); + + public BucketManager(ILogger logger) + { + _logger = logger.ForContext(); + _worker = PruneWorker(_workerCts.Token); + } + + public void Dispose() + { + _workerCts.Dispose(); + _worker.Dispose(); + } + + public Bucket? GetBucket(string endpoint, ulong major) + { + if (!_endpointKeyMap.TryGetValue(endpoint, out var key)) + return null; + + if (_buckets.TryGetValue((key, major), out var bucket)) + return bucket; + + if (!_knownKeyLimits.TryGetValue(key, out var knownLimit)) + return null; + + return _buckets.GetOrAdd((key, major), + k => new Bucket(_logger, k.Item1, k.Item2, knownLimit)); + } + + public void UpdateEndpointInfo(string endpoint, string key, int? limit) + { + _endpointKeyMap[endpoint] = key; + + if (limit != null) + _knownKeyLimits[key] = limit.Value; + } + + private async Task PruneWorker(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(PruneWorkerInterval, ct); + PruneStaleBuckets(DateTimeOffset.UtcNow); + } + } + + private void PruneStaleBuckets(DateTimeOffset now) + { + foreach (var (key, bucket) in _buckets) + if (now - bucket.LastUsed > StaleBucketTimeout) + { + _logger.Debug("Pruning unused bucket {Bucket} (last used at {BucketLastUsed})", bucket, + bucket.LastUsed); + _buckets.TryRemove(key, out _); + } + } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs b/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs new file mode 100644 index 00000000..9c9e2d00 --- /dev/null +++ b/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs @@ -0,0 +1,46 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Polly; + +namespace Myriad.Rest.Ratelimit +{ + public class DiscordRateLimitPolicy: AsyncPolicy + { + public const string EndpointContextKey = "Endpoint"; + public const string MajorContextKey = "Major"; + + private readonly Ratelimiter _ratelimiter; + + public DiscordRateLimitPolicy(Ratelimiter ratelimiter, PolicyBuilder? policyBuilder = null) + : base(policyBuilder) + { + _ratelimiter = ratelimiter; + } + + protected override async Task ImplementationAsync( + Func> action, Context context, CancellationToken ct, + bool continueOnCapturedContext) + { + if (!context.TryGetValue(EndpointContextKey, out var endpointObj) || !(endpointObj is string endpoint)) + throw new ArgumentException("Must provide endpoint in Polly context"); + + if (!context.TryGetValue(MajorContextKey, out var majorObj) || !(majorObj is ulong major)) + throw new ArgumentException("Must provide major in Polly context"); + + // Check rate limit, throw if we're not allowed... + _ratelimiter.AllowRequestOrThrow(endpoint, major, DateTimeOffset.Now); + + // We're OK, push it through + var response = await action(context, ct).ConfigureAwait(continueOnCapturedContext); + + // Update rate limit state with headers + var headers = new RatelimitHeaders(response); + _ratelimiter.HandleResponse(headers, endpoint, major); + + return response; + } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Ratelimit/RatelimitHeaders.cs b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs new file mode 100644 index 00000000..4a867deb --- /dev/null +++ b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Net.Http; + +namespace Myriad.Rest.Ratelimit +{ + public record RatelimitHeaders + { + public RatelimitHeaders() { } + + public RatelimitHeaders(HttpResponseMessage response) + { + ServerDate = response.Headers.Date; + + if (response.Headers.TryGetValues("X-RateLimit-Limit", out var limit)) + Limit = int.Parse(limit!.First()); + + if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining)) + Remaining = int.Parse(remaining!.First()); + + if (response.Headers.TryGetValues("X-RateLimit-Reset", out var reset)) + Reset = DateTimeOffset.FromUnixTimeMilliseconds((long) (double.Parse(reset!.First()) * 1000)); + + if (response.Headers.TryGetValues("X-RateLimit-Reset-After", out var resetAfter)) + ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter!.First())); + + if (response.Headers.TryGetValues("X-RateLimit-Bucket", out var bucket)) + Bucket = bucket.First(); + + if (response.Headers.TryGetValues("X-RateLimit-Global", out var global)) + Global = bool.Parse(global!.First()); + } + + public bool Global { get; init; } + public int? Limit { get; init; } + public int? Remaining { get; init; } + public DateTimeOffset? Reset { get; init; } + public TimeSpan? ResetAfter { get; init; } + public string? Bucket { get; init; } + + public DateTimeOffset? ServerDate { get; init; } + + public bool HasRatelimitInfo => + Limit != null && Remaining != null && Reset != null && ResetAfter != null && Bucket != null; + } +} \ No newline at end of file diff --git a/Myriad/Rest/Ratelimit/Ratelimiter.cs b/Myriad/Rest/Ratelimit/Ratelimiter.cs new file mode 100644 index 00000000..089656f1 --- /dev/null +++ b/Myriad/Rest/Ratelimit/Ratelimiter.cs @@ -0,0 +1,86 @@ +using System; + +using Myriad.Rest.Exceptions; + +using Serilog; + +namespace Myriad.Rest.Ratelimit +{ + public class Ratelimiter: IDisposable + { + private readonly BucketManager _buckets; + private readonly ILogger _logger; + + private DateTimeOffset? _globalRateLimitExpiry; + + public Ratelimiter(ILogger logger) + { + _logger = logger.ForContext(); + _buckets = new BucketManager(logger); + } + + public void Dispose() + { + _buckets.Dispose(); + } + + public void AllowRequestOrThrow(string endpoint, ulong major, DateTimeOffset now) + { + if (IsGloballyRateLimited(now)) + { + _logger.Warning("Globally rate limited until {GlobalRateLimitExpiry}, cancelling request", + _globalRateLimitExpiry); + throw new GloballyRatelimitedException(); + } + + var bucket = _buckets.GetBucket(endpoint, major); + if (bucket == null) + { + // No rate limit for this endpoint (yet), allow through + _logger.Debug("No rate limit data for endpoint {Endpoint}, allowing through", endpoint); + return; + } + + bucket.Tick(now); + + if (bucket.TryAcquire()) + // We're allowed to send it! :) + return; + + // We can't send this request right now; retrying... + var waitTime = bucket.GetResetDelay(now); + + // add a small buffer for Timing:tm: + waitTime += TimeSpan.FromMilliseconds(50); + + // (this is caught by a WaitAndRetry Polly handler, if configured) + throw new RatelimitBucketExhaustedException(bucket, waitTime); + } + + public void HandleResponse(RatelimitHeaders headers, string endpoint, ulong major) + { + if (!headers.HasRatelimitInfo) + return; + + // TODO: properly calculate server time? + if (headers.Global) + { + _logger.Warning( + "Global rate limit hit, resetting at {GlobalRateLimitExpiry} (in {GlobalRateLimitResetAfter}!", + _globalRateLimitExpiry, headers.ResetAfter); + _globalRateLimitExpiry = headers.Reset; + } + else + { + // Update buckets first, then get it again, to properly "transfer" this info over to the new value + _buckets.UpdateEndpointInfo(endpoint, headers.Bucket!, headers.Limit); + + var bucket = _buckets.GetBucket(endpoint, major); + bucket?.HandleResponse(headers); + } + } + + private bool IsGloballyRateLimited(DateTimeOffset now) => + _globalRateLimitExpiry > now; + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/AllowedMentions.cs b/Myriad/Rest/Types/AllowedMentions.cs new file mode 100644 index 00000000..019c735d --- /dev/null +++ b/Myriad/Rest/Types/AllowedMentions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Myriad.Rest.Types +{ + public record AllowedMentions + { + public enum ParseType + { + Roles, + Users, + Everyone + } + + public List? Parse { get; set; } + public List? Users { get; set; } + public List? Roles { get; set; } + public bool RepliedUser { get; set; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/MultipartFile.cs b/Myriad/Rest/Types/MultipartFile.cs new file mode 100644 index 00000000..e5a488d6 --- /dev/null +++ b/Myriad/Rest/Types/MultipartFile.cs @@ -0,0 +1,6 @@ +using System.IO; + +namespace Myriad.Rest.Types +{ + public record MultipartFile(string Filename, Stream Data); +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/CommandRequest.cs b/Myriad/Rest/Types/Requests/CommandRequest.cs new file mode 100644 index 00000000..3958f44b --- /dev/null +++ b/Myriad/Rest/Types/Requests/CommandRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +using Myriad.Types; + +namespace Myriad.Rest.Types +{ + public record ApplicationCommandRequest + { + public string Name { get; init; } + public string Description { get; init; } + public List? Options { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/CreateWebhookRequest.cs b/Myriad/Rest/Types/Requests/CreateWebhookRequest.cs new file mode 100644 index 00000000..cd38f67d --- /dev/null +++ b/Myriad/Rest/Types/Requests/CreateWebhookRequest.cs @@ -0,0 +1,4 @@ +namespace Myriad.Rest.Types.Requests +{ + public record CreateWebhookRequest(string Name); +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs new file mode 100644 index 00000000..dcd19a35 --- /dev/null +++ b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs @@ -0,0 +1,13 @@ +using Myriad.Types; + +namespace Myriad.Rest.Types.Requests +{ + public record ExecuteWebhookRequest + { + public string? Content { get; init; } + public string? Username { get; init; } + public string? AvatarUrl { get; init; } + public Embed[] Embeds { get; init; } + public AllowedMentions? AllowedMentions { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/MessageEditRequest.cs b/Myriad/Rest/Types/Requests/MessageEditRequest.cs new file mode 100644 index 00000000..1fe03193 --- /dev/null +++ b/Myriad/Rest/Types/Requests/MessageEditRequest.cs @@ -0,0 +1,10 @@ +using Myriad.Types; + +namespace Myriad.Rest.Types.Requests +{ + public record MessageEditRequest + { + public string? Content { get; set; } + public Embed? Embed { get; set; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs new file mode 100644 index 00000000..ae9625f7 --- /dev/null +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -0,0 +1,13 @@ +using Myriad.Types; + +namespace Myriad.Rest.Types.Requests +{ + public record MessageRequest + { + public string? Content { get; set; } + public object? Nonce { get; set; } + public bool Tts { get; set; } + public AllowedMentions AllowedMentions { get; set; } + public Embed? Embeds { get; set; } + } +} \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/ModifyGuildMemberRequest.cs b/Myriad/Rest/Types/Requests/ModifyGuildMemberRequest.cs new file mode 100644 index 00000000..6fcd8fc0 --- /dev/null +++ b/Myriad/Rest/Types/Requests/ModifyGuildMemberRequest.cs @@ -0,0 +1,7 @@ +namespace Myriad.Rest.Types +{ + public record ModifyGuildMemberRequest + { + public string? Nick { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..b72bec2e --- /dev/null +++ b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Myriad.Serialization +{ + public static class JsonSerializerOptionsExtensions + { + public static JsonSerializerOptions ConfigureForNewcord(this JsonSerializerOptions opts) + { + opts.PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy(); + opts.NumberHandling = JsonNumberHandling.AllowReadingFromString; + opts.IncludeFields = true; + + opts.Converters.Add(new PermissionSetJsonConverter()); + opts.Converters.Add(new ShardInfoJsonConverter()); + + return opts; + } + } +} \ No newline at end of file diff --git a/Myriad/Serialization/JsonSnakeCaseNamingPolicy.cs b/Myriad/Serialization/JsonSnakeCaseNamingPolicy.cs new file mode 100644 index 00000000..4a09d8f0 --- /dev/null +++ b/Myriad/Serialization/JsonSnakeCaseNamingPolicy.cs @@ -0,0 +1,88 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace Myriad.Serialization +{ + // From https://github.com/J0rgeSerran0/JsonNamingPolicy/blob/master/JsonSnakeCaseNamingPolicy.cs, no NuGet :/ + public class JsonSnakeCaseNamingPolicy: JsonNamingPolicy + { + private readonly string _separator = "_"; + + public override string ConvertName(string name) + { + if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(name)) return string.Empty; + + ReadOnlySpan spanName = name.Trim(); + + var stringBuilder = new StringBuilder(); + var addCharacter = true; + + var isPreviousSpace = false; + var isPreviousSeparator = false; + var isCurrentSpace = false; + var isNextLower = false; + var isNextUpper = false; + var isNextSpace = false; + + for (var position = 0; position < spanName.Length; position++) + { + if (position != 0) + { + isCurrentSpace = spanName[position] == 32; + isPreviousSpace = spanName[position - 1] == 32; + isPreviousSeparator = spanName[position - 1] == 95; + + if (position + 1 != spanName.Length) + { + isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; + isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; + isNextSpace = spanName[position + 1] == 32; + } + + if (isCurrentSpace && + (isPreviousSpace || + isPreviousSeparator || + isNextUpper || + isNextSpace)) + { + addCharacter = false; + } + else + { + var isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; + var isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; + var isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; + + if (isCurrentUpper && + (isPreviousLower || + isPreviousNumber || + isNextLower || + isNextSpace || + isNextLower && !isPreviousSpace)) + { + stringBuilder.Append(_separator); + } + else + { + if (isCurrentSpace && + !isPreviousSpace && + !isNextSpace) + { + stringBuilder.Append(_separator); + addCharacter = false; + } + } + } + } + + if (addCharacter) + stringBuilder.Append(spanName[position]); + else + addCharacter = true; + } + + return stringBuilder.ToString().ToLower(); + } + } +} \ No newline at end of file diff --git a/Myriad/Serialization/JsonStringConverter.cs b/Myriad/Serialization/JsonStringConverter.cs new file mode 100644 index 00000000..975da967 --- /dev/null +++ b/Myriad/Serialization/JsonStringConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Myriad.Serialization +{ + public class JsonStringConverter: JsonConverter + { + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = JsonSerializer.Deserialize(ref reader); + var inner = JsonSerializer.Deserialize(str!, typeToConvert, options); + return inner; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + var inner = JsonSerializer.Serialize(value, options); + writer.WriteStringValue(inner); + } + } +} \ No newline at end of file diff --git a/Myriad/Serialization/PermissionSetJsonConverter.cs b/Myriad/Serialization/PermissionSetJsonConverter.cs new file mode 100644 index 00000000..02fc313b --- /dev/null +++ b/Myriad/Serialization/PermissionSetJsonConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Myriad.Types; + +namespace Myriad.Serialization +{ + public class PermissionSetJsonConverter: JsonConverter + { + public override PermissionSet Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = reader.GetString(); + if (str == null) return default; + + return (PermissionSet) ulong.Parse(str); + } + + public override void Write(Utf8JsonWriter writer, PermissionSet value, JsonSerializerOptions options) + { + writer.WriteStringValue(((ulong) value).ToString()); + } + } +} \ No newline at end of file diff --git a/Myriad/Serialization/ShardInfoJsonConverter.cs b/Myriad/Serialization/ShardInfoJsonConverter.cs new file mode 100644 index 00000000..a504d1b3 --- /dev/null +++ b/Myriad/Serialization/ShardInfoJsonConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Myriad.Gateway; + +namespace Myriad.Serialization +{ + public class ShardInfoJsonConverter: JsonConverter + { + public override ShardInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var arr = JsonSerializer.Deserialize(ref reader); + if (arr?.Length != 2) + throw new JsonException("Expected shard info as array of length 2"); + + return new ShardInfo(arr[0], arr[1]); + } + + public override void Write(Utf8JsonWriter writer, ShardInfo value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + writer.WriteNumberValue(value.ShardId); + writer.WriteNumberValue(value.NumShards); + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/Myriad/Types/Activity.cs b/Myriad/Types/Activity.cs new file mode 100644 index 00000000..261a6c66 --- /dev/null +++ b/Myriad/Types/Activity.cs @@ -0,0 +1,22 @@ +namespace Myriad.Types +{ + public record Activity: ActivityPartial + { + } + + public record ActivityPartial + { + public string Name { get; init; } + public ActivityType Type { get; init; } + public string? Url { get; init; } + } + + public enum ActivityType + { + Game = 0, + Streaming = 1, + Listening = 2, + Custom = 4, + Competing = 5 + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/Application.cs b/Myriad/Types/Application/Application.cs new file mode 100644 index 00000000..1fe04127 --- /dev/null +++ b/Myriad/Types/Application/Application.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Myriad.Types +{ + public record Application: ApplicationPartial + { + public string Name { get; init; } + public string? Icon { get; init; } + public string Description { get; init; } + public string[]? RpcOrigins { get; init; } + public bool BotPublic { get; init; } + public bool BotRequireCodeGrant { get; init; } + public User Owner { get; init; } // TODO: docs specify this is "partial", what does that mean + public string Summary { get; init; } + public string VerifyKey { get; init; } + public ulong? GuildId { get; init; } + public ulong? PrimarySkuId { get; init; } + public string? Slug { get; init; } + public string? CoverImage { get; init; } + } + + public record ApplicationPartial + { + public ulong Id { get; init; } + public int Flags { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommand.cs b/Myriad/Types/Application/ApplicationCommand.cs new file mode 100644 index 00000000..92ecd856 --- /dev/null +++ b/Myriad/Types/Application/ApplicationCommand.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Myriad.Types +{ + public record ApplicationCommand + { + public ulong Id { get; init; } + public ulong ApplicationId { get; init; } + public string Name { get; init; } + public string Description { get; init; } + public ApplicationCommandOption[]? Options { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommandInteractionData.cs b/Myriad/Types/Application/ApplicationCommandInteractionData.cs new file mode 100644 index 00000000..3c4543a3 --- /dev/null +++ b/Myriad/Types/Application/ApplicationCommandInteractionData.cs @@ -0,0 +1,9 @@ +namespace Myriad.Types +{ + public record ApplicationCommandInteractionData + { + public ulong Id { get; init; } + public string Name { get; init; } + public ApplicationCommandInteractionDataOption[] Options { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommandInteractionDataOption.cs b/Myriad/Types/Application/ApplicationCommandInteractionDataOption.cs new file mode 100644 index 00000000..0f5a2730 --- /dev/null +++ b/Myriad/Types/Application/ApplicationCommandInteractionDataOption.cs @@ -0,0 +1,9 @@ +namespace Myriad.Types +{ + public record ApplicationCommandInteractionDataOption + { + public string Name { get; init; } + public object? Value { get; init; } + public ApplicationCommandInteractionDataOption[]? Options { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommandOption.cs b/Myriad/Types/Application/ApplicationCommandOption.cs new file mode 100644 index 00000000..09ba945f --- /dev/null +++ b/Myriad/Types/Application/ApplicationCommandOption.cs @@ -0,0 +1,24 @@ +namespace Myriad.Types +{ + public record ApplicationCommandOption(ApplicationCommandOption.OptionType Type, string Name, string Description) + { + public enum OptionType + { + Subcommand = 1, + SubcommandGroup = 2, + String = 3, + Integer = 4, + Boolean = 5, + User = 6, + Channel = 7, + Role = 8 + } + + public bool Default { get; init; } + public bool Required { get; init; } + public Choice[]? Choices { get; init; } + public ApplicationCommandOption[]? Options { get; init; } + + public record Choice(string Name, object Value); + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/Interaction.cs b/Myriad/Types/Application/Interaction.cs new file mode 100644 index 00000000..cc269f3a --- /dev/null +++ b/Myriad/Types/Application/Interaction.cs @@ -0,0 +1,19 @@ +namespace Myriad.Types +{ + public record Interaction + { + public enum InteractionType + { + Ping = 1, + ApplicationCommand = 2 + } + + public ulong Id { get; init; } + public InteractionType Type { get; init; } + public ApplicationCommandInteractionData? Data { get; init; } + public ulong GuildId { get; init; } + public ulong ChannelId { get; init; } + public GuildMember Member { get; init; } + public string Token { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs b/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs new file mode 100644 index 00000000..2718aa0e --- /dev/null +++ b/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +using Myriad.Rest.Types; + +namespace Myriad.Types +{ + public record InteractionApplicationCommandCallbackData + { + public bool? Tts { get; init; } + public string Content { get; init; } + public Embed[]? Embeds { get; init; } + public AllowedMentions? AllowedMentions { get; init; } + public Message.MessageFlags Flags { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Application/InteractionResponse.cs b/Myriad/Types/Application/InteractionResponse.cs new file mode 100644 index 00000000..12e1259d --- /dev/null +++ b/Myriad/Types/Application/InteractionResponse.cs @@ -0,0 +1,17 @@ +namespace Myriad.Types +{ + public record InteractionResponse + { + public enum ResponseType + { + Pong = 1, + Acknowledge = 2, + ChannelMessage = 3, + ChannelMessageWithSource = 4, + AckWithSource = 5 + } + + public ResponseType Type { get; init; } + public InteractionApplicationCommandCallbackData? Data { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Channel.cs b/Myriad/Types/Channel.cs new file mode 100644 index 00000000..72e1854c --- /dev/null +++ b/Myriad/Types/Channel.cs @@ -0,0 +1,40 @@ +namespace Myriad.Types +{ + public record Channel + { + public enum ChannelType + { + GuildText = 0, + Dm = 1, + GuildVoice = 2, + GroupDm = 3, + GuildCategory = 4, + GuildNews = 5, + GuildStore = 6 + } + + public ulong Id { get; init; } + public ChannelType Type { get; init; } + public ulong? GuildId { get; init; } + public int? Position { get; init; } + public string? Name { get; init; } + public string? Topic { get; init; } + public bool? Nsfw { get; init; } + public long? ParentId { get; init; } + public Overwrite[]? PermissionOverwrites { get; init; } + + public record Overwrite + { + public ulong Id { get; init; } + public OverwriteType Type { get; init; } + public PermissionSet Allow { get; init; } + public PermissionSet Deny { get; init; } + } + + public enum OverwriteType + { + Role = 0, + Member = 1 + } + } +} \ No newline at end of file diff --git a/Myriad/Types/Embed.cs b/Myriad/Types/Embed.cs new file mode 100644 index 00000000..46560cb8 --- /dev/null +++ b/Myriad/Types/Embed.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace Myriad.Types +{ + public record Embed + { + public string? Title { get; init; } + public string? Type { get; init; } + public string? Description { get; init; } + public string? Url { get; init; } + public string? Timestamp { get; init; } + public uint? Color { get; init; } + public EmbedFooter? Footer { get; init; } + public EmbedImage? Image { get; init; } + public EmbedThumbnail? Thumbnail { get; init; } + public EmbedVideo? Video { get; init; } + public EmbedProvider? Provider { get; init; } + public EmbedAuthor? Author { get; init; } + public Field[]? Fields { get; init; } + + public record EmbedFooter ( + string Text, + string? IconUrl = null, + string? ProxyIconUrl = null + ); + + public record EmbedImage ( + string? Url, + uint? Width = null, + uint? Height = null + ); + + public record EmbedThumbnail ( + string? Url, + string? ProxyUrl = null, + uint? Width = null, + uint? Height = null + ); + + public record EmbedVideo ( + string? Url, + uint? Width = null, + uint? Height = null + ); + + public record EmbedProvider ( + string? Name, + string? Url + ); + + public record EmbedAuthor ( + string? Name = null, + string? Url = null, + string? IconUrl = null, + string? ProxyIconUrl = null + ); + + public record Field ( + string Name, + string Value, + bool Inline = false + ); + } +} \ No newline at end of file diff --git a/Myriad/Types/Emoji.cs b/Myriad/Types/Emoji.cs new file mode 100644 index 00000000..415d42a1 --- /dev/null +++ b/Myriad/Types/Emoji.cs @@ -0,0 +1,9 @@ +namespace Myriad.Types +{ + public record Emoji + { + public ulong? Id { get; init; } + public string? Name { get; init; } + public bool? Animated { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Gateway/GatewayInfo.cs b/Myriad/Types/Gateway/GatewayInfo.cs new file mode 100644 index 00000000..055681cb --- /dev/null +++ b/Myriad/Types/Gateway/GatewayInfo.cs @@ -0,0 +1,13 @@ +namespace Myriad.Types +{ + public record GatewayInfo + { + public string Url { get; init; } + + public record Bot: GatewayInfo + { + public int Shards { get; init; } + public SessionStartLimit SessionStartLimit { get; init; } + } + } +} \ No newline at end of file diff --git a/Myriad/Types/Gateway/SessionStartLimit.cs b/Myriad/Types/Gateway/SessionStartLimit.cs new file mode 100644 index 00000000..381c7cd9 --- /dev/null +++ b/Myriad/Types/Gateway/SessionStartLimit.cs @@ -0,0 +1,9 @@ +namespace Myriad.Types +{ + public record SessionStartLimit + { + public int Total { get; init; } + public int Remaining { get; init; } + public int ResetAfter { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Guild.cs b/Myriad/Types/Guild.cs new file mode 100644 index 00000000..9b9cccfe --- /dev/null +++ b/Myriad/Types/Guild.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Myriad.Types +{ + public record Guild + { + public ulong Id { get; init; } + public string Name { get; init; } + public string? Icon { get; init; } + public string? Splash { get; init; } + public string? DiscoverySplash { get; init; } + public bool? Owner { get; init; } + public ulong OwnerId { get; init; } + public string Region { get; init; } + public ulong? AfkChannelId { get; init; } + public int AfkTimeout { get; init; } + public bool? WidgetEnabled { get; init; } + public bool? WidgetChannelId { get; init; } + public int VerificationLevel { get; init; } + + public Role[] Roles { get; init; } + public string[] Features { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/GuildMember.cs b/Myriad/Types/GuildMember.cs new file mode 100644 index 00000000..da25fd65 --- /dev/null +++ b/Myriad/Types/GuildMember.cs @@ -0,0 +1,14 @@ +namespace Myriad.Types +{ + public record GuildMember: GuildMemberPartial + { + public User User { get; init; } + } + + public record GuildMemberPartial + { + public string Nick { get; init; } + public ulong[] Roles { get; init; } + public string JoinedAt { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs new file mode 100644 index 00000000..c74f67bf --- /dev/null +++ b/Myriad/Types/Message.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Net.Mail; + +namespace Myriad.Types +{ + public record Message + { + [Flags] + public enum MessageFlags + { + Crossposted = 1 << 0, + IsCrosspost = 1 << 1, + SuppressEmbeds = 1 << 2, + SourceMessageDeleted = 1 << 3, + Urgent = 1 << 4, + Ephemeral = 1 << 6 + } + + public enum MessageType + { + Default = 0, + RecipientAdd = 1, + RecipientRemove = 2, + Call = 3, + ChannelNameChange = 4, + ChannelIconChange = 5, + ChannelPinnedMessage = 6, + GuildMemberJoin = 7, + UserPremiumGuildSubscription = 8, + UserPremiumGuildSubscriptionTier1 = 9, + UserPremiumGuildSubscriptionTier2 = 10, + UserPremiumGuildSubscriptionTier3 = 11, + ChannelFollowAdd = 12, + GuildDiscoveryDisqualified = 14, + GuildDiscoveryRequalified = 15, + Reply = 19, + ApplicationCommand = 20 + } + + public ulong Id { get; init; } + public ulong ChannelId { get; init; } + public ulong? GuildId { get; init; } + public User Author { get; init; } + public string? Content { get; init; } + public string? Timestamp { get; init; } + public string? EditedTimestamp { get; init; } + public bool Tts { get; init; } + public bool MentionEveryone { get; init; } + public User.Extra[] Mentions { get; init; } + public ulong[] MentionRoles { get; init; } + + public Attachment[] Attachments { get; init; } + public Embed[] Embeds { get; init; } + public Reaction[] Reactions { get; init; } + public bool Pinned { get; init; } + public ulong? WebhookId { get; init; } + public MessageType Type { get; init; } + public Reference? MessageReference { get; set; } + public MessageFlags Flags { get; init; } + + // todo: null vs. absence + public Message? ReferencedMessage { get; init; } + + public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); + + public record Attachment + { + public ulong Id { get; init; } + public string Filename { get; init; } + public int Size { get; init; } + public string Url { get; init; } + public string ProxyUrl { get; init; } + public int? Width { get; init; } + public int? Height { get; init; } + } + + public record Reaction + { + public int Count { get; init; } + public bool Me { get; init; } + public Emoji Emoji { get; init; } + } + } +} \ No newline at end of file diff --git a/Myriad/Types/PermissionSet.cs b/Myriad/Types/PermissionSet.cs new file mode 100644 index 00000000..ce4c3a06 --- /dev/null +++ b/Myriad/Types/PermissionSet.cs @@ -0,0 +1,47 @@ +using System; + +namespace Myriad.Types +{ + [Flags] + public enum PermissionSet: ulong + { + CreateInvite = 0x1, + KickMembers = 0x2, + BanMembers = 0x4, + Administrator = 0x8, + ManageChannels = 0x10, + ManageGuild = 0x20, + AddReactions = 0x40, + ViewAuditLog = 0x80, + PrioritySpeaker = 0x100, + Stream = 0x200, + ViewChannel = 0x400, + SendMessages = 0x800, + SendTtsMessages = 0x1000, + ManageMessages = 0x2000, + EmbedLinks = 0x4000, + AttachFiles = 0x8000, + ReadMessageHistory = 0x10000, + MentionEveryone = 0x20000, + UseExternalEmojis = 0x40000, + ViewGuildInsights = 0x80000, + Connect = 0x100000, + Speak = 0x200000, + MuteMembers = 0x400000, + DeafenMembers = 0x800000, + MoveMembers = 0x1000000, + UseVad = 0x2000000, + ChangeNickname = 0x4000000, + ManageNicknames = 0x8000000, + ManageRoles = 0x10000000, + ManageWebhooks = 0x20000000, + ManageEmojis = 0x40000000, + + // Special: + None = 0, + All = 0x7FFFFFFF, + + Dm = ViewChannel | SendMessages | ReadMessageHistory | AddReactions | AttachFiles | EmbedLinks | + UseExternalEmojis | Connect | Speak | UseVad + } +} \ No newline at end of file diff --git a/Myriad/Types/Permissions.cs b/Myriad/Types/Permissions.cs new file mode 100644 index 00000000..423b7ae8 --- /dev/null +++ b/Myriad/Types/Permissions.cs @@ -0,0 +1,6 @@ +namespace Myriad.Types +{ + public static class Permissions + { + } +} \ No newline at end of file diff --git a/Myriad/Types/Role.cs b/Myriad/Types/Role.cs new file mode 100644 index 00000000..2e77f2ac --- /dev/null +++ b/Myriad/Types/Role.cs @@ -0,0 +1,14 @@ +namespace Myriad.Types +{ + public record Role + { + public ulong Id { get; init; } + public string Name { get; init; } + public uint Color { get; init; } + public bool Hoist { get; init; } + public int Position { get; init; } + public PermissionSet Permissions { get; init; } + public bool Managed { get; init; } + public bool Mentionable { get; init; } + } +} \ No newline at end of file diff --git a/Myriad/Types/User.cs b/Myriad/Types/User.cs new file mode 100644 index 00000000..ad6a4795 --- /dev/null +++ b/Myriad/Types/User.cs @@ -0,0 +1,38 @@ +using System; + +namespace Myriad.Types +{ + public record User + { + [Flags] + public enum Flags + { + DiscordEmployee = 1 << 0, + PartneredServerOwner = 1 << 1, + HypeSquadEvents = 1 << 2, + BugHunterLevel1 = 1 << 3, + HouseBravery = 1 << 6, + HouseBrilliance = 1 << 7, + HouseBalance = 1 << 8, + EarlySupporter = 1 << 9, + TeamUser = 1 << 10, + System = 1 << 12, + BugHunterLevel2 = 1 << 14, + VerifiedBot = 1 << 16, + EarlyVerifiedBotDeveloper = 1 << 17 + } + + public ulong Id { get; init; } + public string Username { get; init; } + public string Discriminator { get; init; } + public string? Avatar { get; init; } + public bool Bot { get; init; } + public bool? System { get; init; } + public Flags PublicFlags { get; init; } + + public record Extra: User + { + public GuildMemberPartial? Member { get; init; } + } + } +} \ No newline at end of file diff --git a/Myriad/Types/Webhook.cs b/Myriad/Types/Webhook.cs new file mode 100644 index 00000000..d5ac3c03 --- /dev/null +++ b/Myriad/Types/Webhook.cs @@ -0,0 +1,21 @@ +namespace Myriad.Types +{ + public record Webhook + { + public ulong Id { get; init; } + public WebhookType Type { get; init; } + public ulong? GuildId { get; init; } + public ulong ChannelId { get; init; } + public User? User { get; init; } + public string? Name { get; init; } + public string? Avatar { get; init; } + public string? Token { get; init; } + public ulong? ApplicationId { get; init; } + } + + public enum WebhookType + { + Incoming = 1, + ChannelFollower = 2 + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index f60ca6c9..b7914d91 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; @@ -9,10 +10,10 @@ using App.Metrics; using Autofac; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; +using Myriad.Cache; +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Types; using NodaTime; @@ -27,47 +28,38 @@ namespace PluralKit.Bot { public class Bot { - private readonly DiscordShardedClient _client; + private readonly ConcurrentDictionary _guildMembers = new(); + + private readonly Cluster _cluster; + private readonly DiscordApiClient _rest; private readonly ILogger _logger; private readonly ILifetimeScope _services; private readonly PeriodicStatCollector _collector; private readonly IMetrics _metrics; private readonly ErrorMessageService _errorMessageService; private readonly CommandMessageService _commandMessageService; + private readonly IDiscordCache _cache; private bool _hasReceivedReady = false; private Timer _periodicTask; // Never read, just kept here for GC reasons - public Bot(DiscordShardedClient client, ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics, - ErrorMessageService errorMessageService, CommandMessageService commandMessageService) + public Bot(ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics, + ErrorMessageService errorMessageService, CommandMessageService commandMessageService, Cluster cluster, DiscordApiClient rest, IDiscordCache cache) { - _client = client; _logger = logger.ForContext(); _services = services; _collector = collector; _metrics = metrics; _errorMessageService = errorMessageService; _commandMessageService = commandMessageService; + _cluster = cluster; + _rest = rest; + _cache = cache; } public void Init() { - // HandleEvent takes a type parameter, automatically inferred by the event type - // It will then look up an IEventHandler in the DI container and call that object's handler method - // For registering new ones, see Modules.cs - _client.MessageCreated += HandleEvent; - _client.MessageDeleted += HandleEvent; - _client.MessageUpdated += HandleEvent; - _client.MessagesBulkDeleted += HandleEvent; - _client.MessageReactionAdded += HandleEvent; - - // Update shard status for shards immediately on connect - _client.Ready += (client, _) => - { - _hasReceivedReady = true; - return UpdateBotStatus(client); - }; - _client.Resumed += (client, _) => UpdateBotStatus(client); + _cluster.EventReceived += OnEventReceived; // Init the shard stuff _services.Resolve().Init(); @@ -83,6 +75,58 @@ namespace PluralKit.Bot }, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1)); } + public GuildMemberPartial? BotMemberIn(ulong guildId) => _guildMembers.GetValueOrDefault(guildId); + + private async Task OnEventReceived(Shard shard, IGatewayEvent evt) + { + await _cache.HandleGatewayEvent(evt); + + TryUpdateSelfMember(shard, evt); + + // HandleEvent takes a type parameter, automatically inferred by the event type + // It will then look up an IEventHandler in the DI container and call that object's handler method + // For registering new ones, see Modules.cs + if (evt is MessageCreateEvent mc) + await HandleEvent(shard, mc); + if (evt is MessageUpdateEvent mu) + await HandleEvent(shard, mu); + if (evt is MessageDeleteEvent md) + await HandleEvent(shard, md); + if (evt is MessageDeleteBulkEvent mdb) + await HandleEvent(shard, mdb); + if (evt is MessageReactionAddEvent mra) + await HandleEvent(shard, mra); + + // Update shard status for shards immediately on connect + if (evt is ReadyEvent re) + await HandleReady(shard, re); + if (evt is ResumedEvent) + await HandleResumed(shard); + } + + private void TryUpdateSelfMember(Shard shard, IGatewayEvent evt) + { + if (evt is GuildCreateEvent gc) + _guildMembers[gc.Id] = gc.Members.FirstOrDefault(m => m.User.Id == shard.User?.Id); + if (evt is MessageCreateEvent mc && mc.Member != null && mc.Author.Id == shard.User?.Id) + _guildMembers[mc.GuildId!.Value] = mc.Member; + if (evt is GuildMemberAddEvent gma && gma.User.Id == shard.User?.Id) + _guildMembers[gma.GuildId] = gma; + if (evt is GuildMemberUpdateEvent gmu && gmu.User.Id == shard.User?.Id) + _guildMembers[gmu.GuildId] = gmu; + } + + private Task HandleResumed(Shard shard) + { + return UpdateBotStatus(shard); + } + + private Task HandleReady(Shard shard, ReadyEvent _) + { + _hasReceivedReady = true; + return UpdateBotStatus(shard); + } + public async Task Shutdown() { // This will stop the timer and prevent any subsequent invocations @@ -92,10 +136,24 @@ namespace PluralKit.Bot // We're not actually properly disconnecting from the gateway (lol) so it'll linger for a few minutes // Should be plenty of time for the bot to connect again next startup and set the real status if (_hasReceivedReady) - await _client.UpdateStatusAsync(new DiscordActivity("Restarting... (please wait)"), UserStatus.Idle); + { + await Task.WhenAll(_cluster.Shards.Values.Select(shard => + shard.UpdateStatus(new GatewayStatusUpdate + { + Activities = new[] + { + new ActivityPartial + { + Name = "Restarting... (please wait)", + Type = ActivityType.Game + } + }, + Status = GatewayStatusUpdate.UserStatus.Idle + }))); + } } - private Task HandleEvent(DiscordClient shard, T evt) where T: DiscordEventArgs + private Task HandleEvent(Shard shard, T evt) where T: IGatewayEvent { // We don't want to stall the event pipeline, so we'll "fork" inside here var _ = HandleEventInner(); @@ -121,7 +179,7 @@ namespace PluralKit.Bot try { using var timer = _metrics.Measure.Timer.Time(BotMetrics.EventsHandled, - new MetricTags("event", typeof(T).Name.Replace("EventArgs", ""))); + new MetricTags("event", typeof(T).Name.Replace("Event", ""))); // Delegate to the queue to see if it wants to handle this event // the TryHandle call returns true if it's handled the event @@ -131,13 +189,13 @@ namespace PluralKit.Bot } catch (Exception exc) { - await HandleError(handler, evt, serviceScope, exc); + await HandleError(shard, handler, evt, serviceScope, exc); } } } - - private async Task HandleError(IEventHandler handler, T evt, ILifetimeScope serviceScope, Exception exc) - where T: DiscordEventArgs + + private async Task HandleError(Shard shard, IEventHandler handler, T evt, ILifetimeScope serviceScope, Exception exc) + where T: IGatewayEvent { _metrics.Measure.Meter.Mark(BotMetrics.BotErrors, exc.GetType().FullName); @@ -149,7 +207,7 @@ namespace PluralKit.Bot .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) + if (evt is MessageCreateEvent mc && mc.Author.Id == shard.User?.Id) return; var shouldReport = exc.IsOurProblem(); @@ -160,19 +218,21 @@ namespace PluralKit.Bot var sentryScope = serviceScope.Resolve(); // Add some specific info about Discord error responses, as a breadcrumb - if (exc is BadRequestException bre) - sentryScope.AddBreadcrumb(bre.WebResponse.Response, "response.error", data: new Dictionary(bre.WebResponse.Headers)); - if (exc is NotFoundException nfe) - sentryScope.AddBreadcrumb(nfe.WebResponse.Response, "response.error", data: new Dictionary(nfe.WebResponse.Headers)); - if (exc is UnauthorizedException ue) - sentryScope.AddBreadcrumb(ue.WebResponse.Response, "response.error", data: new Dictionary(ue.WebResponse.Headers)); + // TODO: headers to dict + // if (exc is BadRequestException bre) + // sentryScope.AddBreadcrumb(bre.Response, "response.error", data: new Dictionary(bre.Response.Headers)); + // if (exc is NotFoundException nfe) + // sentryScope.AddBreadcrumb(nfe.Response, "response.error", data: new Dictionary(nfe.Response.Headers)); + // if (exc is UnauthorizedException ue) + // sentryScope.AddBreadcrumb(ue.Response, "response.error", data: new Dictionary(ue.Response.Headers)); 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.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) - await _errorMessageService.SendErrorMessage(reportChannel, sentryEvent.EventId.ToString()); + // TODO: ID lookup + // if (reportChannel != null && reportChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) + // await _errorMessageService.SendErrorMessage(reportChannel, sentryEvent.EventId.ToString()); } } @@ -191,23 +251,38 @@ namespace PluralKit.Bot _logger.Debug("Submitted metrics to backend"); } - private async Task UpdateBotStatus(DiscordClient specificShard = null) + private async Task UpdateBotStatus(Shard specificShard = null) { // If we're not on any shards, don't bother (this happens if the periodic timer fires before the first Ready) if (!_hasReceivedReady) return; - - var totalGuilds = _client.ShardClients.Values.Sum(c => c.Guilds.Count); + + var totalGuilds = await _cache.GetAllGuilds().CountAsync(); + try // DiscordClient may throw an exception if the socket is closed (e.g just after OP 7 received) { - Task UpdateStatus(DiscordClient shard) => - shard.UpdateStatusAsync(new DiscordActivity($"pk;help | in {totalGuilds} servers | shard #{shard.ShardId}")); - + Task UpdateStatus(Shard shard) => + shard.UpdateStatus(new GatewayStatusUpdate + { + Activities = new[] + { + new ActivityPartial + { + Name = $"pk;help | in {totalGuilds} servers | shard #{shard.ShardInfo?.ShardId}", + Type = ActivityType.Game, + Url = "https://pluralkit.me/" + } + } + }); + if (specificShard != null) await UpdateStatus(specificShard); else // Run shard updates concurrently - await Task.WhenAll(_client.ShardClients.Values.Select(UpdateStatus)); + await Task.WhenAll(_cluster.Shards.Values.Select(UpdateStatus)); + } + catch (WebSocketException) + { + // TODO: this still thrown? } - catch (WebSocketException) { } } } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index effdfe46..ad96ecd5 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -9,8 +9,14 @@ using Autofac; using DSharpPlus; using DSharpPlus.Entities; +using Myriad.Extensions; +using Myriad.Gateway; +using Myriad.Types; + using PluralKit.Core; +using Permissions = DSharpPlus.Permissions; + namespace PluralKit.Bot { public class Context @@ -19,10 +25,17 @@ namespace PluralKit.Bot private readonly DiscordRestClient _rest; private readonly DiscordShardedClient _client; - private readonly DiscordClient _shard; - private readonly DiscordMessage _message; + private readonly DiscordClient _shard = null; + private readonly Shard _shardNew; + private readonly Guild? _guild; + private readonly Channel _channel; + private readonly DiscordMessage _message = null; + private readonly Message _messageNew; private readonly Parameters _parameters; private readonly MessageContext _messageContext; + private readonly GuildMemberPartial? _botMember; + private readonly PermissionSet _botPermissions; + private readonly PermissionSet _userPermissions; private readonly IDatabase _db; private readonly ModelRepository _repo; @@ -32,31 +45,47 @@ namespace PluralKit.Bot private Command _currentCommand; - public Context(ILifetimeScope provider, DiscordClient shard, DiscordMessage message, int commandParseOffset, - PKSystem senderSystem, MessageContext messageContext) + public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, + PKSystem senderSystem, MessageContext messageContext, GuildMemberPartial? botMember) { _rest = provider.Resolve(); _client = provider.Resolve(); - _message = message; - _shard = shard; + _messageNew = message; + _shardNew = shard; + _guild = guild; + _channel = channel; _senderSystem = senderSystem; _messageContext = messageContext; + _botMember = botMember; _db = provider.Resolve(); _repo = provider.Resolve(); _metrics = provider.Resolve(); _provider = provider; _commandMessageService = provider.Resolve(); _parameters = new Parameters(message.Content.Substring(commandParseOffset)); + + _botPermissions = message.GuildId != null + ? PermissionExtensions.PermissionsFor(guild!, channel, shard.User?.Id ?? default, botMember!.Roles) + : PermissionSet.Dm; + _userPermissions = message.GuildId != null + ? PermissionExtensions.PermissionsFor(guild!, channel, message.Author.Id, message.Member!.Roles) + : PermissionSet.Dm; } public DiscordUser Author => _message.Author; public DiscordChannel Channel => _message.Channel; + public Channel ChannelNew => _channel; public DiscordMessage Message => _message; + public Message MessageNew => _messageNew; public DiscordGuild Guild => _message.Channel.Guild; + public Guild GuildNew => _guild; public DiscordClient Shard => _shard; public DiscordShardedClient Client => _client; public MessageContext MessageContext => _messageContext; + public PermissionSet BotPermissions => _botPermissions; + public PermissionSet UserPermissions => _userPermissions; + public DiscordRestClient Rest => _rest; public PKSystem System => _senderSystem; diff --git a/PluralKit.Bot/Handlers/IEventHandler.cs b/PluralKit.Bot/Handlers/IEventHandler.cs index 839eeba0..4a086706 100644 --- a/PluralKit.Bot/Handlers/IEventHandler.cs +++ b/PluralKit.Bot/Handlers/IEventHandler.cs @@ -1,15 +1,13 @@ using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; +using Myriad.Gateway; namespace PluralKit.Bot { - public interface IEventHandler where T: DiscordEventArgs + public interface IEventHandler where T: IGatewayEvent { - Task Handle(DiscordClient shard, T evt); + Task Handle(Shard shard, T evt); - DiscordChannel ErrorChannelFor(T evt) => null; + ulong? ErrorChannelFor(T evt) => null; } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index d51fe0fc..95cc9995 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -5,18 +5,22 @@ using App.Metrics; using Autofac; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; +using Myriad.Cache; +using Myriad.Extensions; +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Rest.Types.Requests; +using Myriad.Types; using PluralKit.Core; namespace PluralKit.Bot { - public class MessageCreated: IEventHandler + public class MessageCreated: IEventHandler { + private readonly Bot _bot; private readonly CommandTree _tree; - private readonly DiscordShardedClient _client; + private readonly IDiscordCache _cache; private readonly LastMessageCacheService _lastMessageCache; private readonly LoggerCleanService _loggerClean; private readonly IMetrics _metrics; @@ -25,73 +29,81 @@ namespace PluralKit.Bot private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly BotConfig _config; + private readonly DiscordApiClient _rest; public MessageCreated(LastMessageCacheService lastMessageCache, LoggerCleanService loggerClean, - IMetrics metrics, ProxyService proxy, DiscordShardedClient client, - CommandTree tree, ILifetimeScope services, IDatabase db, BotConfig config, ModelRepository repo) + IMetrics metrics, ProxyService proxy, + CommandTree tree, ILifetimeScope services, IDatabase db, BotConfig config, ModelRepository repo, IDiscordCache cache, Bot bot, DiscordApiClient rest) { _lastMessageCache = lastMessageCache; _loggerClean = loggerClean; _metrics = metrics; _proxy = proxy; - _client = client; _tree = tree; _services = services; _db = db; _config = config; _repo = repo; + _cache = cache; + _bot = bot; + _rest = rest; } - public DiscordChannel ErrorChannelFor(MessageCreateEventArgs evt) => evt.Channel; + public ulong? ErrorChannelFor(MessageCreateEvent evt) => evt.ChannelId; - private bool IsDuplicateMessage(DiscordMessage evt) => + private bool IsDuplicateMessage(Message msg) => // We consider a message duplicate if it has the same ID as the previous message that hit the gateway - _lastMessageCache.GetLastMessage(evt.ChannelId) == evt.Id; + _lastMessageCache.GetLastMessage(msg.ChannelId) == msg.Id; - public async Task Handle(DiscordClient shard, MessageCreateEventArgs evt) + public async Task Handle(Shard shard, MessageCreateEvent evt) { - if (evt.Author?.Id == _client.CurrentUser?.Id) return; - if (evt.Message.MessageType != MessageType.Default) return; - if (IsDuplicateMessage(evt.Message)) return; + if (evt.Author.Id == shard.User?.Id) return; + if (evt.Type != Message.MessageType.Default) return; + if (IsDuplicateMessage(evt)) return; + + var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null; + var channel = await _cache.GetChannel(evt.ChannelId); // Log metrics and message info _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); - _lastMessageCache.AddMessage(evt.Channel.Id, evt.Message.Id); + _lastMessageCache.AddMessage(evt.ChannelId, evt.Id); // Get message context from DB (tracking w/ metrics) MessageContext ctx; await using (var conn = await _db.Obtain()) using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) - ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.Channel.GuildId, evt.Channel.Id); - + ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.GuildId ?? default, evt.ChannelId); + // Try each handler until we find one that succeeds if (await TryHandleLogClean(evt, ctx)) return; // Only do command/proxy handling if it's a user account - if (evt.Message.Author.IsBot || evt.Message.WebhookMessage || evt.Message.Author.IsSystem == true) + if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true) return; - if (await TryHandleCommand(shard, evt, ctx)) + + if (await TryHandleCommand(shard, evt, guild, channel, ctx)) return; - await TryHandleProxy(shard, evt, ctx); + await TryHandleProxy(shard, evt, guild, channel, ctx); } - private async ValueTask TryHandleLogClean(MessageCreateEventArgs evt, MessageContext ctx) + private async ValueTask TryHandleLogClean(MessageCreateEvent evt, MessageContext ctx) { - if (!evt.Message.Author.IsBot || evt.Message.Channel.Type != ChannelType.Text || + var channel = await _cache.GetChannel(evt.ChannelId); + if (!evt.Author.Bot || channel!.Type != Channel.ChannelType.GuildText || !ctx.LogCleanupEnabled) return false; - await _loggerClean.HandleLoggerBotCleanup(evt.Message); + await _loggerClean.HandleLoggerBotCleanup(evt); return true; } - private async ValueTask TryHandleCommand(DiscordClient shard, MessageCreateEventArgs evt, MessageContext ctx) + private async ValueTask TryHandleCommand(Shard shard, MessageCreateEvent evt, Guild? guild, Channel channel, MessageContext ctx) { - var content = evt.Message.Content; + var content = evt.Content; if (content == null) return false; // Check for command prefix - if (!HasCommandPrefix(content, out var cmdStart)) + if (!HasCommandPrefix(content, shard.User?.Id ?? default, out var cmdStart)) return false; // Trim leading whitespace from command without actually modifying the string @@ -102,7 +114,7 @@ namespace PluralKit.Bot try { var system = ctx.SystemId != null ? await _db.Execute(c => _repo.GetSystem(c, ctx.SystemId.Value)) : null; - await _tree.ExecuteCommand(new Context(_services, shard, evt.Message, cmdStart, system, ctx)); + await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.BotMemberIn(channel.GuildId!.Value))); } catch (PKError) { @@ -113,7 +125,7 @@ namespace PluralKit.Bot return true; } - private bool HasCommandPrefix(string message, out int argPos) + private bool HasCommandPrefix(string message, ulong currentUserId, out int argPos) { // First, try prefixes defined in the config var prefixes = _config.Prefixes ?? BotConfig.DefaultPrefixes; @@ -128,23 +140,28 @@ namespace PluralKit.Bot // Then, check mention prefix (must be the bot user, ofc) argPos = -1; if (DiscordUtils.HasMentionPrefix(message, ref argPos, out var id)) - return id == _client.CurrentUser.Id; + return id == currentUserId; return false; } - private async ValueTask TryHandleProxy(DiscordClient shard, MessageCreateEventArgs evt, MessageContext ctx) + private async ValueTask TryHandleProxy(Shard shard, MessageCreateEvent evt, Guild guild, Channel channel, MessageContext ctx) { + var botMember = _bot.BotMemberIn(channel.GuildId!.Value); + var botPermissions = PermissionExtensions.PermissionsFor(guild, channel, shard.User!.Id, botMember!.Roles); + try { - return await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: ctx.AllowAutoproxy); + return await _proxy.HandleIncomingMessage(shard, evt, ctx, guild, channel, allowAutoproxy: ctx.AllowAutoproxy, botPermissions); } catch (PKError e) { // User-facing errors, print to the channel properly formatted - var msg = evt.Message; - if (msg.Channel.Guild == null || msg.Channel.BotHasAllPermissions(Permissions.SendMessages)) - await msg.Channel.SendMessageFixedAsync($"{Emojis.Error} {e.Message}"); + if (botPermissions.HasFlag(PermissionSet.SendMessages)) + { + await _rest.CreateMessage(evt.ChannelId, + new MessageRequest {Content = $"{Emojis.Error} {e.Message}"}); + } } return false; diff --git a/PluralKit.Bot/Handlers/MessageDeleted.cs b/PluralKit.Bot/Handlers/MessageDeleted.cs index f3a5cf70..3d2c236c 100644 --- a/PluralKit.Bot/Handlers/MessageDeleted.cs +++ b/PluralKit.Bot/Handlers/MessageDeleted.cs @@ -1,9 +1,7 @@ using System; -using System.Linq; using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.EventArgs; +using Myriad.Gateway; using PluralKit.Core; @@ -12,7 +10,7 @@ using Serilog; namespace PluralKit.Bot { // Double duty :) - public class MessageDeleted: IEventHandler, IEventHandler + public class MessageDeleted: IEventHandler, IEventHandler { private static readonly TimeSpan MessageDeleteDelay = TimeSpan.FromSeconds(15); @@ -27,7 +25,7 @@ namespace PluralKit.Bot _logger = logger.ForContext(); } - public Task Handle(DiscordClient shard, MessageDeleteEventArgs evt) + public Task Handle(Shard shard, MessageDeleteEvent evt) { // Delete deleted webhook messages from the data store // Most of the data in the given message is wrong/missing, so always delete just to be sure. @@ -35,7 +33,8 @@ namespace PluralKit.Bot async Task Inner() { await Task.Delay(MessageDeleteDelay); - await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); + // TODO + // await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); } // Fork a task to delete the message after a short delay @@ -44,14 +43,15 @@ namespace PluralKit.Bot return Task.CompletedTask; } - public Task Handle(DiscordClient shard, MessageBulkDeleteEventArgs evt) + public Task Handle(Shard shard, MessageDeleteBulkEvent evt) { // Same as above, but bulk async Task Inner() { await Task.Delay(MessageDeleteDelay); - _logger.Information("Bulk deleting {Count} messages in channel {Channel}", evt.Messages.Count, evt.Channel.Id); - await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Messages.Select(m => m.Id).ToList())); + // TODO + // _logger.Information("Bulk deleting {Count} messages in channel {Channel}", evt.Messages.Count, evt.Channel.Id); + // await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Messages.Select(m => m.Id).ToList())); } _ = Inner(); diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index ac627ef0..a88e271f 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -3,14 +3,15 @@ using System.Threading.Tasks; using App.Metrics; using DSharpPlus; -using DSharpPlus.EventArgs; + +using Myriad.Gateway; using PluralKit.Core; namespace PluralKit.Bot { - public class MessageEdited: IEventHandler + public class MessageEdited: IEventHandler { private readonly LastMessageCacheService _lastMessageCache; private readonly ProxyService _proxy; @@ -29,22 +30,23 @@ namespace PluralKit.Bot _client = client; } - public async Task Handle(DiscordClient shard, MessageUpdateEventArgs evt) + public async Task Handle(Shard shard, MessageUpdateEvent evt) { - if (evt.Author?.Id == _client.CurrentUser?.Id) return; - - // Edit message events sometimes arrive with missing data; double-check it's all there - if (evt.Message.Content == null || evt.Author == null || evt.Channel.Guild == null) return; - - // Only react to the last message in the channel - if (_lastMessageCache.GetLastMessage(evt.Channel.Id) != evt.Message.Id) return; - - // Just run the normal message handling code, with a flag to disable autoproxying - MessageContext ctx; - await using (var conn = await _db.Obtain()) - using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) - ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.Channel.GuildId, evt.Channel.Id); - await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: false); + // TODO: fix + // if (evt.Author?.Id == _client.CurrentUser?.Id) return; + // + // // Edit message events sometimes arrive with missing data; double-check it's all there + // if (evt.Message.Content == null || evt.Author == null || evt.Channel.Guild == null) return; + // + // // Only react to the last message in the channel + // if (_lastMessageCache.GetLastMessage(evt.Channel.Id) != evt.Message.Id) return; + // + // // Just run the normal message handling code, with a flag to disable autoproxying + // MessageContext ctx; + // await using (var conn = await _db.Obtain()) + // using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) + // ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.Channel.GuildId, evt.Channel.Id); + // await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: false); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index b3f29ea4..6d2c2a15 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -5,13 +5,15 @@ using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; +using Myriad.Gateway; + using PluralKit.Core; using Serilog; namespace PluralKit.Bot { - public class ReactionAdded: IEventHandler + public class ReactionAdded: IEventHandler { private readonly IDatabase _db; private readonly ModelRepository _repo; @@ -28,9 +30,9 @@ namespace PluralKit.Bot _logger = logger.ForContext(); } - public async Task Handle(DiscordClient shard, MessageReactionAddEventArgs evt) + public async Task Handle(Shard shard, MessageReactionAddEvent evt) { - await TryHandleProxyMessageReactions(shard, evt); + // await TryHandleProxyMessageReactions(shard, evt); } private async ValueTask TryHandleProxyMessageReactions(DiscordClient shard, MessageReactionAddEventArgs evt) diff --git a/PluralKit.Bot/Init.cs b/PluralKit.Bot/Init.cs index f3255434..630fd297 100644 --- a/PluralKit.Bot/Init.cs +++ b/PluralKit.Bot/Init.cs @@ -4,10 +4,11 @@ using System.Threading.Tasks; using Autofac; -using DSharpPlus; - using Microsoft.Extensions.Configuration; +using Myriad.Gateway; +using Myriad.Rest; + using PluralKit.Core; using Serilog; @@ -47,7 +48,8 @@ namespace PluralKit.Bot // Start the Discord shards themselves (handlers already set up) logger.Information("Connecting to Discord"); - await services.Resolve().StartAsync(); + var info = await services.Resolve().GetGatewayBot(); + await services.Resolve().Start(info); logger.Information("Connected! All is good (probably)."); // Lastly, we just... wait. Everything else is handled in the DiscordClient event loop diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index b960a835..8dfd189c 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -6,12 +6,17 @@ using Autofac; using DSharpPlus; using DSharpPlus.EventArgs; +using Myriad.Cache; +using Myriad.Gateway; + using NodaTime; using PluralKit.Core; using Sentry; +using Serilog; + namespace PluralKit.Bot { public class BotModule: Module @@ -30,6 +35,22 @@ namespace PluralKit.Bot builder.Register(c => new DiscordShardedClient(c.Resolve())).AsSelf().SingleInstance(); builder.Register(c => new DiscordRestClient(c.Resolve())).AsSelf().SingleInstance(); + builder.Register(c => new GatewaySettings + { + Token = c.Resolve().Token, + Intents = GatewayIntent.Guilds | + GatewayIntent.DirectMessages | + GatewayIntent.DirectMessageReactions | + GatewayIntent.GuildEmojis | + GatewayIntent.GuildMessages | + GatewayIntent.GuildWebhooks | + GatewayIntent.GuildMessageReactions + }).AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.Register(c => new Myriad.Rest.DiscordApiClient(c.Resolve().Token, c.Resolve())) + .AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().As().SingleInstance(); + // Commands builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); @@ -55,10 +76,10 @@ namespace PluralKit.Bot // Bot core builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As>(); - builder.RegisterType().As>().As>(); - builder.RegisterType().As>(); - builder.RegisterType().As>(); + builder.RegisterType().As>(); + builder.RegisterType().As>().As>(); + builder.RegisterType().As>(); + builder.RegisterType().As>(); // Event handler queue builder.RegisterType>().AsSelf().SingleInstance(); @@ -81,13 +102,14 @@ namespace PluralKit.Bot // Sentry stuff builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); - builder.RegisterType() - .As>() - .As>() - .As>() - .As>() - .As>() - .SingleInstance(); + // TODO: + // builder.RegisterType() + // .As>() + // .As>() + // .As>() + // .As>() + // .As>() + // .SingleInstance(); // Proxy stuff builder.RegisterType().AsSelf().SingleInstance(); diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 7063f7de..031a901b 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -11,6 +11,7 @@ + diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index ed02af8c..20330f33 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -7,9 +7,13 @@ using System.Threading.Tasks; using App.Metrics; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; +using Myriad.Cache; +using Myriad.Extensions; +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Rest.Exceptions; +using Myriad.Rest.Types.Requests; +using Myriad.Types; using PluralKit.Core; @@ -28,9 +32,11 @@ namespace PluralKit.Bot private readonly WebhookExecutorService _webhookExecutor; private readonly ProxyMatcher _matcher; private readonly IMetrics _metrics; + private readonly IDiscordCache _cache; + private readonly DiscordApiClient _rest; public ProxyService(LogChannelService logChannel, ILogger logger, - WebhookExecutorService webhookExecutor, IDatabase db, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo) + WebhookExecutorService webhookExecutor, IDatabase db, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) { _logChannel = logChannel; _webhookExecutor = webhookExecutor; @@ -38,71 +44,75 @@ namespace PluralKit.Bot _matcher = matcher; _metrics = metrics; _repo = repo; + _cache = cache; + _rest = rest; _logger = logger.ForContext(); } - public async Task HandleIncomingMessage(DiscordClient shard, DiscordMessage message, MessageContext ctx, bool allowAutoproxy) + public async Task HandleIncomingMessage(Shard shard, MessageCreateEvent message, MessageContext ctx, Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions) { - if (!ShouldProxy(message, ctx)) return false; + if (!ShouldProxy(channel, message, ctx)) + return false; // Fetch members and try to match to a specific member await using var conn = await _db.Obtain(); List members; using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime)) - members = (await _repo.GetProxyMembers(conn, message.Author.Id, message.Channel.GuildId)).ToList(); + members = (await _repo.GetProxyMembers(conn, message.Author.Id, message.GuildId!.Value)).ToList(); - if (!_matcher.TryMatch(ctx, members, out var match, message.Content, message.Attachments.Count > 0, + if (!_matcher.TryMatch(ctx, members, out var match, message.Content, message.Attachments.Length > 0, allowAutoproxy)) return false; // Permission check after proxy match so we don't get spammed when not actually proxying - if (!await CheckBotPermissionsOrError(message.Channel)) return false; + if (!await CheckBotPermissionsOrError(botPermissions, message.ChannelId)) + return false; // this method throws, so no need to wrap it in an if statement CheckProxyNameBoundsOrError(match.Member.ProxyName(ctx)); // Check if the sender account can mention everyone/here + embed links // we need to "mirror" these permissions when proxying to prevent exploits - var senderPermissions = message.Channel.PermissionsInSync(message.Author); - var allowEveryone = (senderPermissions & Permissions.MentionEveryone) != 0; - var allowEmbeds = (senderPermissions & Permissions.EmbedLinks) != 0; + var senderPermissions = PermissionExtensions.PermissionsFor(guild, channel, message); + var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone); + var allowEmbeds = senderPermissions.HasFlag(PermissionSet.EmbedLinks); // Everything's in order, we can execute the proxy! await ExecuteProxy(shard, conn, message, ctx, match, allowEveryone, allowEmbeds); return true; } - private bool ShouldProxy(DiscordMessage msg, MessageContext ctx) + private bool ShouldProxy(Channel channel, Message msg, MessageContext ctx) { // Make sure author has a system if (ctx.SystemId == null) return false; // Make sure channel is a guild text channel and this is a normal message - if ((msg.Channel.Type != ChannelType.Text && msg.Channel.Type != ChannelType.News) || msg.MessageType != MessageType.Default) return false; + if ((channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildNews) || msg.Type != Message.MessageType.Default) return false; // Make sure author is a normal user - if (msg.Author.IsSystem == true || msg.Author.IsBot || msg.WebhookMessage) return false; + if (msg.Author.System == true || msg.Author.Bot || msg.WebhookId != null) return false; // Make sure proxying is enabled here if (!ctx.ProxyEnabled || ctx.InBlacklist) return false; // Make sure we have either an attachment or message content var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0; - if (isMessageBlank && msg.Attachments.Count == 0) return false; + if (isMessageBlank && msg.Attachments.Length == 0) return false; // All good! return true; } - private async Task ExecuteProxy(DiscordClient shard, IPKConnection conn, DiscordMessage trigger, MessageContext ctx, + private async Task ExecuteProxy(Shard shard, IPKConnection conn, Message trigger, MessageContext ctx, ProxyMatch match, bool allowEveryone, bool allowEmbeds) { // Create reply embed - var embeds = new List(); - if (trigger.Reference?.Channel?.Id == trigger.ChannelId) + var embeds = new List(); + if (trigger.MessageReference?.ChannelId == trigger.ChannelId) { - var repliedTo = await FetchReplyOriginalMessage(trigger.Reference); - var embed = await CreateReplyEmbed(repliedTo); + var repliedTo = await FetchReplyOriginalMessage(trigger.MessageReference); + var embed = CreateReplyEmbed(repliedTo); if (embed != null) embeds.Add(embed); } @@ -110,35 +120,44 @@ namespace PluralKit.Bot // Send the webhook var content = match.ProxyContent; if (!allowEmbeds) content = content.BreakLinkEmbeds(); - var proxyMessage = await _webhookExecutor.ExecuteWebhook(trigger.Channel, FixSingleCharacterName(match.Member.ProxyName(ctx)), - match.Member.ProxyAvatar(ctx), - content, trigger.Attachments, embeds, allowEveryone); + var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest + { + GuildId = trigger.GuildId!.Value, + ChannelId = trigger.ChannelId, + Name = FixSingleCharacterName(match.Member.ProxyName(ctx)), + AvatarUrl = match.Member.ProxyAvatar(ctx), + Content = content, + Attachments = trigger.Attachments, + Embeds = embeds.ToArray(), + AllowEveryone = allowEveryone, + }); await HandleProxyExecutedActions(shard, conn, ctx, trigger, proxyMessage, match); } - private async Task FetchReplyOriginalMessage(DiscordMessageReference reference) + private async Task FetchReplyOriginalMessage(Message.Reference reference) { try { - return await reference.Channel.GetMessageAsync(reference.Message.Id); - } - catch (NotFoundException) - { - _logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but it was not found", - reference.Channel.Id, reference.Message.Id); + var msg = await _rest.GetMessage(reference.ChannelId!.Value, reference.MessageId!.Value); + if (msg == null) + _logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but it was not found", + reference.ChannelId, reference.MessageId); + return msg; } catch (UnauthorizedException) { _logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but bot was not allowed to", - reference.Channel.Id, reference.Message.Id); + reference.ChannelId, reference.MessageId); } return null; } - private async Task CreateReplyEmbed(DiscordMessage original) + private Embed CreateReplyEmbed(Message original) { + var jumpLink = $"https://discord.com/channels/{original.GuildId}/{original.ChannelId}/{original.Id}"; + var content = new StringBuilder(); var hasContent = !string.IsNullOrWhiteSpace(original.Content); @@ -155,40 +174,45 @@ namespace PluralKit.Bot msg += "…"; } - content.Append($"**[Reply to:]({original.JumpLink})** "); + content.Append($"**[Reply to:]({jumpLink})** "); content.Append(msg); - if (original.Attachments.Count > 0) + if (original.Attachments.Length > 0) content.Append($" {Emojis.Paperclip}"); } else { - content.Append($"*[(click to see attachment)]({original.JumpLink})*"); + content.Append($"*[(click to see attachment)]({jumpLink})*"); } - var username = (original.Author as DiscordMember)?.Nickname ?? original.Author.Username; - - return new DiscordEmbedBuilder() + // TODO: get the nickname somehow + var username = original.Author.Username; + // var username = original.Member?.Nick ?? original.Author.Username; + + var avatarUrl = $"https://cdn.discordapp.com/avatars/{original.Author.Id}/{original.Author.Avatar}.png"; + + return new Embed + { // unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation] - .WithAuthor($"{username}\u2004\u21a9\ufe0f", iconUrl: original.Author.AvatarUrl) - .WithDescription(content.ToString()) - .Build(); + Author = new($"{username}\u2004\u21a9\ufe0f", IconUrl: avatarUrl), + Description = content.ToString() + }; } - private async Task HandleProxyExecutedActions(DiscordClient shard, IPKConnection conn, MessageContext ctx, - DiscordMessage triggerMessage, DiscordMessage proxyMessage, + private async Task HandleProxyExecutedActions(Shard shard, IPKConnection conn, MessageContext ctx, + Message triggerMessage, Message proxyMessage, ProxyMatch match) { Task SaveMessageInDatabase() => _repo.AddMessage(conn, new PKMessage { Channel = triggerMessage.ChannelId, - Guild = triggerMessage.Channel.GuildId, + Guild = triggerMessage.GuildId, Member = match.Member.Id, Mid = proxyMessage.Id, OriginalMid = triggerMessage.Id, Sender = triggerMessage.Author.Id }); - Task LogMessageToChannel() => _logChannel.LogMessage(shard, ctx, match, triggerMessage, proxyMessage.Id).AsTask(); + Task LogMessageToChannel() => _logChannel.LogMessage(ctx, match, triggerMessage, proxyMessage.Id).AsTask(); async Task DeleteProxyTriggerMessage() { @@ -196,7 +220,7 @@ namespace PluralKit.Bot await Task.Delay(MessageDeletionDelay); try { - await triggerMessage.DeleteAsync(); + await _rest.DeleteMessage(triggerMessage.ChannelId, triggerMessage.Id); } catch (NotFoundException) { @@ -216,7 +240,7 @@ namespace PluralKit.Bot ); } - private async Task HandleTriggerAlreadyDeleted(DiscordMessage proxyMessage) + private async Task HandleTriggerAlreadyDeleted(Message proxyMessage) { // If a trigger message is deleted before we get to delete it, we can assume a mod bot or similar got to it // In this case we should also delete the now-proxied message. @@ -224,32 +248,35 @@ namespace PluralKit.Bot try { - await proxyMessage.DeleteAsync(); + await _rest.DeleteMessage(proxyMessage.ChannelId, proxyMessage.Id); } catch (NotFoundException) { } catch (UnauthorizedException) { } } - private async Task CheckBotPermissionsOrError(DiscordChannel channel) + private async Task CheckBotPermissionsOrError(PermissionSet permissions, ulong responseChannel) { - var permissions = channel.BotPermissions(); - // If we can't send messages at all, just bail immediately. // 2020-04-22: Manage Messages does *not* override a lack of Send Messages. - if ((permissions & Permissions.SendMessages) == 0) return false; + if (!permissions.HasFlag(PermissionSet.SendMessages)) + return false; - if ((permissions & Permissions.ManageWebhooks) == 0) + if (!permissions.HasFlag(PermissionSet.ManageWebhooks)) { // todo: PKError-ify these - await channel.SendMessageFixedAsync( - $"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this."); + await _rest.CreateMessage(responseChannel, new MessageRequest + { + Content = $"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this." + }); return false; } - if ((permissions & Permissions.ManageMessages) == 0) + if (!permissions.HasFlag(PermissionSet.ManageMessages)) { - await channel.SendMessageFixedAsync( - $"{Emojis.Error} PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the original trigger message. Please contact a server administrator to remedy this."); + await _rest.CreateMessage(responseChannel, new MessageRequest + { + Content = $"{Emojis.Error} PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the original trigger message. Please contact a server administrator to remedy this." + }); return false; } diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index f360221c..241edde3 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -2,8 +2,9 @@ using System.Threading.Tasks; using Dapper; -using DSharpPlus; -using DSharpPlus.Entities; +using Myriad.Cache; +using Myriad.Rest; +using Myriad.Types; using PluralKit.Core; @@ -15,56 +16,62 @@ namespace PluralKit.Bot { private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly ILogger _logger; + private readonly IDiscordCache _cache; + private readonly DiscordApiClient _rest; - public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo) + public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) { _embed = embed; _db = db; _repo = repo; + _cache = cache; + _rest = rest; _logger = logger.ForContext(); } - public async ValueTask LogMessage(DiscordClient client, MessageContext ctx, ProxyMatch proxy, DiscordMessage trigger, ulong hookMessage) + public async ValueTask LogMessage(MessageContext ctx, ProxyMatch proxy, Message trigger, ulong hookMessage) { if (ctx.SystemId == null || ctx.LogChannel == null || ctx.InLogBlacklist) return; // Find log channel and check if valid - var logChannel = await FindLogChannel(client, trigger.Channel.GuildId, ctx.LogChannel.Value); - if (logChannel == null || logChannel.Type != ChannelType.Text) return; + var logChannel = await FindLogChannel(trigger.GuildId!.Value, ctx.LogChannel.Value); + if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return; // Check bot permissions - if (!logChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) - { - _logger.Information( - "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", - ctx.LogChannel.Value, trigger.Channel.GuildId, trigger.Channel.BotPermissions()); - return; - } - + // if (!logChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) + // { + // _logger.Information( + // "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", + // ctx.LogChannel.Value, trigger.GuildId!.Value, trigger.Channel.BotPermissions()); + // return; + // } + // // Send embed! - await using var conn = await _db.Obtain(); - var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), - await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, - trigger.Channel); - var url = $"https://discord.com/channels/{trigger.Channel.GuildId}/{trigger.ChannelId}/{hookMessage}"; - await logChannel.SendMessageFixedAsync(content: url, embed: embed); + + // TODO: fix? + // await using var conn = await _db.Obtain(); + // var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), + // await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, + // trigger.Channel); + // var url = $"https://discord.com/channels/{trigger.Channel.GuildId}/{trigger.ChannelId}/{hookMessage}"; + // await logChannel.SendMessageFixedAsync(content: url, embed: embed); } - private async Task FindLogChannel(DiscordClient client, ulong guild, ulong channel) + private async Task FindLogChannel(ulong guildId, ulong channelId) { - // MUST use this client here, otherwise we get strange cache issues where the guild doesn't exist... >.> - var obj = await client.GetChannel(channel); + // TODO: fetch it directly on cache miss? + var channel = await _cache.GetChannel(channelId); - if (obj == null) + if (channel == null) { // Channel doesn't exist or we don't have permission to access it, let's remove it from the database too - _logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channel, guild); + _logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId); await using var conn = await _db.Obtain(); await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild", - new {Guild = guild}); + new {Guild = guildId}); } - return obj; + return channel; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index 41ff4fa6..b0b56b9e 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -4,11 +4,10 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Dapper; - using DSharpPlus; using DSharpPlus.Entities; -using DSharpPlus.Exceptions; + +using Myriad.Types; using PluralKit.Core; @@ -68,8 +67,10 @@ namespace PluralKit.Bot public ICollection Bots => _bots.Values; - public async ValueTask HandleLoggerBotCleanup(DiscordMessage msg) + public async ValueTask HandleLoggerBotCleanup(Message msg) { + // TODO: fix!! + /* if (msg.Channel.Type != ChannelType.Text) return; if (!msg.Channel.BotHasAllPermissions(Permissions.ManageMessages)) return; @@ -130,6 +131,7 @@ namespace PluralKit.Bot // The only thing I can think of that'd cause this are the DeleteAsync() calls which 404 when // the message doesn't exist anyway - so should be safe to just ignore it, right? } + */ } private static ulong? ExtractAuttaja(DiscordMessage msg) diff --git a/PluralKit.Bot/Services/WebhookCacheService.cs b/PluralKit.Bot/Services/WebhookCacheService.cs index 99caf97e..9005c276 100644 --- a/PluralKit.Bot/Services/WebhookCacheService.cs +++ b/PluralKit.Bot/Services/WebhookCacheService.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using App.Metrics; -using DSharpPlus; -using DSharpPlus.Entities; +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Rest.Types.Requests; +using Myriad.Types; using Serilog; @@ -17,38 +18,32 @@ namespace PluralKit.Bot public class WebhookCacheService { public static readonly string WebhookName = "PluralKit Proxy Webhook"; - - private readonly DiscordShardedClient _client; - private readonly ConcurrentDictionary>> _webhooks; + + private readonly DiscordApiClient _rest; + private readonly ConcurrentDictionary>> _webhooks; private readonly IMetrics _metrics; private readonly ILogger _logger; + private readonly Cluster _cluster; - public WebhookCacheService(DiscordShardedClient client, ILogger logger, IMetrics metrics) + public WebhookCacheService(ILogger logger, IMetrics metrics, DiscordApiClient rest, Cluster cluster) { - _client = client; _metrics = metrics; + _rest = rest; + _cluster = cluster; _logger = logger.ForContext(); - _webhooks = new ConcurrentDictionary>>(); + _webhooks = new ConcurrentDictionary>>(); } - - public async Task GetWebhook(DiscordClient client, ulong channelId) - { - var channel = await client.GetChannel(channelId); - if (channel == null) return null; - if (channel.Type == ChannelType.Text) return null; - return await GetWebhook(channel); - } - - public async Task GetWebhook(DiscordChannel channel) + + public async Task GetWebhook(ulong channelId) { // We cache the webhook through a Lazy>, this way we make sure to only create one webhook per channel // If the webhook is requested twice before it's actually been found, the Lazy wrapper will stop the // webhook from being created twice. - Lazy> GetWebhookTaskInner() + Lazy> GetWebhookTaskInner() { - Task Factory() => GetOrCreateWebhook(channel); - return _webhooks.GetOrAdd(channel.Id, new Lazy>(Factory)); + Task Factory() => GetOrCreateWebhook(channelId); + return _webhooks.GetOrAdd(channelId, new Lazy>(Factory)); } var lazyWebhookValue = GetWebhookTaskInner(); @@ -57,36 +52,38 @@ namespace PluralKit.Bot // although, keep in mind this block gets hit the call *after* the task failed (since we only await it below) if (lazyWebhookValue.IsValueCreated && lazyWebhookValue.Value.IsFaulted) { - _logger.Warning(lazyWebhookValue.Value.Exception, "Cached webhook task for {Channel} faulted with below exception", channel.Id); + _logger.Warning(lazyWebhookValue.Value.Exception, "Cached webhook task for {Channel} faulted with below exception", channelId); // Specifically don't recurse here so we don't infinite-loop - if this one errors too, it'll "stick" // until next time this function gets hit (which is okay, probably). - _webhooks.TryRemove(channel.Id, out _); + _webhooks.TryRemove(channelId, out _); lazyWebhookValue = GetWebhookTaskInner(); } // It's possible to "move" a webhook to a different channel after creation // Here, we ensure it's actually still pointing towards the proper channel, and if not, wipe and refetch one. var webhook = await lazyWebhookValue.Value; - if (webhook.ChannelId != channel.Id && webhook.ChannelId != 0) return await InvalidateAndRefreshWebhook(channel, webhook); + if (webhook.ChannelId != channelId && webhook.ChannelId != 0) + return await InvalidateAndRefreshWebhook(channelId, webhook); return webhook; } - public async Task InvalidateAndRefreshWebhook(DiscordChannel channel, DiscordWebhook webhook) + public async Task InvalidateAndRefreshWebhook(ulong channelId, Webhook webhook) { + // note: webhook.ChannelId may not be the same as channelId >.> _logger.Information("Refreshing webhook for channel {Channel}", webhook.ChannelId); _webhooks.TryRemove(webhook.ChannelId, out _); - return await GetWebhook(channel); + return await GetWebhook(channelId); } - private async Task GetOrCreateWebhook(DiscordChannel channel) + private async Task GetOrCreateWebhook(ulong channelId) { - _logger.Debug("Webhook for channel {Channel} not found in cache, trying to fetch", channel.Id); + _logger.Debug("Webhook for channel {Channel} not found in cache, trying to fetch", channelId); _metrics.Measure.Meter.Mark(BotMetrics.WebhookCacheMisses); - _logger.Debug("Finding webhook for channel {Channel}", channel.Id); - var webhooks = await FetchChannelWebhooks(channel); + _logger.Debug("Finding webhook for channel {Channel}", channelId); + var webhooks = await FetchChannelWebhooks(channelId); // If the channel has a webhook created by PK, just return that one var ourWebhook = webhooks.FirstOrDefault(IsWebhookMine); @@ -95,17 +92,17 @@ namespace PluralKit.Bot // We don't have one, so we gotta create a new one // but first, make sure we haven't hit the webhook cap yet... - if (webhooks.Count >= 10) + if (webhooks.Length >= 10) throw new PKError("This channel has the maximum amount of possible webhooks (10) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying."); - return await DoCreateWebhook(channel); + return await DoCreateWebhook(channelId); } - private async Task> FetchChannelWebhooks(DiscordChannel channel) + private async Task FetchChannelWebhooks(ulong channelId) { try { - return await channel.GetWebhooksAsync(); + return await _rest.GetChannelWebhooks(channelId); } catch (HttpRequestException e) { @@ -113,33 +110,17 @@ namespace PluralKit.Bot // This happens sometimes when Discord returns a malformed request for the webhook list // Nothing we can do than just assume that none exist. - return new DiscordWebhook[0]; + return new Webhook[0]; } } - - private async Task FindExistingWebhook(DiscordChannel channel) + + private async Task DoCreateWebhook(ulong channelId) { - _logger.Debug("Finding webhook for channel {Channel}", channel.Id); - try - { - return (await channel.GetWebhooksAsync()).FirstOrDefault(IsWebhookMine); - } - catch (HttpRequestException e) - { - _logger.Warning(e, "Error occurred while fetching webhook list"); - // This happens sometimes when Discord returns a malformed request for the webhook list - // Nothing we can do than just assume that none exist and return null. - return null; - } + _logger.Information("Creating new webhook for channel {Channel}", channelId); + return await _rest.CreateWebhook(channelId, new CreateWebhookRequest(WebhookName)); } - private Task DoCreateWebhook(DiscordChannel channel) - { - _logger.Information("Creating new webhook for channel {Channel}", channel.Id); - return channel.CreateWebhookAsync(WebhookName); - } - - private bool IsWebhookMine(DiscordWebhook arg) => arg.User.Id == _client.CurrentUser.Id && arg.Name == WebhookName; + private bool IsWebhookMine(Webhook arg) => arg.User?.Id == _cluster.User?.Id && arg.Name == WebhookName; public int CacheSize => _webhooks.Count; } diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index c307562d..61efabbe 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -1,19 +1,20 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using App.Metrics; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - using Humanizer; +using Myriad.Cache; +using Myriad.Rest; +using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; +using Myriad.Types; + using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using Serilog; @@ -26,64 +27,84 @@ namespace PluralKit.Bot // Exceptions for control flow? don't mind if I do // TODO: rewrite both of these as a normal exceptional return value (0?) in case of error to be discarded by caller } + + public record ProxyRequest + { + public ulong GuildId { get; init; } + public ulong ChannelId { get; init; } + public string Name { get; init; } + public string? AvatarUrl { get; init; } + public string? Content { get; init; } + public Message.Attachment[] Attachments { get; init; } + public Embed[] Embeds { get; init; } + public bool AllowEveryone { get; init; } + } public class WebhookExecutorService { + private readonly IDiscordCache _cache; private readonly WebhookCacheService _webhookCache; + private readonly DiscordApiClient _rest; private readonly ILogger _logger; private readonly IMetrics _metrics; private readonly HttpClient _client; - public WebhookExecutorService(IMetrics metrics, WebhookCacheService webhookCache, ILogger logger, HttpClient client) + public WebhookExecutorService(IMetrics metrics, WebhookCacheService webhookCache, ILogger logger, HttpClient client, IDiscordCache cache, DiscordApiClient rest) { _metrics = metrics; _webhookCache = webhookCache; _client = client; + _cache = cache; + _rest = rest; _logger = logger.ForContext(); } - public async Task ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList attachments, IReadOnlyList embeds, bool allowEveryone) + public async Task ExecuteWebhook(ProxyRequest req) { - _logger.Verbose("Invoking webhook in channel {Channel}", channel.Id); + _logger.Verbose("Invoking webhook in channel {Channel}", req.ChannelId); // Get a webhook, execute it - var webhook = await _webhookCache.GetWebhook(channel); - var webhookMessage = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments, embeds, allowEveryone); + var webhook = await _webhookCache.GetWebhook(req.ChannelId); + var webhookMessage = await ExecuteWebhookInner(webhook, req); // Log the relevant metrics _metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied); _logger.Information("Invoked webhook {Webhook} in channel {Channel}", webhook.Id, - channel.Id); + req.ChannelId); return webhookMessage; } - private async Task ExecuteWebhookInner( - DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content, - IReadOnlyList attachments, IReadOnlyList embeds, bool allowEveryone, bool hasRetried = false) + private async Task ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false) { - content = content.Truncate(2000); + var guild = await _cache.GetGuild(req.GuildId)!; + var content = req.Content.Truncate(2000); - var dwb = new DiscordWebhookBuilder(); - dwb.WithUsername(FixClyde(name).Truncate(80)); - dwb.WithContent(content); - dwb.AddMentions(content.ParseAllMentions(allowEveryone, channel.Guild)); - if (!string.IsNullOrWhiteSpace(avatarUrl)) - dwb.WithAvatarUrl(avatarUrl); - dwb.AddEmbeds(embeds); + var webhookReq = new ExecuteWebhookRequest + { + Username = FixClyde(req.Name).Truncate(80), + Content = content, + AllowedMentions = null, // todo + AvatarUrl = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null, + Embeds = req.Embeds + }; - var attachmentChunks = ChunkAttachmentsOrThrow(attachments, 8 * 1024 * 1024); + // dwb.AddMentions(content.ParseAllMentions(guild, req.AllowEveryone)); + + MultipartFile[] files = null; + var attachmentChunks = ChunkAttachmentsOrThrow(req.Attachments, 8 * 1024 * 1024); if (attachmentChunks.Count > 0) { - _logger.Information("Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks", attachments.Count, attachments.Select(a => a.FileSize).Sum() / 1024 / 1024, attachmentChunks.Count); - await AddAttachmentsToBuilder(dwb, attachmentChunks[0]); + _logger.Information("Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks", + req.Attachments.Length, req.Attachments.Select(a => a.Size).Sum() / 1024 / 1024, attachmentChunks.Count); + files = await GetAttachmentFiles(attachmentChunks[0]); } - DiscordMessage webhookMessage; + Message webhookMessage; using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime)) { try { - webhookMessage = await webhook.ExecuteAsync(dwb); + webhookMessage = await _rest.ExecuteWebhook(webhook.Id, webhook.Token, webhookReq, files); } catch (JsonReaderException) { @@ -91,17 +112,16 @@ namespace PluralKit.Bot // Nothing we can do about this - happens sometimes under server load, so just drop the message and give up throw new WebhookExecutionErrorOnDiscordsEnd(); } - catch (NotFoundException e) + catch (Myriad.Rest.Exceptions.NotFoundException e) { - var errorText = e.WebResponse?.Response; - if (errorText != null && errorText.Contains("10015") && !hasRetried) + if (e.ErrorCode == 10015 && !hasRetried) { // Error 10015 = "Unknown Webhook" - this likely means the webhook was deleted // but is still in our cache. Invalidate, refresh, try again _logger.Warning("Error invoking webhook {Webhook} in channel {Channel}", webhook.Id, webhook.ChannelId); - var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(channel, webhook); - return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, embeds, allowEveryone, hasRetried: true); + var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(req.ChannelId, webhook); + return await ExecuteWebhookInner(newWebhook, req, hasRetried: true); } throw; @@ -109,53 +129,50 @@ namespace PluralKit.Bot } // We don't care about whether the sending succeeds, and we don't want to *wait* for it, so we just fork it off - var _ = TrySendRemainingAttachments(webhook, name, avatarUrl, attachmentChunks); + var _ = TrySendRemainingAttachments(webhook, req.Name, req.AvatarUrl, attachmentChunks); return webhookMessage; } - private async Task TrySendRemainingAttachments(DiscordWebhook webhook, string name, string avatarUrl, IReadOnlyList> attachmentChunks) + private async Task TrySendRemainingAttachments(Webhook webhook, string name, string avatarUrl, IReadOnlyList> attachmentChunks) { if (attachmentChunks.Count <= 1) return; for (var i = 1; i < attachmentChunks.Count; i++) { - var dwb = new DiscordWebhookBuilder(); - if (avatarUrl != null) dwb.WithAvatarUrl(avatarUrl); - dwb.WithUsername(name); - await AddAttachmentsToBuilder(dwb, attachmentChunks[i]); - await webhook.ExecuteAsync(dwb); + var files = await GetAttachmentFiles(attachmentChunks[i]); + var req = new ExecuteWebhookRequest {Username = name, AvatarUrl = avatarUrl}; + await _rest.ExecuteWebhook(webhook.Id, webhook.Token!, req, files); } } - - private async Task AddAttachmentsToBuilder(DiscordWebhookBuilder dwb, IReadOnlyCollection attachments) + + private async Task GetAttachmentFiles(IReadOnlyCollection attachments) { - async Task<(DiscordAttachment, Stream)> GetStream(DiscordAttachment attachment) + async Task GetStream(Message.Attachment attachment) { var attachmentResponse = await _client.GetAsync(attachment.Url, HttpCompletionOption.ResponseHeadersRead); - return (attachment, await attachmentResponse.Content.ReadAsStreamAsync()); + return new(attachment.Filename, await attachmentResponse.Content.ReadAsStreamAsync()); } - - foreach (var (attachment, attachmentStream) in await Task.WhenAll(attachments.Select(GetStream))) - dwb.AddFile(attachment.FileName, attachmentStream); + + return await Task.WhenAll(attachments.Select(GetStream)); } - private IReadOnlyList> ChunkAttachmentsOrThrow( - IReadOnlyList attachments, int sizeThreshold) + private IReadOnlyList> ChunkAttachmentsOrThrow( + IReadOnlyList attachments, int sizeThreshold) { // Splits a list of attachments into "chunks" of at most 8MB each // If any individual attachment is larger than 8MB, will throw an error - var chunks = new List>(); - var list = new List(); + var chunks = new List>(); + var list = new List(); foreach (var attachment in attachments) { - if (attachment.FileSize >= sizeThreshold) throw Errors.AttachmentTooLarge; + if (attachment.Size >= sizeThreshold) throw Errors.AttachmentTooLarge; - if (list.Sum(a => a.FileSize) + attachment.FileSize >= sizeThreshold) + if (list.Sum(a => a.Size) + attachment.Size >= sizeThreshold) { chunks.Add(list); - list = new List(); + list = new List(); } list.Add(attachment); diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 90075445..903f2b31 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -12,10 +12,14 @@ using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; +using Myriad.Types; + using NodaTime; using PluralKit.Core; +using Permissions = DSharpPlus.Permissions; + namespace PluralKit.Bot { public static class DiscordUtils @@ -190,8 +194,7 @@ namespace PluralKit.Bot return false; } - public static IEnumerable ParseAllMentions(this string input, bool allowEveryone = false, - DiscordGuild guild = null) + public static IEnumerable ParseAllMentions(this string input, Guild guild, bool allowEveryone = false) { var mentions = new List(); mentions.AddRange(USER_MENTION.Matches(input) @@ -203,7 +206,7 @@ namespace PluralKit.Bot // Original fix by Gwen mentions.AddRange(ROLE_MENTION.Matches(input) .Select(x => ulong.Parse(x.Groups[1].Value)) - .Where(x => allowEveryone || guild != null && guild.GetRole(x).IsMentionable) + .Where(x => allowEveryone || guild != null && (guild.Roles.FirstOrDefault(g => g.Id == x)?.Mentionable ?? false)) .Select(x => new RoleMention(x) as IMention)); if (EVERYONE_HERE_MENTION.IsMatch(input) && allowEveryone) mentions.Add(new EveryoneMention()); diff --git a/PluralKit.Bot/Utils/SentryUtils.cs b/PluralKit.Bot/Utils/SentryUtils.cs index 7b11f876..0d9cfb19 100644 --- a/PluralKit.Bot/Utils/SentryUtils.cs +++ b/PluralKit.Bot/Utils/SentryUtils.cs @@ -4,26 +4,29 @@ using System.Linq; using DSharpPlus; using DSharpPlus.EventArgs; +using Myriad.Gateway; + using Sentry; namespace PluralKit.Bot { - public interface ISentryEnricher where T: DiscordEventArgs + public interface ISentryEnricher where T: IGatewayEvent { - void Enrich(Scope scope, DiscordClient shard, T evt); + void Enrich(Scope scope, Shard shard, T evt); } - public class SentryEnricher: - ISentryEnricher, - ISentryEnricher, - ISentryEnricher, - ISentryEnricher, - ISentryEnricher + public class SentryEnricher //: + // TODO!!! + // ISentryEnricher, + // ISentryEnricher, + // ISentryEnricher, + // ISentryEnricher, + // ISentryEnricher { // TODO: should this class take the Scope by dependency injection instead? // Would allow us to create a centralized "chain of handlers" where this class could just be registered as an entry in - public void Enrich(Scope scope, DiscordClient shard, MessageCreateEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageCreateEventArgs evt) { scope.AddBreadcrumb(evt.Message.Content, "event.message", data: new Dictionary { @@ -32,7 +35,7 @@ namespace PluralKit.Bot {"guild", evt.Channel.GuildId.ToString()}, {"message", evt.Message.Id.ToString()}, }); - scope.SetTag("shard", shard.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); // Also report information about the bot's permissions in the channel // We get a lot of permission errors so this'll be useful for determining problems @@ -40,7 +43,7 @@ namespace PluralKit.Bot scope.AddBreadcrumb(perms.ToPermissionString(), "permissions"); } - public void Enrich(Scope scope, DiscordClient shard, MessageDeleteEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageDeleteEventArgs evt) { scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() @@ -49,10 +52,10 @@ namespace PluralKit.Bot {"guild", evt.Channel.GuildId.ToString()}, {"message", evt.Message.Id.ToString()}, }); - scope.SetTag("shard", shard.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); } - public void Enrich(Scope scope, DiscordClient shard, MessageUpdateEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageUpdateEventArgs evt) { scope.AddBreadcrumb(evt.Message.Content ?? "", "event.messageEdit", data: new Dictionary() @@ -61,10 +64,10 @@ namespace PluralKit.Bot {"guild", evt.Channel.GuildId.ToString()}, {"message", evt.Message.Id.ToString()} }); - scope.SetTag("shard", shard.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); } - public void Enrich(Scope scope, DiscordClient shard, MessageBulkDeleteEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageBulkDeleteEventArgs evt) { scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() @@ -73,10 +76,10 @@ namespace PluralKit.Bot {"guild", evt.Channel.Id.ToString()}, {"messages", string.Join(",", evt.Messages.Select(m => m.Id))}, }); - scope.SetTag("shard", shard.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); } - public void Enrich(Scope scope, DiscordClient shard, MessageReactionAddEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageReactionAddEventArgs evt) { scope.AddBreadcrumb("", "event.reaction", data: new Dictionary() @@ -87,7 +90,7 @@ namespace PluralKit.Bot {"message", evt.Message.Id.ToString()}, {"reaction", evt.Emoji.Name} }); - scope.SetTag("shard", shard.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); } } } \ No newline at end of file diff --git a/PluralKit.sln b/PluralKit.sln index 84b03bec..05212782 100644 --- a/PluralKit.sln +++ b/PluralKit.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.API", "PluralKit. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Tests", "PluralKit.Tests\PluralKit.Tests.csproj", "{752FE725-5EE1-45E9-B721-0CDD28171AC8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Myriad", "Myriad\Myriad.csproj", "{ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {752FE725-5EE1-45E9-B721-0CDD28171AC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {752FE725-5EE1-45E9-B721-0CDD28171AC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {752FE725-5EE1-45E9-B721-0CDD28171AC8}.Release|Any CPU.Build.0 = Release|Any CPU + {ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 05334f0d259583872fa5d8feaa1bd75aed264b9e Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 22 Dec 2020 16:55:13 +0100 Subject: [PATCH 002/608] Converted enough to send the system card --- Myriad/Cache/IDiscordCache.cs | 10 +-- Myriad/Cache/MemoryDiscordCache.cs | 25 +++++-- Myriad/Extensions/CacheExtensions.cs | 45 ++++++++++++ Myriad/Extensions/ChannelExtensions.cs | 6 +- Myriad/Extensions/GuildExtensions.cs | 7 ++ Myriad/Extensions/MessageExtensions.cs | 3 +- Myriad/Extensions/PermissionExtensions.cs | 33 +++++++-- Myriad/Extensions/UserExtensions.cs | 2 + Myriad/Rest/Types/AllowedMentions.cs | 6 +- Myriad/Rest/Types/Requests/MessageRequest.cs | 2 +- Myriad/Types/Channel.cs | 2 +- PluralKit.Bot/CommandSystem/Context.cs | 52 +++++++++---- .../CommandSystem/ContextChecksExt.cs | 11 ++- .../ContextEntityArgumentsExt.cs | 11 ++- PluralKit.Bot/Commands/CommandTree.cs | 1 - PluralKit.Bot/Commands/MemberAvatar.cs | 16 ++-- PluralKit.Bot/Commands/MemberEdit.cs | 39 +++++----- PluralKit.Bot/Commands/ServerConfig.cs | 73 +++++++++++-------- PluralKit.Bot/Commands/Switch.cs | 2 - PluralKit.Bot/Commands/SystemEdit.cs | 13 +--- PluralKit.Bot/Commands/SystemFront.cs | 1 - PluralKit.Bot/Commands/SystemList.cs | 2 - PluralKit.Bot/Handlers/MessageCreated.cs | 8 +- PluralKit.Bot/Services/EmbedService.cs | 65 ++++++++++++----- PluralKit.Bot/Services/LogChannelService.cs | 18 ++--- .../Services/WebhookExecutorService.cs | 11 ++- PluralKit.Bot/Utils/ContextUtils.cs | 21 +++--- PluralKit.Bot/Utils/DiscordUtils.cs | 56 +++++++------- 28 files changed, 343 insertions(+), 198 deletions(-) create mode 100644 Myriad/Extensions/CacheExtensions.cs create mode 100644 Myriad/Extensions/GuildExtensions.cs diff --git a/Myriad/Cache/IDiscordCache.cs b/Myriad/Cache/IDiscordCache.cs index fdc348c6..7c72b272 100644 --- a/Myriad/Cache/IDiscordCache.cs +++ b/Myriad/Cache/IDiscordCache.cs @@ -17,12 +17,12 @@ namespace Myriad.Cache public ValueTask RemoveUser(ulong userId); public ValueTask RemoveRole(ulong guildId, ulong roleId); - public ValueTask GetGuild(ulong guildId); - public ValueTask GetChannel(ulong channelId); - public ValueTask GetUser(ulong userId); - public ValueTask GetRole(ulong roleId); + public bool TryGetGuild(ulong guildId, out Guild guild); + public bool TryGetChannel(ulong channelId, out Channel channel); + public bool TryGetUser(ulong userId, out User user); + public bool TryGetRole(ulong roleId, out Role role); public IAsyncEnumerable GetAllGuilds(); - public ValueTask> GetGuildChannels(ulong guildId); + public IEnumerable GetGuildChannels(ulong guildId); } } \ No newline at end of file diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs index 8ba50366..2a6c194f 100644 --- a/Myriad/Cache/MemoryDiscordCache.cs +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -110,13 +110,26 @@ namespace Myriad.Cache return default; } - public ValueTask GetGuild(ulong guildId) => new(_guilds.GetValueOrDefault(guildId)?.Guild); + public bool TryGetGuild(ulong guildId, out Guild guild) + { + if (_guilds.TryGetValue(guildId, out var cg)) + { + guild = cg.Guild; + return true; + } - public ValueTask GetChannel(ulong channelId) => new(_channels.GetValueOrDefault(channelId)); + guild = null!; + return false; + } - public ValueTask GetUser(ulong userId) => new(_users.GetValueOrDefault(userId)); + public bool TryGetChannel(ulong channelId, out Channel channel) => + _channels.TryGetValue(channelId, out channel!); - public ValueTask GetRole(ulong roleId) => new(_roles.GetValueOrDefault(roleId)); + public bool TryGetUser(ulong userId, out User user) => + _users.TryGetValue(userId, out user!); + + public bool TryGetRole(ulong roleId, out Role role) => + _roles.TryGetValue(roleId, out role!); public async IAsyncEnumerable GetAllGuilds() { @@ -124,12 +137,12 @@ namespace Myriad.Cache yield return guild.Guild; } - public ValueTask> GetGuildChannels(ulong guildId) + public IEnumerable GetGuildChannels(ulong guildId) { if (!_guilds.TryGetValue(guildId, out var guild)) throw new ArgumentException("Guild not found", nameof(guildId)); - return new ValueTask>(guild.Channels.Keys.Select(c => _channels[c])); + return guild.Channels.Keys.Select(c => _channels[c]); } private CachedGuild SaveGuildRaw(Guild guild) => diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs new file mode 100644 index 00000000..260c5932 --- /dev/null +++ b/Myriad/Extensions/CacheExtensions.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +using Myriad.Cache; +using Myriad.Types; + +namespace Myriad.Extensions +{ + public static class CacheExtensions + { + public static Guild GetGuild(this IDiscordCache cache, ulong guildId) + { + if (!cache.TryGetGuild(guildId, out var guild)) + throw new KeyNotFoundException($"Guild {guildId} not found in cache"); + return guild; + } + + public static Channel GetChannel(this IDiscordCache cache, ulong channelId) + { + if (!cache.TryGetChannel(channelId, out var channel)) + throw new KeyNotFoundException($"Channel {channelId} not found in cache"); + return channel; + } + + public static Channel? GetChannelOrNull(this IDiscordCache cache, ulong channelId) + { + if (cache.TryGetChannel(channelId, out var channel)) + return channel; + return null; + } + + public static User GetUser(this IDiscordCache cache, ulong userId) + { + if (!cache.TryGetUser(userId, out var user)) + throw new KeyNotFoundException($"User {userId} not found in cache"); + return user; + } + + public static Role GetRole(this IDiscordCache cache, ulong roleId) + { + if (!cache.TryGetRole(roleId, out var role)) + throw new KeyNotFoundException($"User {roleId} not found in cache"); + return role; + } + } +} \ No newline at end of file diff --git a/Myriad/Extensions/ChannelExtensions.cs b/Myriad/Extensions/ChannelExtensions.cs index 99344138..0f04cb03 100644 --- a/Myriad/Extensions/ChannelExtensions.cs +++ b/Myriad/Extensions/ChannelExtensions.cs @@ -1,7 +1,9 @@ -namespace Myriad.Extensions +using Myriad.Types; + +namespace Myriad.Extensions { public static class ChannelExtensions { - + public static string Mention(this Channel channel) => $"<#{channel.Id}>"; } } \ No newline at end of file diff --git a/Myriad/Extensions/GuildExtensions.cs b/Myriad/Extensions/GuildExtensions.cs new file mode 100644 index 00000000..1e95b8bc --- /dev/null +++ b/Myriad/Extensions/GuildExtensions.cs @@ -0,0 +1,7 @@ +namespace Myriad.Extensions +{ + public static class GuildExtensions + { + + } +} \ No newline at end of file diff --git a/Myriad/Extensions/MessageExtensions.cs b/Myriad/Extensions/MessageExtensions.cs index ef999fc0..7393a9a2 100644 --- a/Myriad/Extensions/MessageExtensions.cs +++ b/Myriad/Extensions/MessageExtensions.cs @@ -1,7 +1,6 @@ namespace Myriad.Extensions { - public class MessageExtensions + public static class MessageExtensions { - } } \ No newline at end of file diff --git a/Myriad/Extensions/PermissionExtensions.cs b/Myriad/Extensions/PermissionExtensions.cs index 02fd3292..60f4f52b 100644 --- a/Myriad/Extensions/PermissionExtensions.cs +++ b/Myriad/Extensions/PermissionExtensions.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Myriad.Cache; using Myriad.Gateway; using Myriad.Types; @@ -9,17 +11,39 @@ namespace Myriad.Extensions { public static class PermissionExtensions { + public static PermissionSet PermissionsFor(this IDiscordCache cache, MessageCreateEvent message) => + PermissionsFor(cache, message.ChannelId, message.Author.Id, message.Member?.Roles); + + public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, GuildMember member) => + PermissionsFor(cache, channelId, member.User.Id, member.Roles); + + public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, GuildMemberPartial member) => + PermissionsFor(cache, channelId, userId, member.Roles); + + public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, ICollection? userRoles) + { + var channel = cache.GetChannel(channelId); + if (channel.GuildId == null) + return PermissionSet.Dm; + + var guild = cache.GetGuild(channel.GuildId.Value); + return PermissionsFor(guild, channel, userId, userRoles); + } + public static PermissionSet EveryonePermissions(this Guild guild) => guild.Roles.FirstOrDefault(r => r.Id == guild.Id)?.Permissions ?? PermissionSet.Dm; public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg) => - PermissionsFor(guild, channel, msg.Author.Id, msg.Member!.Roles); + PermissionsFor(guild, channel, msg.Author.Id, msg.Member?.Roles); public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId, - ICollection roleIds) + ICollection? roleIds) { if (channel.Type == Channel.ChannelType.Dm) return PermissionSet.Dm; + + if (roleIds == null) + throw new ArgumentException($"User roles must be specified for guild channels"); var perms = GuildPermissions(guild, userId, roleIds); perms = ApplyChannelOverwrites(perms, channel, userId, roleIds); @@ -36,9 +60,6 @@ namespace Myriad.Extensions return perms; } - public static bool Has(this PermissionSet value, PermissionSet flag) => - (value & flag) == flag; - public static PermissionSet GuildPermissions(this Guild guild, ulong userId, ICollection roleIds) { if (guild.OwnerId == userId) @@ -51,7 +72,7 @@ namespace Myriad.Extensions perms |= role.Permissions; } - if (perms.Has(PermissionSet.Administrator)) + if (perms.HasFlag(PermissionSet.Administrator)) return PermissionSet.All; return perms; diff --git a/Myriad/Extensions/UserExtensions.cs b/Myriad/Extensions/UserExtensions.cs index 1f31b231..e4b1e5ef 100644 --- a/Myriad/Extensions/UserExtensions.cs +++ b/Myriad/Extensions/UserExtensions.cs @@ -4,6 +4,8 @@ namespace Myriad.Extensions { public static class UserExtensions { + public static string Mention(this User user) => $"<@{user.Id}>"; + public static string AvatarUrl(this User user) => $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png"; } diff --git a/Myriad/Rest/Types/AllowedMentions.cs b/Myriad/Rest/Types/AllowedMentions.cs index 019c735d..d3ab3199 100644 --- a/Myriad/Rest/Types/AllowedMentions.cs +++ b/Myriad/Rest/Types/AllowedMentions.cs @@ -11,9 +11,9 @@ namespace Myriad.Rest.Types Everyone } - public List? Parse { get; set; } - public List? Users { get; set; } - public List? Roles { get; set; } + public ParseType[]? Parse { get; set; } + public ulong[]? Users { get; set; } + public ulong[]? Roles { get; set; } public bool RepliedUser { get; set; } } } \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs index ae9625f7..72f018e5 100644 --- a/Myriad/Rest/Types/Requests/MessageRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -8,6 +8,6 @@ namespace Myriad.Rest.Types.Requests public object? Nonce { get; set; } public bool Tts { get; set; } public AllowedMentions AllowedMentions { get; set; } - public Embed? Embeds { get; set; } + public Embed? Embed { get; set; } } } \ No newline at end of file diff --git a/Myriad/Types/Channel.cs b/Myriad/Types/Channel.cs index 72e1854c..2ac13cc6 100644 --- a/Myriad/Types/Channel.cs +++ b/Myriad/Types/Channel.cs @@ -20,7 +20,7 @@ public string? Name { get; init; } public string? Topic { get; init; } public bool? Nsfw { get; init; } - public long? ParentId { get; init; } + public ulong? ParentId { get; init; } public Overwrite[]? PermissionOverwrites { get; init; } public record Overwrite diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index ad96ecd5..a5d8c29a 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -8,14 +8,17 @@ using Autofac; using DSharpPlus; using DSharpPlus.Entities; +using DSharpPlus.Net; +using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Rest.Types.Requests; using Myriad.Types; using PluralKit.Core; -using Permissions = DSharpPlus.Permissions; +using DiscordApiClient = Myriad.Rest.DiscordApiClient; namespace PluralKit.Bot { @@ -24,6 +27,7 @@ namespace PluralKit.Bot private readonly ILifetimeScope _provider; private readonly DiscordRestClient _rest; + private readonly DiscordApiClient _newRest; private readonly DiscordShardedClient _client; private readonly DiscordClient _shard = null; private readonly Shard _shardNew; @@ -42,6 +46,7 @@ namespace PluralKit.Bot private readonly PKSystem _senderSystem; private readonly IMetrics _metrics; private readonly CommandMessageService _commandMessageService; + private readonly IDiscordCache _cache; private Command _currentCommand; @@ -57,24 +62,25 @@ namespace PluralKit.Bot _senderSystem = senderSystem; _messageContext = messageContext; _botMember = botMember; + _cache = provider.Resolve(); _db = provider.Resolve(); _repo = provider.Resolve(); _metrics = provider.Resolve(); _provider = provider; _commandMessageService = provider.Resolve(); _parameters = new Parameters(message.Content.Substring(commandParseOffset)); + _newRest = provider.Resolve(); - _botPermissions = message.GuildId != null - ? PermissionExtensions.PermissionsFor(guild!, channel, shard.User?.Id ?? default, botMember!.Roles) - : PermissionSet.Dm; - _userPermissions = message.GuildId != null - ? PermissionExtensions.PermissionsFor(guild!, channel, message.Author.Id, message.Member!.Roles) - : PermissionSet.Dm; + _botPermissions = _cache.PermissionsFor(message.ChannelId, shard.User!.Id, botMember!); + _userPermissions = _cache.PermissionsFor(message); } + public IDiscordCache Cache => _cache; + public DiscordUser Author => _message.Author; public DiscordChannel Channel => _message.Channel; public Channel ChannelNew => _channel; + public User AuthorNew => _messageNew.Author; public DiscordMessage Message => _message; public Message MessageNew => _messageNew; public DiscordGuild Guild => _message.Channel.Guild; @@ -95,24 +101,44 @@ namespace PluralKit.Bot internal IDatabase Database => _db; internal ModelRepository Repository => _repo; - public async Task Reply(string text = null, DiscordEmbed embed = null, IEnumerable mentions = null) + public Task Reply(string text, DiscordEmbed embed, + IEnumerable? mentions = null) { - if (!this.BotHasAllPermissions(Permissions.SendMessages)) + return Reply(text, (DiscordEmbed) null, mentions); + } + + public Task Reply(DiscordEmbed embed, + IEnumerable? mentions = null) + { + return Reply(null, (DiscordEmbed) null, mentions); + } + + public async Task Reply(string text = null, Embed embed = null, IEnumerable? mentions = null) + { + if (!BotPermissions.HasFlag(PermissionSet.SendMessages)) // Will be "swallowed" during the error handler anyway, this message is never shown. throw new PKError("PluralKit does not have permission to send messages in this channel."); - if (embed != null && !this.BotHasAllPermissions(Permissions.EmbedLinks)) + if (embed != null && !BotPermissions.HasFlag(PermissionSet.EmbedLinks)) throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled."); - var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); + + var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest + { + Content = text, + Embed = embed + }); + // TODO: embeds/mentions + // var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); if (embed != null) { // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) // This may need to be changed at some point but works well enough for now - await _commandMessageService.RegisterMessage(msg.Id, Author.Id); + await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id); } - return msg; + // return msg; + return null; } public async Task Execute(Command commandDef, Func handler) diff --git a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs index 5ae896bb..53ae3015 100644 --- a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs @@ -1,5 +1,7 @@ using DSharpPlus; +using Myriad.Types; + using PluralKit.Core; namespace PluralKit.Bot @@ -8,7 +10,7 @@ namespace PluralKit.Bot { public static Context CheckGuildContext(this Context ctx) { - if (ctx.Channel.Guild != null) return ctx; + if (ctx.ChannelNew.GuildId != null) return ctx; throw new PKError("This command can not be run in a DM."); } @@ -46,12 +48,9 @@ namespace PluralKit.Bot return ctx; } - public static Context CheckAuthorPermission(this Context ctx, Permissions neededPerms, string permissionName) + public static Context CheckAuthorPermission(this Context ctx, PermissionSet neededPerms, string permissionName) { - // TODO: can we always assume Author is a DiscordMember? I would think so, given they always come from a - // message received event... - var hasPerms = ctx.Channel.PermissionsInSync(ctx.Author); - if ((hasPerms & neededPerms) != neededPerms) + if ((ctx.UserPermissions & neededPerms) != neededPerms) throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command."); return ctx; } diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 32dd11c0..97e2efa4 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -3,6 +3,8 @@ using DSharpPlus; using DSharpPlus.Entities; +using Myriad.Types; + using PluralKit.Bot.Utils; using PluralKit.Core; @@ -153,13 +155,16 @@ namespace PluralKit.Bot return $"Group not found. Note that a group ID is 5 characters long."; } - public static async Task MatchChannel(this Context ctx) + public static async Task MatchChannel(this Context ctx) { if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id)) return null; + + if (!ctx.Cache.TryGetChannel(id, out var channel)) + return null; - var channel = await ctx.Shard.GetChannel(id); - if (channel == null || !(channel.Type == ChannelType.Text || channel.Type == ChannelType.News)) return null; + if (!(channel.Type == Channel.ChannelType.GuildText || channel.Type == Channel.ChannelType.GuildText)) + return null; ctx.PopArgument(); return channel; diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 60f2426f..b29d3816 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading.Tasks; using DSharpPlus; -using DSharpPlus.Exceptions; using Humanizer; diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 786779b6..06d5d9c7 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -25,7 +25,7 @@ namespace PluralKit.Bot if (location == AvatarLocation.Server) { if (target.AvatarUrl != null) - await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.Guild.Name}**)."); + await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.GuildNew.Name}**)."); else await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar."); } @@ -55,7 +55,7 @@ namespace PluralKit.Bot throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar."); } - var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar"; + var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.GuildNew.Name})" : "avatar"; var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar"; var eb = new DiscordEmbedBuilder() @@ -69,14 +69,14 @@ namespace PluralKit.Bot public async Task ServerAvatar(Context ctx, PKMember target) { ctx.CheckGuildContext(); - var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData); } public async Task Avatar(Context ctx, PKMember target) { - var guildData = ctx.Guild != null ? - await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)) + var guildData = ctx.GuildNew != null ? + await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)) : null; await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData); @@ -119,8 +119,8 @@ namespace PluralKit.Bot var serverFrag = location switch { - AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).", - AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.Guild.Name}**), and thus changing the global avatar will have no effect here.", + AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.GuildNew.Name}**).", + AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.GuildNew.Name}**), and thus changing the global avatar will have no effect here.", _ => "" }; @@ -145,7 +145,7 @@ namespace PluralKit.Bot { case AvatarLocation.Server: var serverPatch = new MemberGuildPatch { AvatarUrl = url }; - return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.Guild.Id, serverPatch)); + return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.GuildNew.Id, serverPatch)); case AvatarLocation.Member: var memberPatch = new MemberPatch { AvatarUrl = url }; return _db.Execute(c => _repo.UpdateMember(c, target.Id, memberPatch)); diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 43d8fa82..710269d6 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -2,9 +2,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System; - -using Dapper; - using DSharpPlus.Entities; using NodaTime; @@ -49,11 +46,11 @@ namespace PluralKit.Bot if (newName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); if (target.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead."); - if (ctx.Guild != null) + if (ctx.GuildNew != null) { - var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); if (memberGuildConfig.DisplayName != null) - await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.Guild.Name}), and will be proxied using that name here."); + await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.GuildNew.Name}), and will be proxied using that name here."); } } @@ -229,8 +226,8 @@ namespace PluralKit.Bot var lcx = ctx.LookupContextFor(target); MemberGuildSettings memberGuildConfig = null; - if (ctx.Guild != null) - memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + if (ctx.GuildNew != null) + memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); var eb = new DiscordEmbedBuilder().WithTitle($"Member names") .WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name."); @@ -248,12 +245,12 @@ namespace PluralKit.Bot eb.AddField("Display Name", target.DisplayName ?? "*(none)*"); } - if (ctx.Guild != null) + if (ctx.GuildNew != null) { if (memberGuildConfig?.DisplayName != null) - eb.AddField($"Server Name (in {ctx.Guild.Name})", $"**{memberGuildConfig.DisplayName}**"); + eb.AddField($"Server Name (in {ctx.GuildNew.Name})", $"**{memberGuildConfig.DisplayName}**"); else - eb.AddField($"Server Name (in {ctx.Guild.Name})", memberGuildConfig?.DisplayName ?? "*(none)*"); + eb.AddField($"Server Name (in {ctx.GuildNew.Name})", memberGuildConfig?.DisplayName ?? "*(none)*"); } return eb; @@ -264,11 +261,11 @@ namespace PluralKit.Bot async Task PrintSuccess(string text) { var successStr = text; - if (ctx.Guild != null) + if (ctx.GuildNew != null) { - var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); if (memberGuildConfig.DisplayName != null) - successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; + successStr += $" However, this member has a server name set in this server ({ctx.GuildNew.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; } await ctx.Reply(successStr); @@ -313,12 +310,12 @@ namespace PluralKit.Bot ctx.CheckOwnMember(target); var patch = new MemberGuildPatch {DisplayName = null}; - await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch)); if (target.DisplayName != null) - await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.Guild.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.GuildNew.Name})."); else - await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.Guild.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.GuildNew.Name})."); } else if (!ctx.HasNext()) { @@ -335,9 +332,9 @@ namespace PluralKit.Bot var newServerName = ctx.RemainderOrNull(); var patch = new MemberGuildPatch {DisplayName = newServerName}; - await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch)); - await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.GuildNew.Name})."); } } @@ -417,8 +414,8 @@ namespace PluralKit.Bot // Get guild settings (mostly for warnings and such) MemberGuildSettings guildSettings = null; - if (ctx.Guild != null) - guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + if (ctx.GuildNew != null) + guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); async Task SetAll(PrivacyLevel level) { diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 86bbd4ff..507a2ceb 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -6,29 +6,37 @@ using System.Threading.Tasks; using DSharpPlus; using DSharpPlus.Entities; +using Myriad.Cache; +using Myriad.Extensions; +using Myriad.Types; + using PluralKit.Core; +using Permissions = DSharpPlus.Permissions; + namespace PluralKit.Bot { public class ServerConfig { private readonly IDatabase _db; private readonly ModelRepository _repo; + private readonly IDiscordCache _cache; private readonly LoggerCleanService _cleanService; - public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo) + public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo, IDiscordCache cache) { _cleanService = cleanService; _db = db; _repo = repo; + _cache = cache; } public async Task SetLogChannel(Context ctx) { - ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); if (await ctx.MatchClear("the server log channel")) { - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null})); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, new GuildPatch {LogChannel = null})); await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); return; } @@ -36,36 +44,36 @@ namespace PluralKit.Bot if (!ctx.HasNext()) throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it."); - DiscordChannel channel = null; + Channel channel = null; var channelString = ctx.PeekArgument(); channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); var patch = new GuildPatch {LogChannel = channel.Id}; - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch)); await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}."); } public async Task SetLogEnabled(Context ctx, bool enable) { - ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var affectedChannels = new List(); + var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList(); + affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { var channelString = ctx.PeekArgument(); var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } ulong? logChannel = null; await using (var conn = await _db.Obtain()) { - var config = await _repo.GetGuild(conn, ctx.Guild.Id); + var config = await _repo.GetGuild(conn, ctx.GuildNew.Id); logChannel = config.LogChannel; var blacklist = config.LogBlacklist.ToHashSet(); if (enable) @@ -74,7 +82,7 @@ namespace PluralKit.Bot blacklist.UnionWith(affectedChannels.Select(c => c.Id)); var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()}; - await _repo.UpsertGuild(conn, ctx.Guild.Id, patch); + await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch); } await ctx.Reply( @@ -84,13 +92,13 @@ namespace PluralKit.Bot public async Task ShowBlacklisted(Context ctx) { - ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id)); + var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id)); // Resolve all channels from the cache and order by position var channels = blacklist.Blacklist - .Select(id => ctx.Guild.GetChannel(id)) + .Select(id => _cache.GetChannelOrNull(id)) .Where(c => c != null) .OrderBy(c => c.Position) .ToList(); @@ -102,26 +110,29 @@ namespace PluralKit.Bot } await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, - $"Blacklisted channels for {ctx.Guild.Name}", + $"Blacklisted channels for {ctx.GuildNew.Name}", (eb, l) => { - DiscordChannel lastCategory = null; + string CategoryName(ulong? id) => + id != null ? _cache.GetChannel(id.Value).Name : "(no category)"; + + ulong? lastCategory = null; var fieldValue = new StringBuilder(); foreach (var channel in l) { - if (lastCategory != channel.Parent && fieldValue.Length > 0) + if (lastCategory != channel!.ParentId && fieldValue.Length > 0) { - eb.AddField(lastCategory?.Name ?? "(no category)", fieldValue.ToString()); + eb.AddField(CategoryName(lastCategory), fieldValue.ToString()); fieldValue.Clear(); } else fieldValue.Append("\n"); - fieldValue.Append(channel.Mention); - lastCategory = channel.Parent; + fieldValue.Append(channel.Mention()); + lastCategory = channel.ParentId; } - eb.AddField(lastCategory?.Name ?? "(no category)", fieldValue.ToString()); + eb.AddField(CategoryName(lastCategory), fieldValue.ToString()); return Task.CompletedTask; }); @@ -129,23 +140,23 @@ namespace PluralKit.Bot public async Task SetBlacklisted(Context ctx, bool shouldAdd) { - ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var affectedChannels = new List(); + var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList(); + affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { var channelString = ctx.PeekArgument(); var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } await using (var conn = await _db.Obtain()) { - var guild = await _repo.GetGuild(conn, ctx.Guild.Id); + var guild = await _repo.GetGuild(conn, ctx.GuildNew.Id); var blacklist = guild.Blacklist.ToHashSet(); if (shouldAdd) blacklist.UnionWith(affectedChannels.Select(c => c.Id)); @@ -153,7 +164,7 @@ namespace PluralKit.Bot blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); var patch = new GuildPatch {Blacklist = blacklist.ToArray()}; - await _repo.UpsertGuild(conn, ctx.Guild.Id, patch); + await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch); } await ctx.Reply($"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist."); @@ -161,7 +172,7 @@ namespace PluralKit.Bot public async Task SetLogCleanup(Context ctx) { - ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var botList = string.Join(", ", _cleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); @@ -176,7 +187,7 @@ namespace PluralKit.Bot .WithTitle("Log cleanup settings") .AddField("Supported bots", botList); - var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id)); + var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id)); if (guildCfg.LogCleanupEnabled) eb.WithDescription("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); else @@ -186,7 +197,7 @@ namespace PluralKit.Bot } var patch = new GuildPatch {LogCleanupEnabled = newValue}; - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch)); if (newValue) await ctx.Reply($"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts."); diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index a5094e21..9486ceba 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using DSharpPlus.Entities; - using NodaTime; using NodaTime.TimeZones; diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index a4f641af..ab95b105 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -2,9 +2,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using Dapper; - -using DSharpPlus; using DSharpPlus.Entities; using NodaTime; @@ -13,8 +10,6 @@ using NodaTime.TimeZones; using PluralKit.Core; -using Sentry.Protocol; - namespace PluralKit.Bot { public class SystemEdit @@ -196,7 +191,7 @@ namespace PluralKit.Bot public async Task SystemProxy(Context ctx) { ctx.CheckSystem().CheckGuildContext(); - var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.Guild.Id, ctx.System.Id)); + var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.GuildNew.Id, ctx.System.Id)); bool newValue; if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; @@ -212,12 +207,12 @@ namespace PluralKit.Bot } var patch = new SystemGuildPatch {ProxyEnabled = newValue}; - await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch)); if (newValue) - await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **enabled** for your system."); + await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **enabled** for your system."); else - await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **disabled** for your system."); + await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **disabled** for your system."); } public async Task SystemTimezone(Context ctx) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 9dfc15da..1f22b88a 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 7d7db63d..493e5bda 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -1,8 +1,6 @@ using System.Text; using System.Threading.Tasks; -using NodaTime; - using PluralKit.Core; namespace PluralKit.Bot diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 95cc9995..0a72c424 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -61,8 +61,8 @@ namespace PluralKit.Bot if (evt.Type != Message.MessageType.Default) return; if (IsDuplicateMessage(evt)) return; - var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null; - var channel = await _cache.GetChannel(evt.ChannelId); + var guild = evt.GuildId != null ? _cache.GetGuild(evt.GuildId.Value) : null; + var channel = _cache.GetChannel(evt.ChannelId); // Log metrics and message info _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); @@ -89,8 +89,8 @@ namespace PluralKit.Bot private async ValueTask TryHandleLogClean(MessageCreateEvent evt, MessageContext ctx) { - var channel = await _cache.GetChannel(evt.ChannelId); - if (!evt.Author.Bot || channel!.Type != Channel.ChannelType.GuildText || + var channel = _cache.GetChannel(evt.ChannelId); + if (!evt.Author.Bot || channel.Type != Channel.ChannelType.GuildText || !ctx.LogCleanupEnabled) return false; await _loggerClean.HandleLoggerBotCleanup(evt); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index f9ca05bb..0631d8b1 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -5,9 +5,13 @@ using System.Threading.Tasks; using DSharpPlus; using DSharpPlus.Entities; -using DSharpPlus.Exceptions; using Humanizer; + +using Myriad.Cache; +using Myriad.Rest; +using Myriad.Types; + using NodaTime; using PluralKit.Core; @@ -18,54 +22,79 @@ namespace PluralKit.Bot { private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly DiscordShardedClient _client; + private readonly IDiscordCache _cache; + private readonly DiscordApiClient _rest; - public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo) + public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) { _client = client; _db = db; _repo = repo; + _cache = cache; + _rest = rest; + } + + private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable ids) + { + async Task<(ulong Id, User? User)> Inner(ulong id) + { + if (_cache.TryGetUser(id, out var cachedUser)) + return (id, cachedUser); + + var user = await _rest.GetUser(id); + if (user == null) + return (id, null); + // todo: move to "GetUserCached" helper + await _cache.SaveUser(user); + return (id, user); + } + + return Task.WhenAll(ids.Select(Inner)); } - public async Task CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx) + public async Task CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx) { await using var conn = await _db.Obtain(); // Fetch/render info for all accounts simultaneously var accounts = await _repo.GetSystemAccounts(conn, system.Id); - var users = await Task.WhenAll(accounts.Select(async uid => (await cctx.Shard.GetUser(uid))?.NameAndMention() ?? $"(deleted account {uid})")); + var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})"); var memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id); - var eb = new DiscordEmbedBuilder() - .WithColor(DiscordUtils.Gray) - .WithTitle(system.Name ?? null) - .WithThumbnail(system.AvatarUrl) - .WithFooter($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}"); + var embed = new Embed + { + Title = system.Name, + Thumbnail = new(system.AvatarUrl), + Footer = new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}"), + Color = (uint?) DiscordUtils.Gray.Value + }; + var fields = new List(); var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) { var switchMembers = await _repo.GetSwitchMembers(conn, latestSwitch.Id).ToListAsync(); - if (switchMembers.Count > 0) - eb.AddField("Fronter".ToQuantity(switchMembers.Count(), ShowQuantityAs.None), - string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))); + if (switchMembers.Count > 0) + fields.Add(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))))); } - if (system.Tag != null) eb.AddField("Tag", system.Tag.EscapeMarkdown()); - eb.AddField("Linked accounts", string.Join("\n", users).Truncate(1000), true); + if (system.Tag != null) + fields.Add(new("Tag", system.Tag.EscapeMarkdown())); + fields.Add(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); if (system.MemberListPrivacy.CanAccess(ctx)) { if (memberCount > 0) - eb.AddField($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true); + fields.Add(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true)); else - eb.AddField($"Members ({memberCount})", "Add one with `pk;member new`!", true); + fields.Add(new($"Members ({memberCount})", "Add one with `pk;member new`!", true)); } if (system.DescriptionFor(ctx) is { } desc) - eb.AddField("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false); + fields.Add(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false)); - return eb.Build(); + return embed with { Fields = fields.ToArray() }; } public DiscordEmbed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, DiscordUser sender, string content, DiscordChannel channel) { diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index 241edde3..c4bad54a 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -60,18 +60,16 @@ namespace PluralKit.Bot { private async Task FindLogChannel(ulong guildId, ulong channelId) { // TODO: fetch it directly on cache miss? - var channel = await _cache.GetChannel(channelId); + if (_cache.TryGetChannel(channelId, out var channel)) + return channel; - if (channel == null) - { - // Channel doesn't exist or we don't have permission to access it, let's remove it from the database too - _logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId); - await using var conn = await _db.Obtain(); - await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild", - new {Guild = guildId}); - } + // Channel doesn't exist or we don't have permission to access it, let's remove it from the database too + _logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId); + await using var conn = await _db.Obtain(); + await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild", + new {Guild = guildId}); - return channel; + return null; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 61efabbe..005f2b45 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -9,6 +9,7 @@ using App.Metrics; using Humanizer; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Rest; using Myriad.Rest.Types; using Myriad.Rest.Types.Requests; @@ -77,20 +78,22 @@ namespace PluralKit.Bot private async Task ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false) { - var guild = await _cache.GetGuild(req.GuildId)!; + var guild = _cache.GetGuild(req.GuildId); var content = req.Content.Truncate(2000); + var allowedMentions = content.ParseMentions(); + if (!req.AllowEveryone) + allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild); + var webhookReq = new ExecuteWebhookRequest { Username = FixClyde(req.Name).Truncate(80), Content = content, - AllowedMentions = null, // todo + AllowedMentions = allowedMentions, AvatarUrl = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null, Embeds = req.Embeds }; - // dwb.AddMentions(content.ParseAllMentions(guild, req.AllowEveryone)); - MultipartFile[] files = null; var attachmentChunks = ChunkAttachmentsOrThrow(req.Attachments, 8 * 1024 * 1024); if (attachmentChunks.Count > 0) diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 4733bd99..67bb369d 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -11,10 +11,14 @@ using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; +using Myriad.Types; + using NodaTime; using PluralKit.Core; +using Permissions = DSharpPlus.Permissions; + namespace PluralKit.Bot { public static class ContextUtils { public static async Task ConfirmClear(this Context ctx, string toClear) @@ -149,7 +153,8 @@ namespace PluralKit.Bot { if (currentPage < 0) currentPage += pageCount; // If we can, remove the user's reaction (so they can press again quickly) - if (ctx.BotHasAllPermissions(Permissions.ManageMessages)) await msg.DeleteReactionAsync(reaction.Emoji, reaction.User); + if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) + await msg.DeleteReactionAsync(reaction.Emoji, reaction.User); // Edit the embed with the new page var embed = await MakeEmbedForPage(currentPage); @@ -159,7 +164,8 @@ namespace PluralKit.Bot { // "escape hatch", clean up as if we hit X } - if (ctx.BotHasAllPermissions(Permissions.ManageMessages)) await msg.DeleteAllReactionsAsync(); + if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) + await msg.DeleteAllReactionsAsync(); } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } @@ -245,12 +251,7 @@ namespace PluralKit.Bot { return items[Array.IndexOf(indicators, reaction.Emoji.Name)]; } } - - public static Permissions BotPermissions(this Context ctx) => ctx.Channel.BotPermissions(); - - public static bool BotHasAllPermissions(this Context ctx, Permissions permission) => - ctx.Channel.BotHasAllPermissions(permission); - + public static async Task BusyIndicator(this Context ctx, Func f, string emoji = "\u23f3" /* hourglass */) { await ctx.BusyIndicator(async () => @@ -265,8 +266,8 @@ namespace PluralKit.Bot { var task = f(); // If we don't have permission to add reactions, don't bother, and just await the task normally. - var neededPermissions = Permissions.AddReactions | Permissions.ReadMessageHistory; - if ((ctx.BotPermissions() & neededPermissions) != neededPermissions) return await task; + var neededPermissions = PermissionSet.AddReactions | PermissionSet.ReadMessageHistory; + if ((ctx.BotPermissions & neededPermissions) != neededPermissions) return await task; try { diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 903f2b31..ee3e391d 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -12,6 +12,8 @@ using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; +using Myriad.Extensions; +using Myriad.Rest.Types; using Myriad.Types; using NodaTime; @@ -50,6 +52,11 @@ namespace PluralKit.Bot { return $"{user.Username}#{user.Discriminator} ({user.Mention})"; } + + public static string NameAndMention(this User user) + { + return $"{user.Username}#{user.Discriminator} ({user.Mention()})"; + } // We funnel all "permissions from DiscordMember" calls through here // This way we can ensure we do the read permission correction everywhere @@ -74,20 +81,7 @@ namespace PluralKit.Bot var invalidRoleIds = roleIdCache.Where(x => !currentRoleIds.Contains(x)).ToList(); roleIdCache.RemoveAll(x => invalidRoleIds.Contains(x)); } - - public static async Task PermissionsIn(this DiscordChannel channel, DiscordUser user) - { - // Just delegates to PermissionsInSync, but handles the case of a non-member User in a guild properly - // This is a separate method because it requires an async call - if (channel.Guild != null && !(user is DiscordMember)) - { - var member = await channel.Guild.GetMember(user.Id); - if (member != null) - return PermissionsInSync(channel, member); - } - - return PermissionsInSync(channel, user); - } + // 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) @@ -194,23 +188,27 @@ namespace PluralKit.Bot return false; } - public static IEnumerable ParseAllMentions(this string input, Guild guild, bool allowEveryone = false) + public static AllowedMentions ParseMentions(this string input) { - var mentions = new List(); - mentions.AddRange(USER_MENTION.Matches(input) - .Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention)); + var users = USER_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value)); + var roles = ROLE_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value)); + var everyone = EVERYONE_HERE_MENTION.IsMatch(input); + + return new AllowedMentions + { + Users = users.ToArray(), + Roles = roles.ToArray(), + Parse = everyone ? new[] {AllowedMentions.ParseType.Everyone} : null + }; + } - // Only allow role mentions through where the role is actually listed as *mentionable* - // (ie. any user can @ them, regardless of permissions) - // Still let the allowEveryone flag override this though (privileged users can @ *any* role) - // Original fix by Gwen - mentions.AddRange(ROLE_MENTION.Matches(input) - .Select(x => ulong.Parse(x.Groups[1].Value)) - .Where(x => allowEveryone || guild != null && (guild.Roles.FirstOrDefault(g => g.Id == x)?.Mentionable ?? false)) - .Select(x => new RoleMention(x) as IMention)); - if (EVERYONE_HERE_MENTION.IsMatch(input) && allowEveryone) - mentions.Add(new EveryoneMention()); - return mentions; + public static AllowedMentions RemoveUnmentionableRoles(this AllowedMentions mentions, Guild guild) + { + return mentions with { + Roles = mentions.Roles + ?.Where(id => guild.Roles.FirstOrDefault(r => r.Id == id)?.Mentionable == true) + .ToArray() + }; } public static string EscapeMarkdown(this string input) From f6fb8204bb314d2b824fbbf1cee0ee260c84b757 Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 23 Dec 2020 02:19:02 +0100 Subject: [PATCH 003/608] Add embed builder, some more ported classes --- Myriad/Builders/EmbedBuilder.cs | 86 +++++++++++ Myriad/Extensions/MessageExtensions.cs | 10 +- Myriad/Rest/DiscordApiClient.cs | 6 +- Myriad/Types/Embed.cs | 4 +- PluralKit.Bot/Bot.cs | 14 +- PluralKit.Bot/CommandSystem/Context.cs | 6 +- PluralKit.Bot/Commands/CommandTree.cs | 10 -- PluralKit.Bot/Handlers/MessageCreated.cs | 5 +- PluralKit.Bot/Handlers/MessageDeleted.cs | 9 +- PluralKit.Bot/Handlers/ReactionAdded.cs | 120 +++++++------- PluralKit.Bot/Services/EmbedService.cs | 146 +++++++++--------- PluralKit.Bot/Services/ErrorMessageService.cs | 42 +++-- PluralKit.Bot/Services/LogChannelService.cs | 36 +++-- 13 files changed, 305 insertions(+), 189 deletions(-) create mode 100644 Myriad/Builders/EmbedBuilder.cs diff --git a/Myriad/Builders/EmbedBuilder.cs b/Myriad/Builders/EmbedBuilder.cs new file mode 100644 index 00000000..ecfc3524 --- /dev/null +++ b/Myriad/Builders/EmbedBuilder.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; + +using Myriad.Types; + +namespace Myriad.Builders +{ + public class EmbedBuilder + { + private Embed _embed = new(); + private readonly List _fields = new(); + + public EmbedBuilder Title(string? title) + { + _embed = _embed with {Title = title}; + return this; + } + + public EmbedBuilder Description(string? description) + { + _embed = _embed with { Description = description}; + return this; + } + + public EmbedBuilder Url(string? url) + { + _embed = _embed with {Url = url}; + return this; + } + + public EmbedBuilder Color(uint? color) + { + _embed = _embed with {Color = color}; + return this; + } + + public EmbedBuilder Footer(Embed.EmbedFooter? footer) + { + _embed = _embed with { + Footer = footer + }; + return this; + } + + public EmbedBuilder Image(Embed.EmbedImage? image) + { + _embed = _embed with { + Image = image + }; + return this; + } + + + public EmbedBuilder Thumbnail(Embed.EmbedThumbnail? thumbnail) + { + _embed = _embed with { + Thumbnail = thumbnail + }; + return this; + } + + public EmbedBuilder Author(Embed.EmbedAuthor? author) + { + _embed = _embed with { + Author = author + }; + return this; + } + + public EmbedBuilder Timestamp(string? timestamp) + { + _embed = _embed with { + Timestamp = timestamp + }; + return this; + } + + public EmbedBuilder Field(Embed.Field field) + { + _fields.Add(field); + return this; + } + + public Embed Build() => + _embed with { Fields = _fields.ToArray() }; + } +} \ No newline at end of file diff --git a/Myriad/Extensions/MessageExtensions.cs b/Myriad/Extensions/MessageExtensions.cs index 7393a9a2..60adb532 100644 --- a/Myriad/Extensions/MessageExtensions.cs +++ b/Myriad/Extensions/MessageExtensions.cs @@ -1,6 +1,14 @@ -namespace Myriad.Extensions +using Myriad.Gateway; +using Myriad.Types; + +namespace Myriad.Extensions { public static class MessageExtensions { + public static string JumpLink(this Message msg) => + $"https://discord.com/channels/{msg.GuildId}/{msg.ChannelId}/{msg.Id}"; + + public static string JumpLink(this MessageReactionAddEvent msg) => + $"https://discord.com/channels/{msg.GuildId}/{msg.ChannelId}/{msg.MessageId}"; } } \ No newline at end of file diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 27588b51..71813481 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -39,6 +39,10 @@ namespace Myriad.Rest public Task GetUser(ulong id) => _client.Get($"/users/{id}", ("GetUser", default)); + public Task GetGuildMember(ulong guildId, ulong userId) => + _client.Get($"/guilds/{guildId}/members/{userId}", + ("GetGuildMember", guildId)); + public Task CreateMessage(ulong channelId, MessageRequest request) => _client.Post($"/channels/{channelId}/messages", ("CreateMessage", channelId), request)!; @@ -110,7 +114,7 @@ namespace Myriad.Rest public Task ExecuteWebhook(ulong webhookId, string webhookToken, ExecuteWebhookRequest request, MultipartFile[]? files = null) => - _client.PostMultipart($"/webhooks/{webhookId}/{webhookToken}", + _client.PostMultipart($"/webhooks/{webhookId}/{webhookToken}?wait=true", ("ExecuteWebhook", webhookId), request, files)!; private static string EncodeEmoji(Emoji emoji) => diff --git a/Myriad/Types/Embed.cs b/Myriad/Types/Embed.cs index 46560cb8..7aa09e17 100644 --- a/Myriad/Types/Embed.cs +++ b/Myriad/Types/Embed.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Myriad.Types +namespace Myriad.Types { public record Embed { diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index b7914d91..b73a6db3 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -11,6 +11,7 @@ using App.Metrics; using Autofac; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Gateway; using Myriad.Rest; using Myriad.Types; @@ -75,8 +76,19 @@ namespace PluralKit.Bot }, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1)); } - public GuildMemberPartial? BotMemberIn(ulong guildId) => _guildMembers.GetValueOrDefault(guildId); + public PermissionSet PermissionsIn(ulong channelId) + { + var channel = _cache.GetChannel(channelId); + if (channel.GuildId != null) + { + var member = _guildMembers.GetValueOrDefault(channel.GuildId.Value); + return _cache.PermissionsFor(channelId, _cluster.User?.Id ?? default, member?.Roles); + } + + return PermissionSet.Dm; + } + private async Task OnEventReceived(Shard shard, IGatewayEvent evt) { await _cache.HandleGatewayEvent(evt); diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index a5d8c29a..1402705e 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -37,7 +37,6 @@ namespace PluralKit.Bot private readonly Message _messageNew; private readonly Parameters _parameters; private readonly MessageContext _messageContext; - private readonly GuildMemberPartial? _botMember; private readonly PermissionSet _botPermissions; private readonly PermissionSet _userPermissions; @@ -51,7 +50,7 @@ namespace PluralKit.Bot private Command _currentCommand; public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, - PKSystem senderSystem, MessageContext messageContext, GuildMemberPartial? botMember) + PKSystem senderSystem, MessageContext messageContext, PermissionSet botPermissions) { _rest = provider.Resolve(); _client = provider.Resolve(); @@ -61,7 +60,6 @@ namespace PluralKit.Bot _channel = channel; _senderSystem = senderSystem; _messageContext = messageContext; - _botMember = botMember; _cache = provider.Resolve(); _db = provider.Resolve(); _repo = provider.Resolve(); @@ -71,7 +69,7 @@ namespace PluralKit.Bot _parameters = new Parameters(message.Content.Substring(commandParseOffset)); _newRest = provider.Resolve(); - _botPermissions = _cache.PermissionsFor(message.ChannelId, shard.User!.Id, botMember!); + _botPermissions = botPermissions; _userPermissions = _cache.PermissionsFor(message); } diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index b29d3816..a5868184 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -1,8 +1,6 @@ using System.Linq; using System.Threading.Tasks; -using DSharpPlus; - using Humanizer; using PluralKit.Core; @@ -119,14 +117,6 @@ namespace PluralKit.Bot public static Command[] LogCommands = {LogChannel, LogChannelClear, LogEnable, LogDisable}; public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow}; - - private DiscordShardedClient _client; - - public CommandTree(DiscordShardedClient client) - { - - _client = client; - } public Task ExecuteCommand(Context ctx) { diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 0a72c424..cd5a0f00 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -114,7 +114,7 @@ namespace PluralKit.Bot try { var system = ctx.SystemId != null ? await _db.Execute(c => _repo.GetSystem(c, ctx.SystemId.Value)) : null; - await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.BotMemberIn(channel.GuildId!.Value))); + await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.PermissionsIn(channel.Id))); } catch (PKError) { @@ -147,8 +147,7 @@ namespace PluralKit.Bot private async ValueTask TryHandleProxy(Shard shard, MessageCreateEvent evt, Guild guild, Channel channel, MessageContext ctx) { - var botMember = _bot.BotMemberIn(channel.GuildId!.Value); - var botPermissions = PermissionExtensions.PermissionsFor(guild, channel, shard.User!.Id, botMember!.Roles); + var botPermissions = _bot.PermissionsIn(channel.Id); try { diff --git a/PluralKit.Bot/Handlers/MessageDeleted.cs b/PluralKit.Bot/Handlers/MessageDeleted.cs index 3d2c236c..084ee861 100644 --- a/PluralKit.Bot/Handlers/MessageDeleted.cs +++ b/PluralKit.Bot/Handlers/MessageDeleted.cs @@ -34,7 +34,7 @@ namespace PluralKit.Bot { await Task.Delay(MessageDeleteDelay); // TODO - // await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); + await _db.Execute(c => _repo.DeleteMessage(c, evt.Id)); } // Fork a task to delete the message after a short delay @@ -49,9 +49,10 @@ namespace PluralKit.Bot async Task Inner() { await Task.Delay(MessageDeleteDelay); - // TODO - // _logger.Information("Bulk deleting {Count} messages in channel {Channel}", evt.Messages.Count, evt.Channel.Id); - // await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Messages.Select(m => m.Id).ToList())); + + _logger.Information("Bulk deleting {Count} messages in channel {Channel}", + evt.Ids.Length, evt.ChannelId); + await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Ids)); } _ = Inner(); diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 6d2c2a15..9c20c3b0 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -1,11 +1,13 @@ using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; - +using Myriad.Builders; +using Myriad.Cache; +using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Rest.Exceptions; +using Myriad.Rest.Types; +using Myriad.Types; using PluralKit.Core; @@ -18,37 +20,42 @@ namespace PluralKit.Bot private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly CommandMessageService _commandMessageService; - private readonly EmbedService _embeds; private readonly ILogger _logger; + private readonly IDiscordCache _cache; + private readonly Bot _bot; + private readonly DiscordApiClient _rest; - public ReactionAdded(EmbedService embeds, ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService) + public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest) { - _embeds = embeds; _db = db; _repo = repo; _commandMessageService = commandMessageService; + _cache = cache; + _bot = bot; + _rest = rest; _logger = logger.ForContext(); } public async Task Handle(Shard shard, MessageReactionAddEvent evt) { - // await TryHandleProxyMessageReactions(shard, evt); + await TryHandleProxyMessageReactions(evt); } - private async ValueTask TryHandleProxyMessageReactions(DiscordClient shard, MessageReactionAddEventArgs evt) + private async ValueTask TryHandleProxyMessageReactions(MessageReactionAddEvent evt) { - // Sometimes we get events from users that aren't in the user cache - // In that case we get a "broken" user object (where eg. calling IsBot throws an exception) // We just ignore all of those for now, should be quite rare... - if (!shard.TryGetCachedUser(evt.User.Id, out _)) return; + if (!_cache.TryGetUser(evt.UserId, out var user)) + return; + + var channel = _cache.GetChannel(evt.ChannelId); // check if it's a command message first // since this can happen in DMs as well if (evt.Emoji.Name == "\u274c") { await using var conn = await _db.Obtain(); - var commandMsg = await _commandMessageService.GetCommandMessage(conn, evt.Message.Id); + var commandMsg = await _commandMessageService.GetCommandMessage(conn, evt.MessageId); if (commandMsg != null) { await HandleCommandDeleteReaction(evt, commandMsg); @@ -57,10 +64,10 @@ namespace PluralKit.Bot } // Only proxies in guild text channels - if (evt.Channel == null || evt.Channel.Type != ChannelType.Text) return; + if (channel.Type != Channel.ChannelType.GuildText) return; // Ignore reactions from bots (we can't DM them anyway) - if (evt.User.IsBot) return; + if (user.Bot) return; switch (evt.Emoji.Name) { @@ -68,7 +75,7 @@ namespace PluralKit.Bot case "\u274C": // Red X { await using var conn = await _db.Obtain(); - var msg = await _repo.GetMessage(conn, evt.Message.Id); + var msg = await _repo.GetMessage(conn, evt.MessageId); if (msg != null) await HandleProxyDeleteReaction(evt, msg); @@ -78,9 +85,9 @@ namespace PluralKit.Bot case "\u2754": // White question mark { await using var conn = await _db.Obtain(); - var msg = await _repo.GetMessage(conn, evt.Message.Id); + var msg = await _repo.GetMessage(conn, evt.MessageId); if (msg != null) - await HandleQueryReaction(shard, evt, msg); + await HandleQueryReaction(evt, msg); break; } @@ -92,7 +99,7 @@ namespace PluralKit.Bot case "\u2757": // Exclamation mark { await using var conn = await _db.Obtain(); - var msg = await _repo.GetMessage(conn, evt.Message.Id); + var msg = await _repo.GetMessage(conn, evt.MessageId); if (msg != null) await HandlePingReaction(evt, msg); break; @@ -100,37 +107,39 @@ namespace PluralKit.Bot } } - private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEventArgs evt, FullMessage msg) + private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, FullMessage msg) { - if (!evt.Channel.BotHasAllPermissions(Permissions.ManageMessages)) return; + if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages)) + return; // Can only delete your own message - if (msg.Message.Sender != evt.User.Id) return; + if (msg.Message.Sender != evt.UserId) return; try { - await evt.Message.DeleteAsync(); + await _rest.DeleteMessage(evt.ChannelId, evt.MessageId); } catch (NotFoundException) { // Message was deleted by something/someone else before we got to it } - await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); + await _db.Execute(c => _repo.DeleteMessage(c, evt.MessageId)); } - private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEventArgs evt, CommandMessage msg) + private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEvent evt, CommandMessage msg) { - if (!evt.Channel.BotHasAllPermissions(Permissions.ManageMessages) && evt.Channel.Guild != null) + // TODO: why does the bot need manage messages if it's deleting its own messages?? + if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages)) return; // Can only delete your own message - if (msg.AuthorId != evt.User.Id) + if (msg.AuthorId != evt.UserId) return; try { - await evt.Message.DeleteAsync(); + await _rest.DeleteMessage(evt.ChannelId, evt.MessageId); } catch (NotFoundException) { @@ -140,44 +149,52 @@ namespace PluralKit.Bot // No need to delete database row here, it'll get deleted by the once-per-minute scheduled task. } - private async ValueTask HandleQueryReaction(DiscordClient shard, MessageReactionAddEventArgs evt, FullMessage msg) + private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg) { // Try to DM the user info about the message - var member = await evt.Guild.GetMember(evt.User.Id); + // var member = await evt.Guild.GetMember(evt.User.Id); try { - await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner)); - await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg)); + // TODO: how to DM? + // await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner)); + // await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg)); } catch (UnauthorizedException) { } // No permissions to DM, can't check for this :( await TryRemoveOriginalReaction(evt); } - private async ValueTask HandlePingReaction(MessageReactionAddEventArgs evt, FullMessage msg) + private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg) { - if (!evt.Channel.BotHasAllPermissions(Permissions.SendMessages)) return; + if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages)) + return; // Check if the "pinger" has permission to send messages in this channel // (if not, PK shouldn't send messages on their behalf) - var guildUser = await evt.Guild.GetMember(evt.User.Id); - var requiredPerms = Permissions.AccessChannels | Permissions.SendMessages; - if (guildUser == null || (guildUser.PermissionsIn(evt.Channel) & requiredPerms) != requiredPerms) return; + var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId); + var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages; + if (member == null || !_cache.PermissionsFor(evt.ChannelId, member).HasFlag(requiredPerms)) return; if (msg.System.PingsEnabled) { // If the system has pings enabled, go ahead - var embed = new DiscordEmbedBuilder().WithDescription($"[Jump to pinged message]({evt.Message.JumpLink})"); - await evt.Channel.SendMessageFixedAsync($"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.User.Id}>.", embed: embed.Build(), - new IMention[] {new UserMention(msg.Message.Sender) }); + var embed = new EmbedBuilder().Description($"[Jump to pinged message]({evt.JumpLink()})"); + await _rest.CreateMessage(evt.ChannelId, new() + { + Content = + $"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.UserId}>.", + Embed = embed.Build(), + AllowedMentions = new AllowedMentions {Users = new[] {msg.Message.Sender}} + }); } else { // If not, tell them in DMs (if we can) try { - await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"); - await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode()); + // todo: how to dm + // await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"); + // await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode()); } catch (UnauthorizedException) { } } @@ -185,21 +202,10 @@ namespace PluralKit.Bot await TryRemoveOriginalReaction(evt); } - private async Task TryRemoveOriginalReaction(MessageReactionAddEventArgs evt) + private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt) { - try - { - if (evt.Channel.BotHasAllPermissions(Permissions.ManageMessages)) - await evt.Message.DeleteReactionAsync(evt.Emoji, evt.User); - } - catch (UnauthorizedException) - { - var botPerms = evt.Channel.BotPermissions(); - // So, in some cases (see Sentry issue 11K) the above check somehow doesn't work, and - // Discord returns a 403 Unauthorized. TODO: figure out the root cause here instead of a workaround - _logger.Warning("Attempted to remove reaction {Emoji} from user {User} on message {Channel}/{Message}, but got 403. Bot has permissions {Permissions} according to itself.", - evt.Emoji.Id, evt.User.Id, evt.Channel.Id, evt.Message.Id, botPerms); - } + if (_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages)) + await _rest.DeleteOwnReaction(evt.ChannelId, evt.MessageId, evt.Emoji); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 0631d8b1..da8b7d10 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -8,6 +8,7 @@ using DSharpPlus.Entities; using Humanizer; +using Myriad.Builders; using Myriad.Cache; using Myriad.Rest; using Myriad.Types; @@ -62,55 +63,52 @@ namespace PluralKit.Bot { var memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id); - var embed = new Embed - { - Title = system.Name, - Thumbnail = new(system.AvatarUrl), - Footer = new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}"), - Color = (uint?) DiscordUtils.Gray.Value - }; - var fields = new List(); - + var eb = new EmbedBuilder() + .Title(system.Name) + .Thumbnail(new(system.AvatarUrl)) + .Footer(new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}")) + .Color((uint) DiscordUtils.Gray.Value); + var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) { var switchMembers = await _repo.GetSwitchMembers(conn, latestSwitch.Id).ToListAsync(); if (switchMembers.Count > 0) - fields.Add(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))))); + eb.Field(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))))); } if (system.Tag != null) - fields.Add(new("Tag", system.Tag.EscapeMarkdown())); - fields.Add(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); + eb.Field(new("Tag", system.Tag.EscapeMarkdown())); + eb.Field(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); if (system.MemberListPrivacy.CanAccess(ctx)) { if (memberCount > 0) - fields.Add(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true)); + eb.Field(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true)); else - fields.Add(new($"Members ({memberCount})", "Add one with `pk;member new`!", true)); + eb.Field(new($"Members ({memberCount})", "Add one with `pk;member new`!", true)); } if (system.DescriptionFor(ctx) is { } desc) - fields.Add(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false)); + eb.Field(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false)); - return embed with { Fields = fields.ToArray() }; + return eb.Build(); } - public DiscordEmbed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, DiscordUser sender, string content, DiscordChannel channel) { + public Embed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, User sender, string content, Channel channel) { // TODO: pronouns in ?-reacted response using this card var timestamp = DiscordUtils.SnowflakeToInstant(messageId); var name = member.NameFor(LookupContext.ByNonOwner); - return new DiscordEmbedBuilder() - .WithAuthor($"#{channel.Name}: {name}", iconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner))) - .WithThumbnail(member.AvatarFor(LookupContext.ByNonOwner)) - .WithDescription(content?.NormalizeLineEndSpacing()) - .WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}") - .WithTimestamp(timestamp.ToDateTimeOffset()) + return new EmbedBuilder() + .Author(new($"#{channel.Name}: {name}", IconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner)))) + .Thumbnail(new(member.AvatarFor(LookupContext.ByNonOwner))) + .Description(content?.NormalizeLineEndSpacing()) + .Footer(new($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}")) + .Timestamp(timestamp.ToDateTimeOffset().ToString("O")) .Build(); } - public async Task CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) + public async Task CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -141,13 +139,14 @@ namespace PluralKit.Bot { .Where(g => g.Visibility.CanAccess(ctx)) .OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase) .ToListAsync(); - - var eb = new DiscordEmbedBuilder() + + var eb = new EmbedBuilder() // TODO: add URL of website when that's up - .WithAuthor(name, iconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)) + .Author(new(name, IconUrl: DiscordUtils.WorkaroundForUrlBug(avatar))) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) - .WithColor(color) - .WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}":"")}"); + .Color((uint?) color.Value) + .Footer(new( + $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}" : "")}")); var description = ""; if (member.MemberVisibility == PrivacyLevel.Private) description += "*(this member is hidden)*\n"; @@ -156,21 +155,21 @@ namespace PluralKit.Bot { description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n"; else description += "*(this member has a server-specific avatar set)*\n"; - if (description != "") eb.WithDescription(description); + if (description != "") eb.Description(description); - if (avatar != null) eb.WithThumbnail(avatar); + if (avatar != null) eb.Thumbnail(new(avatar)); - if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) eb.AddField("Display Name", member.DisplayName.Truncate(1024), true); - if (guild != null && guildDisplayName != null) eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true); - if (member.BirthdayFor(ctx) != null) eb.AddField("Birthdate", member.BirthdayString, true); - if (member.PronounsFor(ctx) is {} pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.AddField("Pronouns", pronouns.Truncate(1024), true); - if (member.MessageCountFor(ctx) is {} count && count > 0) eb.AddField("Message Count", member.MessageCount.ToString(), true); - if (member.HasProxyTags) eb.AddField("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true); + if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) eb.Field(new("Display Name", member.DisplayName.Truncate(1024), true)); + if (guild != null && guildDisplayName != null) eb.Field(new($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true)); + if (member.BirthdayFor(ctx) != null) eb.Field(new("Birthdate", member.BirthdayString, true)); + if (member.PronounsFor(ctx) is {} pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.Field(new("Pronouns", pronouns.Truncate(1024), true)); + if (member.MessageCountFor(ctx) is {} count && count > 0) eb.Field(new("Message Count", member.MessageCount.ToString(), true)); + if (member.HasProxyTags) eb.Field(new("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true)); // --- For when this gets added to the member object itself or however they get added // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))); // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value)); // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true); - if (!member.Color.EmptyOrNull()) eb.AddField("Color", $"#{member.Color}", true); + if (!member.Color.EmptyOrNull()) eb.Field(new("Color", $"#{member.Color}", true)); if (groups.Count > 0) { @@ -178,15 +177,16 @@ namespace PluralKit.Bot { var content = groups.Count > 5 ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name)) : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**")); - eb.AddField($"Groups ({groups.Count})", content.Truncate(1000)); + eb.Field(new($"Groups ({groups.Count})", content.Truncate(1000))); } - if (member.DescriptionFor(ctx) is {} desc) eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false); + if (member.DescriptionFor(ctx) is {} desc) + eb.Field(new("Description", member.Description.NormalizeLineEndSpacing(), false)); return eb.Build(); } - public async Task CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target) + public async Task CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target) { await using var conn = await _db.Obtain(); @@ -197,43 +197,43 @@ namespace PluralKit.Bot { if (system.Name != null) nameField = $"{nameField} ({system.Name})"; - var eb = new DiscordEmbedBuilder() - .WithAuthor(nameField, iconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx))) - .WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}"); + var eb = new EmbedBuilder() + .Author(new(nameField, IconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx)))) + .Footer(new($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}")); if (target.DisplayName != null) - eb.AddField("Display Name", target.DisplayName); + eb.Field(new("Display Name", target.DisplayName)); if (target.ListPrivacy.CanAccess(pctx)) { if (memberCount == 0 && pctx == LookupContext.ByOwner) // Only suggest the add command if this is actually the owner lol - eb.AddField("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", true); + eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", true)); else - eb.AddField($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true); + eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true)); } - if (target.DescriptionFor(pctx) is {} desc) - eb.AddField("Description", desc); + if (target.DescriptionFor(pctx) is { } desc) + eb.Field(new("Description", desc)); if (target.IconFor(pctx) is {} icon) - eb.WithThumbnail(icon); + eb.Thumbnail(new(icon)); return eb.Build(); } - public async Task CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx) + public async Task CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx) { var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id).ToListAsync().AsTask()); var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; - return new DiscordEmbedBuilder() - .WithColor(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray) - .AddField($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*") - .AddField("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)") + return new EmbedBuilder() + .Color((uint?) (members.FirstOrDefault()?.Color?.ToDiscordColor()?.Value ?? DiscordUtils.Gray.Value)) + .Field(new($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*")) + .Field(new("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)")) .Build(); } - public async Task CreateMessageInfoEmbed(DiscordClient client, FullMessage msg) + public async Task CreateMessageInfoEmbed(DiscordClient client, FullMessage msg) { var ctx = LookupContext.ByNonOwner; var channel = await _client.GetChannel(msg.Message.Channel); @@ -257,32 +257,32 @@ namespace PluralKit.Bot { else userStr = $"*(deleted user {msg.Message.Sender})*"; // Put it all together - var eb = new DiscordEmbedBuilder() - .WithAuthor(msg.Member.NameFor(ctx), iconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx))) - .WithDescription(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*") - .WithImageUrl(serverMsg?.Attachments?.FirstOrDefault()?.Url) - .AddField("System", - msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true) - .AddField("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true) - .AddField("Sent by", userStr, inline: true) - .WithTimestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset()); + var eb = new EmbedBuilder() + .Author(new(msg.Member.NameFor(ctx), IconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx)))) + .Description(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*") + .Image(new(serverMsg?.Attachments?.FirstOrDefault()?.Url)) + .Field(new("System", + msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true)) + .Field(new("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true)) + .Field(new("Sent by", userStr, true)) + .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O")); var roles = memberInfo?.Roles?.ToList(); if (roles != null && roles.Count > 0) { var rolesString = string.Join(", ", roles.Select(role => role.Name)); - eb.AddField($"Account roles ({roles.Count})", rolesString.Truncate(1024)); + eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024))); } return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; - var eb = new DiscordEmbedBuilder() - .WithColor(DiscordUtils.Gray) - .WithFooter($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)"); + var eb = new EmbedBuilder() + .Color((uint?) DiscordUtils.Gray.Value) + .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" @@ -296,15 +296,15 @@ namespace PluralKit.Bot { foreach (var pair in membersOrdered) { var frac = pair.Value / actualPeriod; - eb.AddField(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})"); + eb.Field(new(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})")); } if (membersOrdered.Count > maxEntriesToDisplay) { - eb.AddField("(others)", + eb.Field(new("(others)", membersOrdered.Skip(maxEntriesToDisplay) .Aggregate(Duration.Zero, (prod, next) => prod + next.Value) - .FormatDuration(), true); + .FormatDuration(), true)); } return Task.FromResult(eb.Build()); diff --git a/PluralKit.Bot/Services/ErrorMessageService.cs b/PluralKit.Bot/Services/ErrorMessageService.cs index 1a22e04b..bd4a6581 100644 --- a/PluralKit.Bot/Services/ErrorMessageService.cs +++ b/PluralKit.Bot/Services/ErrorMessageService.cs @@ -4,7 +4,8 @@ using System.Threading.Tasks; using App.Metrics; -using DSharpPlus.Entities; +using Myriad.Builders; +using Myriad.Rest; using NodaTime; @@ -19,54 +20,61 @@ namespace PluralKit.Bot private readonly IMetrics _metrics; private readonly ILogger _logger; + private readonly DiscordApiClient _rest; - public ErrorMessageService(IMetrics metrics, ILogger logger) + public ErrorMessageService(IMetrics metrics, ILogger logger, DiscordApiClient rest) { _metrics = metrics; _logger = logger; + _rest = rest; } - public async Task SendErrorMessage(DiscordChannel channel, string errorId) + public async Task SendErrorMessage(ulong channelId, string errorId) { var now = SystemClock.Instance.GetCurrentInstant(); - if (!ShouldSendErrorMessage(channel, now)) + if (!ShouldSendErrorMessage(channelId, now)) { - _logger.Warning("Rate limited sending error message to {ChannelId} with error code {ErrorId}", channel.Id, errorId); + _logger.Warning("Rate limited sending error message to {ChannelId} with error code {ErrorId}", channelId, errorId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "throttled"); return; } - var embed = new DiscordEmbedBuilder() - .WithColor(new DiscordColor(0xE74C3C)) - .WithTitle("Internal error occurred") - .WithDescription("For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") - .WithFooter(errorId) - .WithTimestamp(now.ToDateTimeOffset()); + var embed = new EmbedBuilder() + .Color(0xE74C3C) + .Title("Internal error occurred") + .Description("For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") + .Footer(new(errorId)) + .Timestamp(now.ToDateTimeOffset().ToString("O")); try { - await channel.SendMessageAsync($"> **Error code:** `{errorId}`", embed: embed.Build()); - _logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channel.Id, errorId); + await _rest.CreateMessage(channelId, new() + { + Content = $"> **Error code:** `{errorId}`", + Embed = embed.Build() + }); + + _logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channelId, errorId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "sent"); } catch (Exception e) { - _logger.Error(e, "Error sending error message to {ChannelId}", channel.Id); + _logger.Error(e, "Error sending error message to {ChannelId}", channelId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "failed"); throw; } } - private bool ShouldSendErrorMessage(DiscordChannel channel, Instant now) + private bool ShouldSendErrorMessage(ulong channelId, Instant now) { - if (_lastErrorInChannel.TryGetValue(channel.Id, out var lastErrorTime)) + if (_lastErrorInChannel.TryGetValue(channelId, out var lastErrorTime)) { var interval = now - lastErrorTime; if (interval < MinErrorInterval) return false; } - _lastErrorInChannel[channel.Id] = now; + _lastErrorInChannel[channelId] = now; return true; } } diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index c4bad54a..d08e42a7 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dapper; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Rest; using Myriad.Types; @@ -18,14 +19,16 @@ namespace PluralKit.Bot { private readonly ILogger _logger; private readonly IDiscordCache _cache; private readonly DiscordApiClient _rest; + private readonly Bot _bot; - public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) + public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest, Bot bot) { _embed = embed; _db = db; _repo = repo; _cache = cache; _rest = rest; + _bot = bot; _logger = logger.ForContext(); } @@ -36,25 +39,28 @@ namespace PluralKit.Bot { // Find log channel and check if valid var logChannel = await FindLogChannel(trigger.GuildId!.Value, ctx.LogChannel.Value); if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return; + + var triggerChannel = _cache.GetChannel(trigger.ChannelId); // Check bot permissions - // if (!logChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) - // { - // _logger.Information( - // "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", - // ctx.LogChannel.Value, trigger.GuildId!.Value, trigger.Channel.BotPermissions()); - // return; - // } - // + var perms = _bot.PermissionsIn(logChannel.Id); + if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks)) + { + _logger.Information( + "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", + ctx.LogChannel.Value, trigger.GuildId!.Value, perms); + return; + } + // Send embed! // TODO: fix? - // await using var conn = await _db.Obtain(); - // var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), - // await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, - // trigger.Channel); - // var url = $"https://discord.com/channels/{trigger.Channel.GuildId}/{trigger.ChannelId}/{hookMessage}"; - // await logChannel.SendMessageFixedAsync(content: url, embed: embed); + await using var conn = await _db.Obtain(); + var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), + await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, + triggerChannel); + var url = $"https://discord.com/channels/{trigger.GuildId}/{trigger.ChannelId}/{hookMessage}"; + await _rest.CreateMessage(logChannel.Id, new() {Content = url, Embed = embed}); } private async Task FindLogChannel(ulong guildId, ulong channelId) From 47b16dc51baaa9fc716b2e962e5a046a898bf563 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 24 Dec 2020 14:52:44 +0100 Subject: [PATCH 004/608] Port more things! --- Myriad/Cache/DiscordCacheExtensions.cs | 22 ++- Myriad/Extensions/UserExtensions.cs | 4 +- Myriad/Gateway/Cluster.cs | 1 + Myriad/Gateway/Shard.cs | 2 + Myriad/Rest/DiscordApiClient.cs | 7 +- Myriad/Rest/Ratelimit/Bucket.cs | 6 +- .../Rest/Types/Requests/MessageEditRequest.cs | 18 ++- Myriad/Rest/Types/Requests/MessageRequest.cs | 2 +- Myriad/Serialization/OptionalConverter.cs | 43 ++++++ Myriad/Utils/Optional.cs | 32 +++++ PluralKit.Bot/CommandSystem/Context.cs | 21 +-- .../ContextEntityArgumentsExt.cs | 9 +- PluralKit.Bot/Commands/Autoproxy.cs | 31 +++-- .../Commands/Avatars/ContextAvatarExt.cs | 8 +- PluralKit.Bot/Commands/Groups.cs | 8 +- .../Commands/Lists/ContextListExt.cs | 14 +- PluralKit.Bot/Commands/MemberAvatar.cs | 4 +- PluralKit.Bot/Commands/Misc.cs | 124 ++++++++++------- PluralKit.Bot/Commands/ServerConfig.cs | 4 +- PluralKit.Bot/Commands/SystemEdit.cs | 4 +- PluralKit.Bot/Commands/SystemFront.cs | 4 +- PluralKit.Bot/Commands/SystemLink.cs | 7 +- PluralKit.Bot/Modules.cs | 2 + PluralKit.Bot/Utils/ContextUtils.cs | 127 +++++++++--------- PluralKit.Bot/Utils/DiscordUtils.cs | 12 +- PluralKit.Core/Utils/HandlerQueue.cs | 2 +- 26 files changed, 332 insertions(+), 186 deletions(-) create mode 100644 Myriad/Serialization/OptionalConverter.cs create mode 100644 Myriad/Utils/Optional.cs diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index ff9a251f..b4165987 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Types; namespace Myriad.Cache { @@ -29,7 +31,7 @@ namespace Myriad.Cache case GuildRoleDeleteEvent grd: return cache.RemoveRole(grd.GuildId, grd.RoleId); case MessageCreateEvent mc: - return cache.SaveUser(mc.Author); + return cache.SaveMessageCreate(mc); } return default; @@ -46,5 +48,23 @@ namespace Myriad.Cache foreach (var member in guildCreate.Members) await cache.SaveUser(member.User); } + + private static async ValueTask SaveMessageCreate(this IDiscordCache cache, MessageCreateEvent evt) + { + await cache.SaveUser(evt.Author); + foreach (var mention in evt.Mentions) + await cache.SaveUser(mention); + } + + public static async ValueTask GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest, ulong userId) + { + if (cache.TryGetUser(userId, out var cacheUser)) + return cacheUser; + + var restUser = await rest.GetUser(userId); + if (restUser != null) + await cache.SaveUser(restUser); + return restUser; + } } } \ No newline at end of file diff --git a/Myriad/Extensions/UserExtensions.cs b/Myriad/Extensions/UserExtensions.cs index e4b1e5ef..81d16706 100644 --- a/Myriad/Extensions/UserExtensions.cs +++ b/Myriad/Extensions/UserExtensions.cs @@ -6,7 +6,7 @@ namespace Myriad.Extensions { public static string Mention(this User user) => $"<@{user.Id}>"; - public static string AvatarUrl(this User user) => - $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png"; + public static string AvatarUrl(this User user, string? format = "png", int? size = 128) => + $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.{format}?size={size}"; } } \ No newline at end of file diff --git a/Myriad/Gateway/Cluster.cs b/Myriad/Gateway/Cluster.cs index 304cfb8a..63e8a2cc 100644 --- a/Myriad/Gateway/Cluster.cs +++ b/Myriad/Gateway/Cluster.cs @@ -27,6 +27,7 @@ namespace Myriad.Gateway public IReadOnlyDictionary Shards => _shards; public ClusterSessionState SessionState => GetClusterState(); public User? User => _shards.Values.Select(s => s.User).FirstOrDefault(s => s != null); + public ApplicationPartial? Application => _shards.Values.Select(s => s.Application).FirstOrDefault(s => s != null); private ClusterSessionState GetClusterState() { diff --git a/Myriad/Gateway/Shard.cs b/Myriad/Gateway/Shard.cs index a4b65592..cb00fb81 100644 --- a/Myriad/Gateway/Shard.cs +++ b/Myriad/Gateway/Shard.cs @@ -32,6 +32,7 @@ namespace Myriad.Gateway public ShardState State { get; private set; } public TimeSpan? Latency { get; private set; } public User? User { get; private set; } + public ApplicationPartial? Application { get; private set; } public Func? OnEventReceived { get; set; } @@ -258,6 +259,7 @@ namespace Myriad.Gateway ShardInfo = ready.Shard; SessionInfo = SessionInfo with { Session = ready.SessionId }; User = ready.User; + Application = ready.Application; State = ShardState.Open; return Task.CompletedTask; diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 71813481..953ce2d0 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -33,8 +33,11 @@ namespace Myriad.Rest public Task GetMessage(ulong channelId, ulong messageId) => _client.Get($"/channels/{channelId}/messages/{messageId}", ("GetMessage", channelId)); - public Task GetGuild(ulong id) => - _client.Get($"/guilds/{id}", ("GetGuild", id)); + public Task GetGuild(ulong id) => + _client.Get($"/guilds/{id}", ("GetGuild", id)); + + public Task GetGuildChannels(ulong id) => + _client.Get($"/guilds/{id}/channels", ("GetGuildChannels", id))!; public Task GetUser(ulong id) => _client.Get($"/users/{id}", ("GetUser", default)); diff --git a/Myriad/Rest/Ratelimit/Bucket.cs b/Myriad/Rest/Ratelimit/Bucket.cs index 31e7ea24..6210ce89 100644 --- a/Myriad/Rest/Ratelimit/Bucket.cs +++ b/Myriad/Rest/Ratelimit/Bucket.cs @@ -77,8 +77,8 @@ namespace Myriad.Rest.Ratelimit var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time if (headerNextReset > _nextReset) { - _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server", - Key, Major, _nextReset); + _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter})", + Key, Major, headerNextReset, headers.ResetAfter.Value); _nextReset = headerNextReset; _resetTimeValid = true; @@ -101,7 +101,7 @@ namespace Myriad.Rest.Ratelimit _semaphore.Wait(); // If we're past the reset time *and* we haven't reset already, do that - var timeSinceReset = _nextReset - now; + var timeSinceReset = now - _nextReset; var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero; if (shouldReset) { diff --git a/Myriad/Rest/Types/Requests/MessageEditRequest.cs b/Myriad/Rest/Types/Requests/MessageEditRequest.cs index 1fe03193..bf217c83 100644 --- a/Myriad/Rest/Types/Requests/MessageEditRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageEditRequest.cs @@ -1,10 +1,22 @@ -using Myriad.Types; +using System.Text.Json.Serialization; + +using Myriad.Types; +using Myriad.Utils; namespace Myriad.Rest.Types.Requests { public record MessageEditRequest { - public string? Content { get; set; } - public Embed? Embed { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Content { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Embed { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Flags { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional AllowedMentions { get; init; } } } \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs index 72f018e5..992eb08e 100644 --- a/Myriad/Rest/Types/Requests/MessageRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -7,7 +7,7 @@ namespace Myriad.Rest.Types.Requests public string? Content { get; set; } public object? Nonce { get; set; } public bool Tts { get; set; } - public AllowedMentions AllowedMentions { get; set; } + public AllowedMentions? AllowedMentions { get; set; } public Embed? Embed { get; set; } } } \ No newline at end of file diff --git a/Myriad/Serialization/OptionalConverter.cs b/Myriad/Serialization/OptionalConverter.cs new file mode 100644 index 00000000..c45d1caa --- /dev/null +++ b/Myriad/Serialization/OptionalConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Myriad.Utils; + +namespace Myriad.Serialization +{ + public class OptionalConverter: JsonConverter + { + public override IOptional? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var innerType = typeToConvert.GetGenericArguments()[0]; + var inner = JsonSerializer.Deserialize(ref reader, innerType, options); + + // TODO: rewrite to JsonConverterFactory to cut down on reflection + return (IOptional?) Activator.CreateInstance( + typeof(Optional<>).MakeGenericType(innerType), + BindingFlags.Instance | BindingFlags.Public, + null, + new[] {inner}, + null); + } + + public override void Write(Utf8JsonWriter writer, IOptional value, JsonSerializerOptions options) + { + var innerType = value.GetType().GetGenericArguments()[0]; + JsonSerializer.Serialize(writer, value.GetValue(), innerType, options); + } + + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + return false; + + if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) + return false; + + return true; + } + } +} \ No newline at end of file diff --git a/Myriad/Utils/Optional.cs b/Myriad/Utils/Optional.cs new file mode 100644 index 00000000..7b1e4139 --- /dev/null +++ b/Myriad/Utils/Optional.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +using Myriad.Serialization; + +namespace Myriad.Utils +{ + public interface IOptional + { + bool HasValue { get; } + object? GetValue(); + } + + [JsonConverter(typeof(OptionalConverter))] + public readonly struct Optional: IOptional + { + public Optional(T value) + { + HasValue = true; + Value = value; + } + + public bool HasValue { get; } + public object? GetValue() => Value; + + public T Value { get; } + + public static implicit operator Optional(T value) => new(value); + + public static Optional Some(T value) => new(value); + public static Optional None() => default; + } +} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 1402705e..037d8726 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -8,11 +8,11 @@ using Autofac; using DSharpPlus; using DSharpPlus.Entities; -using DSharpPlus.Net; using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Rest.Types; using Myriad.Rest.Types.Requests; using Myriad.Types; @@ -34,7 +34,7 @@ namespace PluralKit.Bot private readonly Guild? _guild; private readonly Channel _channel; private readonly DiscordMessage _message = null; - private readonly Message _messageNew; + private readonly MessageCreateEvent _messageNew; private readonly Parameters _parameters; private readonly MessageContext _messageContext; private readonly PermissionSet _botPermissions; @@ -79,6 +79,7 @@ namespace PluralKit.Bot public DiscordChannel Channel => _message.Channel; public Channel ChannelNew => _channel; public User AuthorNew => _messageNew.Author; + public GuildMemberPartial MemberNew => _messageNew.Member; public DiscordMessage Message => _message; public Message MessageNew => _messageNew; public DiscordGuild Guild => _message.Channel.Guild; @@ -91,6 +92,7 @@ namespace PluralKit.Bot public PermissionSet UserPermissions => _userPermissions; public DiscordRestClient Rest => _rest; + public DiscordApiClient RestNew => _newRest; public PKSystem System => _senderSystem; @@ -102,16 +104,16 @@ namespace PluralKit.Bot public Task Reply(string text, DiscordEmbed embed, IEnumerable? mentions = null) { - return Reply(text, (DiscordEmbed) null, mentions); + throw new NotImplementedException(); } public Task Reply(DiscordEmbed embed, IEnumerable? mentions = null) { - return Reply(null, (DiscordEmbed) null, mentions); + throw new NotImplementedException(); } - public async Task Reply(string text = null, Embed embed = null, IEnumerable? mentions = null) + public async Task Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null) { if (!BotPermissions.HasFlag(PermissionSet.SendMessages)) // Will be "swallowed" during the error handler anyway, this message is never shown. @@ -123,10 +125,10 @@ namespace PluralKit.Bot var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest { Content = text, - Embed = embed + Embed = embed, + AllowedMentions = mentions }); - // TODO: embeds/mentions - // var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); + // TODO: mentions should default to empty and not null? if (embed != null) { @@ -135,8 +137,7 @@ namespace PluralKit.Bot await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id); } - // return msg; - return null; + return msg; } public async Task Execute(Command commandDef, Func handler) diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 97e2efa4..cb915c99 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -1,8 +1,6 @@ using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; - +using Myriad.Cache; using Myriad.Types; using PluralKit.Bot.Utils; @@ -12,11 +10,12 @@ namespace PluralKit.Bot { public static class ContextEntityArgumentsExt { - public static async Task MatchUser(this Context ctx) + public static async Task MatchUser(this Context ctx) { var text = ctx.PeekArgument(); if (text.TryParseMention(out var id)) - return await ctx.Shard.GetUser(id); + return await ctx.Cache.GetOrFetchUser(ctx.RestNew, id); + return null; } diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 94e94dd2..b3efde4e 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -1,10 +1,11 @@ using System; using System.Threading.Tasks; -using DSharpPlus.Entities; - using Humanizer; +using Myriad.Builders; +using Myriad.Types; + using NodaTime; using PluralKit.Core; @@ -84,10 +85,11 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server."); } - private async Task CreateAutoproxyStatusEmbed(Context ctx) + private async Task CreateAutoproxyStatusEmbed(Context ctx) { var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy ** - Autoproxies as a specific member"; - var eb = new DiscordEmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); + var eb = new EmbedBuilder() + .Title($"Current autoproxy status (for {ctx.GuildNew.Name.EscapeMarkdown()})"); var fronters = ctx.MessageContext.LastSwitchMembers; var relevantMember = ctx.MessageContext.AutoproxyMode switch @@ -98,35 +100,36 @@ namespace PluralKit.Bot }; switch (ctx.MessageContext.AutoproxyMode) { - case AutoproxyMode.Off: eb.WithDescription($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); + case AutoproxyMode.Off: + eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); break; case AutoproxyMode.Front: { if (fronters.Length == 0) - eb.WithDescription("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch."); + eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch."); else { if (relevantMember == null) throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately."); - eb.WithDescription($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`."); + eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`."); } break; } // AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up case AutoproxyMode.Member when relevantMember != null: { - eb.WithDescription($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`."); + eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`."); break; } case AutoproxyMode.Latch: - eb.WithDescription("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`."); + eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`."); break; default: throw new ArgumentOutOfRangeException(); } if (!ctx.MessageContext.AllowAutoproxy) - eb.AddField("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`."); + eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.")); return eb.Build(); } @@ -178,7 +181,7 @@ namespace PluralKit.Bot else { var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; - await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>.", mentions: new IMention[]{}); + await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>."); } } @@ -187,18 +190,18 @@ namespace PluralKit.Bot var statusString = allow ? "enabled" : "disabled"; if (ctx.MessageContext.AllowAutoproxy == allow) { - await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{}); + await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); return; } var patch = new AccountPatch { AllowAutoproxy = allow }; await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch)); - await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{}); + await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); } private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) { var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember}; - return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch)); + return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs index 045f52e5..43207639 100644 --- a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs +++ b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; +using Myriad.Extensions; +using Myriad.Types; namespace PluralKit.Bot { @@ -22,7 +22,7 @@ namespace PluralKit.Bot // If we have a user @mention/ID, use their avatar if (await ctx.MatchUser() is { } user) { - var url = user.GetAvatarUrl(ImageFormat.Png, 256); + var url = user.AvatarUrl("png", 256); return new ParsedImage {Url = url, Source = AvatarSource.User, SourceUser = user}; } @@ -64,7 +64,7 @@ namespace PluralKit.Bot { public string Url; public AvatarSource Source; - public DiscordUser? SourceUser; + public User? SourceUser; } public enum AvatarSource diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index f3f63fa5..36ee74fb 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -10,6 +10,8 @@ using DSharpPlus.Entities; using Humanizer; +using Myriad.Builders; + using PluralKit.Core; namespace PluralKit.Bot @@ -194,7 +196,7 @@ namespace PluralKit.Bot // The attachment's already right there, no need to preview it. var hasEmbed = img.Source != AvatarSource.Attachment; await (hasEmbed - ? ctx.Reply(msg, embed: new DiscordEmbedBuilder().WithImageUrl(img.Url).Build()) + ? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build()) : ctx.Reply(msg)); } @@ -265,7 +267,7 @@ namespace PluralKit.Bot var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`"; await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, Renderer); - Task Renderer(DiscordEmbedBuilder eb, IEnumerable page) + Task Renderer(EmbedBuilder eb, IEnumerable page) { eb.WithSimpleLineContent(page.Select(g => { @@ -274,7 +276,7 @@ namespace PluralKit.Bot else return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({"member".ToQuantity(g.MemberCount)})"; })); - eb.WithFooter($"{groups.Count} total."); + eb.Footer(new($"{groups.Count} total.")); return Task.CompletedTask; } } diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 5c9da71c..acca3b5f 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -3,10 +3,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DSharpPlus.Entities; - using Humanizer; +using Myriad.Builders; + using NodaTime; using PluralKit.Core; @@ -90,10 +90,10 @@ namespace PluralKit.Bot await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer); // Base renderer, dispatches based on type - Task Renderer(DiscordEmbedBuilder eb, IEnumerable page) + Task Renderer(EmbedBuilder eb, IEnumerable page) { // Add a global footer with the filter/sort string + result count - eb.WithFooter($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."); + eb.Footer(new($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}.")); // Then call the specific renderers if (opts.Type == ListType.Short) @@ -104,7 +104,7 @@ namespace PluralKit.Bot return Task.CompletedTask; } - void ShortRenderer(DiscordEmbedBuilder eb, IEnumerable page) + void ShortRenderer(EmbedBuilder eb, IEnumerable page) { // We may end up over the description character limit // so run it through a helper that "makes it work" :) @@ -122,7 +122,7 @@ namespace PluralKit.Bot })); } - void LongRenderer(DiscordEmbedBuilder eb, IEnumerable page) + void LongRenderer(EmbedBuilder eb, IEnumerable page) { var zone = ctx.System?.Zone ?? DateTimeZone.Utc; foreach (var m in page) @@ -162,7 +162,7 @@ namespace PluralKit.Bot if (m.MemberVisibility == PrivacyLevel.Private) profile.Append("\n*(this member is hidden)*"); - eb.AddField(m.NameFor(ctx), profile.ToString().Truncate(1024)); + eb.Field(new(m.NameFor(ctx), profile.ToString().Truncate(1024))); } } } diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 06d5d9c7..65ad0b56 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using DSharpPlus.Entities; +using Myriad.Builders; + using PluralKit.Core; namespace PluralKit.Bot @@ -135,7 +137,7 @@ namespace PluralKit.Bot // The attachment's already right there, no need to preview it. var hasEmbed = avatar.Source != AvatarSource.Attachment; return hasEmbed - ? ctx.Reply(msg, embed: new DiscordEmbedBuilder().WithImageUrl(avatar.Url).Build()) + ? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build()) : ctx.Reply(msg); } diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index fb814f84..04b465ee 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -13,7 +13,16 @@ using Humanizer; using NodaTime; using PluralKit.Core; -using DSharpPlus.Entities; + +using Myriad.Builders; +using Myriad.Cache; +using Myriad.Extensions; +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Rest.Types.Requests; +using Myriad.Types; + +using Permissions = DSharpPlus.Permissions; namespace PluralKit.Bot { public class Misc @@ -25,8 +34,12 @@ namespace PluralKit.Bot { private readonly EmbedService _embeds; private readonly IDatabase _db; private readonly ModelRepository _repo; + private readonly IDiscordCache _cache; + private readonly DiscordApiClient _rest; + private readonly Cluster _cluster; + private readonly Bot _bot; - public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, IDatabase db) + public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, IDatabase db, IDiscordCache cache, DiscordApiClient rest, Bot bot, Cluster cluster) { _botConfig = botConfig; _metrics = metrics; @@ -35,20 +48,26 @@ namespace PluralKit.Bot { _embeds = embeds; _repo = repo; _db = db; + _cache = cache; + _rest = rest; + _bot = bot; + _cluster = cluster; } public async Task Invite(Context ctx) { - var clientId = _botConfig.ClientId ?? ctx.Client.CurrentApplication.Id; - var permissions = new Permissions() - .Grant(Permissions.AddReactions) - .Grant(Permissions.AttachFiles) - .Grant(Permissions.EmbedLinks) - .Grant(Permissions.ManageMessages) - .Grant(Permissions.ManageWebhooks) - .Grant(Permissions.ReadMessageHistory) - .Grant(Permissions.SendMessages); - var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(long)permissions}"; + var clientId = _botConfig.ClientId ?? _cluster.Application?.Id; + + var permissions = + PermissionSet.AddReactions | + PermissionSet.AttachFiles | + PermissionSet.EmbedLinks | + PermissionSet.ManageMessages | + PermissionSet.ManageWebhooks | + PermissionSet.ReadMessageHistory | + PermissionSet.SendMessages; + + var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(ulong)permissions}"; await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>"); } @@ -69,6 +88,7 @@ namespace PluralKit.Bot { var totalSwitches = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.SwitchCount.Name)?.Value ?? 0; var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name)?.Value ?? 0; + // TODO: shard stuff var shardId = ctx.Shard.ShardId; var shardTotal = ctx.Client.ShardClients.Count; var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count(); @@ -79,30 +99,31 @@ namespace PluralKit.Bot { var shardUptime = SystemClock.Instance.GetCurrentInstant() - shardInfo.LastConnectionTime; - var embed = new DiscordEmbedBuilder(); - if (messagesReceived != null) embed.AddField("Messages processed",$"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true); - if (messagesProxied != null) embed.AddField("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true); - if (commandsRun != null) embed.AddField("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true); + var embed = new EmbedBuilder(); + if (messagesReceived != null) embed.Field(new("Messages processed",$"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true)); + if (messagesProxied != null) embed.Field(new("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true)); + if (commandsRun != null) embed.Field(new("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true)); embed - .AddField("Current shard", $"Shard #{shardId} (of {shardTotal} total, {shardUpTotal} are up)", true) - .AddField("Shard uptime", $"{shardUptime.FormatDuration()} ({shardInfo.DisconnectionCount} disconnections)", true) - .AddField("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true) - .AddField("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true) - .AddField("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true) - .AddField("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages"); - await msg.ModifyAsync("", embed.Build()); + .Field(new("Current shard", $"Shard #{shardId} (of {shardTotal} total, {shardUpTotal} are up)", true)) + .Field(new("Shard uptime", $"{shardUptime.FormatDuration()} ({shardInfo.DisconnectionCount} disconnections)", true)) + .Field(new("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true)) + .Field(new("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true)) + .Field(new("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true)) + .Field(new("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages")); + await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, + new MessageEditRequest {Content = "", Embed = embed.Build()}); } public async Task PermCheckGuild(Context ctx) { - DiscordGuild guild; - DiscordMember senderGuildUser = null; + Guild guild; + GuildMemberPartial senderGuildUser = null; - if (ctx.Guild != null && !ctx.HasNext()) + if (ctx.GuildNew != null && !ctx.HasNext()) { - guild = ctx.Guild; - senderGuildUser = (DiscordMember)ctx.Author; + guild = ctx.GuildNew; + senderGuildUser = ctx.MemberNew; } else { @@ -110,31 +131,33 @@ namespace PluralKit.Bot { if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID."); - guild = ctx.Client.GetGuild(guildId); - if (guild != null) senderGuildUser = await guild.GetMember(ctx.Author.Id); - if (guild == null || senderGuildUser == null) throw Errors.GuildNotFound(guildId); + guild = await _rest.GetGuild(guildId); + if (guild != null) + senderGuildUser = await _rest.GetGuildMember(guildId, ctx.AuthorNew.Id); + if (guild == null || senderGuildUser == null) + throw Errors.GuildNotFound(guildId); } var requiredPermissions = new [] { - Permissions.AccessChannels, - Permissions.SendMessages, - Permissions.AddReactions, - Permissions.AttachFiles, - Permissions.EmbedLinks, - Permissions.ManageMessages, - Permissions.ManageWebhooks + PermissionSet.ViewChannel, + PermissionSet.SendMessages, + PermissionSet.AddReactions, + PermissionSet.AttachFiles, + PermissionSet.EmbedLinks, + PermissionSet.ManageMessages, + PermissionSet.ManageWebhooks }; // Loop through every channel and group them by sets of permissions missing - var permissionsMissing = new Dictionary>(); + var permissionsMissing = new Dictionary>(); var hiddenChannels = 0; - foreach (var channel in await guild.GetChannelsAsync()) + foreach (var channel in await _rest.GetGuildChannels(guild.Id)) { - var botPermissions = channel.BotPermissions(); + var botPermissions = _bot.PermissionsIn(channel.Id); + var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.AuthorNew.Id, senderGuildUser.Roles); - var userPermissions = senderGuildUser.PermissionsIn(channel); - if ((userPermissions & Permissions.AccessChannels) == 0) + if ((userPermissions & PermissionSet.ViewChannel) == 0) { // If the user can't see this channel, don't calculate permissions for it // (to prevent info-leaking, mostly) @@ -154,18 +177,18 @@ namespace PluralKit.Bot { // This means we can check if the dict is empty to see if all channels are proxyable if (missingPermissionField != 0) { - permissionsMissing.TryAdd(missingPermissionField, new List()); + permissionsMissing.TryAdd(missingPermissionField, new List()); permissionsMissing[missingPermissionField].Add(channel); } } // Generate the output embed - var eb = new DiscordEmbedBuilder() - .WithTitle($"Permission check for **{guild.Name}**"); + var eb = new EmbedBuilder() + .Title($"Permission check for **{guild.Name}**"); if (permissionsMissing.Count == 0) { - eb.WithDescription($"No errors found, all channels proxyable :)").WithColor(DiscordUtils.Green); + eb.Description($"No errors found, all channels proxyable :)").Color((uint?) DiscordUtils.Green.Value); } else { @@ -173,18 +196,19 @@ namespace PluralKit.Bot { { // Each missing permission field can have multiple missing channels // so we extract them all and generate a comma-separated list + // TODO: port ToPermissionString? var missingPermissionNames = ((Permissions)missingPermissionField).ToPermissionString(); var channelsList = string.Join("\n", channels .OrderBy(c => c.Position) .Select(c => $"#{c.Name}")); - eb.AddField($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)); - eb.WithColor(DiscordUtils.Red); + eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000))); + eb.Color((uint?) DiscordUtils.Red.Value); } } if (hiddenChannels > 0) - eb.WithFooter($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them."); + eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them.")); // Send! :) await ctx.Reply(embed: eb.Build()); diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 507a2ceb..2660df04 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -123,7 +123,7 @@ namespace PluralKit.Bot { if (lastCategory != channel!.ParentId && fieldValue.Length > 0) { - eb.AddField(CategoryName(lastCategory), fieldValue.ToString()); + eb.Field(new(CategoryName(lastCategory), fieldValue.ToString())); fieldValue.Clear(); } else fieldValue.Append("\n"); @@ -132,7 +132,7 @@ namespace PluralKit.Bot lastCategory = channel.ParentId; } - eb.AddField(CategoryName(lastCategory), fieldValue.ToString()); + eb.Field(new(CategoryName(lastCategory), fieldValue.ToString())); return Task.CompletedTask; }); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index ab95b105..2c96babb 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using DSharpPlus.Entities; +using Myriad.Builders; + using NodaTime; using NodaTime.Text; using NodaTime.TimeZones; @@ -150,7 +152,7 @@ namespace PluralKit.Bot // The attachment's already right there, no need to preview it. var hasEmbed = img.Source != AvatarSource.Attachment; await (hasEmbed - ? ctx.Reply(msg, embed: new DiscordEmbedBuilder().WithImageUrl(img.Url).Build()) + ? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build()) : ctx.Reply(msg)); } diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 1f22b88a..47a01d03 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -98,9 +98,11 @@ namespace PluralKit.Bot stringToAdd = $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago)\n"; } + try // Unfortunately the only way to test DiscordEmbedBuilder.Description max length is this { - builder.Description += stringToAdd; + // TODO: what is this?? + // builder.Description += stringToAdd; } catch (ArgumentException) { diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 70c829dd..0ebc0d83 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -1,7 +1,8 @@ using System.Linq; using System.Threading.Tasks; -using DSharpPlus.Entities; +using Myriad.Extensions; +using Myriad.Rest.Types; using PluralKit.Core; @@ -33,8 +34,8 @@ namespace PluralKit.Bot if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount); - var msg = $"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message."; - var mentions = new IMention[] { new UserMention(account) }; + var msg = $"{account.Mention()}, please confirm the link by clicking the {Emojis.Success} reaction on this message."; + var mentions = new AllowedMentions {Users = new[] {account.Id}}; if (!await ctx.PromptYesNo(msg, user: account, mentions: mentions, matchFlag: false)) throw Errors.MemberLinkCancelled; await _repo.AddAccount(conn, ctx.System.Id, account.Id); await ctx.Reply($"{Emojis.Success} Account linked to system."); diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 8dfd189c..7229581d 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -83,7 +83,9 @@ namespace PluralKit.Bot // Event handler queue builder.RegisterType>().AsSelf().SingleInstance(); + builder.RegisterType>().AsSelf().SingleInstance(); builder.RegisterType>().AsSelf().SingleInstance(); + builder.RegisterType>().AsSelf().SingleInstance(); // Bot services builder.RegisterType().AsSelf().SingleInstance(); diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 67bb369d..c245d549 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -6,19 +6,17 @@ using System.Threading.Tasks; using Autofac; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; - +using Myriad.Builders; +using Myriad.Gateway; +using Myriad.Rest.Exceptions; +using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; using Myriad.Types; using NodaTime; using PluralKit.Core; -using Permissions = DSharpPlus.Permissions; - namespace PluralKit.Bot { public static class ContextUtils { public static async Task ConfirmClear(this Context ctx, string toClear) @@ -27,52 +25,45 @@ namespace PluralKit.Bot { else return true; } - public static async Task PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable mentions = null, bool matchFlag = true) + public static async Task PromptYesNo(this Context ctx, string msgString, User user = null, Duration? timeout = null, AllowedMentions mentions = null, bool matchFlag = true) { - DiscordMessage message; + Message message; if (matchFlag && ctx.MatchFlag("y", "yes")) return true; else message = await ctx.Reply(msgString, mentions: mentions); var cts = new CancellationTokenSource(); - if (user == null) user = ctx.Author; + if (user == null) user = ctx.AuthorNew; if (timeout == null) timeout = Duration.FromMinutes(5); // "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses - var _ = message.CreateReactionsBulk(new[] {Emojis.Success, Emojis.Error}); + await ctx.RestNew.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error}); - bool ReactionPredicate(MessageReactionAddEventArgs e) + bool ReactionPredicate(MessageReactionAddEvent e) { - if (e.Channel.Id != message.ChannelId || e.Message.Id != message.Id) return false; - if (e.User.Id != user.Id) return false; + if (e.ChannelId != message.ChannelId || e.MessageId != message.Id) return false; + if (e.UserId != user.Id) return false; return true; } - bool MessagePredicate(MessageCreateEventArgs e) + bool MessagePredicate(MessageCreateEvent e) { - if (e.Channel.Id != message.ChannelId) return false; + if (e.ChannelId != message.ChannelId) return false; if (e.Author.Id != user.Id) return false; var strings = new [] {"y", "yes", "n", "no"}; - foreach (var str in strings) - if (e.Message.Content.Equals(str, StringComparison.InvariantCultureIgnoreCase)) - return true; - - return false; + return strings.Any(str => string.Equals(e.Content, str, StringComparison.InvariantCultureIgnoreCase)); } - var messageTask = ctx.Services.Resolve>().WaitFor(MessagePredicate, timeout, cts.Token); - var reactionTask = ctx.Services.Resolve>().WaitFor(ReactionPredicate, timeout, cts.Token); + var messageTask = ctx.Services.Resolve>().WaitFor(MessagePredicate, timeout, cts.Token); + var reactionTask = ctx.Services.Resolve>().WaitFor(ReactionPredicate, timeout, cts.Token); var theTask = await Task.WhenAny(messageTask, reactionTask); cts.Cancel(); if (theTask == messageTask) { - var responseMsg = (await messageTask).Message; + var responseMsg = (await messageTask); var positives = new[] {"y", "yes"}; - foreach (var p in positives) - if (responseMsg.Content.Equals(p, StringComparison.InvariantCultureIgnoreCase)) - return true; - return false; + return positives.Any(p => string.Equals(responseMsg.Content, p, StringComparison.InvariantCultureIgnoreCase)); } if (theTask == reactionTask) @@ -81,50 +72,45 @@ namespace PluralKit.Bot { return false; } - public static async Task AwaitReaction(this Context ctx, DiscordMessage message, DiscordUser user = null, Func predicate = null, TimeSpan? timeout = null) { - var tcs = new TaskCompletionSource(); - Task Inner(DiscordClient _, MessageReactionAddEventArgs args) { - if (message.Id != args.Message.Id) return Task.CompletedTask; // Ignore reactions for different messages - if (user != null && user.Id != args.User.Id) return Task.CompletedTask; // Ignore messages from other users if a user was defined - if (predicate != null && !predicate.Invoke(args)) return Task.CompletedTask; // Check predicate - tcs.SetResult(args); - return Task.CompletedTask; - } - - ctx.Shard.MessageReactionAdded += Inner; - try { - return await tcs.Task.TimeoutAfter(timeout); - } finally { - ctx.Shard.MessageReactionAdded -= Inner; + public static async Task AwaitReaction(this Context ctx, Message message, User user = null, Func predicate = null, Duration? timeout = null) + { + bool ReactionPredicate(MessageReactionAddEvent evt) + { + if (message.Id != evt.MessageId) return false; // Ignore reactions for different messages + if (user != null && user.Id != evt.UserId) return false; // Ignore messages from other users if a user was defined + if (predicate != null && !predicate.Invoke(evt)) return false; // Check predicate + return true; } + + return await ctx.Services.Resolve>().WaitFor(ReactionPredicate, timeout); } public static async Task ConfirmWithReply(this Context ctx, string expectedReply) { - bool Predicate(MessageCreateEventArgs e) => - e.Author == ctx.Author && e.Channel.Id == ctx.Channel.Id; + bool Predicate(MessageCreateEvent e) => + e.Author.Id == ctx.AuthorNew.Id && e.ChannelId == ctx.Channel.Id; - var msg = await ctx.Services.Resolve>() + var msg = await ctx.Services.Resolve>() .WaitFor(Predicate, Duration.FromMinutes(1)); - return string.Equals(msg.Message.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase); + return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase); } - public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, Func, Task> renderer) { + public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, Func, Task> renderer) { // TODO: make this generic enough we can use it in Choose below var buffer = new List(); await using var enumerator = items.GetAsyncEnumerator(); var pageCount = (int) Math.Ceiling(totalCount / (double) itemsPerPage); - async Task MakeEmbedForPage(int page) + async Task MakeEmbedForPage(int page) { var bufferedItemsNeeded = (page + 1) * itemsPerPage; while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync()) buffer.Add(enumerator.Current); - var eb = new DiscordEmbedBuilder(); - eb.Title = pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title; + var eb = new EmbedBuilder(); + eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title); await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage)); return eb.Build(); } @@ -134,13 +120,13 @@ namespace PluralKit.Bot { var msg = await ctx.Reply(embed: await MakeEmbedForPage(0)); if (pageCount <= 1) return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error }; - - var _ = msg.CreateReactionsBulk(botEmojis); // Again, "fork" + + var _ = ctx.RestNew.CreateReactionsBulk(msg, botEmojis); // Again, "fork" try { var currentPage = 0; while (true) { - var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: TimeSpan.FromMinutes(5)); + var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew, timeout: Duration.FromMinutes(5)); // Increment/decrement page counter based on which reaction was clicked if (reaction.Emoji.Name == "\u23EA") currentPage = 0; // << @@ -154,18 +140,18 @@ namespace PluralKit.Bot { // If we can, remove the user's reaction (so they can press again quickly) if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) - await msg.DeleteReactionAsync(reaction.Emoji, reaction.User); + await ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId); // Edit the embed with the new page var embed = await MakeEmbedForPage(currentPage); - await msg.ModifyAsync(embed: embed); + await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed}); } } catch (TimeoutException) { // "escape hatch", clean up as if we hit X } if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) - await msg.DeleteAllReactionsAsync(); + await ctx.RestNew.DeleteAllReactions(msg.ChannelId, msg.Id); } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } @@ -203,9 +189,10 @@ namespace PluralKit.Bot { // Add back/forward reactions and the actual indicator emojis async Task AddEmojis() { - await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u2B05")); - await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u27A1")); - for (int i = 0; i < items.Count; i++) await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(indicators[i])); + await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" }); + await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" }); + for (int i = 0; i < items.Count; i++) + await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] }); } var _ = AddEmojis(); // Not concerned about awaiting @@ -213,7 +200,7 @@ namespace PluralKit.Bot { while (true) { // Wait for a reaction - var reaction = await ctx.AwaitReaction(msg, ctx.Author); + var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew); // If it's a movement reaction, inc/dec the page index if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // < @@ -230,8 +217,13 @@ namespace PluralKit.Bot { if (idx < items.Count) return items[idx]; } - var __ = msg.DeleteReactionAsync(reaction.Emoji, ctx.Author); // don't care about awaiting - await msg.ModifyAsync($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}"); + var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id); + await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, + new() + { + Content = + $"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}" + }); } } else @@ -241,13 +233,14 @@ namespace PluralKit.Bot { // Add the relevant reactions (we don't care too much about awaiting) async Task AddEmojis() { - for (int i = 0; i < items.Count; i++) await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(indicators[i])); + for (int i = 0; i < items.Count; i++) + await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() {Name = indicators[i]}); } var _ = AddEmojis(); // Then wait for a reaction and return whichever one we found - var reaction = await ctx.AwaitReaction(msg, ctx.Author,rx => indicators.Contains(rx.Emoji.Name)); + var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew,rx => indicators.Contains(rx.Emoji.Name)); return items[Array.IndexOf(indicators, reaction.Emoji.Name)]; } } @@ -271,12 +264,12 @@ namespace PluralKit.Bot { try { - await Task.WhenAll(ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode(emoji)), task); + await Task.WhenAll(ctx.RestNew.CreateReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() {Name = emoji}), task); return await task; } finally { - var _ = ctx.Message.DeleteReactionAsync(DiscordEmoji.FromUnicode(emoji), ctx.Shard.CurrentUser); + var _ = ctx.RestNew.DeleteOwnReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() { Name = emoji }); } } } diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index ee3e391d..281324e8 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -12,7 +12,9 @@ using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; +using Myriad.Builders; using Myriad.Extensions; +using Myriad.Rest; using Myriad.Rest.Types; using Myriad.Types; @@ -116,11 +118,11 @@ namespace PluralKit.Bot public static ulong InstantToSnowflake(DateTimeOffset time) => (ulong) (time - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalMilliseconds << 22; - public static async Task CreateReactionsBulk(this DiscordMessage msg, string[] reactions) + public static async Task CreateReactionsBulk(this DiscordApiClient rest, Message msg, string[] reactions) { foreach (var reaction in reactions) { - await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(reaction)); + await rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = reaction}); } } @@ -329,7 +331,7 @@ namespace PluralKit.Bot } } - public static DiscordEmbedBuilder WithSimpleLineContent(this DiscordEmbedBuilder eb, IEnumerable lines) + public static EmbedBuilder WithSimpleLineContent(this EmbedBuilder eb, IEnumerable lines) { static int CharacterLimit(int pageNumber) => // First chunk goes in description (2048 chars), rest go in embed values (1000 chars) @@ -340,11 +342,11 @@ namespace PluralKit.Bot // Add the first page to the embed description if (pages.Count > 0) - eb.WithDescription(pages[0]); + eb.Description(pages[0]); // Add the rest to blank-named (\u200B) fields for (var i = 1; i < pages.Count; i++) - eb.AddField("\u200B", pages[i]); + eb.Field(new("\u200B", pages[i])); return eb; } diff --git a/PluralKit.Core/Utils/HandlerQueue.cs b/PluralKit.Core/Utils/HandlerQueue.cs index 9d261c0a..b114e679 100644 --- a/PluralKit.Core/Utils/HandlerQueue.cs +++ b/PluralKit.Core/Utils/HandlerQueue.cs @@ -10,7 +10,7 @@ namespace PluralKit.Core public class HandlerQueue { private long _seq; - private readonly ConcurrentDictionary _handlers = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _handlers = new(); public async Task WaitFor(Func predicate, Duration? timeout = null, CancellationToken ct = default) { From 9d919d687b4d58923d4dc2f4ed8f8a282c896ffb Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 24 Dec 2020 14:54:45 +0100 Subject: [PATCH 005/608] GH Build with .NET 5 --- .github/workflows/dotnetcore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 6091e9df..3cd0fb37 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -14,6 +14,6 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.100 + dotnet-version: 5.0.x - name: Build and test with dotnet run: dotnet test --configuration Release From 2e0c30eb5d495f189f4cea16161ca8a9b3502930 Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 25 Dec 2020 12:56:46 +0100 Subject: [PATCH 006/608] Port some more commands, mostly for embeds --- Myriad/Rest/Ratelimit/Bucket.cs | 11 ++- Myriad/Rest/Ratelimit/BucketManager.cs | 1 + .../Commands/Avatars/ContextAvatarExt.cs | 2 +- PluralKit.Bot/Commands/Groups.cs | 54 +++++++------- PluralKit.Bot/Commands/Help.cs | 24 +++--- PluralKit.Bot/Commands/ImportExport.cs | 11 +-- PluralKit.Bot/Commands/Member.cs | 12 +-- PluralKit.Bot/Commands/MemberAvatar.cs | 10 +-- PluralKit.Bot/Commands/MemberEdit.cs | 73 ++++++++++--------- PluralKit.Bot/Commands/MemberGroup.cs | 4 +- PluralKit.Bot/Commands/ServerConfig.cs | 16 ++-- PluralKit.Bot/Commands/SystemEdit.cs | 34 ++++----- PluralKit.Bot/Commands/Token.cs | 23 +++--- 13 files changed, 140 insertions(+), 135 deletions(-) diff --git a/Myriad/Rest/Ratelimit/Bucket.cs b/Myriad/Rest/Ratelimit/Bucket.cs index 6210ce89..e9d0eb5f 100644 --- a/Myriad/Rest/Ratelimit/Bucket.cs +++ b/Myriad/Rest/Ratelimit/Bucket.cs @@ -17,6 +17,7 @@ namespace Myriad.Rest.Ratelimit private DateTimeOffset _nextReset; private bool _resetTimeValid; + private bool _hasReceivedRemaining; public Bucket(ILogger logger, string key, ulong major, int limit) { @@ -77,8 +78,8 @@ namespace Myriad.Rest.Ratelimit var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time if (headerNextReset > _nextReset) { - _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter})", - Key, Major, headerNextReset, headers.ResetAfter.Value); + _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, new remaining: {Remaining})", + Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining); _nextReset = headerNextReset; _resetTimeValid = true; @@ -87,6 +88,12 @@ namespace Myriad.Rest.Ratelimit if (headers.Limit != null) Limit = headers.Limit.Value; + + if (headers.Remaining != null && !_hasReceivedRemaining) + { + _hasReceivedRemaining = true; + Remaining = headers.Remaining.Value; + } } finally { diff --git a/Myriad/Rest/Ratelimit/BucketManager.cs b/Myriad/Rest/Ratelimit/BucketManager.cs index b5326903..edea0825 100644 --- a/Myriad/Rest/Ratelimit/BucketManager.cs +++ b/Myriad/Rest/Ratelimit/BucketManager.cs @@ -44,6 +44,7 @@ namespace Myriad.Rest.Ratelimit if (!_knownKeyLimits.TryGetValue(key, out var knownLimit)) return null; + _logger.Debug("Creating new bucket {BucketKey}/{BucketMajor} with limit {KnownLimit}", key, major, knownLimit); return _buckets.GetOrAdd((key, major), k => new Bucket(_logger, k.Item1, k.Item2, knownLimit)); } diff --git a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs index 43207639..98646da2 100644 --- a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs +++ b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs @@ -45,7 +45,7 @@ namespace PluralKit.Bot } // If we have an attachment, use that - if (ctx.Message.Attachments.FirstOrDefault() is {} attachment) + if (ctx.MessageNew.Attachments.FirstOrDefault() is {} attachment) { var url = TryRewriteCdnUrl(attachment.ProxyUrl); return new ParsedImage {Url = url, Source = AvatarSource.Attachment}; diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 36ee74fb..ce93f8fe 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -6,8 +6,6 @@ using System.Threading.Tasks; using Dapper; -using DSharpPlus.Entities; - using Humanizer; using Myriad.Builders; @@ -56,12 +54,12 @@ namespace PluralKit.Bot var newGroup = await _repo.CreateGroup(conn, ctx.System.Id, groupName); - var eb = new DiscordEmbedBuilder() - .WithDescription($"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:") - .AddField("View the group card", $"> pk;group **{newGroup.Reference()}**") - .AddField("Add members to the group", $"> pk;group **{newGroup.Reference()}** add **MemberName**\n> pk;group **{newGroup.Reference()}** add **Member1** **Member2** **Member3** (and so on...)") - .AddField("Set the description", $"> pk;group **{newGroup.Reference()}** description **This is my new group, and here is the description!**") - .AddField("Set the group icon", $"> pk;group **{newGroup.Reference()}** icon\n*(with an image attached)*"); + var eb = new EmbedBuilder() + .Description($"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:") + .Field(new("View the group card", $"> pk;group **{newGroup.Reference()}**")) + .Field(new("Add members to the group", $"> pk;group **{newGroup.Reference()}** add **MemberName**\n> pk;group **{newGroup.Reference()}** add **Member1** **Member2** **Member3** (and so on...)")) + .Field(new("Set the description", $"> pk;group **{newGroup.Reference()}** description **This is my new group, and here is the description!**")) + .Field(new("Set the group icon", $"> pk;group **{newGroup.Reference()}** icon\n*(with an image attached)*")); await ctx.Reply($"{Emojis.Success} Group created!", eb.Build()); } @@ -103,12 +101,12 @@ namespace PluralKit.Bot else if (!ctx.HasNext()) { // No perms check, display name isn't covered by member privacy - var eb = new DiscordEmbedBuilder() - .AddField("Name", target.Name) - .AddField("Display Name", target.DisplayName ?? "*(none)*"); + var eb = new EmbedBuilder() + .Field(new("Name", target.Name)) + .Field(new("Display Name", target.DisplayName ?? "*(none)*")); if (ctx.System?.Id == target.System) - eb.WithDescription($"To change display name, type `pk;group {target.Reference()} displayname `.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`."); + eb.Description($"To change display name, type `pk;group {target.Reference()} displayname `.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`."); await ctx.Reply(embed: eb.Build()); } @@ -145,11 +143,11 @@ namespace PluralKit.Bot else if (ctx.MatchFlag("r", "raw")) await ctx.Reply($"```\n{target.Description}\n```"); else - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle("Group description") - .WithDescription(target.Description) - .AddField("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`." - + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : "")) + await ctx.Reply(embed: new EmbedBuilder() + .Title("Group description") + .Description(target.Description) + .Field(new("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`." + + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : ""))) .Build()); } else @@ -204,13 +202,13 @@ namespace PluralKit.Bot { if ((target.Icon?.Trim() ?? "").Length > 0) { - var eb = new DiscordEmbedBuilder() - .WithTitle("Group icon") - .WithImageUrl(target.Icon); + var eb = new EmbedBuilder() + .Title("Group icon") + .Image(new(target.Icon)); if (target.System == ctx.System?.Id) { - eb.WithDescription($"To clear, use `pk;group {target.Reference()} icon -clear`."); + eb.Description($"To clear, use `pk;group {target.Reference()} icon -clear`."); } await ctx.Reply(embed: eb.Build()); @@ -359,13 +357,13 @@ namespace PluralKit.Bot // Display privacy settings if (!ctx.HasNext() && newValueFromCommand == null) { - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle($"Current privacy settings for {target.Name}") - .AddField("Description", target.DescriptionPrivacy.Explanation()) - .AddField("Icon", target.IconPrivacy.Explanation()) - .AddField("Member list", target.ListPrivacy.Explanation()) - .AddField("Visibility", target.Visibility.Explanation()) - .WithDescription($"To edit privacy settings, use the command:\n> pk;group **{target.Reference()}** privacy **** ****\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + await ctx.Reply(embed: new EmbedBuilder() + .Title($"Current privacy settings for {target.Name}") + .Field(new("Description", target.DescriptionPrivacy.Explanation()) ) + .Field(new("Icon", target.IconPrivacy.Explanation())) + .Field(new("Member list", target.ListPrivacy.Explanation())) + .Field(new("Visibility", target.Visibility.Explanation())) + .Description($"To edit privacy settings, use the command:\n> pk;group **{target.Reference()}** privacy **** ****\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`.") .Build()); return; } diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 20b41c85..dff1bf33 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using DSharpPlus.Entities; +using Myriad.Builders; using PluralKit.Core; @@ -10,17 +10,17 @@ namespace PluralKit.Bot { public async Task HelpRoot(Context ctx) { - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle("PluralKit") - .WithDescription("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.") - .AddField("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.") - .AddField("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation.") - .AddField("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information.") - .AddField("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nReact with {Emojis.Bell} on a proxied message to \"ping\" the sender\nType **`pk;invite`** to get a link to invite this bot to your own server!") - .AddField("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.") - .AddField("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78") - .WithFooter($"By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/") - .WithColor(DiscordUtils.Blue) + await ctx.Reply(embed: new EmbedBuilder() + .Title("PluralKit") + .Description("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.") + .Field(new("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.")) + .Field(new("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation.")) + .Field(new("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information.")) + .Field(new("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nReact with {Emojis.Bell} on a proxied message to \"ping\" the sender\nType **`pk;invite`** to get a link to invite this bot to your own server!")) + .Field(new("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.")) + .Field(new("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78")) + .Footer(new($"By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/")) + .Color((uint?) DiscordUtils.Blue.Value) .Build()); } diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 9003d1dc..6d79f1bf 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -5,9 +5,9 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Myriad.Rest.Exceptions; + using Newtonsoft.Json; -using DSharpPlus.Exceptions; -using DSharpPlus.Entities; using Newtonsoft.Json.Linq; @@ -18,7 +18,7 @@ namespace PluralKit.Bot public class ImportExport { private readonly DataFileService _dataFiles; - private readonly JsonSerializerSettings _settings = new JsonSerializerSettings + private readonly JsonSerializerSettings _settings = new() { // Otherwise it'll mess up/reformat the ISO strings for ???some??? reason >.> DateParseHandling = DateParseHandling.None @@ -145,8 +145,9 @@ namespace PluralKit.Bot await dm.SendMessageAsync($"<{msg.Attachments[0].Url}>"); // If the original message wasn't posted in DMs, send a public reminder - if (!(ctx.Channel is DiscordDmChannel)) - await ctx.Reply($"{Emojis.Success} Check your DMs!"); + // TODO: DMs + // if (!(ctx.Channel is DiscordDmChannel)) + // await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 68c2830e..59fce5cd 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -7,6 +7,8 @@ using Dapper; using DSharpPlus.Entities; +using Myriad.Builders; + using Newtonsoft.Json.Linq; using PluralKit.Core; @@ -89,11 +91,11 @@ namespace PluralKit.Bot var data = JObject.Parse(await resp.Content.ReadAsStringAsync()); var scream = data["soulscream"]!.Value(); - var eb = new DiscordEmbedBuilder() - .WithColor(DiscordColor.Red) - .WithTitle(name) - .WithUrl($"https://onomancer.sibr.dev/reflect?name={encoded}") - .WithDescription($"*{scream}*"); + var eb = new EmbedBuilder() + .Color((uint?) DiscordColor.Red.Value) + .Title(name) + .Url($"https://onomancer.sibr.dev/reflect?name={encoded}") + .Description($"*{scream}*"); await ctx.Reply(embed: eb.Build()); } } diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 65ad0b56..fa08b4b6 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -2,8 +2,6 @@ using System; using System.Threading.Tasks; -using DSharpPlus.Entities; - using Myriad.Builders; using PluralKit.Core; @@ -60,11 +58,11 @@ namespace PluralKit.Bot var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.GuildNew.Name})" : "avatar"; var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar"; - var eb = new DiscordEmbedBuilder() - .WithTitle($"{target.NameFor(ctx)}'s {field}") - .WithImageUrl(currentValue); + var eb = new EmbedBuilder() + .Title($"{target.NameFor(ctx)}'s {field}") + .Image(new(currentValue)); if (target.System == ctx.System?.Id) - eb.WithDescription($"To clear, use `pk;member {target.Reference()} {cmd} clear`."); + eb.Description($"To clear, use `pk;member {target.Reference()} {cmd} clear`."); await ctx.Reply(embed: eb.Build()); } diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 710269d6..017441d0 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -2,7 +2,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System; -using DSharpPlus.Entities; +using Myriad.Builders; using NodaTime; @@ -75,11 +75,11 @@ namespace PluralKit.Bot else if (ctx.MatchFlag("r", "raw")) await ctx.Reply($"```\n{target.Description}\n```"); else - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle("Member description") - .WithDescription(target.Description) - .AddField("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`." - + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : "")) + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member description") + .Description(target.Description) + .Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`." + + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : ""))) .Build()); } else @@ -158,11 +158,11 @@ namespace PluralKit.Bot else await ctx.Reply("This member does not have a color set."); else - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle("Member color") - .WithColor(target.Color.ToDiscordColor().Value) - .WithThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") - .WithDescription($"This member's color is **#{target.Color}**." + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member color") + .Color((uint?) target.Color.ToDiscordColor()!.Value.Value) + .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) + .Description($"This member's color is **#{target.Color}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} color -clear`." : "")) .Build()); } @@ -176,10 +176,10 @@ namespace PluralKit.Bot var patch = new MemberPatch {Color = Partial.Present(color.ToLowerInvariant())}; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle($"{Emojis.Success} Member color changed.") - .WithColor(color.ToDiscordColor().Value) - .WithThumbnail($"https://fakeimg.pl/256x256/{color}/?text=%20") + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Member color changed.") + .Color((uint?) color.ToDiscordColor()!.Value.Value) + .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) .Build()); } } @@ -221,7 +221,7 @@ namespace PluralKit.Bot } } - private async Task CreateMemberNameInfoEmbed(Context ctx, PKMember target) + private async Task CreateMemberNameInfoEmbed(Context ctx, PKMember target) { var lcx = ctx.LookupContextFor(target); @@ -229,28 +229,29 @@ namespace PluralKit.Bot if (ctx.GuildNew != null) memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); - var eb = new DiscordEmbedBuilder().WithTitle($"Member names") - .WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name."); + var eb = new EmbedBuilder() + .Title($"Member names") + .Footer(new($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name.")); if (target.DisplayName == null && memberGuildConfig?.DisplayName == null) - eb.AddField("Name", $"**{target.NameFor(ctx)}**"); + eb.Field(new("Name", $"**{target.NameFor(ctx)}**")); else - eb.AddField("Name", target.NameFor(ctx)); + eb.Field(new("Name", target.NameFor(ctx))); if (target.NamePrivacy.CanAccess(lcx)) { if (target.DisplayName != null && memberGuildConfig?.DisplayName == null) - eb.AddField("Display Name", $"**{target.DisplayName}**"); + eb.Field(new("Display Name", $"**{target.DisplayName}**")); else - eb.AddField("Display Name", target.DisplayName ?? "*(none)*"); + eb.Field(new("Display Name", target.DisplayName ?? "*(none)*")); } if (ctx.GuildNew != null) { if (memberGuildConfig?.DisplayName != null) - eb.AddField($"Server Name (in {ctx.GuildNew.Name})", $"**{memberGuildConfig.DisplayName}**"); + eb.Field(new($"Server Name (in {ctx.GuildNew.Name})", $"**{memberGuildConfig.DisplayName}**")); else - eb.AddField($"Server Name (in {ctx.GuildNew.Name})", memberGuildConfig?.DisplayName ?? "*(none)*"); + eb.Field(new($"Server Name (in {ctx.GuildNew.Name})", memberGuildConfig?.DisplayName ?? "*(none)*")); } return eb; @@ -285,7 +286,7 @@ namespace PluralKit.Bot // No perms check, display name isn't covered by member privacy var eb = await CreateMemberNameInfoEmbed(ctx, target); if (ctx.System?.Id == target.System) - eb.WithDescription($"To change display name, type `pk;member {target.Reference()} displayname `.\nTo clear it, type `pk;member {target.Reference()} displayname -clear`."); + eb.Description($"To change display name, type `pk;member {target.Reference()} displayname `.\nTo clear it, type `pk;member {target.Reference()} displayname -clear`."); await ctx.Reply(embed: eb.Build()); } else @@ -322,7 +323,7 @@ namespace PluralKit.Bot // No perms check, server name isn't covered by member privacy var eb = await CreateMemberNameInfoEmbed(ctx, target); if (ctx.System?.Id == target.System) - eb.WithDescription($"To change server name, type `pk;member {target.Reference()} servername `.\nTo clear it, type `pk;member {target.Reference()} servername -clear`."); + eb.Description($"To change server name, type `pk;member {target.Reference()} servername `.\nTo clear it, type `pk;member {target.Reference()} servername -clear`."); await ctx.Reply(embed: eb.Build()); } else @@ -398,16 +399,16 @@ namespace PluralKit.Bot // Display privacy settings if (!ctx.HasNext() && newValueFromCommand == null) { - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle($"Current privacy settings for {target.NameFor(ctx)}") - .AddField("Name (replaces name with display name if member has one)",target.NamePrivacy.Explanation()) - .AddField("Description", target.DescriptionPrivacy.Explanation()) - .AddField("Avatar", target.AvatarPrivacy.Explanation()) - .AddField("Birthday", target.BirthdayPrivacy.Explanation()) - .AddField("Pronouns", target.PronounPrivacy.Explanation()) - .AddField("Meta (message count, last front, last message)",target.MetadataPrivacy.Explanation()) - .AddField("Visibility", target.MemberVisibility.Explanation()) - .WithDescription("To edit privacy settings, use the command:\n`pk;member privacy `\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + await ctx.Reply(embed: new EmbedBuilder() + .Title($"Current privacy settings for {target.NameFor(ctx)}") + .Field(new("Name (replaces name with display name if member has one)",target.NamePrivacy.Explanation())) + .Field(new("Description", target.DescriptionPrivacy.Explanation())) + .Field(new("Avatar", target.AvatarPrivacy.Explanation())) + .Field(new("Birthday", target.BirthdayPrivacy.Explanation())) + .Field(new("Pronouns", target.PronounPrivacy.Explanation())) + .Field(new("Meta (message count, last front, last message)",target.MetadataPrivacy.Explanation())) + .Field(new("Visibility", target.MemberVisibility.Explanation())) + .Description("To edit privacy settings, use the command:\n`pk;member privacy `\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`.") .Build()); return; } diff --git a/PluralKit.Bot/Commands/MemberGroup.cs b/PluralKit.Bot/Commands/MemberGroup.cs index 6a8f9f7b..7d19e5e7 100644 --- a/PluralKit.Bot/Commands/MemberGroup.cs +++ b/PluralKit.Bot/Commands/MemberGroup.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using DSharpPlus.Entities; +using Myriad.Builders; using PluralKit.Core; @@ -84,7 +84,7 @@ namespace PluralKit.Bot msg += $"\nTo remove this member from one or more groups, use `pk;m {target.Reference()} group remove [group 2] [group 3...]`"; } - await ctx.Reply(msg, embed: (new DiscordEmbedBuilder().WithTitle($"{target.Name}'s groups").WithDescription(description)).Build()); + await ctx.Reply(msg, (new EmbedBuilder().Title($"{target.Name}'s groups").Description(description)).Build()); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 2660df04..6bcbb5b0 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -3,17 +3,13 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; - +using Myriad.Builders; using Myriad.Cache; using Myriad.Extensions; using Myriad.Types; using PluralKit.Core; -using Permissions = DSharpPlus.Permissions; - namespace PluralKit.Bot { public class ServerConfig @@ -183,15 +179,15 @@ namespace PluralKit.Bot newValue = false; else { - var eb = new DiscordEmbedBuilder() - .WithTitle("Log cleanup settings") - .AddField("Supported bots", botList); + var eb = new EmbedBuilder() + .Title("Log cleanup settings") + .Field(new("Supported bots", botList)); var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id)); if (guildCfg.LogCleanupEnabled) - eb.WithDescription("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); + eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); else - eb.WithDescription("Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`."); + eb.Description("Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`."); await ctx.Reply(embed: eb.Build()); return; } diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 2c96babb..4f492ea2 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -2,8 +2,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using DSharpPlus.Entities; - using Myriad.Builders; using NodaTime; @@ -77,10 +75,10 @@ namespace PluralKit.Bot else if (ctx.MatchFlag("r", "raw")) await ctx.Reply($"```\n{ctx.System.Description}\n```"); else - await ctx.Reply(embed: new DiscordEmbedBuilder() - .WithTitle("System description") - .WithDescription(ctx.System.Description) - .WithFooter("To print the description with formatting, type `pk;s description -raw`. To clear it, type `pk;s description -clear`. To change it, type `pk;s description `.") + await ctx.Reply(embed: new EmbedBuilder() + .Title("System description") + .Description(ctx.System.Description) + .Footer(new("To print the description with formatting, type `pk;s description -raw`. To clear it, type `pk;s description -clear`. To change it, type `pk;s description `.")) .Build()); } else @@ -160,10 +158,10 @@ namespace PluralKit.Bot { if ((ctx.System.AvatarUrl?.Trim() ?? "").Length > 0) { - var eb = new DiscordEmbedBuilder() - .WithTitle("System icon") - .WithImageUrl(ctx.System.AvatarUrl) - .WithDescription("To clear, use `pk;system icon clear`."); + var eb = new EmbedBuilder() + .Title("System icon") + .Image(new(ctx.System.AvatarUrl)) + .Description("To clear, use `pk;system icon clear`."); await ctx.Reply(embed: eb.Build()); } else @@ -257,14 +255,14 @@ namespace PluralKit.Bot Task PrintEmbed() { - var eb = new DiscordEmbedBuilder() - .WithTitle("Current privacy settings for your system") - .AddField("Description", ctx.System.DescriptionPrivacy.Explanation()) - .AddField("Member list", ctx.System.MemberListPrivacy.Explanation()) - .AddField("Group list", ctx.System.GroupListPrivacy.Explanation()) - .AddField("Current fronter(s)", ctx.System.FrontPrivacy.Explanation()) - .AddField("Front/switch history", ctx.System.FrontHistoryPrivacy.Explanation()) - .WithDescription("To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); + var eb = new EmbedBuilder() + .Title("Current privacy settings for your system") + .Field(new("Description", ctx.System.DescriptionPrivacy.Explanation())) + .Field(new("Member list", ctx.System.MemberListPrivacy.Explanation())) + .Field(new("Group list", ctx.System.GroupListPrivacy.Explanation())) + .Field(new("Current fronter(s)", ctx.System.FrontPrivacy.Explanation())) + .Field(new("Front/switch history", ctx.System.FrontHistoryPrivacy.Explanation())) + .Description("To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); return ctx.Reply(embed: eb.Build()); } diff --git a/PluralKit.Bot/Commands/Token.cs b/PluralKit.Bot/Commands/Token.cs index e6a26cb9..00bc8f63 100644 --- a/PluralKit.Bot/Commands/Token.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; +using Myriad.Rest.Exceptions; using PluralKit.Core; @@ -33,14 +32,16 @@ namespace PluralKit.Bot await dm.SendMessageFixedAsync(token); // If we're not already in a DM, reply with a reminder to check - if (!(ctx.Channel is DiscordDmChannel)) - await ctx.Reply($"{Emojis.Success} Check your DMs!"); + // TODO: DMs + // if (!(ctx.Channel is DiscordDmChannel)) + // await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - if (!(ctx.Channel is DiscordDmChannel)) - await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); + // TODO: DMs + // if (!(ctx.Channel is DiscordDmChannel)) + // await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } @@ -74,14 +75,16 @@ namespace PluralKit.Bot await dm.SendMessageFixedAsync(token); // If we're not already in a DM, reply with a reminder to check - if (!(ctx.Channel is DiscordDmChannel)) - await ctx.Reply($"{Emojis.Success} Check your DMs!"); + // TODO: DMs + // if (!(ctx.Channel is DiscordDmChannel)) + // await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - if (!(ctx.Channel is DiscordDmChannel)) - await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); + // TODO: DMs + // if (!(ctx.Channel is DiscordDmChannel)) + // await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } } From a2c8cbb5605bdc9783c2441265ebed65633483a1 Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 25 Dec 2020 13:19:35 +0100 Subject: [PATCH 007/608] Add DM support --- Myriad/Cache/DiscordCacheExtensions.cs | 11 ----- Myriad/Cache/IDiscordCache.cs | 1 + Myriad/Cache/MemoryDiscordCache.cs | 38 ++++++++++------- Myriad/Extensions/CacheExtensions.cs | 23 ++++++++++ Myriad/Rest/DiscordApiClient.cs | 3 ++ Myriad/Rest/Types/Requests/CreateDmRequest.cs | 4 ++ Myriad/Types/Channel.cs | 1 + .../ContextEntityArgumentsExt.cs | 1 + PluralKit.Bot/Commands/Token.cs | 42 ++++++++++--------- 9 files changed, 79 insertions(+), 45 deletions(-) create mode 100644 Myriad/Rest/Types/Requests/CreateDmRequest.cs diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index b4165987..e50c3453 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -55,16 +55,5 @@ namespace Myriad.Cache foreach (var mention in evt.Mentions) await cache.SaveUser(mention); } - - public static async ValueTask GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest, ulong userId) - { - if (cache.TryGetUser(userId, out var cacheUser)) - return cacheUser; - - var restUser = await rest.GetUser(userId); - if (restUser != null) - await cache.SaveUser(restUser); - return restUser; - } } } \ No newline at end of file diff --git a/Myriad/Cache/IDiscordCache.cs b/Myriad/Cache/IDiscordCache.cs index 7c72b272..c778ed32 100644 --- a/Myriad/Cache/IDiscordCache.cs +++ b/Myriad/Cache/IDiscordCache.cs @@ -19,6 +19,7 @@ namespace Myriad.Cache public bool TryGetGuild(ulong guildId, out Guild guild); public bool TryGetChannel(ulong channelId, out Channel channel); + public bool TryGetDmChannel(ulong userId, out Channel channel); public bool TryGetUser(ulong userId, out User user); public bool TryGetRole(ulong roleId, out Role role); diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs index 2a6c194f..2dcfde6a 100644 --- a/Myriad/Cache/MemoryDiscordCache.cs +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -10,19 +10,12 @@ namespace Myriad.Cache { public class MemoryDiscordCache: IDiscordCache { - private readonly ConcurrentDictionary _channels; - private readonly ConcurrentDictionary _guilds; - private readonly ConcurrentDictionary _roles; - private readonly ConcurrentDictionary _users; - - public MemoryDiscordCache() - { - _guilds = new ConcurrentDictionary(); - _channels = new ConcurrentDictionary(); - _users = new ConcurrentDictionary(); - _roles = new ConcurrentDictionary(); - } - + private readonly ConcurrentDictionary _channels = new(); + private readonly ConcurrentDictionary _dmChannels = new(); + private readonly ConcurrentDictionary _guilds = new(); + private readonly ConcurrentDictionary _roles = new(); + private readonly ConcurrentDictionary _users = new(); + public ValueTask SaveGuild(Guild guild) { SaveGuildRaw(guild); @@ -35,14 +28,21 @@ namespace Myriad.Cache return default; } - public ValueTask SaveChannel(Channel channel) + public async ValueTask SaveChannel(Channel channel) { _channels[channel.Id] = channel; if (channel.GuildId != null && _guilds.TryGetValue(channel.GuildId.Value, out var guild)) guild.Channels.TryAdd(channel.Id, true); - return default; + if (channel.Recipients != null) + { + foreach (var recipient in channel.Recipients) + { + _dmChannels[recipient.Id] = channel.Id; + await SaveUser(recipient); + } + } } public ValueTask SaveUser(User user) @@ -125,6 +125,14 @@ namespace Myriad.Cache public bool TryGetChannel(ulong channelId, out Channel channel) => _channels.TryGetValue(channelId, out channel!); + public bool TryGetDmChannel(ulong userId, out Channel channel) + { + channel = default!; + if (!_dmChannels.TryGetValue(userId, out var channelId)) + return false; + return TryGetChannel(channelId, out channel); + } + public bool TryGetUser(ulong userId, out User user) => _users.TryGetValue(userId, out user!); diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs index 260c5932..5686606e 100644 --- a/Myriad/Extensions/CacheExtensions.cs +++ b/Myriad/Extensions/CacheExtensions.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Myriad.Cache; +using Myriad.Rest; using Myriad.Types; namespace Myriad.Extensions @@ -41,5 +43,26 @@ namespace Myriad.Extensions throw new KeyNotFoundException($"User {roleId} not found in cache"); return role; } + + public static async ValueTask GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest, ulong userId) + { + if (cache.TryGetUser(userId, out var cacheUser)) + return cacheUser; + + var restUser = await rest.GetUser(userId); + if (restUser != null) + await cache.SaveUser(restUser); + return restUser; + } + + public static async Task GetOrCreateDmChannel(this IDiscordCache cache, DiscordApiClient rest, ulong recipientId) + { + if (cache.TryGetDmChannel(recipientId, out var cacheChannel)) + return cacheChannel; + + var restChannel = await rest.CreateDm(recipientId); + await cache.SaveChannel(restChannel); + return restChannel; + } } } \ No newline at end of file diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 953ce2d0..11bdddb9 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -120,6 +120,9 @@ namespace Myriad.Rest _client.PostMultipart($"/webhooks/{webhookId}/{webhookToken}?wait=true", ("ExecuteWebhook", webhookId), request, files)!; + public Task CreateDm(ulong recipientId) => + _client.Post($"/users/@me/channels", ("CreateDM", default), new CreateDmRequest(recipientId))!; + private static string EncodeEmoji(Emoji emoji) => WebUtility.UrlEncode(emoji.Name) ?? emoji.Id?.ToString() ?? throw new ArgumentException("Could not encode emoji"); diff --git a/Myriad/Rest/Types/Requests/CreateDmRequest.cs b/Myriad/Rest/Types/Requests/CreateDmRequest.cs new file mode 100644 index 00000000..f28b2fe2 --- /dev/null +++ b/Myriad/Rest/Types/Requests/CreateDmRequest.cs @@ -0,0 +1,4 @@ +namespace Myriad.Rest.Types.Requests +{ + public record CreateDmRequest(ulong RecipientId); +} \ No newline at end of file diff --git a/Myriad/Types/Channel.cs b/Myriad/Types/Channel.cs index 2ac13cc6..841a2e1d 100644 --- a/Myriad/Types/Channel.cs +++ b/Myriad/Types/Channel.cs @@ -22,6 +22,7 @@ public bool? Nsfw { get; init; } public ulong? ParentId { get; init; } public Overwrite[]? PermissionOverwrites { get; init; } + public User[]? Recipients { get; init; } public record Overwrite { diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index cb915c99..136b75d2 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Types; using PluralKit.Bot.Utils; diff --git a/PluralKit.Bot/Commands/Token.cs b/PluralKit.Bot/Commands/Token.cs index 00bc8f63..2c34fe38 100644 --- a/PluralKit.Bot/Commands/Token.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -1,6 +1,9 @@ using System.Threading.Tasks; +using Myriad.Extensions; using Myriad.Rest.Exceptions; +using Myriad.Rest.Types.Requests; +using Myriad.Types; using PluralKit.Core; @@ -26,22 +29,22 @@ namespace PluralKit.Bot try { // DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile) - var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); - await dm.SendMessageFixedAsync( - $"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"); - await dm.SendMessageFixedAsync(token); + var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.RestNew, ctx.AuthorNew.Id); + await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest + { + Content = $"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:" + }); + await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest {Content = token}); // If we're not already in a DM, reply with a reminder to check - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Success} Check your DMs!"); + if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); + if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } @@ -66,25 +69,26 @@ namespace PluralKit.Bot try { // DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile) - var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); - await dm.SendMessageFixedAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"); + var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.RestNew, ctx.AuthorNew.Id); + await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest + { + Content = $"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:" + }); // Make the new token after sending the first DM; this ensures if we can't DM, we also don't end up // breaking their existing token as a side effect :) var token = await MakeAndSetNewToken(ctx.System); - await dm.SendMessageFixedAsync(token); + await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest { Content = token }); // If we're not already in a DM, reply with a reminder to check - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Success} Check your DMs!"); + if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); + if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } } From 9079f1c59c13c411f08b53f316937ba5815428ed Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 25 Dec 2020 13:58:45 +0100 Subject: [PATCH 008/608] Port the DM stuff --- Myriad/Extensions/CacheExtensions.cs | 11 +++++ Myriad/Gateway/Events/MessageUpdateEvent.cs | 5 +- .../JsonSerializerOptionsExtensions.cs | 1 + Myriad/Serialization/OptionalConverter.cs | 33 +++++++------ Myriad/Types/Message.cs | 7 ++- Myriad/Utils/Optional.cs | 8 +--- PluralKit.Bot/CommandSystem/Context.cs | 12 ----- .../ContextEntityArgumentsExt.cs | 1 - PluralKit.Bot/Commands/ImportExport.cs | 9 ++-- PluralKit.Bot/Commands/Member.cs | 2 +- PluralKit.Bot/Commands/Misc.cs | 2 +- PluralKit.Bot/Commands/Random.cs | 4 +- PluralKit.Bot/Handlers/MessageDeleted.cs | 1 - PluralKit.Bot/Handlers/ReactionAdded.cs | 29 +++++++++--- PluralKit.Bot/Services/EmbedService.cs | 47 +++++++++---------- 15 files changed, 95 insertions(+), 77 deletions(-) diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs index 5686606e..d331e9e5 100644 --- a/Myriad/Extensions/CacheExtensions.cs +++ b/Myriad/Extensions/CacheExtensions.cs @@ -55,6 +55,17 @@ namespace Myriad.Extensions return restUser; } + public static async ValueTask GetOrFetchChannel(this IDiscordCache cache, DiscordApiClient rest, ulong channelId) + { + if (cache.TryGetChannel(channelId, out var cacheChannel)) + return cacheChannel; + + var restChannel = await rest.GetChannel(channelId); + if (restChannel != null) + await cache.SaveChannel(restChannel); + return restChannel; + } + public static async Task GetOrCreateDmChannel(this IDiscordCache cache, DiscordApiClient rest, ulong recipientId) { if (cache.TryGetDmChannel(recipientId, out var cacheChannel)) diff --git a/Myriad/Gateway/Events/MessageUpdateEvent.cs b/Myriad/Gateway/Events/MessageUpdateEvent.cs index 9e77d076..63b34c1d 100644 --- a/Myriad/Gateway/Events/MessageUpdateEvent.cs +++ b/Myriad/Gateway/Events/MessageUpdateEvent.cs @@ -1,7 +1,10 @@ -namespace Myriad.Gateway +using Myriad.Utils; + +namespace Myriad.Gateway { public record MessageUpdateEvent(ulong Id, ulong ChannelId): IGatewayEvent { + public Optional Content { get; init; } // TODO: lots of partials } } \ No newline at end of file diff --git a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs index b72bec2e..5f45bba0 100644 --- a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs +++ b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs @@ -13,6 +13,7 @@ namespace Myriad.Serialization opts.Converters.Add(new PermissionSetJsonConverter()); opts.Converters.Add(new ShardInfoJsonConverter()); + opts.Converters.Add(new OptionalConverterFactory()); return opts; } diff --git a/Myriad/Serialization/OptionalConverter.cs b/Myriad/Serialization/OptionalConverter.cs index c45d1caa..af7149f3 100644 --- a/Myriad/Serialization/OptionalConverter.cs +++ b/Myriad/Serialization/OptionalConverter.cs @@ -7,28 +7,33 @@ using Myriad.Utils; namespace Myriad.Serialization { - public class OptionalConverter: JsonConverter + public class OptionalConverterFactory: JsonConverterFactory { - public override IOptional? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public class Inner: JsonConverter> + { + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize(ref reader, options); + return new(inner!); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.HasValue ? value.GetValue() : default, typeof(T), options); + } + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var innerType = typeToConvert.GetGenericArguments()[0]; - var inner = JsonSerializer.Deserialize(ref reader, innerType, options); - - // TODO: rewrite to JsonConverterFactory to cut down on reflection - return (IOptional?) Activator.CreateInstance( - typeof(Optional<>).MakeGenericType(innerType), + return (JsonConverter?) Activator.CreateInstance( + typeof(Inner<>).MakeGenericType(innerType), BindingFlags.Instance | BindingFlags.Public, null, - new[] {inner}, + null, null); } - public override void Write(Utf8JsonWriter writer, IOptional value, JsonSerializerOptions options) - { - var innerType = value.GetType().GetGenericArguments()[0]; - JsonSerializer.Serialize(writer, value.GetValue(), innerType, options); - } - public override bool CanConvert(Type typeToConvert) { if (!typeToConvert.IsGenericType) diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index c74f67bf..977e1b9d 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Net.Mail; +using System.Text.Json.Serialization; + +using Myriad.Utils; namespace Myriad.Types { @@ -59,8 +62,8 @@ namespace Myriad.Types public Reference? MessageReference { get; set; } public MessageFlags Flags { get; init; } - // todo: null vs. absence - public Message? ReferencedMessage { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional ReferencedMessage { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); diff --git a/Myriad/Utils/Optional.cs b/Myriad/Utils/Optional.cs index 7b1e4139..881a53ce 100644 --- a/Myriad/Utils/Optional.cs +++ b/Myriad/Utils/Optional.cs @@ -1,16 +1,10 @@ -using System.Text.Json.Serialization; - -using Myriad.Serialization; - -namespace Myriad.Utils +namespace Myriad.Utils { public interface IOptional { - bool HasValue { get; } object? GetValue(); } - [JsonConverter(typeof(OptionalConverter))] public readonly struct Optional: IOptional { public Optional(T value) diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 037d8726..1ed55bb0 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -101,18 +101,6 @@ namespace PluralKit.Bot internal IDatabase Database => _db; internal ModelRepository Repository => _repo; - public Task Reply(string text, DiscordEmbed embed, - IEnumerable? mentions = null) - { - throw new NotImplementedException(); - } - - public Task Reply(DiscordEmbed embed, - IEnumerable? mentions = null) - { - throw new NotImplementedException(); - } - public async Task Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null) { if (!BotPermissions.HasFlag(PermissionSet.SendMessages)) diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 136b75d2..44779677 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; -using Myriad.Cache; using Myriad.Extensions; using Myriad.Types; diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 6d79f1bf..eb546e19 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Myriad.Rest.Exceptions; +using Myriad.Types; using Newtonsoft.Json; @@ -140,14 +141,14 @@ namespace PluralKit.Bot try { - var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); + var dm = await ctx.Rest.CreateDmAsync(ctx.AuthorNew.Id); + // TODO: send file var msg = await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); await dm.SendMessageAsync($"<{msg.Attachments[0].Url}>"); // If the original message wasn't posted in DMs, send a public reminder - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Success} Check your DMs!"); + if (ctx.ChannelNew.Type == Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 59fce5cd..2a914090 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -73,7 +73,7 @@ namespace PluralKit.Bot public async Task ViewMember(Context ctx, PKMember target) { var system = await _db.Execute(c => _repo.GetSystem(c, target.System)); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.GuildNew, ctx.LookupContextFor(system))); } public async Task Soulscream(Context ctx, PKMember target) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 04b465ee..db4bc36e 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -228,7 +228,7 @@ namespace PluralKit.Bot { var message = await _db.Execute(c => _repo.GetMessage(c, messageId)); if (message == null) throw Errors.MessageNotFound(messageId); - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(ctx.Shard, message)); + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 6c154cbc..51770206 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -38,7 +38,7 @@ namespace PluralKit.Bot throw new PKError("Your system has no members! Please create at least one member before using this command."); var randInt = randGen.Next(members.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); } public async Task Group(Context ctx) @@ -73,7 +73,7 @@ namespace PluralKit.Bot var ms = members.ToList(); var randInt = randGen.Next(ms.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageDeleted.cs b/PluralKit.Bot/Handlers/MessageDeleted.cs index 084ee861..9f1a607a 100644 --- a/PluralKit.Bot/Handlers/MessageDeleted.cs +++ b/PluralKit.Bot/Handlers/MessageDeleted.cs @@ -33,7 +33,6 @@ namespace PluralKit.Bot async Task Inner() { await Task.Delay(MessageDeleteDelay); - // TODO await _db.Execute(c => _repo.DeleteMessage(c, evt.Id)); } diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 9c20c3b0..36412bce 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -7,6 +7,7 @@ using Myriad.Gateway; using Myriad.Rest; using Myriad.Rest.Exceptions; using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; using Myriad.Types; using PluralKit.Core; @@ -22,10 +23,11 @@ namespace PluralKit.Bot private readonly CommandMessageService _commandMessageService; private readonly ILogger _logger; private readonly IDiscordCache _cache; + private readonly EmbedService _embeds; private readonly Bot _bot; private readonly DiscordApiClient _rest; - public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest) + public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest, EmbedService embeds) { _db = db; _repo = repo; @@ -33,6 +35,7 @@ namespace PluralKit.Bot _cache = cache; _bot = bot; _rest = rest; + _embeds = embeds; _logger = logger.ForContext(); } @@ -151,13 +154,22 @@ namespace PluralKit.Bot private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg) { + var guild = _cache.GetGuild(evt.GuildId!.Value); + // Try to DM the user info about the message // var member = await evt.Guild.GetMember(evt.User.Id); try { - // TODO: how to DM? - // await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner)); - // await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg)); + var dm = await _cache.GetOrCreateDmChannel(_rest, evt.UserId); + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Embed = await _embeds.CreateMemberEmbed(msg.System, msg.Member, guild, LookupContext.ByNonOwner) + }); + + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Embed = await _embeds.CreateMessageInfoEmbed(msg) + }); } catch (UnauthorizedException) { } // No permissions to DM, can't check for this :( @@ -192,9 +204,12 @@ namespace PluralKit.Bot // If not, tell them in DMs (if we can) try { - // todo: how to dm - // await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"); - // await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode()); + var dm = await _cache.GetOrCreateDmChannel(_rest, evt.UserId); + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Content = $"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:" + }); + await _rest.CreateMessage(dm.Id, new MessageRequest {Content = $"<@{msg.Message.Sender}>".AsCode()}); } catch (UnauthorizedException) { } } diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index da8b7d10..5b33c70b 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using DSharpPlus; using DSharpPlus.Entities; using Humanizer; using Myriad.Builders; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Rest; using Myriad.Types; @@ -22,13 +22,11 @@ namespace PluralKit.Bot { { private readonly IDatabase _db; private readonly ModelRepository _repo; - private readonly DiscordShardedClient _client; private readonly IDiscordCache _cache; private readonly DiscordApiClient _rest; - public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) + public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) { - _client = client; _db = db; _repo = repo; _cache = cache; @@ -39,14 +37,7 @@ namespace PluralKit.Bot { { async Task<(ulong Id, User? User)> Inner(ulong id) { - if (_cache.TryGetUser(id, out var cachedUser)) - return (id, cachedUser); - - var user = await _rest.GetUser(id); - if (user == null) - return (id, null); - // todo: move to "GetUserCached" helper - await _cache.SaveUser(user); + var user = await _cache.GetOrFetchUser(_rest, id); return (id, user); } @@ -108,7 +99,7 @@ namespace PluralKit.Bot { .Build(); } - public async Task CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -233,26 +224,33 @@ namespace PluralKit.Bot { .Build(); } - public async Task CreateMessageInfoEmbed(DiscordClient client, FullMessage msg) + public async Task CreateMessageInfoEmbed(FullMessage msg) { + var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel); var ctx = LookupContext.ByNonOwner; - var channel = await _client.GetChannel(msg.Message.Channel); - var serverMsg = channel != null ? await channel.GetMessage(msg.Message.Mid) : null; + var serverMsg = channel != null ? await _rest.GetMessage(msg.Message.Channel, msg.Message.Mid) : null; // Need this whole dance to handle cases where: // - the user is deleted (userInfo == null) // - the bot's no longer in the server we're querying (channel == null) // - the member is no longer in the server we're querying (memberInfo == null) - DiscordMember memberInfo = null; - DiscordUser userInfo = null; - if (channel != null) memberInfo = await channel.Guild.GetMember(msg.Message.Sender); - if (memberInfo != null) userInfo = memberInfo; // Don't do an extra request if we already have this info from the member lookup - else userInfo = await client.GetUser(msg.Message.Sender); + // TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s? + GuildMemberPartial memberInfo = null; + User userInfo = null; + if (channel != null) + { + var m = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender); + if (m != null) + // Don't do an extra request if we already have this info from the member lookup + userInfo = m.User; + memberInfo = m; + } + else userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender); // Calculate string displayed under "Sent by" string userStr; - if (memberInfo != null && memberInfo.Nickname != null) - userStr = $"**Username:** {memberInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nickname}"; + if (memberInfo != null && memberInfo.Nick != null) + userStr = $"**Username:** {userInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nick}"; else if (userInfo != null) userStr = userInfo.NameAndMention(); else userStr = $"*(deleted user {msg.Message.Sender})*"; @@ -270,7 +268,8 @@ namespace PluralKit.Bot { var roles = memberInfo?.Roles?.ToList(); if (roles != null && roles.Count > 0) { - var rolesString = string.Join(", ", roles.Select(role => role.Name)); + // TODO: what if role isn't in cache? figure out a fallback + var rolesString = string.Join(", ", roles.Select(id => _cache.GetRole(id).Name)); eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024))); } From da9d84a197abc96979eb934076c513e415f8ab33 Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 15 Jan 2021 11:29:43 +0100 Subject: [PATCH 009/608] Get rid of more D#+ references --- Myriad/Extensions/PermissionExtensions.cs | 6 +++++ Myriad/Gateway/Shard.cs | 1 + Myriad/Rest/DiscordApiClient.cs | 4 ++-- PluralKit.Bot/CommandSystem/Context.cs | 27 ++++++---------------- PluralKit.Bot/Commands/Autoproxy.cs | 10 ++++---- PluralKit.Bot/Commands/CommandTree.cs | 2 +- PluralKit.Bot/Commands/Help.cs | 2 +- PluralKit.Bot/Commands/ImportExport.cs | 19 +++++++++------ PluralKit.Bot/Commands/Member.cs | 4 +--- PluralKit.Bot/Commands/MemberEdit.cs | 4 ++-- PluralKit.Bot/Commands/Misc.cs | 17 +++++--------- PluralKit.Bot/Commands/System.cs | 2 +- PluralKit.Bot/Commands/SystemLink.cs | 2 +- PluralKit.Bot/Services/EmbedService.cs | 12 ++++------ PluralKit.Bot/Services/ShardInfoService.cs | 4 +++- PluralKit.Bot/Utils/ContextUtils.cs | 4 ++-- PluralKit.Bot/Utils/DiscordUtils.cs | 14 +++++------ 17 files changed, 63 insertions(+), 71 deletions(-) diff --git a/Myriad/Extensions/PermissionExtensions.cs b/Myriad/Extensions/PermissionExtensions.cs index 60f4f52b..d78288a3 100644 --- a/Myriad/Extensions/PermissionExtensions.cs +++ b/Myriad/Extensions/PermissionExtensions.cs @@ -143,5 +143,11 @@ namespace Myriad.Extensions PermissionSet.SendTtsMessages | PermissionSet.AttachFiles | PermissionSet.EmbedLinks; + + public static string ToPermissionString(this PermissionSet perms) + { + // TODO: clean string + return perms.ToString(); + } } } \ No newline at end of file diff --git a/Myriad/Gateway/Shard.cs b/Myriad/Gateway/Shard.cs index cb00fb81..1ace1b91 100644 --- a/Myriad/Gateway/Shard.cs +++ b/Myriad/Gateway/Shard.cs @@ -27,6 +27,7 @@ namespace Myriad.Gateway private Task _worker; public ShardInfo? ShardInfo { get; private set; } + public int ShardId => ShardInfo?.ShardId ?? 0; public GatewaySettings Settings { get; } public ShardSessionInfo SessionInfo { get; private set; } public ShardState State { get; private set; } diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 11bdddb9..257b3be6 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -46,8 +46,8 @@ namespace Myriad.Rest _client.Get($"/guilds/{guildId}/members/{userId}", ("GetGuildMember", guildId)); - public Task CreateMessage(ulong channelId, MessageRequest request) => - _client.Post($"/channels/{channelId}/messages", ("CreateMessage", channelId), request)!; + public Task CreateMessage(ulong channelId, MessageRequest request, MultipartFile[]? files = null) => + _client.PostMultipart($"/channels/{channelId}/messages", ("CreateMessage", channelId), request, files)!; public Task EditMessage(ulong channelId, ulong messageId, MessageEditRequest request) => _client.Patch($"/channels/{channelId}/messages/{messageId}", ("EditMessage", channelId), request)!; diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 1ed55bb0..d0135ced 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -1,39 +1,31 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using App.Metrics; using Autofac; -using DSharpPlus; -using DSharpPlus.Entities; - using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Rest; using Myriad.Rest.Types; using Myriad.Rest.Types.Requests; using Myriad.Types; using PluralKit.Core; -using DiscordApiClient = Myriad.Rest.DiscordApiClient; - namespace PluralKit.Bot { public class Context { private readonly ILifetimeScope _provider; - private readonly DiscordRestClient _rest; private readonly DiscordApiClient _newRest; - private readonly DiscordShardedClient _client; - private readonly DiscordClient _shard = null; + private readonly Cluster _cluster; private readonly Shard _shardNew; private readonly Guild? _guild; private readonly Channel _channel; - private readonly DiscordMessage _message = null; private readonly MessageCreateEvent _messageNew; private readonly Parameters _parameters; private readonly MessageContext _messageContext; @@ -52,8 +44,6 @@ namespace PluralKit.Bot public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, PKSystem senderSystem, MessageContext messageContext, PermissionSet botPermissions) { - _rest = provider.Resolve(); - _client = provider.Resolve(); _messageNew = message; _shardNew = shard; _guild = guild; @@ -66,8 +56,9 @@ namespace PluralKit.Bot _metrics = provider.Resolve(); _provider = provider; _commandMessageService = provider.Resolve(); - _parameters = new Parameters(message.Content.Substring(commandParseOffset)); + _parameters = new Parameters(message.Content?.Substring(commandParseOffset)); _newRest = provider.Resolve(); + _cluster = provider.Resolve(); _botPermissions = botPermissions; _userPermissions = _cache.PermissionsFor(message); @@ -75,23 +66,19 @@ namespace PluralKit.Bot public IDiscordCache Cache => _cache; - public DiscordUser Author => _message.Author; - public DiscordChannel Channel => _message.Channel; public Channel ChannelNew => _channel; public User AuthorNew => _messageNew.Author; public GuildMemberPartial MemberNew => _messageNew.Member; - public DiscordMessage Message => _message; + public Message MessageNew => _messageNew; - public DiscordGuild Guild => _message.Channel.Guild; public Guild GuildNew => _guild; - public DiscordClient Shard => _shard; - public DiscordShardedClient Client => _client; + public Shard ShardNew => _shardNew; + public Cluster Cluster => _cluster; public MessageContext MessageContext => _messageContext; public PermissionSet BotPermissions => _botPermissions; public PermissionSet UserPermissions => _userPermissions; - public DiscordRestClient Rest => _rest; public DiscordApiClient RestNew => _newRest; public PKSystem System => _senderSystem; diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index d7f51975..15abc6fc 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -129,7 +129,7 @@ namespace PluralKit.Bot } if (!ctx.MessageContext.AllowAutoproxy) - eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.")); + eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.AuthorNew.Id}>). To enable it, use `pk;autoproxy account enable`.")); return eb.Build(); } @@ -191,7 +191,7 @@ namespace PluralKit.Bot else { var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; - await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>."); + await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.AuthorNew.Id}>."); } } @@ -200,12 +200,12 @@ namespace PluralKit.Bot var statusString = allow ? "enabled" : "disabled"; if (ctx.MessageContext.AllowAutoproxy == allow) { - await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); + await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.AuthorNew.Id}>."); return; } var patch = new AccountPatch { AllowAutoproxy = allow }; - await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch)); - await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); + await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.AuthorNew.Id, patch)); + await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.AuthorNew.Id}>."); } private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index a5868184..a8132c5e 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -524,7 +524,7 @@ namespace PluralKit.Bot { // Try to resolve the user ID to find the associated account, // so we can print their username. - var user = await ctx.Shard.GetUser(id); + var user = await ctx.RestNew.GetUser(id); if (user != null) return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered."; else diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index dff1bf33..d4bd9e9f 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -20,7 +20,7 @@ namespace PluralKit.Bot .Field(new("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.")) .Field(new("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78")) .Footer(new($"By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/")) - .Color((uint?) DiscordUtils.Blue.Value) + .Color(DiscordUtils.Blue) .Build()); } diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index eb546e19..fda3afe7 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -6,6 +6,8 @@ using System.Text; using System.Threading.Tasks; using Myriad.Rest.Exceptions; +using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; using Myriad.Types; using Newtonsoft.Json; @@ -32,7 +34,7 @@ namespace PluralKit.Bot public async Task Import(Context ctx) { - var url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url; + var url = ctx.RemainderOrNull() ?? ctx.MessageNew.Attachments.FirstOrDefault()?.Url; if (url == null) throw Errors.NoImportFilePassed; await ctx.BusyIndicator(async () => @@ -67,7 +69,7 @@ namespace PluralKit.Bot if (!data.Valid) throw Errors.InvalidImportFile; - if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id)) + if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.AuthorNew.Id)) { var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?"; if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled; @@ -75,7 +77,7 @@ namespace PluralKit.Bot // If passed system is null, it'll create a new one // (and that's okay!) - var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.Author.Id); + var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.AuthorNew.Id); if (!result.Success) await ctx.Reply($"{Emojis.Error} The provided system profile could not be imported. {result.Message}"); else if (ctx.System == null) @@ -141,13 +143,16 @@ namespace PluralKit.Bot try { - var dm = await ctx.Rest.CreateDmAsync(ctx.AuthorNew.Id); + var dm = await ctx.RestNew.CreateDm(ctx.AuthorNew.Id); // TODO: send file - var msg = await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); - await dm.SendMessageAsync($"<{msg.Attachments[0].Url}>"); + + var msg = await ctx.RestNew.CreateMessage(dm.Id, + new MessageRequest {Content = $"{Emojis.Success} Here you go!"}, + new[] {new MultipartFile("system.json", stream)}); + await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest { Content = $"<{msg.Attachments[0].Url}>" }); // If the original message wasn't posted in DMs, send a public reminder - if (ctx.ChannelNew.Type == Channel.ChannelType.Dm) + if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 2a914090..20229368 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -5,8 +5,6 @@ using System.Web; using Dapper; -using DSharpPlus.Entities; - using Myriad.Builders; using Newtonsoft.Json.Linq; @@ -92,7 +90,7 @@ namespace PluralKit.Bot var scream = data["soulscream"]!.Value(); var eb = new EmbedBuilder() - .Color((uint?) DiscordColor.Red.Value) + .Color(DiscordUtils.Red) .Title(name) .Url($"https://onomancer.sibr.dev/reflect?name={encoded}") .Description($"*{scream}*"); diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 017441d0..6dc1f1c6 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -160,7 +160,7 @@ namespace PluralKit.Bot else await ctx.Reply(embed: new EmbedBuilder() .Title("Member color") - .Color((uint?) target.Color.ToDiscordColor()!.Value.Value) + .Color(target.Color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) .Description($"This member's color is **#{target.Color}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} color -clear`." : "")) @@ -178,7 +178,7 @@ namespace PluralKit.Bot await ctx.Reply(embed: new EmbedBuilder() .Title($"{Emojis.Success} Member color changed.") - .Color((uint?) color.ToDiscordColor()!.Value.Value) + .Color(color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) .Build()); } diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index db4bc36e..92ea99e4 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -6,8 +6,6 @@ using System.Threading.Tasks; using App.Metrics; -using DSharpPlus; - using Humanizer; using NodaTime; @@ -22,8 +20,6 @@ using Myriad.Rest; using Myriad.Rest.Types.Requests; using Myriad.Types; -using Permissions = DSharpPlus.Permissions; - namespace PluralKit.Bot { public class Misc { @@ -89,10 +85,10 @@ namespace PluralKit.Bot { var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name)?.Value ?? 0; // TODO: shard stuff - var shardId = ctx.Shard.ShardId; - var shardTotal = ctx.Client.ShardClients.Count; + var shardId = ctx.ShardNew.ShardInfo?.ShardId ?? -1; + var shardTotal = ctx.Cluster.Shards.Count; var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count(); - var shardInfo = _shards.GetShardInfo(ctx.Shard); + var shardInfo = _shards.GetShardInfo(ctx.ShardNew); var process = Process.GetCurrentProcess(); var memoryUsage = process.WorkingSet64; @@ -188,7 +184,7 @@ namespace PluralKit.Bot { if (permissionsMissing.Count == 0) { - eb.Description($"No errors found, all channels proxyable :)").Color((uint?) DiscordUtils.Green.Value); + eb.Description($"No errors found, all channels proxyable :)").Color(DiscordUtils.Green); } else { @@ -196,14 +192,13 @@ namespace PluralKit.Bot { { // Each missing permission field can have multiple missing channels // so we extract them all and generate a comma-separated list - // TODO: port ToPermissionString? - var missingPermissionNames = ((Permissions)missingPermissionField).ToPermissionString(); + var missingPermissionNames = ((PermissionSet) missingPermissionField).ToPermissionString(); var channelsList = string.Join("\n", channels .OrderBy(c => c.Position) .Select(c => $"#{c.Name}")); eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000))); - eb.Color((uint?) DiscordUtils.Red.Value); + eb.Color(DiscordUtils.Red); } } diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index d531196d..b1575c95 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -34,7 +34,7 @@ namespace PluralKit.Bot var system = _db.Execute(async c => { var system = await _repo.CreateSystem(c, systemName); - await _repo.AddAccount(c, system.Id, ctx.Author.Id); + await _repo.AddAccount(c, system.Id, ctx.AuthorNew.Id); return system; }); diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 0ebc0d83..24042094 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -49,7 +49,7 @@ namespace PluralKit.Bot ulong id; if (!ctx.HasNext()) - id = ctx.Author.Id; + id = ctx.AuthorNew.Id; else if (!ctx.MatchUserRaw(out id)) throw new PKSyntaxError("You must pass an account to link with (either ID or @mention)."); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 5b33c70b..1efc3506 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using DSharpPlus.Entities; - using Humanizer; using Myriad.Builders; @@ -58,7 +56,7 @@ namespace PluralKit.Bot { .Title(system.Name) .Thumbnail(new(system.AvatarUrl)) .Footer(new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}")) - .Color((uint) DiscordUtils.Gray.Value); + .Color(DiscordUtils.Gray); var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) @@ -107,7 +105,7 @@ namespace PluralKit.Bot { var name = member.NameFor(ctx); if (system.Name != null) name = $"{name} ({system.Name})"; - DiscordColor color; + uint color; try { color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray; @@ -135,7 +133,7 @@ namespace PluralKit.Bot { // TODO: add URL of website when that's up .Author(new(name, IconUrl: DiscordUtils.WorkaroundForUrlBug(avatar))) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) - .Color((uint?) color.Value) + .Color(color) .Footer(new( $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}" : "")}")); @@ -218,7 +216,7 @@ namespace PluralKit.Bot { var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id).ToListAsync().AsTask()); var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; return new EmbedBuilder() - .Color((uint?) (members.FirstOrDefault()?.Color?.ToDiscordColor()?.Value ?? DiscordUtils.Gray.Value)) + .Color(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray) .Field(new($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*")) .Field(new("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)")) .Build(); @@ -280,7 +278,7 @@ namespace PluralKit.Bot { { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var eb = new EmbedBuilder() - .Color((uint?) DiscordUtils.Gray.Value) + .Color(DiscordUtils.Gray) .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" diff --git a/PluralKit.Bot/Services/ShardInfoService.cs b/PluralKit.Bot/Services/ShardInfoService.cs index 7bbbe2a1..a89a0bd2 100644 --- a/PluralKit.Bot/Services/ShardInfoService.cs +++ b/PluralKit.Bot/Services/ShardInfoService.cs @@ -7,6 +7,8 @@ using App.Metrics; using DSharpPlus; using DSharpPlus.EventArgs; +using Myriad.Gateway; + using NodaTime; using NodaTime.Extensions; @@ -144,7 +146,7 @@ namespace PluralKit.Bot return Task.CompletedTask; } - public ShardInfo GetShardInfo(DiscordClient shard) => _shardInfo[shard.ShardId]; + public ShardInfo GetShardInfo(Shard shard) => _shardInfo[shard.ShardId]; public ICollection Shards => _shardInfo.Values; } diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index c245d549..58b281c5 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -88,7 +88,7 @@ namespace PluralKit.Bot { public static async Task ConfirmWithReply(this Context ctx, string expectedReply) { bool Predicate(MessageCreateEvent e) => - e.Author.Id == ctx.AuthorNew.Id && e.ChannelId == ctx.Channel.Id; + e.Author.Id == ctx.AuthorNew.Id && e.ChannelId == ctx.ChannelNew.Id; var msg = await ctx.Services.Resolve>() .WaitFor(Predicate, Duration.FromMinutes(1)); @@ -217,7 +217,7 @@ namespace PluralKit.Bot { if (idx < items.Count) return items[idx]; } - var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id); + var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.AuthorNew.Id); await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, new() { diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 281324e8..1581f689 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -28,10 +28,10 @@ namespace PluralKit.Bot { public static class DiscordUtils { - public static DiscordColor Blue = new DiscordColor(0x1f99d8); - public static DiscordColor Green = new DiscordColor(0x00cc78); - public static DiscordColor Red = new DiscordColor(0xef4b3d); - public static DiscordColor Gray = new DiscordColor(0x979c9f); + public const uint Blue = 0x1f99d8; + public const uint Green = 0x00cc78; + public const uint Red = 0xef4b3d; + public const uint Gray = 0x979c9f; public static Permissions DM_PERMISSIONS = (Permissions) 0b00000_1000110_1011100110000_000000; @@ -154,10 +154,10 @@ namespace PluralKit.Bot return cache != null && cache.TryGetValue(id, out user); } - public static DiscordColor? ToDiscordColor(this string color) + public static uint? ToDiscordColor(this string color) { - if (int.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) - return new DiscordColor(colorInt); + if (uint.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) + return colorInt; throw new ArgumentException($"Invalid color string '{color}'."); } From d56e878c28d9ed1c690e4488d69b6c119de9a814 Mon Sep 17 00:00:00 2001 From: Ske Date: Sat, 30 Jan 2021 01:07:43 +0100 Subject: [PATCH 010/608] Converted shard and logclean service --- Myriad/Extensions/MessageExtensions.cs | 4 +- Myriad/Extensions/SnowflakeExtensions.cs | 20 +++ Myriad/Gateway/Cluster.cs | 6 +- Myriad/Gateway/Shard.cs | 84 +++++---- Myriad/Gateway/ShardConnection.cs | 17 +- Myriad/Rest/BaseRestClient.cs | 2 +- Myriad/Rest/Ratelimit/Bucket.cs | 29 ++- .../JsonSerializerOptionsExtensions.cs | 2 +- PluralKit.Bot/Bot.cs | 2 + PluralKit.Bot/Services/LoggerCleanService.cs | 165 +++++++++++------- PluralKit.Bot/Services/ShardInfoService.cs | 86 +++++---- 11 files changed, 264 insertions(+), 153 deletions(-) create mode 100644 Myriad/Extensions/SnowflakeExtensions.cs diff --git a/Myriad/Extensions/MessageExtensions.cs b/Myriad/Extensions/MessageExtensions.cs index 60adb532..56664154 100644 --- a/Myriad/Extensions/MessageExtensions.cs +++ b/Myriad/Extensions/MessageExtensions.cs @@ -1,4 +1,6 @@ -using Myriad.Gateway; +using System; + +using Myriad.Gateway; using Myriad.Types; namespace Myriad.Extensions diff --git a/Myriad/Extensions/SnowflakeExtensions.cs b/Myriad/Extensions/SnowflakeExtensions.cs new file mode 100644 index 00000000..71446138 --- /dev/null +++ b/Myriad/Extensions/SnowflakeExtensions.cs @@ -0,0 +1,20 @@ +using System; + +using Myriad.Types; + +namespace Myriad.Extensions +{ + public static class SnowflakeExtensions + { + public static readonly DateTimeOffset DiscordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); + + public static DateTimeOffset SnowflakeToTimestamp(ulong snowflake) => + DiscordEpoch + TimeSpan.FromMilliseconds(snowflake >> 22); + + public static DateTimeOffset Timestamp(this Message msg) => SnowflakeToTimestamp(msg.Id); + public static DateTimeOffset Timestamp(this Channel channel) => SnowflakeToTimestamp(channel.Id); + public static DateTimeOffset Timestamp(this Guild guild) => SnowflakeToTimestamp(guild.Id); + public static DateTimeOffset Timestamp(this Webhook webhook) => SnowflakeToTimestamp(webhook.Id); + public static DateTimeOffset Timestamp(this User user) => SnowflakeToTimestamp(user.Id); + } +} \ No newline at end of file diff --git a/Myriad/Gateway/Cluster.cs b/Myriad/Gateway/Cluster.cs index 63e8a2cc..220eadc1 100644 --- a/Myriad/Gateway/Cluster.cs +++ b/Myriad/Gateway/Cluster.cs @@ -23,6 +23,7 @@ namespace Myriad.Gateway } public Func? EventReceived { get; set; } + public event Action? ShardCreated; public IReadOnlyDictionary Shards => _shards; public ClusterSessionState SessionState => GetClusterState(); @@ -35,7 +36,8 @@ namespace Myriad.Gateway foreach (var (id, shard) in _shards) shards.Add(new ClusterSessionState.ShardState { - Shard = shard.ShardInfo ?? new ShardInfo(id, _shards.Count), Session = shard.SessionInfo + Shard = shard.ShardInfo, + Session = shard.SessionInfo }); return new ClusterSessionState {Shards = shards}; @@ -78,6 +80,8 @@ namespace Myriad.Gateway var shard = new Shard(_logger, new Uri(url), _gatewaySettings, shardInfo, session); shard.OnEventReceived += evt => OnShardEventReceived(shard, evt); _shards[shardInfo.ShardId] = shard; + + ShardCreated?.Invoke(shard); } private async Task OnShardEventReceived(Shard shard, IGatewayEvent evt) diff --git a/Myriad/Gateway/Shard.cs b/Myriad/Gateway/Shard.cs index 1ace1b91..25cbba81 100644 --- a/Myriad/Gateway/Shard.cs +++ b/Myriad/Gateway/Shard.cs @@ -12,10 +12,10 @@ namespace Myriad.Gateway { public class Shard: IAsyncDisposable { - private const string LibraryName = "Newcord Test"; + private const string LibraryName = "Myriad (for PluralKit)"; private readonly JsonSerializerOptions _jsonSerializerOptions = - new JsonSerializerOptions().ConfigureForNewcord(); + new JsonSerializerOptions().ConfigureForMyriad(); private readonly ILogger _logger; private readonly Uri _uri; @@ -26,8 +26,8 @@ namespace Myriad.Gateway private DateTimeOffset? _lastHeartbeatSent; private Task _worker; - public ShardInfo? ShardInfo { get; private set; } - public int ShardId => ShardInfo?.ShardId ?? 0; + public ShardInfo ShardInfo { get; private set; } + public int ShardId => ShardInfo.ShardId; public GatewaySettings Settings { get; } public ShardSessionInfo SessionInfo { get; private set; } public ShardState State { get; private set; } @@ -36,11 +36,16 @@ namespace Myriad.Gateway public ApplicationPartial? Application { get; private set; } public Func? OnEventReceived { get; set; } + public event Action? HeartbeatReceived; + public event Action? SocketOpened; + public event Action? Resumed; + public event Action? Ready; + public event Action? SocketClosed; - public Shard(ILogger logger, Uri uri, GatewaySettings settings, ShardInfo? info = null, + public Shard(ILogger logger, Uri uri, GatewaySettings settings, ShardInfo info, ShardSessionInfo? sessionInfo = null) { - _logger = logger; + _logger = logger.ForContext(); _uri = uri; Settings = settings; @@ -71,23 +76,23 @@ namespace Myriad.Gateway while (true) try { - _logger.Information("Connecting..."); + _logger.Information("Shard {ShardId}: Connecting...", ShardId); State = ShardState.Connecting; await Connect(); - _logger.Information("Connected. Entering main loop..."); + _logger.Information("Shard {ShardId}: Connected. Entering main loop...", ShardId); // Tick returns false if we need to stop and reconnect while (await Tick(_conn!)) await Task.Delay(TimeSpan.FromMilliseconds(1000)); - _logger.Information("Connection closed, reconnecting..."); + _logger.Information("Shard {ShardId}: Connection closed, reconnecting...", ShardId); State = ShardState.Closed; } catch (Exception e) { - _logger.Error(e, "Error in shard state handler"); + _logger.Error(e, "Shard {ShardId}: Error in shard state handler", ShardId); } } @@ -116,8 +121,8 @@ namespace Myriad.Gateway if (!_hasReceivedAck) { _logger.Warning( - "Did not receive heartbeat Ack from gateway within interval ({HeartbeatInterval})", - _currentHeartbeatInterval); + "Shard {ShardId}: Did not receive heartbeat Ack from gateway within interval ({HeartbeatInterval})", + ShardId, _currentHeartbeatInterval); State = ShardState.Closing; await conn.Disconnect(WebSocketCloseStatus.ProtocolError, "Did not receive ACK in time"); return false; @@ -131,7 +136,8 @@ namespace Myriad.Gateway private async Task SendHeartbeat(ShardConnection conn) { - _logger.Debug("Sending heartbeat"); + _logger.Debug("Shard {ShardId}: Sending heartbeat with seq.no. {LastSequence}", + ShardId, SessionInfo.LastSequence); await conn.Send(new GatewayPacket {Opcode = GatewayOpcode.Heartbeat, Payload = SessionInfo.LastSequence}); _lastHeartbeatSent = DateTimeOffset.UtcNow; @@ -144,7 +150,12 @@ namespace Myriad.Gateway _currentHeartbeatInterval = null; - _conn = new ShardConnection(_uri, _logger, _jsonSerializerOptions) {OnReceive = OnReceive}; + _conn = new ShardConnection(_uri, _logger, _jsonSerializerOptions) + { + OnReceive = OnReceive, + OnOpen = () => SocketOpened?.Invoke(), + OnClose = (closeStatus, message) => SocketClosed?.Invoke(closeStatus, message) + }; } private async Task OnReceive(GatewayPacket packet) @@ -158,21 +169,23 @@ namespace Myriad.Gateway } case GatewayOpcode.Heartbeat: { - _logger.Debug("Received heartbeat request from shard, sending Ack"); + _logger.Debug("Shard {ShardId}: Received heartbeat request from shard, sending Ack", ShardId); await _conn!.Send(new GatewayPacket {Opcode = GatewayOpcode.HeartbeatAck}); break; } case GatewayOpcode.HeartbeatAck: { Latency = DateTimeOffset.UtcNow - _lastHeartbeatSent; - _logger.Debug("Received heartbeat Ack (latency {Latency})", Latency); + _logger.Debug("Shard {ShardId}: Received heartbeat Ack with latency {Latency}", ShardId, Latency); + if (Latency != null) + HeartbeatReceived?.Invoke(Latency!.Value); _hasReceivedAck = true; break; } case GatewayOpcode.Reconnect: { - _logger.Information("Received Reconnect, closing and reconnecting"); + _logger.Information("Shard {ShardId}: Received Reconnect, closing and reconnecting", ShardId); await _conn!.Disconnect(WebSocketCloseStatus.Empty, null); break; } @@ -187,8 +200,8 @@ namespace Myriad.Gateway var delay = TimeSpan.FromMilliseconds(new Random().Next(1000, 5000)); _logger.Information( - "Received Invalid Session (can resume? {CanResume}), reconnecting after {ReconnectDelay}", - canResume, delay); + "Shard {ShardId}: Received Invalid Session (can resume? {CanResume}), reconnecting after {ReconnectDelay}", + ShardId, canResume, delay); await _conn!.Disconnect(WebSocketCloseStatus.Empty, null); // Will reconnect after exiting this "loop" @@ -205,15 +218,16 @@ namespace Myriad.Gateway if (State == ShardState.Connecting) await HandleReady(rdy); else - _logger.Warning("Received Ready event in unexpected state {ShardState}, ignoring?", State); + _logger.Warning("Shard {ShardId}: Received Ready event in unexpected state {ShardState}, ignoring?", + ShardId, State); } else if (evt is ResumedEvent) { if (State == ShardState.Connecting) await HandleResumed(); else - _logger.Warning("Received Resumed event in unexpected state {ShardState}, ignoring?", - State); + _logger.Warning("Shard {ShardId}: Received Resumed event in unexpected state {ShardState}, ignoring?", + ShardId, State); } await HandleEvent(evt); @@ -221,7 +235,7 @@ namespace Myriad.Gateway } default: { - _logger.Debug("Received unknown gateway opcode {Opcode}", packet.Opcode); + _logger.Debug("Shard {ShardId}: Received unknown gateway opcode {Opcode}", ShardId, packet.Opcode); break; } } @@ -238,44 +252,47 @@ namespace Myriad.Gateway { if (!IGatewayEvent.EventTypes.TryGetValue(eventType, out var clrType)) { - _logger.Information("Received unknown event type {EventType}", eventType); + _logger.Information("Shard {ShardId}: Received unknown event type {EventType}", ShardId, eventType); return null; } try { - _logger.Verbose("Deserializing {EventType} to {ClrType}", eventType, clrType); + _logger.Verbose("Shard {ShardId}: Deserializing {EventType} to {ClrType}", ShardId, eventType, clrType); return JsonSerializer.Deserialize(data.GetRawText(), clrType, _jsonSerializerOptions) as IGatewayEvent; } catch (JsonException e) { - _logger.Error(e, "Error deserializing event {EventType} to {ClrType}", eventType, clrType); + _logger.Error(e, "Shard {ShardId}: Error deserializing event {EventType} to {ClrType}", ShardId, eventType, clrType); return null; } } private Task HandleReady(ReadyEvent ready) { - ShardInfo = ready.Shard; + // TODO: when is ready.Shard ever null? + ShardInfo = ready.Shard ?? new ShardInfo(0, 0); SessionInfo = SessionInfo with { Session = ready.SessionId }; User = ready.User; Application = ready.Application; State = ShardState.Open; - + + Ready?.Invoke(); return Task.CompletedTask; } private Task HandleResumed() { State = ShardState.Open; + Resumed?.Invoke(); return Task.CompletedTask; } private async Task HandleHello(JsonElement json) { var hello = JsonSerializer.Deserialize(json.GetRawText(), _jsonSerializerOptions)!; - _logger.Debug("Received Hello with interval {Interval} ms", hello.HeartbeatInterval); + _logger.Debug("Shard {ShardId}: Received Hello with interval {Interval} ms", ShardId, hello.HeartbeatInterval); _currentHeartbeatInterval = TimeSpan.FromMilliseconds(hello.HeartbeatInterval); await SendHeartbeat(_conn!); @@ -293,7 +310,7 @@ namespace Myriad.Gateway private async Task SendIdentify() { - _logger.Information("Sending gateway Identify for shard {@ShardInfo}", SessionInfo); + _logger.Information("Shard {ShardId}: Sending gateway Identify for shard {@ShardInfo}", ShardId, ShardInfo); await _conn!.Send(new GatewayPacket { Opcode = GatewayOpcode.Identify, @@ -312,11 +329,12 @@ namespace Myriad.Gateway private async Task SendResume(string session, int lastSequence) { - _logger.Information("Sending gateway Resume for session {@SessionInfo}", ShardInfo, - SessionInfo); + _logger.Information("Shard {ShardId}: Sending gateway Resume for session {@SessionInfo}", + ShardId, SessionInfo); await _conn!.Send(new GatewayPacket { - Opcode = GatewayOpcode.Resume, Payload = new GatewayResume(Settings.Token, session, lastSequence) + Opcode = GatewayOpcode.Resume, + Payload = new GatewayResume(Settings.Token, session, lastSequence) }); } diff --git a/Myriad/Gateway/ShardConnection.cs b/Myriad/Gateway/ShardConnection.cs index 77453de2..886e0664 100644 --- a/Myriad/Gateway/ShardConnection.cs +++ b/Myriad/Gateway/ShardConnection.cs @@ -29,9 +29,12 @@ namespace Myriad.Gateway } public Func? OnReceive { get; set; } + public Action? OnOpen { get; set; } + + public Action? OnClose { get; set; } public WebSocketState State => _client.State; - + public async ValueTask DisposeAsync() { _cts.Cancel(); @@ -50,8 +53,14 @@ namespace Myriad.Gateway }.Uri; _logger.Debug("Connecting to gateway WebSocket at {GatewayUrl}", realUrl); await _client.ConnectAsync(realUrl, default); - + _logger.Debug("Gateway connection opened"); + + OnOpen?.Invoke(); + + // Main worker loop, spins until we manually disconnect (which hits the cancellation token) + // or the server disconnects us (which sets state to closed) while (!_cts.IsCancellationRequested && _client.State == WebSocketState.Open) + { try { await HandleReceive(); @@ -60,6 +69,9 @@ namespace Myriad.Gateway { _logger.Error(e, "Error in WebSocket receive worker"); } + } + + OnClose?.Invoke(_client.CloseStatus ?? default, _client.CloseStatusDescription); } private async Task HandleReceive() @@ -92,6 +104,7 @@ namespace Myriad.Gateway private async Task ReadData(MemoryStream stream) { + // TODO: does this throw if we disconnect mid-read? using var buf = MemoryPool.Shared.Rent(); ValueWebSocketReceiveResult result; do diff --git a/Myriad/Rest/BaseRestClient.cs b/Myriad/Rest/BaseRestClient.cs index ad35cc0a..40a85c68 100644 --- a/Myriad/Rest/BaseRestClient.cs +++ b/Myriad/Rest/BaseRestClient.cs @@ -40,7 +40,7 @@ namespace Myriad.Rest Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", userAgent); Client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", token); - _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForNewcord(); + _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForMyriad(); _ratelimiter = new Ratelimiter(logger); var discordPolicy = new DiscordRateLimitPolicy(_ratelimiter); diff --git a/Myriad/Rest/Ratelimit/Bucket.cs b/Myriad/Rest/Ratelimit/Bucket.cs index e9d0eb5f..7f49ec33 100644 --- a/Myriad/Rest/Ratelimit/Bucket.cs +++ b/Myriad/Rest/Ratelimit/Bucket.cs @@ -15,9 +15,9 @@ namespace Myriad.Rest.Ratelimit private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore = new(1, 1); - private DateTimeOffset _nextReset; + private DateTimeOffset? _nextReset; private bool _resetTimeValid; - private bool _hasReceivedRemaining; + private bool _hasReceivedHeaders; public Bucket(ILogger logger, string key, ulong major, int limit) { @@ -54,6 +54,7 @@ namespace Myriad.Rest.Ratelimit "{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit} left], allowing through", Key, Major, Remaining, Limit); Remaining--; + return true; } @@ -78,21 +79,25 @@ namespace Myriad.Rest.Ratelimit var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time if (headerNextReset > _nextReset) { - _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, new remaining: {Remaining})", - Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining); + _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, remaining: {Remaining}, local remaining: {LocalRemaining})", + Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining, Remaining); _nextReset = headerNextReset; _resetTimeValid = true; } } - if (headers.Limit != null) + if (headers.Limit != null) Limit = headers.Limit.Value; - if (headers.Remaining != null && !_hasReceivedRemaining) + if (headers.Remaining != null && !_hasReceivedHeaders) { - _hasReceivedRemaining = true; - Remaining = headers.Remaining.Value; + var oldRemaining = Remaining; + Remaining = Math.Min(headers.Remaining.Value, Remaining); + + _logger.Debug("{BucketKey}/{BucketMajor}: Received first remaining of {HeaderRemaining}, previous local remaining is {LocalRemaining}, new local remaining is {Remaining}", + Key, Major, headers.Remaining.Value, oldRemaining, Remaining); + _hasReceivedHeaders = true; } } finally @@ -106,6 +111,12 @@ namespace Myriad.Rest.Ratelimit try { _semaphore.Wait(); + + // If we don't have any reset data, "snap" it to now + // This happens before first request and at this point the reset is invalid anyway, so it's fine + // but it ensures the stale timeout doesn't trigger early by using `default` value + if (_nextReset == null) + _nextReset = now; // If we're past the reset time *and* we haven't reset already, do that var timeSinceReset = now - _nextReset; @@ -147,7 +158,7 @@ namespace Myriad.Rest.Ratelimit if (!_resetTimeValid) return FallbackDelay; - var delay = _nextReset - now; + var delay = (_nextReset ?? now) - now; // If we have a really small (or negative) value, return a fallback delay too if (delay < Epsilon) diff --git a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs index 5f45bba0..50b1192f 100644 --- a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs +++ b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs @@ -5,7 +5,7 @@ namespace Myriad.Serialization { public static class JsonSerializerOptionsExtensions { - public static JsonSerializerOptions ConfigureForNewcord(this JsonSerializerOptions opts) + public static JsonSerializerOptions ConfigureForMyriad(this JsonSerializerOptions opts) { opts.PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy(); opts.NumberHandling = JsonNumberHandling.AllowReadingFromString; diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index b73a6db3..17c8bfad 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -173,6 +173,8 @@ namespace PluralKit.Bot async Task HandleEventInner() { + await Task.Yield(); + using var _ = LogContext.PushProperty("EventId", Guid.NewGuid()); _logger .ForContext("Elastic", "yes?") diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index b0b56b9e..9b97104d 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -4,31 +4,40 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; - +using Myriad.Cache; +using Myriad.Extensions; +using Myriad.Rest; +using Myriad.Rest.Exceptions; using Myriad.Types; +using Dapper; + +using NodaTime; +using NodaTime.Extensions; +using NodaTime.Text; + using PluralKit.Core; +using Serilog; + namespace PluralKit.Bot { public class LoggerCleanService { - private static readonly Regex _basicRegex = new Regex("(\\d{17,19})"); - private static readonly Regex _dynoRegex = new Regex("Message ID: (\\d{17,19})"); - private static readonly Regex _carlRegex = new Regex("ID: (\\d{17,19})"); - private static readonly Regex _circleRegex = new Regex("\\(`(\\d{17,19})`\\)"); - private static readonly Regex _loggerARegex = new Regex("Message = (\\d{17,19})"); - private static readonly Regex _loggerBRegex = new Regex("MessageID:(\\d{17,19})"); - private static readonly Regex _auttajaRegex = new Regex("Message (\\d{17,19}) deleted"); - private static readonly Regex _mantaroRegex = new Regex("Message \\(?ID:? (\\d{17,19})\\)? created by .* in channel .* was deleted\\."); - private static readonly Regex _pancakeRegex = new Regex("Message from <@(\\d{17,19})> deleted in"); - private static readonly Regex _unbelievaboatRegex = new Regex("Message ID: (\\d{17,19})"); - private static readonly Regex _vanessaRegex = new Regex("Message sent by <@!?(\\d{17,19})> deleted in"); - private static readonly Regex _salRegex = new Regex("\\(ID: (\\d{17,19})\\)"); - private static readonly Regex _GearBotRegex = new Regex("\\(``(\\d{17,19})``\\) in <#\\d{17,19}> has been removed."); - private static readonly Regex _GiselleRegex = new Regex("\\*\\*Message ID\\*\\*: `(\\d{17,19})`"); + private static readonly Regex _basicRegex = new("(\\d{17,19})"); + private static readonly Regex _dynoRegex = new("Message ID: (\\d{17,19})"); + private static readonly Regex _carlRegex = new("ID: (\\d{17,19})"); + private static readonly Regex _circleRegex = new("\\(`(\\d{17,19})`\\)"); + private static readonly Regex _loggerARegex = new("Message = (\\d{17,19})"); + private static readonly Regex _loggerBRegex = new("MessageID:(\\d{17,19})"); + private static readonly Regex _auttajaRegex = new("Message (\\d{17,19}) deleted"); + private static readonly Regex _mantaroRegex = new("Message \\(?ID:? (\\d{17,19})\\)? created by .* in channel .* was deleted\\."); + private static readonly Regex _pancakeRegex = new("Message from <@(\\d{17,19})> deleted in"); + private static readonly Regex _unbelievaboatRegex = new("Message ID: (\\d{17,19})"); + private static readonly Regex _vanessaRegex = new("Message sent by <@!?(\\d{17,19})> deleted in"); + private static readonly Regex _salRegex = new("\\(ID: (\\d{17,19})\\)"); + private static readonly Regex _GearBotRegex = new("\\(``(\\d{17,19})``\\) in <#\\d{17,19}> has been removed."); + private static readonly Regex _GiselleRegex = new("\\*\\*Message ID\\*\\*: `(\\d{17,19})`"); private static readonly Dictionary _bots = new[] { @@ -57,29 +66,35 @@ namespace PluralKit.Bot .ToDictionary(b => b.WebhookName); private readonly IDatabase _db; - private DiscordShardedClient _client; + private readonly DiscordApiClient _client; + private readonly IDiscordCache _cache; + private readonly Bot _bot; // todo: get rid of this nasty + private readonly ILogger _logger; - public LoggerCleanService(IDatabase db, DiscordShardedClient client) + public LoggerCleanService(IDatabase db, DiscordApiClient client, IDiscordCache cache, Bot bot, ILogger logger) { _db = db; _client = client; + _cache = cache; + _bot = bot; + _logger = logger.ForContext(); } public ICollection Bots => _bots.Values; public async ValueTask HandleLoggerBotCleanup(Message msg) { - // TODO: fix!! - /* - if (msg.Channel.Type != ChannelType.Text) return; - if (!msg.Channel.BotHasAllPermissions(Permissions.ManageMessages)) return; + var channel = _cache.GetChannel(msg.ChannelId); + + if (channel.Type != Channel.ChannelType.GuildText) return; + if (!_bot.PermissionsIn(channel.Id).HasFlag(PermissionSet.ManageMessages)) return; // If this message is from a *webhook*, check if the name matches one of the bots we know // TODO: do we need to do a deeper webhook origin check, or would that be too hard on the rate limit? // If it's from a *bot*, check the bot ID to see if we know it. LoggerBot bot = null; - if (msg.WebhookMessage) _botsByWebhookName.TryGetValue(msg.Author.Username, out bot); - else if (msg.Author.IsBot) _bots.TryGetValue(msg.Author.Id, out bot); + if (msg.WebhookId != null) _botsByWebhookName.TryGetValue(msg.Author.Username, out bot); + else if (msg.Author.Bot) _bots.TryGetValue(msg.Author.Id, out bot); // If we didn't find anything before, or what we found is an unsupported bot, bail if (bot == null) return; @@ -96,33 +111,43 @@ namespace PluralKit.Bot // either way but shouldn't be too much, given it's constrained by user ID and guild. var fuzzy = bot.FuzzyExtractFunc(msg); if (fuzzy == null) return; - + + _logger.Debug("Fuzzy logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}", + bot.Name, msg.Id, fuzzy); + var mid = await _db.Execute(conn => conn.QuerySingleOrDefaultAsync( "select mid from messages where sender = @User and mid > @ApproxID and guild = @Guild limit 1", new { fuzzy.Value.User, - Guild = msg.Channel.GuildId, + Guild = msg.GuildId, ApproxId = DiscordUtils.InstantToSnowflake( - fuzzy.Value.ApproxTimestamp - TimeSpan.FromSeconds(3)) + fuzzy.Value.ApproxTimestamp - Duration.FromSeconds(3)) })); - if (mid == null) return; // If we didn't find a corresponding message, bail + + // If we didn't find a corresponding message, bail + if (mid == null) + return; + // Otherwise, we can *reasonably assume* that this is a logged deletion, so delete the log message. - await msg.DeleteAsync(); + await _client.DeleteMessage(msg.ChannelId, msg.Id); } else if (bot.ExtractFunc != null) { // Other bots give us the message ID itself, and we can just extract that from the database directly. var extractedId = bot.ExtractFunc(msg); if (extractedId == null) return; // If we didn't find anything, bail. + + _logger.Debug("Pure logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}", + bot.Name, msg.Id, extractedId); var mid = await _db.Execute(conn => conn.QuerySingleOrDefaultAsync( "select mid from messages where original_mid = @Mid", new {Mid = extractedId.Value})); if (mid == null) return; // If we've gotten this far, we found a logged deletion of a trigger message. Just yeet it! - await msg.DeleteAsync(); + await _client.DeleteMessage(msg.ChannelId, msg.Id); } // else should not happen, but idk, it might } catch (NotFoundException) @@ -131,10 +156,9 @@ namespace PluralKit.Bot // The only thing I can think of that'd cause this are the DeleteAsync() calls which 404 when // the message doesn't exist anyway - so should be safe to just ignore it, right? } - */ } - private static ulong? ExtractAuttaja(DiscordMessage msg) + private static ulong? ExtractAuttaja(Message msg) { // Auttaja has an optional "compact mode" that logs without embeds // That one puts the ID in the message content, non-compact puts it in the embed description. @@ -146,7 +170,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractDyno(DiscordMessage msg) + private static ulong? ExtractDyno(Message msg) { // Embed *description* contains "Message sent by [mention] deleted in [channel]", contains message ID in footer per regex var embed = msg.Embeds.FirstOrDefault(); @@ -155,7 +179,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractLoggerA(DiscordMessage msg) + private static ulong? ExtractLoggerA(Message msg) { // This is for Logger#6088 (298822483060981760), distinct from Logger#6278 (327424261180620801). // Embed contains title "Message deleted in [channel]", and an ID field containing both message and user ID (see regex). @@ -169,7 +193,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractLoggerB(DiscordMessage msg) + private static ulong? ExtractLoggerB(Message msg) { // This is for Logger#6278 (327424261180620801), distinct from Logger#6088 (298822483060981760). // Embed title ends with "A Message Was Deleted!", footer contains message ID as per regex. @@ -179,7 +203,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractGenericBot(DiscordMessage msg) + private static ulong? ExtractGenericBot(Message msg) { // Embed, title is "Message Deleted", ID plain in footer. var embed = msg.Embeds.FirstOrDefault(); @@ -188,7 +212,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractBlargBot(DiscordMessage msg) + private static ulong? ExtractBlargBot(Message msg) { // Embed, title ends with "Message Deleted", contains ID plain in a field. var embed = msg.Embeds.FirstOrDefault(); @@ -198,7 +222,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static ulong? ExtractMantaro(DiscordMessage msg) + private static ulong? ExtractMantaro(Message msg) { // Plain message, "Message (ID: [id]) created by [user] (ID: [id]) in channel [channel] was deleted. if (!(msg.Content?.Contains("was deleted.") ?? false)) return null; @@ -206,7 +230,7 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static FuzzyExtractResult? ExtractCarlBot(DiscordMessage msg) + private static FuzzyExtractResult? ExtractCarlBot(Message msg) { // Embed, title is "Message deleted in [channel], **user** ID in the footer, timestamp as, well, timestamp in embed. // This is the *deletion* timestamp, which we can assume is a couple seconds at most after the message was originally sent @@ -214,17 +238,21 @@ namespace PluralKit.Bot if (embed?.Footer == null || embed.Timestamp == null || !(embed.Title?.StartsWith("Message deleted in") ?? false)) return null; var match = _carlRegex.Match(embed.Footer.Text ?? ""); return match.Success - ? new FuzzyExtractResult { User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = embed.Timestamp.Value } + ? new FuzzyExtractResult + { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = OffsetDateTimePattern.Rfc3339.Parse(embed.Timestamp).GetValueOrThrow().ToInstant() + } : (FuzzyExtractResult?) null; } - private static FuzzyExtractResult? ExtractCircle(DiscordMessage msg) + private static FuzzyExtractResult? ExtractCircle(Message msg) { // Like Auttaja, Circle has both embed and compact modes, but the regex works for both. // Compact: "Message from [user] ([id]) deleted in [channel]", no timestamp (use message time) // Embed: Message Author field: "[user] ([id])", then an embed timestamp string stringWithId = msg.Content; - if (msg.Embeds.Count > 0) + if (msg.Embeds.Length > 0) { var embed = msg.Embeds.First(); if (embed.Author?.Name == null || !embed.Author.Name.StartsWith("Message Deleted in")) return null; @@ -236,11 +264,14 @@ namespace PluralKit.Bot var match = _circleRegex.Match(stringWithId); return match.Success - ? new FuzzyExtractResult {User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = msg.Timestamp} + ? new FuzzyExtractResult { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = msg.Timestamp().ToInstant() + } : (FuzzyExtractResult?) null; } - private static FuzzyExtractResult? ExtractPancake(DiscordMessage msg) + private static FuzzyExtractResult? ExtractPancake(Message msg) { // Embed, author is "Message Deleted", description includes a mention, timestamp is *message send time* (but no ID) // so we use the message timestamp to get somewhere *after* the message was proxied @@ -248,11 +279,15 @@ namespace PluralKit.Bot if (embed?.Description == null || embed.Author?.Name != "Message Deleted") return null; var match = _pancakeRegex.Match(embed.Description); return match.Success - ? new FuzzyExtractResult {User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = msg.Timestamp} + ? new FuzzyExtractResult + { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = msg.Timestamp().ToInstant() + } : (FuzzyExtractResult?) null; } - private static ulong? ExtractUnbelievaBoat(DiscordMessage msg) + private static ulong? ExtractUnbelievaBoat(Message msg) { // Embed author is "Message Deleted", footer contains message ID per regex var embed = msg.Embeds.FirstOrDefault(); @@ -261,18 +296,22 @@ namespace PluralKit.Bot return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; } - private static FuzzyExtractResult? ExtractVanessa(DiscordMessage msg) + private static FuzzyExtractResult? ExtractVanessa(Message msg) { // Title is "Message Deleted", embed description contains mention var embed = msg.Embeds.FirstOrDefault(); if (embed?.Title == null || embed.Title != "Message Deleted" || embed.Description == null) return null; var match = _vanessaRegex.Match(embed.Description); return match.Success - ? new FuzzyExtractResult {User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = msg.Timestamp} + ? new FuzzyExtractResult + { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = msg.Timestamp().ToInstant() + } : (FuzzyExtractResult?) null; } - private static FuzzyExtractResult? ExtractSAL(DiscordMessage msg) + private static FuzzyExtractResult? ExtractSAL(Message msg) { // Title is "Message Deleted!", field "Message Author" contains ID var embed = msg.Embeds.FirstOrDefault(); @@ -281,22 +320,30 @@ namespace PluralKit.Bot if (authorField == null) return null; var match = _salRegex.Match(authorField.Value); return match.Success - ? new FuzzyExtractResult {User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = msg.Timestamp} + ? new FuzzyExtractResult + { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = msg.Timestamp().ToInstant() + } : (FuzzyExtractResult?) null; } - private static FuzzyExtractResult? ExtractGearBot(DiscordMessage msg) + private static FuzzyExtractResult? ExtractGearBot(Message msg) { // Simple text based message log. // No message ID, but we have timestamp and author ID. // Not using timestamp here though (seems to be same as message timestamp), might be worth implementing in the future. var match = _GearBotRegex.Match(msg.Content); return match.Success - ? new FuzzyExtractResult {User = ulong.Parse(match.Groups[1].Value), ApproxTimestamp = msg.Timestamp} + ? new FuzzyExtractResult + { + User = ulong.Parse(match.Groups[1].Value), + ApproxTimestamp = msg.Timestamp().ToInstant() + } : (FuzzyExtractResult?) null; } - private static ulong? ExtractGiselleBot(DiscordMessage msg) + private static ulong? ExtractGiselleBot(Message msg) { var embed = msg.Embeds.FirstOrDefault(); if (embed?.Title == null || embed.Title != "🗑 Message Deleted") return null; @@ -308,11 +355,11 @@ namespace PluralKit.Bot { public string Name; public ulong Id; - public Func ExtractFunc; - public Func FuzzyExtractFunc; + public Func ExtractFunc; + public Func FuzzyExtractFunc; public string WebhookName; - public LoggerBot(string name, ulong id, Func extractFunc = null, Func fuzzyExtractFunc = null, string webhookName = null) + public LoggerBot(string name, ulong id, Func extractFunc = null, Func fuzzyExtractFunc = null, string webhookName = null) { Name = name; Id = id; @@ -324,8 +371,8 @@ namespace PluralKit.Bot public struct FuzzyExtractResult { - public ulong User; - public DateTimeOffset ApproxTimestamp; + public ulong User { get; set; } + public Instant ApproxTimestamp { get; set; } } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/ShardInfoService.cs b/PluralKit.Bot/Services/ShardInfoService.cs index a89a0bd2..d35cc299 100644 --- a/PluralKit.Bot/Services/ShardInfoService.cs +++ b/PluralKit.Bot/Services/ShardInfoService.cs @@ -1,12 +1,11 @@ +using System; using System.Collections.Generic; using System.Linq; +using System.Net.WebSockets; using System.Threading.Tasks; using App.Metrics; -using DSharpPlus; -using DSharpPlus.EventArgs; - using Myriad.Gateway; using NodaTime; @@ -16,6 +15,8 @@ using Serilog; namespace PluralKit.Bot { + // TODO: how much of this do we need now that we have logging in the shard library? + // A lot could probably be cleaned up... public class ShardInfoService { public class ShardInfo @@ -30,10 +31,10 @@ namespace PluralKit.Bot private readonly IMetrics _metrics; private readonly ILogger _logger; - private readonly DiscordShardedClient _client; - private readonly Dictionary _shardInfo = new Dictionary(); + private readonly Cluster _client; + private readonly Dictionary _shardInfo = new(); - public ShardInfoService(ILogger logger, DiscordShardedClient client, IMetrics metrics) + public ShardInfoService(ILogger logger, Cluster client, IMetrics metrics) { _client = client; _metrics = metrics; @@ -44,7 +45,7 @@ namespace PluralKit.Bot { // We initialize this before any shards are actually created and connected // This means the client won't know the shard count, so we attach a listener every time a shard gets connected - _client.SocketOpened += (_, __) => RefreshShardList(); + _client.ShardCreated += InitializeShard; } private void ReportShardStatus() @@ -54,44 +55,40 @@ namespace PluralKit.Bot _metrics.Measure.Gauge.SetValue(BotMetrics.ShardsConnected, _shardInfo.Count(s => s.Value.Connected)); } - private async Task RefreshShardList() + private void InitializeShard(Shard shard) { - // This callback doesn't actually receive the shard that was opening, so we just try to check we have 'em all (so far) - foreach (var (id, shard) in _client.ShardClients) + // Get or insert info in the client dict + if (_shardInfo.TryGetValue(shard.ShardId, out var info)) { - // Get or insert info in the client dict - if (_shardInfo.TryGetValue(id, out var info)) - { - // Skip adding listeners if we've seen this shard & already added listeners to it - if (info.HasAttachedListeners) continue; - } else _shardInfo[id] = info = new ShardInfo(); + // Skip adding listeners if we've seen this shard & already added listeners to it + if (info.HasAttachedListeners) + return; + } else _shardInfo[shard.ShardId] = info = new ShardInfo(); + + // Call our own SocketOpened listener manually (and then attach the listener properly) + SocketOpened(shard); + shard.SocketOpened += () => SocketOpened(shard); + // Register listeners for new shards + _logger.Information("Attaching listeners to new shard #{Shard}", shard.ShardId); + shard.Resumed += () => Resumed(shard); + shard.Ready += () => Ready(shard); + shard.SocketClosed += (closeStatus, message) => SocketClosed(shard, closeStatus, message); + shard.HeartbeatReceived += latency => Heartbeated(shard, latency); - // Call our own SocketOpened listener manually (and then attach the listener properly) - await SocketOpened(shard, null); - shard.SocketOpened += SocketOpened; - - // Register listeners for new shards - _logger.Information("Attaching listeners to new shard #{Shard}", shard.ShardId); - shard.Resumed += Resumed; - shard.Ready += Ready; - shard.SocketClosed += SocketClosed; - shard.Heartbeated += Heartbeated; - - // Register that we've seen it - info.HasAttachedListeners = true; - } + // Register that we've seen it + info.HasAttachedListeners = true; + } - private Task SocketOpened(DiscordClient shard, SocketEventArgs _) + private void SocketOpened(Shard shard) { // We do nothing else here, since this kinda doesn't mean *much*? It's only really started once we get Ready/Resumed // And it doesn't get fired first time around since we don't have time to add the event listener before it's fired' _logger.Information("Shard #{Shard} opened socket", shard.ShardId); - return Task.CompletedTask; } - private ShardInfo TryGetShard(DiscordClient shard) + private ShardInfo TryGetShard(Shard shard) { // If we haven't seen this shard before, add it to the dict! // I don't think this will ever occur since the shard number is constant up-front and we handle those @@ -101,7 +98,7 @@ namespace PluralKit.Bot return info; } - private Task Resumed(DiscordClient shard, ReadyEventArgs e) + private void Resumed(Shard shard) { _logger.Information("Shard #{Shard} resumed connection", shard.ShardId); @@ -109,10 +106,9 @@ namespace PluralKit.Bot // info.LastConnectionTime = SystemClock.Instance.GetCurrentInstant(); info.Connected = true; ReportShardStatus(); - return Task.CompletedTask; } - private Task Ready(DiscordClient shard, ReadyEventArgs e) + private void Ready(Shard shard) { _logger.Information("Shard #{Shard} sent Ready event", shard.ShardId); @@ -120,30 +116,28 @@ namespace PluralKit.Bot info.LastConnectionTime = SystemClock.Instance.GetCurrentInstant(); info.Connected = true; ReportShardStatus(); - return Task.CompletedTask; } - private Task SocketClosed(DiscordClient shard, SocketCloseEventArgs e) + private void SocketClosed(Shard shard, WebSocketCloseStatus closeStatus, string message) { - _logger.Warning("Shard #{Shard} disconnected ({CloseCode}: {CloseMessage})", shard.ShardId, e.CloseCode, e.CloseMessage); + _logger.Warning("Shard #{Shard} disconnected ({CloseCode}: {CloseMessage})", + shard.ShardId, closeStatus, message); var info = TryGetShard(shard); info.DisconnectionCount++; info.Connected = false; ReportShardStatus(); - return Task.CompletedTask; } - private Task Heartbeated(DiscordClient shard, HeartbeatEventArgs e) + private void Heartbeated(Shard shard, TimeSpan latency) { - var latency = Duration.FromMilliseconds(e.Ping); - _logger.Information("Shard #{Shard} received heartbeat (latency: {Latency} ms)", shard.ShardId, latency.Milliseconds); + _logger.Information("Shard #{Shard} received heartbeat (latency: {Latency} ms)", + shard.ShardId, latency.Milliseconds); var info = TryGetShard(shard); - info.LastHeartbeatTime = e.Timestamp.ToInstant(); + info.LastHeartbeatTime = SystemClock.Instance.GetCurrentInstant(); info.Connected = true; - info.ShardLatency = latency; - return Task.CompletedTask; + info.ShardLatency = latency.ToDuration(); } public ShardInfo GetShardInfo(Shard shard) => _shardInfo[shard.ShardId]; From 0c1bb6cc6adb691c36d5aa61598463d7e1102f2d Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 14:42:28 +0100 Subject: [PATCH 011/608] Convert message update handler --- Myriad/Gateway/Events/MessageUpdateEvent.cs | 6 +- PluralKit.Bot/Handlers/MessageEdited.cs | 63 ++++++++++++++------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Myriad/Gateway/Events/MessageUpdateEvent.cs b/Myriad/Gateway/Events/MessageUpdateEvent.cs index 63b34c1d..0bd1293b 100644 --- a/Myriad/Gateway/Events/MessageUpdateEvent.cs +++ b/Myriad/Gateway/Events/MessageUpdateEvent.cs @@ -1,10 +1,14 @@ -using Myriad.Utils; +using Myriad.Types; +using Myriad.Utils; namespace Myriad.Gateway { public record MessageUpdateEvent(ulong Id, ulong ChannelId): IGatewayEvent { public Optional Content { get; init; } + public Optional Author { get; init; } + public Optional Member { get; init; } + public Optional Attachments { get; init; } // TODO: lots of partials } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index a88e271f..a00f7b22 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -1,10 +1,12 @@ +using System; using System.Threading.Tasks; using App.Metrics; -using DSharpPlus; - +using Myriad.Cache; +using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Types; using PluralKit.Core; @@ -18,9 +20,11 @@ namespace PluralKit.Bot private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly IMetrics _metrics; - private readonly DiscordShardedClient _client; + private readonly Cluster _client; + private readonly IDiscordCache _cache; + private readonly Bot _bot; - public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, DiscordShardedClient client) + public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot) { _lastMessageCache = lastMessageCache; _proxy = proxy; @@ -28,25 +32,46 @@ namespace PluralKit.Bot _metrics = metrics; _repo = repo; _client = client; + _cache = cache; + _bot = bot; } public async Task Handle(Shard shard, MessageUpdateEvent evt) { - // TODO: fix - // if (evt.Author?.Id == _client.CurrentUser?.Id) return; - // - // // Edit message events sometimes arrive with missing data; double-check it's all there - // if (evt.Message.Content == null || evt.Author == null || evt.Channel.Guild == null) return; - // - // // Only react to the last message in the channel - // if (_lastMessageCache.GetLastMessage(evt.Channel.Id) != evt.Message.Id) return; - // - // // Just run the normal message handling code, with a flag to disable autoproxying - // MessageContext ctx; - // await using (var conn = await _db.Obtain()) - // using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) - // ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.Channel.GuildId, evt.Channel.Id); - // await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: false); + if (evt.Author.Value?.Id == _client.User?.Id) return; + + // Edit message events sometimes arrive with missing data; double-check it's all there + if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue) + return; + + var channel = _cache.GetChannel(evt.ChannelId); + if (channel.Type != Channel.ChannelType.GuildText) + return; + var guild = _cache.GetGuild(channel.GuildId!.Value); + + // Only react to the last message in the channel + if (_lastMessageCache.GetLastMessage(evt.ChannelId) != evt.Id) + return; + + // Just run the normal message handling code, with a flag to disable autoproxying + MessageContext ctx; + await using (var conn = await _db.Obtain()) + using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) + ctx = await _repo.GetMessageContext(conn, evt.Author.Value!.Id, channel.GuildId!.Value, evt.ChannelId); + + // TODO: is this missing anything? + var equivalentEvt = new MessageCreateEvent + { + Id = evt.Id, + ChannelId = evt.ChannelId, + GuildId = channel.GuildId, + Author = evt.Author.Value, + Member = evt.Member.Value, + Content = evt.Content.Value, + Attachments = evt.Attachments.Value ?? Array.Empty() + }; + var botPermissions = _bot.PermissionsIn(channel.Id); + await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy: false, guild: guild, channel: channel, botPermissions: botPermissions); } } } \ No newline at end of file From e06a6ecf85422b63d9c56892016ca89b0fc7efd0 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 14:44:37 +0100 Subject: [PATCH 012/608] Remove now-unused DiscordUtils functions --- PluralKit.Bot/Utils/DiscordUtils.cs | 129 +--------------------------- 1 file changed, 4 insertions(+), 125 deletions(-) diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 1581f689..69bf51e6 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,7 +9,6 @@ using System.Threading.Tasks; using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; using Myriad.Builders; using Myriad.Extensions; @@ -49,11 +47,6 @@ namespace PluralKit.Bot private static readonly FieldInfo _roleIdsField = typeof(DiscordMember).GetField("_role_ids", BindingFlags.NonPublic | BindingFlags.Instance); - - public static string NameAndMention(this DiscordUser user) - { - return $"{user.Username}#{user.Discriminator} ({user.Mention})"; - } public static string NameAndMention(this User user) { @@ -105,19 +98,13 @@ namespace PluralKit.Bot if (channel.Type == ChannelType.Private) return DM_PERMISSIONS; return Permissions.None; } - - public static bool BotHasAllPermissions(this DiscordChannel channel, Permissions permissionSet) => - (BotPermissions(channel) & permissionSet) == permissionSet; - + public static Instant SnowflakeToInstant(ulong snowflake) => Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22); public static ulong InstantToSnowflake(Instant time) => (ulong) (time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22; - - public static ulong InstantToSnowflake(DateTimeOffset time) => - (ulong) (time - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalMilliseconds << 22; - + public static async Task CreateReactionsBulk(this DiscordApiClient rest, Message msg, string[] reactions) { foreach (var reaction in reactions) @@ -131,29 +118,7 @@ namespace PluralKit.Bot // Workaround for https://github.com/DSharpPlus/DSharpPlus/issues/565 return input?.Replace("%20", "+"); } - - public static Task SendMessageFixedAsync(this DiscordChannel channel, string content = null, - DiscordEmbed embed = null, - IEnumerable mentions = null) => - // Passing an empty list blocks all mentions by default (null allows all through) - 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) - // It's just here for consistency so we don't use the standard SendMessageAsync method >.> - public static Task SendMessageFixedAsync(this DiscordMember member, string content = null, - DiscordEmbed embed = null) => - member.SendMessageAsync(content, embed: embed); - - public static bool TryGetCachedUser(this DiscordClient client, ulong id, out DiscordUser user) - { - user = null; - - var cache = (ConcurrentDictionary) typeof(BaseDiscordClient) - .GetProperty("UserCache", BindingFlags.Instance | BindingFlags.NonPublic) - ?.GetValue(client); - return cache != null && cache.TryGetValue(id, out user); - } - + public static uint? ToDiscordColor(this string color) { if (uint.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) @@ -244,93 +209,7 @@ namespace PluralKit.Bot // So, surrounding with two backticks, then escaping all backtick pairs makes it impossible(!) to "break out" return $"``{EscapeBacktickPair(input)}``"; } - - public static Task GetUser(this DiscordRestClient client, ulong id) => - WrapDiscordCall(client.GetUserAsync(id)); - - public static Task GetUser(this DiscordClient client, ulong id) => - WrapDiscordCall(client.GetUserAsync(id)); - - public static Task GetChannel(this DiscordRestClient client, ulong id) => - WrapDiscordCall(client.GetChannelAsync(id)); - - public static Task GetChannel(this DiscordClient client, ulong id) => - WrapDiscordCall(client.GetChannelAsync(id)); - - public static Task GetGuild(this DiscordRestClient client, ulong id) => - WrapDiscordCall(client.GetGuildAsync(id)); - - public static Task GetGuild(this DiscordClient client, ulong id) => - WrapDiscordCall(client.GetGuildAsync(id)); - - public static Task GetMember(this DiscordRestClient client, ulong guild, ulong user) - { - async Task Inner() => - await (await client.GetGuildAsync(guild)).GetMemberAsync(user); - - return WrapDiscordCall(Inner()); - } - - public static Task GetMember(this DiscordClient client, ulong guild, ulong user) - { - async Task Inner() => - await (await client.GetGuildAsync(guild)).GetMemberAsync(user); - - return WrapDiscordCall(Inner()); - } - - public static Task GetMember(this DiscordGuild guild, ulong user) => - WrapDiscordCall(guild.GetMemberAsync(user)); - - public static Task GetMessage(this DiscordChannel channel, ulong id) => - WrapDiscordCall(channel.GetMessageAsync(id)); - - public static Task GetMessage(this DiscordRestClient client, ulong channel, ulong message) => - WrapDiscordCall(client.GetMessageAsync(channel, message)); - - public static DiscordGuild GetGuild(this DiscordShardedClient client, ulong id) - { - DiscordGuild guild; - foreach (DiscordClient shard in client.ShardClients.Values) - { - shard.Guilds.TryGetValue(id, out guild); - if (guild != null) return guild; - } - - return null; - } - - public static async Task 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 - if (guildId == null) - { - var channel = await WrapDiscordCall(client.ShardClients.Values.FirstOrDefault().GetChannelAsync(id)); - 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 - } - - return client.GetGuild(guildId.Value).GetChannel(id); - } - - private static async Task WrapDiscordCall(Task t) - where T: class - { - try - { - return await t; - } - catch (NotFoundException) - { - return null; - } - catch (UnauthorizedException) - { - return null; - } - } - + public static EmbedBuilder WithSimpleLineContent(this EmbedBuilder eb, IEnumerable lines) { static int CharacterLimit(int pageNumber) => From 5a52abed77ad2d85d07f5f2680596dea7933a07d Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 14:50:10 +0100 Subject: [PATCH 013/608] Convert Sentry enrichers --- Myriad/Gateway/Events/MessageUpdateEvent.cs | 1 + PluralKit.Bot/Utils/DiscordUtils.cs | 53 +------------ PluralKit.Bot/Utils/SentryUtils.cs | 83 +++++++++++---------- 3 files changed, 45 insertions(+), 92 deletions(-) diff --git a/Myriad/Gateway/Events/MessageUpdateEvent.cs b/Myriad/Gateway/Events/MessageUpdateEvent.cs index 0bd1293b..09ef4316 100644 --- a/Myriad/Gateway/Events/MessageUpdateEvent.cs +++ b/Myriad/Gateway/Events/MessageUpdateEvent.cs @@ -9,6 +9,7 @@ namespace Myriad.Gateway public Optional Author { get; init; } public Optional Member { get; init; } public Optional Attachments { get; init; } + public Optional GuildId { get; init; } // TODO: lots of partials } } \ No newline at end of file diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 69bf51e6..28b21ae0 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -30,9 +30,7 @@ namespace PluralKit.Bot public const uint Green = 0x00cc78; public const uint Red = 0xef4b3d; public const uint Gray = 0x979c9f; - - 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 ROLE_MENTION = new Regex("<@&(\\d{17,19})>"); private static readonly Regex EVERYONE_HERE_MENTION = new Regex("@(everyone|here)"); @@ -44,60 +42,11 @@ namespace PluralKit.Bot // corresponding to: https://github.com/Khan/simple-markdown/blob/master/src/index.js#L1489 // I added ? at the start/end; they need to be handled specially later... private static readonly Regex UNBROKEN_LINK_REGEX = new Regex("?"); - - private static readonly FieldInfo _roleIdsField = - typeof(DiscordMember).GetField("_role_ids", BindingFlags.NonPublic | BindingFlags.Instance); public static string NameAndMention(this User user) { return $"{user.Username}#{user.Discriminator} ({user.Mention()})"; } - - // We funnel all "permissions from DiscordMember" calls through here - // This way we can ensure we do the read permission correction everywhere - private static Permissions PermissionsInGuild(DiscordChannel channel, DiscordMember member) - { - ValidateCachedRoles(member); - var permissions = channel.PermissionsFor(member); - - // 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 ((permissions & Permissions.AccessChannels) != Permissions.AccessChannels) - return Permissions.None; - - return permissions; - } - - // Workaround for DSP internal error - private static void ValidateCachedRoles(DiscordMember member) - { - var roleIdCache = _roleIdsField.GetValue(member) as List; - var currentRoleIds = member.Roles.Where(x => x != null).Select(x => x.Id); - var invalidRoleIds = roleIdCache.Where(x => !currentRoleIds.Contains(x)).ToList(); - roleIdCache.RemoveAll(x => invalidRoleIds.Contains(x)); - } - - - // 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) - { - if (channel.Guild != null && !(user is DiscordMember)) - throw new ArgumentException("Function was passed a guild channel but a non-member DiscordUser"); - - if (user is DiscordMember m) return PermissionsInGuild(channel, m); - if (channel.Type == ChannelType.Private) return DM_PERMISSIONS; - return Permissions.None; - } - - public static Permissions BotPermissions(this DiscordChannel channel) - { - // TODO: can we get a CurrentMember somehow without a guild context? - // at least, without somehow getting a DiscordClient reference as an arg(which I don't want to do) - if (channel.Guild != null) - return PermissionsInSync(channel, channel.Guild.CurrentMember); - if (channel.Type == ChannelType.Private) return DM_PERMISSIONS; - return Permissions.None; - } public static Instant SnowflakeToInstant(ulong snowflake) => Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22); diff --git a/PluralKit.Bot/Utils/SentryUtils.cs b/PluralKit.Bot/Utils/SentryUtils.cs index 0d9cfb19..ed1bf2f5 100644 --- a/PluralKit.Bot/Utils/SentryUtils.cs +++ b/PluralKit.Bot/Utils/SentryUtils.cs @@ -1,9 +1,6 @@ using System.Collections.Generic; -using System.Linq; - -using DSharpPlus; -using DSharpPlus.EventArgs; +using Myriad.Extensions; using Myriad.Gateway; using Sentry; @@ -15,82 +12,88 @@ namespace PluralKit.Bot void Enrich(Scope scope, Shard shard, T evt); } - public class SentryEnricher //: - // TODO!!! - // ISentryEnricher, - // ISentryEnricher, - // ISentryEnricher, - // ISentryEnricher, - // ISentryEnricher + public class SentryEnricher: + ISentryEnricher, + ISentryEnricher, + ISentryEnricher, + ISentryEnricher, + ISentryEnricher { + private readonly Bot _bot; + + public SentryEnricher(Bot bot) + { + _bot = bot; + } + // TODO: should this class take the Scope by dependency injection instead? // Would allow us to create a centralized "chain of handlers" where this class could just be registered as an entry in - public void Enrich(Scope scope, Shard shard, MessageCreateEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageCreateEvent evt) { - scope.AddBreadcrumb(evt.Message.Content, "event.message", data: new Dictionary + scope.AddBreadcrumb(evt.Content, "event.message", data: new Dictionary { {"user", evt.Author.Id.ToString()}, - {"channel", evt.Channel.Id.ToString()}, - {"guild", evt.Channel.GuildId.ToString()}, - {"message", evt.Message.Id.ToString()}, + {"channel", evt.ChannelId.ToString()}, + {"guild", evt.GuildId.ToString()}, + {"message", evt.Id.ToString()}, }); - scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo.ShardId.ToString()); // Also report information about the bot's permissions in the channel // We get a lot of permission errors so this'll be useful for determining problems - var perms = evt.Channel.BotPermissions(); + var perms = _bot.PermissionsIn(evt.ChannelId); scope.AddBreadcrumb(perms.ToPermissionString(), "permissions"); } - public void Enrich(Scope scope, Shard shard, MessageDeleteEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageDeleteEvent evt) { scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() { - {"channel", evt.Channel.Id.ToString()}, - {"guild", evt.Channel.GuildId.ToString()}, - {"message", evt.Message.Id.ToString()}, + {"channel", evt.ChannelId.ToString()}, + {"guild", evt.GuildId.ToString()}, + {"message", evt.Id.ToString()}, }); - scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo.ShardId.ToString()); } - public void Enrich(Scope scope, Shard shard, MessageUpdateEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageUpdateEvent evt) { - scope.AddBreadcrumb(evt.Message.Content ?? "", "event.messageEdit", + scope.AddBreadcrumb(evt.Content.Value ?? "", "event.messageEdit", data: new Dictionary() { - {"channel", evt.Channel.Id.ToString()}, - {"guild", evt.Channel.GuildId.ToString()}, - {"message", evt.Message.Id.ToString()} + {"channel", evt.ChannelId.ToString()}, + {"guild", evt.GuildId.Value.ToString()}, + {"message", evt.Id.ToString()} }); - scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo.ShardId.ToString()); } - public void Enrich(Scope scope, Shard shard, MessageBulkDeleteEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageDeleteBulkEvent evt) { scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() { - {"channel", evt.Channel.Id.ToString()}, - {"guild", evt.Channel.Id.ToString()}, - {"messages", string.Join(",", evt.Messages.Select(m => m.Id))}, + {"channel", evt.ChannelId.ToString()}, + {"guild", evt.GuildId.ToString()}, + {"messages", string.Join(",", evt.Ids)}, }); - scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo.ShardId.ToString()); } - public void Enrich(Scope scope, Shard shard, MessageReactionAddEventArgs evt) + public void Enrich(Scope scope, Shard shard, MessageReactionAddEvent evt) { scope.AddBreadcrumb("", "event.reaction", data: new Dictionary() { - {"user", evt.User.Id.ToString()}, - {"channel", (evt.Channel?.Id ?? 0).ToString()}, - {"guild", (evt.Channel?.GuildId ?? 0).ToString()}, - {"message", evt.Message.Id.ToString()}, + {"user", evt.UserId.ToString()}, + {"channel", evt.ChannelId.ToString()}, + {"guild", (evt.GuildId ?? 0).ToString()}, + {"message", evt.MessageId.ToString()}, {"reaction", evt.Emoji.Name} }); - scope.SetTag("shard", shard.ShardInfo?.ShardId.ToString()); + scope.SetTag("shard", shard.ShardInfo.ShardId.ToString()); } } } \ No newline at end of file From 227d68a2a4e640bf3b0766eb31b9149d86e4a329 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 14:55:57 +0100 Subject: [PATCH 014/608] Convert event destructuring --- PluralKit.Bot/Init.cs | 3 +- PluralKit.Bot/Tracing/EventDestructuring.cs | 40 ++++++++++----------- PluralKit.Bot/Utils/DiscordUtils.cs | 12 ++----- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/PluralKit.Bot/Init.cs b/PluralKit.Bot/Init.cs index 630fd297..f4b7c6f5 100644 --- a/PluralKit.Bot/Init.cs +++ b/PluralKit.Bot/Init.cs @@ -120,7 +120,8 @@ namespace PluralKit.Bot builder.RegisterModule(new ConfigModule("Bot")); builder.RegisterModule(new LoggingModule("bot", cfg => { - cfg.Destructure.With(); + // TODO: do we need this? + // cfg.Destructure.With(); })); builder.RegisterModule(new MetricsModule()); builder.RegisterModule(); diff --git a/PluralKit.Bot/Tracing/EventDestructuring.cs b/PluralKit.Bot/Tracing/EventDestructuring.cs index fcc655bf..e685c43f 100644 --- a/PluralKit.Bot/Tracing/EventDestructuring.cs +++ b/PluralKit.Bot/Tracing/EventDestructuring.cs @@ -1,19 +1,19 @@ using System.Collections.Generic; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; +using Myriad.Gateway; using Serilog.Core; using Serilog.Events; namespace PluralKit.Bot { + // This class is unused and commented out in Init.cs - it's here from before the lib conversion. Is it needed?? public class EventDestructuring: IDestructuringPolicy { public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) { - if (!(value is DiscordEventArgs dea)) + if (!(value is IGatewayEvent evt)) { result = null; return false; @@ -21,30 +21,30 @@ namespace PluralKit.Bot var props = new List { - new LogEventProperty("Type", new ScalarValue(dea.EventType())), + new("Type", new ScalarValue(evt.EventType())), }; - void AddMessage(DiscordMessage msg) + void AddMessage(ulong id, ulong channelId, ulong? guildId, ulong? author) { - 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))); + props.Add(new LogEventProperty("MessageId", new ScalarValue(id))); + props.Add(new LogEventProperty("ChannelId", new ScalarValue(channelId))); + props.Add(new LogEventProperty("GuildId", new ScalarValue(guildId ?? 0))); - if (msg.Author != null) - props.Add(new LogEventProperty("AuthorId", new ScalarValue(msg.Author.Id))); + if (author != null) + props.Add(new LogEventProperty("AuthorId", new ScalarValue(author))); } - 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) + if (value is MessageCreateEvent mc) + AddMessage(mc.Id, mc.ChannelId, mc.GuildId, mc.Author.Id); + else if (value is MessageUpdateEvent mu) + AddMessage(mu.Id, mu.ChannelId, mu.GuildId.Value, mu.Author.Value?.Id); + else if (value is MessageDeleteEvent md) + AddMessage(md.Id, md.ChannelId, md.GuildId, null); + else if (value is MessageReactionAddEvent mra) { - AddMessage(mra.Message); - props.Add(new LogEventProperty("ReactingUserId", new ScalarValue(mra.User.Id))); - props.Add(new LogEventProperty("Emoji", new ScalarValue(mra.Emoji.GetDiscordName()))); + AddMessage(mra.MessageId, mra.ChannelId, mra.GuildId, null); + props.Add(new LogEventProperty("ReactingUserId", new ScalarValue(mra.Emoji))); + props.Add(new LogEventProperty("Emoji", new ScalarValue(mra.Emoji.Name))); } // Want shard last, just for visual reasons diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 28b21ae0..c581c12b 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -2,16 +2,12 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - using Myriad.Builders; using Myriad.Extensions; +using Myriad.Gateway; using Myriad.Rest; using Myriad.Rest.Types; using Myriad.Types; @@ -20,8 +16,6 @@ using NodaTime; using PluralKit.Core; -using Permissions = DSharpPlus.Permissions; - namespace PluralKit.Bot { public static class DiscordUtils @@ -190,7 +184,7 @@ namespace PluralKit.Bot return $"<{match.Value}>"; }); - public static string EventType(this DiscordEventArgs evt) => - evt.GetType().Name.Replace("EventArgs", ""); + public static string EventType(this IGatewayEvent evt) => + evt.GetType().Name.Replace("Event", ""); } } From b48a77df8dd2391f989b74519bab6ad44a6c8287 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 14:59:45 +0100 Subject: [PATCH 015/608] Convert periodic stat collector --- .../Services/PeriodicStatCollector.cs | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/PluralKit.Bot/Services/PeriodicStatCollector.cs b/PluralKit.Bot/Services/PeriodicStatCollector.cs index d1b77742..032e5fed 100644 --- a/PluralKit.Bot/Services/PeriodicStatCollector.cs +++ b/PluralKit.Bot/Services/PeriodicStatCollector.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using App.Metrics; using Dapper; -using DSharpPlus; -using DSharpPlus.Entities; +using Myriad.Cache; +using Myriad.Types; using NodaTime.Extensions; using PluralKit.Core; @@ -17,8 +16,8 @@ namespace PluralKit.Bot { public class PeriodicStatCollector { - private readonly DiscordShardedClient _client; private readonly IMetrics _metrics; + private readonly IDiscordCache _cache; private readonly CpuStatService _cpu; private readonly IDatabase _db; @@ -29,14 +28,14 @@ namespace PluralKit.Bot private readonly ILogger _logger; - public PeriodicStatCollector(DiscordShardedClient client, IMetrics metrics, ILogger logger, WebhookCacheService webhookCache, DbConnectionCountHolder countHolder, CpuStatService cpu, IDatabase db) + public PeriodicStatCollector(IMetrics metrics, ILogger logger, WebhookCacheService webhookCache, DbConnectionCountHolder countHolder, CpuStatService cpu, IDatabase db, IDiscordCache cache) { - _client = client; _metrics = metrics; _webhookCache = webhookCache; _countHolder = countHolder; _cpu = cpu; _db = db; + _cache = cache; _logger = logger.ForContext(); } @@ -46,36 +45,22 @@ namespace PluralKit.Bot stopwatch.Start(); // Aggregate guild/channel stats - var guildCount = 0; var channelCount = 0; + // No LINQ today, sorry - foreach (var shard in _client.ShardClients.Values) + await foreach (var guild in _cache.GetAllGuilds()) { - guildCount += shard.Guilds.Count; - foreach (var guild in shard.Guilds.Values) - foreach (var channel in guild.Channels.Values) - if (channel.Type == ChannelType.Text) + guildCount++; + foreach (var channel in _cache.GetGuildChannels(guild.Id)) + { + if (channel.Type == Channel.ChannelType.GuildText) channelCount++; + } } _metrics.Measure.Gauge.SetValue(BotMetrics.Guilds, guildCount); _metrics.Measure.Gauge.SetValue(BotMetrics.Channels, channelCount); - - // Aggregate member stats - var usersKnown = new HashSet(); - var usersOnline = new HashSet(); - foreach (var shard in _client.ShardClients.Values) - foreach (var guild in shard.Guilds.Values) - foreach (var user in guild.Members.Values) - { - usersKnown.Add(user.Id); - if (user.Presence?.Status == UserStatus.Online) - usersOnline.Add(user.Id); - } - - _metrics.Measure.Gauge.SetValue(BotMetrics.MembersTotal, usersKnown.Count); - _metrics.Measure.Gauge.SetValue(BotMetrics.MembersOnline, usersOnline.Count); // Aggregate DB stats var counts = await _db.Execute(c => c.QueryFirstAsync("select (select count(*) from systems) as systems, (select count(*) from members) as members, (select count(*) from switches) as switches, (select count(*) from messages) as messages, (select count(*) from groups) as groups")); From 35433b0d82c4084b921b90addb78f138bfb24d13 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 15:03:11 +0100 Subject: [PATCH 016/608] Convert a few more things --- .../Exceptions/DiscordRequestException.cs | 34 +++++++++---------- .../CommandSystem/ContextChecksExt.cs | 4 +-- PluralKit.Bot/Modules.cs | 31 ++++------------- PluralKit.Bot/Utils/MiscUtils.cs | 12 +++---- 4 files changed, 31 insertions(+), 50 deletions(-) diff --git a/Myriad/Rest/Exceptions/DiscordRequestException.cs b/Myriad/Rest/Exceptions/DiscordRequestException.cs index 6570ad81..0aa94d98 100644 --- a/Myriad/Rest/Exceptions/DiscordRequestException.cs +++ b/Myriad/Rest/Exceptions/DiscordRequestException.cs @@ -6,14 +6,14 @@ namespace Myriad.Rest.Exceptions { public class DiscordRequestException: Exception { - public DiscordRequestException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError) + public DiscordRequestException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError) { - RequestBody = requestBody; + ResponseBody = responseBody; Response = response; ApiError = apiError; } - public string RequestBody { get; init; } = null!; + public string ResponseBody { get; init; } = null!; public HttpResponseMessage Response { get; init; } = null!; public HttpStatusCode StatusCode => Response.StatusCode; @@ -29,43 +29,43 @@ namespace Myriad.Rest.Exceptions public class NotFoundException: DiscordRequestException { - public NotFoundException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( - response, requestBody, apiError) { } + public NotFoundException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): base( + response, responseBody, apiError) { } } public class UnauthorizedException: DiscordRequestException { - public UnauthorizedException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( - response, requestBody, apiError) { } + public UnauthorizedException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): base( + response, responseBody, apiError) { } } public class ForbiddenException: DiscordRequestException { - public ForbiddenException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( - response, requestBody, apiError) { } + public ForbiddenException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): base( + response, responseBody, apiError) { } } public class ConflictException: DiscordRequestException { - public ConflictException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( - response, requestBody, apiError) { } + public ConflictException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): base( + response, responseBody, apiError) { } } public class BadRequestException: DiscordRequestException { - public BadRequestException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): base( - response, requestBody, apiError) { } + public BadRequestException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): base( + response, responseBody, apiError) { } } public class TooManyRequestsException: DiscordRequestException { - public TooManyRequestsException(HttpResponseMessage response, string requestBody, DiscordApiError? apiError): - base(response, requestBody, apiError) { } + public TooManyRequestsException(HttpResponseMessage response, string responseBody, DiscordApiError? apiError): + base(response, responseBody, apiError) { } } public class UnknownDiscordRequestException: DiscordRequestException { - public UnknownDiscordRequestException(HttpResponseMessage response, string requestBody, - DiscordApiError? apiError): base(response, requestBody, apiError) { } + public UnknownDiscordRequestException(HttpResponseMessage response, string responseBody, + DiscordApiError? apiError): base(response, responseBody, apiError) { } } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs index 53ae3015..eb1d0e89 100644 --- a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs @@ -1,6 +1,4 @@ -using DSharpPlus; - -using Myriad.Types; +using Myriad.Types; using PluralKit.Core; diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 7229581d..f3265e2c 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -3,9 +3,6 @@ using System.Net.Http; using Autofac; -using DSharpPlus; -using DSharpPlus.EventArgs; - using Myriad.Cache; using Myriad.Gateway; @@ -24,17 +21,6 @@ namespace PluralKit.Bot protected override void Load(ContainerBuilder builder) { // Clients - builder.Register(c => new DiscordConfiguration - { - Token = c.Resolve().Token, - TokenType = TokenType.Bot, - MessageCacheSize = 0, - LargeThreshold = 50, - LoggerFactory = c.Resolve() - }).AsSelf(); - builder.Register(c => new DiscordShardedClient(c.Resolve())).AsSelf().SingleInstance(); - builder.Register(c => new DiscordRestClient(c.Resolve())).AsSelf().SingleInstance(); - builder.Register(c => new GatewaySettings { Token = c.Resolve().Token, @@ -82,9 +68,7 @@ namespace PluralKit.Bot builder.RegisterType().As>(); // Event handler queue - builder.RegisterType>().AsSelf().SingleInstance(); builder.RegisterType>().AsSelf().SingleInstance(); - builder.RegisterType>().AsSelf().SingleInstance(); builder.RegisterType>().AsSelf().SingleInstance(); // Bot services @@ -104,14 +88,13 @@ namespace PluralKit.Bot // Sentry stuff builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); - // TODO: - // builder.RegisterType() - // .As>() - // .As>() - // .As>() - // .As>() - // .As>() - // .SingleInstance(); + builder.RegisterType() + .As>() + .As>() + .As>() + .As>() + .As>() + .SingleInstance(); // Proxy stuff builder.RegisterType().AsSelf().SingleInstance(); diff --git a/PluralKit.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index 38a86618..3fb3b995 100644 --- a/PluralKit.Bot/Utils/MiscUtils.cs +++ b/PluralKit.Bot/Utils/MiscUtils.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Net.Sockets; using System.Threading.Tasks; -using DSharpPlus.Exceptions; +using Myriad.Rest.Exceptions; using Newtonsoft.Json; @@ -64,12 +64,12 @@ namespace PluralKit.Bot if (e is JsonReaderException jre && jre.Message == "Unexpected character encountered while parsing value: <. Path '', line 0, position 0.") return false; // And now (2020-05-12), apparently Discord returns these weird responses occasionally. Also not our problem. - if (e is BadRequestException bre && bre.WebResponse.Response.Contains("
nginx
")) return false; - if (e is NotFoundException ne && ne.WebResponse.Response.Contains("
nginx
")) return false; - if (e is UnauthorizedException ue && ue.WebResponse.Response.Contains("
nginx
")) return false; + if (e is BadRequestException bre && bre.ResponseBody.Contains("
nginx
")) return false; + if (e is NotFoundException ne && ne.ResponseBody.Contains("
nginx
")) return false; + if (e is UnauthorizedException ue && ue.ResponseBody.Contains("
nginx
")) return false; - // 500s? also not our problem :^) - if (e is ServerErrorException) return false; + // 5xxs? also not our problem :^) + if (e is UnknownDiscordRequestException udre && (int) udre.StatusCode >= 500) return false; // Webhook server errors are also *not our problem* // (this includes rate limit errors, WebhookRateLimited is a subclass) From 8785354a2b6fc1760e791165857778b27a418e77 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 15:04:37 +0100 Subject: [PATCH 017/608] Remove D#+ dependency :))))) --- PluralKit.Bot/PluralKit.Bot.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 031a901b..7c06a10c 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -16,8 +16,6 @@ - - From 80c572f5946de42a7c377fc686ad22edf268f05d Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 16:02:34 +0100 Subject: [PATCH 018/608] Fix various bugs and regressions --- Myriad/Rest/Ratelimit/Bucket.cs | 5 +- Myriad/Types/GuildMember.cs | 2 +- PluralKit.Bot/Bot.cs | 10 ++- PluralKit.Bot/CommandSystem/Context.cs | 4 +- PluralKit.Bot/Commands/ImportExport.cs | 1 - PluralKit.Bot/Commands/Misc.cs | 3 +- PluralKit.Bot/Commands/SystemFront.cs | 16 +++-- PluralKit.Bot/Handlers/MessageCreated.cs | 2 +- PluralKit.Bot/Proxy/ProxyService.cs | 61 ++++++++----------- PluralKit.Bot/Services/LogChannelService.cs | 2 - .../Services/WebhookExecutorService.cs | 11 +++- 11 files changed, 60 insertions(+), 57 deletions(-) diff --git a/Myriad/Rest/Ratelimit/Bucket.cs b/Myriad/Rest/Ratelimit/Bucket.cs index 7f49ec33..918b11b2 100644 --- a/Myriad/Rest/Ratelimit/Bucket.cs +++ b/Myriad/Rest/Ratelimit/Bucket.cs @@ -73,11 +73,14 @@ namespace Myriad.Rest.Ratelimit try { _semaphore.Wait(); + + _logger.Verbose("{BucketKey}/{BucketMajor}: Received rate limit headers: {@RateLimitHeaders}", + Key, Major, headers); if (headers.ResetAfter != null) { var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time - if (headerNextReset > _nextReset) + if (_nextReset == null || headerNextReset > _nextReset) { _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, remaining: {Remaining}, local remaining: {LocalRemaining})", Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining, Remaining); diff --git a/Myriad/Types/GuildMember.cs b/Myriad/Types/GuildMember.cs index da25fd65..c08508ff 100644 --- a/Myriad/Types/GuildMember.cs +++ b/Myriad/Types/GuildMember.cs @@ -7,7 +7,7 @@ public record GuildMemberPartial { - public string Nick { get; init; } + public string? Nick { get; init; } public ulong[] Roles { get; init; } public string JoinedAt { get; init; } } diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 17c8bfad..550c8d48 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -14,6 +14,7 @@ using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; using Myriad.Rest; +using Myriad.Rest.Exceptions; using Myriad.Types; using NodaTime; @@ -244,9 +245,12 @@ namespace PluralKit.Bot // Once we've sent it to Sentry, report it to the user (if we have permission to) var reportChannel = handler.ErrorChannelFor(evt); - // TODO: ID lookup - // if (reportChannel != null && reportChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) - // await _errorMessageService.SendErrorMessage(reportChannel, sentryEvent.EventId.ToString()); + if (reportChannel != null) + { + var botPerms = PermissionsIn(reportChannel.Value); + if (botPerms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks)) + await _errorMessageService.SendErrorMessage(reportChannel.Value, sentryEvent.EventId.ToString()); + } } } diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index d0135ced..16ac3abd 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -101,9 +101,9 @@ namespace PluralKit.Bot { Content = text, Embed = embed, - AllowedMentions = mentions + // Default to an empty allowed mentions object instead of null (which means no mentions allowed) + AllowedMentions = mentions ?? new AllowedMentions() }); - // TODO: mentions should default to empty and not null? if (embed != null) { diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index fda3afe7..bbc3973a 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -144,7 +144,6 @@ namespace PluralKit.Bot try { var dm = await ctx.RestNew.CreateDm(ctx.AuthorNew.Id); - // TODO: send file var msg = await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest {Content = $"{Emojis.Success} Here you go!"}, diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 92ea99e4..b663747d 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -84,8 +84,7 @@ namespace PluralKit.Bot { var totalSwitches = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.SwitchCount.Name)?.Value ?? 0; var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name)?.Value ?? 0; - // TODO: shard stuff - var shardId = ctx.ShardNew.ShardInfo?.ShardId ?? -1; + var shardId = ctx.ShardNew.ShardInfo.ShardId; var shardTotal = ctx.Cluster.Shards.Count; var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count(); var shardInfo = _shards.GetShardInfo(ctx.ShardNew); diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 47a01d03..3f8cbc0b 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text; using System.Threading.Tasks; using NodaTime; @@ -70,6 +71,7 @@ namespace PluralKit.Bot embedTitle, async (builder, switches) => { + var sb = new StringBuilder(); foreach (var entry in switches) { var lastSw = entry.LastTime; @@ -98,17 +100,13 @@ namespace PluralKit.Bot stringToAdd = $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago)\n"; } - - try // Unfortunately the only way to test DiscordEmbedBuilder.Description max length is this - { - // TODO: what is this?? - // builder.Description += stringToAdd; - } - catch (ArgumentException) - { + + if (sb.Length + stringToAdd.Length >= 1024) break; - }// TODO: Make sure this works + sb.Append(stringToAdd); } + + builder.Description(sb.ToString()); } ); } diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index cd5a0f00..b5fc477a 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -58,7 +58,7 @@ namespace PluralKit.Bot public async Task Handle(Shard shard, MessageCreateEvent evt) { if (evt.Author.Id == shard.User?.Id) return; - if (evt.Type != Message.MessageType.Default) return; + if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return; if (IsDuplicateMessage(evt)) return; var guild = evt.GuildId != null ? _cache.GetGuild(evt.GuildId.Value) : null; diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 0a362ab4..32781447 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -88,7 +88,8 @@ namespace PluralKit.Bot if (ctx.SystemId == null) return false; // Make sure channel is a guild text channel and this is a normal message - if ((channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildNews) || msg.Type != Message.MessageType.Default) return false; + if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildNews) return false; + if (msg.Type != Message.MessageType.Default && msg.Type != Message.MessageType.Reply) return false; // Make sure author is a normal user if (msg.Author.System == true || msg.Author.Bot || msg.WebhookId != null) return false; @@ -109,12 +110,13 @@ namespace PluralKit.Bot { // Create reply embed var embeds = new List(); - if (trigger.MessageReference?.ChannelId == trigger.ChannelId) + if (trigger.Type == Message.MessageType.Reply && trigger.MessageReference?.ChannelId == trigger.ChannelId) { - var repliedTo = await FetchReplyOriginalMessage(trigger.MessageReference); + var repliedTo = trigger.ReferencedMessage.Value; if (repliedTo != null) { - var embed = CreateReplyEmbed(repliedTo); + var nickname = await FetchReferencedMessageAuthorNickname(trigger, repliedTo); + var embed = CreateReplyEmbed(trigger, repliedTo, nickname); if (embed != null) embeds.Add(embed); } @@ -130,7 +132,7 @@ namespace PluralKit.Bot { GuildId = trigger.GuildId!.Value, ChannelId = trigger.ChannelId, - Name = FixSingleCharacterName(match.Member.ProxyName(ctx)), + Name = match.Member.ProxyName(ctx), AvatarUrl = match.Member.ProxyAvatar(ctx), Content = content, Attachments = trigger.Attachments, @@ -140,39 +142,39 @@ namespace PluralKit.Bot await HandleProxyExecutedActions(shard, conn, ctx, trigger, proxyMessage, match); } - private async Task FetchReplyOriginalMessage(Message.Reference reference) + private async Task FetchReferencedMessageAuthorNickname(Message trigger, Message referenced) { + if (referenced.WebhookId != null) + return null; + try { - var msg = await _rest.GetMessage(reference.ChannelId!.Value, reference.MessageId!.Value); - if (msg == null) - _logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but it was not found", - reference.ChannelId, reference.MessageId); - return msg; + var member = await _rest.GetGuildMember(trigger.GuildId!.Value, referenced.Author.Id); + return member?.Nick; } - catch (UnauthorizedException) + catch (ForbiddenException) { - _logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but bot was not allowed to", - reference.ChannelId, reference.MessageId); + _logger.Warning("Failed to fetch member {UserId} in guild {GuildId} when getting reply nickname, falling back to username", + referenced.Author.Id, trigger.GuildId!.Value); + return null; } - - return null; } - private Embed CreateReplyEmbed(Message original) + private Embed CreateReplyEmbed(Message trigger, Message repliedTo, string? nickname) { - var jumpLink = $"https://discord.com/channels/{original.GuildId}/{original.ChannelId}/{original.Id}"; + // repliedTo doesn't have a GuildId field :/ + var jumpLink = $"https://discord.com/channels/{trigger.GuildId}/{repliedTo.ChannelId}/{repliedTo.Id}"; var content = new StringBuilder(); - var hasContent = !string.IsNullOrWhiteSpace(original.Content); + var hasContent = !string.IsNullOrWhiteSpace(repliedTo.Content); if (hasContent) { - var msg = original.Content; + var msg = repliedTo.Content; if (msg.Length > 100) { - msg = original.Content.Substring(0, 100); - var spoilersInOriginalString = Regex.Matches(original.Content, @"\|\|").Count; + msg = repliedTo.Content.Substring(0, 100); + var spoilersInOriginalString = Regex.Matches(repliedTo.Content, @"\|\|").Count; var spoilersInTruncatedString = Regex.Matches(msg, @"\|\|").Count; if (spoilersInTruncatedString % 2 == 1 && spoilersInOriginalString % 2 == 0) msg += "||"; @@ -181,7 +183,7 @@ namespace PluralKit.Bot content.Append($"**[Reply to:]({jumpLink})** "); content.Append(msg); - if (original.Attachments.Length > 0) + if (repliedTo.Attachments.Length > 0) content.Append($" {Emojis.Paperclip}"); } else @@ -189,11 +191,8 @@ namespace PluralKit.Bot content.Append($"*[(click to see attachment)]({jumpLink})*"); } - // TODO: get the nickname somehow - var username = original.Author.Username; - // var username = original.Member?.Nick ?? original.Author.Username; - - var avatarUrl = $"https://cdn.discordapp.com/avatars/{original.Author.Id}/{original.Author.Avatar}.png"; + var username = nickname ?? repliedTo.Author.Username; + var avatarUrl = $"https://cdn.discordapp.com/avatars/{repliedTo.Author.Id}/{repliedTo.Author.Avatar}.png"; return new Embed { @@ -288,12 +287,6 @@ namespace PluralKit.Bot return true; } - private string FixSingleCharacterName(string proxyName) - { - if (proxyName.Length == 1) return proxyName += "\u17b5"; - else return proxyName; - } - private void CheckProxyNameBoundsOrError(string proxyName) { if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName); diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index d08e42a7..6501181f 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -53,8 +53,6 @@ namespace PluralKit.Bot { } // Send embed! - - // TODO: fix? await using var conn = await _db.Obtain(); var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 005f2b45..9017bc30 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -87,7 +87,7 @@ namespace PluralKit.Bot var webhookReq = new ExecuteWebhookRequest { - Username = FixClyde(req.Name).Truncate(80), + Username = FixProxyName(req.Name).Truncate(80), Content = content, AllowedMentions = allowedMentions, AvatarUrl = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null, @@ -185,6 +185,8 @@ namespace PluralKit.Bot return chunks; } + private string FixProxyName(string name) => FixSingleCharacterName(FixClyde(name)); + private string FixClyde(string name) { static string Replacement(Match m) => m.Groups[1].Value + "\u200A" + m.Groups[2].Value; @@ -193,5 +195,12 @@ namespace PluralKit.Bot // since Discord blocks webhooks containing the word "Clyde"... for some reason. /shrug return Regex.Replace(name, "(c)(lyde)", Replacement, RegexOptions.IgnoreCase); } + + private string FixSingleCharacterName(string proxyName) + { + if (proxyName.Length == 1) + return proxyName + "\u17b5"; + return proxyName; + } } } \ No newline at end of file From ef614d07c36957839f74a3756125f30294bbd56f Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 16:16:52 +0100 Subject: [PATCH 019/608] Do the Big Rename --- PluralKit.Bot/CommandSystem/Context.cs | 30 ++++++++-------- .../CommandSystem/ContextChecksExt.cs | 2 +- .../ContextEntityArgumentsExt.cs | 2 +- PluralKit.Bot/Commands/Autoproxy.cs | 14 ++++---- .../Commands/Avatars/ContextAvatarExt.cs | 2 +- PluralKit.Bot/Commands/CommandTree.cs | 2 +- PluralKit.Bot/Commands/ImportExport.cs | 14 ++++---- PluralKit.Bot/Commands/Member.cs | 2 +- PluralKit.Bot/Commands/MemberAvatar.cs | 16 ++++----- PluralKit.Bot/Commands/MemberEdit.cs | 36 +++++++++---------- PluralKit.Bot/Commands/Misc.cs | 16 ++++----- PluralKit.Bot/Commands/Random.cs | 4 +-- PluralKit.Bot/Commands/ServerConfig.cs | 30 ++++++++-------- PluralKit.Bot/Commands/System.cs | 2 +- PluralKit.Bot/Commands/SystemEdit.cs | 8 ++--- PluralKit.Bot/Commands/SystemLink.cs | 2 +- PluralKit.Bot/Commands/Token.cs | 20 +++++------ PluralKit.Bot/Utils/ContextUtils.cs | 36 +++++++++---------- 18 files changed, 119 insertions(+), 119 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 16ac3abd..0ad82b6e 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -21,12 +21,12 @@ namespace PluralKit.Bot { private readonly ILifetimeScope _provider; - private readonly DiscordApiClient _newRest; + private readonly DiscordApiClient _rest; private readonly Cluster _cluster; - private readonly Shard _shardNew; + private readonly Shard _shard; private readonly Guild? _guild; private readonly Channel _channel; - private readonly MessageCreateEvent _messageNew; + private readonly MessageCreateEvent _message; private readonly Parameters _parameters; private readonly MessageContext _messageContext; private readonly PermissionSet _botPermissions; @@ -44,8 +44,8 @@ namespace PluralKit.Bot public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, PKSystem senderSystem, MessageContext messageContext, PermissionSet botPermissions) { - _messageNew = message; - _shardNew = shard; + _message = message; + _shard = shard; _guild = guild; _channel = channel; _senderSystem = senderSystem; @@ -57,7 +57,7 @@ namespace PluralKit.Bot _provider = provider; _commandMessageService = provider.Resolve(); _parameters = new Parameters(message.Content?.Substring(commandParseOffset)); - _newRest = provider.Resolve(); + _rest = provider.Resolve(); _cluster = provider.Resolve(); _botPermissions = botPermissions; @@ -66,20 +66,20 @@ namespace PluralKit.Bot public IDiscordCache Cache => _cache; - public Channel ChannelNew => _channel; - public User AuthorNew => _messageNew.Author; - public GuildMemberPartial MemberNew => _messageNew.Member; + public Channel Channel => _channel; + public User Author => _message.Author; + public GuildMemberPartial Member => _message.Member; - public Message MessageNew => _messageNew; - public Guild GuildNew => _guild; - public Shard ShardNew => _shardNew; + public Message Message => _message; + public Guild Guild => _guild; + public Shard Shard => _shard; public Cluster Cluster => _cluster; public MessageContext MessageContext => _messageContext; public PermissionSet BotPermissions => _botPermissions; public PermissionSet UserPermissions => _userPermissions; - public DiscordApiClient RestNew => _newRest; + public DiscordApiClient Rest => _rest; public PKSystem System => _senderSystem; @@ -97,7 +97,7 @@ namespace PluralKit.Bot if (embed != null && !BotPermissions.HasFlag(PermissionSet.EmbedLinks)) throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled."); - var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest + var msg = await _rest.CreateMessage(_channel.Id, new MessageRequest { Content = text, Embed = embed, @@ -109,7 +109,7 @@ namespace PluralKit.Bot { // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) // This may need to be changed at some point but works well enough for now - await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id); + await _commandMessageService.RegisterMessage(msg.Id, Author.Id); } return msg; diff --git a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs index eb1d0e89..e09b816e 100644 --- a/PluralKit.Bot/CommandSystem/ContextChecksExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextChecksExt.cs @@ -8,7 +8,7 @@ namespace PluralKit.Bot { public static Context CheckGuildContext(this Context ctx) { - if (ctx.ChannelNew.GuildId != null) return ctx; + if (ctx.Channel.GuildId != null) return ctx; throw new PKError("This command can not be run in a DM."); } diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 44779677..00acf4b7 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -14,7 +14,7 @@ namespace PluralKit.Bot { var text = ctx.PeekArgument(); if (text.TryParseMention(out var id)) - return await ctx.Cache.GetOrFetchUser(ctx.RestNew, id); + return await ctx.Cache.GetOrFetchUser(ctx.Rest, id); return null; } diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 15abc6fc..c1d58059 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -89,7 +89,7 @@ namespace PluralKit.Bot { var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy ** - Autoproxies as a specific member"; var eb = new EmbedBuilder() - .Title($"Current autoproxy status (for {ctx.GuildNew.Name.EscapeMarkdown()})"); + .Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); var fronters = ctx.MessageContext.LastSwitchMembers; var relevantMember = ctx.MessageContext.AutoproxyMode switch @@ -129,7 +129,7 @@ namespace PluralKit.Bot } if (!ctx.MessageContext.AllowAutoproxy) - eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.AuthorNew.Id}>). To enable it, use `pk;autoproxy account enable`.")); + eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.")); return eb.Build(); } @@ -191,7 +191,7 @@ namespace PluralKit.Bot else { var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; - await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.AuthorNew.Id}>."); + await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>."); } } @@ -200,18 +200,18 @@ namespace PluralKit.Bot var statusString = allow ? "enabled" : "disabled"; if (ctx.MessageContext.AllowAutoproxy == allow) { - await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.AuthorNew.Id}>."); + await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); return; } var patch = new AccountPatch { AllowAutoproxy = allow }; - await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.AuthorNew.Id, patch)); - await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.AuthorNew.Id}>."); + await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch)); + await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); } private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) { var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember}; - return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch)); + return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs index 98646da2..43207639 100644 --- a/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs +++ b/PluralKit.Bot/Commands/Avatars/ContextAvatarExt.cs @@ -45,7 +45,7 @@ namespace PluralKit.Bot } // If we have an attachment, use that - if (ctx.MessageNew.Attachments.FirstOrDefault() is {} attachment) + if (ctx.Message.Attachments.FirstOrDefault() is {} attachment) { var url = TryRewriteCdnUrl(attachment.ProxyUrl); return new ParsedImage {Url = url, Source = AvatarSource.Attachment}; diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index a8132c5e..f418d784 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -524,7 +524,7 @@ namespace PluralKit.Bot { // Try to resolve the user ID to find the associated account, // so we can print their username. - var user = await ctx.RestNew.GetUser(id); + var user = await ctx.Rest.GetUser(id); if (user != null) return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered."; else diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index bbc3973a..3d289f7d 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -34,7 +34,7 @@ namespace PluralKit.Bot public async Task Import(Context ctx) { - var url = ctx.RemainderOrNull() ?? ctx.MessageNew.Attachments.FirstOrDefault()?.Url; + var url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url; if (url == null) throw Errors.NoImportFilePassed; await ctx.BusyIndicator(async () => @@ -69,7 +69,7 @@ namespace PluralKit.Bot if (!data.Valid) throw Errors.InvalidImportFile; - if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.AuthorNew.Id)) + if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id)) { var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?"; if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled; @@ -77,7 +77,7 @@ namespace PluralKit.Bot // If passed system is null, it'll create a new one // (and that's okay!) - var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.AuthorNew.Id); + var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.Author.Id); if (!result.Success) await ctx.Reply($"{Emojis.Error} The provided system profile could not be imported. {result.Message}"); else if (ctx.System == null) @@ -143,15 +143,15 @@ namespace PluralKit.Bot try { - var dm = await ctx.RestNew.CreateDm(ctx.AuthorNew.Id); + var dm = await ctx.Rest.CreateDm(ctx.Author.Id); - var msg = await ctx.RestNew.CreateMessage(dm.Id, + var msg = await ctx.Rest.CreateMessage(dm.Id, new MessageRequest {Content = $"{Emojis.Success} Here you go!"}, new[] {new MultipartFile("system.json", stream)}); - await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest { Content = $"<{msg.Attachments[0].Url}>" }); + await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = $"<{msg.Attachments[0].Url}>" }); // If the original message wasn't posted in DMs, send a public reminder - if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 20229368..ffed8d3b 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -71,7 +71,7 @@ namespace PluralKit.Bot public async Task ViewMember(Context ctx, PKMember target) { var system = await _db.Execute(c => _repo.GetSystem(c, target.System)); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.GuildNew, ctx.LookupContextFor(system))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); } public async Task Soulscream(Context ctx, PKMember target) diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index fa08b4b6..4afb6d15 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -25,7 +25,7 @@ namespace PluralKit.Bot if (location == AvatarLocation.Server) { if (target.AvatarUrl != null) - await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.GuildNew.Name}**)."); + await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.Guild.Name}**)."); else await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar."); } @@ -55,7 +55,7 @@ namespace PluralKit.Bot throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar."); } - var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.GuildNew.Name})" : "avatar"; + var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar"; var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar"; var eb = new EmbedBuilder() @@ -69,14 +69,14 @@ namespace PluralKit.Bot public async Task ServerAvatar(Context ctx, PKMember target) { ctx.CheckGuildContext(); - var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); + var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData); } public async Task Avatar(Context ctx, PKMember target) { - var guildData = ctx.GuildNew != null ? - await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)) + var guildData = ctx.Guild != null ? + await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)) : null; await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData); @@ -119,8 +119,8 @@ namespace PluralKit.Bot var serverFrag = location switch { - AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.GuildNew.Name}**).", - AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.GuildNew.Name}**), and thus changing the global avatar will have no effect here.", + AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).", + AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.Guild.Name}**), and thus changing the global avatar will have no effect here.", _ => "" }; @@ -145,7 +145,7 @@ namespace PluralKit.Bot { case AvatarLocation.Server: var serverPatch = new MemberGuildPatch { AvatarUrl = url }; - return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.GuildNew.Id, serverPatch)); + return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.Guild.Id, serverPatch)); case AvatarLocation.Member: var memberPatch = new MemberPatch { AvatarUrl = url }; return _db.Execute(c => _repo.UpdateMember(c, target.Id, memberPatch)); diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 6dc1f1c6..6d7ba1ee 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -46,11 +46,11 @@ namespace PluralKit.Bot if (newName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); if (target.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead."); - if (ctx.GuildNew != null) + if (ctx.Guild != null) { - var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); + var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) - await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.GuildNew.Name}), and will be proxied using that name here."); + await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.Guild.Name}), and will be proxied using that name here."); } } @@ -226,8 +226,8 @@ namespace PluralKit.Bot var lcx = ctx.LookupContextFor(target); MemberGuildSettings memberGuildConfig = null; - if (ctx.GuildNew != null) - memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); + if (ctx.Guild != null) + memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); var eb = new EmbedBuilder() .Title($"Member names") @@ -246,12 +246,12 @@ namespace PluralKit.Bot eb.Field(new("Display Name", target.DisplayName ?? "*(none)*")); } - if (ctx.GuildNew != null) + if (ctx.Guild != null) { if (memberGuildConfig?.DisplayName != null) - eb.Field(new($"Server Name (in {ctx.GuildNew.Name})", $"**{memberGuildConfig.DisplayName}**")); + eb.Field(new($"Server Name (in {ctx.Guild.Name})", $"**{memberGuildConfig.DisplayName}**")); else - eb.Field(new($"Server Name (in {ctx.GuildNew.Name})", memberGuildConfig?.DisplayName ?? "*(none)*")); + eb.Field(new($"Server Name (in {ctx.Guild.Name})", memberGuildConfig?.DisplayName ?? "*(none)*")); } return eb; @@ -262,11 +262,11 @@ namespace PluralKit.Bot async Task PrintSuccess(string text) { var successStr = text; - if (ctx.GuildNew != null) + if (ctx.Guild != null) { - var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); + var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) - successStr += $" However, this member has a server name set in this server ({ctx.GuildNew.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; + successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; } await ctx.Reply(successStr); @@ -311,12 +311,12 @@ namespace PluralKit.Bot ctx.CheckOwnMember(target); var patch = new MemberGuildPatch {DisplayName = null}; - await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch)); + await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch)); if (target.DisplayName != null) - await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.GuildNew.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.Guild.Name})."); else - await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.GuildNew.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.Guild.Name})."); } else if (!ctx.HasNext()) { @@ -333,9 +333,9 @@ namespace PluralKit.Bot var newServerName = ctx.RemainderOrNull(); var patch = new MemberGuildPatch {DisplayName = newServerName}; - await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch)); + await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch)); - await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.GuildNew.Name})."); + await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name})."); } } @@ -415,8 +415,8 @@ namespace PluralKit.Bot // Get guild settings (mostly for warnings and such) MemberGuildSettings guildSettings = null; - if (ctx.GuildNew != null) - guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id)); + if (ctx.Guild != null) + guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); async Task SetAll(PrivacyLevel level) { diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index b663747d..5f89c245 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -84,10 +84,10 @@ namespace PluralKit.Bot { var totalSwitches = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.SwitchCount.Name)?.Value ?? 0; var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name)?.Value ?? 0; - var shardId = ctx.ShardNew.ShardInfo.ShardId; + var shardId = ctx.Shard.ShardInfo.ShardId; var shardTotal = ctx.Cluster.Shards.Count; var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count(); - var shardInfo = _shards.GetShardInfo(ctx.ShardNew); + var shardInfo = _shards.GetShardInfo(ctx.Shard); var process = Process.GetCurrentProcess(); var memoryUsage = process.WorkingSet64; @@ -106,7 +106,7 @@ namespace PluralKit.Bot { .Field(new("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true)) .Field(new("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true)) .Field(new("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages")); - await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, + await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Content = "", Embed = embed.Build()}); } @@ -115,10 +115,10 @@ namespace PluralKit.Bot { Guild guild; GuildMemberPartial senderGuildUser = null; - if (ctx.GuildNew != null && !ctx.HasNext()) + if (ctx.Guild != null && !ctx.HasNext()) { - guild = ctx.GuildNew; - senderGuildUser = ctx.MemberNew; + guild = ctx.Guild; + senderGuildUser = ctx.Member; } else { @@ -128,7 +128,7 @@ namespace PluralKit.Bot { guild = await _rest.GetGuild(guildId); if (guild != null) - senderGuildUser = await _rest.GetGuildMember(guildId, ctx.AuthorNew.Id); + senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); if (guild == null || senderGuildUser == null) throw Errors.GuildNotFound(guildId); } @@ -150,7 +150,7 @@ namespace PluralKit.Bot { foreach (var channel in await _rest.GetGuildChannels(guild.Id)) { var botPermissions = _bot.PermissionsIn(channel.Id); - var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.AuthorNew.Id, senderGuildUser.Roles); + var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser.Roles); if ((userPermissions & PermissionSet.ViewChannel) == 0) { diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 51770206..6c154cbc 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -38,7 +38,7 @@ namespace PluralKit.Bot throw new PKError("Your system has no members! Please create at least one member before using this command."); var randInt = randGen.Next(members.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); } public async Task Group(Context ctx) @@ -73,7 +73,7 @@ namespace PluralKit.Bot var ms = members.ToList(); var randInt = randGen.Next(ms.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 6bcbb5b0..e29db8d4 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -32,7 +32,7 @@ namespace PluralKit.Bot if (await ctx.MatchClear("the server log channel")) { - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, new GuildPatch {LogChannel = null})); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null})); await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); return; } @@ -43,10 +43,10 @@ namespace PluralKit.Bot Channel channel = null; var channelString = ctx.PeekArgument(); channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); var patch = new GuildPatch {LogChannel = channel.Id}; - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch)); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch)); await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}."); } @@ -56,20 +56,20 @@ namespace PluralKit.Bot var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); + affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { var channelString = ctx.PeekArgument(); var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } ulong? logChannel = null; await using (var conn = await _db.Obtain()) { - var config = await _repo.GetGuild(conn, ctx.GuildNew.Id); + var config = await _repo.GetGuild(conn, ctx.Guild.Id); logChannel = config.LogChannel; var blacklist = config.LogBlacklist.ToHashSet(); if (enable) @@ -78,7 +78,7 @@ namespace PluralKit.Bot blacklist.UnionWith(affectedChannels.Select(c => c.Id)); var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()}; - await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch); + await _repo.UpsertGuild(conn, ctx.Guild.Id, patch); } await ctx.Reply( @@ -90,7 +90,7 @@ namespace PluralKit.Bot { ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id)); + var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id)); // Resolve all channels from the cache and order by position var channels = blacklist.Blacklist @@ -106,7 +106,7 @@ namespace PluralKit.Bot } await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, - $"Blacklisted channels for {ctx.GuildNew.Name}", + $"Blacklisted channels for {ctx.Guild.Name}", (eb, l) => { string CategoryName(ulong? id) => @@ -140,19 +140,19 @@ namespace PluralKit.Bot var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); + affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { var channelString = ctx.PeekArgument(); var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } await using (var conn = await _db.Obtain()) { - var guild = await _repo.GetGuild(conn, ctx.GuildNew.Id); + var guild = await _repo.GetGuild(conn, ctx.Guild.Id); var blacklist = guild.Blacklist.ToHashSet(); if (shouldAdd) blacklist.UnionWith(affectedChannels.Select(c => c.Id)); @@ -160,7 +160,7 @@ namespace PluralKit.Bot blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); var patch = new GuildPatch {Blacklist = blacklist.ToArray()}; - await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch); + await _repo.UpsertGuild(conn, ctx.Guild.Id, patch); } await ctx.Reply($"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist."); @@ -183,7 +183,7 @@ namespace PluralKit.Bot .Title("Log cleanup settings") .Field(new("Supported bots", botList)); - var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id)); + var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id)); if (guildCfg.LogCleanupEnabled) eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); else @@ -193,7 +193,7 @@ namespace PluralKit.Bot } var patch = new GuildPatch {LogCleanupEnabled = newValue}; - await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch)); + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch)); if (newValue) await ctx.Reply($"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts."); diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index b1575c95..d531196d 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -34,7 +34,7 @@ namespace PluralKit.Bot var system = _db.Execute(async c => { var system = await _repo.CreateSystem(c, systemName); - await _repo.AddAccount(c, system.Id, ctx.AuthorNew.Id); + await _repo.AddAccount(c, system.Id, ctx.Author.Id); return system; }); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 4f492ea2..7895b932 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -191,7 +191,7 @@ namespace PluralKit.Bot public async Task SystemProxy(Context ctx) { ctx.CheckSystem().CheckGuildContext(); - var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.GuildNew.Id, ctx.System.Id)); + var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.Guild.Id, ctx.System.Id)); bool newValue; if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; @@ -207,12 +207,12 @@ namespace PluralKit.Bot } var patch = new SystemGuildPatch {ProxyEnabled = newValue}; - await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch)); + await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch)); if (newValue) - await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **enabled** for your system."); + await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **enabled** for your system."); else - await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **disabled** for your system."); + await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **disabled** for your system."); } public async Task SystemTimezone(Context ctx) diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 24042094..0ebc0d83 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -49,7 +49,7 @@ namespace PluralKit.Bot ulong id; if (!ctx.HasNext()) - id = ctx.AuthorNew.Id; + id = ctx.Author.Id; else if (!ctx.MatchUserRaw(out id)) throw new PKSyntaxError("You must pass an account to link with (either ID or @mention)."); diff --git a/PluralKit.Bot/Commands/Token.cs b/PluralKit.Bot/Commands/Token.cs index 2c34fe38..a1888fb6 100644 --- a/PluralKit.Bot/Commands/Token.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -29,21 +29,21 @@ namespace PluralKit.Bot try { // DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile) - var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.RestNew, ctx.AuthorNew.Id); - await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest + var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id); + await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = $"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:" }); - await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest {Content = token}); + await ctx.Rest.CreateMessage(dm.Id, new MessageRequest {Content = token}); // If we're not already in a DM, reply with a reminder to check - if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } @@ -69,8 +69,8 @@ namespace PluralKit.Bot try { // DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile) - var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.RestNew, ctx.AuthorNew.Id); - await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest + var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id); + await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = $"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:" }); @@ -78,16 +78,16 @@ namespace PluralKit.Bot // Make the new token after sending the first DM; this ensures if we can't DM, we also don't end up // breaking their existing token as a side effect :) var token = await MakeAndSetNewToken(ctx.System); - await ctx.RestNew.CreateMessage(dm.Id, new MessageRequest { Content = token }); + await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = token }); // If we're not already in a DM, reply with a reminder to check - if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { // Can't check for permission errors beforehand, so have to handle here :/ - if (ctx.ChannelNew.Type != Channel.ChannelType.Dm) + if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?"); } } diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 58b281c5..2472c098 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -31,11 +31,11 @@ namespace PluralKit.Bot { if (matchFlag && ctx.MatchFlag("y", "yes")) return true; else message = await ctx.Reply(msgString, mentions: mentions); var cts = new CancellationTokenSource(); - if (user == null) user = ctx.AuthorNew; + if (user == null) user = ctx.Author; if (timeout == null) timeout = Duration.FromMinutes(5); // "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses - await ctx.RestNew.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error}); + await ctx.Rest.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error}); bool ReactionPredicate(MessageReactionAddEvent e) { @@ -88,7 +88,7 @@ namespace PluralKit.Bot { public static async Task ConfirmWithReply(this Context ctx, string expectedReply) { bool Predicate(MessageCreateEvent e) => - e.Author.Id == ctx.AuthorNew.Id && e.ChannelId == ctx.ChannelNew.Id; + e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id; var msg = await ctx.Services.Resolve>() .WaitFor(Predicate, Duration.FromMinutes(1)); @@ -121,12 +121,12 @@ namespace PluralKit.Bot { if (pageCount <= 1) return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error }; - var _ = ctx.RestNew.CreateReactionsBulk(msg, botEmojis); // Again, "fork" + var _ = ctx.Rest.CreateReactionsBulk(msg, botEmojis); // Again, "fork" try { var currentPage = 0; while (true) { - var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew, timeout: Duration.FromMinutes(5)); + var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: Duration.FromMinutes(5)); // Increment/decrement page counter based on which reaction was clicked if (reaction.Emoji.Name == "\u23EA") currentPage = 0; // << @@ -140,18 +140,18 @@ namespace PluralKit.Bot { // If we can, remove the user's reaction (so they can press again quickly) if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) - await ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId); + await ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId); // Edit the embed with the new page var embed = await MakeEmbedForPage(currentPage); - await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed}); + await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed}); } } catch (TimeoutException) { // "escape hatch", clean up as if we hit X } if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) - await ctx.RestNew.DeleteAllReactions(msg.ChannelId, msg.Id); + await ctx.Rest.DeleteAllReactions(msg.ChannelId, msg.Id); } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } @@ -189,10 +189,10 @@ namespace PluralKit.Bot { // Add back/forward reactions and the actual indicator emojis async Task AddEmojis() { - await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" }); - await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" }); + await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" }); + await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" }); for (int i = 0; i < items.Count; i++) - await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] }); + await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] }); } var _ = AddEmojis(); // Not concerned about awaiting @@ -200,7 +200,7 @@ namespace PluralKit.Bot { while (true) { // Wait for a reaction - var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew); + var reaction = await ctx.AwaitReaction(msg, ctx.Author); // If it's a movement reaction, inc/dec the page index if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // < @@ -217,8 +217,8 @@ namespace PluralKit.Bot { if (idx < items.Count) return items[idx]; } - var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.AuthorNew.Id); - await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, + var __ = ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id); + await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new() { Content = @@ -234,13 +234,13 @@ namespace PluralKit.Bot { async Task AddEmojis() { for (int i = 0; i < items.Count; i++) - await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() {Name = indicators[i]}); + await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = indicators[i]}); } var _ = AddEmojis(); // Then wait for a reaction and return whichever one we found - var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew,rx => indicators.Contains(rx.Emoji.Name)); + var reaction = await ctx.AwaitReaction(msg, ctx.Author,rx => indicators.Contains(rx.Emoji.Name)); return items[Array.IndexOf(indicators, reaction.Emoji.Name)]; } } @@ -264,12 +264,12 @@ namespace PluralKit.Bot { try { - await Task.WhenAll(ctx.RestNew.CreateReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() {Name = emoji}), task); + await Task.WhenAll(ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new() {Name = emoji}), task); return await task; } finally { - var _ = ctx.RestNew.DeleteOwnReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() { Name = emoji }); + var _ = ctx.Rest.DeleteOwnReaction(ctx.Message.ChannelId, ctx.Message.Id, new() { Name = emoji }); } } } From 557ec4234e964fd6a313c2275e99f2e9b71b6aab Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 17:56:33 +0100 Subject: [PATCH 020/608] Fix fetching messages we can't access --- PluralKit.Bot/Services/EmbedService.cs | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 1efc3506..fd0a971a 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -9,6 +9,7 @@ using Myriad.Builders; using Myriad.Cache; using Myriad.Extensions; using Myriad.Rest; +using Myriad.Rest.Exceptions; using Myriad.Types; using NodaTime; @@ -226,7 +227,16 @@ namespace PluralKit.Bot { { var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel); var ctx = LookupContext.ByNonOwner; - var serverMsg = channel != null ? await _rest.GetMessage(msg.Message.Channel, msg.Message.Mid) : null; + + Message serverMsg = null; + try + { + serverMsg = await _rest.GetMessage(msg.Message.Channel, msg.Message.Mid); + } + catch (ForbiddenException) + { + // no permission, couldn't fetch, oh well + } // Need this whole dance to handle cases where: // - the user is deleted (userInfo == null) @@ -237,11 +247,20 @@ namespace PluralKit.Bot { User userInfo = null; if (channel != null) { - var m = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender); - if (m != null) + GuildMember member = null; + try + { + member = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender); + } + catch (ForbiddenException) + { + // no permission, couldn't fetch, oh well + } + + if (member != null) // Don't do an extra request if we already have this info from the member lookup - userInfo = m.User; - memberInfo = m; + userInfo = member.User; + memberInfo = member; } else userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender); From ccd12df99613d4f196c869cbe1337a7142068bba Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 31 Jan 2021 17:56:44 +0100 Subject: [PATCH 021/608] Fix removing original reaction --- PluralKit.Bot/Handlers/ReactionAdded.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 36412bce..e14070f0 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -157,7 +157,6 @@ namespace PluralKit.Bot var guild = _cache.GetGuild(evt.GuildId!.Value); // Try to DM the user info about the message - // var member = await evt.Guild.GetMember(evt.User.Id); try { var dm = await _cache.GetOrCreateDmChannel(_rest, evt.UserId); @@ -220,7 +219,7 @@ namespace PluralKit.Bot private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt) { if (_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages)) - await _rest.DeleteOwnReaction(evt.ChannelId, evt.MessageId, evt.Emoji); + await _rest.DeleteUserReaction(evt.ChannelId, evt.MessageId, evt.Emoji, evt.UserId); } } } \ No newline at end of file From e7ae9dbe44108ae7964669b1d1744f63509c7b92 Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 1 Feb 2021 14:26:39 +0100 Subject: [PATCH 022/608] Respect shard concurrency limit --- Myriad/Gateway/Cluster.cs | 39 ++++++++++++++++++----- Myriad/Types/Gateway/SessionStartLimit.cs | 1 + 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Myriad/Gateway/Cluster.cs b/Myriad/Gateway/Cluster.cs index 220eadc1..cbb0bd51 100644 --- a/Myriad/Gateway/Cluster.cs +++ b/Myriad/Gateway/Cluster.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Myriad.Types; @@ -46,33 +47,55 @@ namespace Myriad.Gateway public async Task Start(GatewayInfo.Bot info, ClusterSessionState? lastState = null) { if (lastState != null && lastState.Shards.Count == info.Shards) - await Resume(info.Url, lastState); + await Resume(info.Url, lastState, info.SessionStartLimit.MaxConcurrency); else - await Start(info.Url, info.Shards); + await Start(info.Url, info.Shards, info.SessionStartLimit.MaxConcurrency); } - public async Task Resume(string url, ClusterSessionState sessionState) + public async Task Resume(string url, ClusterSessionState sessionState, int concurrency) { _logger.Information("Resuming session with {ShardCount} shards at {Url}", sessionState.Shards.Count, url); foreach (var shardState in sessionState.Shards) CreateAndAddShard(url, shardState.Shard, shardState.Session); - await StartShards(); + await StartShards(concurrency); } - public async Task Start(string url, int shardCount) + public async Task Start(string url, int shardCount, int concurrency) { _logger.Information("Starting {ShardCount} shards at {Url}", shardCount, url); for (var i = 0; i < shardCount; i++) CreateAndAddShard(url, new ShardInfo(i, shardCount), null); - await StartShards(); + await StartShards(concurrency); } - private async Task StartShards() + private async Task StartShards(int concurrency) { + var lastTime = DateTimeOffset.UtcNow; + var identifyCalls = 0; + _logger.Information("Connecting shards..."); - await Task.WhenAll(_shards.Values.Select(s => s.Start())); + foreach (var shard in _shards.Values) + { + if (identifyCalls >= concurrency) + { + var timeout = lastTime + TimeSpan.FromSeconds(5.5); + var delay = timeout - DateTimeOffset.UtcNow; + + if (delay > TimeSpan.Zero) + { + _logger.Information("Hit shard concurrency limit, waiting {Delay}", delay); + await Task.Delay(delay); + } + + identifyCalls = 0; + lastTime = DateTimeOffset.UtcNow; + } + + await shard.Start(); + identifyCalls++; + } } private void CreateAndAddShard(string url, ShardInfo shardInfo, ShardSessionInfo? session) diff --git a/Myriad/Types/Gateway/SessionStartLimit.cs b/Myriad/Types/Gateway/SessionStartLimit.cs index 381c7cd9..b5da4770 100644 --- a/Myriad/Types/Gateway/SessionStartLimit.cs +++ b/Myriad/Types/Gateway/SessionStartLimit.cs @@ -5,5 +5,6 @@ public int Total { get; init; } public int Remaining { get; init; } public int ResetAfter { get; init; } + public int MaxConcurrency { get; init; } } } \ No newline at end of file From ef9b69a99737d96ac05443eb484f46da4458be2a Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 1 Feb 2021 14:26:51 +0100 Subject: [PATCH 023/608] Fix some grammar in group member add/remove --- PluralKit.Bot/Utils/MiscUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index 3fb3b995..041f7ae7 100644 --- a/PluralKit.Bot/Utils/MiscUtils.cs +++ b/PluralKit.Bot/Utils/MiscUtils.cs @@ -46,7 +46,7 @@ namespace PluralKit.Bot if (notActionedOn == 0) return $"{Emojis.Success} {entityTerm(actionedOn.Count, true)} {opStr} {entityTerm(actionedOn.Count, false).ToLower()}."; else - return $"{Emojis.Success} {entityTerm(actionedOn.Count, true)} {opStr} {actionedOn.Count} {entityTerm(actionedOn.Count, false).ToLower()} ({memberNotActionedPosStr}{entityTerm(actionedOn.Count, true).ToLower()} already {inStr} {groupNotActionedPosStr}{entityTerm(notActionedOn, false).ToLower()})."; + return $"{Emojis.Success} {actionedOn.Count} {entityTerm(actionedOn.Count, true).ToLower()} {opStr} {entityTerm(actionedOn.Count, false).ToLower()} ({memberNotActionedPosStr}{entityTerm(actionedOn.Count, true).ToLower()} already {inStr} {groupNotActionedPosStr}{entityTerm(notActionedOn, false).ToLower()})."; } public static bool IsOurProblem(this Exception e) From 18cf8638342d257ea4c58c54c3aa567061fec165 Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 8 Feb 2021 16:30:18 +0100 Subject: [PATCH 024/608] Make rate limit parser more resilient --- Myriad/Rest/Ratelimit/RatelimitHeaders.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Myriad/Rest/Ratelimit/RatelimitHeaders.cs b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs index 4a867deb..5387a10a 100644 --- a/Myriad/Rest/Ratelimit/RatelimitHeaders.cs +++ b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs @@ -13,22 +13,27 @@ namespace Myriad.Rest.Ratelimit ServerDate = response.Headers.Date; if (response.Headers.TryGetValues("X-RateLimit-Limit", out var limit)) - Limit = int.Parse(limit!.First()); + if (int.TryParse(limit.First(), out var limitNum)) + Limit = limitNum; if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining)) - Remaining = int.Parse(remaining!.First()); + if (int.TryParse(remaining!.First(), out var remainingNum)) + Remaining = remainingNum; if (response.Headers.TryGetValues("X-RateLimit-Reset", out var reset)) - Reset = DateTimeOffset.FromUnixTimeMilliseconds((long) (double.Parse(reset!.First()) * 1000)); + if (double.TryParse(reset!.First(), out var resetNum)) + Reset = DateTimeOffset.FromUnixTimeMilliseconds((long) (resetNum * 1000)); if (response.Headers.TryGetValues("X-RateLimit-Reset-After", out var resetAfter)) - ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter!.First())); + if (double.TryParse(resetAfter!.First(), out var resetAfterNum)) + ResetAfter = TimeSpan.FromSeconds(resetAfterNum); if (response.Headers.TryGetValues("X-RateLimit-Bucket", out var bucket)) Bucket = bucket.First(); if (response.Headers.TryGetValues("X-RateLimit-Global", out var global)) - Global = bool.Parse(global!.First()); + if (bool.TryParse(global!.First(), out var globalBool)) + Global = globalBool; } public bool Global { get; init; } From 74424edc8956fef5a31b3bef239e42789a158f4f Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 8 Feb 2021 19:53:06 +0100 Subject: [PATCH 025/608] Refactor rate limit parser, fix locale also --- .../Rest/Ratelimit/DiscordRateLimitPolicy.cs | 2 +- Myriad/Rest/Ratelimit/RatelimitHeaders.cs | 108 ++++++++++++------ 2 files changed, 72 insertions(+), 38 deletions(-) diff --git a/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs b/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs index 9c9e2d00..0ec340e1 100644 --- a/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs +++ b/Myriad/Rest/Ratelimit/DiscordRateLimitPolicy.cs @@ -37,7 +37,7 @@ namespace Myriad.Rest.Ratelimit var response = await action(context, ct).ConfigureAwait(continueOnCapturedContext); // Update rate limit state with headers - var headers = new RatelimitHeaders(response); + var headers = RatelimitHeaders.Parse(response); _ratelimiter.HandleResponse(headers, endpoint, major); return response; diff --git a/Myriad/Rest/Ratelimit/RatelimitHeaders.cs b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs index 5387a10a..581b360e 100644 --- a/Myriad/Rest/Ratelimit/RatelimitHeaders.cs +++ b/Myriad/Rest/Ratelimit/RatelimitHeaders.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Net.Http; @@ -6,46 +7,79 @@ namespace Myriad.Rest.Ratelimit { public record RatelimitHeaders { - public RatelimitHeaders() { } + private const string LimitHeader = "X-RateLimit-Limit"; + private const string RemainingHeader = "X-RateLimit-Remaining"; + private const string ResetHeader = "X-RateLimit-Reset"; + private const string ResetAfterHeader = "X-RateLimit-Reset-After"; + private const string BucketHeader = "X-RateLimit-Bucket"; + private const string GlobalHeader = "X-RateLimit-Global"; + + public bool Global { get; private set; } + public int? Limit { get; private set; } + public int? Remaining { get; private set; } + public DateTimeOffset? Reset { get; private set; } + public TimeSpan? ResetAfter { get; private set; } + public string? Bucket { get; private set; } - public RatelimitHeaders(HttpResponseMessage response) - { - ServerDate = response.Headers.Date; - - if (response.Headers.TryGetValues("X-RateLimit-Limit", out var limit)) - if (int.TryParse(limit.First(), out var limitNum)) - Limit = limitNum; - - if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining)) - if (int.TryParse(remaining!.First(), out var remainingNum)) - Remaining = remainingNum; - - if (response.Headers.TryGetValues("X-RateLimit-Reset", out var reset)) - if (double.TryParse(reset!.First(), out var resetNum)) - Reset = DateTimeOffset.FromUnixTimeMilliseconds((long) (resetNum * 1000)); - - if (response.Headers.TryGetValues("X-RateLimit-Reset-After", out var resetAfter)) - if (double.TryParse(resetAfter!.First(), out var resetAfterNum)) - ResetAfter = TimeSpan.FromSeconds(resetAfterNum); - - if (response.Headers.TryGetValues("X-RateLimit-Bucket", out var bucket)) - Bucket = bucket.First(); - - if (response.Headers.TryGetValues("X-RateLimit-Global", out var global)) - if (bool.TryParse(global!.First(), out var globalBool)) - Global = globalBool; - } - - public bool Global { get; init; } - public int? Limit { get; init; } - public int? Remaining { get; init; } - public DateTimeOffset? Reset { get; init; } - public TimeSpan? ResetAfter { get; init; } - public string? Bucket { get; init; } - - public DateTimeOffset? ServerDate { get; init; } + public DateTimeOffset? ServerDate { get; private set; } public bool HasRatelimitInfo => Limit != null && Remaining != null && Reset != null && ResetAfter != null && Bucket != null; + + public RatelimitHeaders() { } + + public static RatelimitHeaders Parse(HttpResponseMessage response) + { + var headers = new RatelimitHeaders + { + ServerDate = response.Headers.Date, + Limit = TryGetInt(response, LimitHeader), + Remaining = TryGetInt(response, RemainingHeader), + Bucket = TryGetHeader(response, BucketHeader) + }; + + + var resetTimestamp = TryGetDouble(response, ResetHeader); + if (resetTimestamp != null) + headers.Reset = DateTimeOffset.FromUnixTimeMilliseconds((long) (resetTimestamp.Value * 1000)); + + var resetAfterSeconds = TryGetDouble(response, ResetAfterHeader); + if (resetAfterSeconds != null) + headers.ResetAfter = TimeSpan.FromSeconds(resetAfterSeconds.Value); + + var global = TryGetHeader(response, GlobalHeader); + if (global != null && bool.TryParse(global, out var globalBool)) + headers.Global = globalBool; + + return headers; + } + + private static string? TryGetHeader(HttpResponseMessage response, string headerName) + { + if (!response.Headers.TryGetValues(headerName, out var values)) + return null; + + return values.FirstOrDefault(); + } + + private static int? TryGetInt(HttpResponseMessage response, string headerName) + { + var valueString = TryGetHeader(response, headerName); + + if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + return null; + + return value; + } + + private static double? TryGetDouble(HttpResponseMessage response, string headerName) + { + var valueString = TryGetHeader(response, headerName); + + if (!double.TryParse(valueString, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) + return null; + + return value; + } } } \ No newline at end of file From 9d80b7b141285869aec6155676b34b9984da3182 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Tue, 9 Feb 2021 23:36:43 +0100 Subject: [PATCH 026/608] Add group front percentages Also add a title to the system frontpercent embed, and tweak the footer --- PluralKit.Bot/Commands/CommandTree.cs | 5 +++- PluralKit.Bot/Commands/Groups.cs | 27 +++++++++++++++++++ PluralKit.Bot/Commands/SystemFront.cs | 10 +++++-- PluralKit.Bot/Services/EmbedService.cs | 5 ++-- .../Repository/ModelRepository.Switch.cs | 15 ++++++++--- PluralKit.Core/Services/DataFileService.cs | 2 +- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index f418d784..cfd59279 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -60,6 +60,7 @@ namespace PluralKit.Bot public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); public static Command GroupIcon = new Command("group icon", "group icon [url|@mention]", "Changes a group's icon"); public static Command GroupDelete = new Command("group delete", "group delete", "Deletes a group"); + public static Command GroupFrontPercent = new Command("group frontpercent", "group frontpercent [timespan]", "Shows a group's front breakdown."); public static Command GroupMemberRandom = new Command("group random", "group random", "Shows the info card of a randomly selected member in a group."); public static Command GroupRandom = new Command("random", "random group", "Shows the info card of a randomly selected group in your system."); public static Command Switch = new Command("switch", "switch [member 2] [member 3...]", "Registers a switch"); @@ -107,7 +108,7 @@ namespace PluralKit.Bot public static Command[] GroupCommandsTargeted = { GroupInfo, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, GroupIcon, GroupPrivacy, - GroupDelete, GroupMemberRandom + GroupDelete, GroupMemberRandom, GroupFrontPercent }; public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll}; @@ -397,6 +398,8 @@ namespace PluralKit.Bot await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) + await ctx.Execute(GroupFrontPercent, g => g.GroupFrontPercent(ctx, target)); else if (!ctx.HasNext()) await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); else diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index ce93f8fe..4259dff8 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -8,6 +8,8 @@ using Dapper; using Humanizer; +using NodaTime; + using Myriad.Builders; using PluralKit.Core; @@ -428,6 +430,31 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Group deleted."); } + public async Task GroupFrontPercent(Context ctx, PKGroup target) + { + await using var conn = await _db.Obtain(); + + var targetSystem = await GetGroupSystem(ctx, target, conn); + ctx.CheckSystemPrivacy(targetSystem, targetSystem.FrontHistoryPrivacy); + + string durationStr = ctx.RemainderOrNull() ?? "30d"; + + var now = SystemClock.Instance.GetCurrentInstant(); + + var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone); + if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); + if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; + + var title = new StringBuilder($"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in "); + if (targetSystem.Name != null) + title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)"); + else + title.Append($"`{targetSystem.Hid}`"); + + var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, targetSystem.Id, target.Id, rangeStart.Value.ToInstant(), now)); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString())); + } + private async Task GetGroupSystem(Context ctx, PKGroup target, IPKConnection conn) { var system = ctx.System; diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 3f8cbc0b..d9531216 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -124,8 +124,14 @@ namespace PluralKit.Bot if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; - var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system))); + var title = new StringBuilder($"Frontpercent of "); + if (system.Name != null) + title.Append($"{system.Name} (`{system.Hid}`)"); + else + title.Append($"`{system.Hid}`"); + + var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, null, rangeStart.Value.ToInstant(), now)); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString())); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index fd0a971a..5528f1cf 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -293,12 +293,13 @@ namespace PluralKit.Bot { return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var eb = new EmbedBuilder() + .Title(embedTitle) .Color(DiscordUtils.Gray) - .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); + .Footer(new($"System ID: {system.Hid} | {(group != null ? $"Group ID: {group.Hid} | " : "") }Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs index f313f00a..d8633acf 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs @@ -122,7 +122,7 @@ namespace PluralKit.Core await GetSwitches(conn, system).FirstOrDefaultAsync(); public async Task> GetPeriodFronters(IPKConnection conn, - SystemId system, Instant periodStart, + SystemId system, GroupId? group, Instant periodStart, Instant periodEnd) { // TODO: IAsyncEnumerable-ify this one @@ -139,7 +139,14 @@ namespace PluralKit.Core new {Switches = switchMembers.Select(m => m.Member.Value).Distinct().ToList()}); var memberObjects = membersList.ToDictionary(m => m.Id); + // check if a group ID is provided. if so, query DB for all members of said group, otherwise use membersList + var groupMembersList = group != null ? await conn.QueryAsync( + "select * from members inner join group_members on members.id = group_members.member_id where group_id = @id", + new {id = group}) : membersList; + var groupMemberObjects = groupMembersList.ToDictionary(m => m.Id); + // Initialize entries - still need to loop to determine the TimespanEnd below + // use groupMemberObjects to make sure no members outside of the specified group (if present) are selected var entries = from item in switchMembers group item by item.Timestamp @@ -147,7 +154,7 @@ namespace PluralKit.Core select new SwitchListEntry { TimespanStart = g.Key, - Members = g.Where(x => x.Member != default(MemberId)).Select(x => memberObjects[x.Member]) + Members = g.Where(x => x.Member != default(MemberId) && groupMemberObjects.Any(m => x.Member == m.Key) ).Select(x => memberObjects[x.Member]) .ToList() }; @@ -174,7 +181,7 @@ namespace PluralKit.Core return outList; } - public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, Instant periodStart, + public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, GroupId? group, Instant periodStart, Instant periodEnd) { // TODO: this doesn't belong in the repo @@ -188,7 +195,7 @@ namespace PluralKit.Core var actualStart = periodEnd; // will be "pulled" down var actualEnd = periodStart; // will be "pulled" up - foreach (var sw in await GetPeriodFronters(conn, system, periodStart, periodEnd)) + foreach (var sw in await GetPeriodFronters(conn, system, group, periodStart, periodEnd)) { var span = sw.TimespanEnd - sw.TimespanStart; foreach (var member in sw.Members) diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index c694a156..0601efd9 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -51,7 +51,7 @@ namespace PluralKit.Core // Export switches var switches = new List(); - var switchList = await _repo.GetPeriodFronters(conn, system.Id, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); + var switchList = await _repo.GetPeriodFronters(conn, system.Id, null, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); switches.AddRange(switchList.Select(x => new DataFileSwitch { Timestamp = x.TimespanStart.FormatExport(), From 2039a34c1651d04ab7145166aa5ff585333da837 Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 3 Mar 2021 23:27:54 +0100 Subject: [PATCH 027/608] Fix Dockerfile --- .dockerignore | 1 + Dockerfile | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index ea37ec39..ebc82ce6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ # Include project code and build files !PluralKit.*/ +!Myriad/ !PluralKit.sln !nuget.config diff --git a/Dockerfile b/Dockerfile index 82090e41..1b1a2284 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.401 +FROM mcr.microsoft.com/dotnet/sdk:5.0 WORKDIR /app # Restore/fetch dependencies excluding app code to make use of caching COPY PluralKit.sln nuget.config /app/ +COPY Myriad/Myriad.csproj /app/Myriad/ COPY PluralKit.API/PluralKit.API.csproj /app/PluralKit.API/ COPY PluralKit.Bot/PluralKit.Bot.csproj /app/PluralKit.Bot/ COPY PluralKit.Core/PluralKit.Core.csproj /app/PluralKit.Core/ From 2cd5047b141b9213d814890c05c41613931ec92f Mon Sep 17 00:00:00 2001 From: spiral Date: Tue, 9 Mar 2021 15:19:26 +0000 Subject: [PATCH 028/608] fix importing pronouns and message count --- PluralKit.Core/Services/DataFileService.cs | 1 + PluralKit.Core/Utils/BulkImporter.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index c694a156..05dd3b37 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -85,6 +85,7 @@ namespace PluralKit.Core Pronouns = fileMember.Pronouns, AvatarUrl = fileMember.AvatarUrl, KeepProxy = fileMember.KeepProxy, + MessageCount = fileMember.MessageCount, }; if (fileMember.Prefix != null || fileMember.Suffix != null) diff --git a/PluralKit.Core/Utils/BulkImporter.cs b/PluralKit.Core/Utils/BulkImporter.cs index bf7d764a..1d409457 100644 --- a/PluralKit.Core/Utils/BulkImporter.cs +++ b/PluralKit.Core/Utils/BulkImporter.cs @@ -80,12 +80,17 @@ namespace PluralKit.Core if (patch.Name.IsPresent) qb.Variable("name", "@Name"); if (patch.DisplayName.IsPresent) qb.Variable("display_name", "@DisplayName"); if (patch.Description.IsPresent) qb.Variable("description", "@Description"); + if (patch.Pronouns.IsPresent) qb.Variable("pronouns", "@Pronouns"); if (patch.Color.IsPresent) qb.Variable("color", "@Color"); if (patch.AvatarUrl.IsPresent) qb.Variable("avatar_url", "@AvatarUrl"); if (patch.ProxyTags.IsPresent) qb.Variable("proxy_tags", "@ProxyTags"); if (patch.Birthday.IsPresent) qb.Variable("birthday", "@Birthday"); if (patch.KeepProxy.IsPresent) qb.Variable("keep_proxy", "@KeepProxy"); + // don't overwrite message count on existing members + if (existingMember == null) + if (patch.MessageCount.IsPresent) qb.Variable("message_count", "@MessageCount"); + var newMember = await _conn.QueryFirstAsync(qb.Build("returning *"), new { @@ -94,11 +99,13 @@ namespace PluralKit.Core Name = patch.Name.Value, DisplayName = patch.DisplayName.Value, Description = patch.Description.Value, + Pronouns = patch.Pronouns.Value, Color = patch.Color.Value, AvatarUrl = patch.AvatarUrl.Value, KeepProxy = patch.KeepProxy.Value, ProxyTags = patch.ProxyTags.Value, - Birthday = patch.Birthday.Value + Birthday = patch.Birthday.Value, + MessageCount = patch.MessageCount.Value, }); // Log this member ID by the given identifier From 13e3289c26b1b0bf5946e88f8bb74ae31ceb304c Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 09:47:58 +0100 Subject: [PATCH 029/608] Add config option for max shard concurrency --- Myriad/Gateway/Cluster.cs | 12 ++++++++++-- Myriad/Gateway/GatewaySettings.cs | 1 + PluralKit.Bot/BotConfig.cs | 2 ++ PluralKit.Bot/Modules.cs | 23 ++++++++++++++--------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Myriad/Gateway/Cluster.cs b/Myriad/Gateway/Cluster.cs index cbb0bd51..bc5805fa 100644 --- a/Myriad/Gateway/Cluster.cs +++ b/Myriad/Gateway/Cluster.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Myriad.Types; @@ -69,9 +68,10 @@ namespace Myriad.Gateway await StartShards(concurrency); } - private async Task StartShards(int concurrency) { + concurrency = GetActualShardConcurrency(concurrency); + var lastTime = DateTimeOffset.UtcNow; var identifyCalls = 0; @@ -112,5 +112,13 @@ namespace Myriad.Gateway if (EventReceived != null) await EventReceived(shard, evt); } + + private int GetActualShardConcurrency(int recommendedConcurrency) + { + if (_gatewaySettings.MaxShardConcurrency == null) + return recommendedConcurrency; + + return Math.Min(_gatewaySettings.MaxShardConcurrency.Value, recommendedConcurrency); + } } } \ No newline at end of file diff --git a/Myriad/Gateway/GatewaySettings.cs b/Myriad/Gateway/GatewaySettings.cs index fdaf13ea..1fdb12fd 100644 --- a/Myriad/Gateway/GatewaySettings.cs +++ b/Myriad/Gateway/GatewaySettings.cs @@ -4,5 +4,6 @@ { public string Token { get; init; } public GatewayIntent Intents { get; init; } + public int? MaxShardConcurrency { get; init; } } } \ No newline at end of file diff --git a/PluralKit.Bot/BotConfig.cs b/PluralKit.Bot/BotConfig.cs index 153b965c..095b77cd 100644 --- a/PluralKit.Bot/BotConfig.cs +++ b/PluralKit.Bot/BotConfig.cs @@ -11,5 +11,7 @@ namespace PluralKit.Bot // and fall back to the separate default array at the use site :) // This does bind [] as null (therefore default) instead of an empty array, but I can live w/ that. public string[] Prefixes { get; set; } + + public int? MaxShardConcurrency { get; set; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index f3265e2c..cef29570 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -21,16 +21,21 @@ namespace PluralKit.Bot protected override void Load(ContainerBuilder builder) { // Clients - builder.Register(c => new GatewaySettings + builder.Register(c => { - Token = c.Resolve().Token, - Intents = GatewayIntent.Guilds | - GatewayIntent.DirectMessages | - GatewayIntent.DirectMessageReactions | - GatewayIntent.GuildEmojis | - GatewayIntent.GuildMessages | - GatewayIntent.GuildWebhooks | - GatewayIntent.GuildMessageReactions + var botConfig = c.Resolve(); + return new GatewaySettings + { + Token = botConfig.Token, + MaxShardConcurrency = botConfig.MaxShardConcurrency, + Intents = GatewayIntent.Guilds | + GatewayIntent.DirectMessages | + GatewayIntent.DirectMessageReactions | + GatewayIntent.GuildEmojis | + GatewayIntent.GuildMessages | + GatewayIntent.GuildWebhooks | + GatewayIntent.GuildMessageReactions + }; }).AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.Register(c => new Myriad.Rest.DiscordApiClient(c.Resolve().Token, c.Resolve())) From 1c548e9d7bf04dbb07b352f1f4912a6d9c02afdd Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 11:28:11 +0100 Subject: [PATCH 030/608] Fix messages in DMs --- Myriad/Cache/DiscordCacheExtensions.cs | 7 +++++-- Myriad/Cache/IDiscordCache.cs | 1 + Myriad/Cache/MemoryDiscordCache.cs | 12 ++++++++++++ Myriad/Types/Channel.cs | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index e50c3453..aa0541fb 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -1,8 +1,6 @@ using System.Threading.Tasks; using Myriad.Gateway; -using Myriad.Rest; -using Myriad.Types; namespace Myriad.Cache { @@ -51,6 +49,11 @@ namespace Myriad.Cache private static async ValueTask SaveMessageCreate(this IDiscordCache cache, MessageCreateEvent evt) { + // DM messages don't get Channel Create events first, so we need to save + // some kind of stub channel object until we get the real one + if (evt.GuildId == null) + await cache.SaveDmChannelStub(evt.ChannelId); + await cache.SaveUser(evt.Author); foreach (var mention in evt.Mentions) await cache.SaveUser(mention); diff --git a/Myriad/Cache/IDiscordCache.cs b/Myriad/Cache/IDiscordCache.cs index c778ed32..34207d74 100644 --- a/Myriad/Cache/IDiscordCache.cs +++ b/Myriad/Cache/IDiscordCache.cs @@ -11,6 +11,7 @@ namespace Myriad.Cache public ValueTask SaveChannel(Channel channel); public ValueTask SaveUser(User user); public ValueTask SaveRole(ulong guildId, Role role); + public ValueTask SaveDmChannelStub(ulong channelId); public ValueTask RemoveGuild(ulong guildId); public ValueTask RemoveChannel(ulong channelId); diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs index 2dcfde6a..9dc4ca1c 100644 --- a/Myriad/Cache/MemoryDiscordCache.cs +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -81,6 +81,18 @@ namespace Myriad.Cache return default; } + public ValueTask SaveDmChannelStub(ulong channelId) + { + // Use existing channel object if present, otherwise add a stub + // We may get a message create before channel create and we want to have it saved + _channels.GetOrAdd(channelId, id => new Channel + { + Id = id, + Type = Channel.ChannelType.Dm + }); + return default; + } + public ValueTask RemoveGuild(ulong guildId) { _guilds.TryRemove(guildId, out _); diff --git a/Myriad/Types/Channel.cs b/Myriad/Types/Channel.cs index 841a2e1d..180e992b 100644 --- a/Myriad/Types/Channel.cs +++ b/Myriad/Types/Channel.cs @@ -22,7 +22,7 @@ public bool? Nsfw { get; init; } public ulong? ParentId { get; init; } public Overwrite[]? PermissionOverwrites { get; init; } - public User[]? Recipients { get; init; } + public User[]? Recipients { get; init; } // NOTE: this may be null for stub channel objects public record Overwrite { From 4ee0c13d62991bef5f19ecc6e255845286c8ce81 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 11:34:35 +0100 Subject: [PATCH 031/608] Fix error on @ everyone --- Myriad/Rest/Types/AllowedMentions.cs | 5 ++++- .../JsonSnakeCaseStringEnumConverter.cs | 17 +++++++++++++++++ .../Services/WebhookExecutorService.cs | 5 ++++- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 Myriad/Serialization/JsonSnakeCaseStringEnumConverter.cs diff --git a/Myriad/Rest/Types/AllowedMentions.cs b/Myriad/Rest/Types/AllowedMentions.cs index d3ab3199..2c663ffc 100644 --- a/Myriad/Rest/Types/AllowedMentions.cs +++ b/Myriad/Rest/Types/AllowedMentions.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System.Text.Json.Serialization; + +using Myriad.Serialization; namespace Myriad.Rest.Types { public record AllowedMentions { + [JsonConverter(typeof(JsonSnakeCaseStringEnumConverter))] public enum ParseType { Roles, diff --git a/Myriad/Serialization/JsonSnakeCaseStringEnumConverter.cs b/Myriad/Serialization/JsonSnakeCaseStringEnumConverter.cs new file mode 100644 index 00000000..5ff76513 --- /dev/null +++ b/Myriad/Serialization/JsonSnakeCaseStringEnumConverter.cs @@ -0,0 +1,17 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Myriad.Serialization +{ + public class JsonSnakeCaseStringEnumConverter: JsonConverterFactory + { + private readonly JsonStringEnumConverter _inner = new(new JsonSnakeCaseNamingPolicy()); + + public override bool CanConvert(Type typeToConvert) => + _inner.CanConvert(typeToConvert); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + _inner.CreateConverter(typeToConvert, options); + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 9017bc30..39379cdf 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -83,7 +83,10 @@ namespace PluralKit.Bot var allowedMentions = content.ParseMentions(); if (!req.AllowEveryone) - allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild); + allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild) with { + // also clear @everyones + Parse = Array.Empty() + }; var webhookReq = new ExecuteWebhookRequest { From 3196f6009329a4db2b3b1d1ea418953bad2c8149 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 11:38:28 +0100 Subject: [PATCH 032/608] Fix error on DMing with no permission --- PluralKit.Bot/Commands/ImportExport.cs | 5 +++-- PluralKit.Bot/Commands/Token.cs | 4 ++-- PluralKit.Bot/Handlers/ReactionAdded.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 3d289f7d..a4da57ba 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Myriad.Extensions; using Myriad.Rest.Exceptions; using Myriad.Rest.Types; using Myriad.Rest.Types.Requests; @@ -143,7 +144,7 @@ namespace PluralKit.Bot try { - var dm = await ctx.Rest.CreateDm(ctx.Author.Id); + var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id); var msg = await ctx.Rest.CreateMessage(dm.Id, new MessageRequest {Content = $"{Emojis.Success} Here you go!"}, @@ -154,7 +155,7 @@ namespace PluralKit.Bot if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } - catch (UnauthorizedException) + catch (ForbiddenException) { // If user has DMs closed, tell 'em to open them await ctx.Reply( diff --git a/PluralKit.Bot/Commands/Token.cs b/PluralKit.Bot/Commands/Token.cs index a1888fb6..f861b8fb 100644 --- a/PluralKit.Bot/Commands/Token.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -40,7 +40,7 @@ namespace PluralKit.Bot if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } - catch (UnauthorizedException) + catch (ForbiddenException) { // Can't check for permission errors beforehand, so have to handle here :/ if (ctx.Channel.Type != Channel.ChannelType.Dm) @@ -84,7 +84,7 @@ namespace PluralKit.Bot if (ctx.Channel.Type != Channel.ChannelType.Dm) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } - catch (UnauthorizedException) + catch (ForbiddenException) { // Can't check for permission errors beforehand, so have to handle here :/ if (ctx.Channel.Type != Channel.ChannelType.Dm) diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index e14070f0..fcbef124 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -170,7 +170,7 @@ namespace PluralKit.Bot Embed = await _embeds.CreateMessageInfoEmbed(msg) }); } - catch (UnauthorizedException) { } // No permissions to DM, can't check for this :( + catch (ForbiddenException) { } // No permissions to DM, can't check for this :( await TryRemoveOriginalReaction(evt); } @@ -210,7 +210,7 @@ namespace PluralKit.Bot }); await _rest.CreateMessage(dm.Id, new MessageRequest {Content = $"<@{msg.Message.Sender}>".AsCode()}); } - catch (UnauthorizedException) { } + catch (ForbiddenException) { } } await TryRemoveOriginalReaction(evt); From cddecb7558c127ba438d324e6feac7070941403c Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 20:16:28 +0100 Subject: [PATCH 033/608] Upgrade exception filter --- PluralKit.Bot/Utils/MiscUtils.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PluralKit.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index 041f7ae7..abd373c0 100644 --- a/PluralKit.Bot/Utils/MiscUtils.cs +++ b/PluralKit.Bot/Utils/MiscUtils.cs @@ -12,6 +12,8 @@ using Npgsql; using PluralKit.Core; +using Polly.Timeout; + namespace PluralKit.Bot { public static class MiscUtils { @@ -68,6 +70,11 @@ namespace PluralKit.Bot if (e is NotFoundException ne && ne.ResponseBody.Contains("
nginx
")) return false; if (e is UnauthorizedException ue && ue.ResponseBody.Contains("
nginx
")) return false; + // Filter out timeout/ratelimit related stuff + if (e is TooManyRequestsException) return false; + if (e is RatelimitBucketExhaustedException) return false; + if (e is TimeoutRejectedException) return false; + // 5xxs? also not our problem :^) if (e is UnknownDiscordRequestException udre && (int) udre.StatusCode >= 500) return false; From 4231709967092832dae549b2f3da56d5b9a6d6fb Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 20:21:36 +0100 Subject: [PATCH 034/608] Remove file size limit on logs --- PluralKit.Core/Modules/LoggingModule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PluralKit.Core/Modules/LoggingModule.cs b/PluralKit.Core/Modules/LoggingModule.cs index fd52293f..54f8a3af 100644 --- a/PluralKit.Core/Modules/LoggingModule.cs +++ b/PluralKit.Core/Modules/LoggingModule.cs @@ -71,6 +71,7 @@ namespace PluralKit.Core (config.LogDir ?? "logs") + $"/pluralkit.{_component}.log", outputTemplate: outputTemplate, rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: null, flushToDiskInterval: TimeSpan.FromMilliseconds(50), restrictedToMinimumLevel: config.FileLogLevel, formatProvider: new UTCTimestampFormatProvider(), From c7daea549769722795eac5dc339485c82db97d2a Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 18 Mar 2021 20:21:57 +0100 Subject: [PATCH 035/608] Lower log rotation retention --- PluralKit.Core/Modules/LoggingModule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PluralKit.Core/Modules/LoggingModule.cs b/PluralKit.Core/Modules/LoggingModule.cs index 54f8a3af..7a9ffdaa 100644 --- a/PluralKit.Core/Modules/LoggingModule.cs +++ b/PluralKit.Core/Modules/LoggingModule.cs @@ -70,6 +70,7 @@ namespace PluralKit.Core a.File( (config.LogDir ?? "logs") + $"/pluralkit.{_component}.log", outputTemplate: outputTemplate, + retainedFileCountLimit: 10, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: null, flushToDiskInterval: TimeSpan.FromMilliseconds(50), From 01ea0d99716dac1c3767eb2330c8aa110af8efd3 Mon Sep 17 00:00:00 2001 From: spiral Date: Sun, 21 Mar 2021 09:45:26 +0000 Subject: [PATCH 036/608] Add 'pk;msg delete' --- Myriad/Rest/DiscordApiClient.cs | 3 ++- PluralKit.Bot/Commands/CommandTree.cs | 2 +- PluralKit.Bot/Commands/Misc.cs | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 257b3be6..4612fd2c 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -54,7 +54,8 @@ namespace Myriad.Rest public Task DeleteMessage(ulong channelId, ulong messageId) => _client.Delete($"/channels/{channelId}/messages/{messageId}", ("DeleteMessage", channelId)); - + public Task DeleteMessage(Message message) => + _client.Delete($"/channels/{message.ChannelId}/messages/{message.Id}", ("DeleteMessage", message.ChannelId)); public Task CreateReaction(ulong channelId, ulong messageId, Emoji emoji) => _client.Put($"/channels/{channelId}/messages/{messageId}/reactions/{EncodeEmoji(emoji)}/@me", ("CreateReaction", channelId), null); diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index f418d784..6363c5e7 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -75,7 +75,7 @@ namespace PluralKit.Bot public static Command Export = new Command("export", "export", "Exports system information to a data file"); public static Command Help = new Command("help", "help", "Shows help information about PluralKit"); public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying"); - public static Command Message = new Command("message", "message ", "Looks up a proxied message"); + public static Command Message = new Command("message", "message [delete]", "Looks up a proxied message"); public static Command LogChannel = new Command("log channel", "log channel ", "Designates a channel to post proxied messages to"); public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 5f89c245..ad04c60a 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -222,6 +222,15 @@ namespace PluralKit.Bot { var message = await _db.Execute(c => _repo.GetMessage(c, messageId)); if (message == null) throw Errors.MessageNotFound(messageId); + if (ctx.Match("delete") || ctx.MatchFlag("delete")) + { + if (message.System.Id != ctx.System.Id) + throw new PKError("You can only delete your own messages."); + await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid); + await ctx.Rest.DeleteMessage(ctx.Message); + return; + } + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } } From a63e75a490ea18b877582120ae86b14462260e94 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 21 Mar 2021 14:15:13 +0100 Subject: [PATCH 037/608] Properly remake request object on retry --- Myriad/Rest/BaseRestClient.cs | 50 ++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Myriad/Rest/BaseRestClient.cs b/Myriad/Rest/BaseRestClient.cs index 40a85c68..374640a6 100644 --- a/Myriad/Rest/BaseRestClient.cs +++ b/Myriad/Rest/BaseRestClient.cs @@ -69,8 +69,8 @@ namespace Myriad.Rest public async Task Get(string path, (string endpointName, ulong major) ratelimitParams) where T: class { - var request = new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + path); - var response = await Send(request, ratelimitParams, true); + using var response = await Send(() => new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + path), + ratelimitParams, true); // GET-only special case: 404s are nulls and not exceptions if (response.StatusCode == HttpStatusCode.NotFound) @@ -82,47 +82,54 @@ namespace Myriad.Rest public async Task Post(string path, (string endpointName, ulong major) ratelimitParams, object? body) where T: class { - var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); - SetRequestJsonBody(request, body); - - var response = await Send(request, ratelimitParams); + using var response = await Send(() => + { + var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + return request; + }, ratelimitParams); return await ReadResponse(response); } public async Task PostMultipart(string path, (string endpointName, ulong major) ratelimitParams, object? payload, MultipartFile[]? files) where T: class { - var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); - SetRequestFormDataBody(request, payload, files); - - var response = await Send(request, ratelimitParams); + using var response = await Send(() => + { + var request = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + path); + SetRequestFormDataBody(request, payload, files); + return request; + }, ratelimitParams); return await ReadResponse(response); } public async Task Patch(string path, (string endpointName, ulong major) ratelimitParams, object? body) where T: class { - var request = new HttpRequestMessage(HttpMethod.Patch, ApiBaseUrl + path); - SetRequestJsonBody(request, body); - - var response = await Send(request, ratelimitParams); + using var response = await Send(() => + { + var request = new HttpRequestMessage(HttpMethod.Patch, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + return request; + }, ratelimitParams); return await ReadResponse(response); } public async Task Put(string path, (string endpointName, ulong major) ratelimitParams, object? body) where T: class { - var request = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + path); - SetRequestJsonBody(request, body); - - var response = await Send(request, ratelimitParams); + using var response = await Send(() => + { + var request = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + path); + SetRequestJsonBody(request, body); + return request; + }, ratelimitParams); return await ReadResponse(response); } public async Task Delete(string path, (string endpointName, ulong major) ratelimitParams) { - var request = new HttpRequestMessage(HttpMethod.Delete, ApiBaseUrl + path); - await Send(request, ratelimitParams); + using var _ = await Send(() => new HttpRequestMessage(HttpMethod.Delete, ApiBaseUrl + path), ratelimitParams); } private void SetRequestJsonBody(HttpRequestMessage request, object? body) @@ -159,12 +166,13 @@ namespace Myriad.Rest return await response.Content.ReadFromJsonAsync(_jsonSerializerOptions); } - private async Task Send(HttpRequestMessage request, + private async Task Send(Func createRequest, (string endpointName, ulong major) ratelimitParams, bool ignoreNotFound = false) { return await _retryPolicy.ExecuteAsync(async _ => { + var request = createRequest(); _logger.Debug("Sending request: {RequestMethod} {RequestPath}", request.Method, request.RequestUri); From ce790b13aea103247a7384d5c0eca78973caeec6 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 21 Mar 2021 14:22:55 +0100 Subject: [PATCH 038/608] Fix DM events not having proper channel data --- Myriad/Cache/DiscordCacheExtensions.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index aa0541fb..5c3e26d6 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -28,8 +28,16 @@ namespace Myriad.Cache return cache.SaveRole(gru.GuildId, gru.Role); case GuildRoleDeleteEvent grd: return cache.RemoveRole(grd.GuildId, grd.RoleId); + case MessageReactionAddEvent mra: + return cache.TrySaveDmChannelStub(mra.GuildId, mra.ChannelId); case MessageCreateEvent mc: return cache.SaveMessageCreate(mc); + case MessageUpdateEvent mu: + return cache.TrySaveDmChannelStub(mu.GuildId.Value, mu.ChannelId); + case MessageDeleteEvent md: + return cache.TrySaveDmChannelStub(md.GuildId, md.ChannelId); + case MessageDeleteBulkEvent md: + return cache.TrySaveDmChannelStub(md.GuildId, md.ChannelId); } return default; @@ -49,14 +57,18 @@ namespace Myriad.Cache private static async ValueTask SaveMessageCreate(this IDiscordCache cache, MessageCreateEvent evt) { - // DM messages don't get Channel Create events first, so we need to save - // some kind of stub channel object until we get the real one - if (evt.GuildId == null) - await cache.SaveDmChannelStub(evt.ChannelId); + await cache.TrySaveDmChannelStub(evt.GuildId, evt.ChannelId); await cache.SaveUser(evt.Author); foreach (var mention in evt.Mentions) await cache.SaveUser(mention); } + + private static ValueTask TrySaveDmChannelStub(this IDiscordCache cache, ulong? guildId, ulong channelId) + { + // DM messages don't get Channel Create events first, so we need to save + // some kind of stub channel object until we get the real one + return guildId == null ? default : cache.SaveDmChannelStub(channelId); + } } } \ No newline at end of file From c5f56cd7ddd63de02e3e055c445a092aeae07463 Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 21 Mar 2021 14:38:28 +0100 Subject: [PATCH 039/608] Add correct serialization for gateway user status --- Myriad/Gateway/Payloads/GatewayStatusUpdate.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs b/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs index 23ad01b7..7ca57090 100644 --- a/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs +++ b/Myriad/Gateway/Payloads/GatewayStatusUpdate.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; +using System.Text.Json.Serialization; +using Myriad.Serialization; using Myriad.Types; namespace Myriad.Gateway { public record GatewayStatusUpdate { + [JsonConverter(typeof(JsonSnakeCaseStringEnumConverter))] public enum UserStatus { Online, From c02a7572b3152beb4b9af32aca3ef9149b8c2d3d Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 21 Mar 2021 16:03:42 +0100 Subject: [PATCH 040/608] i am good at programming --- Myriad/Cache/DiscordCacheExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index 5c3e26d6..03fbc9b4 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -68,7 +68,7 @@ namespace Myriad.Cache { // DM messages don't get Channel Create events first, so we need to save // some kind of stub channel object until we get the real one - return guildId == null ? default : cache.SaveDmChannelStub(channelId); + return guildId != null ? default : cache.SaveDmChannelStub(channelId); } } } \ No newline at end of file From 3471f7f28c24ae1f319182a718ff665f9d798a3f Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 21 Mar 2021 23:05:56 +0100 Subject: [PATCH 041/608] Fix error when proxying duplicate pings --- PluralKit.Bot/Utils/DiscordUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index c581c12b..9be30f4e 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -106,8 +106,8 @@ namespace PluralKit.Bot return new AllowedMentions { - Users = users.ToArray(), - Roles = roles.ToArray(), + Users = users.Distinct().ToArray(), + Roles = roles.Distinct().ToArray(), Parse = everyone ? new[] {AllowedMentions.ParseType.Everyone} : null }; } From a7189fab8c92d8c034f4eb808e663f70f6b7a78c Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 22 Mar 2021 07:07:33 +0000 Subject: [PATCH 042/608] Fix handling replies in edited messages --- PluralKit.Bot/Handlers/MessageCreated.cs | 4 ++-- PluralKit.Bot/Handlers/MessageEdited.cs | 14 +++++++++--- .../Services/LastMessageCacheService.cs | 22 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index b5fc477a..30ced3ee 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -53,7 +53,7 @@ namespace PluralKit.Bot private bool IsDuplicateMessage(Message msg) => // We consider a message duplicate if it has the same ID as the previous message that hit the gateway - _lastMessageCache.GetLastMessage(msg.ChannelId) == msg.Id; + _lastMessageCache.GetLastMessage(msg.ChannelId)?.mid == msg.Id; public async Task Handle(Shard shard, MessageCreateEvent evt) { @@ -66,7 +66,7 @@ namespace PluralKit.Bot // Log metrics and message info _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); - _lastMessageCache.AddMessage(evt.ChannelId, evt.Id); + _lastMessageCache.AddMessage(evt); // Get message context from DB (tracking w/ metrics) MessageContext ctx; diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index a00f7b22..4633bbae 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -6,6 +6,7 @@ using App.Metrics; using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; +using Myriad.Rest; using Myriad.Types; using PluralKit.Core; @@ -23,8 +24,10 @@ namespace PluralKit.Bot private readonly Cluster _client; private readonly IDiscordCache _cache; private readonly Bot _bot; + private readonly DiscordApiClient _rest; + - public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot) + public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot, DiscordApiClient rest) { _lastMessageCache = lastMessageCache; _proxy = proxy; @@ -34,6 +37,7 @@ namespace PluralKit.Bot _client = client; _cache = cache; _bot = bot; + _rest = rest; } public async Task Handle(Shard shard, MessageUpdateEvent evt) @@ -48,9 +52,10 @@ namespace PluralKit.Bot if (channel.Type != Channel.ChannelType.GuildText) return; var guild = _cache.GetGuild(channel.GuildId!.Value); + var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId); // Only react to the last message in the channel - if (_lastMessageCache.GetLastMessage(evt.ChannelId) != evt.Id) + if (lastMessage?.mid != evt.Id) return; // Just run the normal message handling code, with a flag to disable autoproxying @@ -68,7 +73,10 @@ namespace PluralKit.Bot Author = evt.Author.Value, Member = evt.Member.Value, Content = evt.Content.Value, - Attachments = evt.Attachments.Value ?? Array.Empty() + Attachments = evt.Attachments.Value ?? Array.Empty(), + MessageReference = (lastMessage.referenced_message.HasValue) ? new (channel.GuildId, evt.ChannelId, lastMessage.referenced_message.Value.Id) : null, + ReferencedMessage = (lastMessage.referenced_message.HasValue) ? lastMessage.referenced_message : null, + Type = (lastMessage.referenced_message.HasValue) ? Message.MessageType.Reply : Message.MessageType.Default, }; var botPermissions = _bot.PermissionsIn(channel.Id); await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy: false, guild: guild, channel: channel, botPermissions: botPermissions); diff --git a/PluralKit.Bot/Services/LastMessageCacheService.cs b/PluralKit.Bot/Services/LastMessageCacheService.cs index 6b88e18a..084b39a2 100644 --- a/PluralKit.Bot/Services/LastMessageCacheService.cs +++ b/PluralKit.Bot/Services/LastMessageCacheService.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using Myriad.Types; + namespace PluralKit.Bot { // Doing things like this instead of enabling D.NET's message cache because the message cache is, let's face it, @@ -10,17 +12,29 @@ namespace PluralKit.Bot // TODO: is this still needed after the D#+ migration? public class LastMessageCacheService { - private readonly IDictionary _cache = new ConcurrentDictionary(); + private readonly IDictionary _cache = new ConcurrentDictionary(); - public void AddMessage(ulong channel, ulong message) + public void AddMessage(Message msg) { - _cache[channel] = message; + _cache[msg.ChannelId] = new CachedMessage(msg); } - public ulong? GetLastMessage(ulong channel) + public CachedMessage GetLastMessage(ulong channel) { if (_cache.TryGetValue(channel, out var message)) return message; return null; } } + + public class CachedMessage + { + public ulong mid; + public Myriad.Utils.Optional referenced_message; + + public CachedMessage(Message msg) + { + mid = msg.Id; + referenced_message = msg.ReferencedMessage; + } + } } \ No newline at end of file From 0acadee8034d91bcdd0f0a99e56a8ec712d46fc1 Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 22 Mar 2021 07:17:10 +0000 Subject: [PATCH 043/608] don't cache the full message --- PluralKit.Bot/Handlers/MessageEdited.cs | 8 +++++--- PluralKit.Bot/Services/LastMessageCacheService.cs | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index 4633bbae..816914fe 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -64,6 +64,8 @@ namespace PluralKit.Bot using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) ctx = await _repo.GetMessageContext(conn, evt.Author.Value!.Id, channel.GuildId!.Value, evt.ChannelId); + Message referencedMessage = (lastMessage.referenced_message != null) ? await _rest.GetMessage(evt.ChannelId, lastMessage.referenced_message.Value) : null; + // TODO: is this missing anything? var equivalentEvt = new MessageCreateEvent { @@ -74,9 +76,9 @@ namespace PluralKit.Bot Member = evt.Member.Value, Content = evt.Content.Value, Attachments = evt.Attachments.Value ?? Array.Empty(), - MessageReference = (lastMessage.referenced_message.HasValue) ? new (channel.GuildId, evt.ChannelId, lastMessage.referenced_message.Value.Id) : null, - ReferencedMessage = (lastMessage.referenced_message.HasValue) ? lastMessage.referenced_message : null, - Type = (lastMessage.referenced_message.HasValue) ? Message.MessageType.Reply : Message.MessageType.Default, + MessageReference = (lastMessage.referenced_message != null) ? new (channel.GuildId, evt.ChannelId, lastMessage.referenced_message.Value) : null, + ReferencedMessage = referencedMessage, + Type = (lastMessage.referenced_message != null) ? Message.MessageType.Reply : Message.MessageType.Default, }; var botPermissions = _bot.PermissionsIn(channel.Id); await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy: false, guild: guild, channel: channel, botPermissions: botPermissions); diff --git a/PluralKit.Bot/Services/LastMessageCacheService.cs b/PluralKit.Bot/Services/LastMessageCacheService.cs index 084b39a2..9ebbbc50 100644 --- a/PluralKit.Bot/Services/LastMessageCacheService.cs +++ b/PluralKit.Bot/Services/LastMessageCacheService.cs @@ -29,12 +29,13 @@ namespace PluralKit.Bot public class CachedMessage { public ulong mid; - public Myriad.Utils.Optional referenced_message; + public ulong? referenced_message; public CachedMessage(Message msg) { mid = msg.Id; - referenced_message = msg.ReferencedMessage; + if (msg.ReferencedMessage.HasValue) + referenced_message = msg.ReferencedMessage.Value.Id; } } } \ No newline at end of file From 4d86acafc31a4a9b2e4c773e037b8bb7fd0461b6 Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 22 Mar 2021 11:41:50 +0000 Subject: [PATCH 044/608] Update FAQ --- docs/content/faq.md | 70 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/content/faq.md b/docs/content/faq.md index 53400a88..c5282ac2 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -6,17 +6,73 @@ permalink: /faq # Frequently asked questions -## Can I use this bot for kin/roleplay/other non-plurality uses? Can I use it if I'm not plural myself? Is that appropriating? +## General + +### What is this bot for? +PluralKit detects messages with certain prefixes and/or suffixes associated with a registered profile, then replaces that message under a "pseudo-account" of that profile using things called webhooks. This is useful for multiple people sharing one body (aka "systems"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account. + +### Can I use this bot for kin/roleplay/other non-plurality uses? Can I use it if I'm not plural myself? Is that appropriating? Although this bot is designed with plural systems and their use cases in mind, the bot's feature set is still useful for many other types of communities, including role-playing and otherkin. By all means go ahead and use it for those communities, too. We don't gatekeep, and neither should you. -## Who's the mascot? -[Our lovely bot mascot](https://imgur.com/a/LTqQHHL)'s name is Myriad! They were drawn by the lovely [Layl](https://twitter.com/braindemons). Yes, there are fictives. +### Who's the mascot? +[Our lovely bot mascot](/favicon.png)'s name is Myriad! They were drawn by the lovely [Layl](https://twitter.com/braindemons). Yes, there are fictives. -## How can I support the bot's development? +### How do I suggest features to be added to PluralKit? + +You can suggest features in the [support server](https://discord.gg/PczBt78)'s `#suggestions-feedback` channel. Check the `#frequent-suggestions` channel to see if your idea has already been suggested! + +We also track feature requests through [Github Issues](https://github.com/xSke/PluralKit/issues). Feel free to open issue reports or feature requests there as well. + +### How can I support the bot's development? I (the bot author, [Ske](https://twitter.com/floofstrid)) have a Patreon. The income from there goes towards server hosting, domains, infrastructure, my Monster Energy addiction, et cetera. There are no benefits. There might never be any. But nevertheless, it can be found here: [https://www.patreon.com/floofstrid](https://www.patreon.com/floofstrid) -## The name color doesn't work/can we color our proxy names? +## Privacy / safety + +### Who has access to the bot's data? + +The only people with access to the database or the information the bot processes is the developer, no one else has access. More information about how information is processed is described on the [Privacy Policy](/privacy). + +But in short: the bot does not save or log messages beyond metadata necessary for the bot's functioning, and we do not have the ability to read messages going through the bot, proxied or not. The bot is [open-source](https://github.com/xSke/PluralKit), so anyone with technical knowledge can confirm this. + +### I set all my privacy options to private. Why can others still see some information? +There are two possible answers here: + +1. Your system privacy options are set to private. You must set *each member*'s privacy options to private if you wish to hide that member's information. + +2. There is some information that, for moderation reasons, must always remain public. +This includes: + * a system's linked accounts; + * the information that would be shown when a member proxies (proxy name/avatar image); + * and the system ID on a member card. + +### Is there a way to restrict PluralKit usage to a certain role? / Can I remove PluralKit access for specific users in my server? +This is not a feature currently available in PluralKit. It may be added in the future. +In the meantime, this feature is supported in Tupperbox (an alternative proxying bot) - ask about it in their support server: + +### Is it possible to block proxied messages (like blocking a user)? +No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/ + +### What can people see if I have all the privacy options on a member set to private? +You can see an example of exactly this by querying `cmpuv` with the command `pk;m cmpuv`. (Also useful: there is an example system with every field filled, you can use `pk;s exmpl`) + +## Known issues + +### The name color doesn't work/can we color our proxy names? No. This is a limitation in Discord itself, and cannot be changed. The color command instead colors your member card that come up when you type `pk;member `. -## Is it possible to block proxied messages (like blocking a user)? -No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/ \ No newline at end of file +### Why does my avatar not work? +* PluralKit doesn't check if the avatar will be accepted by Discord. If you just set your avatar and it's not showing up, please try a different avatar. +* The message with the avatar may have been deleted, or the link may have become invalid. Try re-setting your avatar. +* Discord sometimes has issues displaying avatars. We can't do anything about that, sorry :( + +### Why can't I use nitro emotes in some channels? +Webhooks inherit the nitro emoji permissions from the `@everyone` role, so the `@everyone` role must have the permission "Use External Emoji" to be able to use nitro emotes through PluralKit. + +### Why can't I invite PluralKit to my server? + +You might not be logged into the invite screen. Make sure you're logged into Discord in your browser, and check that you have permissions to add the bot to the server. If it still doesn't work, try logging out of Discord in your browser, logging back in, then re-inviting the bot. + +If you are on mobile and are having issues, try copy-pasting the link into your browser instead of tapping it. + +### Why are my member lists broken on mobile? +This is a bug in Discord's mobile client. It handles formatting slightly different than other clients. This can't be fixed without breaking things elsewhere, so we're waiting for a fix on their end. \ No newline at end of file From 66e483be6f1202ee60b888b38fd3c8f670f05c4c Mon Sep 17 00:00:00 2001 From: spiral Date: Thu, 25 Mar 2021 18:02:44 +0000 Subject: [PATCH 045/608] Fix NullReferenceError when HasValue is true but Value is null --- PluralKit.Bot/Services/LastMessageCacheService.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/Services/LastMessageCacheService.cs b/PluralKit.Bot/Services/LastMessageCacheService.cs index 9ebbbc50..1a8b1d17 100644 --- a/PluralKit.Bot/Services/LastMessageCacheService.cs +++ b/PluralKit.Bot/Services/LastMessageCacheService.cs @@ -5,11 +5,7 @@ using Myriad.Types; namespace PluralKit.Bot { - // Doing things like this instead of enabling D.NET's message cache because the message cache is, let's face it, - // not particularly efficient? It allocates a dictionary *and* a queue for every single channel (500k in prod!) - // whereas this is, worst case, one dictionary *entry* of a single ulong per channel, and one dictionary instance - // on the whole instance, total. Yeah, much more efficient. - // TODO: is this still needed after the D#+ migration? + // TODO: Should this be moved to Myriad.Cache? public class LastMessageCacheService { private readonly IDictionary _cache = new ConcurrentDictionary(); @@ -34,8 +30,8 @@ namespace PluralKit.Bot public CachedMessage(Message msg) { mid = msg.Id; - if (msg.ReferencedMessage.HasValue) + if (msg.ReferencedMessage.Value != null) referenced_message = msg.ReferencedMessage.Value.Id; } } -} \ No newline at end of file +} From 3603d52e9eac33bdc87a9ba0fd0768ec8746848e Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:02:41 +0200 Subject: [PATCH 046/608] Add group and system colors --- .gitignore | 3 +- PluralKit.Bot/Commands/CommandTree.cs | 10 ++++- PluralKit.Bot/Commands/Groups.cs | 48 ++++++++++++++++++++++ PluralKit.Bot/Commands/SystemEdit.cs | 42 +++++++++++++++++++ PluralKit.Bot/Services/EmbedService.cs | 25 ++++++++++- PluralKit.Core/Database/Database.cs | 2 +- PluralKit.Core/Database/Migrations/13.sql | 7 ++++ PluralKit.Core/Models/PKGroup.cs | 1 + PluralKit.Core/Models/PKSystem.cs | 1 + PluralKit.Core/Models/Patch/GroupPatch.cs | 2 + PluralKit.Core/Models/Patch/SystemPatch.cs | 2 + 11 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 PluralKit.Core/Database/Migrations/13.sql diff --git a/.gitignore b/.gitignore index 35c2cd6f..764fd1b3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ pluralkit.*.conf *.DotSettings.user # Generated -logs/ \ No newline at end of file +logs/ +*.dll diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index ad763670..a36f9090 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -13,6 +13,7 @@ namespace PluralKit.Bot public static Command SystemNew = new Command("system new", "system new [name]", "Creates a new system"); public static Command SystemRename = new Command("system name", "system rename [name]", "Renames your system"); public static Command SystemDesc = new Command("system description", "system description [description]", "Changes your system's description"); + public static Command SystemColor = new Command("system color", "system color [color]", "Changes your system's color"); public static Command SystemTag = new Command("system tag", "system tag [tag]", "Changes your system's tag"); public static Command SystemAvatar = new Command("system icon", "system icon [url|@mention]", "Changes your system's icon"); public static Command SystemDelete = new Command("system delete", "system delete", "Deletes your system"); @@ -55,6 +56,7 @@ namespace PluralKit.Bot public static Command GroupRename = new Command("group rename", "group rename ", "Renames a group"); public static Command GroupDisplayName = new Command("group displayname", "group displayname [display name]", "Changes a group's display name"); public static Command GroupDesc = new Command("group description", "group description [description]", "Changes a group's description"); + public static Command GroupColor = new Command("group color", "group color [color]", "Changes a group's color"); public static Command GroupAdd = new Command("group add", "group add [member 2] [member 3...]", "Adds one or more members to a group"); public static Command GroupRemove = new Command("group remove", "group remove [member 2] [member 3...]", "Removes one or more members from a group"); public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); @@ -89,7 +91,7 @@ namespace PluralKit.Bot public static Command PermCheck = new Command("permcheck", "permcheck ", "Checks whether a server's permission setup is correct"); public static Command[] SystemCommands = { - SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone, + SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemColor, SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy, SystemProxy }; @@ -102,7 +104,7 @@ namespace PluralKit.Bot public static Command[] GroupCommands = { GroupInfo, GroupList, GroupNew, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, - GroupIcon, GroupPrivacy, GroupDelete + GroupIcon, GroupColor, GroupPrivacy, GroupDelete }; public static Command[] GroupCommandsTargeted = @@ -218,6 +220,8 @@ namespace PluralKit.Bot await ctx.Execute(SystemTag, m => m.Tag(ctx)); else if (ctx.Match("description", "desc", "bio")) await ctx.Execute(SystemDesc, m => m.Description(ctx)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(SystemColor, m => m.Color(ctx)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(SystemAvatar, m => m.Avatar(ctx)); else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) @@ -398,6 +402,8 @@ namespace PluralKit.Bot await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) await ctx.Execute(GroupFrontPercent, g => g.GroupFrontPercent(ctx, target)); else if (!ctx.HasNext()) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 4259dff8..6c68bb24 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Dapper; @@ -226,6 +227,53 @@ namespace PluralKit.Bot else await ShowIcon(); } + public async Task GroupColor(Context ctx, PKGroup target) + { + var color = ctx.RemainderOrNull(); + if (await ctx.MatchClear()) + { + ctx.CheckOwnGroup(target); + + var patch = new GroupPatch {Color = Partial.Null()}; + await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); + + await ctx.Reply($"{Emojis.Success} Group color cleared."); + } + else if (!ctx.HasNext()) + { + + if (target.Color == null) + if (ctx.System?.Id == target.System) + await ctx.Reply( + $"This group does not have a color set. To set one, type `pk;group {target.Reference()} color `."); + else + await ctx.Reply("This group does not have a color set."); + else + await ctx.Reply(embed: new EmbedBuilder() + .Title("Group color") + .Color(target.Color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) + .Description($"This group's color is **#{target.Color}**." + + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} color -clear`." : "")) + .Build()); + } + else + { + ctx.CheckOwnGroup(target); + + if (color.StartsWith("#")) color = color.Substring(1); + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + + var patch = new GroupPatch {Color = Partial.Present(color.ToLowerInvariant())}; + await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Group color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) + .Build()); + } + } public async Task ListSystemGroups(Context ctx, PKSystem system) { diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 7895b932..915d3bd8 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Myriad.Builders; @@ -91,6 +92,47 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} System description changed."); } } + + public async Task Color(Context ctx) { + ctx.CheckSystem(); + + if (await ctx.MatchClear()) + { + var patch = new SystemPatch {Color = Partial.Null()}; + await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); + + await ctx.Reply($"{Emojis.Success} System color cleared."); + } + else if (!ctx.HasNext()) + { + if (ctx.System.Color == null) + await ctx.Reply( + $"Your system does not have a color set. To set one, type `pk;system color `."); + else + await ctx.Reply(embed: new EmbedBuilder() + .Title("System color") + .Color(ctx.System.Color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{ctx.System.Color}/?text=%20")) + .Description($"Your system's color is **#{ctx.System.Color}**. To clear it, type `pk;s color -clear`.") + .Build()); + } + else + { + var color = ctx.RemainderOrNull(); + + if (color.StartsWith("#")) color = color.Substring(1); + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + + var patch = new SystemPatch {Color = Partial.Present(color.ToLowerInvariant())}; + await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Member color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) + .Build()); + } + } public async Task Tag(Context ctx) { diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 5528f1cf..c0ec8265 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -53,11 +53,22 @@ namespace PluralKit.Bot { var memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id); + uint color; + try + { + color = system.Color?.ToDiscordColor() ?? DiscordUtils.Gray; + } + catch (ArgumentException) + { + // There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea + color = DiscordUtils.Gray; + } + var eb = new EmbedBuilder() .Title(system.Name) .Thumbnail(new(system.AvatarUrl)) .Footer(new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}")) - .Color(DiscordUtils.Gray); + .Color(color); var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) @@ -187,8 +198,20 @@ namespace PluralKit.Bot { if (system.Name != null) nameField = $"{nameField} ({system.Name})"; + uint color; + try + { + color = target.Color?.ToDiscordColor() ?? DiscordUtils.Gray; + } + catch (ArgumentException) + { + // There's no API for group colors yet, but defaulting to a blank color regardless + color = DiscordUtils.Gray; + } + var eb = new EmbedBuilder() .Author(new(nameField, IconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx)))) + .Color(color) .Footer(new($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}")); if (target.DisplayName != null) diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 72a8591e..043b9e68 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -19,7 +19,7 @@ namespace PluralKit.Core internal class Database: IDatabase { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 12; + private const int TargetSchemaVersion = 13; private readonly CoreConfig _config; private readonly ILogger _logger; diff --git a/PluralKit.Core/Database/Migrations/13.sql b/PluralKit.Core/Database/Migrations/13.sql new file mode 100644 index 00000000..0ada9d65 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/13.sql @@ -0,0 +1,7 @@ +-- SCHEMA VERSION 13: 2021-03-28 -- +-- Add system and group colors -- + +alter table systems add column color char(6); +alter table groups add column color char(6); + +update info set schema_version = 13; \ No newline at end of file diff --git a/PluralKit.Core/Models/PKGroup.cs b/PluralKit.Core/Models/PKGroup.cs index 7a45e900..21bea358 100644 --- a/PluralKit.Core/Models/PKGroup.cs +++ b/PluralKit.Core/Models/PKGroup.cs @@ -13,6 +13,7 @@ namespace PluralKit.Core public string? DisplayName { get; private set; } public string? Description { get; private set; } public string? Icon { get; private set; } + public string? Color { get; private set; } public PrivacyLevel DescriptionPrivacy { get; private set; } public PrivacyLevel IconPrivacy { get; private set; } diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index 436f9cc9..88d42266 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -14,6 +14,7 @@ namespace PluralKit.Core { public string Description { get; } public string Tag { get; } public string AvatarUrl { get; } + public string Color { get; } public string Token { get; } public Instant Created { get; } public string UiTz { get; set; } diff --git a/PluralKit.Core/Models/Patch/GroupPatch.cs b/PluralKit.Core/Models/Patch/GroupPatch.cs index 2f154c90..ee624df8 100644 --- a/PluralKit.Core/Models/Patch/GroupPatch.cs +++ b/PluralKit.Core/Models/Patch/GroupPatch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Core public Partial DisplayName { get; set; } public Partial Description { get; set; } public Partial Icon { get; set; } + public Partial Color { get; set; } public Partial DescriptionPrivacy { get; set; } public Partial IconPrivacy { get; set; } @@ -18,6 +19,7 @@ namespace PluralKit.Core .With("display_name", DisplayName) .With("description", Description) .With("icon", Icon) + .With("color", Color) .With("description_privacy", DescriptionPrivacy) .With("icon_privacy", IconPrivacy) .With("list_privacy", ListPrivacy) diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index 7c3a9551..0f787749 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Core public Partial Description { get; set; } public Partial Tag { get; set; } public Partial AvatarUrl { get; set; } + public Partial Color { get; set; } public Partial Token { get; set; } public Partial UiTz { get; set; } public Partial DescriptionPrivacy { get; set; } @@ -22,6 +23,7 @@ namespace PluralKit.Core .With("description", Description) .With("tag", Tag) .With("avatar_url", AvatarUrl) + .With("color", Color) .With("token", Token) .With("ui_tz", UiTz) .With("description_privacy", DescriptionPrivacy) From ba30a96c7cf1462115dee070f0fa9962fcd6531b Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:07:25 +0200 Subject: [PATCH 047/608] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 764fd1b3..3c4fe014 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,3 @@ pluralkit.*.conf # Generated logs/ -*.dll From e7cfd85bd5f6b86887239a3af6acd2a63c6057f3 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:15:21 +0200 Subject: [PATCH 048/608] I messed up the branches oops --- PluralKit.Bot/Commands/CommandTree.cs | 2 -- PluralKit.Bot/Commands/Groups.cs | 25 ------------------- PluralKit.Bot/Commands/SystemFront.cs | 2 +- .../Repository/ModelRepository.Switch.cs | 15 +++-------- PluralKit.Core/Services/DataFileService.cs | 2 +- 5 files changed, 6 insertions(+), 40 deletions(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index a36f9090..08c56ba6 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -404,8 +404,6 @@ namespace PluralKit.Bot await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); else if (ctx.Match("color", "colour")) await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); - else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.Execute(GroupFrontPercent, g => g.GroupFrontPercent(ctx, target)); else if (!ctx.HasNext()) await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); else diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 6c68bb24..f9b7b73e 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -478,31 +478,6 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Group deleted."); } - public async Task GroupFrontPercent(Context ctx, PKGroup target) - { - await using var conn = await _db.Obtain(); - - var targetSystem = await GetGroupSystem(ctx, target, conn); - ctx.CheckSystemPrivacy(targetSystem, targetSystem.FrontHistoryPrivacy); - - string durationStr = ctx.RemainderOrNull() ?? "30d"; - - var now = SystemClock.Instance.GetCurrentInstant(); - - var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone); - if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); - if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; - - var title = new StringBuilder($"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in "); - if (targetSystem.Name != null) - title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)"); - else - title.Append($"`{targetSystem.Hid}`"); - - var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, targetSystem.Id, target.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString())); - } - private async Task GetGroupSystem(Context ctx, PKGroup target, IPKConnection conn) { var system = ctx.System; diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index d9531216..9121be04 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -130,7 +130,7 @@ namespace PluralKit.Bot else title.Append($"`{system.Hid}`"); - var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, null, rangeStart.Value.ToInstant(), now)); + var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString())); } } diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs index d8633acf..f313f00a 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs @@ -122,7 +122,7 @@ namespace PluralKit.Core await GetSwitches(conn, system).FirstOrDefaultAsync(); public async Task> GetPeriodFronters(IPKConnection conn, - SystemId system, GroupId? group, Instant periodStart, + SystemId system, Instant periodStart, Instant periodEnd) { // TODO: IAsyncEnumerable-ify this one @@ -139,14 +139,7 @@ namespace PluralKit.Core new {Switches = switchMembers.Select(m => m.Member.Value).Distinct().ToList()}); var memberObjects = membersList.ToDictionary(m => m.Id); - // check if a group ID is provided. if so, query DB for all members of said group, otherwise use membersList - var groupMembersList = group != null ? await conn.QueryAsync( - "select * from members inner join group_members on members.id = group_members.member_id where group_id = @id", - new {id = group}) : membersList; - var groupMemberObjects = groupMembersList.ToDictionary(m => m.Id); - // Initialize entries - still need to loop to determine the TimespanEnd below - // use groupMemberObjects to make sure no members outside of the specified group (if present) are selected var entries = from item in switchMembers group item by item.Timestamp @@ -154,7 +147,7 @@ namespace PluralKit.Core select new SwitchListEntry { TimespanStart = g.Key, - Members = g.Where(x => x.Member != default(MemberId) && groupMemberObjects.Any(m => x.Member == m.Key) ).Select(x => memberObjects[x.Member]) + Members = g.Where(x => x.Member != default(MemberId)).Select(x => memberObjects[x.Member]) .ToList() }; @@ -181,7 +174,7 @@ namespace PluralKit.Core return outList; } - public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, GroupId? group, Instant periodStart, + public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, Instant periodStart, Instant periodEnd) { // TODO: this doesn't belong in the repo @@ -195,7 +188,7 @@ namespace PluralKit.Core var actualStart = periodEnd; // will be "pulled" down var actualEnd = periodStart; // will be "pulled" up - foreach (var sw in await GetPeriodFronters(conn, system, group, periodStart, periodEnd)) + foreach (var sw in await GetPeriodFronters(conn, system, periodStart, periodEnd)) { var span = sw.TimespanEnd - sw.TimespanStart; foreach (var member in sw.Members) diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index cc259d3b..05dd3b37 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -51,7 +51,7 @@ namespace PluralKit.Core // Export switches var switches = new List(); - var switchList = await _repo.GetPeriodFronters(conn, system.Id, null, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); + var switchList = await _repo.GetPeriodFronters(conn, system.Id, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); switches.AddRange(switchList.Select(x => new DataFileSwitch { Timestamp = x.TimespanStart.FormatExport(), From 1e876ce283a0656a155861c30b5d884a0c531f3e Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:20:01 +0200 Subject: [PATCH 049/608] more branch fixing --- PluralKit.Bot/Commands/CommandTree.cs | 3 +-- PluralKit.Bot/Commands/Groups.cs | 2 -- PluralKit.Bot/Commands/SystemFront.cs | 8 +------- PluralKit.Bot/Services/EmbedService.cs | 6 ++---- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 08c56ba6..b76d6261 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -62,7 +62,6 @@ namespace PluralKit.Bot public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); public static Command GroupIcon = new Command("group icon", "group icon [url|@mention]", "Changes a group's icon"); public static Command GroupDelete = new Command("group delete", "group delete", "Deletes a group"); - public static Command GroupFrontPercent = new Command("group frontpercent", "group frontpercent [timespan]", "Shows a group's front breakdown."); public static Command GroupMemberRandom = new Command("group random", "group random", "Shows the info card of a randomly selected member in a group."); public static Command GroupRandom = new Command("random", "random group", "Shows the info card of a randomly selected group in your system."); public static Command Switch = new Command("switch", "switch [member 2] [member 3...]", "Registers a switch"); @@ -110,7 +109,7 @@ namespace PluralKit.Bot public static Command[] GroupCommandsTargeted = { GroupInfo, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, GroupIcon, GroupPrivacy, - GroupDelete, GroupMemberRandom, GroupFrontPercent + GroupDelete, GroupMemberRandom }; public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll}; diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index f9b7b73e..ac4d4425 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -9,8 +9,6 @@ using Dapper; using Humanizer; -using NodaTime; - using Myriad.Builders; using PluralKit.Core; diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 9121be04..6fba56e6 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -124,14 +124,8 @@ namespace PluralKit.Bot if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; - var title = new StringBuilder($"Frontpercent of "); - if (system.Name != null) - title.Append($"{system.Name} (`{system.Hid}`)"); - else - title.Append($"`{system.Hid}`"); - var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString())); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, system.Zone, ctx.LookupContextFor(system), title.ToString())); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index c0ec8265..b80c0aa5 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -316,14 +316,12 @@ namespace PluralKit.Bot { return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx, string embedTitle) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var eb = new EmbedBuilder() - .Title(embedTitle) .Color(DiscordUtils.Gray) - .Footer(new($"System ID: {system.Hid} | {(group != null ? $"Group ID: {group.Hid} | " : "") }Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); - + .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" // We convert to a list of pairs so we can add the no-fronter value From 5a9671559a1f08cc250ec765e40b80fa6db8ad1f Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:21:06 +0200 Subject: [PATCH 050/608] more... branch fixing --- PluralKit.Bot/Commands/SystemFront.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 6fba56e6..3d2f10aa 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -125,7 +125,7 @@ namespace PluralKit.Bot if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, system.Zone, ctx.LookupContextFor(system), title.ToString())); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system), title.ToString())); } } } \ No newline at end of file From 8da5c94b1c40ea7c8e49b93ce29a99d7ffb37282 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 12:22:19 +0200 Subject: [PATCH 051/608] even more branch fixing --- PluralKit.Bot/Commands/SystemFront.cs | 2 +- PluralKit.Bot/Services/EmbedService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 3d2f10aa..3f8cbc0b 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -125,7 +125,7 @@ namespace PluralKit.Bot if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system), title.ToString())); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system))); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index b80c0aa5..ee246a18 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -316,7 +316,7 @@ namespace PluralKit.Bot { return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx, string embedTitle) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var eb = new EmbedBuilder() From 2898b3989af1899dc3b10b9d94e7aeedcb40f585 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 19:22:31 +0200 Subject: [PATCH 052/608] Add color to all lists --- PluralKit.Bot/Commands/Groups.cs | 4 ++-- PluralKit.Bot/Commands/Lists/ContextListExt.cs | 4 ++-- PluralKit.Bot/Commands/ServerConfig.cs | 1 + PluralKit.Bot/Commands/SystemFront.cs | 1 + PluralKit.Bot/Commands/SystemList.cs | 2 +- PluralKit.Bot/Utils/ContextUtils.cs | 4 +++- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index ac4d4425..eb23c685 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -311,7 +311,7 @@ namespace PluralKit.Bot } var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`"; - await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, Renderer); + await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, ctx.System.Color, Renderer); Task Renderer(EmbedBuilder eb, IEnumerable page) { @@ -390,7 +390,7 @@ namespace PluralKit.Bot if (opts.Search != null) title.Append($" matching **{opts.Search}**"); - await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), opts); + await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), target.Color, opts); } public enum AddRemoveOperation diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index acca3b5f..fa990c4b 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -78,7 +78,7 @@ namespace PluralKit.Bot return p; } - public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db, SystemId system, string embedTitle, MemberListOptions opts) + public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db, SystemId system, string embedTitle, string color, MemberListOptions opts) { // We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime // We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout) @@ -87,7 +87,7 @@ namespace PluralKit.Bot .ToList(); var itemsPerPage = opts.Type == ListType.Short ? 25 : 5; - await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer); + await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, color, Renderer); // Base renderer, dispatches based on type Task Renderer(EmbedBuilder eb, IEnumerable page) diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index e29db8d4..12641288 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -107,6 +107,7 @@ namespace PluralKit.Bot await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, $"Blacklisted channels for {ctx.Guild.Name}", + null, (eb, l) => { string CategoryName(ulong? id) => diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 3f8cbc0b..cde6359d 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -69,6 +69,7 @@ namespace PluralKit.Bot totalSwitches, 10, embedTitle, + system.Color, async (builder, switches) => { var sb = new StringBuilder(); diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 493e5bda..8a33e552 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -20,7 +20,7 @@ namespace PluralKit.Bot ctx.CheckSystemPrivacy(target, target.MemberListPrivacy); var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(target)); - await ctx.RenderMemberList(ctx.LookupContextFor(target), _db, target.Id, GetEmbedTitle(target, opts), opts); + await ctx.RenderMemberList(ctx.LookupContextFor(target), _db, target.Id, GetEmbedTitle(target, opts), target.Color, opts); } private string GetEmbedTitle(PKSystem target, MemberListOptions opts) diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 2472c098..9f24d176 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -96,7 +96,7 @@ namespace PluralKit.Bot { return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase); } - public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, Func, Task> renderer) { + public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, string color, Func, Task> renderer) { // TODO: make this generic enough we can use it in Choose below var buffer = new List(); @@ -111,6 +111,8 @@ namespace PluralKit.Bot { var eb = new EmbedBuilder(); eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title); + if (color != null) + eb.Color(color.ToDiscordColor()); await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage)); return eb.Build(); } From c764f922ecb8bcb2bccf42b5db48deb5a9df57ee Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Sun, 28 Mar 2021 19:22:45 +0200 Subject: [PATCH 053/608] Add color field to embeds --- PluralKit.Bot/Services/EmbedService.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index ee246a18..2a9d5bff 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -79,7 +79,10 @@ namespace PluralKit.Bot { } if (system.Tag != null) - eb.Field(new("Tag", system.Tag.EscapeMarkdown())); + eb.Field(new("Tag", system.Tag.EscapeMarkdown(), true)); + + if (!system.Color.EmptyOrNull()) eb.Field(new("Color", $"#{system.Color}", true)); + eb.Field(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); if (system.MemberListPrivacy.CanAccess(ctx)) @@ -215,15 +218,17 @@ namespace PluralKit.Bot { .Footer(new($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}")); if (target.DisplayName != null) - eb.Field(new("Display Name", target.DisplayName)); + eb.Field(new("Display Name", target.DisplayName, true)); + + if (!target.Color.EmptyOrNull()) eb.Field(new("Color", $"#{target.Color}", true)); if (target.ListPrivacy.CanAccess(pctx)) { if (memberCount == 0 && pctx == LookupContext.ByOwner) // Only suggest the add command if this is actually the owner lol - eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", true)); + eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", false)); else - eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true)); + eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", false)); } if (target.DescriptionFor(pctx) is { } desc) From d6cb2db621fc712cf6de15dd183c551a58bce121 Mon Sep 17 00:00:00 2001 From: spiral Date: Thu, 1 Apr 2021 21:58:48 +0100 Subject: [PATCH 054/608] Check for reaction permissions before adding reactions --- PluralKit.Bot/Utils/ContextUtils.cs | 6 ++++-- PluralKit.Bot/Utils/DiscordUtils.cs | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 9f24d176..71da564c 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -34,6 +34,9 @@ namespace PluralKit.Bot { if (user == null) user = ctx.Author; if (timeout == null) timeout = Duration.FromMinutes(5); + if (!DiscordUtils.HasReactionPermissions(ctx)) + await ctx.Reply($"{Emojis.Note} PluralKit does not have permissions to add reactions in this channel. \nPlease reply with 'yes' to confirm, or 'no' to cancel."); + else // "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses await ctx.Rest.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error}); @@ -261,8 +264,7 @@ namespace PluralKit.Bot { var task = f(); // If we don't have permission to add reactions, don't bother, and just await the task normally. - var neededPermissions = PermissionSet.AddReactions | PermissionSet.ReadMessageHistory; - if ((ctx.BotPermissions & neededPermissions) != neededPermissions) return await task; + if (!DiscordUtils.HasReactionPermissions(ctx)) return await task; try { diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 9be30f4e..bf49b42f 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -186,5 +186,11 @@ namespace PluralKit.Bot public static string EventType(this IGatewayEvent evt) => evt.GetType().Name.Replace("Event", ""); + + public static bool HasReactionPermissions(Context ctx) + { + var neededPermissions = PermissionSet.AddReactions | PermissionSet.ReadMessageHistory; + return ((ctx.BotPermissions & neededPermissions) == neededPermissions); + } } } From 9d17c130c50b886e1ebb59578c874c32bd164f6a Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 5 Apr 2021 02:39:14 +0100 Subject: [PATCH 055/608] Fix matching canary/ptb Discord links --- PluralKit.Bot/Commands/Misc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index ad04c60a..70a0c9ab 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -215,7 +215,7 @@ namespace PluralKit.Bot { ulong messageId; if (ulong.TryParse(word, out var id)) messageId = id; - else if (Regex.Match(word, "https://discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)") is Match match && match.Success) + else if (Regex.Match(word, "https://(?:\\w+.)discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)") is Match match && match.Success) messageId = ulong.Parse(match.Groups[1].Value); else throw new PKSyntaxError($"Could not parse {word.AsCode()} as a message ID or link."); From 29561662abb781765ebd07c09ac07c01f2487755 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Tue, 6 Apr 2021 12:27:39 +0200 Subject: [PATCH 056/608] Fix typo in system color --- PluralKit.Bot/Commands/SystemEdit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 915d3bd8..068fe838 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -127,7 +127,7 @@ namespace PluralKit.Bot await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); await ctx.Reply(embed: new EmbedBuilder() - .Title($"{Emojis.Success} Member color changed.") + .Title($"{Emojis.Success} System color changed.") .Color(color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) .Build()); From 7010becd3226e068fdf5b6d52efd8e3db4805783 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Tue, 9 Feb 2021 23:36:43 +0100 Subject: [PATCH 057/608] Add group front percentages (fixing Git history) Signed-off-by: Ske --- PluralKit.Bot/Commands/CommandTree.cs | 5 +++- PluralKit.Bot/Commands/Groups.cs | 27 +++++++++++++++++++ PluralKit.Bot/Commands/SystemFront.cs | 10 +++++-- PluralKit.Bot/Services/EmbedService.cs | 3 ++- .../Repository/ModelRepository.Switch.cs | 15 ++++++++--- PluralKit.Core/Services/DataFileService.cs | 2 +- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index b76d6261..3c491205 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -62,6 +62,7 @@ namespace PluralKit.Bot public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); public static Command GroupIcon = new Command("group icon", "group icon [url|@mention]", "Changes a group's icon"); public static Command GroupDelete = new Command("group delete", "group delete", "Deletes a group"); + public static Command GroupFrontPercent = new Command("group frontpercent", "group frontpercent [timespan]", "Shows a group's front breakdown."); public static Command GroupMemberRandom = new Command("group random", "group random", "Shows the info card of a randomly selected member in a group."); public static Command GroupRandom = new Command("random", "random group", "Shows the info card of a randomly selected group in your system."); public static Command Switch = new Command("switch", "switch [member 2] [member 3...]", "Registers a switch"); @@ -109,7 +110,7 @@ namespace PluralKit.Bot public static Command[] GroupCommandsTargeted = { GroupInfo, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, GroupIcon, GroupPrivacy, - GroupDelete, GroupMemberRandom + GroupDelete, GroupMemberRandom, GroupFrontPercent }; public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll}; @@ -401,6 +402,8 @@ namespace PluralKit.Bot await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) + await ctx.Execute(GroupFrontPercent, g => g.GroupFrontPercent(ctx, target)); else if (ctx.Match("color", "colour")) await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); else if (!ctx.HasNext()) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index eb23c685..7cbc5200 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -9,6 +9,8 @@ using Dapper; using Humanizer; +using NodaTime; + using Myriad.Builders; using PluralKit.Core; @@ -476,6 +478,31 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Group deleted."); } + public async Task GroupFrontPercent(Context ctx, PKGroup target) + { + await using var conn = await _db.Obtain(); + + var targetSystem = await GetGroupSystem(ctx, target, conn); + ctx.CheckSystemPrivacy(targetSystem, targetSystem.FrontHistoryPrivacy); + + string durationStr = ctx.RemainderOrNull() ?? "30d"; + + var now = SystemClock.Instance.GetCurrentInstant(); + + var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone); + if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); + if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; + + var title = new StringBuilder($"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in "); + if (targetSystem.Name != null) + title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)"); + else + title.Append($"`{targetSystem.Hid}`"); + + var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, targetSystem.Id, target.Id, rangeStart.Value.ToInstant(), now)); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString())); + } + private async Task GetGroupSystem(Context ctx, PKGroup target, IPKConnection conn) { var system = ctx.System; diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index cde6359d..0be2429a 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -125,8 +125,14 @@ namespace PluralKit.Bot if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; - var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system))); + var title = new StringBuilder($"Frontpercent of "); + if (system.Name != null) + title.Append($"{system.Name} (`{system.Hid}`)"); + else + title.Append($"`{system.Hid}`"); + + var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, null, rangeStart.Value.ToInstant(), now)); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString())); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 2a9d5bff..56f197b3 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -321,10 +321,11 @@ namespace PluralKit.Bot { return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var eb = new EmbedBuilder() + .Title(embedTitle) .Color(DiscordUtils.Gray) .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs index f313f00a..d8633acf 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs @@ -122,7 +122,7 @@ namespace PluralKit.Core await GetSwitches(conn, system).FirstOrDefaultAsync(); public async Task> GetPeriodFronters(IPKConnection conn, - SystemId system, Instant periodStart, + SystemId system, GroupId? group, Instant periodStart, Instant periodEnd) { // TODO: IAsyncEnumerable-ify this one @@ -139,7 +139,14 @@ namespace PluralKit.Core new {Switches = switchMembers.Select(m => m.Member.Value).Distinct().ToList()}); var memberObjects = membersList.ToDictionary(m => m.Id); + // check if a group ID is provided. if so, query DB for all members of said group, otherwise use membersList + var groupMembersList = group != null ? await conn.QueryAsync( + "select * from members inner join group_members on members.id = group_members.member_id where group_id = @id", + new {id = group}) : membersList; + var groupMemberObjects = groupMembersList.ToDictionary(m => m.Id); + // Initialize entries - still need to loop to determine the TimespanEnd below + // use groupMemberObjects to make sure no members outside of the specified group (if present) are selected var entries = from item in switchMembers group item by item.Timestamp @@ -147,7 +154,7 @@ namespace PluralKit.Core select new SwitchListEntry { TimespanStart = g.Key, - Members = g.Where(x => x.Member != default(MemberId)).Select(x => memberObjects[x.Member]) + Members = g.Where(x => x.Member != default(MemberId) && groupMemberObjects.Any(m => x.Member == m.Key) ).Select(x => memberObjects[x.Member]) .ToList() }; @@ -174,7 +181,7 @@ namespace PluralKit.Core return outList; } - public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, Instant periodStart, + public async Task GetFrontBreakdown(IPKConnection conn, SystemId system, GroupId? group, Instant periodStart, Instant periodEnd) { // TODO: this doesn't belong in the repo @@ -188,7 +195,7 @@ namespace PluralKit.Core var actualStart = periodEnd; // will be "pulled" down var actualEnd = periodStart; // will be "pulled" up - foreach (var sw in await GetPeriodFronters(conn, system, periodStart, periodEnd)) + foreach (var sw in await GetPeriodFronters(conn, system, group, periodStart, periodEnd)) { var span = sw.TimespanEnd - sw.TimespanStart; foreach (var member in sw.Members) diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index 05dd3b37..cc259d3b 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -51,7 +51,7 @@ namespace PluralKit.Core // Export switches var switches = new List(); - var switchList = await _repo.GetPeriodFronters(conn, system.Id, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); + var switchList = await _repo.GetPeriodFronters(conn, system.Id, null, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant()); switches.AddRange(switchList.Select(x => new DataFileSwitch { Timestamp = x.TimespanStart.FormatExport(), From a5ad16780c2bd9b9a742727b5c3f3a4817b51aa1 Mon Sep 17 00:00:00 2001 From: spiral Date: Fri, 9 Apr 2021 10:51:24 +0100 Subject: [PATCH 058/608] Fix matching Discord Stable links (oops) --- PluralKit.Bot/Commands/Misc.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 70a0c9ab..23b654f3 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -215,7 +215,7 @@ namespace PluralKit.Bot { ulong messageId; if (ulong.TryParse(word, out var id)) messageId = id; - else if (Regex.Match(word, "https://(?:\\w+.)discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)") is Match match && match.Success) + else if (Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)") is Match match && match.Success) messageId = ulong.Parse(match.Groups[1].Value); else throw new PKSyntaxError($"Could not parse {word.AsCode()} as a message ID or link."); @@ -234,4 +234,4 @@ namespace PluralKit.Bot { await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } } -} \ No newline at end of file +} From a2d2036851139c18736371600f1d2625f3b2ba62 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 13 Apr 2021 04:07:03 +0200 Subject: [PATCH 059/608] Fix nullability for embeds Signed-off-by: Ske --- Myriad/Types/Message.cs | 2 +- PluralKit.Bot/Services/LoggerCleanService.cs | 28 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index 977e1b9d..a7cb88c6 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -54,7 +54,7 @@ namespace Myriad.Types public ulong[] MentionRoles { get; init; } public Attachment[] Attachments { get; init; } - public Embed[] Embeds { get; init; } + public Embed[]? Embeds { get; init; } public Reaction[] Reactions { get; init; } public bool Pinned { get; init; } public ulong? WebhookId { get; init; } diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index 9b97104d..000279a8 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -163,7 +163,7 @@ namespace PluralKit.Bot // Auttaja has an optional "compact mode" that logs without embeds // That one puts the ID in the message content, non-compact puts it in the embed description. // Regex also checks that this is a deletion. - var stringWithId = msg.Embeds.FirstOrDefault()?.Description ?? msg.Content; + var stringWithId = msg.Embeds?.FirstOrDefault()?.Description ?? msg.Content; if (stringWithId == null) return null; var match = _auttajaRegex.Match(stringWithId); @@ -173,7 +173,7 @@ namespace PluralKit.Bot private static ulong? ExtractDyno(Message msg) { // Embed *description* contains "Message sent by [mention] deleted in [channel]", contains message ID in footer per regex - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Footer == null || !(embed.Description?.Contains("deleted in") ?? false)) return null; var match = _dynoRegex.Match(embed.Footer.Text ?? ""); return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; @@ -183,7 +183,7 @@ namespace PluralKit.Bot { // This is for Logger#6088 (298822483060981760), distinct from Logger#6278 (327424261180620801). // Embed contains title "Message deleted in [channel]", and an ID field containing both message and user ID (see regex). - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed == null) return null; if (!embed.Description.StartsWith("Message deleted in")) return null; @@ -197,7 +197,7 @@ namespace PluralKit.Bot { // This is for Logger#6278 (327424261180620801), distinct from Logger#6088 (298822483060981760). // Embed title ends with "A Message Was Deleted!", footer contains message ID as per regex. - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Footer == null || !(embed.Title?.EndsWith("A Message Was Deleted!") ?? false)) return null; var match = _loggerBRegex.Match(embed.Footer.Text ?? ""); return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; @@ -206,7 +206,7 @@ namespace PluralKit.Bot private static ulong? ExtractGenericBot(Message msg) { // Embed, title is "Message Deleted", ID plain in footer. - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Footer == null || !(embed.Title?.Contains("Message Deleted") ?? false)) return null; var match = _basicRegex.Match(embed.Footer.Text ?? ""); return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; @@ -215,7 +215,7 @@ namespace PluralKit.Bot private static ulong? ExtractBlargBot(Message msg) { // Embed, title ends with "Message Deleted", contains ID plain in a field. - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed == null || !(embed.Title?.EndsWith("Message Deleted") ?? false)) return null; var field = embed.Fields.FirstOrDefault(f => f.Name == "Message ID"); var match = _basicRegex.Match(field.Value ?? ""); @@ -234,7 +234,7 @@ namespace PluralKit.Bot { // Embed, title is "Message deleted in [channel], **user** ID in the footer, timestamp as, well, timestamp in embed. // This is the *deletion* timestamp, which we can assume is a couple seconds at most after the message was originally sent - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Footer == null || embed.Timestamp == null || !(embed.Title?.StartsWith("Message deleted in") ?? false)) return null; var match = _carlRegex.Match(embed.Footer.Text ?? ""); return match.Success @@ -252,9 +252,9 @@ namespace PluralKit.Bot // Compact: "Message from [user] ([id]) deleted in [channel]", no timestamp (use message time) // Embed: Message Author field: "[user] ([id])", then an embed timestamp string stringWithId = msg.Content; - if (msg.Embeds.Length > 0) + if (msg.Embeds?.Length > 0) { - var embed = msg.Embeds.First(); + var embed = msg.Embeds?.First(); if (embed.Author?.Name == null || !embed.Author.Name.StartsWith("Message Deleted in")) return null; var field = embed.Fields.FirstOrDefault(f => f.Name == "Message Author"); if (field.Value == null) return null; @@ -275,7 +275,7 @@ namespace PluralKit.Bot { // Embed, author is "Message Deleted", description includes a mention, timestamp is *message send time* (but no ID) // so we use the message timestamp to get somewhere *after* the message was proxied - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Description == null || embed.Author?.Name != "Message Deleted") return null; var match = _pancakeRegex.Match(embed.Description); return match.Success @@ -290,7 +290,7 @@ namespace PluralKit.Bot private static ulong? ExtractUnbelievaBoat(Message msg) { // Embed author is "Message Deleted", footer contains message ID per regex - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Footer == null || embed.Author?.Name != "Message Deleted") return null; var match = _unbelievaboatRegex.Match(embed.Footer.Text ?? ""); return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; @@ -299,7 +299,7 @@ namespace PluralKit.Bot private static FuzzyExtractResult? ExtractVanessa(Message msg) { // Title is "Message Deleted", embed description contains mention - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Title == null || embed.Title != "Message Deleted" || embed.Description == null) return null; var match = _vanessaRegex.Match(embed.Description); return match.Success @@ -314,7 +314,7 @@ namespace PluralKit.Bot private static FuzzyExtractResult? ExtractSAL(Message msg) { // Title is "Message Deleted!", field "Message Author" contains ID - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Title == null || embed.Title != "Message Deleted!") return null; var authorField = embed.Fields.FirstOrDefault(f => f.Name == "Message Author"); if (authorField == null) return null; @@ -345,7 +345,7 @@ namespace PluralKit.Bot private static ulong? ExtractGiselleBot(Message msg) { - var embed = msg.Embeds.FirstOrDefault(); + var embed = msg.Embeds?.FirstOrDefault(); if (embed?.Title == null || embed.Title != "🗑 Message Deleted") return null; var match = _GiselleRegex.Match(embed?.Description); return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null; From e016229be5dd544bc9a3e06848f90be5bec865d5 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Tue, 13 Apr 2021 11:25:05 +0200 Subject: [PATCH 060/608] Add raw display name printing --- PluralKit.Bot/Commands/Groups.cs | 33 +++++++++++----- PluralKit.Bot/Commands/MemberEdit.cs | 56 ++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 7cbc5200..ce707ac5 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -103,15 +103,30 @@ namespace PluralKit.Bot } else if (!ctx.HasNext()) { - // No perms check, display name isn't covered by member privacy - var eb = new EmbedBuilder() - .Field(new("Name", target.Name)) - .Field(new("Display Name", target.DisplayName ?? "*(none)*")); - - if (ctx.System?.Id == target.System) - eb.Description($"To change display name, type `pk;group {target.Reference()} displayname `.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`."); - - await ctx.Reply(embed: eb.Build()); + // No perms check, display name isn't covered by member privacy + if (ctx.MatchFlag("r", "raw")) + { + if (target.DisplayName == null) + { + if (ctx.System?.Id == target.System) + await ctx.Reply($"This group does not have a display name set. To set one, type `pk;group {target.Reference()} displayname `."); + else + await ctx.Reply("This group does not have a display name set."); + } + else + await ctx.Reply($"```\n{target.DisplayName}\n```"); + } + else + { + var eb = new EmbedBuilder() + .Field(new("Name", target.Name)) + .Field(new("Display Name", target.DisplayName ?? "*(none)*")); + + if (ctx.System?.Id == target.System) + eb.Description($"To change display name, type `pk;group {target.Reference()} displayname `.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`.\nTo print the raw display name, type `pk;group {target.Reference()} displayname -raw`."); + + await ctx.Reply(embed: eb.Build()); + } } else { diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 6d7ba1ee..b643ba88 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -115,8 +115,10 @@ namespace PluralKit.Bot await ctx.Reply($"This member does not have pronouns set. To set some, type `pk;member {target.Reference()} pronouns `."); else await ctx.Reply("This member does not have pronouns set."); + else if (ctx.MatchFlag("r", "raw")) + await ctx.Reply($"```\n{target.Pronouns}\n```"); else - await ctx.Reply($"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**." + await ctx.Reply($"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference()} pronouns -raw`." + (ctx.System?.Id == target.System ? $" To clear them, type `pk;member {target.Reference()} pronouns -clear`." : "")); } else @@ -283,11 +285,26 @@ namespace PluralKit.Bot } else if (!ctx.HasNext()) { - // No perms check, display name isn't covered by member privacy - var eb = await CreateMemberNameInfoEmbed(ctx, target); - if (ctx.System?.Id == target.System) - eb.Description($"To change display name, type `pk;member {target.Reference()} displayname `.\nTo clear it, type `pk;member {target.Reference()} displayname -clear`."); - await ctx.Reply(embed: eb.Build()); + // No perms check, display name isn't covered by member privacy + if (ctx.MatchFlag("r", "raw")) + { + if (target.DisplayName == null) + { + if (ctx.System?.Id == target.System) + await ctx.Reply($"This member does not have a display name set. To set one, type `pk;member {target.Reference()} displayname `."); + else + await ctx.Reply("This member does not have a display name set."); + } + else + await ctx.Reply($"```\n{target.DisplayName}\n```"); + } + else + { + var eb = await CreateMemberNameInfoEmbed(ctx, target); + if (ctx.System?.Id == target.System) + eb.Description($"To change display name, type `pk;member {target.Reference()} displayname `.\nTo clear it, type `pk;member {target.Reference()} displayname -clear`.\nTo print the raw display name, type `pk;member {target.Reference()} displayname -raw`."); + await ctx.Reply(embed: eb.Build()); + } } else { @@ -320,11 +337,28 @@ namespace PluralKit.Bot } else if (!ctx.HasNext()) { - // No perms check, server name isn't covered by member privacy - var eb = await CreateMemberNameInfoEmbed(ctx, target); - if (ctx.System?.Id == target.System) - eb.Description($"To change server name, type `pk;member {target.Reference()} servername `.\nTo clear it, type `pk;member {target.Reference()} servername -clear`."); - await ctx.Reply(embed: eb.Build()); + // No perms check, display name isn't covered by member privacy + if (ctx.MatchFlag("r", "raw")) + { + MemberGuildSettings memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); + + if (memberGuildConfig.DisplayName == null) + { + if (ctx.System?.Id == target.System) + await ctx.Reply($"This member does not have a server name set. To set one, type `pk;member {target.Reference()} servername `."); + else + await ctx.Reply("This member does not have a server name set."); + } + else + await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```"); + } + else + { + var eb = await CreateMemberNameInfoEmbed(ctx, target); + if (ctx.System?.Id == target.System) + eb.Description($"To change server name, type `pk;member {target.Reference()} servername `.\nTo clear it, type `pk;member {target.Reference()} servername -clear`.\nTo print the raw server name, type `pk;member {target.Reference()} servername -raw`."); + await ctx.Reply(embed: eb.Build()); + } } else { From 8219aaa5bdb729d455028223aa23283c51340210 Mon Sep 17 00:00:00 2001 From: spiral Date: Tue, 13 Apr 2021 14:55:46 +0100 Subject: [PATCH 061/608] Add support for sub-hour latch timeout (just use ParsePeriod) --- PluralKit.Bot/Commands/Autoproxy.cs | 45 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index c1d58059..a24b81d2 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -143,40 +143,45 @@ namespace PluralKit.Bot : (Duration?) null; if (timeout == null) - await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize()}."); + await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}."); else if (timeout == Duration.Zero) await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out."); else - await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize()}."); + await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); return; } - // todo: somehow parse a more human-friendly date format - int newTimeoutHours; - if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeoutHours = 0; - else if (ctx.Match("reset", "default")) newTimeoutHours = -1; - else if (!int.TryParse(ctx.RemainderOrNull(), out newTimeoutHours)) throw new PKError("Duration must be a number of hours."); - - int? overflow = null; - if (newTimeoutHours > 100000) + Duration? newTimeout; + Duration overflow = Duration.Zero; + if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeout = Duration.Zero; + else if (ctx.Match("reset", "default")) newTimeout = null; + else { - // sanity check to prevent seconds overflow if someone types in 999999999 - overflow = newTimeoutHours; - newTimeoutHours = 0; + var timeoutStr = ctx.RemainderOrNull(); + var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); + if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); + Console.WriteLine(timeoutPeriod.Value.TotalHours); + if (timeoutPeriod.Value.TotalHours > (ulong)100000) + { + // sanity check to prevent seconds overflow if someone types in 999999999 + overflow = timeoutPeriod.Value; + newTimeout = Duration.Zero; + } + else newTimeout = timeoutPeriod; } - var newTimeout = newTimeoutHours > -1 ? Duration.FromHours(newTimeoutHours) : (Duration?) null; + var timeoutSeconds = newTimeout.HasValue ? newTimeout?.TotalSeconds : -1; await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, new SystemPatch { LatchTimeout = (int?) newTimeout?.TotalSeconds })); - if (newTimeoutHours == -1) - await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize()})."); - else if (newTimeoutHours == 0 && overflow != null) - await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow} hours is too long)"); - else if (newTimeoutHours == 0) + if (newTimeout == null) + await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); + else if (newTimeout == Duration.Zero && overflow != Duration.Zero) + await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); + else if (newTimeout == Duration.Zero) await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); else - await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize()}."); + await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); } public async Task AutoproxyAccount(Context ctx) From d3e3cb0abc682f7e7961394ad2bbfe46dbd4f080 Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 14 Apr 2021 13:56:41 +0100 Subject: [PATCH 062/608] feat: add avatar to member via attachment on 'pk;member new' command --- PluralKit.Bot/Commands/Member.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index ffed8d3b..0ebf72c6 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -1,7 +1,9 @@ +using System; using System.Threading.Tasks; using System.Net; using System.Net.Http; using System.Web; +using System.Linq; using Dapper; @@ -53,13 +55,30 @@ namespace PluralKit.Bot // Create the member var member = await _repo.CreateMember(conn, ctx.System.Id, memberName); memberCount++; - + + // Try to match an image attached to the message + var avatarArg = ctx.Message.Attachments.FirstOrDefault(); + Exception imageMatchError = null; + if (avatarArg != null) + { + try { + await AvatarUtils.VerifyAvatarOrThrow(avatarArg.Url); + await _db.Execute(conn => _repo.UpdateMember(conn, member.Id, new MemberPatch { AvatarUrl = avatarArg.Url })); + } catch (Exception e) { + imageMatchError = e; + } + } + // Send confirmation and space hint await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member"); if (await _db.Execute(conn => conn.QuerySingleAsync("select has_private_members(@System)", new {System = ctx.System.Id}))) //if has private members await ctx.Reply($"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`."); - + if (avatarArg != null) + if (imageMatchError == null) + await ctx.Reply($"{Emojis.Success} Member avatar set to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working."); + else + await ctx.Reply($"{Emojis.Error} Couldn't set avatar: {imageMatchError.Message}"); if (memberName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`)."); if (memberCount >= memberLimit) From 58a51ee9d2678a9611d93cb8ecdf4a2b906f0989 Mon Sep 17 00:00:00 2001 From: Starshine System Date: Wed, 14 Apr 2021 16:50:15 +0200 Subject: [PATCH 063/608] add "author" subcommand to pk;msg --- PluralKit.Bot/Commands/CommandTree.cs | 2 +- PluralKit.Bot/Commands/Misc.cs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 3c491205..872bc86b 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -78,7 +78,7 @@ namespace PluralKit.Bot public static Command Export = new Command("export", "export", "Exports system information to a data file"); public static Command Help = new Command("help", "help", "Shows help information about PluralKit"); public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying"); - public static Command Message = new Command("message", "message [delete]", "Looks up a proxied message"); + public static Command Message = new Command("message", "message [delete|author]", "Looks up a proxied message"); public static Command LogChannel = new Command("log channel", "log channel ", "Designates a channel to post proxied messages to"); public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 23b654f3..8b66a069 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -230,6 +230,16 @@ namespace PluralKit.Bot { await ctx.Rest.DeleteMessage(ctx.Message); return; } + if (ctx.Match("author") || ctx.MatchFlag("author")) + { + var user = await _cache.GetOrFetchUser(_rest, message.Message.Sender); + var eb = new EmbedBuilder() + .Author(new(user != null ? $"{user.Username}#{user.Discriminator}" : $"Deleted user ${message.Message.Sender}", IconUrl: user != null ? user.AvatarUrl() : null)) + .Description(message.Message.Sender.ToString()); + + await ctx.Reply(user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*", embed: eb.Build()); + return; + } await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } From 85615b3f68e79cc20974bbbd95d7c73c52166729 Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 19 Apr 2021 21:38:03 +0100 Subject: [PATCH 064/608] fix error message on invalid guild ID --- PluralKit.Bot/Commands/Misc.cs | 7 ++++++- PluralKit.Bot/Errors.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 23b654f3..3f75b24f 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -126,7 +126,12 @@ namespace PluralKit.Bot { if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID."); - guild = await _rest.GetGuild(guildId); + try { + guild = await _rest.GetGuild(guildId); + } catch (Myriad.Rest.Exceptions.ForbiddenException) { + throw Errors.GuildNotFound(guildId); + } + if (guild != null) senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); if (guild == null || senderGuildUser == null) diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index 4ad5a2d0..a3a2abf1 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -98,7 +98,7 @@ namespace PluralKit.Bot { public static PKError DurationParseError(string durationStr) => new PKError($"Could not parse {durationStr.AsCode()} as a valid duration. Try a format such as `30d`, `1d3h` or `20m30s`."); public static PKError FrontPercentTimeInFuture => new PKError("Cannot get the front percent between now and a time in the future."); - public static PKError GuildNotFound(ulong guildId) => new PKError($"Guild with ID {guildId} not found. Note that you must be a member of the guild you are querying."); + public static PKError GuildNotFound(ulong guildId) => new PKError($"Guild with ID `{guildId}` not found, or I cannot access it. Note that you must be a member of the guild you are querying."); public static PKError DisplayNameTooLong(string displayName, int maxLength) => new PKError( $"Display name too long ({displayName.Length} > {maxLength} characters). Use a shorter display name, or shorten your system tag."); From b34ed5c4c089cbce05b367dacb08a6f6ff571830 Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 21 Apr 2021 22:57:19 +0100 Subject: [PATCH 065/608] API patch improvements - add PatchObject.CheckIsValid - use transaction when creating member, as to not create a member if the patch is invalid - return edited system in `PATCH /s` endpoint --- .../Controllers/v1/MemberController.cs | 20 ++++++++++++++-- .../Controllers/v1/SystemController.cs | 8 ++++++- PluralKit.Bot/Utils/AvatarUtils.cs | 11 +-------- .../Repository/ModelRepository.Group.cs | 9 +++---- .../Repository/ModelRepository.Member.cs | 9 +++---- PluralKit.Core/Models/Patch/GroupPatch.cs | 11 +++++++++ PluralKit.Core/Models/Patch/MemberPatch.cs | 10 ++++++++ PluralKit.Core/Models/Patch/PatchObject.cs | 12 +++++++++- PluralKit.Core/Models/Patch/SystemPatch.cs | 11 +++++++++ PluralKit.Core/Utils/MiscUtils.cs | 24 +++++++++++++++++++ 10 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 PluralKit.Core/Utils/MiscUtils.cs diff --git a/PluralKit.API/Controllers/v1/MemberController.cs b/PluralKit.API/Controllers/v1/MemberController.cs index 539893c2..1020887a 100644 --- a/PluralKit.API/Controllers/v1/MemberController.cs +++ b/PluralKit.API/Controllers/v1/MemberController.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Dapper; @@ -54,18 +55,28 @@ namespace PluralKit.API if (memberCount >= memberLimit) return BadRequest($"Member limit reached ({memberCount} / {memberLimit})."); - var member = await _repo.CreateMember(conn, systemId, properties.Value("name")); + await using var tx = await conn.BeginTransactionAsync(); + var member = await _repo.CreateMember(conn, systemId, properties.Value("name"), transaction: tx); + MemberPatch patch; try { patch = JsonModelExt.ToMemberPatch(properties); + patch.CheckIsValid(); } catch (JsonModelParseError e) { + await tx.RollbackAsync(); return BadRequest(e.Message); } + catch (InvalidPatchException e) + { + await tx.RollbackAsync(); + return BadRequest($"Request field '{e.Message}' is invalid."); + } - member = await _repo.UpdateMember(conn, member.Id, patch); + member = await _repo.UpdateMember(conn, member.Id, patch, transaction: tx); + await tx.CommitAsync(); return Ok(member.ToJson(User.ContextFor(member))); } @@ -85,11 +96,16 @@ namespace PluralKit.API try { patch = JsonModelExt.ToMemberPatch(changes); + patch.CheckIsValid(); } catch (JsonModelParseError e) { return BadRequest(e.Message); } + catch (InvalidPatchException e) + { + return BadRequest($"Request field is invalid: {e.Message}"); + } var newMember = await _repo.UpdateMember(conn, member.Id, patch); return Ok(newMember.ToJson(User.ContextFor(newMember))); diff --git a/PluralKit.API/Controllers/v1/SystemController.cs b/PluralKit.API/Controllers/v1/SystemController.cs index 0dce14e1..b0bf7003 100644 --- a/PluralKit.API/Controllers/v1/SystemController.cs +++ b/PluralKit.API/Controllers/v1/SystemController.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -140,13 +141,18 @@ namespace PluralKit.API try { patch = JsonModelExt.ToSystemPatch(changes); + patch.CheckIsValid(); } catch (JsonModelParseError e) { return BadRequest(e.Message); } + catch (InvalidPatchException e) + { + return BadRequest($"Request field '{e.Message}' is invalid."); + } - await _repo.UpdateSystem(conn, system!.Id, patch); + system = await _repo.UpdateSystem(conn, system!.Id, patch); return Ok(system.ToJson(User.ContextFor(system))); } diff --git a/PluralKit.Bot/Utils/AvatarUtils.cs b/PluralKit.Bot/Utils/AvatarUtils.cs index df4881ab..790c6bd3 100644 --- a/PluralKit.Bot/Utils/AvatarUtils.cs +++ b/PluralKit.Bot/Utils/AvatarUtils.cs @@ -25,17 +25,8 @@ namespace PluralKit.Bot { using (var client = new HttpClient()) { - Uri uri; - try - { - uri = new Uri(url); - if (!uri.IsAbsoluteUri || (uri.Scheme != "http" && uri.Scheme != "https")) - throw Errors.InvalidUrl(url); - } - catch (UriFormatException) - { + if (!PluralKit.Core.MiscUtils.TryMatchUri(url, out var uri)) throw Errors.InvalidUrl(url); - } var response = await client.GetAsync(uri); if (!response.IsSuccessStatusCode) // Check status code diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Group.cs b/PluralKit.Core/Database/Repository/ModelRepository.Group.cs index 7a88dba7..2afb43eb 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Group.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Group.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -30,22 +31,22 @@ namespace PluralKit.Core return conn.QuerySingleOrDefaultAsync(query.ToString(), new {Id = id, PrivacyFilter = privacyFilter}); } - public async Task CreateGroup(IPKConnection conn, SystemId system, string name) + public async Task CreateGroup(IPKConnection conn, SystemId system, string name, IDbTransaction? transaction = null) { var group = await conn.QueryFirstAsync( "insert into groups (hid, system, name) values (find_free_group_hid(), @System, @Name) returning *", - new {System = system, Name = name}); + new {System = system, Name = name}, transaction); _logger.Information("Created group {GroupId} in system {SystemId}: {GroupName}", group.Id, system, name); return group; } - public Task UpdateGroup(IPKConnection conn, GroupId id, GroupPatch patch) + public Task UpdateGroup(IPKConnection conn, GroupId id, GroupPatch patch, IDbTransaction? transaction = null) { _logger.Information("Updated {GroupId}: {@GroupPatch}", id, patch); var (query, pms) = patch.Apply(UpdateQueryBuilder.Update("groups", "id = @id")) .WithConstant("id", id) .Build("returning *"); - return conn.QueryFirstAsync(query, pms); + return conn.QueryFirstAsync(query, pms, transaction); } public Task DeleteGroup(IPKConnection conn, GroupId group) diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Member.cs b/PluralKit.Core/Database/Repository/ModelRepository.Member.cs index e2e25888..c7dfa34c 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Member.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Member.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Data; using System.Threading.Tasks; using Dapper; @@ -19,23 +20,23 @@ namespace PluralKit.Core public Task GetMemberByDisplayName(IPKConnection conn, SystemId system, string name) => conn.QueryFirstOrDefaultAsync("select * from members where lower(display_name) = lower(@Name) and system = @SystemID", new { Name = name, SystemID = system }); - public async Task CreateMember(IPKConnection conn, SystemId id, string memberName) + public async Task CreateMember(IPKConnection conn, SystemId id, string memberName, IDbTransaction? transaction = null) { var member = await conn.QueryFirstAsync( "insert into members (hid, system, name) values (find_free_member_hid(), @SystemId, @Name) returning *", - new {SystemId = id, Name = memberName}); + new {SystemId = id, Name = memberName}, transaction); _logger.Information("Created {MemberId} in {SystemId}: {MemberName}", member.Id, id, memberName); return member; } - public Task UpdateMember(IPKConnection conn, MemberId id, MemberPatch patch) + public Task UpdateMember(IPKConnection conn, MemberId id, MemberPatch patch, IDbTransaction? transaction = null) { _logger.Information("Updated {MemberId}: {@MemberPatch}", id, patch); var (query, pms) = patch.Apply(UpdateQueryBuilder.Update("members", "id = @id")) .WithConstant("id", id) .Build("returning *"); - return conn.QueryFirstAsync(query, pms); + return conn.QueryFirstAsync(query, pms, transaction); } public Task DeleteMember(IPKConnection conn, MemberId id) diff --git a/PluralKit.Core/Models/Patch/GroupPatch.cs b/PluralKit.Core/Models/Patch/GroupPatch.cs index ee624df8..933a3376 100644 --- a/PluralKit.Core/Models/Patch/GroupPatch.cs +++ b/PluralKit.Core/Models/Patch/GroupPatch.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Text.RegularExpressions; + namespace PluralKit.Core { public class GroupPatch: PatchObject @@ -24,5 +26,14 @@ namespace PluralKit.Core .With("icon_privacy", IconPrivacy) .With("list_privacy", ListPrivacy) .With("visibility", Visibility); + + public new void CheckIsValid() + { + if (Icon.Value != null && !MiscUtils.TryMatchUri(Icon.Value, out var avatarUri)) + throw new InvalidPatchException("avatar_url"); + if (Color.Value != null && (!Regex.IsMatch(Color.Value, "^[0-9a-fA-F]{6}$"))) + throw new InvalidPatchException("color"); + } + } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/MemberPatch.cs b/PluralKit.Core/Models/Patch/MemberPatch.cs index 645e2b1a..f07b3074 100644 --- a/PluralKit.Core/Models/Patch/MemberPatch.cs +++ b/PluralKit.Core/Models/Patch/MemberPatch.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Text.RegularExpressions; using NodaTime; @@ -44,5 +45,14 @@ namespace PluralKit.Core .With("birthday_privacy", BirthdayPrivacy) .With("avatar_privacy", AvatarPrivacy) .With("metadata_privacy", MetadataPrivacy); + + public new void CheckIsValid() + { + if (AvatarUrl.Value != null && !MiscUtils.TryMatchUri(AvatarUrl.Value, out var avatarUri)) + throw new InvalidPatchException("avatar_url"); + if (Color.Value != null && (!Regex.IsMatch(Color.Value, "^[0-9a-fA-F]{6}$"))) + throw new InvalidPatchException("color"); + } + } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/PatchObject.cs b/PluralKit.Core/Models/Patch/PatchObject.cs index 476007ef..03fcaf81 100644 --- a/PluralKit.Core/Models/Patch/PatchObject.cs +++ b/PluralKit.Core/Models/Patch/PatchObject.cs @@ -1,7 +1,17 @@ -namespace PluralKit.Core +using System; + +namespace PluralKit.Core { + + public class InvalidPatchException : Exception + { + public InvalidPatchException(string message) : base(message) {} + } + public abstract class PatchObject { public abstract UpdateQueryBuilder Apply(UpdateQueryBuilder b); + + public void CheckIsValid() {} } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index 0f787749..dbbfa1f5 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Text.RegularExpressions; + namespace PluralKit.Core { public class SystemPatch: PatchObject @@ -33,5 +35,14 @@ namespace PluralKit.Core .With("front_history_privacy", FrontHistoryPrivacy) .With("pings_enabled", PingsEnabled) .With("latch_timeout", LatchTimeout); + + public new void CheckIsValid() + { + if (AvatarUrl.Value != null && !MiscUtils.TryMatchUri(AvatarUrl.Value, out var avatarUri)) + throw new InvalidPatchException("avatar_url"); + if (Color.Value != null && (!Regex.IsMatch(Color.Value, "^[0-9a-fA-F]{6}$"))) + throw new InvalidPatchException("color"); + } + } } \ No newline at end of file diff --git a/PluralKit.Core/Utils/MiscUtils.cs b/PluralKit.Core/Utils/MiscUtils.cs new file mode 100644 index 00000000..bd21281a --- /dev/null +++ b/PluralKit.Core/Utils/MiscUtils.cs @@ -0,0 +1,24 @@ +using System; + +namespace PluralKit.Core +{ + public static class MiscUtils + { + public static bool TryMatchUri(string input, out Uri uri) + { + try + { + uri = new Uri(input); + if (!uri.IsAbsoluteUri || (uri.Scheme != "http" && uri.Scheme != "https")) + return false; + } + catch (UriFormatException) + { + uri = null; + return false; + } + + return true; + } + } +} \ No newline at end of file From 7f3f81a111e2b8f72657c26f50330f67f4ffac5b Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 21 Apr 2021 23:09:45 +0100 Subject: [PATCH 066/608] Update error string --- PluralKit.API/Controllers/v1/MemberController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.API/Controllers/v1/MemberController.cs b/PluralKit.API/Controllers/v1/MemberController.cs index 1020887a..56a5ae43 100644 --- a/PluralKit.API/Controllers/v1/MemberController.cs +++ b/PluralKit.API/Controllers/v1/MemberController.cs @@ -104,7 +104,7 @@ namespace PluralKit.API } catch (InvalidPatchException e) { - return BadRequest($"Request field is invalid: {e.Message}"); + return BadRequest($"Request field '{e.Message}' is invalid."); } var newMember = await _repo.UpdateMember(conn, member.Id, patch); From dfa25b77c79ce2d25eab92dd84431c59ad573a53 Mon Sep 17 00:00:00 2001 From: spiral Date: Thu, 22 Apr 2021 01:18:41 +0100 Subject: [PATCH 067/608] Add flag to remove 'no fronter' item on frontpercent card --- PluralKit.Bot/Commands/Groups.cs | 3 ++- PluralKit.Bot/Commands/SystemFront.cs | 3 ++- PluralKit.Bot/Services/EmbedService.cs | 9 ++++++--- docs/content/tips-and-tricks.md | 8 +++++++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 7cbc5200..6be11216 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -499,8 +499,9 @@ namespace PluralKit.Bot else title.Append($"`{targetSystem.Hid}`"); + var ignoreNoFronters = ctx.MatchFlag("fo", "fronters-only"); var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, targetSystem.Id, target.Id, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString())); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString(), ignoreNoFronters)); } private async Task GetGroupSystem(Context ctx, PKGroup target, IPKConnection conn) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 0be2429a..2129dcae 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -131,8 +131,9 @@ namespace PluralKit.Bot else title.Append($"`{system.Hid}`"); + var ignoreNoFronters = ctx.MatchFlag("fo", "fronters-only"); var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, null, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString())); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, ctx.LookupContextFor(system), title.ToString(), ignoreNoFronters)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 56f197b3..0ffc89fc 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -321,9 +321,12 @@ namespace PluralKit.Bot { return eb.Build(); } - public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle) + public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle, bool ignoreNoFronters) { var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; + // this is kinda messy? + var hasFrontersPeriod = Duration.FromTicks(breakdown.MemberSwitchDurations.Values.ToList().Sum(i => i.TotalTicks)); + var eb = new EmbedBuilder() .Title(embedTitle) .Color(DiscordUtils.Gray) @@ -333,13 +336,13 @@ namespace PluralKit.Bot { // We convert to a list of pairs so we can add the no-fronter value // Dictionary doesn't allow for null keys so we instead have a pair with a null key ;) var pairs = breakdown.MemberSwitchDurations.ToList(); - if (breakdown.NoFronterDuration != Duration.Zero) + if (breakdown.NoFronterDuration != Duration.Zero && !ignoreNoFronters) pairs.Add(new KeyValuePair(null, breakdown.NoFronterDuration)); var membersOrdered = pairs.OrderByDescending(pair => pair.Value).Take(maxEntriesToDisplay).ToList(); foreach (var pair in membersOrdered) { - var frac = pair.Value / actualPeriod; + var frac = pair.Value / (ignoreNoFronters ? hasFrontersPeriod : actualPeriod); eb.Field(new(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})")); } diff --git a/docs/content/tips-and-tricks.md b/docs/content/tips-and-tricks.md index 3bea60eb..2d7b19ea 100644 --- a/docs/content/tips-and-tricks.md +++ b/docs/content/tips-and-tricks.md @@ -61,4 +61,10 @@ You cannot look up private members of another system. |-with-last-message|-with-last-proxy, -wlm, -wlp|Show each member's last message date| |-with-message-count|-wmc|Show each member's message count| |-with-created|-wc|Show each member's creation date| -|-with-avatar|-wa, -wi, -ia, -ii, -img|Show each member's avatar URL| \ No newline at end of file +|-with-avatar|-wa, -wi, -ia, -ii, -img|Show each member's avatar URL| + +## Miscellaneous flags +|Command|Flag|Aliases|Description| +|---|---|---|---| +|pk;system frontpercent|fronters-only|fo|Hides the "no fronters" list item| +|pk;group \ frontpercent|fronters-only|fo|Same as above, but for groups| \ No newline at end of file From e6bfc6c3744393f2fb60df8aac4883eb9a842d91 Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 26 Apr 2021 03:44:02 +0100 Subject: [PATCH 068/608] Fix error on paginate timeout when missing permissions --- PluralKit.Bot/Utils/ContextUtils.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 71da564c..8121bb27 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -160,6 +160,10 @@ namespace PluralKit.Bot { } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } + // If we get an "Unauthorized" error, we don't have permissions to remove our reaction + // which means we probably didn't add it in the first place, or permissions changed since then + // either way, nothing to do here + catch (UnauthorizedException) { } } public static async Task Choose(this Context ctx, string description, IList items, Func display = null) From 478857e8c61b80d3e8da4bce819358c7ecc27e6f Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 29 Apr 2021 11:14:04 +0200 Subject: [PATCH 069/608] Format guild count in activity properly Signed-off-by: Ske --- PluralKit.Bot/Bot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 550c8d48..f9328af4 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -285,7 +285,7 @@ namespace PluralKit.Bot { new ActivityPartial { - Name = $"pk;help | in {totalGuilds} servers | shard #{shard.ShardInfo?.ShardId}", + Name = $"pk;help | in {totalGuilds:N0} servers | shard #{shard.ShardInfo?.ShardId}", Type = ActivityType.Game, Url = "https://pluralkit.me/" } From 1e5ba5f9853a3d5f3ad1886963fb1f5716d81cba Mon Sep 17 00:00:00 2001 From: spiral Date: Sat, 1 May 2021 19:17:35 +0100 Subject: [PATCH 070/608] feat: case-insensitive "text" keyword matching for proxy tags --- PluralKit.Bot/Commands/MemberProxy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs index d09ea4cf..44873fb8 100644 --- a/PluralKit.Bot/Commands/MemberProxy.cs +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -26,6 +26,7 @@ namespace PluralKit.Bot { // // Make sure there's one and only one instance of "text" in the example proxy given var prefixAndSuffix = exampleProxy.Split("text"); + if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); From cf93b8b3cc909cd956e84f0f9357783c324a15d0 Mon Sep 17 00:00:00 2001 From: spiral Date: Sat, 1 May 2021 19:18:04 +0100 Subject: [PATCH 071/608] fix: don't try matching commands if there is nothing to match --- PluralKit.Bot/Handlers/MessageCreated.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 30ced3ee..002a641a 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -103,7 +103,7 @@ namespace PluralKit.Bot if (content == null) return false; // Check for command prefix - if (!HasCommandPrefix(content, shard.User?.Id ?? default, out var cmdStart)) + if (!HasCommandPrefix(content, shard.User?.Id ?? default, out var cmdStart) || cmdStart == content.Length) return false; // Trim leading whitespace from command without actually modifying the string From 517abf7ff14deec856d30d1d877381a55814ce6f Mon Sep 17 00:00:00 2001 From: spiral Date: Sat, 1 May 2021 19:20:00 +0100 Subject: [PATCH 072/608] feat: show member color in reply embed --- PluralKit.Bot/Proxy/ProxyService.cs | 9 ++++++--- PluralKit.Core/Database/Functions/ProxyMember.cs | 2 ++ PluralKit.Core/Database/Functions/functions.sql | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 32781447..e1ac5ac8 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -116,7 +116,7 @@ namespace PluralKit.Bot if (repliedTo != null) { var nickname = await FetchReferencedMessageAuthorNickname(trigger, repliedTo); - var embed = CreateReplyEmbed(trigger, repliedTo, nickname); + var embed = CreateReplyEmbed(match, trigger, repliedTo, nickname); if (embed != null) embeds.Add(embed); } @@ -160,7 +160,7 @@ namespace PluralKit.Bot } } - private Embed CreateReplyEmbed(Message trigger, Message repliedTo, string? nickname) + private Embed CreateReplyEmbed(ProxyMatch match, Message trigger, Message repliedTo, string? nickname) { // repliedTo doesn't have a GuildId field :/ var jumpLink = $"https://discord.com/channels/{trigger.GuildId}/{repliedTo.ChannelId}/{repliedTo.Id}"; @@ -194,11 +194,14 @@ namespace PluralKit.Bot var username = nickname ?? repliedTo.Author.Username; var avatarUrl = $"https://cdn.discordapp.com/avatars/{repliedTo.Author.Id}/{repliedTo.Author.Avatar}.png"; + Console.WriteLine($"color {match.Member.Color}"); + return new Embed { // unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation] Author = new($"{username}\u2004\u21a9\ufe0f", IconUrl: avatarUrl), - Description = content.ToString() + Description = content.ToString(), + Color = match.Member.Color?.ToDiscordColor(), }; } diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index fc18a582..681ef179 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -19,7 +19,9 @@ namespace PluralKit.Core public string? ServerAvatar { get; } public string? Avatar { get; } + public bool AllowAutoproxy { get; } + public string? Color { get; } public string ProxyName(MessageContext ctx) => ctx.SystemTag != null ? $"{ServerName ?? DisplayName ?? Name} {ctx.SystemTag}" diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index f959f447..55628d49 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -64,10 +64,12 @@ create function proxy_members(account_id bigint, guild_id bigint) server_name text, display_name text, name text, - + server_avatar text, avatar text, + color char(6), + allow_autoproxy bool ) as $$ @@ -86,6 +88,8 @@ as $$ member_guild.avatar_url as server_avatar, members.avatar_url as avatar, + members.color as color, + members.allow_autoproxy as allow_autoproxy from accounts inner join systems on systems.id = accounts.system From 653c7b22bd8ed6dbcc0c78329ace66e68acf94fb Mon Sep 17 00:00:00 2001 From: spiral Date: Sat, 1 May 2021 19:32:37 +0100 Subject: [PATCH 073/608] remove random console log --- PluralKit.Bot/Proxy/ProxyService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index e1ac5ac8..bb0f6232 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -194,8 +194,6 @@ namespace PluralKit.Bot var username = nickname ?? repliedTo.Author.Username; var avatarUrl = $"https://cdn.discordapp.com/avatars/{repliedTo.Author.Id}/{repliedTo.Author.Avatar}.png"; - Console.WriteLine($"color {match.Member.Color}"); - return new Embed { // unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation] @@ -295,4 +293,4 @@ namespace PluralKit.Bot if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName); } } -} \ No newline at end of file +} From 65387bfea40739b7f65c3d5803c223ee3db2ff38 Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 3 May 2021 09:24:18 +0200 Subject: [PATCH 074/608] Loosen regex for normalizing route path logging Signed-off-by: Ske --- .../Tracing/DiscordRequestObserver.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/PluralKit.Bot/Tracing/DiscordRequestObserver.cs b/PluralKit.Bot/Tracing/DiscordRequestObserver.cs index 85859693..66a8a8b9 100644 --- a/PluralKit.Bot/Tracing/DiscordRequestObserver.cs +++ b/PluralKit.Bot/Tracing/DiscordRequestObserver.cs @@ -36,20 +36,20 @@ namespace PluralKit.Bot private string NormalizeRoutePath(string url) { - 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, @"/members/\d{17,19}", "/members/{user_id}"); - url = Regex.Replace(url, @"/webhooks/\d{17,19}/[^/]+", "/webhooks/{webhook_id}/{webhook_token}"); - url = Regex.Replace(url, @"/webhooks/\d{17,19}", "/webhooks/{webhook_id}"); - url = Regex.Replace(url, @"/users/\d{17,19}", "/users/{user_id}"); - url = Regex.Replace(url, @"/bans/\d{17,19}", "/bans/{user_id}"); - url = Regex.Replace(url, @"/roles/\d{17,19}", "/roles/{role_id}"); - url = Regex.Replace(url, @"/pins/\d{17,19}", "/pins/{message_id}"); - url = Regex.Replace(url, @"/emojis/\d{17,19}", "/emojis/{emoji_id}"); - url = Regex.Replace(url, @"/guilds/\d{17,19}", "/guilds/{guild_id}"); - url = Regex.Replace(url, @"/integrations/\d{17,19}", "/integrations/{integration_id}"); - url = Regex.Replace(url, @"/permissions/\d{17,19}", "/permissions/{overwrite_id}"); - url = Regex.Replace(url, @"/reactions/[^{/]+/\d{17,19}", "/reactions/{emoji}/{user_id}"); + url = Regex.Replace(url, @"/channels/\d+", "/channels/{channel_id}"); + url = Regex.Replace(url, @"/messages/\d+", "/messages/{message_id}"); + url = Regex.Replace(url, @"/members/\d+", "/members/{user_id}"); + url = Regex.Replace(url, @"/webhooks/\d+/[^/]+", "/webhooks/{webhook_id}/{webhook_token}"); + url = Regex.Replace(url, @"/webhooks/\d+", "/webhooks/{webhook_id}"); + url = Regex.Replace(url, @"/users/\d+", "/users/{user_id}"); + url = Regex.Replace(url, @"/bans/\d+", "/bans/{user_id}"); + url = Regex.Replace(url, @"/roles/\d+", "/roles/{role_id}"); + url = Regex.Replace(url, @"/pins/\d+", "/pins/{message_id}"); + url = Regex.Replace(url, @"/emojis/\d+", "/emojis/{emoji_id}"); + url = Regex.Replace(url, @"/guilds/\d+", "/guilds/{guild_id}"); + url = Regex.Replace(url, @"/integrations/\d+", "/integrations/{integration_id}"); + url = Regex.Replace(url, @"/permissions/\d+", "/permissions/{overwrite_id}"); + url = Regex.Replace(url, @"/reactions/[^{/]+/\d+", "/reactions/{emoji}/{user_id}"); url = Regex.Replace(url, @"/reactions/[^{/]+", "/reactions/{emoji}"); url = Regex.Replace(url, @"/invites/[^{/]+", "/invites/{invite_code}"); From aa2a234f8dd0a32dff015708938162d275832cfd Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 3 May 2021 10:29:22 +0200 Subject: [PATCH 075/608] Fix permission error on message edit handler Signed-off-by: Ske --- PluralKit.Bot/Handlers/MessageCreated.cs | 2 +- PluralKit.Bot/Handlers/MessageEdited.cs | 49 ++++++++++++++++--- .../Services/LastMessageCacheService.cs | 23 +++------ 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 002a641a..72e6b8be 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -53,7 +53,7 @@ namespace PluralKit.Bot private bool IsDuplicateMessage(Message msg) => // We consider a message duplicate if it has the same ID as the previous message that hit the gateway - _lastMessageCache.GetLastMessage(msg.ChannelId)?.mid == msg.Id; + _lastMessageCache.GetLastMessage(msg.ChannelId)?.Id == msg.Id; public async Task Handle(Shard shard, MessageCreateEvent evt) { diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index 816914fe..8a35df3a 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -11,6 +11,8 @@ using Myriad.Types; using PluralKit.Core; +using Serilog; + namespace PluralKit.Bot { @@ -25,9 +27,9 @@ namespace PluralKit.Bot private readonly IDiscordCache _cache; private readonly Bot _bot; private readonly DiscordApiClient _rest; + private readonly ILogger _logger; - - public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot, DiscordApiClient rest) + public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot, DiscordApiClient rest, ILogger logger) { _lastMessageCache = lastMessageCache; _proxy = proxy; @@ -38,6 +40,7 @@ namespace PluralKit.Bot _cache = cache; _bot = bot; _rest = rest; + _logger = logger.ForContext(); } public async Task Handle(Shard shard, MessageUpdateEvent evt) @@ -55,7 +58,7 @@ namespace PluralKit.Bot var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId); // Only react to the last message in the channel - if (lastMessage?.mid != evt.Id) + if (lastMessage?.Id != evt.Id) return; // Just run the normal message handling code, with a flag to disable autoproxying @@ -64,8 +67,23 @@ namespace PluralKit.Bot using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) ctx = await _repo.GetMessageContext(conn, evt.Author.Value!.Id, channel.GuildId!.Value, evt.ChannelId); - Message referencedMessage = (lastMessage.referenced_message != null) ? await _rest.GetMessage(evt.ChannelId, lastMessage.referenced_message.Value) : null; + var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel); + var botPermissions = _bot.PermissionsIn(channel.Id); + await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy: false, guild: guild, channel: channel, botPermissions: botPermissions); + } + private async Task GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage, Channel channel) + { + var referencedMessage = await GetReferencedMessage(evt.ChannelId, lastMessage.ReferencedMessage); + + var messageReference = lastMessage.ReferencedMessage != null + ? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value) + : null; + + var messageType = lastMessage.ReferencedMessage != null + ? Message.MessageType.Reply + : Message.MessageType.Default; + // TODO: is this missing anything? var equivalentEvt = new MessageCreateEvent { @@ -76,12 +94,27 @@ namespace PluralKit.Bot Member = evt.Member.Value, Content = evt.Content.Value, Attachments = evt.Attachments.Value ?? Array.Empty(), - MessageReference = (lastMessage.referenced_message != null) ? new (channel.GuildId, evt.ChannelId, lastMessage.referenced_message.Value) : null, + MessageReference = messageReference, ReferencedMessage = referencedMessage, - Type = (lastMessage.referenced_message != null) ? Message.MessageType.Reply : Message.MessageType.Default, + Type = messageType, }; - var botPermissions = _bot.PermissionsIn(channel.Id); - await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy: false, guild: guild, channel: channel, botPermissions: botPermissions); + return equivalentEvt; + } + + private async Task GetReferencedMessage(ulong channelId, ulong? referencedMessageId) + { + if (referencedMessageId == null) + return null; + + var botPermissions = _bot.PermissionsIn(channelId); + if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory)) + { + _logger.Warning("Tried to get referenced message in channel {ChannelId} to reply but bot does not have Read Message History", + channelId); + return null; + } + + return await _rest.GetMessage(channelId, referencedMessageId.Value); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/LastMessageCacheService.cs b/PluralKit.Bot/Services/LastMessageCacheService.cs index 1a8b1d17..0c77d6fc 100644 --- a/PluralKit.Bot/Services/LastMessageCacheService.cs +++ b/PluralKit.Bot/Services/LastMessageCacheService.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; using System.Collections.Generic; using Myriad.Types; @@ -12,26 +13,14 @@ namespace PluralKit.Bot public void AddMessage(Message msg) { - _cache[msg.ChannelId] = new CachedMessage(msg); + _cache[msg.ChannelId] = new CachedMessage(msg.Id, msg.ReferencedMessage.Value?.Id); } - public CachedMessage GetLastMessage(ulong channel) + public CachedMessage? GetLastMessage(ulong channel) { - if (_cache.TryGetValue(channel, out var message)) return message; - return null; + return _cache.TryGetValue(channel, out var message) ? message : null; } } - public class CachedMessage - { - public ulong mid; - public ulong? referenced_message; - - public CachedMessage(Message msg) - { - mid = msg.Id; - if (msg.ReferencedMessage.Value != null) - referenced_message = msg.ReferencedMessage.Value.Id; - } - } + public record CachedMessage(ulong Id, ulong? ReferencedMessage); } From 33cabff3591c7f2e3d2c9965d2f6fee7911fbc90 Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 3 May 2021 10:34:53 +0200 Subject: [PATCH 076/608] Clean up autoproxy timeout command code Signed-off-by: Ske --- PluralKit.Bot/Commands/Autoproxy.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index a24b81d2..7b9adf48 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -160,8 +160,7 @@ namespace PluralKit.Bot var timeoutStr = ctx.RemainderOrNull(); var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); - Console.WriteLine(timeoutPeriod.Value.TotalHours); - if (timeoutPeriod.Value.TotalHours > (ulong)100000) + if (timeoutPeriod.Value.TotalHours > 100000) { // sanity check to prevent seconds overflow if someone types in 999999999 overflow = timeoutPeriod.Value; @@ -170,7 +169,6 @@ namespace PluralKit.Bot else newTimeout = timeoutPeriod; } - var timeoutSeconds = newTimeout.HasValue ? newTimeout?.TotalSeconds : -1; await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, new SystemPatch { LatchTimeout = (int?) newTimeout?.TotalSeconds })); From f120a592f0d3b1ae3cb20722b079db0feac1d081 Mon Sep 17 00:00:00 2001 From: spiral Date: Mon, 3 May 2021 10:10:21 +0100 Subject: [PATCH 077/608] add latest faq entry --- docs/content/faq.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/content/faq.md b/docs/content/faq.md index c5282ac2..a2d64bb8 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -75,4 +75,7 @@ You might not be logged into the invite screen. Make sure you're logged into Dis If you are on mobile and are having issues, try copy-pasting the link into your browser instead of tapping it. ### Why are my member lists broken on mobile? -This is a bug in Discord's mobile client. It handles formatting slightly different than other clients. This can't be fixed without breaking things elsewhere, so we're waiting for a fix on their end. \ No newline at end of file +This is a bug in Discord's mobile client. It handles formatting slightly different than other clients. This can't be fixed without breaking things elsewhere, so we're waiting for a fix on their end. + +### Why is my time showing up incorrectly after Daylight savings time change? +You probably set your timezone in PluralKit to a specific timezone, and PluralKit doesn't know how to calculate daylight savings time with that. Instead, set your timezone to a city (for example, "America/Toronto") - you can find your correct city identifier on From 3d624b39e4240be69232da2f54adb465abf69ebb Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 3 May 2021 12:33:30 +0200 Subject: [PATCH 078/608] Add message editing command Signed-off-by: Ske --- Myriad/Rest/DiscordApiClient.cs | 5 + .../Requests/WebhookMessageEditRequest.cs | 15 +++ .../CommandSystem/ContextArgumentsExt.cs | 24 +++++ PluralKit.Bot/Commands/CommandTree.cs | 3 + PluralKit.Bot/Commands/MessageEdit.cs | 91 +++++++++++++++++++ PluralKit.Bot/Commands/Misc.cs | 21 ++--- PluralKit.Bot/Modules.cs | 1 + .../Services/WebhookExecutorService.cs | 12 +++ .../Repository/ModelRepository.Message.cs | 12 +++ 9 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs create mode 100644 PluralKit.Bot/Commands/MessageEdit.cs diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 4612fd2c..6ab105a5 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -121,6 +121,11 @@ namespace Myriad.Rest _client.PostMultipart($"/webhooks/{webhookId}/{webhookToken}?wait=true", ("ExecuteWebhook", webhookId), request, files)!; + public Task EditWebhookMessage(ulong webhookId, string webhookToken, ulong messageId, + WebhookMessageEditRequest request) => + _client.Patch($"/webhooks/{webhookId}/{webhookToken}/messages/{messageId}", + ("EditWebhookMessage", webhookId), request)!; + public Task CreateDm(ulong recipientId) => _client.Post($"/users/@me/channels", ("CreateDM", default), new CreateDmRequest(recipientId))!; diff --git a/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs new file mode 100644 index 00000000..039ac625 --- /dev/null +++ b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +using Myriad.Utils; + +namespace Myriad.Rest.Types.Requests +{ + public record WebhookMessageEditRequest + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Content { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional AllowedMentions { get; init; } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs index 3e1b2572..06dfa5fe 100644 --- a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using Myriad.Types; + using PluralKit.Core; namespace PluralKit.Bot @@ -68,6 +71,27 @@ namespace PluralKit.Bot return matched; } + public static ulong? MatchMessage(this Context ctx, bool parseRawMessageId) + { + if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference != null) + return ctx.Message.MessageReference.MessageId; + + var word = ctx.PeekArgument(); + if (word == null) + return null; + + if (parseRawMessageId && ulong.TryParse(word, out var mid)) + return mid; + + var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)"); + if (!match.Success) + return null; + + var messageId = ulong.Parse(match.Groups[1].Value); + ctx.PopArgument(); + return messageId; + } + public static async Task> ParseMemberList(this Context ctx, SystemId? restrictToSystem) { var members = new List(); diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 872bc86b..717a5667 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -79,6 +79,7 @@ namespace PluralKit.Bot public static Command Help = new Command("help", "help", "Shows help information about PluralKit"); public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying"); public static Command Message = new Command("message", "message [delete|author]", "Looks up a proxied message"); + public static Command MessageEdit = new Command("edit", "edit [link] ", "Edit a previously proxied message"); public static Command LogChannel = new Command("log channel", "log channel ", "Designates a channel to post proxied messages to"); public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); @@ -160,6 +161,8 @@ namespace PluralKit.Bot return ctx.Execute(Explain, m => m.Explain(ctx)); if (ctx.Match("message", "msg")) return ctx.Execute(Message, m => m.GetMessage(ctx)); + if (ctx.Match("edit", "e")) + return ctx.Execute(MessageEdit, m => m.EditMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx)); diff --git a/PluralKit.Bot/Commands/MessageEdit.cs b/PluralKit.Bot/Commands/MessageEdit.cs new file mode 100644 index 00000000..c979bc59 --- /dev/null +++ b/PluralKit.Bot/Commands/MessageEdit.cs @@ -0,0 +1,91 @@ +#nullable enable +using System.Threading.Tasks; + +using Myriad.Rest; +using Myriad.Rest.Exceptions; +using Myriad.Types; + +using NodaTime; + +using PluralKit.Core; + +namespace PluralKit.Bot +{ + public class MessageEdit + { + private static readonly Duration EditTimeout = Duration.FromMinutes(10); + + private readonly IDatabase _db; + private readonly ModelRepository _repo; + private readonly IClock _clock; + private readonly DiscordApiClient _rest; + private readonly WebhookExecutorService _webhookExecutor; + + public MessageEdit(IDatabase db, ModelRepository repo, IClock clock, DiscordApiClient rest, WebhookExecutorService webhookExecutor) + { + _db = db; + _repo = repo; + _clock = clock; + _rest = rest; + _webhookExecutor = webhookExecutor; + } + + public async Task EditMessage(Context ctx) + { + var msg = await GetMessageToEdit(ctx); + if (!ctx.HasNext()) + throw new PKSyntaxError("You need to include the message to edit in."); + + if (ctx.Author.Id != msg.Sender) + throw new PKError("Can't edit a message sent from a different account."); + + var newContent = ctx.RemainderOrNull(); + + try + { + await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent); + + if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) + await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id); + } + catch (NotFoundException) + { + throw new PKError("Could not edit message."); + } + } + + private async Task GetMessageToEdit(Context ctx) + { + var referencedMessage = ctx.MatchMessage(false); + if (referencedMessage != null) + { + await using var conn = await _db.Obtain(); + var msg = await _repo.GetMessage(conn, referencedMessage.Value); + if (msg == null) + throw new PKError("This is not a message proxied by PluralKit."); + + return msg.Message; + } + + var recent = await FindRecentMessage(ctx); + if (recent == null) + throw new PKError("Could not find a recent message to edit."); + + return recent; + } + + private async Task FindRecentMessage(Context ctx) + { + await using var conn = await _db.Obtain(); + var lastMessage = await _repo.GetLastMessage(conn, ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id); + if (lastMessage == null) + return null; + + var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Mid); + if (_clock.GetCurrentInstant() - timestamp > EditTimeout) + return null; + + return lastMessage; + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index fda8e14c..7d208954 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -215,17 +215,16 @@ namespace PluralKit.Bot { public async Task GetMessage(Context ctx) { - var word = ctx.PopArgument() ?? throw new PKSyntaxError("You must pass a message ID or link."); - - ulong messageId; - if (ulong.TryParse(word, out var id)) - messageId = id; - else if (Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)") is Match match && match.Success) - messageId = ulong.Parse(match.Groups[1].Value); - else throw new PKSyntaxError($"Could not parse {word.AsCode()} as a message ID or link."); - - var message = await _db.Execute(c => _repo.GetMessage(c, messageId)); - if (message == null) throw Errors.MessageNotFound(messageId); + var messageId = ctx.MatchMessage(true); + if (messageId == null) + { + if (!ctx.HasNext()) + throw new PKSyntaxError("You must pass a message ID or link."); + throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link."); + } + + var message = await _db.Execute(c => _repo.GetMessage(c, messageId.Value)); + if (message == null) throw Errors.MessageNotFound(messageId.Value); if (ctx.Match("delete") || ctx.MatchFlag("delete")) { diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index cef29570..f96a64a8 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -54,6 +54,7 @@ namespace PluralKit.Bot builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 39379cdf..bbc4c20a 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -76,6 +76,18 @@ namespace PluralKit.Bot return webhookMessage; } + public async Task EditWebhookMessage(ulong channelId, ulong messageId, string newContent) + { + var webhook = await _webhookCache.GetWebhook(channelId); + var allowedMentions = newContent.ParseMentions() with { + Roles = Array.Empty(), + Parse = Array.Empty() + }; + + return await _rest.EditWebhookMessage(webhook.Id, webhook.Token, messageId, + new WebhookMessageEditRequest {Content = newContent, AllowedMentions = allowedMentions}); + } + private async Task ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false) { var guild = _cache.GetGuild(req.GuildId); diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Message.cs b/PluralKit.Core/Database/Repository/ModelRepository.Message.cs index 9acf8be2..2622ef38 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Message.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Message.cs @@ -42,6 +42,18 @@ namespace PluralKit.Core _logger.Information("Bulk deleted messages ({FoundCount} found) from database: {MessageIds}", rowCount, ids); } + + public async Task GetLastMessage(IPKConnection conn, ulong guildId, ulong channelId, ulong accountId) + { + // Want to index scan on the (guild, sender, mid) index so need the additional constraint + return await conn.QuerySingleOrDefaultAsync( + "select * from messages where guild = @Guild and channel = @Channel and sender = @Sender order by mid desc limit 1", new + { + Guild = guildId, + Channel = channelId, + Sender = accountId + }); + } } public class PKMessage From 6395872f2d91bdca6e61e2bb9ed7aafd0a9917a3 Mon Sep 17 00:00:00 2001 From: Mikaela Szekely Date: Mon, 5 Apr 2021 22:25:13 -0600 Subject: [PATCH 079/608] allow pk;system proxy to take a server ID So people can disable proxying in a server without outing themselves :) --- .../ContextEntityArgumentsExt.cs | 20 ++++++++++++++- PluralKit.Bot/Commands/CommandTree.cs | 2 +- PluralKit.Bot/Commands/SystemEdit.cs | 25 +++++++++++++------ docs/content/command-list.md | 2 +- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 00acf4b7..a5995f89 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Myriad.Extensions; using Myriad.Types; @@ -168,5 +169,22 @@ namespace PluralKit.Bot ctx.PopArgument(); return channel; } + + public static Guild MatchGuild(this Context ctx) + { + try + { + var id = ulong.Parse(ctx.PeekArgument()); + ctx.Cache.TryGetGuild(id, out var guild); + if (guild != null) + ctx.PopArgument(); + + return guild; + } + catch (FormatException) + { + return null; + } + } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 717a5667..8c3ade92 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -18,7 +18,7 @@ namespace PluralKit.Bot public static Command SystemAvatar = new Command("system icon", "system icon [url|@mention]", "Changes your system's icon"); public static Command SystemDelete = new Command("system delete", "system delete", "Deletes your system"); public static Command SystemTimezone = new Command("system timezone", "system timezone [timezone]", "Changes your system's time zone"); - public static Command SystemProxy = new Command("system proxy", "system proxy [on|off]", "Enables or disables message proxying in a specific server"); + public static Command SystemProxy = new Command("system proxy", "system proxy [server id] [on|off]", "Enables or disables message proxying in a specific server"); public static Command SystemList = new Command("system list", "system [system] list [full]", "Lists a system's members"); public static Command SystemFind = new Command("system find", "system [system] find [full] ", "Searches a system's members given a search term"); public static Command SystemFronter = new Command("system fronter", "system [system] fronter", "Shows a system's fronter(s)"); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 068fe838..1e1988ec 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Myriad.Builders; +using Myriad.Types; using NodaTime; using NodaTime.Text; @@ -232,8 +233,18 @@ namespace PluralKit.Bot public async Task SystemProxy(Context ctx) { - ctx.CheckSystem().CheckGuildContext(); - var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.Guild.Id, ctx.System.Id)); + ctx.CheckSystem(); + + var guild = ctx.MatchGuild() ?? ctx.Guild ?? + throw new PKError("You must run this command in a server or pass a server ID."); + + var gs = await _db.Execute(c => _repo.GetSystemGuild(c, guild.Id, ctx.System.Id)); + + string serverText; + if (guild.Id == ctx.Guild?.Id) + serverText = $"this server ({guild.Name.EscapeMarkdown()})"; + else + serverText = $"the server {guild.Name.EscapeMarkdown()}"; bool newValue; if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; @@ -242,19 +253,19 @@ namespace PluralKit.Bot else { if (gs.ProxyEnabled) - await ctx.Reply("Proxying in this server is currently **enabled** for your system. To disable it, type `pk;system proxy off`."); + await ctx.Reply($"Proxying in {serverText} is currently **enabled** for your system. To disable it, type `pk;system proxy off`."); else - await ctx.Reply("Proxying in this server is currently **disabled** for your system. To enable it, type `pk;system proxy on`."); + await ctx.Reply($"Proxying in {serverText} is currently **disabled** for your system. To enable it, type `pk;system proxy on`."); return; } var patch = new SystemGuildPatch {ProxyEnabled = newValue}; - await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch)); + await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, guild.Id, patch)); if (newValue) - await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **enabled** for your system."); + await ctx.Reply($"Message proxying in {serverText} is now **enabled** for your system."); else - await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **disabled** for your system."); + await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system."); } public async Task SystemTimezone(Context ctx) diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 93ba23b9..66b34675 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -21,7 +21,7 @@ Words in **\** or **[square brackets]** mean fill-in-the-blank. - `pk;system privacy ` - Changes your systems privacy settings. - `pk;system tag [tag]` - Changes the system tag of your system. - `pk;system timezone [location]` - Changes the time zone of your system. -- `pk;system proxy [on|off]` - Toggles message proxying for a specific server. +- `pk;system proxy [server id] [on|off]` - Toggles message proxying for a specific server. - `pk;system delete` - Deletes your system. - `pk;system [system] fronter` - Shows the current fronter of a system. - `pk;system [system] fronthistory` - Shows the last 10 fronters of a system. From b1c7fbd2dfe50153937818d83c5893c738c7e6a5 Mon Sep 17 00:00:00 2001 From: Spectralitree <72747870+Spectralitree@users.noreply.github.com> Date: Fri, 7 May 2021 10:40:57 +0200 Subject: [PATCH 080/608] Add color to frontpercent embed (#307) * Add color to frontpercent embed * don't pass color as a separate argument Co-authored-by: Astrid --- PluralKit.Bot/Services/EmbedService.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 0ffc89fc..2e88a81e 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -323,13 +323,29 @@ namespace PluralKit.Bot { public Task CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle, bool ignoreNoFronters) { + string color = system.Color; + if (group != null) + { + color = group.Color; + } + + uint embedColor; + try + { + embedColor = color?.ToDiscordColor() ?? DiscordUtils.Gray; + } + catch (ArgumentException) + { + embedColor = DiscordUtils.Gray; + } + var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; // this is kinda messy? var hasFrontersPeriod = Duration.FromTicks(breakdown.MemberSwitchDurations.Values.ToList().Sum(i => i.TotalTicks)); var eb = new EmbedBuilder() .Title(embedTitle) - .Color(DiscordUtils.Gray) + .Color(embedColor) .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" From dd2c669cd2240e8845087b4f1b9af120db2aa9e2 Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 7 May 2021 10:48:19 +0200 Subject: [PATCH 081/608] Fix some compiler warnings Signed-off-by: Ske --- Myriad/Myriad.csproj | 5 +++++ PluralKit.API/PluralKit.API.csproj | 1 + PluralKit.Bot/PluralKit.Bot.csproj | 1 + PluralKit.Core/PluralKit.Core.csproj | 1 + 4 files changed, 8 insertions(+) diff --git a/Myriad/Myriad.csproj b/Myriad/Myriad.csproj index 2b027a76..f8c73f84 100644 --- a/Myriad/Myriad.csproj +++ b/Myriad/Myriad.csproj @@ -4,6 +4,11 @@ net5.0 enable + + + + $(NoWarn);8618 + true diff --git a/PluralKit.API/PluralKit.API.csproj b/PluralKit.API/PluralKit.API.csproj index ab363d32..07dc2372 100644 --- a/PluralKit.API/PluralKit.API.csproj +++ b/PluralKit.API/PluralKit.API.csproj @@ -2,6 +2,7 @@ net5.0 + annotations diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 7c06a10c..9d14abce 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -3,6 +3,7 @@ Exe net5.0 + annotations diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index d31fd3fe..d47c4ff1 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -2,6 +2,7 @@ net5.0 + annotations From dbde8c07ad83fbfbbefa543988560a042f77a434 Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 7 May 2021 10:56:15 +0200 Subject: [PATCH 082/608] Fix a few more compiler warnings Signed-off-by: Ske --- Myriad/Cache/MemoryDiscordCache.cs | 7 ++++--- Myriad/Myriad.csproj | 1 + .../CommandSystem/ContextEntityArgumentsExt.cs | 12 ++++++------ PluralKit.Core/Database/Wrappers/IPKCommand.cs | 2 +- PluralKit.Core/Database/Wrappers/PKCommand.cs | 4 +++- PluralKit.Core/Database/Wrappers/PKConnection.cs | 4 +++- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs index 9dc4ca1c..3174870b 100644 --- a/Myriad/Cache/MemoryDiscordCache.cs +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -151,10 +151,11 @@ namespace Myriad.Cache public bool TryGetRole(ulong roleId, out Role role) => _roles.TryGetValue(roleId, out role!); - public async IAsyncEnumerable GetAllGuilds() + public IAsyncEnumerable GetAllGuilds() { - foreach (var guild in _guilds.Values) - yield return guild.Guild; + return _guilds.Values + .Select(g => g.Guild) + .ToAsyncEnumerable(); } public IEnumerable GetGuildChannels(ulong guildId) diff --git a/Myriad/Myriad.csproj b/Myriad/Myriad.csproj index f8c73f84..aa02602f 100644 --- a/Myriad/Myriad.csproj +++ b/Myriad/Myriad.csproj @@ -19,6 +19,7 @@ + diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index a5995f89..9a88b4d5 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -155,19 +155,19 @@ namespace PluralKit.Bot return $"Group not found. Note that a group ID is 5 characters long."; } - public static async Task MatchChannel(this Context ctx) + public static Task MatchChannel(this Context ctx) { if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id)) - return null; + return Task.FromResult(null); if (!ctx.Cache.TryGetChannel(id, out var channel)) - return null; + return Task.FromResult(null); - if (!(channel.Type == Channel.ChannelType.GuildText || channel.Type == Channel.ChannelType.GuildText)) - return null; + if (!(channel.Type == Channel.ChannelType.GuildText || channel.Type == Channel.ChannelType.GuildNews)) + return Task.FromResult(null); ctx.PopArgument(); - return channel; + return Task.FromResult(channel); } public static Guild MatchGuild(this Context ctx) diff --git a/PluralKit.Core/Database/Wrappers/IPKCommand.cs b/PluralKit.Core/Database/Wrappers/IPKCommand.cs index 3f814d6c..affdf72d 100644 --- a/PluralKit.Core/Database/Wrappers/IPKCommand.cs +++ b/PluralKit.Core/Database/Wrappers/IPKCommand.cs @@ -10,7 +10,7 @@ namespace PluralKit.Core { public Task PrepareAsync(CancellationToken ct = default); public Task ExecuteNonQueryAsync(CancellationToken ct = default); - public Task ExecuteScalarAsync(CancellationToken ct = default); + public Task ExecuteScalarAsync(CancellationToken ct = default); public Task ExecuteReaderAsync(CancellationToken ct = default); public Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken ct = default); } diff --git a/PluralKit.Core/Database/Wrappers/PKCommand.cs b/PluralKit.Core/Database/Wrappers/PKCommand.cs index 02d75079..c6a5282a 100644 --- a/PluralKit.Core/Database/Wrappers/PKCommand.cs +++ b/PluralKit.Core/Database/Wrappers/PKCommand.cs @@ -2,6 +2,7 @@ using System; using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -32,13 +33,14 @@ namespace PluralKit.Core } public override Task ExecuteNonQueryAsync(CancellationToken ct) => LogQuery(Inner.ExecuteNonQueryAsync(ct)); - public override Task ExecuteScalarAsync(CancellationToken ct) => LogQuery(Inner.ExecuteScalarAsync(ct)); + public override Task ExecuteScalarAsync(CancellationToken ct) => LogQuery(Inner.ExecuteScalarAsync(ct)); protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken ct) => await LogQuery(Inner.ExecuteReaderAsync(behavior, ct)); public override Task PrepareAsync(CancellationToken ct = default) => Inner.PrepareAsync(ct); public override void Cancel() => Inner.Cancel(); protected override DbParameter CreateDbParameter() => Inner.CreateParameter(); + [AllowNull] public override string CommandText { get => Inner.CommandText; diff --git a/PluralKit.Core/Database/Wrappers/PKConnection.cs b/PluralKit.Core/Database/Wrappers/PKConnection.cs index acf171be..fe04948c 100644 --- a/PluralKit.Core/Database/Wrappers/PKConnection.cs +++ b/PluralKit.Core/Database/Wrappers/PKConnection.cs @@ -2,6 +2,7 @@ using System; using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -73,13 +74,14 @@ namespace PluralKit.Core IDbTransaction IPKConnection.BeginTransaction() => throw SyncError(nameof(BeginTransaction)); IDbTransaction IPKConnection.BeginTransaction(IsolationLevel level) => throw SyncError(nameof(BeginTransaction)); + [AllowNull] public override string ConnectionString { get => Inner.ConnectionString; set => Inner.ConnectionString = value; } - public override string? Database => Inner.Database; + public override string Database => Inner.Database!; public override ConnectionState State => Inner.State; public override string DataSource => Inner.DataSource; public override string ServerVersion => Inner.ServerVersion; From 1f0c75d6771e43ed781eafd810be8414c199ec60 Mon Sep 17 00:00:00 2001 From: spiral Date: Fri, 7 May 2021 17:35:09 +0100 Subject: [PATCH 083/608] Add logging message edits --- PluralKit.Bot/Commands/MessageEdit.cs | 8 ++- PluralKit.Bot/Services/EmbedService.cs | 13 ++++ PluralKit.Bot/Services/LogChannelService.cs | 68 ++++++++++++++++----- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/PluralKit.Bot/Commands/MessageEdit.cs b/PluralKit.Bot/Commands/MessageEdit.cs index c979bc59..782a6f6f 100644 --- a/PluralKit.Bot/Commands/MessageEdit.cs +++ b/PluralKit.Bot/Commands/MessageEdit.cs @@ -20,14 +20,16 @@ namespace PluralKit.Bot private readonly IClock _clock; private readonly DiscordApiClient _rest; private readonly WebhookExecutorService _webhookExecutor; + private readonly LogChannelService _logChannel; - public MessageEdit(IDatabase db, ModelRepository repo, IClock clock, DiscordApiClient rest, WebhookExecutorService webhookExecutor) + public MessageEdit(IDatabase db, ModelRepository repo, IClock clock, DiscordApiClient rest, WebhookExecutorService webhookExecutor, LogChannelService logChannel) { _db = db; _repo = repo; _clock = clock; _rest = rest; _webhookExecutor = webhookExecutor; + _logChannel = logChannel; } public async Task EditMessage(Context ctx) @@ -41,12 +43,16 @@ namespace PluralKit.Bot var newContent = ctx.RemainderOrNull(); + var originalMsg = await _rest.GetMessage(msg.Channel, msg.Mid); + try { await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent); if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id); + + await _logChannel.LogEditedMessage(ctx.MessageContext, msg, ctx.Message, originalMsg!, newContent); } catch (NotFoundException) { diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 2e88a81e..6c5111c8 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -112,6 +112,19 @@ namespace PluralKit.Bot { .Build(); } + public Embed CreateEditedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, User sender, string content, string oldContent, Channel channel) { + var timestamp = DiscordUtils.SnowflakeToInstant(messageId); + var name = member.NameFor(LookupContext.ByNonOwner); + return new EmbedBuilder() + .Author(new($"[Edited] #{channel.Name}: {name}", IconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner)))) + .Thumbnail(new(member.AvatarFor(LookupContext.ByNonOwner))) + .Field(new("Old message", oldContent?.NormalizeLineEndSpacing().Truncate(1000))) + .Description(content?.NormalizeLineEndSpacing()) + .Footer(new($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}")) + .Timestamp(timestamp.ToDateTimeOffset().ToString("O")) + .Build(); + } + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx) { diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index 6501181f..308d452f 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Dapper; @@ -34,24 +35,11 @@ namespace PluralKit.Bot { public async ValueTask LogMessage(MessageContext ctx, ProxyMatch proxy, Message trigger, ulong hookMessage) { - if (ctx.SystemId == null || ctx.LogChannel == null || ctx.InLogBlacklist) return; - - // Find log channel and check if valid - var logChannel = await FindLogChannel(trigger.GuildId!.Value, ctx.LogChannel.Value); - if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return; + var logChannel = await GetAndCheckLogChannel(ctx, trigger); + if (logChannel == null) return; var triggerChannel = _cache.GetChannel(trigger.ChannelId); - // Check bot permissions - var perms = _bot.PermissionsIn(logChannel.Id); - if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks)) - { - _logger.Information( - "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", - ctx.LogChannel.Value, trigger.GuildId!.Value, perms); - return; - } - // Send embed! await using var conn = await _db.Obtain(); var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), @@ -61,6 +49,56 @@ namespace PluralKit.Bot { await _rest.CreateMessage(logChannel.Id, new() {Content = url, Embed = embed}); } + public async ValueTask LogEditedMessage(MessageContext ctx, PKMessage proxy, Message trigger, Message originalMessage, string newContent) + { + var logChannel = await GetAndCheckLogChannel(ctx, trigger, proxy); + if (logChannel == null) return; + + var triggerChannel = _cache.GetChannel(proxy.Channel); + + // Send embed! + await using var conn = await _db.Obtain(); + var embed = _embed.CreateEditedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), + await _repo.GetMember(conn, proxy.Member), originalMessage.Id, trigger.Id, trigger.Author, newContent, originalMessage.Content, + triggerChannel); + var url = $"https://discord.com/channels/{proxy.Guild.Value}/{proxy.Channel}/{proxy.Mid}"; + await _rest.CreateMessage(logChannel.Id, new() {Content = url, Embed = embed}); + } + + private async Task GetAndCheckLogChannel(MessageContext ctx, Message trigger, PKMessage original = null) + { + var guildId = trigger.GuildId != null ? trigger.GuildId!.Value : original.Guild.Value; + var logChannelId = ctx.LogChannel; + var isBlacklisted = ctx.InLogBlacklist; + + if (original != null) + { + // we're editing a message, get log channel info from the database + var guild = await _db.Execute(c => _repo.GetGuild(c, original.Guild.Value)); + logChannelId = guild.LogChannel; + isBlacklisted = guild.Blacklist.Any(x => x == logChannelId); + } + + if (ctx.SystemId == null || logChannelId == null || isBlacklisted) return null; + + + // Find log channel and check if valid + var logChannel = await FindLogChannel(guildId, logChannelId.Value); + if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return null; + + // Check bot permissions + var perms = _bot.PermissionsIn(logChannel.Id); + if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks)) + { + _logger.Information( + "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", + ctx.LogChannel.Value, trigger.GuildId!.Value, perms); + return null; + } + + return logChannel; + } + private async Task FindLogChannel(ulong guildId, ulong channelId) { // TODO: fetch it directly on cache miss? From 239afd9b103105f9f6ad32a7b02a8f6fd3498cdf Mon Sep 17 00:00:00 2001 From: spiral Date: Fri, 7 May 2021 22:31:43 +0100 Subject: [PATCH 084/608] Fix error when trying to edit message in DM without link --- PluralKit.Bot/Commands/MessageEdit.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PluralKit.Bot/Commands/MessageEdit.cs b/PluralKit.Bot/Commands/MessageEdit.cs index c979bc59..3297eb85 100644 --- a/PluralKit.Bot/Commands/MessageEdit.cs +++ b/PluralKit.Bot/Commands/MessageEdit.cs @@ -67,6 +67,9 @@ namespace PluralKit.Bot return msg.Message; } + if (ctx.Guild == null) + throw new PKError("You must use a message link to edit messages in DMs."); + var recent = await FindRecentMessage(ctx); if (recent == null) throw new PKError("Could not find a recent message to edit."); From 0051ccf2fbf7bfd70ea44bb7b4d246b618dd7645 Mon Sep 17 00:00:00 2001 From: rladenson <78043712+rladenson@users.noreply.github.com> Date: Sat, 8 May 2021 01:10:32 -0600 Subject: [PATCH 085/608] Update user-guide.md It said that pk doesn't have some things that have since been implemented. --- docs/content/user-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index 313652f5..425a6ece 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -594,7 +594,7 @@ pk;import https://link/to/the/data/file.json ``` *(alternatively, run `pk;import` by itself and attach the .json file)* -Note that while Tupperbox supports features such as multiple proxies per member, per-member system tags, and member groups, PluralKit does not. +Note that while Tupperbox supports features such as per-member system tags PluralKit does not. Pluralkit also does not currently support exporting/importing member groups. PluralKit will warn you when you're importing a Tupperbox file that makes use of such features, as they will not carry over. ### Importing from PluralKit @@ -611,4 +611,4 @@ pk;import https://link/to/the/data/file.json *(alternatively, run `pk;import` by itself and attach the .json file)* ### Exporting your PluralKit data -To export all the data associated with your system, run the `pk;export` command. This will send you a JSON file containing your system, member, and switch information. \ No newline at end of file +To export all the data associated with your system, run the `pk;export` command. This will send you a JSON file containing your system, member, and switch information. From 828ddf8bce070a4c9e2f812bf3a238d1c7559733 Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 12 May 2021 23:54:03 +0100 Subject: [PATCH 086/608] Update faq.md --- docs/content/faq.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/content/faq.md b/docs/content/faq.md index a2d64bb8..0be71088 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -65,8 +65,9 @@ No. This is a limitation in Discord itself, and cannot be changed. The color com * The message with the avatar may have been deleted, or the link may have become invalid. Try re-setting your avatar. * Discord sometimes has issues displaying avatars. We can't do anything about that, sorry :( -### Why can't I use nitro emotes in some channels? -Webhooks inherit the nitro emoji permissions from the `@everyone` role, so the `@everyone` role must have the permission "Use External Emoji" to be able to use nitro emotes through PluralKit. +### Why can't I use nitro emoji in some channels? +Webhooks inherit nitro emoji permissions from the @everyone role, so @everyone must have the "Use External Emoji" permission to be able to use nitro emoji with PluralKit. +If it still doesn't work, make sure this permission isn't denied in channel overrides (found in channel settings -> permissions). ### Why can't I invite PluralKit to my server? @@ -79,3 +80,7 @@ This is a bug in Discord's mobile client. It handles formatting slightly differe ### Why is my time showing up incorrectly after Daylight savings time change? You probably set your timezone in PluralKit to a specific timezone, and PluralKit doesn't know how to calculate daylight savings time with that. Instead, set your timezone to a city (for example, "America/Toronto") - you can find your correct city identifier on + +### Why am I not able to edit a message via ID? or, Why is PluralKit editing the wrong message? +It is not possible to edit messages via ID. Please use the full link, or reply to the message. + From de0713277aee63349c70feab1841d4ffa8da1674 Mon Sep 17 00:00:00 2001 From: spiral Date: Thu, 20 May 2021 12:39:53 +0100 Subject: [PATCH 087/608] Add Catalogger to PluralKit-compatible bots --- docs/content/staff/compatibility.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/staff/compatibility.md b/docs/content/staff/compatibility.md index 2f715dd9..ddce0781 100644 --- a/docs/content/staff/compatibility.md +++ b/docs/content/staff/compatibility.md @@ -6,6 +6,7 @@ Because PluralKit deletes messages as part of proxying, this can often clutter u Some logger bots have offical PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages: - [**Gabby Gums**](https://github.com/amadea-system/GabbyGums) +- [**Catalogger**](https://catalogger.starshines.xyz/docs) If your server uses an in-house bot for logging, you can use [the API](../api-documentation.md) to implement support yourself. From 5a649d77515d10e7b81d0cbac98869f808c71298 Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Mon, 24 May 2021 21:05:27 +0200 Subject: [PATCH 088/608] Fix looking up private group icons --- PluralKit.Bot/Commands/Groups.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 9ffb1900..0f051eb1 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -218,6 +218,8 @@ namespace PluralKit.Bot async Task ShowIcon() { + if (!target.IconPrivacy.CanAccess(ctx.LookupContextFor(target.System))) + throw Errors.LookupNotAllowed; if ((target.Icon?.Trim() ?? "").Length > 0) { var eb = new EmbedBuilder() From 06c0e05396c1c96b17f173c817464c7a7e0571fe Mon Sep 17 00:00:00 2001 From: Spectralitree Date: Mon, 24 May 2021 21:18:57 +0200 Subject: [PATCH 089/608] fix looking up private group descriptions --- PluralKit.Bot/Commands/Groups.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 0f051eb1..d1c27ecb 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -153,6 +153,9 @@ namespace PluralKit.Bot } else if (!ctx.HasNext()) { + if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System))) + throw Errors.LookupNotAllowed; + if (target.Description == null) if (ctx.System?.Id == target.System) await ctx.Reply($"This group does not have a description set. To set one, type `pk;group {target.Reference()} description `."); From d7c059294730c510a3e25a43dcebd1779d9e368c Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 26 May 2021 22:27:52 +0200 Subject: [PATCH 090/608] Add preliminary support for buttons --- Myriad/Rest/Types/Requests/MessageRequest.cs | 1 + .../ApplicationCommandInteractionData.cs | 8 +- Myriad/Types/Application/Interaction.cs | 4 +- .../Types/Application/InteractionResponse.cs | 6 +- Myriad/Types/Message.cs | 1 + Myriad/Types/MessageComponent.cs | 29 ++++++ PluralKit.Bot/Bot.cs | 2 + PluralKit.Bot/Handlers/InteractionCreated.cs | 34 +++++++ PluralKit.Bot/Modules.cs | 2 + .../Services/InteractionDispatchService.cs | 92 +++++++++++++++++++ PluralKit.Bot/Utils/InteractionContext.cs | 45 +++++++++ 11 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 Myriad/Types/MessageComponent.cs create mode 100644 PluralKit.Bot/Handlers/InteractionCreated.cs create mode 100644 PluralKit.Bot/Services/InteractionDispatchService.cs create mode 100644 PluralKit.Bot/Utils/InteractionContext.cs diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs index 992eb08e..57f49e24 100644 --- a/Myriad/Rest/Types/Requests/MessageRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -9,5 +9,6 @@ namespace Myriad.Rest.Types.Requests public bool Tts { get; set; } public AllowedMentions? AllowedMentions { get; set; } public Embed? Embed { get; set; } + public MessageComponent[]? Components { get; set; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/ApplicationCommandInteractionData.cs b/Myriad/Types/Application/ApplicationCommandInteractionData.cs index 3c4543a3..e9401485 100644 --- a/Myriad/Types/Application/ApplicationCommandInteractionData.cs +++ b/Myriad/Types/Application/ApplicationCommandInteractionData.cs @@ -2,8 +2,10 @@ { public record ApplicationCommandInteractionData { - public ulong Id { get; init; } - public string Name { get; init; } - public ApplicationCommandInteractionDataOption[] Options { get; init; } + public ulong? Id { get; init; } + public string? Name { get; init; } + public ApplicationCommandInteractionDataOption[]? Options { get; init; } + public string? CustomId { get; init; } + public MessageComponent.ComponentType? ComponentType { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/Interaction.cs b/Myriad/Types/Application/Interaction.cs index cc269f3a..2b4834d8 100644 --- a/Myriad/Types/Application/Interaction.cs +++ b/Myriad/Types/Application/Interaction.cs @@ -5,7 +5,8 @@ public enum InteractionType { Ping = 1, - ApplicationCommand = 2 + ApplicationCommand = 2, + MessageComponent = 3 } public ulong Id { get; init; } @@ -15,5 +16,6 @@ public ulong ChannelId { get; init; } public GuildMember Member { get; init; } public string Token { get; init; } + public Message? Message { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/InteractionResponse.cs b/Myriad/Types/Application/InteractionResponse.cs index 12e1259d..9f07e3ec 100644 --- a/Myriad/Types/Application/InteractionResponse.cs +++ b/Myriad/Types/Application/InteractionResponse.cs @@ -5,10 +5,10 @@ public enum ResponseType { Pong = 1, - Acknowledge = 2, - ChannelMessage = 3, ChannelMessageWithSource = 4, - AckWithSource = 5 + DeferredChannelMessageWithSource = 5, + DeferredUpdateMessage = 6, + UpdateMessage = 7 } public ResponseType Type { get; init; } diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index a7cb88c6..1b7ec77d 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -64,6 +64,7 @@ namespace Myriad.Types [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional ReferencedMessage { get; init; } + public MessageComponent[]? Components { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); diff --git a/Myriad/Types/MessageComponent.cs b/Myriad/Types/MessageComponent.cs new file mode 100644 index 00000000..f68e50c9 --- /dev/null +++ b/Myriad/Types/MessageComponent.cs @@ -0,0 +1,29 @@ +namespace Myriad.Types +{ + public record MessageComponent + { + public ComponentType Type { get; init; } + public ButtonStyle? Style { get; init; } + public string? Label { get; init; } + public Emoji? Emoji { get; init; } + public string? CustomId { get; init; } + public string? Url { get; init; } + public bool? Disabled { get; init; } + public MessageComponent[]? Components { get; init; } + + public enum ComponentType + { + ActionRow = 1, + Button = 2 + } + + public enum ButtonStyle + { + Primary = 1, + Secondary = 2, + Success = 3, + Danger = 4, + Link = 5 + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index f9328af4..abc11458 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -109,6 +109,8 @@ namespace PluralKit.Bot await HandleEvent(shard, mdb); if (evt is MessageReactionAddEvent mra) await HandleEvent(shard, mra); + if (evt is InteractionCreateEvent ic) + await HandleEvent(shard, ic); // Update shard status for shards immediately on connect if (evt is ReadyEvent re) diff --git a/PluralKit.Bot/Handlers/InteractionCreated.cs b/PluralKit.Bot/Handlers/InteractionCreated.cs new file mode 100644 index 00000000..4ac75910 --- /dev/null +++ b/PluralKit.Bot/Handlers/InteractionCreated.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; + +using Autofac; + +using Myriad.Gateway; +using Myriad.Types; + +namespace PluralKit.Bot +{ + public class InteractionCreated: IEventHandler + { + private readonly InteractionDispatchService _interactionDispatch; + private readonly ILifetimeScope _services; + + public InteractionCreated(InteractionDispatchService interactionDispatch, ILifetimeScope services) + { + _interactionDispatch = interactionDispatch; + _services = services; + } + + public async Task Handle(Shard shard, InteractionCreateEvent evt) + { + if (evt.Type == Interaction.InteractionType.MessageComponent) + { + var customId = evt.Data?.CustomId; + if (customId != null) + { + var ctx = new InteractionContext(evt, _services); + await _interactionDispatch.Dispatch(customId, ctx); + } + } + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index f96a64a8..28bfac5f 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -72,6 +72,7 @@ namespace PluralKit.Bot builder.RegisterType().As>().As>(); builder.RegisterType().As>(); builder.RegisterType().As>(); + builder.RegisterType().As>(); // Event handler queue builder.RegisterType>().AsSelf().SingleInstance(); @@ -91,6 +92,7 @@ namespace PluralKit.Bot builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); // Sentry stuff builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); diff --git a/PluralKit.Bot/Services/InteractionDispatchService.cs b/PluralKit.Bot/Services/InteractionDispatchService.cs new file mode 100644 index 00000000..4579223f --- /dev/null +++ b/PluralKit.Bot/Services/InteractionDispatchService.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +using NodaTime; + +using Serilog; + +namespace PluralKit.Bot +{ + public class InteractionDispatchService: IDisposable + { + private static readonly Duration DefaultExpiry = Duration.FromMinutes(15); + + private readonly ConcurrentDictionary _handlers = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly IClock _clock; + private readonly ILogger _logger; + private readonly Task _cleanupWorker; + + public InteractionDispatchService(IClock clock, ILogger logger) + { + _clock = clock; + _logger = logger.ForContext(); + + _cleanupWorker = CleanupLoop(_cts.Token); + } + + public async ValueTask Dispatch(string customId, InteractionContext context) + { + if (!Guid.TryParse(customId, out var customIdGuid)) + return false; + + if (!_handlers.TryGetValue(customIdGuid, out var handler)) + return false; + + await handler.Callback.Invoke(context); + return true; + } + + public string Register(Func callback, Duration? expiry = null) + { + var key = Guid.NewGuid(); + var handler = new RegisteredInteraction + { + Callback = callback, + Expiry = _clock.GetCurrentInstant() + (expiry ?? DefaultExpiry) + }; + + _handlers[key] = handler; + return key.ToString(); + } + + private async Task CleanupLoop(CancellationToken ct) + { + while (true) + { + DoCleanup(); + await Task.Delay(TimeSpan.FromMinutes(1), ct); + } + } + + private void DoCleanup() + { + var now = _clock.GetCurrentInstant(); + var removedCount = 0; + foreach (var (key, value) in _handlers.ToArray()) + { + if (value.Expiry < now) + { + _handlers.TryRemove(key, out _); + removedCount++; + } + } + + _logger.Debug("Removed {ExpiredInteractions} expired interactions", removedCount); + } + + private struct RegisteredInteraction + { + public Instant Expiry; + public Func Callback; + } + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs new file mode 100644 index 00000000..70c616d7 --- /dev/null +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; + +using Autofac; + +using Myriad.Gateway; +using Myriad.Rest; +using Myriad.Types; + +namespace PluralKit.Bot +{ + public class InteractionContext + { + private readonly InteractionCreateEvent _evt; + private readonly ILifetimeScope _services; + + public InteractionContext(InteractionCreateEvent evt, ILifetimeScope services) + { + _evt = evt; + _services = services; + } + + public ulong ChannelId => _evt.ChannelId; + public ulong? MessageId => _evt.Message?.Id; + public GuildMember User => _evt.Member; + public string Token => _evt.Token; + public string? CustomId => _evt.Data?.CustomId; + public InteractionCreateEvent Event => _evt; + + public async Task Reply(string content) + { + await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource, + new InteractionApplicationCommandCallbackData + { + Content = content, + Flags = Message.MessageFlags.Ephemeral + }); + } + + public async Task Respond(InteractionResponse.ResponseType type, InteractionApplicationCommandCallbackData data) + { + var rest = _services.Resolve(); + await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data}); + } + } +} \ No newline at end of file From ba6beab245c573adbb882be87fb05a943085583f Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 26 May 2021 23:06:55 +0100 Subject: [PATCH 091/608] Add README for Myriad library --- Myriad/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Myriad/README.md diff --git a/Myriad/README.md b/Myriad/README.md new file mode 100644 index 00000000..9ddc72cf --- /dev/null +++ b/Myriad/README.md @@ -0,0 +1,5 @@ +## Myriad + +'Myriad' is a .NET library used to interact with the Discord API. It's primarily intended for use with PluralKit, but feel free to fork or submodule it! + +You can find a simple example bot using the Myriad library here: \ No newline at end of file From b894a9f86ea0f84112111e794336251a75eb1549 Mon Sep 17 00:00:00 2001 From: spiral Date: Wed, 26 May 2021 23:09:43 +0100 Subject: [PATCH 092/608] Set correct REST user-agent --- Myriad/Rest/DiscordApiClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 6ab105a5..1b4299ea 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net; using System.Threading.Tasks; @@ -13,7 +12,7 @@ namespace Myriad.Rest { public class DiscordApiClient { - private const string UserAgent = "Test Discord Library by @Ske#6201"; + private const string UserAgent = "DiscordBot (https://github.com/xSke/PluralKit/tree/main/Myriad/, vMyriad)"; private readonly BaseRestClient _client; public DiscordApiClient(string token, ILogger logger) From 4bd2d06b0bd5102ec5afe221da3d9db1dcb9e18b Mon Sep 17 00:00:00 2001 From: Ske Date: Sun, 30 May 2021 16:45:29 +0200 Subject: [PATCH 093/608] Add basic interactivity framework --- .../Types/Application/ApplicationCommand.cs | 4 +- .../ApplicationCommandInteractionData.cs | 2 +- ...teractionApplicationCommandCallbackData.cs | 7 +- Myriad/Types/Component/ButtonStyle.cs | 11 ++ Myriad/Types/Component/ComponentType.cs | 8 ++ .../Types/{ => Component}/MessageComponent.cs | 15 --- Myriad/Types/Message.cs | 2 - PluralKit.Bot/Interactive/BaseInteractive.cs | 125 ++++++++++++++++++ PluralKit.Bot/Interactive/Button.cs | 25 ++++ PluralKit.Bot/Interactive/YesNoPrompt.cs | 43 ++++++ .../Services/InteractionDispatchService.cs | 8 ++ PluralKit.Bot/Utils/InteractionContext.cs | 22 ++- 12 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 Myriad/Types/Component/ButtonStyle.cs create mode 100644 Myriad/Types/Component/ComponentType.cs rename Myriad/Types/{ => Component}/MessageComponent.cs (61%) create mode 100644 PluralKit.Bot/Interactive/BaseInteractive.cs create mode 100644 PluralKit.Bot/Interactive/Button.cs create mode 100644 PluralKit.Bot/Interactive/YesNoPrompt.cs diff --git a/Myriad/Types/Application/ApplicationCommand.cs b/Myriad/Types/Application/ApplicationCommand.cs index 92ecd856..53f88dd6 100644 --- a/Myriad/Types/Application/ApplicationCommand.cs +++ b/Myriad/Types/Application/ApplicationCommand.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Myriad.Types +namespace Myriad.Types { public record ApplicationCommand { diff --git a/Myriad/Types/Application/ApplicationCommandInteractionData.cs b/Myriad/Types/Application/ApplicationCommandInteractionData.cs index e9401485..768f9e85 100644 --- a/Myriad/Types/Application/ApplicationCommandInteractionData.cs +++ b/Myriad/Types/Application/ApplicationCommandInteractionData.cs @@ -6,6 +6,6 @@ public string? Name { get; init; } public ApplicationCommandInteractionDataOption[]? Options { get; init; } public string? CustomId { get; init; } - public MessageComponent.ComponentType? ComponentType { get; init; } + public ComponentType? ComponentType { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs b/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs index 2718aa0e..b9ac4372 100644 --- a/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs +++ b/Myriad/Types/Application/InteractionApplicationCommandCallbackData.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; - -using Myriad.Rest.Types; +using Myriad.Rest.Types; namespace Myriad.Types { public record InteractionApplicationCommandCallbackData { public bool? Tts { get; init; } - public string Content { get; init; } + public string? Content { get; init; } public Embed[]? Embeds { get; init; } public AllowedMentions? AllowedMentions { get; init; } public Message.MessageFlags Flags { get; init; } + public MessageComponent[]? Components { get; init; } } } \ No newline at end of file diff --git a/Myriad/Types/Component/ButtonStyle.cs b/Myriad/Types/Component/ButtonStyle.cs new file mode 100644 index 00000000..de88ee7e --- /dev/null +++ b/Myriad/Types/Component/ButtonStyle.cs @@ -0,0 +1,11 @@ +namespace Myriad.Types +{ + public enum ButtonStyle + { + Primary = 1, + Secondary = 2, + Success = 3, + Danger = 4, + Link = 5 + } +} \ No newline at end of file diff --git a/Myriad/Types/Component/ComponentType.cs b/Myriad/Types/Component/ComponentType.cs new file mode 100644 index 00000000..5641c08a --- /dev/null +++ b/Myriad/Types/Component/ComponentType.cs @@ -0,0 +1,8 @@ +namespace Myriad.Types +{ + public enum ComponentType + { + ActionRow = 1, + Button = 2 + } +} \ No newline at end of file diff --git a/Myriad/Types/MessageComponent.cs b/Myriad/Types/Component/MessageComponent.cs similarity index 61% rename from Myriad/Types/MessageComponent.cs rename to Myriad/Types/Component/MessageComponent.cs index f68e50c9..4beee8de 100644 --- a/Myriad/Types/MessageComponent.cs +++ b/Myriad/Types/Component/MessageComponent.cs @@ -10,20 +10,5 @@ namespace Myriad.Types public string? Url { get; init; } public bool? Disabled { get; init; } public MessageComponent[]? Components { get; init; } - - public enum ComponentType - { - ActionRow = 1, - Button = 2 - } - - public enum ButtonStyle - { - Primary = 1, - Secondary = 2, - Success = 3, - Danger = 4, - Link = 5 - } } } \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index 1b7ec77d..479fca4e 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Net.Mail; using System.Text.Json.Serialization; using Myriad.Utils; diff --git a/PluralKit.Bot/Interactive/BaseInteractive.cs b/PluralKit.Bot/Interactive/BaseInteractive.cs new file mode 100644 index 00000000..36cbf29f --- /dev/null +++ b/PluralKit.Bot/Interactive/BaseInteractive.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Autofac; + +using Myriad.Rest.Types.Requests; +using Myriad.Types; + +using NodaTime; + +namespace PluralKit.Bot.Interactive +{ + public abstract class BaseInteractive + { + private readonly Context _ctx; + private readonly List