From 47b16dc51baaa9fc716b2e962e5a046a898bf563 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 24 Dec 2020 14:52:44 +0100 Subject: [PATCH] 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) {