From 05334f0d259583872fa5d8feaa1bd75aed264b9e Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 22 Dec 2020 16:55:13 +0100 Subject: [PATCH] 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)