diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs index 5686606e..d331e9e5 100644 --- a/Myriad/Extensions/CacheExtensions.cs +++ b/Myriad/Extensions/CacheExtensions.cs @@ -55,6 +55,17 @@ namespace Myriad.Extensions return restUser; } + public static async ValueTask GetOrFetchChannel(this IDiscordCache cache, DiscordApiClient rest, ulong channelId) + { + if (cache.TryGetChannel(channelId, out var cacheChannel)) + return cacheChannel; + + var restChannel = await rest.GetChannel(channelId); + if (restChannel != null) + await cache.SaveChannel(restChannel); + return restChannel; + } + public static async Task GetOrCreateDmChannel(this IDiscordCache cache, DiscordApiClient rest, ulong recipientId) { if (cache.TryGetDmChannel(recipientId, out var cacheChannel)) diff --git a/Myriad/Gateway/Events/MessageUpdateEvent.cs b/Myriad/Gateway/Events/MessageUpdateEvent.cs index 9e77d076..63b34c1d 100644 --- a/Myriad/Gateway/Events/MessageUpdateEvent.cs +++ b/Myriad/Gateway/Events/MessageUpdateEvent.cs @@ -1,7 +1,10 @@ -namespace Myriad.Gateway +using Myriad.Utils; + +namespace Myriad.Gateway { public record MessageUpdateEvent(ulong Id, ulong ChannelId): IGatewayEvent { + public Optional Content { get; init; } // TODO: lots of partials } } \ No newline at end of file diff --git a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs index b72bec2e..5f45bba0 100644 --- a/Myriad/Serialization/JsonSerializerOptionsExtensions.cs +++ b/Myriad/Serialization/JsonSerializerOptionsExtensions.cs @@ -13,6 +13,7 @@ namespace Myriad.Serialization opts.Converters.Add(new PermissionSetJsonConverter()); opts.Converters.Add(new ShardInfoJsonConverter()); + opts.Converters.Add(new OptionalConverterFactory()); return opts; } diff --git a/Myriad/Serialization/OptionalConverter.cs b/Myriad/Serialization/OptionalConverter.cs index c45d1caa..af7149f3 100644 --- a/Myriad/Serialization/OptionalConverter.cs +++ b/Myriad/Serialization/OptionalConverter.cs @@ -7,28 +7,33 @@ using Myriad.Utils; namespace Myriad.Serialization { - public class OptionalConverter: JsonConverter + public class OptionalConverterFactory: JsonConverterFactory { - public override IOptional? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public class Inner: JsonConverter> + { + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize(ref reader, options); + return new(inner!); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.HasValue ? value.GetValue() : default, typeof(T), options); + } + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var innerType = typeToConvert.GetGenericArguments()[0]; - var inner = JsonSerializer.Deserialize(ref reader, innerType, options); - - // TODO: rewrite to JsonConverterFactory to cut down on reflection - return (IOptional?) Activator.CreateInstance( - typeof(Optional<>).MakeGenericType(innerType), + return (JsonConverter?) Activator.CreateInstance( + typeof(Inner<>).MakeGenericType(innerType), BindingFlags.Instance | BindingFlags.Public, null, - new[] {inner}, + null, null); } - public override void Write(Utf8JsonWriter writer, IOptional value, JsonSerializerOptions options) - { - var innerType = value.GetType().GetGenericArguments()[0]; - JsonSerializer.Serialize(writer, value.GetValue(), innerType, options); - } - public override bool CanConvert(Type typeToConvert) { if (!typeToConvert.IsGenericType) diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index c74f67bf..977e1b9d 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Net.Mail; +using System.Text.Json.Serialization; + +using Myriad.Utils; namespace Myriad.Types { @@ -59,8 +62,8 @@ namespace Myriad.Types public Reference? MessageReference { get; set; } public MessageFlags Flags { get; init; } - // todo: null vs. absence - public Message? ReferencedMessage { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional ReferencedMessage { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); diff --git a/Myriad/Utils/Optional.cs b/Myriad/Utils/Optional.cs index 7b1e4139..881a53ce 100644 --- a/Myriad/Utils/Optional.cs +++ b/Myriad/Utils/Optional.cs @@ -1,16 +1,10 @@ -using System.Text.Json.Serialization; - -using Myriad.Serialization; - -namespace Myriad.Utils +namespace Myriad.Utils { public interface IOptional { - bool HasValue { get; } object? GetValue(); } - [JsonConverter(typeof(OptionalConverter))] public readonly struct Optional: IOptional { public Optional(T value) diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 037d8726..1ed55bb0 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -101,18 +101,6 @@ namespace PluralKit.Bot internal IDatabase Database => _db; internal ModelRepository Repository => _repo; - public Task Reply(string text, DiscordEmbed embed, - IEnumerable? mentions = null) - { - throw new NotImplementedException(); - } - - public Task Reply(DiscordEmbed embed, - IEnumerable? mentions = null) - { - throw new NotImplementedException(); - } - public async Task Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null) { if (!BotPermissions.HasFlag(PermissionSet.SendMessages)) diff --git a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs index 136b75d2..44779677 100644 --- a/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextEntityArgumentsExt.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; -using Myriad.Cache; using Myriad.Extensions; using Myriad.Types; diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 6d79f1bf..eb546e19 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Myriad.Rest.Exceptions; +using Myriad.Types; using Newtonsoft.Json; @@ -140,14 +141,14 @@ namespace PluralKit.Bot try { - var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); + var dm = await ctx.Rest.CreateDmAsync(ctx.AuthorNew.Id); + // TODO: send file var msg = await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); await dm.SendMessageAsync($"<{msg.Attachments[0].Url}>"); // If the original message wasn't posted in DMs, send a public reminder - // TODO: DMs - // if (!(ctx.Channel is DiscordDmChannel)) - // await ctx.Reply($"{Emojis.Success} Check your DMs!"); + if (ctx.ChannelNew.Type == Channel.ChannelType.Dm) + await ctx.Reply($"{Emojis.Success} Check your DMs!"); } catch (UnauthorizedException) { diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 59fce5cd..2a914090 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -73,7 +73,7 @@ namespace PluralKit.Bot public async Task ViewMember(Context ctx, PKMember target) { var system = await _db.Execute(c => _repo.GetSystem(c, target.System)); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.GuildNew, ctx.LookupContextFor(system))); } public async Task Soulscream(Context ctx, PKMember target) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 04b465ee..db4bc36e 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -228,7 +228,7 @@ namespace PluralKit.Bot { var message = await _db.Execute(c => _repo.GetMessage(c, messageId)); if (message == null) throw Errors.MessageNotFound(messageId); - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(ctx.Shard, message)); + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 6c154cbc..51770206 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -38,7 +38,7 @@ namespace PluralKit.Bot throw new PKError("Your system has no members! Please create at least one member before using this command."); var randInt = randGen.Next(members.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); } public async Task Group(Context ctx) @@ -73,7 +73,7 @@ namespace PluralKit.Bot var ms = members.ToList(); var randInt = randGen.Next(ms.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.GuildNew, ctx.LookupContextFor(ctx.System))); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageDeleted.cs b/PluralKit.Bot/Handlers/MessageDeleted.cs index 084ee861..9f1a607a 100644 --- a/PluralKit.Bot/Handlers/MessageDeleted.cs +++ b/PluralKit.Bot/Handlers/MessageDeleted.cs @@ -33,7 +33,6 @@ namespace PluralKit.Bot async Task Inner() { await Task.Delay(MessageDeleteDelay); - // TODO await _db.Execute(c => _repo.DeleteMessage(c, evt.Id)); } diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 9c20c3b0..36412bce 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -7,6 +7,7 @@ using Myriad.Gateway; using Myriad.Rest; using Myriad.Rest.Exceptions; using Myriad.Rest.Types; +using Myriad.Rest.Types.Requests; using Myriad.Types; using PluralKit.Core; @@ -22,10 +23,11 @@ namespace PluralKit.Bot private readonly CommandMessageService _commandMessageService; private readonly ILogger _logger; private readonly IDiscordCache _cache; + private readonly EmbedService _embeds; private readonly Bot _bot; private readonly DiscordApiClient _rest; - public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest) + public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest, EmbedService embeds) { _db = db; _repo = repo; @@ -33,6 +35,7 @@ namespace PluralKit.Bot _cache = cache; _bot = bot; _rest = rest; + _embeds = embeds; _logger = logger.ForContext(); } @@ -151,13 +154,22 @@ namespace PluralKit.Bot private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg) { + var guild = _cache.GetGuild(evt.GuildId!.Value); + // Try to DM the user info about the message // var member = await evt.Guild.GetMember(evt.User.Id); try { - // TODO: how to DM? - // await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner)); - // await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg)); + var dm = await _cache.GetOrCreateDmChannel(_rest, evt.UserId); + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Embed = await _embeds.CreateMemberEmbed(msg.System, msg.Member, guild, LookupContext.ByNonOwner) + }); + + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Embed = await _embeds.CreateMessageInfoEmbed(msg) + }); } catch (UnauthorizedException) { } // No permissions to DM, can't check for this :( @@ -192,9 +204,12 @@ namespace PluralKit.Bot // If not, tell them in DMs (if we can) try { - // todo: how to dm - // await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"); - // await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode()); + var dm = await _cache.GetOrCreateDmChannel(_rest, evt.UserId); + await _rest.CreateMessage(dm.Id, new MessageRequest + { + Content = $"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:" + }); + await _rest.CreateMessage(dm.Id, new MessageRequest {Content = $"<@{msg.Message.Sender}>".AsCode()}); } catch (UnauthorizedException) { } } diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index da8b7d10..5b33c70b 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using DSharpPlus; using DSharpPlus.Entities; using Humanizer; using Myriad.Builders; using Myriad.Cache; +using Myriad.Extensions; using Myriad.Rest; using Myriad.Types; @@ -22,13 +22,11 @@ namespace PluralKit.Bot { { private readonly IDatabase _db; private readonly ModelRepository _repo; - private readonly DiscordShardedClient _client; private readonly IDiscordCache _cache; private readonly DiscordApiClient _rest; - public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) + public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) { - _client = client; _db = db; _repo = repo; _cache = cache; @@ -39,14 +37,7 @@ namespace PluralKit.Bot { { async Task<(ulong Id, User? User)> Inner(ulong id) { - if (_cache.TryGetUser(id, out var cachedUser)) - return (id, cachedUser); - - var user = await _rest.GetUser(id); - if (user == null) - return (id, null); - // todo: move to "GetUserCached" helper - await _cache.SaveUser(user); + var user = await _cache.GetOrFetchUser(_rest, id); return (id, user); } @@ -108,7 +99,7 @@ namespace PluralKit.Bot { .Build(); } - public async Task CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -233,26 +224,33 @@ namespace PluralKit.Bot { .Build(); } - public async Task CreateMessageInfoEmbed(DiscordClient client, FullMessage msg) + public async Task CreateMessageInfoEmbed(FullMessage msg) { + var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel); var ctx = LookupContext.ByNonOwner; - var channel = await _client.GetChannel(msg.Message.Channel); - var serverMsg = channel != null ? await channel.GetMessage(msg.Message.Mid) : null; + var serverMsg = channel != null ? await _rest.GetMessage(msg.Message.Channel, msg.Message.Mid) : null; // Need this whole dance to handle cases where: // - the user is deleted (userInfo == null) // - the bot's no longer in the server we're querying (channel == null) // - the member is no longer in the server we're querying (memberInfo == null) - DiscordMember memberInfo = null; - DiscordUser userInfo = null; - if (channel != null) memberInfo = await channel.Guild.GetMember(msg.Message.Sender); - if (memberInfo != null) userInfo = memberInfo; // Don't do an extra request if we already have this info from the member lookup - else userInfo = await client.GetUser(msg.Message.Sender); + // TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s? + GuildMemberPartial memberInfo = null; + User userInfo = null; + if (channel != null) + { + var m = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender); + if (m != null) + // Don't do an extra request if we already have this info from the member lookup + userInfo = m.User; + memberInfo = m; + } + else userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender); // Calculate string displayed under "Sent by" string userStr; - if (memberInfo != null && memberInfo.Nickname != null) - userStr = $"**Username:** {memberInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nickname}"; + if (memberInfo != null && memberInfo.Nick != null) + userStr = $"**Username:** {userInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nick}"; else if (userInfo != null) userStr = userInfo.NameAndMention(); else userStr = $"*(deleted user {msg.Message.Sender})*"; @@ -270,7 +268,8 @@ namespace PluralKit.Bot { var roles = memberInfo?.Roles?.ToList(); if (roles != null && roles.Count > 0) { - var rolesString = string.Join(", ", roles.Select(role => role.Name)); + // TODO: what if role isn't in cache? figure out a fallback + var rolesString = string.Join(", ", roles.Select(id => _cache.GetRole(id).Name)); eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024))); }