diff --git a/Myriad/Rest/BaseRestClient.cs b/Myriad/Rest/BaseRestClient.cs index 9b11f30f..82390aa5 100644 --- a/Myriad/Rest/BaseRestClient.cs +++ b/Myriad/Rest/BaseRestClient.cs @@ -35,7 +35,10 @@ public class BaseRestClient: IAsyncDisposable if (!token.StartsWith("Bot ")) token = "Bot " + token; - Client = new HttpClient(); + Client = new HttpClient(new SocketsHttpHandler + { + PooledConnectionIdleTimeout = TimeSpan.FromSeconds(3), + }); Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", userAgent); Client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", token); diff --git a/PluralKit.API/Program.cs b/PluralKit.API/Program.cs index 4f746b57..4958baed 100644 --- a/PluralKit.API/Program.cs +++ b/PluralKit.API/Program.cs @@ -2,6 +2,8 @@ using Autofac.Extensions.DependencyInjection; using PluralKit.Core; +using Sentry; + using Serilog; namespace PluralKit.API; @@ -21,9 +23,16 @@ public class Program opts.Dsn = config.SentryUrl ?? ""; opts.Release = BuildInfoService.FullVersion; opts.AutoSessionTracking = true; - // opts.DisableTaskUnobservedTaskExceptionCapture(); + opts.DisableUnobservedTaskExceptionCapture(); }); + TaskScheduler.UnobservedTaskException += (_, e) => + { + foreach (var inner in e.Exception.Flatten().InnerExceptions) + SentrySdk.CaptureException(inner); + e.SetObserved(); + }; + await host.Services.GetRequiredService().InitAsync(config); await host.RunAsync(); } diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index c61dd88b..e947c6e8 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -442,7 +442,7 @@ public class ProxiedMessage return; } - await ctx.Reply(components: await _embeds.CreateAuthorMessageComponents(user, message)); + await ctx.Reply(components: _embeds.CreateAuthorMessageComponents(user, message)); return; } diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index a732fa5b..81b75a02 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -58,13 +58,19 @@ public class MessageEdited: IEventHandler var channel = await _cache.TryGetChannel(guildId, evt.ChannelId); // todo: is this correct for message update? if (channel == null) - throw new Exception("could not find self channel in MessageEdited event"); + { + _logger.Warning("could not find self channel in MessageEdited event"); + return; + } if (!DiscordUtils.IsValidGuildChannel(channel)) return; var rootChannel = await _cache.GetRootChannel(guildId, channel.Id); var guild = await _cache.TryGetGuild(channel.GuildId!.Value); if (guild == null) - throw new Exception("could not find self guild in MessageEdited event"); + { + _logger.Warning("could not find self guild in MessageEdited event"); + return; + } var lastMessage = (await _lastMessageCache.GetLastMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId))?.Current; // Only react to the last message in the channel diff --git a/PluralKit.Bot/Init.cs b/PluralKit.Bot/Init.cs index 25b98f1b..7ccd3696 100644 --- a/PluralKit.Bot/Init.cs +++ b/PluralKit.Bot/Init.cs @@ -45,9 +45,16 @@ public class Init opts.Dsn = services.Resolve().SentryUrl ?? ""; opts.Release = BuildInfoService.FullVersion; opts.AutoSessionTracking = true; - // opts.DisableTaskUnobservedTaskExceptionCapture(); + opts.DisableUnobservedTaskExceptionCapture(); }); + TaskScheduler.UnobservedTaskException += (_, e) => + { + foreach (var inner in e.Exception.Flatten().InnerExceptions) + SentrySdk.CaptureException(inner); + e.SetObserved(); + }; + var config = services.Resolve(); var coreConfig = services.Resolve(); diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index ee6108af..393af23c 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -324,6 +324,12 @@ public class ProxyService // Mangle embeds (for reply embed color changing) var mangledEmbeds = originalMsg.Embeds!.Select(embed => MangleReproxyEmbed(embed, member)).Where(embed => embed != null).ToArray(); + Message.MessageFlags flags = 0; + if (originalMsg.Flags.HasFlag(Message.MessageFlags.SuppressNotifications)) + flags |= Message.MessageFlags.SuppressNotifications; + if (originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage)) + flags |= Message.MessageFlags.VoiceMessage; + // Send the reproxied webhook var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest { @@ -339,7 +345,7 @@ public class ProxyService Embeds = mangledEmbeds, Stickers = originalMsg.StickerItems!, AllowEveryone = allowEveryone, - Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, + Flags = flags, Tts = tts, Poll = originalMsg.Poll, }); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index f33d56f8..a5a4a123 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -1004,7 +1004,7 @@ public class EmbedService return eb.Build(); } - public async Task CreateAuthorMessageComponents(User? user, FullMessage msg) + public MessageComponent[] CreateAuthorMessageComponents(User? user, FullMessage msg) { MessageComponent authorInfo; var author = user != null @@ -1049,16 +1049,15 @@ public class EmbedService authorInfo, ] }; - return ( - [ - new MessageComponent() - { - Type = ComponentType.Text, - Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {msg.Message.Sender})*" - }, - container - ] - ); + 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) diff --git a/PluralKit.Bot/Services/LastMessageCacheService.cs b/PluralKit.Bot/Services/LastMessageCacheService.cs index 46a51c64..03db8b8e 100644 --- a/PluralKit.Bot/Services/LastMessageCacheService.cs +++ b/PluralKit.Bot/Services/LastMessageCacheService.cs @@ -32,7 +32,7 @@ public class LastMessageCacheService public async Task GetLastMessage(ulong guild, ulong channel) { if (_maybeHttp is HttpDiscordCache) - return await (_maybeHttp as HttpDiscordCache).GetLastMessage(guild, channel); + return await (_maybeHttp as HttpDiscordCache)!.GetLastMessage(guild, channel); return _cache.TryGetValue(channel, out var message) ? message : null; } diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Member.cs b/PluralKit.Core/Database/Repository/ModelRepository.Member.cs index 8a45d0e6..d00a7db7 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Member.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Member.cs @@ -13,7 +13,7 @@ public partial class ModelRepository public Task GetMemberByHid(string hid, SystemId? system = null) { - var query = new Query("members").Where("hid", hid.ToLower()); + var query = new Query("members").WhereRaw("hid = (?)::char(6)", hid.ToLower()); if (system != null) query = query.Where("system", system); return _db.QueryFirst(query); diff --git a/docs/content/assets/premiumTeaser.png b/docs/content/assets/premiumTeaser.png new file mode 100644 index 00000000..1ea3ca31 Binary files /dev/null and b/docs/content/assets/premiumTeaser.png differ diff --git a/docs/content/posts/2026-01-16-pluralkit-premium.md b/docs/content/posts/2026-01-16-pluralkit-premium.md new file mode 100644 index 00000000..c2cd0884 --- /dev/null +++ b/docs/content/posts/2026-01-16-pluralkit-premium.md @@ -0,0 +1,89 @@ +--- +title: Announcing PluralKit Premium +permalink: /posts/2026-01-16-pluralkit-premium/ +--- + +## Announcing PluralKit Premium + +As we've teased earlier in [the support server](https://discord.gg/PczBt78), +we will be adding a "premium" subscription tier to PluralKit. We do want to +assure everyone, however, that **the bot will always be free to use, and no +existing features (or new core features) will ever be paywalled.** + +Our goal for PK Premium is to have the income from subscriptions cover all the +running costs of the bot - which, up until now, has been paid for partly by +donations to our [Patreon](https://patreon.com/pluralkit) / +[Buy Me A Coffee](https://buymeacoffee.com/pluralkit); and partly out of the +pockets of the developers. + +PluralKit Premium will cost **US$5/month** (plus tax), and will be **launching +before the end of February 2026**. + +PluralKit Premium will offer both cosmetic perks, and "power user" features. +At launch, PK Premium will offer the following: + +- the ability to set custom system/member/group IDs, +- lossless, higher resolution image hosting on PluralKit's CDN, +- the ability to upload avatars/banners directly from the PluralKit Dashboard, +- and a badge on your PluralKit system card to show off your support. + +We will be adding more perks to PK Premium in future - including, but not +limited to: +- more description customisation options, +- automatic regex-based proxy text substitution ("automated typing quirks"), +- and more! + +
+ +![PluralKit Premium teaser screenshot](../assets/premiumTeaser.png) + +
+ +### FAQs + +#### I didn't read any of the stuff above, give me a tl;dr! + +- PK Premium is launching before the end of February 2026, at US$5/month (+ tax) +- No existing features, or new core features, will be paywalled - the premium + subscription offers cosmetic perks and power-user features only +- Features at launch include custom IDs; high-resolution image hosting; and + direct image upload from the web dashboard +- More premium features are still to come! + +#### Can I pay yearly? + +Yes, there will be a yearly subscription option. There is no discount for paying +yearly. + +#### Can I gift a PK Premium subscription to someone? + +Not at launch, but we will likely be revisiting this in future. + +#### How will ID changes work? + +There will be bot commands for changing your own system, member, and group IDs. + +Each month of PK Premium you pay for will grant you a number of ID change "tokens," +and each ID you change uses one of those "tokens." The exact number of ID changes +you will get each month has not yet been confirmed, but they will stack over time. + +In addition to the "token" system, to ensure fairness, there is a cap on the number +of IDs you can change in a 24-hour period. + +#### Can I transfer custom IDs to another system? + +No, sorry. + +#### If I subscribe; customise my IDs; and then cancel, do I keep my custom IDs? + +Yes! + +#### How does this affect PluralKit being open source? + +PluralKit will remain open source, and there is no change to the licensing. +Premium features are included in the open source code. + +#### I have another question! + +Please ask in [#bot-support in the PluralKit support server](https://discord.gg/PczBt78), +and we'll update this FAQ! diff --git a/docs/content/posts/index.md b/docs/content/posts/index.md index c6b3c412..d2d96982 100644 --- a/docs/content/posts/index.md +++ b/docs/content/posts/index.md @@ -4,6 +4,7 @@ title: Announcements & other posts # Announcements & other posts +- 2026-01-16: [Announcing PluralKit Premium](/posts/2026-01-16-pluralkit-premium/) - 2025-09-08: [on the switch to Components V2](/posts/2025-09-08-components-v2/) - 2025-01-14: [january 2025 funding update](/posts/2025-01-14-funding-update/) - 2024-12-05: [late 2024 downtime notes & funding update](/posts/2024-12-05-downtime-notes/) diff --git a/docs/content/staff/permissions.md b/docs/content/staff/permissions.md index 41f27747..b48cf82f 100644 --- a/docs/content/staff/permissions.md +++ b/docs/content/staff/permissions.md @@ -2,21 +2,23 @@ PluralKit requires some channel permissions in order to function properly: -- Message proxying requires the **Manage Messages** and **Manage Webhooks** permissions in a channel. -- Most commands require the **Embed Links**, **Attach Files** and **Add Reactions** permissions to function properly. +- *Everything* PluralKit does aside from the Message Info app command requires **View Channel** permissions in a channel. +- Message proxying requires the **Manage Messages**, **Manage Webhooks**, and **Send Messages** permissions in a channel. +- Most commands require the **Embed Links** and **Add Reactions** permissions to function properly. - Commands with reaction menus also require **Manage Messages** to remove reactions after clicking. + - Commands executed via reactions (for example the :x:, :bell:, and :question: reactions, as well as any commands with reaction menus) need **Read Message History** to be able to see that reactions were added. + - A couple commands (`pk;s color` and `pk;m color`) currently require **Attach Files**. - [Proxy logging](/staff/logging) requires the **Send Messages** permission in the log channel. - [Log cleanup](/staff/compatibility/#log-cleanup) requires the **Manage Messages** permission in the log channels. -Denying the **Send Messages** permission will *not* stop the bot from proxying, although it will prevent it from sending command responses. Denying the **Read Messages** permission will, as any other bot, prevent the bot from interacting in that channel at all. - ## Webhook permissions -Webhooks exist outside of the normal Discord permissions system, and (with a few exceptions) it's not possible to modify their permissions. +Webhooks exist outside of the normal Discord permissions system, but as of August 2022 they mostly follow the permissions of the webhook owner (in this case, PluralKit). -However, PluralKit will make an attempt to apply the sender account's permissions to proxied messages. For example, role mentions, `@everyone`, and `@here` +PluralKit will also make an attempt to apply the sender account's permissions to proxied messages. For example, role mentions, `@everyone`, and `@here` will only function if the sender account has that permission. The same applies to link embeds. -For external emojis to work in proxied messages, the `@everyone` role must have the "Use External Emojis" permission. If it still doesn't work, check if the permission was denied in channel-specific permission settings. +For external emojis to work in proxied messages, PluralKit or one of its roles must have the "Use External Emojis" permission. If it still doesn't work, +check if the permission was denied in channel-specific permission settings. PluralKit must also be in the server the external emoji belongs to. ## Troubleshooting @@ -30,4 +32,4 @@ For example: pk;debug permissions 466707357099884544 -You can find this ID [by enabling Developer Mode and right-clicking (or long-pressing) on the server icon](https://discordia.me/developer-mode). \ No newline at end of file +You can find this ID [by enabling Developer Mode and right-clicking (or long-pressing) on the server icon](https://discordia.me/developer-mode). diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index 51654841..cfe02c47 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -453,7 +453,7 @@ You can #### Pinging the user who sent it If you'd like to "ping" the account behind a proxied message without having to query the message and ping them yourself, -you can react to the message with the `:bell:` :bell: emoji (or `:bellhop:` :bellhop:, `:exclamation:` :exclamation:, or even `:ping_pong:` :ping_pong:), and PluralKit will ping the relevant member and account in the same channel on your behalf with a link to the message you reacted to. +you can react to the message with the `:bell:` :bell: emoji (or `:bellhop:` :bellhop_bell:, `:exclamation:` :exclamation:, or even `:ping_pong:` :ping_pong:), and PluralKit will ping the relevant member and account in the same channel on your behalf with a link to the message you reacted to. ## Autoproxy The bot's *autoproxy* feature allows you to have messages be proxied without directly including the proxy tags. Autoproxy can be set up in various ways. There are three autoproxy modes currently implemented: