From 83f2d33c3d6a10f1b008f25bc3083af4980b8b32 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Fri, 24 Oct 2025 10:23:38 -0400 Subject: [PATCH 1/9] feat(bot): port message info embeds to cv2 --- Myriad/Types/Component/MessageComponent.cs | 1 + PluralKit.Bot/ApplicationCommands/Message.cs | 21 +- PluralKit.Bot/Commands/Message.cs | 41 ++- PluralKit.Bot/Handlers/ReactionAdded.cs | 15 +- PluralKit.Bot/Services/EmbedService.cs | 252 +++++++++++++++++++ PluralKit.Bot/Utils/DiscordUtils.cs | 3 + PluralKit.Bot/Utils/InteractionContext.cs | 10 + 7 files changed, 312 insertions(+), 31 deletions(-) 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..34d9bf1b 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -43,11 +43,10 @@ public class ApplicationCommandProxiedMessage if (channel == null) showContent = false; - var embeds = new List(); - + 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, @@ -55,10 +54,12 @@ public class ApplicationCommandProxiedMessage LookupContext.ByNonOwner, DateTimeZone.Utc )); - - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config)); - - await ctx.Reply(embeds: embeds.ToArray()); + components.Add(new MessageComponent() + { + Type = ComponentType.Separator + }); + components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, showContent, ctx.Config)); + await ctx.Reply(components: components.ToArray()); } private async Task QueryCommandMessage(InteractionContext ctx) @@ -68,11 +69,7 @@ public class ApplicationCommandProxiedMessage if (msg == null) throw Errors.MessageNotFound(messageId); - var embeds = new List(); - - embeds.Add(await _embeds.CreateCommandMessageInfoEmbed(msg, true)); - - await ctx.Reply(embeds: embeds.ToArray()); + await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, true)); } public async Task DeleteMessage(InteractionContext ctx) diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index ec34cea9..c61dd88b 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -426,21 +426,33 @@ 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; + } + + await ctx.Reply(components: await _embeds.CreateAuthorMessageComponents(user, message)); 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) @@ -472,6 +484,11 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent)); + if (ctx.MatchFlag("show-embed", "se")) + { + await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent)); + return; + } + await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, showContent)); } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index c073119d..a517a69b 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -186,10 +186,9 @@ public class ReactionAdded: IEventHandler { var dm = await _dmCache.GetOrCreateDmChannel(evt.UserId); - var embeds = new List(); - + var components = new List(); if (msg.Member != null) - embeds.Add(await _embeds.CreateMemberEmbed( + components.AddRange(await _embeds.CreateMemberMessageComponents( msg.System, msg.Member, guild, @@ -197,10 +196,12 @@ public class ReactionAdded: IEventHandler LookupContext.ByNonOwner, DateTimeZone.Utc )); - - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config)); - - await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() }); + components.Add(new MessageComponent() + { + Type = ComponentType.Separator + }); + 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 c8c07996..8a3d1fd0 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -766,6 +766,158 @@ 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 + } + ]; + if (content != "") + { + body.Add(new MessageComponent() + { + Type = ComponentType.Text, + Content = content + }); + } + + if (showContent) + { + if (serverMsg != null) + { + var media = new List(); + foreach (Message.Attachment attachment in serverMsg?.Attachments) + { + var url = attachment.Url; + if (url != null && url != "") + media.Add(new ComponentMediaItem() + { + Media = new ComponentMedia() + { + Url = url + } + }); + } + if (media.Count > 0) + body.Add(new MessageComponent() + { + Type = ComponentType.MediaGallery, + Items = media.ToArray() + }); + } + } + + 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); @@ -852,6 +1004,106 @@ public class EmbedService return eb.Build(); } + public async Task CreateAuthorMessageComponents(User? user, FullMessage msg) + { + MessageComponent authorInfo; + var author = user != null + ? $"{user.Username}#{user.Discriminator}" + : $"Deleted user ${msg.Message.Sender}"; + var avatarUrl = user?.AvatarUrl(); + var authorString = $"{author}\n**ID: **`{msg.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, + ] + }; + return ( + [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {msg.Message.Sender})*" + }, + container + ] + ); + } + + public async Task CreateCommandMessageInfoMessageComponents(Core.CommandMessage msg, bool showContent) + { + var content = "*(command message deleted or inaccessible)*"; + if (showContent) + { + var discordMessage = await _rest.GetMessageOrNull(msg.Channel, msg.OriginalMid); + if (discordMessage != null) + content = discordMessage.Content; + } + + List body = [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"### Command response message\n**Original message:** https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}\n**Sent By:** <@{msg.Sender}>" + }, + new MessageComponent() + { + Type = ComponentType.Separator, + }, + new MessageComponent() + { + Type = ComponentType.Text, + Content = content + }, + ]; + + MessageComponent footer = new MessageComponent() + { + Type = ComponentType.Text, + Content = $"-# Original Message ID: {msg.OriginalMid} · " + }; + + return [ + new MessageComponent(){ + Type = ComponentType.Container, + Components = [ + ..body + ] + }, + footer + ]; + } public async Task CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent) { var content = "*(command message deleted or inaccessible)*"; 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, From 49ce00e67563fdd325590a5dea3c9290a7a7bca5 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Fri, 24 Oct 2025 19:55:53 -0400 Subject: [PATCH 2/9] fix(bot): check for null avatar in msg info --- PluralKit.Bot/Services/EmbedService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 8a3d1fd0..f33d56f8 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -844,7 +844,7 @@ public class EmbedService }; var avatarURL = msg.Member?.AvatarFor(ctx).TryGetCleanCdnUrl(); - MessageComponent header = avatarURL == "" ? authorData : new MessageComponent() + MessageComponent header = (avatarURL == "" || avatarURL == null) ? authorData : new MessageComponent() { Type = ComponentType.Section, Components = [authorData], From 098317924047092c2028233a007aa702e638a7d9 Mon Sep 17 00:00:00 2001 From: asleepyskye Date: Fri, 24 Oct 2025 21:26:33 -0400 Subject: [PATCH 3/9] fix(bot): add allowed mentions to msg info replies --- PluralKit.Bot/Handlers/ReactionAdded.cs | 2 +- PluralKit.Bot/Utils/InteractionContext.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index a517a69b..ef4ab4f9 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -201,7 +201,7 @@ public class ReactionAdded: IEventHandler Type = ComponentType.Separator }); components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, true, config)); - await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2 }); + await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2, AllowedMentions = new AllowedMentions() }); } catch (ForbiddenException) { } // No permissions to DM, can't check for this :( diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs index acc23b14..ddecadf3 100644 --- a/PluralKit.Bot/Utils/InteractionContext.cs +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -5,6 +5,7 @@ using Autofac; using Myriad.Cache; using Myriad.Gateway; using Myriad.Rest; +using Myriad.Rest.Types; using Myriad.Types; using PluralKit.Core; @@ -76,13 +77,14 @@ public class InteractionContext }); } - public async Task Reply(MessageComponent[] components = null) + public async Task Reply(MessageComponent[] components = null, AllowedMentions? mentions = null) { await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource, new InteractionApplicationCommandCallbackData { Components = components, - Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2 + Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2, + AllowedMentions = mentions ?? new AllowedMentions() }); } From c0a5bc81a02b1ed041a85256234da3c53ec6834d Mon Sep 17 00:00:00 2001 From: alyssa Date: Tue, 30 Sep 2025 03:30:43 +0000 Subject: [PATCH 4/9] feat(api): improve logging --- crates/api/src/main.rs | 5 +--- crates/api/src/middleware/auth.rs | 7 ++++- crates/api/src/middleware/logger.rs | 42 +++++++++++++++++++---------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index f22450ce..5c2bcfd4 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -127,13 +127,10 @@ fn router(ctx: ApiContext) -> Router { .route("/v2/groups/{group_id}/oembed.json", get(rproxy)) .layer(middleware::ratelimit::ratelimiter(middleware::ratelimit::do_request_ratelimited)) // this sucks - .layer(axum::middleware::from_fn(middleware::ignore_invalid_routes::ignore_invalid_routes)) - .layer(axum::middleware::from_fn(middleware::logger::logger)) - .layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::params::params)) .layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::auth::auth)) - + .layer(axum::middleware::from_fn(middleware::logger::logger)) .layer(axum::middleware::from_fn(middleware::cors::cors)) .layer(tower_http::catch_panic::CatchPanicLayer::custom(util::handle_panic)) diff --git a/crates/api/src/middleware/auth.rs b/crates/api/src/middleware/auth.rs index 1d536e97..8487e932 100644 --- a/crates/api/src/middleware/auth.rs +++ b/crates/api/src/middleware/auth.rs @@ -76,5 +76,10 @@ pub async fn auth(State(ctx): State, mut req: Request, next: Next) - req.extensions_mut() .insert(AuthState::new(authed_system_id, authed_app_id, internal)); - next.run(req).await + let mut res = next.run(req).await; + + res.extensions_mut() + .insert(AuthState::new(authed_system_id, authed_app_id, internal)); + + res } diff --git a/crates/api/src/middleware/logger.rs b/crates/api/src/middleware/logger.rs index 512234bb..5e0ba782 100644 --- a/crates/api/src/middleware/logger.rs +++ b/crates/api/src/middleware/logger.rs @@ -12,9 +12,10 @@ const MIN_LOG_TIME: u128 = 2_000; pub async fn logger(request: Request, next: Next) -> Response { let method = request.method().clone(); + let headers = request.headers().clone(); - let remote_ip = header_or_unknown(request.headers().get("X-PluralKit-Client-IP")); - let user_agent = header_or_unknown(request.headers().get("User-Agent")); + let remote_ip = header_or_unknown(headers.get("X-PluralKit-Client-IP")); + let user_agent = header_or_unknown(headers.get("User-Agent")); let extensions = request.extensions().clone(); @@ -24,10 +25,6 @@ pub async fn logger(request: Request, next: Next) -> Response { .map(|v| v.as_str().to_string()) .unwrap_or("unknown".to_string()); - let auth = extensions - .get::() - .expect("should always have AuthState"); - let uri = request.uri().clone(); let request_span = span!( @@ -43,15 +40,24 @@ pub async fn logger(request: Request, next: Next) -> Response { let response = next.run(request).instrument(request_span).await; let elapsed = start.elapsed().as_millis(); - let system_id = auth - .system_id() - .map(|v| v.to_string()) - .unwrap_or("none".to_string()); + let rext = response.extensions().clone(); + let auth = rext.get::(); - let app_id = auth - .app_id() - .map(|v| v.to_string()) - .unwrap_or("none".to_string()); + let system_id = if let Some(auth) = auth { + auth.system_id() + .map(|v| v.to_string()) + .unwrap_or("none".to_string()) + } else { + "none".to_string() + }; + + let app_id = if let Some(auth) = auth { + auth.app_id() + .map(|v| v.to_string()) + .unwrap_or("none".to_string()) + } else { + "none".to_string() + }; counter!( "pluralkit_api_requests", @@ -73,6 +79,14 @@ pub async fn logger(request: Request, next: Next) -> Response { .record(elapsed as f64 / 1_000_f64); info!( + status = response.status().as_str(), + method = method.to_string(), + endpoint, + elapsed, + user_agent, + remote_ip, + system_id, + app_id, "{} handled request for {} {} in {}ms", response.status(), method, From 83dd880374b15365f7f94fa7c04312d60d201f84 Mon Sep 17 00:00:00 2001 From: alyssa Date: Wed, 15 Oct 2025 21:15:54 +0000 Subject: [PATCH 5/9] chore: move app-commands script to rust --- Cargo.lock | 15 ++++++ Cargo.toml | 2 +- crates/app-commands/Cargo.toml | 14 +++++ crates/app-commands/src/main.rs | 41 +++++++++++++++ scripts/app-commands/.gitignore | 4 -- scripts/app-commands/README.md | 23 -------- scripts/app-commands/commands.py | 10 ---- scripts/app-commands/common/__init__.py | 1 - scripts/app-commands/common/types.py | 7 --- scripts/app-commands/update.py | 70 ------------------------- scripts/dump-db.sh | 3 -- scripts/rclone-db.sh | 15 ------ scripts/run-test-db.sh | 5 -- 13 files changed, 71 insertions(+), 139 deletions(-) create mode 100644 crates/app-commands/Cargo.toml create mode 100644 crates/app-commands/src/main.rs delete mode 100644 scripts/app-commands/.gitignore delete mode 100644 scripts/app-commands/README.md delete mode 100644 scripts/app-commands/commands.py delete mode 100644 scripts/app-commands/common/__init__.py delete mode 100644 scripts/app-commands/common/types.py delete mode 100644 scripts/app-commands/update.py delete mode 100755 scripts/dump-db.sh delete mode 100755 scripts/rclone-db.sh delete mode 100755 scripts/run-test-db.sh diff --git a/Cargo.lock b/Cargo.lock index 858c207a..0bb714cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,20 @@ dependencies = [ "twilight-http", ] +[[package]] +name = "app-commands" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "libpk", + "tokio", + "tracing", + "twilight-http", + "twilight-model", + "twilight-util", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -4560,6 +4574,7 @@ version = "0.16.0" source = "git+https://github.com/pluralkit/twilight?branch=pluralkit-7f08d95#054a2aa5d29fb46220af1cd5df568b73511cdb26" dependencies = [ "twilight-model", + "twilight-validate", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c566e979..69a9048a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ axum = { git = "https://github.com/pluralkit/axum", branch = "v0.8.4-pluralkit" twilight-gateway = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" } twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] } -twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] } +twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator", "builder"] } twilight-model = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" } twilight-http = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", default-features = false, features = ["rustls-aws_lc_rs", "rustls-native-roots"] } diff --git a/crates/app-commands/Cargo.toml b/crates/app-commands/Cargo.toml new file mode 100644 index 00000000..11ab75d4 --- /dev/null +++ b/crates/app-commands/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "app-commands" +version = "0.1.0" +edition = "2024" + +[dependencies] +libpk = { path = "../libpk" } +anyhow = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +twilight-http = { workspace = true } +twilight-model = { workspace = true } +twilight-util = { workspace = true } diff --git a/crates/app-commands/src/main.rs b/crates/app-commands/src/main.rs new file mode 100644 index 00000000..a03f5b70 --- /dev/null +++ b/crates/app-commands/src/main.rs @@ -0,0 +1,41 @@ +use twilight_model::{ + application::command::{Command, CommandType}, + guild::IntegrationApplication, +}; +use twilight_util::builder::command::CommandBuilder; + +#[libpk::main] +async fn main() -> anyhow::Result<()> { + let discord = twilight_http::Client::builder() + .token( + libpk::config + .discord + .as_ref() + .expect("missing discord config") + .bot_token + .clone(), + ) + .build(); + + let interaction = discord.interaction(twilight_model::id::Id::new( + libpk::config + .discord + .as_ref() + .expect("missing discord config") + .client_id + .clone() + .get(), + )); + + let commands = vec![ + // message commands + // description must be empty string + CommandBuilder::new("\u{2753} Message info", "", CommandType::Message).build(), + CommandBuilder::new("\u{274c} Delete message", "", CommandType::Message).build(), + CommandBuilder::new("\u{1f514} Ping author", "", CommandType::Message).build(), + ]; + + interaction.set_global_commands(&commands).await?; + + Ok(()) +} diff --git a/scripts/app-commands/.gitignore b/scripts/app-commands/.gitignore deleted file mode 100644 index 3e370e3f..00000000 --- a/scripts/app-commands/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/commands.json - -*.pyc -__pycache__/ diff --git a/scripts/app-commands/README.md b/scripts/app-commands/README.md deleted file mode 100644 index 9d8d576a..00000000 --- a/scripts/app-commands/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# PluralKit "application command" helpers - -## Adding new commands - -Edit the `COMMAND_LIST` global in `commands.py`, making sure that any -command names that are specified in that file match up with the -command names used in the bot code (which will generally be in the list -in `PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandList.cs`). - -TODO: add helpers for slash commands to this - -## Dumping application command JSON - -Run `python3 commands.py` to get a JSON dump of the available application -commands - this is in a format that can be sent to Discord as a `PUT` to -`/applications/{clientId}/commands`. - -## Updating Discord's list of application commands - -From the root of the repository (where your `pluralkit.conf` resides), -run `python3 ./scripts/app-commands/update.py`. This will **REPLACE** -any existing application commands that Discord knows about, with the -updated list. \ No newline at end of file diff --git a/scripts/app-commands/commands.py b/scripts/app-commands/commands.py deleted file mode 100644 index 6a908ba7..00000000 --- a/scripts/app-commands/commands.py +++ /dev/null @@ -1,10 +0,0 @@ -from common import * - -COMMAND_LIST = [ - MessageCommand("\U00002753 Message info"), - MessageCommand("\U0000274c Delete message"), - MessageCommand("\U0001f514 Ping author"), -] - -if __name__ == "__main__": - print(__import__('json').dumps(COMMAND_LIST)) \ No newline at end of file diff --git a/scripts/app-commands/common/__init__.py b/scripts/app-commands/common/__init__.py deleted file mode 100644 index 2a53ba18..00000000 --- a/scripts/app-commands/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .types import MessageCommand \ No newline at end of file diff --git a/scripts/app-commands/common/types.py b/scripts/app-commands/common/types.py deleted file mode 100644 index 65c35232..00000000 --- a/scripts/app-commands/common/types.py +++ /dev/null @@ -1,7 +0,0 @@ -class MessageCommand(dict): - COMMAND_TYPE = 3 - - def __init__(self, name): - super().__init__() - self["type"] = self.__class__.COMMAND_TYPE - self["name"] = name \ No newline at end of file diff --git a/scripts/app-commands/update.py b/scripts/app-commands/update.py deleted file mode 100644 index 3005db0a..00000000 --- a/scripts/app-commands/update.py +++ /dev/null @@ -1,70 +0,0 @@ -from common import * -from commands import COMMAND_LIST - -import io -import os -import sys -import json - -from pathlib import Path -from urllib import request -from urllib.error import URLError - -DISCORD_API_BASE = "https://discord.com/api/v10" - -def get_config(): - data = {} - - # prefer token from environment if present - envbase = ["PluralKit", "Bot"] - for var in ["Token", "ClientId"]: - for sep in [':', '__']: - envvar = sep.join(envbase + [var]) - if envvar in os.environ: - data[var] = os.environ[envvar] - - if "Token" in data and "ClientId" in data: - return data - - # else fall back to config - cfg_path = Path(os.getcwd()) / "pluralkit.conf" - if cfg_path.exists(): - cfg = {} - with open(str(cfg_path), 'r') as fh: - cfg = json.load(fh) - - if 'PluralKit' in cfg and 'Bot' in cfg['PluralKit']: - return cfg['PluralKit']['Bot'] - - return None - -def main(): - config = get_config() - if config is None: - raise ArgumentError("config was not loaded") - if 'Token' not in config or 'ClientId' not in config: - raise ArgumentError("config is missing 'Token' or 'ClientId'") - - data = json.dumps(COMMAND_LIST) - url = DISCORD_API_BASE + f"/applications/{config['ClientId']}/commands" - req = request.Request(url, method='PUT', data=data.encode('utf-8')) - req.add_header("Content-Type", "application/json") - req.add_header("Authorization", f"Bot {config['Token']}") - req.add_header("User-Agent", "PluralKit (app-commands updater; https://pluralkit.me)") - - try: - with request.urlopen(req) as resp: - if resp.status == 200: - print("Update successful!") - return 0 - - except URLError as resp: - print(f"[!!!] Update not successful: status {resp.status}", file=sys.stderr) - print(f"[!!!] Response body below:\n", file=sys.stderr) - print(resp.read(), file=sys.stderr) - sys.stderr.flush() - - return 1 - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/scripts/dump-db.sh b/scripts/dump-db.sh deleted file mode 100755 index d794dab4..00000000 --- a/scripts/dump-db.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -docker-compose -f "$(dirname $0)/../docker-compose.yml" exec -T -u postgres db pg_dump postgres - diff --git a/scripts/rclone-db.sh b/scripts/rclone-db.sh deleted file mode 100755 index 6e4fda7c..00000000 --- a/scripts/rclone-db.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# Usage: rclone-db.sh : -# eg. rclone-db.sh b2:pluralkit - -FILENAME=pluralkit-$(date -u +"%Y-%m-%dT%H:%M:%S").sql.gz - -echo Dumping database to /tmp/$FILENAME... -$(dirname $0)/dump-db.sh | gzip > /tmp/$FILENAME - -echo Transferring to remote $1... -rclone -P copy /tmp/$FILENAME $1 - -echo Cleaning up... -rm /tmp/$FILENAME \ No newline at end of file diff --git a/scripts/run-test-db.sh b/scripts/run-test-db.sh deleted file mode 100755 index 272d24e5..00000000 --- a/scripts/run-test-db.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# Runs a local database in the background listening on port 5432, deleting itself once stopped -# Requires Docker. May need sudo if your user isn't in the `docker` group. -docker run --rm --detach --publish 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:alpine \ No newline at end of file From 6a7ab2b853f426ae5b7cace75ca17abf05adcd95 Mon Sep 17 00:00:00 2001 From: alyssa Date: Wed, 15 Oct 2025 21:24:19 +0000 Subject: [PATCH 6/9] feat: disable autoproxy with 3 backslashes --- PluralKit.Bot/Proxy/ProxyService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 8a59957d..ee6108af 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -66,6 +66,15 @@ public class ProxyService var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null); + if (IsDisableAutoproxy(message)) + { + await _repo.UpdateAutoproxy(ctx.SystemId.Value, guild.Id, null, new() + { + AutoproxyMode = AutoproxyMode.Off + }); + return false; + } + if (autoproxySettings.AutoproxyMode == AutoproxyMode.Latch && IsUnlatch(message)) { // "unlatch" @@ -495,6 +504,9 @@ public class ProxyService public static bool IsUnlatch(Message message) => message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\"); + public static bool IsDisableAutoproxy(Message message) + => message.Content.StartsWith(@"\\\") || message.Content.StartsWith("\\\u200b\\\u200b\\"); + private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings, Message triggerMessage, Message proxyMessage, ProxyMatch match, bool deletePrevious = true) From 85b2b77730146d80c0943eb35f2d11674256ff28 Mon Sep 17 00:00:00 2001 From: alyssa Date: Sun, 9 Nov 2025 09:11:21 +0000 Subject: [PATCH 7/9] chore(docs): update catalogger website link --- docs/content/staff/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/staff/compatibility.md b/docs/content/staff/compatibility.md index 8d0c6fc5..ed5baf33 100644 --- a/docs/content/staff/compatibility.md +++ b/docs/content/staff/compatibility.md @@ -5,7 +5,7 @@ Because PluralKit deletes messages as part of proxying, this can often clutter u ## Bots with PluralKit support Some moderation bots have official PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages: -- [**Catalogger**](https://catalogger.starshines.xyz/docs) +- [**Catalogger**](https://catalogger.app) - [**Aero**](https://aero.bot/) - [**CoreBot**](https://discord.gg/GAAj6DDrCJ) - [**Quark**](https://quark.bot) From 7d7442eb1602cf747f830b7ede57ddcfb1e27ed7 Mon Sep 17 00:00:00 2001 From: Iris System Date: Wed, 19 Nov 2025 11:53:34 +1300 Subject: [PATCH 8/9] chore(docs): add Prodigi sponsor info --- docs/content/assets/prodigi.png | Bin 0 -> 12195 bytes docs/content/index.md | 12 +++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/content/assets/prodigi.png diff --git a/docs/content/assets/prodigi.png b/docs/content/assets/prodigi.png new file mode 100644 index 0000000000000000000000000000000000000000..9c848e37130f2ea3c6c4672e038b49d2f2002f41 GIT binary patch literal 12195 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 nXj>4|&dmP*b + +![Prodigi logo](./assets/prodigi.png) + + + +PluralKit's infrastructure is generously sponsored by [Prodigi](https://prodigi.nz), a New Zealand based technology services provider. \ No newline at end of file From c4679ccfb8239e4bbc7ed803a259663a6d244158 Mon Sep 17 00:00:00 2001 From: Iris System Date: Wed, 19 Nov 2025 11:54:05 +1300 Subject: [PATCH 9/9] chore(docs): update rolerestrict FAQ to mirror updated blueberry tag --- docs/content/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/faq.md b/docs/content/faq.md index f4141956..eb86f992 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -55,8 +55,8 @@ This includes: For more information about why certain information must be public, see [this github issue](https://github.com/PluralKit/PluralKit/issues/238). ### Is there a way to restrict PluralKit usage to a certain role? / Can I remove PluralKit access for specific users in my server? -This is not a feature currently available in PluralKit. It may be added in the future. -In the meantime, this feature is supported in Tupperbox (an alternative proxying bot) - ask about it in their support server: +PluralKit does not, and *will not*, support restricting usage of the bot by role. +This feature is supported in Tupperbox (an alternative proxying bot) - ask about it in their support server: ### Is it possible to block proxied messages (like blocking a user)? No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/