diff --git a/Myriad/Types/Component/MessageComponent.cs b/Myriad/Types/Component/MessageComponent.cs index bc01bcbb..25153646 100644 --- a/Myriad/Types/Component/MessageComponent.cs +++ b/Myriad/Types/Component/MessageComponent.cs @@ -11,6 +11,7 @@ public record MessageComponent public string? Url { get; init; } public bool? Disabled { get; init; } public uint? AccentColor { get; init; } + public int? Spacing { get; init; } public ComponentMedia? Media { get; init; } public ComponentMediaItem[]? Items { get; init; } diff --git a/PluralKit.Bot/ApplicationCommands/Message.cs b/PluralKit.Bot/ApplicationCommands/Message.cs index 15144717..19c2ca31 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -43,11 +43,27 @@ public class ApplicationCommandProxiedMessage if (channel == null) showContent = false; - var embeds = new List(); + // var embeds = new List(); + // var guild = await _cache.GetGuild(ctx.GuildId); + // if (msg.Member != null) + // embeds.Add(await _embeds.CreateMemberEmbed( + // msg.System, + // msg.Member, + // guild, + // ctx.Config, + // LookupContext.ByNonOwner, + // DateTimeZone.Utc + // )); + + // embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config)); + + // await ctx.Reply(embeds: embeds.ToArray()); + + var components = new List(); var guild = await _cache.GetGuild(ctx.GuildId); if (msg.Member != null) - embeds.Add(await _embeds.CreateMemberEmbed( + components.AddRange(await _embeds.CreateMemberMessageComponents( msg.System, msg.Member, guild, @@ -56,9 +72,8 @@ public class ApplicationCommandProxiedMessage DateTimeZone.Utc )); - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config)); - - await ctx.Reply(embeds: embeds.ToArray()); + components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, showContent, ctx.Config)); + await ctx.Reply(components: components.ToArray()); } private async Task QueryCommandMessage(InteractionContext ctx) diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index ec34cea9..75c7b2c9 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -426,21 +426,82 @@ public class ProxiedMessage if (ctx.Match("author") || ctx.MatchFlag("author")) { var user = await _rest.GetUser(message.Message.Sender); - var eb = new EmbedBuilder() - .Author(new Embed.EmbedAuthor( - user != null - ? $"{user.Username}#{user.Discriminator}" - : $"Deleted user ${message.Message.Sender}", - IconUrl: user != null ? user.AvatarUrl() : null)) - .Description(message.Message.Sender.ToString()); + if (ctx.MatchFlag("show-embed", "se")) + { + var eb = new EmbedBuilder() + .Author(new Embed.EmbedAuthor( + user != null + ? $"{user.Username}#{user.Discriminator}" + : $"Deleted user ${message.Message.Sender}", + IconUrl: user != null ? user.AvatarUrl() : null)) + .Description(message.Message.Sender.ToString()); - await ctx.Reply( - user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*", - eb.Build()); + await ctx.Reply( + user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*", + eb.Build()); + return; + } + + + MessageComponent authorInfo; + var author = user != null + ? $"{user.Username}#{user.Discriminator}" + : $"Deleted user ${message.Message.Sender}"; + var avatarUrl = user?.AvatarUrl(); + var authorString = $"{author}\n**ID: **`{message.Message.Sender.ToString()}`"; + if (user != null && avatarUrl != "") + { + authorInfo = new MessageComponent() + { + Type = ComponentType.Section, + Components = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = authorString + } + ], + Accessory = new MessageComponent() + { + Type = ComponentType.Thumbnail, + Media = new ComponentMedia() + { + Url = avatarUrl + } + } + }; + } + else + { + authorInfo = new MessageComponent() + { + Type = ComponentType.Text, + Content = authorString + }; + } + MessageComponent container = new MessageComponent() + { + Type = ComponentType.Container, + Components = [ + authorInfo, + ] + }; + + await ctx.Reply(components: [new MessageComponent() + { + Type = ComponentType.Text, + Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*" + },container]); return; } - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config)); + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config)); + return; + } + + await ctx.Reply(components: await _embeds.CreateMessageInfoMessageComponents(message, showContent, ctx.Config)); } private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete) diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index c073119d..ac6c1e80 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -186,10 +186,25 @@ public class ReactionAdded: IEventHandler { var dm = await _dmCache.GetOrCreateDmChannel(evt.UserId); - var embeds = new List(); + // var embeds = new List(); + // if (msg.Member != null) + // embeds.Add(await _embeds.CreateMemberEmbed( + // msg.System, + // msg.Member, + // guild, + // config, + // LookupContext.ByNonOwner, + // DateTimeZone.Utc + // )); + + // embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config)); + + // await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() }); + + var components = new List(); if (msg.Member != null) - embeds.Add(await _embeds.CreateMemberEmbed( + components.AddRange(await _embeds.CreateMemberMessageComponents( msg.System, msg.Member, guild, @@ -198,9 +213,8 @@ public class ReactionAdded: IEventHandler DateTimeZone.Utc )); - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config)); - - await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() }); + components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, true, config)); + await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2 }); } catch (ForbiddenException) { } // No permissions to DM, can't check for this :( diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 7ed006e3..f3d7047c 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -766,6 +766,144 @@ public class EmbedService .Build(); } + public async Task CreateMessageInfoMessageComponents(FullMessage msg, bool showContent, SystemConfig? ccfg = null) + { + var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel); + var ctx = LookupContext.ByNonOwner; + + var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid); + + // 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) + // 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) + { + GuildMember member = null; + try + { + member = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender); + } + catch (ForbiddenException) + { + // no permission, couldn't fetch, oh well + } + + if (member != null) + // Don't do an extra request if we already have this info from the member lookup + userInfo = member.User; + memberInfo = member; + } + + if (userInfo == null) + userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender); + + // Calculate string displayed under "Sent by" + string userStr; + if (showContent && memberInfo != null && memberInfo.Nick != null) + userStr = $"**\n Username:** {userInfo.NameAndMention()}\n** Nickname:** {memberInfo.Nick}"; + else if (userInfo != null) userStr = userInfo.NameAndMention(); + else userStr = $"*(deleted user {msg.Message.Sender})*"; + + var content = serverMsg?.Content?.NormalizeLineEndSpacing(); + if (content == null || !showContent) + content = "*(message contents deleted or inaccessible)*"; + + var systemStr = msg.System == null + ? "*(deleted or unknown system)*" + : msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`"; + var memberStr = msg.Member == null + ? "*(deleted member)*" + : $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)"; + + var roles = memberInfo?.Roles?.ToList(); + var rolesContent = ""; + if (roles != null && roles.Count > 0 && showContent) + { + var guild = await _cache.GetGuild(channel.GuildId!.Value); + var rolesString = string.Join(", ", (roles + .Select(id => + { + var role = Array.Find(guild.Roles, r => r.Id == id); + if (role != null) + return role; + return new Role { Name = "*(unknown role)*", Position = 0 }; + })) + .OrderByDescending(role => role.Position) + .Select(role => role.Name)); + rolesContent = $"**Account Roles ({roles.Count})**\n{rolesString}"; + } + + MessageComponent authorData = new MessageComponent() + { + Type = ComponentType.Text, + Content = $"**System:** {systemStr}\n**Member:** {memberStr}\n**Sent by:** {userStr}\n\n{rolesContent}" + }; + + var avatarURL = msg.Member?.AvatarFor(ctx).TryGetCleanCdnUrl(); + MessageComponent header = avatarURL == "" ? authorData : new MessageComponent() + { + Type = ComponentType.Section, + Components = [authorData], + Accessory = new MessageComponent() + { + Type = ComponentType.Thumbnail, + Media = new ComponentMedia() + { + Url = avatarURL + } + } + }; + + List body = [ + new MessageComponent() + { + Type = ComponentType.Separator, + Spacing = 2 + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = content + } + ]; + + if (showContent) + { + var url = serverMsg?.Attachments?.FirstOrDefault()?.Url; + if (url != null && url != "") + body.Add(new MessageComponent() + { + Type = ComponentType.MediaGallery, + Items = [new ComponentMediaItem() + { + Media = new ComponentMedia(){ + Url = url + } + }] + }); + } + + MessageComponent footer = new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# Original Message ID: {msg.Message.OriginalMid} ยท " + }; + + return [ + new MessageComponent(){ + Type = ComponentType.Container, + Components = [ + header, + ..body + ] + }, + footer + ]; + } public async Task CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null) { var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel); diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 26fd099e..71d4cad9 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -39,6 +39,9 @@ public static class DiscordUtils public static Instant SnowflakeToInstant(ulong snowflake) => Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22); + public static ulong SnowflakeToTimestamp(ulong snowflake) => + ((ulong)Instant.FromUtc(2015, 1, 1, 0, 0, 0).ToUnixTimeMilliseconds() + (snowflake >> 22)) / 1000; + public static ulong InstantToSnowflake(Instant time) => (ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22; diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs index 444d39f4..acc23b14 100644 --- a/PluralKit.Bot/Utils/InteractionContext.cs +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -76,6 +76,16 @@ public class InteractionContext }); } + public async Task Reply(MessageComponent[] components = null) + { + await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource, + new InteractionApplicationCommandCallbackData + { + Components = components, + Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2 + }); + } + public async Task Defer() { await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource,