From 2fc5f2a9d9b0848068496cc3de5271c637ea5f7b Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Wed, 27 Aug 2025 17:22:04 -0400 Subject: [PATCH 01/14] chore: add link to new status page to docs, dash --- dashboard/main.go | 4 ++++ dashboard/src/components/common/Navigation.svelte | 2 +- docs/content/.vuepress/config.js | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dashboard/main.go b/dashboard/main.go index e657c740..9a0bc993 100644 --- a/dashboard/main.go +++ b/dashboard/main.go @@ -64,6 +64,10 @@ func main() { createEmbed(rw, r) }) + r.Get("/status", func(rw http.ResponseWriter, r *http.Request) { + http.Redirect(rw, r, "https://status.pluralkit.me/", http.StatusMovedPermanently) + }) + http.ListenAndServe(":8080", r) } diff --git a/dashboard/src/components/common/Navigation.svelte b/dashboard/src/components/common/Navigation.svelte index daf9db1d..498d6470 100644 --- a/dashboard/src/components/common/Navigation.svelte +++ b/dashboard/src/components/common/Navigation.svelte @@ -53,7 +53,7 @@ Public - Bot status + Bot status diff --git a/docs/content/.vuepress/config.js b/docs/content/.vuepress/config.js index 5bdc945e..b66c56e0 100644 --- a/docs/content/.vuepress/config.js +++ b/docs/content/.vuepress/config.js @@ -18,7 +18,7 @@ module.exports = { }, themeConfig: { - repo: 'PluralKit/PluralKit', + repo: false, docsDir: 'docs/content/', docsBranch: 'main', editLinks: true, @@ -29,7 +29,8 @@ module.exports = { nav: [ { text: "Web dashboard", link: "https://dash.pluralkit.me" }, { text: "Support server", link: "https://discord.gg/PczBt78" }, - { text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904" } + { text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904" }, + { text: "Bot status", link: "https://status.pluralkit.me/" } ], sidebar: [ "/", From 1378379e1491fe93edcc3d18e6c8c8d8e7ae9dbb Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Wed, 27 Aug 2025 22:31:23 -0400 Subject: [PATCH 02/14] feat(gateway): add reconnect timestamp to shard state --- crates/gateway/src/discord/gateway.rs | 8 ++++++-- crates/gateway/src/discord/shard_state.rs | 3 ++- crates/gateway/src/main.rs | 11 ++++++++--- crates/libpk/src/state.rs | 2 ++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/gateway/src/discord/gateway.rs b/crates/gateway/src/discord/gateway.rs index 6d16bfd1..2f657138 100644 --- a/crates/gateway/src/discord/gateway.rs +++ b/crates/gateway/src/discord/gateway.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use tokio::sync::mpsc::Sender; use tracing::{error, info, warn}; use twilight_gateway::{ - create_iterator, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, + create_iterator, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, CloseFrame }; use twilight_model::gateway::{ payload::outgoing::update_presence::UpdatePresencePayload, @@ -116,7 +116,11 @@ pub async fn runner( let raw_event = match item { Ok(evt) => match evt { Message::Close(frame) => { + let mut state_event = ShardStateEvent::Closed; let close_code = if let Some(close) = frame { + if close == CloseFrame::RESUME { + state_event = ShardStateEvent::Reconnect; + } close.code.to_string() } else { "unknown".to_string() @@ -132,7 +136,7 @@ pub async fn runner( .increment(1); if let Err(error) = - tx_state.try_send((shard.id(), ShardStateEvent::Closed, None, None)) + tx_state.try_send((shard.id(), state_event, None, None)) { error!("failed to update shard state for socket closure: {error}"); } diff --git a/crates/gateway/src/discord/shard_state.rs b/crates/gateway/src/discord/shard_state.rs index c85e02c8..fe047b50 100644 --- a/crates/gateway/src/discord/shard_state.rs +++ b/crates/gateway/src/discord/shard_state.rs @@ -86,7 +86,7 @@ impl ShardStateManager { Ok(()) } - pub async fn socket_closed(&self, shard_id: u32) -> anyhow::Result<()> { + pub async fn socket_closed(&self, shard_id: u32, reconnect: bool) -> anyhow::Result<()> { gauge!("pluralkit_gateway_shard_up").decrement(1); let mut info = self @@ -97,6 +97,7 @@ impl ShardStateManager { info.shard_id = shard_id as i32; info.cluster_id = Some(cluster_config().node_id as i32); info.up = false; + info.last_reconnect = chrono::offset::Utc::now().timestamp() as i32; info.disconnection_count += 1; self.save_shard(shard_id, info).await?; diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index e61c3445..a4bf7d2a 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -109,8 +109,13 @@ async fn main() -> anyhow::Result<()> { }; } ShardStateEvent::Closed => { - if let Err(error) = shard_state.socket_closed(shard_id.number()).await { - error!("failed to update shard state for heartbeat: {error}") + if let Err(error) = shard_state.socket_closed(shard_id.number(), false).await { + error!("failed to update shard state for closed: {error}") + }; + } + ShardStateEvent::Reconnect => { + if let Err(error) = shard_state.socket_closed(shard_id.number(), true).await { + error!("failed to update shard state for reconnect: {error}") }; } ShardStateEvent::Other => { @@ -121,7 +126,7 @@ async fn main() -> anyhow::Result<()> { ) .await { - error!("failed to update shard state for heartbeat: {error}") + error!("failed to update shard state for other evt: {error}") }; } } diff --git a/crates/libpk/src/state.rs b/crates/libpk/src/state.rs index df44ea1d..3ea6342a 100644 --- a/crates/libpk/src/state.rs +++ b/crates/libpk/src/state.rs @@ -8,11 +8,13 @@ pub struct ShardState { /// unix timestamp pub last_heartbeat: i32, pub last_connection: i32, + pub last_reconnect: i32, pub cluster_id: Option, } pub enum ShardStateEvent { Closed, Heartbeat, + Reconnect, Other, } From 65b40c498b8ef05620080d05724a3ae2b38517fe Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Wed, 27 Aug 2025 22:34:19 -0400 Subject: [PATCH 03/14] chore(gateway): format --- crates/gateway/src/discord/gateway.rs | 6 ++---- crates/gateway/src/main.rs | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/gateway/src/discord/gateway.rs b/crates/gateway/src/discord/gateway.rs index 2f657138..8210e06e 100644 --- a/crates/gateway/src/discord/gateway.rs +++ b/crates/gateway/src/discord/gateway.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use tokio::sync::mpsc::Sender; use tracing::{error, info, warn}; use twilight_gateway::{ - create_iterator, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, CloseFrame + create_iterator, CloseFrame, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, }; use twilight_model::gateway::{ payload::outgoing::update_presence::UpdatePresencePayload, @@ -135,9 +135,7 @@ pub async fn runner( ) .increment(1); - if let Err(error) = - tx_state.try_send((shard.id(), state_event, None, None)) - { + if let Err(error) = tx_state.try_send((shard.id(), state_event, None, None)) { error!("failed to update shard state for socket closure: {error}"); } diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index a4bf7d2a..12db76b5 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -109,12 +109,15 @@ async fn main() -> anyhow::Result<()> { }; } ShardStateEvent::Closed => { - if let Err(error) = shard_state.socket_closed(shard_id.number(), false).await { + if let Err(error) = + shard_state.socket_closed(shard_id.number(), false).await + { error!("failed to update shard state for closed: {error}") }; } ShardStateEvent::Reconnect => { - if let Err(error) = shard_state.socket_closed(shard_id.number(), true).await { + if let Err(error) = shard_state.socket_closed(shard_id.number(), true).await + { error!("failed to update shard state for reconnect: {error}") }; } From 0cd351de4565030289b9b28212f831818ba126f0 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Sun, 31 Aug 2025 22:46:51 -0400 Subject: [PATCH 04/14] fix(gateway): add missing 'if' to shard state --- crates/gateway/src/discord/shard_state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/gateway/src/discord/shard_state.rs b/crates/gateway/src/discord/shard_state.rs index fe047b50..847b14c6 100644 --- a/crates/gateway/src/discord/shard_state.rs +++ b/crates/gateway/src/discord/shard_state.rs @@ -97,7 +97,9 @@ impl ShardStateManager { info.shard_id = shard_id as i32; info.cluster_id = Some(cluster_config().node_id as i32); info.up = false; - info.last_reconnect = chrono::offset::Utc::now().timestamp() as i32; + if reconnect { + info.last_reconnect = chrono::offset::Utc::now().timestamp() as i32 + } info.disconnection_count += 1; self.save_shard(shard_id, info).await?; From b3eb108a13f50e61fb4be654ed815008b554f385 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Mon, 1 Sep 2025 21:23:24 -0400 Subject: [PATCH 05/14] chore(bot): update wording on error message --- PluralKit.Bot/Services/ErrorMessageService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Services/ErrorMessageService.cs b/PluralKit.Bot/Services/ErrorMessageService.cs index 50f47d1c..efab9d02 100644 --- a/PluralKit.Bot/Services/ErrorMessageService.cs +++ b/PluralKit.Bot/Services/ErrorMessageService.cs @@ -116,7 +116,7 @@ public class ErrorMessageService return new EmbedBuilder() .Color(0xE74C3C) .Title("Internal error occurred") - .Description($"For support, please send the error code above as text in {channelInfo} with a description of what you were doing at the time.") + .Description($"**If you need support,** please send/forward the error code above **as text** in {channelInfo} with a description of what you were doing at the time.") .Footer(new Embed.EmbedFooter(errorId)) .Timestamp(now.ToDateTimeOffset().ToString("O")) .Build(); From 2248403140031ecd7fa3ed14e4826763e6558d28 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Mon, 1 Sep 2025 21:33:16 -0400 Subject: [PATCH 06/14] fix(flake): change systems url to default --- flake.lock | 10 +++++----- flake.nix | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 7298bd18..4024bfd9 100644 --- a/flake.lock +++ b/flake.lock @@ -297,16 +297,16 @@ }, "systems": { "locked": { - "lastModified": 1680978846, - "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", - "repo": "x86_64-linux", - "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", - "repo": "x86_64-linux", + "repo": "default", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 8fd2ed6b..85793415 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = "nixpkgs/nixpkgs-unstable"; parts.url = "github:hercules-ci/flake-parts"; - systems.url = "github:nix-systems/x86_64-linux"; + systems.url = "github:nix-systems/default"; # process compose process-compose.url = "github:Platonic-Systems/process-compose-flake"; services.url = "github:juspay/services-flake"; From 02b9783abe5ac46b83e40c085491b038453fd658 Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 11:30:46 +1200 Subject: [PATCH 07/14] feat(bot): initial ComponentsV2 implementation --- Myriad/Rest/Types/Requests/MessageRequest.cs | 1 + Myriad/Types/Component/ComponentMedia.cs | 13 + Myriad/Types/Component/ComponentType.cs | 9 +- Myriad/Types/Component/MessageComponent.cs | 6 + Myriad/Types/Message.cs | 3 +- PluralKit.Bot/ApplicationCommands/Message.cs | 3 +- .../CommandSystem/Context/Context.cs | 35 ++ PluralKit.Bot/Commands/Groups.cs | 2 +- PluralKit.Bot/Commands/Member.cs | 2 +- PluralKit.Bot/Commands/System.cs | 2 +- PluralKit.Bot/Handlers/InteractionCreated.cs | 3 +- PluralKit.Bot/Services/EmbedService.cs | 389 +++++++++++++++--- 12 files changed, 401 insertions(+), 67 deletions(-) create mode 100644 Myriad/Types/Component/ComponentMedia.cs 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); From 41478f0fef6c04cd1edaf3f36666ee2744258697 Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 12:22:22 +1200 Subject: [PATCH 08/14] fix(bot): more null/empty-string checks in CV2 cards --- PluralKit.Bot/Services/EmbedService.cs | 34 +++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index f991b554..d66a6b5a 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -126,7 +126,7 @@ public class EmbedService }); } - if (system.BannerPrivacy.CanAccess(ctx)) + if (system.BannerPrivacy.CanAccess(ctx) && !string.IsNullOrWhiteSpace(system.BannerImage)) descComponents.Add(new() { Type = ComponentType.MediaGallery, @@ -139,23 +139,25 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## [{systemName}](https://dash.pluralkit.me/profile/s/{system.Hid}){premiumText}", + Content = $"## [{systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}](https://dash.pluralkit.me/profile/s/{system.Hid}){premiumText}", }, - new MessageComponent() + ]; + + if (!string.IsNullOrWhiteSpace(headerText)) + header.Add(new MessageComponent() { Type = ComponentType.Text, Content = headerText, - }, - ]; + }); if (cctx.Guild != null) { var guildAvatar = guildSettings.AvatarUrl.TryGetCleanCdnUrl(); - if (guildAvatar != null) + if (!string.IsNullOrWhiteSpace(guildAvatar)) avatar = guildAvatar; } - if (avatar != null) + if (!string.IsNullOrWhiteSpace(avatar)) header = [ new MessageComponent() { @@ -304,14 +306,16 @@ public class EmbedService Type = ComponentType.Text, Content = $"## [{name}](https://dash.pluralkit.me/profile/m/{member.Hid}){(systemName != null ? $" ({systemName})" : "")}", }, - new MessageComponent() + ]; + + if (!string.IsNullOrWhiteSpace(headerText)) + header.Add(new MessageComponent() { Type = ComponentType.Text, Content = headerText, - }, - ]; + }); - if (avatar != null) + if (!string.IsNullOrWhiteSpace(avatar)) header = [ new MessageComponent() { @@ -476,12 +480,14 @@ public class EmbedService Type = ComponentType.Text, Content = $"## [{name}](https://dash.pluralkit.me/profile/g/{target.Hid}){(systemName != null ? $" ({systemName})" : "")}", }, - new MessageComponent() + ]; + + if (!string.IsNullOrWhiteSpace(headerText)) + header.Add(new MessageComponent() { Type = ComponentType.Text, Content = headerText, - }, - ]; + }); if (target.IconFor(pctx) is { } icon) header = [ From 0afcee108eb3d24957eae44d9e7fe9cc3d668b6f Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 14:26:29 +1200 Subject: [PATCH 09/14] feat(bot): allow querying legacy embeds with -se flag --- PluralKit.Bot/Commands/Groups.cs | 6 ++ PluralKit.Bot/Commands/Member.cs | 8 ++ PluralKit.Bot/Commands/System.cs | 5 ++ PluralKit.Bot/Services/EmbedService.cs | 100 +++++++++++++++++++++++++ 4 files changed, 119 insertions(+) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index c4661010..b18764d2 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -520,6 +520,12 @@ public class Groups public async Task ShowGroupCard(Context ctx, PKGroup target) { var system = await GetGroupSystem(ctx, target); + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target)); + return; + } + await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target)); } diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 43582a24..37ab9d18 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -122,6 +122,14 @@ public class Member public async Task ViewMember(Context ctx, PKMember target) { var system = await ctx.Repository.GetSystem(target.System); + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply( + text: EmbedService.LEGACY_EMBED_WARNING, + embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); + return; + } + await ctx.Reply( components: await _embeds.CreateMemberMessageComponents(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index 868fa154..2160dfd1 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -17,6 +17,11 @@ public class System public async Task Query(Context ctx, PKSystem system) { if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id))); + return; + } await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id))); } diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index d66a6b5a..16a9ef13 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -15,6 +15,8 @@ namespace PluralKit.Bot; public class EmbedService { + public const string LEGACY_EMBED_WARNING = "\u26A0\uFE0F The \"legacy\" embeds for system/member/group cards are deprecated, and will be removed in future."; + private readonly IDiscordCache _cache; private readonly IDatabase _db; private readonly ModelRepository _repo; @@ -186,6 +188,104 @@ public class EmbedService ]; } + public async Task CreateSystemEmbed(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 countctx = LookupContext.ByNonOwner; + if (cctx.MatchFlag("a", "all")) + { + if (system.Id == cctx.System.Id) + countctx = LookupContext.ByOwner; + else + throw Errors.LookupNotAllowed; + } + + 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 avatar = system.AvatarFor(ctx); + if (avatar != null) + eb.Thumbnail(new Embed.EmbedThumbnail(avatar)); + + if (system.BannerPrivacy.CanAccess(ctx)) + eb.Image(new Embed.EmbedImage(system.BannerImage)); + + var latestSwitch = await _repo.GetLatestSwitch(system.Id); + if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) + { + var switchMembers = + await _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id)).ToListAsync(); + if (switchMembers.Count > 0) + { + 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)); + } + } + + if (system.Tag != null) + eb.Field(new Embed.Field("Tag", system.Tag.EscapeMarkdown(), true)); + + 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) + { + 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(); + } + public Embed CreateLoggedMessageEmbed(Message triggerMessage, Message proxiedMessage, string systemHid, PKMember member, string channelName, string oldContent = null) { From e031a261a08f4027f38841b9128a5eb804eccf03 Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 14:27:27 +1200 Subject: [PATCH 10/14] fix(bot): add cv2 separator between proxy tags & groups --- PluralKit.Bot/Services/EmbedService.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 16a9ef13..142fe7ea 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -352,11 +352,18 @@ public class EmbedService List extraData = []; if (member.HasProxyTags && member.ProxyPrivacy.CanAccess(ctx)) + { + extraData.Add(new MessageComponent + { + Type = ComponentType.Separator, + }); + extraData.Add(new MessageComponent { Type = ComponentType.Text, Content = $"**Proxy tags:**\n{member.ProxyTagsString("\n").Truncate(1024)}", }); + } if (groups.Count > 0) { @@ -365,6 +372,11 @@ public class EmbedService ? 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.Separator, + }); + extraData.Add(new MessageComponent { Type = ComponentType.Text, @@ -372,12 +384,6 @@ public class EmbedService }); } - if (extraData.Count > 0) - extraData.Insert(0, new MessageComponent - { - Type = ComponentType.Separator, - }); - List descComponents = []; if (member.DescriptionFor(ctx) is { } desc) { From 364c71c76ca9071fc74d6c7f9d7f25d1cfb4132c Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 14:39:16 +1200 Subject: [PATCH 11/14] feat(bot): use button for cv2 cards dashboard link --- PluralKit.Bot/Services/EmbedService.cs | 67 +++++++++++++++++++++----- PluralKit.Core/CoreConfig.cs | 1 + 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 142fe7ea..d82d45cb 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -21,13 +21,15 @@ public class EmbedService private readonly IDatabase _db; private readonly ModelRepository _repo; private readonly DiscordApiClient _rest; + private readonly CoreConfig _coreConfig; - public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) + public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest, CoreConfig coreConfig) { _db = db; _repo = repo; _cache = cache; _rest = rest; + _coreConfig = coreConfig; } private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable ids) @@ -141,7 +143,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## [{systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}](https://dash.pluralkit.me/profile/s/{system.Hid}){premiumText}", + Content = $"## {systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}{premiumText}", }, ]; @@ -182,8 +184,21 @@ public class EmbedService }, new MessageComponent() { - Type = ComponentType.Text, - Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {system.Created.FormatZoned(cctx.Zone)}", + Type = ComponentType.Section, + Components = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {system.Created.FormatZoned(cctx.Zone)}", + }, + ], + Accessory = new MessageComponent() + { + Type = ComponentType.Button, + Style = ButtonStyle.Link, + Label = "View on dashboard", + Url = $"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}", + }, }, ]; } @@ -210,7 +225,7 @@ public class EmbedService .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}"); + .Url($"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}"); var avatar = system.AvatarFor(ctx); if (avatar != null) @@ -410,7 +425,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## [{name}](https://dash.pluralkit.me/profile/m/{member.Hid}){(systemName != null ? $" ({systemName})" : "")}", + Content = $"## {name}{(systemName != null ? $" ({systemName})" : "")}", }, ]; @@ -444,8 +459,21 @@ public class EmbedService }, 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)}" : "")}", + Type = ComponentType.Section, + Components = [ + 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)}" : "")}", + }, + ], + Accessory = new MessageComponent() + { + Type = ComponentType.Button, + Style = ButtonStyle.Link, + Label = "View on dashboard", + Url = $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}", + }, }, ]; } @@ -474,7 +502,7 @@ public class EmbedService .ToListAsync(); var eb = new EmbedBuilder() - .Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}")) + .Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}")) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null) .Color(member.Color?.ToDiscordColor()) .Footer(new Embed.EmbedFooter( @@ -584,7 +612,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## [{name}](https://dash.pluralkit.me/profile/g/{target.Hid}){(systemName != null ? $" ({systemName})" : "")}", + Content = $"## {name}{(systemName != null ? $" ({systemName})" : "")}", }, ]; @@ -618,8 +646,21 @@ public class EmbedService }, 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)}" : "")}", + Type = ComponentType.Section, + Components = [ + 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)}" : "")}", + }, + ], + Accessory = new MessageComponent() + { + Type = ComponentType.Button, + Style = ButtonStyle.Link, + Label = "View on dashboard", + Url = $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}", + }, }, ]; } @@ -649,7 +690,7 @@ public class EmbedService nameField = $"{nameField}"; var eb = new EmbedBuilder() - .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}")) + .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}")) .Color(target.Color?.ToDiscordColor()); eb.Footer(new Embed.EmbedFooter($"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}")); diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index 1e77271b..9792bc8e 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -16,6 +16,7 @@ public class CoreConfig public string? SeqLogUrl { get; set; } public string? DispatchProxyUrl { get; set; } public string? DispatchProxyToken { get; set; } + public string DashboardBaseUrl { get; set; } = "https://dash.pluralkit.local"; public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug; public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information; From 345d06709628b867cd1f1b8cee3f9df89e72ac53 Mon Sep 17 00:00:00 2001 From: Iris System Date: Mon, 25 Aug 2025 16:39:53 +1200 Subject: [PATCH 12/14] feat(bot): use CV2 cards in random commands --- PluralKit.Bot/Commands/Random.cs | 38 +++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 179d8fc5..4b0aa8a4 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -36,8 +36,17 @@ public class Random "This system has no members!"); var randInt = randGen.Next(members.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild, - ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); + + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply( + text: EmbedService.LEGACY_EMBED_WARNING, + embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); + return; + } + + await ctx.Reply( + components: await _embeds.CreateMemberMessageComponents(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); } public async Task Group(Context ctx, PKSystem target) @@ -60,7 +69,17 @@ public class Random $"This system has no groups!"); var randInt = randGen.Next(groups.Count()); - await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt])); + + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply( + text: EmbedService.LEGACY_EMBED_WARNING, + embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt])); + return; + } + + await ctx.Reply( + components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); } public async Task GroupMember(Context ctx, PKGroup group) @@ -92,7 +111,16 @@ public class Random system = await ctx.Repository.GetSystem(group.System); var randInt = randGen.Next(ms.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild, - ctx.Config, ctx.LookupContextFor(group.System), ctx.Zone)); + + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply( + text: EmbedService.LEGACY_EMBED_WARNING, + embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); + return; + } + + await ctx.Reply( + components: await _embeds.CreateMemberMessageComponents(system, ms[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } } \ No newline at end of file From a2417e5f06be8b693a3caeb2eccf53ce0c4a4373 Mon Sep 17 00:00:00 2001 From: Iris System Date: Tue, 26 Aug 2025 09:46:45 +1200 Subject: [PATCH 13/14] fix(bot): smaller header for cv2 cards --- PluralKit.Bot/Services/EmbedService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index d82d45cb..5a229ad6 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -143,7 +143,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## {systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}{premiumText}", + Content = $"### {systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}{premiumText}", }, ]; @@ -425,7 +425,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## {name}{(systemName != null ? $" ({systemName})" : "")}", + Content = $"### {name}{(systemName != null ? $" ({systemName})" : "")}", }, ]; @@ -612,7 +612,7 @@ public class EmbedService new MessageComponent() { Type = ComponentType.Text, - Content = $"## {name}{(systemName != null ? $" ({systemName})" : "")}", + Content = $"### {name}{(systemName != null ? $" ({systemName})" : "")}", }, ]; From e720883ec1566d8d5079c26ca5b070468d56e78e Mon Sep 17 00:00:00 2001 From: Iris System Date: Fri, 5 Sep 2025 14:35:22 +1200 Subject: [PATCH 14/14] feat(bot): add toggle for color codes on cv2 cards --- Cargo.toml | 2 +- PluralKit.Bot/CommandMeta/CommandTree.cs | 2 ++ PluralKit.Bot/Commands/Config.cs | 21 +++++++++++++++++++ PluralKit.Bot/Services/EmbedService.cs | 12 +++++++++-- .../Models/Patch/SystemConfigPatch.cs | 8 +++++++ PluralKit.Core/Models/SystemConfig.cs | 2 ++ crates/migrate/data/migrations/53.sql | 6 ++++++ 7 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 crates/migrate/data/migrations/53.sql diff --git a/Cargo.toml b/Cargo.toml index 270d00a6..444bcee6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ reqwest = { version = "0.12.7" , default-features = false, features = ["rustls-t sentry = { version = "0.36.0", default-features = false, features = ["backtrace", "contexts", "panic", "debug-images", "reqwest", "rustls"] } # replace native-tls with rustls serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.117" -sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "macros", "uuid"] } +sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "chrono", "macros", "uuid"] } tokio = { version = "1.36.0", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 8d111ef4..67379fe1 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -592,6 +592,8 @@ public partial class CommandTree return ctx.Execute(null, m => m.HidDisplayCaps(ctx)); if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids")) return ctx.Execute(null, m => m.HidListPadding(ctx)); + if (ctx.MatchMultiple(new[] { "show" }, new[] { "color", "colour", "colors", "colours" }) || ctx.Match("showcolor", "showcolour", "showcolors", "showcolours", "colorcode", "colorhex")) + return ctx.Execute(null, m => m.CardShowColorHex(ctx)); if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf")) return ctx.Execute(null, m => m.NameFormat(ctx)); if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit")) diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 1b8efab1..1feb7c37 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -123,6 +123,13 @@ public class Config "off" )); + items.Add(new( + "show color", + "Whether to show color codes in system/member/group cards", + EnabledDisabled(ctx.Config.CardShowColorHex), + "disabled" + )); + items.Add(new( "Proxy Switch", "Switching behavior when proxy tags are used", @@ -570,6 +577,20 @@ public class Config else throw new PKError(badInputError); } + public async Task CardShowColorHex(Context ctx) + { + if (!ctx.HasNext()) + { + var msg = $"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = newVal }); + await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(newVal)}."); + } + public async Task ProxySwitch(Context ctx) { if (!ctx.HasNext()) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 5a229ad6..39e85a37 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -75,12 +75,15 @@ public class EmbedService if (system.Tag != null) headerText += $"\n**Tag:** {system.Tag.EscapeMarkdown()}"; + if (cctx.Config.CardShowColorHex && !system.Color.EmptyOrNull()) + headerText += $"\n**Color:** #{system.Color}"; + if (cctx.Guild != null) { if (guildSettings.Tag != null && guildSettings.TagEnabled) - headerText += $"**Tag (in server '{cctx.Guild.Name}'):** {guildSettings.Tag.EscapeMarkdown()}"; + headerText += $"\n**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)*"; + headerText += $"\n**Tag (in server '{cctx.Guild.Name}'):** *(tag is disabled in this server)*"; } if (system.MemberListPrivacy.CanAccess(ctx)) @@ -358,6 +361,8 @@ public class EmbedService headerText += $"\n**Display name:** {member.DisplayName.Truncate(1024)}"; if (guild != null && guildDisplayName != null) headerText += $"\n**Server nickname (for '{guild.Name}'):** {guildDisplayName.Truncate(1024)}"; + if (ccfg.CardShowColorHex && !member.Color.EmptyOrNull()) + headerText += $"\n**Color:** #{member.Color}"; if (member.PronounsFor(ctx) is { } pronouns && !string.IsNullOrWhiteSpace(pronouns)) headerText += $"\n**Pronouns:** {pronouns}"; if (member.BirthdayFor(ctx) != null) @@ -577,6 +582,9 @@ public class EmbedService if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null) headerText += $"\n**Display name:** {target.DisplayName}"; + if (ctx.Config.CardShowColorHex && !target.Color.EmptyOrNull()) + headerText += $"\n**Color:** #{target.Color}"; + if (target.ListPrivacy.CanAccess(pctx)) { headerText += $"\n**Members:** {memberCount}"; diff --git a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs index d3d80428..c29cfae8 100644 --- a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs @@ -22,6 +22,7 @@ public class SystemConfigPatch: PatchObject public Partial ProxyErrorMessageEnabled { get; set; } public Partial HidDisplaySplit { get; set; } public Partial HidDisplayCaps { get; set; } + public Partial CardShowColorHex { get; set; } public Partial NameFormat { get; set; } public Partial HidListPadding { get; set; } public Partial ProxySwitch { get; set; } @@ -41,6 +42,7 @@ public class SystemConfigPatch: PatchObject .With("hid_display_split", HidDisplaySplit) .With("hid_display_caps", HidDisplayCaps) .With("hid_list_padding", HidListPadding) + .With("card_show_color_hex", CardShowColorHex) .With("proxy_switch", ProxySwitch) .With("name_format", NameFormat) ); @@ -107,6 +109,9 @@ public class SystemConfigPatch: PatchObject if (HidListPadding.IsPresent) o.Add("hid_list_padding", HidListPadding.Value.ToUserString()); + if (CardShowColorHex.IsPresent) + o.Add("card_show_color_hex", CardShowColorHex.Value); + if (ProxySwitch.IsPresent) o.Add("proxy_switch", ProxySwitch.Value.ToUserString()); @@ -150,6 +155,9 @@ public class SystemConfigPatch: PatchObject if (o.ContainsKey("hid_display_caps")) patch.HidDisplayCaps = o.Value("hid_display_caps"); + if (o.ContainsKey("card_show_color_hex")) + patch.CardShowColorHex = o.Value("card_show_color_hex"); + if (o.ContainsKey("proxy_switch")) patch.ProxySwitch = o.Value("proxy_switch") switch { diff --git a/PluralKit.Core/Models/SystemConfig.cs b/PluralKit.Core/Models/SystemConfig.cs index b4148211..dac7965f 100644 --- a/PluralKit.Core/Models/SystemConfig.cs +++ b/PluralKit.Core/Models/SystemConfig.cs @@ -23,6 +23,7 @@ public class SystemConfig public bool ProxyErrorMessageEnabled { get; } public bool HidDisplaySplit { get; } public bool HidDisplayCaps { get; } + public bool CardShowColorHex { get; } public HidPadFormat HidListPadding { get; } public ProxySwitchAction ProxySwitch { get; } public string NameFormat { get; } @@ -60,6 +61,7 @@ public static class SystemConfigExt o.Add("hid_display_split", cfg.HidDisplaySplit); o.Add("hid_display_caps", cfg.HidDisplayCaps); o.Add("hid_list_padding", cfg.HidListPadding.ToUserString()); + o.Add("card_show_color_hex", cfg.CardShowColorHex); o.Add("proxy_switch", cfg.ProxySwitch.ToUserString()); o.Add("name_format", cfg.NameFormat); diff --git a/crates/migrate/data/migrations/53.sql b/crates/migrate/data/migrations/53.sql new file mode 100644 index 00000000..3bfa3582 --- /dev/null +++ b/crates/migrate/data/migrations/53.sql @@ -0,0 +1,6 @@ +-- database version 53 +-- add toggle for showing color codes on cv2 cards + +alter table system_config add column card_show_color_hex bool default false; + +update info set schema_version = 53; \ No newline at end of file