From a6941cea083d6c31d7d6413d550fe2cf69ff2dd9 Mon Sep 17 00:00:00 2001 From: Iris System Date: Sat, 27 Dec 2025 15:04:31 +1300 Subject: [PATCH] feat: add premium management admin commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 2 + PluralKit.Bot/Commands/Admin.cs | 81 +++++++++++++++++++ .../Models/Patch/SystemConfigPatch.cs | 10 +++ 3 files changed, 93 insertions(+) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index b64e603a..84bc1041 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -182,6 +182,8 @@ public partial class CommandTree await ctx.Execute(Admin, a => a.SystemRecover(ctx)); else if (ctx.Match("sd", "systemdelete")) await ctx.Execute(Admin, a => a.SystemDelete(ctx)); + else if (ctx.Match("pe", "premiumexpiry", "premium")) + await ctx.Execute(Admin, a => a.PremiumExpiry(ctx)); else if (ctx.Match("sendmsg", "sendmessage")) await ctx.Execute(Admin, a => a.SendAdminMessage(ctx)); else if (ctx.Match("al", "abuselog")) diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 44171345..51317502 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -2,6 +2,8 @@ using System.Text.RegularExpressions; using Humanizer; using Dapper; +using NodaTime; +using NodaTime.Text; using SqlKata; using Myriad.Builders; @@ -79,6 +81,16 @@ public class Admin var groupCount = await ctx.Repository.GetSystemGroupCount(system.Id); eb.Field(new Embed.Field("Group limit", $"{groupLimit} {UntilLimit(groupCount, groupLimit)}", true)); + var premiumEntitlement = "none"; + if (config.PremiumLifetime) + premiumEntitlement = $"<:lifetime_premium:{_botConfig.PremiumLifetimeEmoji}> **lifetime**"; + else if (config.PremiumUntil != null) + if (SystemClock.Instance.GetCurrentInstant() < config.PremiumUntil!) + premiumEntitlement = $"<:premium_subscriber:{_botConfig.PremiumSubscriberEmoji}> "; + else + premiumEntitlement = $"Expired! "; + eb.Field(new Embed.Field("Premium entitlement", premiumEntitlement, false)); + return eb.Build(); } @@ -396,6 +408,75 @@ public class Admin await ctx.Reply($"{Emojis.Success} System deletion succesful."); } + public async Task PremiumExpiry(Context ctx) + { + ctx.AssertBotAdmin(); + + var target = await ctx.MatchSystem(); + if (target == null) + throw new PKError("Unknown system."); + + await ctx.Reply(null, await CreateEmbed(ctx, target)); + if (!ctx.HasNext()) + return; + + if (ctx.Match("lifetime", "staff")) + { + if (!await ctx.PromptYesNo($"Grant system `{target.Hid}` lifetime premium?", "Grant")) + throw new PKError("Premium entitlement change cancelled."); + + await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch + { + PremiumLifetime = true, + PremiumUntil = null, + }); + await ctx.Reply($"{Emojis.Success} Premium entitlement changed."); + } + else if (ctx.Match("none", "clear")) + { + if (!await ctx.PromptYesNo($"Clear premium entitlements for system `{target.Hid}`?", "Clear")) + throw new PKError("Premium entitlement change cancelled."); + + await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch + { + PremiumLifetime = false, + PremiumUntil = null, + }); + await ctx.Reply($"{Emojis.Success} Premium entitlement changed."); + } + else + { + var timeToMove = ctx.RemainderOrNull() ?? + throw new PKSyntaxError("Must pass a date/time to set premium expiry to."); + + Instant? time = null; + + // DateUtils.ParseDateTime expects periods to be in the past, so we have to do + // this explicitly here... + var duration = DateUtils.ParsePeriod(timeToMove); + if (duration != null) + { + time = SystemClock.Instance.GetCurrentInstant() + duration; + } + else + { + var result = DateUtils.ParseDateTime(timeToMove, false); + if (result == null) throw Errors.InvalidDateTime(timeToMove); + time = result.Value.ToInstant(); + } + + if (!await ctx.PromptYesNo($"Change premium expiry for system `{target.Hid}` to ?", "Change")) + throw new PKError("Premium entitlement change cancelled."); + + await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch + { + PremiumLifetime = false, + PremiumUntil = time, + }); + await ctx.Reply($"{Emojis.Success} Premium entitlement changed."); + } + } + public async Task AbuseLogCreate(Context ctx) { var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage"); diff --git a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs index c29cfae8..cc7cae99 100644 --- a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs @@ -26,6 +26,8 @@ public class SystemConfigPatch: PatchObject public Partial NameFormat { get; set; } public Partial HidListPadding { get; set; } public Partial ProxySwitch { get; set; } + public Partial PremiumLifetime { get; set; } + public Partial PremiumUntil { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("ui_tz", UiTz) @@ -45,6 +47,8 @@ public class SystemConfigPatch: PatchObject .With("card_show_color_hex", CardShowColorHex) .With("proxy_switch", ProxySwitch) .With("name_format", NameFormat) + .With("premium_lifetime", PremiumLifetime) + .With("premium_until", PremiumUntil) ); public new void AssertIsValid() @@ -118,6 +122,12 @@ public class SystemConfigPatch: PatchObject if (NameFormat.IsPresent) o.Add("name_format", NameFormat.Value); + if (PremiumLifetime.IsPresent) + o.Add("premium_lifetime", PremiumLifetime.Value); + + if (PremiumUntil.IsPresent) + o.Add("premium_until", PremiumUntil.Value?.FormatExport()); + return o; }