diff --git a/Cargo.lock b/Cargo.lock index 8e52d160..0bc78644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,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" @@ -4844,6 +4858,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 3f2e4c94..c97fe43a 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/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 70e48472..4a0eb699 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -407,21 +407,33 @@ public class ProxiedMessage if (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) @@ -453,6 +465,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..ef4ab4f9 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, AllowedMentions = new AllowedMentions() }); } catch (ForbiddenException) { } // No permissions to DM, can't check for this :( 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) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 620d7229..1ad8292e 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 == "" || avatarURL == null) ? 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..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,6 +77,17 @@ public class InteractionContext }); } + 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, + AllowedMentions = mentions ?? new AllowedMentions() + }); + } + public async Task Defer() { await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource, 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, 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/docs/content/assets/prodigi.png b/docs/content/assets/prodigi.png new file mode 100644 index 00000000..9c848e37 Binary files /dev/null and b/docs/content/assets/prodigi.png differ 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 :/ diff --git a/docs/content/index.md b/docs/content/index.md index 23da5fa6..bb609b4a 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -11,4 +11,14 @@ This bot detects messages with certain tags associated with a profile, then repl #### for example... ![demonstration of PluralKit](./assets/demo.gif) -For more information, see the links to the left, or click [here](https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904) to invite the bot to your server! \ No newline at end of file +For more information, see the links to the left, or click [here](https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904) to invite the bot to your server! + +## Sponsors + +
+ +![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 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) 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