diff --git a/Myriad/Rest/Types/Requests/MessageRequest.cs b/Myriad/Rest/Types/Requests/MessageRequest.cs index d403f8ae..97b272b2 100644 --- a/Myriad/Rest/Types/Requests/MessageRequest.cs +++ b/Myriad/Rest/Types/Requests/MessageRequest.cs @@ -9,6 +9,7 @@ public record MessageRequest public bool Tts { get; set; } public AllowedMentions? AllowedMentions { get; set; } public Embed[]? Embeds { get; set; } + public Message.MessageFlags Flags { get; set; } public MessageComponent[]? Components { get; set; } public Message.Reference? MessageReference { get; set; } } \ No newline at end of file diff --git a/Myriad/Types/Component/ComponentMedia.cs b/Myriad/Types/Component/ComponentMedia.cs new file mode 100644 index 00000000..77a3b50e --- /dev/null +++ b/Myriad/Types/Component/ComponentMedia.cs @@ -0,0 +1,13 @@ +namespace Myriad.Types; + +public record ComponentMedia +{ + public string? Url { get; init; } +} + +public record ComponentMediaItem +{ + public ComponentMedia Media { get; init; } + public string? Description { get; init; } + public bool Spoiler { get; init; } = false; +} \ No newline at end of file diff --git a/Myriad/Types/Component/ComponentType.cs b/Myriad/Types/Component/ComponentType.cs index 0b10a756..61578dbb 100644 --- a/Myriad/Types/Component/ComponentType.cs +++ b/Myriad/Types/Component/ComponentType.cs @@ -3,5 +3,12 @@ namespace Myriad.Types; public enum ComponentType { ActionRow = 1, - Button = 2 + Button = 2, + StringSelect = 3, + Section = 9, + Text = 10, + Thumbnail = 11, + MediaGallery = 12, + Separator = 14, + Container = 17, } \ No newline at end of file diff --git a/Myriad/Types/Component/MessageComponent.cs b/Myriad/Types/Component/MessageComponent.cs index 9421fb89..bc01bcbb 100644 --- a/Myriad/Types/Component/MessageComponent.cs +++ b/Myriad/Types/Component/MessageComponent.cs @@ -5,9 +5,15 @@ public record MessageComponent public ComponentType Type { get; init; } public ButtonStyle? Style { get; set; } public string? Label { get; init; } + public string? Content { get; init; } public Emoji? Emoji { get; init; } public string? CustomId { get; init; } public string? Url { get; init; } public bool? Disabled { get; init; } + public uint? AccentColor { get; init; } + public ComponentMedia? Media { get; init; } + public ComponentMediaItem[]? Items { get; init; } + + public MessageComponent? Accessory { get; init; } public MessageComponent[]? Components { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index 1b8f723e..10416676 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -17,6 +17,7 @@ public record Message Ephemeral = 1 << 6, SuppressNotifications = 1 << 12, VoiceMessage = 1 << 13, + IsComponentsV2 = 1 << 15, } public enum MessageType @@ -73,8 +74,6 @@ public record Message public MessagePoll? Poll { get; init; } - // public MessageComponent[]? Components { get; init; } - public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); public record MessageActivity(int Type, string PartyId); diff --git a/PluralKit.Bot/ApplicationCommands/Message.cs b/PluralKit.Bot/ApplicationCommands/Message.cs index 2f7be692..15144717 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -139,7 +139,8 @@ public class ApplicationCommandProxiedMessage if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, ctx.ChannelId, member)).HasFlag(requiredPerms)) { throw new PKError("You do not have permission to send messages in this channel."); - }; + } + ; var config = await _repo.GetSystemConfig(msg.System.Id); diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 9bab740e..f155c8dc 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -119,6 +119,41 @@ public class Context return msg; } + public async Task Reply(MessageComponent[] components = null, AllowedMentions? mentions = null, MultipartFile[]? files = null) + { + var botPerms = await BotPermissions; + + if (!botPerms.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 (files != null && !botPerms.HasFlag(PermissionSet.AttachFiles)) + throw new PKError("PluralKit does not have permission to attach files in this channel. Please ensure I have the **Attach Files** permission enabled."); + + var msg = await Rest.CreateMessage(Channel.Id, new MessageRequest + { + Components = components, + Flags = Message.MessageFlags.IsComponentsV2, + + // Default to an empty allowed mentions object instead of null (which means no mentions allowed) + AllowedMentions = mentions ?? new AllowedMentions() + }, files: files); + + // store log of sent message, so it can be queried or deleted later + // skip DMs as DM messages can always be deleted + if (Guild != null) + await Repository.AddCommandMessage(new Core.CommandMessage + { + Mid = msg.Id, + Guild = Guild!.Id, + Channel = Channel.Id, + Sender = Author.Id, + OriginalMid = Message.Id, + }); + + return msg; + } + public async Task Execute(Command? commandDef, Func handler, bool deprecated = false) { _currentCommand = commandDef; diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 45c5f8b4..c4661010 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -520,7 +520,7 @@ public class Groups public async Task ShowGroupCard(Context ctx, PKGroup target) { var system = await GetGroupSystem(ctx, target); - await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, system, target)); + await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target)); } public async Task GroupPrivacy(Context ctx, PKGroup target, PrivacyLevel? newValueFromCommand) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 65931e13..43582a24 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -123,7 +123,7 @@ public class Member { var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply( - embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); + components: await _embeds.CreateMemberMessageComponents(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } public async Task Soulscream(Context ctx, PKMember target) diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index efeeb0f1..868fa154 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -18,7 +18,7 @@ public class System { if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id))); + await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id))); } public async Task New(Context ctx) diff --git a/PluralKit.Bot/Handlers/InteractionCreated.cs b/PluralKit.Bot/Handlers/InteractionCreated.cs index becd70d6..57f7090a 100644 --- a/PluralKit.Bot/Handlers/InteractionCreated.cs +++ b/PluralKit.Bot/Handlers/InteractionCreated.cs @@ -61,6 +61,7 @@ public class InteractionCreated: IEventHandler // got some unhandled command, log and ignore _logger.Warning(@"Unhandled ApplicationCommand interaction: {EventId} {CommandName}", evt.Id, evt.Data?.Name); break; - }; + } + ; } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 186e3a45..f991b554 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -39,11 +39,16 @@ public class EmbedService return Task.WhenAll(ids.Select(Inner)); } - public async Task CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx) + public async Task CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx) { // Fetch/render info for all accounts simultaneously var accounts = await _repo.GetSystemAccounts(system.Id); var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})"); + var linkedAccounts = new MessageComponent() + { + Type = ComponentType.Text, + Content = "**Linked accounts:**\n" + string.Join("\n", users).Truncate(1000), + }; var countctx = LookupContext.ByNonOwner; if (cctx.MatchFlag("a", "all")) @@ -55,21 +60,38 @@ public class EmbedService } var memberCount = await _repo.GetSystemMemberCount(system.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public); - - var eb = new EmbedBuilder() - .Title(system.NameFor(ctx)) - .Footer(new Embed.EmbedFooter( - $"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}")) - .Color(system.Color?.ToDiscordColor()) - .Url($"https://dash.pluralkit.me/profile/s/{system.Hid}"); + var guildSettings = cctx.Guild != null ? await _repo.GetSystemGuild(cctx.Guild.Id, system.Id) : null; var avatar = system.AvatarFor(ctx); - if (avatar != null) - eb.Thumbnail(new Embed.EmbedThumbnail(avatar)); + var headerText = ""; - if (system.BannerPrivacy.CanAccess(ctx)) - eb.Image(new Embed.EmbedImage(system.BannerImage)); + if (system.PronounPrivacy.CanAccess(ctx) && system.Pronouns != null) + headerText += $"\n**Pronouns:** {system.Pronouns}"; + if (system.Tag != null) + headerText += $"\n**Tag:** {system.Tag.EscapeMarkdown()}"; + + if (cctx.Guild != null) + { + if (guildSettings.Tag != null && guildSettings.TagEnabled) + headerText += $"**Tag (in server '{cctx.Guild.Name}'):** {guildSettings.Tag.EscapeMarkdown()}"; + if (!guildSettings.TagEnabled) + headerText += $"**Tag (in server '{cctx.Guild.Name}'):** *(tag is disabled in this server)*"; + } + + if (system.MemberListPrivacy.CanAccess(ctx)) + { + headerText += $"\n**Members:** {memberCount}"; + if (system.Id == cctx.System.Id) + if (memberCount > 0) + headerText += $" (see `{cctx.DefaultPrefix}system list`)"; + else + headerText += $" (add one with `{cctx.DefaultPrefix}member new`!)"; + else if (memberCount > 0) + headerText += $" (see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list`)"; + } + + List switchComponent = []; var latestSwitch = await _repo.GetLatestSwitch(system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) { @@ -79,62 +101,87 @@ public class EmbedService { var memberStr = string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))); if (memberStr.Length > 200) - memberStr = $"[too many to show, see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} fronters`]"; - eb.Field(new Embed.Field("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), memberStr)); + memberStr = $"(too many to show, see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} fronters`)"; + + switchComponent.Add(new() + { + Type = ComponentType.Text, + Content = $"**{"Current fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None)}:** {memberStr}", + }); } } - if (system.Tag != null) - eb.Field(new Embed.Field("Tag", system.Tag.EscapeMarkdown(), true)); + List descComponents = []; + if (system.DescriptionFor(ctx) is { } desc) + { + descComponents.Add(new() + { + Type = ComponentType.Separator, + }); + + descComponents.Add(new() + { + Type = ComponentType.Text, + Content = desc.NormalizeLineEndSpacing().Truncate(1024), + }); + } + + if (system.BannerPrivacy.CanAccess(ctx)) + descComponents.Add(new() + { + Type = ComponentType.MediaGallery, + Items = [new() { Media = new() { Url = system.BannerImage } }], + }); + + var systemName = (cctx.Guild != null && guildSettings?.DisplayName != null) ? guildSettings?.DisplayName! : system.NameFor(ctx); + var premiumText = ""; // TODO(iris): "\n\U0001F31F *PluralKit Premium supporter!*"; + List header = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"## [{systemName}](https://dash.pluralkit.me/profile/s/{system.Hid}){premiumText}", + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = headerText, + }, + ]; if (cctx.Guild != null) { - var guildSettings = await _repo.GetSystemGuild(cctx.Guild.Id, system.Id); - - if (guildSettings.Tag != null && guildSettings.TagEnabled) - eb.Field(new Embed.Field($"Tag (in server '{cctx.Guild.Name}')", guildSettings.Tag - .EscapeMarkdown(), true)); - - if (!guildSettings.TagEnabled) - eb.Field(new Embed.Field($"Tag (in server '{cctx.Guild.Name}')", - "*(tag is disabled in this server)*")); - - if (guildSettings.DisplayName != null) - eb.Title(guildSettings.DisplayName); - var guildAvatar = guildSettings.AvatarUrl.TryGetCleanCdnUrl(); if (guildAvatar != null) + avatar = guildAvatar; + } + + if (avatar != null) + header = [ + new MessageComponent() + { + Type = ComponentType.Section, + Components = [.. header], + Accessory = new MessageComponent() + { + Type = ComponentType.Thumbnail, + Media = new() { Url = avatar }, + }, + }, + ]; + + return [ + new MessageComponent() { - eb.Thumbnail(new Embed.EmbedThumbnail(guildAvatar)); - var sysDesc = "*(this system has a server-specific avatar set"; - if (avatar != null) - sysDesc += $"; [click here]({system.AvatarUrl.TryGetCleanCdnUrl()}) to see their global avatar)*"; - else - sysDesc += ")*"; - eb.Description(sysDesc); - } - } - - if (system.PronounPrivacy.CanAccess(ctx) && system.Pronouns != null) - eb.Field(new Embed.Field("Pronouns", system.Pronouns, true)); - - if (!system.Color.EmptyOrNull()) eb.Field(new Embed.Field("Color", $"#{system.Color}", true)); - - eb.Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000), true)); - - if (system.MemberListPrivacy.CanAccess(ctx)) - { - if (memberCount > 0) - eb.Field(new Embed.Field($"Members ({memberCount})", - $"(see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list` or `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list full`)", true)); - else - eb.Field(new Embed.Field($"Members ({memberCount})", $"Add one with `{cctx.DefaultPrefix}member new`!", true)); - } - - if (system.DescriptionFor(ctx) is { } desc) - eb.Field(new Embed.Field("Description", desc.NormalizeLineEndSpacing().Truncate(1024))); - - return eb.Build(); + Type = ComponentType.Container, + AccentColor = system.Color?.ToDiscordColor(), + Components = [ ..header, ..switchComponent, linkedAccounts, ..descComponents ], + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {system.Created.FormatZoned(cctx.Zone)}", + }, + ]; } public Embed CreateLoggedMessageEmbed(Message triggerMessage, Message proxiedMessage, string systemHid, @@ -164,6 +211,135 @@ public class EmbedService return embed.Build(); } + public async Task CreateMemberMessageComponents(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone) + { + var name = member.NameFor(ctx); + var systemGuildSettings = guild != null ? await _repo.GetSystemGuild(guild.Id, system.Id) : null; + var systemName = (guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx); + + var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null; + var guildDisplayName = guildSettings?.DisplayName; + var webhook_avatar = guildSettings?.AvatarUrl ?? member.WebhookAvatarFor(ctx) ?? member.AvatarFor(ctx); + var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx); + + var groups = await _repo.GetMemberGroups(member.Id) + .Where(g => g.Visibility.CanAccess(ctx)) + .OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase) + .ToListAsync(); + + var headerText = ""; + if (member.MemberVisibility == PrivacyLevel.Private) + headerText += "*(this member is hidden)*\n"; + if (guildSettings?.AvatarUrl != null) + if (member.AvatarFor(ctx) != null) + headerText += + $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n"; + else + headerText += "*(this member has a server-specific avatar set)*\n"; + + if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) + headerText += $"\n**Display name:** {member.DisplayName.Truncate(1024)}"; + if (guild != null && guildDisplayName != null) + headerText += $"\n**Server nickname (for '{guild.Name}'):** {guildDisplayName.Truncate(1024)}"; + if (member.PronounsFor(ctx) is { } pronouns && !string.IsNullOrWhiteSpace(pronouns)) + headerText += $"\n**Pronouns:** {pronouns}"; + if (member.BirthdayFor(ctx) != null) + headerText += $"\n**Birthday:** {member.BirthdayString}"; + if (member.MessageCountFor(ctx) is { } count && count > 0) + headerText += $"\n**Message count:** {member.MessageCount}"; + + List extraData = []; + if (member.HasProxyTags && member.ProxyPrivacy.CanAccess(ctx)) + extraData.Add(new MessageComponent + { + Type = ComponentType.Text, + Content = $"**Proxy tags:**\n{member.ProxyTagsString("\n").Truncate(1024)}", + }); + + if (groups.Count > 0) + { + // More than 5 groups show in "compact" format without ID + var content = groups.Count > 5 + ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name)) + : string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**")); + + extraData.Add(new MessageComponent + { + Type = ComponentType.Text, + Content = $"**Groups ({groups.Count}):**\n{content.Truncate(1000)}", + }); + } + + if (extraData.Count > 0) + extraData.Insert(0, new MessageComponent + { + Type = ComponentType.Separator, + }); + + List descComponents = []; + if (member.DescriptionFor(ctx) is { } desc) + { + descComponents.Add(new() + { + Type = ComponentType.Separator, + }); + + descComponents.Add(new() + { + Type = ComponentType.Text, + Content = desc.NormalizeLineEndSpacing().Truncate(1024), + }); + } + + if (member.BannerPrivacy.CanAccess(ctx) && !string.IsNullOrWhiteSpace(member.BannerImage)) + descComponents.Add(new() + { + Type = ComponentType.MediaGallery, + Items = [new() { Media = new() { Url = member.BannerImage } }], + }); + + List header = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"## [{name}](https://dash.pluralkit.me/profile/m/{member.Hid}){(systemName != null ? $" ({systemName})" : "")}", + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = headerText, + }, + ]; + + if (avatar != null) + header = [ + new MessageComponent() + { + Type = ComponentType.Section, + Components = [.. header], + Accessory = new MessageComponent() + { + Type = ComponentType.Thumbnail, + Media = new() { Url = avatar }, + }, + }, + ]; + + return [ + new MessageComponent() + { + Type = ComponentType.Container, + AccentColor = member.Color?.ToDiscordColor(), + Components = [ ..header, ..extraData, ..descComponents ], + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# System ID: `{system.DisplayHid(ccfg)}` \u2219 Member ID: `{member.DisplayHid(ccfg)}`{(member.MetadataPrivacy.CanAccess(ctx) ? $"\n-# Created: {member.Created.FormatZoned(zone)}" : "")}", + }, + ]; + } + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -241,6 +417,101 @@ public class EmbedService return eb.Build(); } + public async Task CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target) + { + var pctx = ctx.LookupContextFor(system.Id); + var name = target.NameFor(ctx); + var systemGuildSettings = ctx.Guild != null ? await _repo.GetSystemGuild(ctx.Guild.Id, system.Id) : null; + var systemName = (ctx.Guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx); + + var countctx = LookupContext.ByNonOwner; + if (ctx.MatchFlag("a", "all")) + { + if (system.Id == ctx.System.Id) + countctx = LookupContext.ByOwner; + else + throw Errors.LookupNotAllowed; + } + + var memberCount = await _repo.GetGroupMemberCount(target.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public); + var headerText = ""; + + if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null) + headerText += $"\n**Display name:** {target.DisplayName}"; + + if (target.ListPrivacy.CanAccess(pctx)) + { + headerText += $"\n**Members:** {memberCount}"; + if (system.Id == ctx.System.Id && memberCount == 0) + headerText += $" (add one with `{ctx.DefaultPrefix}group {target.Reference(ctx)} add `!)"; + else if (memberCount > 0) + headerText += $" (see `{ctx.DefaultPrefix}group {target.Reference(ctx)} list`)"; + } + + List descComponents = []; + if (target.DescriptionFor(pctx) is { } desc) + { + descComponents.Add(new() + { + Type = ComponentType.Separator, + }); + + descComponents.Add(new() + { + Type = ComponentType.Text, + Content = desc.NormalizeLineEndSpacing().Truncate(1024), + }); + } + + if (target.BannerPrivacy.CanAccess(pctx) && !string.IsNullOrWhiteSpace(target.BannerImage)) + descComponents.Add(new() + { + Type = ComponentType.MediaGallery, + Items = [new() { Media = new() { Url = target.BannerImage } }], + }); + + List header = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"## [{name}](https://dash.pluralkit.me/profile/g/{target.Hid}){(systemName != null ? $" ({systemName})" : "")}", + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = headerText, + }, + ]; + + if (target.IconFor(pctx) is { } icon) + header = [ + new MessageComponent() + { + Type = ComponentType.Section, + Components = [.. header], + Accessory = new MessageComponent() + { + Type = ComponentType.Thumbnail, + Media = new() { Url = icon.TryGetCleanCdnUrl() }, + }, + }, + ]; + + return [ + new MessageComponent() + { + Type = ComponentType.Container, + AccentColor = target.Color?.ToDiscordColor(), + Components = [ ..header, ..descComponents ], + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# System ID: `{system.DisplayHid(ctx.Config)}` \u2219 Group ID: `{target.DisplayHid(ctx.Config)}`{(target.MetadataPrivacy.CanAccess(pctx) ? $"\n-# Created: {target.Created.FormatZoned(ctx.Zone)}" : "")}", + }, + ]; + } + public async Task CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target) { var pctx = ctx.LookupContextFor(system.Id);