mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
feat: premium ID changes
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
Build and push Rust service Docker images / rust docker build (push) Has been cancelled
rust checks / cargo fmt (push) Has been cancelled
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
Build and push Rust service Docker images / rust docker build (push) Has been cancelled
rust checks / cargo fmt (push) Has been cancelled
This commit is contained in:
parent
41f8beb2aa
commit
24b6b0d455
16 changed files with 306 additions and 3 deletions
|
|
@ -22,6 +22,7 @@ public partial class CommandTree
|
||||||
public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history");
|
public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history");
|
||||||
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
|
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
|
||||||
public static Command SystemId = new Command("system id", "system [system] id", "Prints your system's id.");
|
public static Command SystemId = new Command("system id", "system [system] id", "Prints your system's id.");
|
||||||
|
public static Command SystemIdChange = new Command("system changeid", "system [system] changeid <id>", "PREMIUM: Changes your system's ID.");
|
||||||
public static Command SystemPrivacy = new Command("system privacy", "system [system] privacy <name|avatar|description|members|fronter|fronthistory|all> <public|private>", "Changes your system's privacy settings");
|
public static Command SystemPrivacy = new Command("system privacy", "system [system] privacy <name|avatar|description|members|fronter|fronthistory|all> <public|private>", "Changes your system's privacy settings");
|
||||||
public static Command ConfigTimezone = new Command("config timezone", "config timezone [timezone]", "Changes your system's time zone");
|
public static Command ConfigTimezone = new Command("config timezone", "config timezone [timezone]", "Changes your system's time zone");
|
||||||
public static Command ConfigPing = new Command("config ping", "config ping [on|off]", "Changes your system's ping preferences");
|
public static Command ConfigPing = new Command("config ping", "config ping [on|off]", "Changes your system's ping preferences");
|
||||||
|
|
@ -61,6 +62,7 @@ public partial class CommandTree
|
||||||
public static Command MemberServerKeepProxy = new Command("member server keepproxy", "member <member> serverkeepproxy [on|off|clear]", "Sets whether to include a member's proxy tags when proxying in the current server.");
|
public static Command MemberServerKeepProxy = new Command("member server keepproxy", "member <member> serverkeepproxy [on|off|clear]", "Sets whether to include a member's proxy tags when proxying in the current server.");
|
||||||
public static Command MemberRandom = new Command("system random", "system [system] random", "Shows the info card of a randomly selected member in a system.");
|
public static Command MemberRandom = new Command("system random", "system [system] random", "Shows the info card of a randomly selected member in a system.");
|
||||||
public static Command MemberId = new Command("member id", "member [member] id", "Prints a member's id.");
|
public static Command MemberId = new Command("member id", "member [member] id", "Prints a member's id.");
|
||||||
|
public static Command MemberIdChange = new Command("member changeid", "member [member] changeid <id>", "PREMIUM: Changes a member's ID.");
|
||||||
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|proxy|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
|
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|proxy|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
|
||||||
public static Command GroupInfo = new Command("group", "group <name>", "Looks up information about a group");
|
public static Command GroupInfo = new Command("group", "group <name>", "Looks up information about a group");
|
||||||
public static Command GroupNew = new Command("group new", "group new <name>", "Creates a new group");
|
public static Command GroupNew = new Command("group new", "group new <name>", "Creates a new group");
|
||||||
|
|
@ -73,6 +75,7 @@ public partial class CommandTree
|
||||||
public static Command GroupAdd = new Command("group add", "group <group> add <member> [member 2] [member 3...]", "Adds one or more members to a group");
|
public static Command GroupAdd = new Command("group add", "group <group> add <member> [member 2] [member 3...]", "Adds one or more members to a group");
|
||||||
public static Command GroupRemove = new Command("group remove", "group <group> remove <member> [member 2] [member 3...]", "Removes one or more members from a group");
|
public static Command GroupRemove = new Command("group remove", "group <group> remove <member> [member 2] [member 3...]", "Removes one or more members from a group");
|
||||||
public static Command GroupId = new Command("group id", "group [group] id", "Prints a group's id.");
|
public static Command GroupId = new Command("group id", "group [group] id", "Prints a group's id.");
|
||||||
|
public static Command GroupIdChange = new Command("group changeid", "group [group] changeid <id>", "PREMIUM: Changes a group's ID.");
|
||||||
public static Command GroupPrivacy = new Command("group privacy", "group <group> privacy <name|description|icon|metadata|visibility|all> <public|private>", "Changes a group's privacy settings");
|
public static Command GroupPrivacy = new Command("group privacy", "group <group> privacy <name|description|icon|metadata|visibility|all> <public|private>", "Changes a group's privacy settings");
|
||||||
public static Command GroupBannerImage = new Command("group banner", "group <group> banner [url]", "Set the group's banner image");
|
public static Command GroupBannerImage = new Command("group banner", "group <group> banner [url]", "Set the group's banner image");
|
||||||
public static Command GroupIcon = new Command("group icon", "group <group> icon [url|@mention]", "Changes a group's icon");
|
public static Command GroupIcon = new Command("group icon", "group <group> icon [url|@mention]", "Changes a group's icon");
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,8 @@ public partial class CommandTree
|
||||||
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
|
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
|
||||||
else if (ctx.Match("pe", "premiumexpiry", "premium"))
|
else if (ctx.Match("pe", "premiumexpiry", "premium"))
|
||||||
await ctx.Execute<Admin>(Admin, a => a.PremiumExpiry(ctx));
|
await ctx.Execute<Admin>(Admin, a => a.PremiumExpiry(ctx));
|
||||||
|
else if (ctx.Match("pid", "premiumidchange", "premiumid"))
|
||||||
|
await ctx.Execute<Admin>(Admin, a => a.PremiumIdChangeAllowance(ctx));
|
||||||
else if (ctx.Match("sendmsg", "sendmessage"))
|
else if (ctx.Match("sendmsg", "sendmessage"))
|
||||||
await ctx.Execute<Admin>(Admin, a => a.SendAdminMessage(ctx));
|
await ctx.Execute<Admin>(Admin, a => a.SendAdminMessage(ctx));
|
||||||
else if (ctx.Match("al", "abuselog"))
|
else if (ctx.Match("al", "abuselog"))
|
||||||
|
|
@ -320,6 +322,8 @@ public partial class CommandTree
|
||||||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx, target));
|
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx, target));
|
||||||
else if (ctx.Match("id"))
|
else if (ctx.Match("id"))
|
||||||
await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target));
|
await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target));
|
||||||
|
else if (ctx.Match("changeid", "updateid"))
|
||||||
|
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemIdChange, m => m.ChangeId(ctx, target));
|
||||||
else if (ctx.Match("random", "rand", "r"))
|
else if (ctx.Match("random", "rand", "r"))
|
||||||
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
|
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
|
||||||
await ctx.CheckSystem(target).Execute<Random>(GroupRandom, r => r.Group(ctx, target));
|
await ctx.CheckSystem(target).Execute<Random>(GroupRandom, r => r.Group(ctx, target));
|
||||||
|
|
@ -395,6 +399,8 @@ public partial class CommandTree
|
||||||
await ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ServerKeepProxy(ctx, target));
|
await ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ServerKeepProxy(ctx, target));
|
||||||
else if (ctx.Match("id"))
|
else if (ctx.Match("id"))
|
||||||
await ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, target));
|
await ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, target));
|
||||||
|
else if (ctx.Match("changeid", "updateid"))
|
||||||
|
await ctx.Execute<MemberEdit>(MemberIdChange, m => m.ChangeId(ctx, target));
|
||||||
else if (ctx.Match("privacy"))
|
else if (ctx.Match("privacy"))
|
||||||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, null));
|
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, null));
|
||||||
else if (ctx.Match("private", "hidden", "hide"))
|
else if (ctx.Match("private", "hidden", "hide"))
|
||||||
|
|
@ -457,6 +463,8 @@ public partial class CommandTree
|
||||||
await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
|
await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
|
||||||
else if (ctx.Match("id"))
|
else if (ctx.Match("id"))
|
||||||
await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
|
await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
|
||||||
|
else if (ctx.Match("changeid", "updateid"))
|
||||||
|
await ctx.Execute<Groups>(GroupIdChange, g => g.ChangeId(ctx, target));
|
||||||
else if (!ctx.HasNext())
|
else if (!ctx.HasNext())
|
||||||
await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
|
await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -416,12 +416,15 @@ public class Admin
|
||||||
if (target == null)
|
if (target == null)
|
||||||
throw new PKError("Unknown system.");
|
throw new PKError("Unknown system.");
|
||||||
|
|
||||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
|
||||||
if (!ctx.HasNext())
|
if (!ctx.HasNext())
|
||||||
|
{
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.Match("lifetime", "staff"))
|
if (ctx.Match("lifetime", "staff"))
|
||||||
{
|
{
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
if (!await ctx.PromptYesNo($"Grant system `{target.Hid}` lifetime premium?", "Grant"))
|
if (!await ctx.PromptYesNo($"Grant system `{target.Hid}` lifetime premium?", "Grant"))
|
||||||
throw new PKError("Premium entitlement change cancelled.");
|
throw new PKError("Premium entitlement change cancelled.");
|
||||||
|
|
||||||
|
|
@ -434,6 +437,7 @@ public class Admin
|
||||||
}
|
}
|
||||||
else if (ctx.Match("none", "clear"))
|
else if (ctx.Match("none", "clear"))
|
||||||
{
|
{
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
if (!await ctx.PromptYesNo($"Clear premium entitlements for system `{target.Hid}`?", "Clear"))
|
if (!await ctx.PromptYesNo($"Clear premium entitlements for system `{target.Hid}`?", "Clear"))
|
||||||
throw new PKError("Premium entitlement change cancelled.");
|
throw new PKError("Premium entitlement change cancelled.");
|
||||||
|
|
||||||
|
|
@ -465,6 +469,7 @@ public class Admin
|
||||||
time = result.Value.ToInstant();
|
time = result.Value.ToInstant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
if (!await ctx.PromptYesNo($"Change premium expiry for system `{target.Hid}` to <t:{time?.ToUnixTimeSeconds()}>?", "Change"))
|
if (!await ctx.PromptYesNo($"Change premium expiry for system `{target.Hid}` to <t:{time?.ToUnixTimeSeconds()}>?", "Change"))
|
||||||
throw new PKError("Premium entitlement change cancelled.");
|
throw new PKError("Premium entitlement change cancelled.");
|
||||||
|
|
||||||
|
|
@ -477,6 +482,37 @@ public class Admin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task PremiumIdChangeAllowance(Context ctx)
|
||||||
|
{
|
||||||
|
ctx.AssertBotAdmin();
|
||||||
|
|
||||||
|
var target = await ctx.MatchSystem();
|
||||||
|
if (target == null)
|
||||||
|
throw new PKError("Unknown system.");
|
||||||
|
|
||||||
|
if (!ctx.HasNext())
|
||||||
|
{
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = await ctx.Repository.GetSystemConfig(target.Id);
|
||||||
|
var newAllowanceStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000");
|
||||||
|
if (!int.TryParse(newAllowanceStr, out var newAllowance))
|
||||||
|
throw new PKError($"Couldn't parse `{newAllowanceStr}` as number.");
|
||||||
|
|
||||||
|
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||||
|
if (!await ctx.PromptYesNo($"Update premium ID change allowance from **{config.PremiumIdChangesRemaining}** to **{newAllowance}**?", "Update"))
|
||||||
|
throw new PKError("ID change allowance cancelled.");
|
||||||
|
|
||||||
|
await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch
|
||||||
|
{
|
||||||
|
PremiumIdChangesRemaining = newAllowance,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.Reply($"{Emojis.Success} Premium entitlement changed.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AbuseLogCreate(Context ctx)
|
public async Task AbuseLogCreate(Context ctx)
|
||||||
{
|
{
|
||||||
var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage");
|
var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ using System.Text.RegularExpressions;
|
||||||
using Myriad.Builders;
|
using Myriad.Builders;
|
||||||
using Myriad.Types;
|
using Myriad.Types;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
@ -99,6 +101,38 @@ public class Groups
|
||||||
await ctx.Reply(replyStr, eb.Build());
|
await ctx.Reply(replyStr, eb.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ChangeId(Context ctx, PKGroup target)
|
||||||
|
{
|
||||||
|
ctx.CheckSystem().CheckOwnGroup(target);
|
||||||
|
if (!ctx.Premium)
|
||||||
|
throw Errors.PremiumExclusiveCommand();
|
||||||
|
|
||||||
|
var input = ctx.PopArgument();
|
||||||
|
if (!input.TryParseHid(out var newHid))
|
||||||
|
throw new PKError($"Invalid new member ID `{input}`.");
|
||||||
|
|
||||||
|
var existingGroup = await ctx.Repository.GetGroupByHid(newHid);
|
||||||
|
if (existingGroup != null)
|
||||||
|
throw new PKError($"Another group already exists with ID `{newHid.DisplayHid(ctx.Config)}`.");
|
||||||
|
|
||||||
|
if (ctx.Config.PremiumIdChangesRemaining < 1)
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
if ((await ctx.Repository.GetHidChangelogCountForDate(ctx.System.Id, SystemClock.Instance.GetCurrentInstant().InUtc().Date)) >= Limits.PremiumDailyHidChanges)
|
||||||
|
throw new PKError($"You have already changed {Limits.PremiumDailyHidChanges} IDs today. Please try again tomorrow.");
|
||||||
|
|
||||||
|
if (!await ctx.PromptYesNo($"Change ID for group **{target.NameFor(ctx)}** (`{target.DisplayHid(ctx.Config)}`) to `{newHid.DisplayHid(ctx.Config)}`?", "Change"))
|
||||||
|
throw new PKError("ID change cancelled.");
|
||||||
|
|
||||||
|
if (!await ctx.Repository.TryUpdateSystemConfigForIdChange(ctx.System.Id))
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
|
||||||
|
await ctx.Repository.CreateHidChangelog(ctx.System.Id, ctx.Message.Author.Id, "group", target.Hid, newHid);
|
||||||
|
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Hid = newHid });
|
||||||
|
|
||||||
|
var newConfig = await ctx.Repository.GetSystemConfig(ctx.System.Id);
|
||||||
|
await ctx.Reply($"{Emojis.Success} Group ID changed to `{newHid.DisplayHid(ctx.Config)}`. You have **{newConfig.PremiumIdChangesRemaining}** ID changes remaining.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RenameGroup(Context ctx, PKGroup target)
|
public async Task RenameGroup(Context ctx, PKGroup target)
|
||||||
{
|
{
|
||||||
ctx.CheckOwnGroup(target);
|
ctx.CheckOwnGroup(target);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,38 @@ public class MemberEdit
|
||||||
_avatarHosting = avatarHosting;
|
_avatarHosting = avatarHosting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ChangeId(Context ctx, PKMember target)
|
||||||
|
{
|
||||||
|
ctx.CheckSystem().CheckOwnMember(target);
|
||||||
|
if (!ctx.Premium)
|
||||||
|
throw Errors.PremiumExclusiveCommand();
|
||||||
|
|
||||||
|
var input = ctx.PopArgument();
|
||||||
|
if (!input.TryParseHid(out var newHid))
|
||||||
|
throw new PKError($"Invalid new member ID `{input}`.");
|
||||||
|
|
||||||
|
var existingMember = await ctx.Repository.GetMemberByHid(newHid);
|
||||||
|
if (existingMember != null)
|
||||||
|
throw new PKError($"Another member already exists with ID `{newHid.DisplayHid(ctx.Config)}`.");
|
||||||
|
|
||||||
|
if (ctx.Config.PremiumIdChangesRemaining < 1)
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
if ((await ctx.Repository.GetHidChangelogCountForDate(ctx.System.Id, SystemClock.Instance.GetCurrentInstant().InUtc().Date)) >= Limits.PremiumDailyHidChanges)
|
||||||
|
throw new PKError($"You have already changed {Limits.PremiumDailyHidChanges} IDs today. Please try again tomorrow.");
|
||||||
|
|
||||||
|
if (!await ctx.PromptYesNo($"Change ID for member **{target.NameFor(ctx)}** (`{target.DisplayHid(ctx.Config)}`) to `{newHid.DisplayHid(ctx.Config)}`?", "Change"))
|
||||||
|
throw new PKError("ID change cancelled.");
|
||||||
|
|
||||||
|
if (!await ctx.Repository.TryUpdateSystemConfigForIdChange(ctx.System.Id))
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
|
||||||
|
await ctx.Repository.CreateHidChangelog(ctx.System.Id, ctx.Message.Author.Id, "member", target.Hid, newHid);
|
||||||
|
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { Hid = newHid });
|
||||||
|
|
||||||
|
var newConfig = await ctx.Repository.GetSystemConfig(ctx.System.Id);
|
||||||
|
await ctx.Reply($"{Emojis.Success} Member ID changed to `{newHid.DisplayHid(ctx.Config)}`. You have **{newConfig.PremiumIdChangesRemaining}** ID changes remaining.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Name(Context ctx, PKMember target)
|
public async Task Name(Context ctx, PKMember target)
|
||||||
{
|
{
|
||||||
var format = ctx.MatchFormat();
|
var format = ctx.MatchFormat();
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,57 @@ public class Misc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Reply(message + $"\n\nManage your subscription at <{_botConfig.PremiumDashboardUrl}>");
|
List<MessageComponent> components = [
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = message,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ctx.Premium)
|
||||||
|
{
|
||||||
|
var hidChangesLeftToday = Limits.PremiumDailyHidChanges - await ctx.Repository.GetHidChangelogCountForDate(ctx.System.Id, SystemClock.Instance.GetCurrentInstant().InUtc().Date);
|
||||||
|
var limitMessage = $"You have **{ctx.Config.PremiumIdChangesRemaining}** ID changes available, of which you can use **{hidChangesLeftToday}** today.";
|
||||||
|
|
||||||
|
components.Add(new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Separator,
|
||||||
|
});
|
||||||
|
components.Add(new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = limitMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Reply(components: [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Container,
|
||||||
|
Components = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = $"## {(_botConfig.PremiumSubscriberEmoji != null ? $"<:premium_subscriber:{_botConfig.PremiumSubscriberEmoji}>" : "\u2729")} PluralKit Premium",
|
||||||
|
},
|
||||||
|
..components,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.ActionRow,
|
||||||
|
Components = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Button,
|
||||||
|
Style = ButtonStyle.Link,
|
||||||
|
Label = "Manage your subscription",
|
||||||
|
Url = _botConfig.PremiumDashboardUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invite(Context ctx)
|
public async Task Invite(Context ctx)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ using Myriad.Types;
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
using SqlKata.Compilers;
|
using SqlKata.Compilers;
|
||||||
|
|
||||||
|
|
@ -20,13 +22,47 @@ public class SystemEdit
|
||||||
private readonly DataFileService _dataFiles;
|
private readonly DataFileService _dataFiles;
|
||||||
private readonly PrivateChannelService _dmCache;
|
private readonly PrivateChannelService _dmCache;
|
||||||
private readonly AvatarHostingService _avatarHosting;
|
private readonly AvatarHostingService _avatarHosting;
|
||||||
|
private readonly BotConfig _botConfig;
|
||||||
|
|
||||||
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache, AvatarHostingService avatarHosting)
|
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache, AvatarHostingService avatarHosting, BotConfig botConfig)
|
||||||
{
|
{
|
||||||
_dataFiles = dataFiles;
|
_dataFiles = dataFiles;
|
||||||
_client = client;
|
_client = client;
|
||||||
_dmCache = dmCache;
|
_dmCache = dmCache;
|
||||||
_avatarHosting = avatarHosting;
|
_avatarHosting = avatarHosting;
|
||||||
|
_botConfig = botConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangeId(Context ctx, PKSystem target)
|
||||||
|
{
|
||||||
|
ctx.CheckSystem().CheckOwnSystem(target);
|
||||||
|
if (!ctx.Premium)
|
||||||
|
throw Errors.PremiumExclusiveCommand();
|
||||||
|
|
||||||
|
var input = ctx.PopArgument();
|
||||||
|
if (!input.TryParseHid(out var newHid))
|
||||||
|
throw new PKError($"Invalid new system ID `{input}`.");
|
||||||
|
|
||||||
|
var existingSystem = await ctx.Repository.GetSystemByHid(newHid);
|
||||||
|
if (existingSystem != null)
|
||||||
|
throw new PKError($"Another system already exists with ID `{newHid.DisplayHid(ctx.Config)}`.");
|
||||||
|
|
||||||
|
if (ctx.Config.PremiumIdChangesRemaining < 1)
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
if ((await ctx.Repository.GetHidChangelogCountForDate(target.Id, SystemClock.Instance.GetCurrentInstant().InUtc().Date)) >= Limits.PremiumDailyHidChanges)
|
||||||
|
throw new PKError($"You have already changed {Limits.PremiumDailyHidChanges} IDs today. Please try again tomorrow.");
|
||||||
|
|
||||||
|
if (!await ctx.PromptYesNo($"Change your system ID to `{newHid.DisplayHid(ctx.Config)}`?", "Change"))
|
||||||
|
throw new PKError("ID change cancelled.");
|
||||||
|
|
||||||
|
if (!await ctx.Repository.TryUpdateSystemConfigForIdChange(target.Id))
|
||||||
|
throw new PKError("You do not have enough available ID changes to do this.");
|
||||||
|
|
||||||
|
await ctx.Repository.CreateHidChangelog(target.Id, ctx.Message.Author.Id, "system", target.Hid, newHid);
|
||||||
|
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Hid = newHid });
|
||||||
|
|
||||||
|
var newConfig = await ctx.Repository.GetSystemConfig(target.Id);
|
||||||
|
await ctx.Reply($"{Emojis.Success} System ID changed to `{newHid.DisplayHid(ctx.Config)}`. You have **{newConfig.PremiumIdChangesRemaining}** ID changes remaining.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Name(Context ctx, PKSystem target)
|
public async Task Name(Context ctx, PKSystem target)
|
||||||
|
|
|
||||||
|
|
@ -185,4 +185,6 @@ public static class Errors
|
||||||
new($"Channel \"{channelString}\" not found or is not in this server.");
|
new($"Channel \"{channelString}\" not found or is not in this server.");
|
||||||
|
|
||||||
public static PKError InteractionWrongAccount(ulong user) => new($"This prompt is only available for <@{user}>");
|
public static PKError InteractionWrongAccount(ulong user) => new($"This prompt is only available for <@{user}>");
|
||||||
|
|
||||||
|
public static PKError PremiumExclusiveCommand() => new("This command is only available for PluralKit Premium subscribers.");
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ public static class ModelUtils
|
||||||
public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList);
|
public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList);
|
||||||
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad);
|
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad);
|
||||||
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad);
|
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad);
|
||||||
|
public static string DisplayHid(this string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(hid, cfg, isList, shouldPad);
|
||||||
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) =>
|
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) =>
|
||||||
HidUtils.HidTransform(
|
HidUtils.HidTransform(
|
||||||
hid,
|
hid,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
using SqlKata;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
public partial class ModelRepository
|
||||||
|
{
|
||||||
|
public Task<HidChangelog?> GetHidChangelogById(int id)
|
||||||
|
{
|
||||||
|
var query = new Query("hid_changelog").Where("id", id);
|
||||||
|
return _db.QueryFirst<HidChangelog?>(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HidChangelog> CreateHidChangelog(SystemId system, ulong discord_uid, string hid_type, string hid_old, string hid_new, IPKConnection? conn = null)
|
||||||
|
{
|
||||||
|
var query = new Query("hid_changelog").AsInsert(new { system, discord_uid, hid_type, hid_old, hid_new, });
|
||||||
|
var changelog = await _db.QueryFirst<HidChangelog>(conn, query, "returning *");
|
||||||
|
_logger.Information("Created HidChangelog {HidChangelogId} for system {SystemId}: {HidType} {OldHid} -> {NewHid}", changelog.Id, system, hid_type, hid_old, hid_new);
|
||||||
|
return changelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> GetHidChangelogCountForDate(SystemId system, LocalDate date)
|
||||||
|
{
|
||||||
|
var query = new Query("hid_changelog")
|
||||||
|
.SelectRaw("count(*)")
|
||||||
|
.Where("system", system)
|
||||||
|
.WhereDate("created", date);
|
||||||
|
|
||||||
|
return _db.QueryFirst<int>(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using SqlKata;
|
using SqlKata;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
namespace PluralKit.Core;
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
|
@ -20,4 +21,27 @@ public partial class ModelRepository
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TryUpdateSystemConfigForIdChange(SystemId system, IPKConnection conn = null)
|
||||||
|
{
|
||||||
|
var query = new Query("system_config")
|
||||||
|
.AsUpdate(new
|
||||||
|
{
|
||||||
|
premium_id_changes_remaining = new UnsafeLiteral("premium_id_changes_remaining - 1")
|
||||||
|
})
|
||||||
|
.Where("system", system);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _db.ExecuteQuery(conn, query);
|
||||||
|
}
|
||||||
|
catch (PostgresException pe)
|
||||||
|
{
|
||||||
|
if (!pe.Message.Contains("violates check constraint"))
|
||||||
|
throw;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
15
PluralKit.Core/Models/HidChangelog.cs
Normal file
15
PluralKit.Core/Models/HidChangelog.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
public class HidChangelog
|
||||||
|
{
|
||||||
|
public int Id { get; }
|
||||||
|
public SystemId System { get; }
|
||||||
|
public ulong DiscordUid { get; }
|
||||||
|
public string HidType { get; }
|
||||||
|
public string HidOld { get; }
|
||||||
|
public string HidNew { get; }
|
||||||
|
public Instant Created { get; }
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ public class SystemConfigPatch: PatchObject
|
||||||
public Partial<SystemConfig.ProxySwitchAction> ProxySwitch { get; set; }
|
public Partial<SystemConfig.ProxySwitchAction> ProxySwitch { get; set; }
|
||||||
public Partial<bool> PremiumLifetime { get; set; }
|
public Partial<bool> PremiumLifetime { get; set; }
|
||||||
public Partial<Instant?> PremiumUntil { get; set; }
|
public Partial<Instant?> PremiumUntil { get; set; }
|
||||||
|
public Partial<int?> PremiumIdChangesRemaining { get; set; }
|
||||||
|
|
||||||
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
|
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
|
||||||
.With("ui_tz", UiTz)
|
.With("ui_tz", UiTz)
|
||||||
|
|
@ -49,6 +50,7 @@ public class SystemConfigPatch: PatchObject
|
||||||
.With("name_format", NameFormat)
|
.With("name_format", NameFormat)
|
||||||
.With("premium_lifetime", PremiumLifetime)
|
.With("premium_lifetime", PremiumLifetime)
|
||||||
.With("premium_until", PremiumUntil)
|
.With("premium_until", PremiumUntil)
|
||||||
|
.With("premium_id_changes_remaining", PremiumIdChangesRemaining)
|
||||||
);
|
);
|
||||||
|
|
||||||
public new void AssertIsValid()
|
public new void AssertIsValid()
|
||||||
|
|
@ -128,6 +130,9 @@ public class SystemConfigPatch: PatchObject
|
||||||
if (PremiumUntil.IsPresent)
|
if (PremiumUntil.IsPresent)
|
||||||
o.Add("premium_until", PremiumUntil.Value?.FormatExport());
|
o.Add("premium_until", PremiumUntil.Value?.FormatExport());
|
||||||
|
|
||||||
|
if (PremiumIdChangesRemaining.IsPresent)
|
||||||
|
o.Add("premium_id_changes_remaining", PremiumIdChangesRemaining.Value);
|
||||||
|
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public class SystemConfig
|
||||||
|
|
||||||
public bool PremiumLifetime { get; }
|
public bool PremiumLifetime { get; }
|
||||||
public Instant? PremiumUntil { get; }
|
public Instant? PremiumUntil { get; }
|
||||||
|
public int? PremiumIdChangesRemaining { get; }
|
||||||
|
|
||||||
public enum HidPadFormat
|
public enum HidPadFormat
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,6 @@ public static class Limits
|
||||||
|
|
||||||
public static readonly long AvatarFileSizeLimit = 1024 * 1024;
|
public static readonly long AvatarFileSizeLimit = 1024 * 1024;
|
||||||
public static readonly int AvatarDimensionLimit = 1000;
|
public static readonly int AvatarDimensionLimit = 1000;
|
||||||
|
|
||||||
|
public static readonly int PremiumDailyHidChanges = 3;
|
||||||
}
|
}
|
||||||
20
crates/migrate/data/migrations/55.sql
Normal file
20
crates/migrate/data/migrations/55.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- database version 55
|
||||||
|
-- add premium ID change allowances
|
||||||
|
|
||||||
|
alter table system_config
|
||||||
|
add column premium_id_changes_remaining int not null default 0,
|
||||||
|
add constraint premium_id_changes_nonzero check (premium_id_changes_remaining >= 0);
|
||||||
|
|
||||||
|
create table hid_changelog (
|
||||||
|
id serial primary key,
|
||||||
|
system integer references systems (id) on delete set null,
|
||||||
|
discord_uid bigint not null,
|
||||||
|
hid_type text not null,
|
||||||
|
hid_old char(6) not null,
|
||||||
|
hid_new char(6) not null,
|
||||||
|
created timestamp not null default (current_timestamp at time zone 'utc')
|
||||||
|
);
|
||||||
|
|
||||||
|
create index hid_changelog_system_idx on hid_changelog (system);
|
||||||
|
|
||||||
|
update info set schema_version = 55;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue