diff --git a/PluralKit.API/Controllers/v1/MemberController.cs b/PluralKit.API/Controllers/v1/MemberController.cs index e40d73f8..6800e8bc 100644 --- a/PluralKit.API/Controllers/v1/MemberController.cs +++ b/PluralKit.API/Controllers/v1/MemberController.cs @@ -42,14 +42,14 @@ public class MemberController: ControllerBase return BadRequest("Member name must be specified."); var systemId = User.CurrentSystem(); - var systemData = await _repo.GetSystem(systemId); + var config = await _repo.GetSystemConfig(systemId); await using var conn = await _db.Obtain(); // Enforce per-system member limit var memberCount = await conn.QuerySingleAsync("select count(*) from members where system = @System", new { System = systemId }); - var memberLimit = systemData?.MemberLimitOverride ?? Limits.MaxMemberCount; + var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) return BadRequest($"Member limit reached ({memberCount} / {memberLimit})."); diff --git a/PluralKit.API/Controllers/v2/GroupControllerV2.cs b/PluralKit.API/Controllers/v2/GroupControllerV2.cs index 91ccafaf..e4c27259 100644 --- a/PluralKit.API/Controllers/v2/GroupControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupControllerV2.cs @@ -54,10 +54,11 @@ public class GroupControllerV2: PKControllerBase public async Task GroupCreate([FromBody] JObject data) { var system = await ResolveSystem("@me"); + var config = await _repo.GetSystemConfig(system.Id); // Check group cap var existingGroupCount = await _repo.GetSystemGroupCount(system.Id); - var groupLimit = system.GroupLimitOverride ?? Limits.MaxGroupCount; + var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; if (existingGroupCount >= groupLimit) throw Errors.GroupLimitReached; diff --git a/PluralKit.API/Controllers/v2/MemberControllerV2.cs b/PluralKit.API/Controllers/v2/MemberControllerV2.cs index 75a5cd96..56d9d175 100644 --- a/PluralKit.API/Controllers/v2/MemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/MemberControllerV2.cs @@ -37,9 +37,10 @@ public class MemberControllerV2: PKControllerBase public async Task MemberCreate([FromBody] JObject data) { var system = await ResolveSystem("@me"); + var config = await _repo.GetSystemConfig(system.Id); var memberCount = await _repo.GetSystemMemberCount(system.Id); - var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount; + var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) throw Errors.MemberLimitReached; diff --git a/PluralKit.API/Controllers/v2/SystemControllerV2.cs b/PluralKit.API/Controllers/v2/SystemControllerV2.cs index 2c16661f..ca0f54b5 100644 --- a/PluralKit.API/Controllers/v2/SystemControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SystemControllerV2.cs @@ -34,4 +34,32 @@ public class SystemControllerV2: PKControllerBase var newSystem = await _repo.UpdateSystem(system.Id, patch); return Ok(newSystem.ToJson(LookupContext.ByOwner, APIVersion.V2)); } + + [HttpGet("{systemRef}/settings")] + public async Task GetSystemSettings(string systemRef) + { + var system = await ResolveSystem(systemRef); + if (ContextFor(system) != LookupContext.ByOwner) + throw Errors.GenericMissingPermissions; + + var config = await _repo.GetSystemConfig(system.Id); + return Ok(config.ToJson()); + } + + [HttpPatch("{systemRef}/settings")] + public async Task DoSystemSettingsPatch(string systemRef, [FromBody] JObject data) + { + var system = await ResolveSystem(systemRef); + if (ContextFor(system) != LookupContext.ByOwner) + throw Errors.GenericMissingPermissions; + + var patch = SystemConfigPatch.FromJson(data); + + patch.AssertIsValid(); + if (patch.Errors.Count > 0) + throw new ModelParseError(patch.Errors); + + var newConfig = await _repo.UpdateSystemConfig(system.Id, patch); + return Ok(newConfig.ToJson()); + } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 1c9717f2..0a412099 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -12,18 +12,18 @@ public partial class CommandTree public static Command SystemAvatar = new Command("system icon", "system icon [url|@mention]", "Changes your system's icon"); public static Command SystemBannerImage = new Command("system banner", "system banner [url]", "Set the system's banner image"); public static Command SystemDelete = new Command("system delete", "system delete", "Deletes your system"); - public static Command SystemTimezone = new Command("system timezone", "system timezone [timezone]", "Changes your system's time zone"); public static Command SystemProxy = new Command("system proxy", "system proxy [server id] [on|off]", "Enables or disables message proxying in a specific server"); public static Command SystemList = new Command("system list", "system [system] list [full]", "Lists a system's members"); public static Command SystemFind = new Command("system find", "system [system] find [full] ", "Searches a system's members given a search term"); public static Command SystemFronter = new Command("system fronter", "system [system] fronter", "Shows a system's fronter(s)"); 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 SystemPing = new Command("system ping", "system ping ", "Changes your system's ping preferences"); public static Command SystemPrivacy = new Command("system privacy", "system privacy ", "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 ConfigPing = new Command("config ping", "config ping ", "Changes your system's ping preferences"); + public static Command ConfigAutoproxyAccount = new Command("config autoproxy account", "autoproxy account [on|off]", "Toggles autoproxy globally for the current account"); + public static Command ConfigAutoproxyTimeout = new Command("config autoproxy timeout", "autoproxy timeout [|off|reset]", "Sets the latch timeout duration for your system"); public static Command AutoproxySet = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for the current server"); - public static Command AutoproxyTimeout = new Command("autoproxy", "autoproxy timeout [|off|reset]", "Sets the latch timeout duration for your system"); - public static Command AutoproxyAccount = new Command("autoproxy", "autoproxy account [on|off]", "Toggles autoproxy globally for the current account"); public static Command MemberInfo = new Command("member", "member ", "Looks up information about a member"); public static Command MemberNew = new Command("member new", "member new ", "Creates a new member"); public static Command MemberRename = new Command("member rename", "member rename ", "Renames a member"); @@ -95,8 +95,7 @@ public partial class CommandTree public static Command[] SystemCommands = { SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemBannerImage, SystemColor, - SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, - SystemPrivacy, SystemProxy + SystemDelete, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy, SystemProxy }; public static Command[] MemberCommands = @@ -124,7 +123,10 @@ public partial class CommandTree Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll }; - public static Command[] AutoproxyCommands = { AutoproxySet, AutoproxyTimeout, AutoproxyAccount }; + public static Command[] ConfigCommands = + { + ConfigTimezone, ConfigPing, ConfigAutoproxyAccount, ConfigAutoproxyTimeout + }; public static Command[] LogCommands = { LogChannel, LogChannelClear, LogEnable, LogDisable }; diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 6bca067a..215ec25e 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -20,6 +20,8 @@ public partial class CommandTree return CommandHelpRoot(ctx); if (ctx.Match("ap", "autoproxy", "auto")) return HandleAutoproxyCommand(ctx); + if (ctx.Match("config", "cfg")) + return HandleConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); if (ctx.Match("link")) @@ -167,7 +169,7 @@ public partial class CommandTree else if (ctx.Match("webhook", "hook")) await ctx.Execute(null, m => m.SystemWebhook(ctx)); else if (ctx.Match("timezone", "tz")) - await ctx.Execute(SystemTimezone, m => m.SystemTimezone(ctx)); + await ctx.Execute(ConfigTimezone, m => m.SystemTimezone(ctx), true); else if (ctx.Match("proxy")) await ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); else if (ctx.Match("list", "l", "members")) @@ -190,7 +192,7 @@ public partial class CommandTree else if (ctx.Match("privacy")) await ctx.Execute(SystemPrivacy, m => m.SystemPrivacy(ctx)); else if (ctx.Match("ping")) - await ctx.Execute(SystemPing, m => m.SystemPing(ctx)); + await ctx.Execute(ConfigPing, m => m.SystemPing(ctx), true); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "systems", SystemCommands); else if (ctx.Match("groups", "gs", "g")) @@ -206,8 +208,7 @@ public partial class CommandTree if (target == null) { var list = CreatePotentialCommandList(SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, - SystemAvatar, SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, - SystemFrontPercent); + SystemAvatar, SystemDelete, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent); await ctx.Reply( $"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\nPerhaps you meant to use one of the following commands?\n{list}"); } @@ -435,9 +436,9 @@ public partial class CommandTree case "bl": await PrintCommandList(ctx, "channel blacklisting", BlacklistCommands); break; - case "autoproxy": - case "ap": - await PrintCommandList(ctx, "autoproxy", AutoproxyCommands); + case "config": + case "cfg": + await PrintCommandList(ctx, "settings", ConfigCommands); break; // todo: are there any commands that still need to be added? default: @@ -448,22 +449,43 @@ public partial class CommandTree private Task HandleAutoproxyCommand(Context ctx) { - if (ctx.Match("commands")) - return PrintCommandList(ctx, "autoproxy", AutoproxyCommands); - // ctx.CheckSystem(); // oops, that breaks stuff! PKErrors before ctx.Execute don't actually do anything. // so we just emulate checking and throwing an error. if (ctx.System == null) return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError.Message}"); + // todo: move this whole block to Autoproxy.cs when these are removed + if (ctx.Match("account", "ac")) - return ctx.Execute(AutoproxyAccount, m => m.AutoproxyAccount(ctx)); + return ctx.Execute(ConfigAutoproxyAccount, m => m.AutoproxyAccount(ctx), true); if (ctx.Match("timeout", "tm")) - return ctx.Execute(AutoproxyTimeout, m => m.AutoproxyTimeout(ctx)); + return ctx.Execute(ConfigAutoproxyTimeout, m => m.AutoproxyTimeout(ctx), true); + return ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx)); } + private Task HandleConfigCommand(Context ctx) + { + if (ctx.System == null) + return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError.Message}"); + + if (!ctx.HasNext()) + return ctx.Execute(null, m => m.ShowConfig(ctx)); + + if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "account", "ac" })) + return ctx.Execute(null, m => m.AutoproxyAccount(ctx)); + if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "timeout", "tm" })) + return ctx.Execute(null, m => m.AutoproxyTimeout(ctx)); + if (ctx.Match("timezone", "zone", "tz")) + return ctx.Execute(null, m => m.SystemTimezone(ctx)); + if (ctx.Match("ping")) + return ctx.Execute(null, m => m.SystemPing(ctx)); + + // todo: maybe add the list of configuration keys here? + return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings."); + } + private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands) { var commandListStr = CreatePotentialCommandList(potentialCommands); diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 30e135ec..d0c7bd7a 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -5,6 +5,8 @@ using App.Metrics; using Autofac; +using NodaTime; + using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; @@ -27,13 +29,14 @@ public class Context private Command? _currentCommand; public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, - PKSystem senderSystem, MessageContext messageContext) + PKSystem senderSystem, SystemConfig config, MessageContext messageContext) { Message = (Message)message; Shard = shard; Guild = guild; Channel = channel; System = senderSystem; + Config = config; MessageContext = messageContext; Cache = provider.Resolve(); Database = provider.Resolve(); @@ -64,6 +67,8 @@ public class Context public readonly PKSystem System; + public readonly SystemConfig Config; + public DateTimeZone Zone => Config?.Zone ?? DateTimeZone.Utc; public readonly Parameters Parameters; @@ -99,7 +104,7 @@ public class Context return msg; } - public async Task Execute(Command? commandDef, Func handler) + public async Task Execute(Command? commandDef, Func handler, bool deprecated = false) { _currentCommand = commandDef; @@ -123,6 +128,9 @@ public class Context // Got a complaint the old error was a bit too patronizing. Hopefully this is better? await Reply($"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?"); } + + if (deprecated && commandDef != null) + await Reply($"{Emojis.Warn} This command is deprecated and will be removed soon. In the future, please use `pk;{commandDef.Key}`."); } public LookupContext LookupContextFor(PKSystem target) => diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 5933fbe5..aca3485e 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -48,6 +48,23 @@ public static class ContextArgumentsExt return ctx.Match(ref used, potentialMatches); } + /// + /// Matches the next *n* parameters against each parameter consecutively. + ///
+ /// Note that this is handled differently than single-parameter Match: + /// each method parameter is an array of potential matches for the *n*th command string parameter. + ///
+ public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches) + { + foreach (var param in potentialParametersMatches) + if (!ctx.Match(param)) return false; + + for (var i = 0; i < potentialParametersMatches.Length; i++) + ctx.PopArgument(); + + return true; + } + public static bool MatchFlag(this Context ctx, params string[] potentialMatches) { // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index cdfdd24a..15a2c4d2 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -97,7 +97,9 @@ public class Admin if (target == null) throw new PKError("Unknown system."); - var currentLimit = target.MemberLimitOverride ?? Limits.MaxMemberCount; + var config = await _repo.GetSystemConfig(target.Id); + + var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (!ctx.HasNext()) { await ctx.Reply($"Current member limit is **{currentLimit}** members."); @@ -111,7 +113,7 @@ public class Admin if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Member limit change cancelled."); - await _repo.UpdateSystem(target.Id, new SystemPatch { MemberLimitOverride = newLimit }); + await _repo.UpdateSystemConfig(target.Id, new SystemConfigPatch { MemberLimitOverride = newLimit }); await ctx.Reply($"{Emojis.Success} Member limit updated."); } @@ -123,7 +125,9 @@ public class Admin if (target == null) throw new PKError("Unknown system."); - var currentLimit = target.GroupLimitOverride ?? Limits.MaxGroupCount; + var config = await _repo.GetSystemConfig(target.Id); + + var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; if (!ctx.HasNext()) { await ctx.Reply($"Current group limit is **{currentLimit}** groups."); @@ -137,7 +141,7 @@ public class Admin if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Group limit change cancelled."); - await _repo.UpdateSystem(target.Id, new SystemPatch { GroupLimitOverride = newLimit }); + await _repo.UpdateSystemConfig(target.Id, new SystemConfigPatch { GroupLimitOverride = newLimit }); await ctx.Reply($"{Emojis.Success} Group limit updated."); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 43069cba..07efabfd 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -143,102 +143,6 @@ public class Autoproxy return eb.Build(); } - public async Task AutoproxyTimeout(Context ctx) - { - if (!ctx.HasNext()) - { - var timeout = ctx.System.LatchTimeout.HasValue - ? Duration.FromSeconds(ctx.System.LatchTimeout.Value) - : (Duration?)null; - - if (timeout == null) - await ctx.Reply( - $"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}."); - else if (timeout == Duration.Zero) - await ctx.Reply( - "Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out."); - else - await ctx.Reply( - $"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); - return; - } - - Duration? newTimeout; - var overflow = Duration.Zero; - if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) - { - newTimeout = Duration.Zero; - } - else if (ctx.Match("reset", "default")) - { - newTimeout = null; - } - else - { - var timeoutStr = ctx.RemainderOrNull(); - var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); - if (timeoutPeriod == null) - throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); - if (timeoutPeriod.Value.TotalHours > 100000) - { - // sanity check to prevent seconds overflow if someone types in 999999999 - overflow = timeoutPeriod.Value; - newTimeout = Duration.Zero; - } - else - { - newTimeout = timeoutPeriod; - } - } - - await _repo.UpdateSystem(ctx.System.Id, new SystemPatch { LatchTimeout = (int?)newTimeout?.TotalSeconds }); - - if (newTimeout == null) - await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); - else if (newTimeout == Duration.Zero && overflow != Duration.Zero) - await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); - else if (newTimeout == Duration.Zero) - await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); - else - await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); - } - - public async Task AutoproxyAccount(Context ctx) - { - // todo: this might be useful elsewhere, consider moving it to ctx.MatchToggle - if (ctx.Match("enable", "on")) - { - await AutoproxyEnableDisable(ctx, true); - } - else if (ctx.Match("disable", "off")) - { - await AutoproxyEnableDisable(ctx, false); - } - else if (ctx.HasNext()) - { - throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); - } - else - { - var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; - await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>."); - } - } - - private async Task AutoproxyEnableDisable(Context ctx, bool allow) - { - var statusString = allow ? "enabled" : "disabled"; - if (ctx.MessageContext.AllowAutoproxy == allow) - { - await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); - return; - } - - var patch = new AccountPatch { AllowAutoproxy = allow }; - await _repo.UpdateAccount(ctx.Author.Id, patch); - await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); - } - private async Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) { await _repo.GetSystemGuild(ctx.Guild.Id, ctx.System.Id); diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs new file mode 100644 index 00000000..3c6189ef --- /dev/null +++ b/PluralKit.Bot/Commands/Config.cs @@ -0,0 +1,301 @@ +using System.Text; + +using Humanizer; + +using NodaTime; +using NodaTime.Text; +using NodaTime.TimeZones; + +using PluralKit.Core; + +namespace PluralKit.Bot; +public class Config +{ + private readonly ModelRepository _repo; + + public Config(ModelRepository repo) + { + _repo = repo; + } + + private record PaginatedConfigItem(string Key, string Description, string? CurrentValue, string DefaultValue); + + public async Task ShowConfig(Context ctx) + { + var items = new List(); + + items.Add(new( + "autoproxy account", + "Whether autoproxy is enabled for the current account", + EnabledDisabled(ctx.MessageContext.AllowAutoproxy), + "enabled" + )); + + items.Add(new( + "autoproxy timeout", + "If this is set, latch-mode autoproxy will not keep autoproxying after this amount of time has elapsed since the last message sent in the server", + ctx.Config.LatchTimeout.HasValue + ? ( + ctx.Config.LatchTimeout.Value != 0 + ? Duration.FromSeconds(ctx.Config.LatchTimeout.Value).ToTimeSpan().Humanize(4) + : "disabled" + ) + : null, + ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4) + )); + + items.Add(new( + "timezone", + "The system's time zone - shows timestamps in your local time", + ctx.Config.UiTz, + "UTC" + )); + + items.Add(new( + "ping", + $"Whether other users are able to mention you via a {Emojis.Bell} reaction", + EnabledDisabled(ctx.Config.PingsEnabled), + "enabled" + )); + + items.Add(new( + "Member limit", + "The maximum number of registered members for your system", + ctx.Config.MemberLimitOverride?.ToString(), + Limits.MaxMemberCount.ToString() + )); + + items.Add(new( + "Group limit", + "The maximum number of registered groups for your system", + ctx.Config.GroupLimitOverride?.ToString(), + Limits.MaxGroupCount.ToString() + )); + + await ctx.Paginate( + items.ToAsyncEnumerable(), + items.Count, + 10, + "Current settings for your system", + ctx.System.Color, + (eb, l) => + { + var description = new StringBuilder(); + + foreach (var item in l) + { + description.Append(item.Key.AsCode()); + description.Append($" **({item.CurrentValue ?? item.DefaultValue})**"); + if (item.CurrentValue != null && item.CurrentValue != item.DefaultValue) + description.Append("\ud83d\udd39"); + + description.AppendLine(); + description.Append(item.Description); + description.AppendLine(); + description.AppendLine(); + } + + eb.Description(description.ToString()); + + return Task.CompletedTask; + } + ); + } + + public async Task AutoproxyAccount(Context ctx) + { + // todo: this might be useful elsewhere, consider moving it to ctx.MatchToggle + if (ctx.Match("enable", "on")) + await AutoproxyEnableDisable(ctx, true); + else if (ctx.Match("disable", "off")) + await AutoproxyEnableDisable(ctx, false); + else if (ctx.HasNext()) + throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); + else + { + var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; + await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>."); + } + } + + private string EnabledDisabled(bool value) => value ? "enabled" : "disabled"; + + private async Task AutoproxyEnableDisable(Context ctx, bool allow) + { + var statusString = EnabledDisabled(allow); + if (ctx.MessageContext.AllowAutoproxy == allow) + { + await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); + return; + } + var patch = new AccountPatch { AllowAutoproxy = allow }; + await _repo.UpdateAccount(ctx.Author.Id, patch); + await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); + } + + + public async Task AutoproxyTimeout(Context ctx) + { + if (!ctx.HasNext()) + { + var timeout = ctx.Config.LatchTimeout.HasValue + ? Duration.FromSeconds(ctx.Config.LatchTimeout.Value) + : (Duration?)null; + + if (timeout == null) + await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}."); + else if (timeout == Duration.Zero) + await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out."); + else + await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); + return; + } + + Duration? newTimeout; + Duration overflow = Duration.Zero; + if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeout = Duration.Zero; + else if (ctx.Match("reset", "default")) newTimeout = null; + else + { + var timeoutStr = ctx.RemainderOrNull(); + var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); + if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); + if (timeoutPeriod.Value.TotalHours > 100000) + { + // sanity check to prevent seconds overflow if someone types in 999999999 + overflow = timeoutPeriod.Value; + newTimeout = Duration.Zero; + } + else newTimeout = timeoutPeriod; + } + + await _repo.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout?.TotalSeconds }); + + if (newTimeout == null) + await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); + else if (newTimeout == Duration.Zero && overflow != Duration.Zero) + await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); + else if (newTimeout == Duration.Zero) + await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); + else + await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); + } + + public async Task SystemPing(Context ctx) + { + ctx.CheckSystem(); + + if (!ctx.HasNext()) + { + if (ctx.Config.PingsEnabled) { await ctx.Reply("Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`."); } + else { await ctx.Reply("Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`."); } + } + else + { + if (ctx.Match("on", "enable")) + { + await _repo.UpdateSystemConfig(ctx.System.Id, new() { PingsEnabled = true }); + + await ctx.Reply("Reaction pings have now been enabled."); + } + if (ctx.Match("off", "disable")) + { + await _repo.UpdateSystemConfig(ctx.System.Id, new() { PingsEnabled = false }); + + await ctx.Reply("Reaction pings have now been disabled."); + } + } + } + + public async Task SystemTimezone(Context ctx) + { + if (ctx.System == null) throw Errors.NoSystemError; + + if (await ctx.MatchClear()) + { + await _repo.UpdateSystemConfig(ctx.System.Id, new() { UiTz = "UTC" }); + + await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC)."); + return; + } + + var zoneStr = ctx.RemainderOrNull(); + if (zoneStr == null) + { + await ctx.Reply( + $"Your current system time zone is set to **{ctx.Config.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.Config.Zone)}** in that time zone. To change your system time zone, type `pk;s tz `."); + return; + } + + var zone = await FindTimeZone(ctx, zoneStr); + if (zone == null) throw Errors.InvalidTimeZone(zoneStr); + + var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone); + var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?"; + if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled; + + await _repo.UpdateSystemConfig(ctx.System.Id, new() { UiTz = zone.Id }); + + await ctx.Reply($"System time zone changed to **{zone.Id}**."); + } + + + private async Task FindTimeZone(Context ctx, string zoneStr) + { + // First, if we're given a flag emoji, we extract the flag emoji code from it. + zoneStr = Core.StringUtils.ExtractCountryFlag(zoneStr) ?? zoneStr; + + // Then, we find all *locations* matching either the given country code or the country name. + var locations = TzdbDateTimeZoneSource.Default.Zone1970Locations; + var matchingLocations = locations.Where(l => l.Countries.Any(c => + string.Equals(c.Code, zoneStr, StringComparison.InvariantCultureIgnoreCase) || + string.Equals(c.Name, zoneStr, StringComparison.InvariantCultureIgnoreCase))); + + // Then, we find all (unique) time zone IDs that match. + var matchingZones = matchingLocations.Select(l => DateTimeZoneProviders.Tzdb.GetZoneOrNull(l.ZoneId)) + .Distinct().ToList(); + + // If the set of matching zones is empty (ie. we didn't find anything), we try a few other things. + if (matchingZones.Count == 0) + { + // First, we try to just find the time zone given directly and return that. + var givenZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(zoneStr); + if (givenZone != null) return givenZone; + + // If we didn't find anything there either, we try parsing the string as an offset, then + // find all possible zones that match that offset. For an offset like UTC+2, this doesn't *quite* + // work, since there are 57(!) matching zones (as of 2019-06-13) - but for less populated time zones + // this could work nicely. + var inputWithoutUtc = zoneStr.Replace("UTC", "").Replace("GMT", ""); + + var res = OffsetPattern.CreateWithInvariantCulture("+H").Parse(inputWithoutUtc); + if (!res.Success) res = OffsetPattern.CreateWithInvariantCulture("+H:mm").Parse(inputWithoutUtc); + + // If *this* didn't parse correctly, fuck it, bail. + if (!res.Success) return null; + var offset = res.Value; + + // To try to reduce the count, we go by locations from the 1970+ database instead of just the full database + // This elides regions that have been identical since 1970, omitting small distinctions due to Ancient History(tm). + var allZones = TzdbDateTimeZoneSource.Default.Zone1970Locations.Select(l => l.ZoneId).Distinct(); + matchingZones = allZones.Select(z => DateTimeZoneProviders.Tzdb.GetZoneOrNull(z)) + .Where(z => z.GetUtcOffset(SystemClock.Instance.GetCurrentInstant()) == offset).ToList(); + } + + // If we have a list of viable time zones, we ask the user which is correct. + + // If we only have one, return that one. + if (matchingZones.Count == 1) + return matchingZones.First(); + + // Otherwise, prompt and return! + return await ctx.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones, + z => + { + if (TzdbDateTimeZoneSource.Default.Aliases.Contains(z.Id)) + return $"**{z.Id}**, {string.Join(", ", TzdbDateTimeZoneSource.Default.Aliases[z.Id])}"; + + return $"**{z.Id}**"; + }); + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 58686c85..8a190f85 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -49,7 +49,7 @@ public class Groups // Check group cap var existingGroupCount = await _repo.GetSystemGroupCount(ctx.System.Id); - var groupLimit = ctx.System.GroupLimitOverride ?? Limits.MaxGroupCount; + var groupLimit = ctx.Config.GroupLimitOverride ?? Limits.MaxGroupCount; if (existingGroupCount >= groupLimit) throw new PKError( $"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones."); @@ -574,7 +574,7 @@ public class Groups var now = SystemClock.Instance.GetCurrentInstant(); - var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone); + var rangeStart = DateUtils.ParseDateTime(durationStr, true, ctx.Zone); if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; @@ -589,7 +589,7 @@ public class Groups var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, targetSystem.Id, target.Id, rangeStart.Value.ToInstant(), now)); await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, targetSystem, target, - targetSystem.Zone, ctx.LookupContextFor(targetSystem), title.ToString(), ignoreNoFronters, showFlat)); + ctx.Zone, ctx.LookupContextFor(targetSystem), title.ToString(), ignoreNoFronters, showFlat)); } private async Task GetGroupSystem(Context ctx, PKGroup target) diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 3981899f..3f3927e1 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -100,7 +100,7 @@ public class ImportExport var json = await ctx.BusyIndicator(async () => { // Make the actual data file - var data = await _dataFiles.ExportSystem(ctx.System); + var data = await _dataFiles.ExportSystem(ctx.System, ctx.Config.UiTz); return JsonConvert.SerializeObject(data, Formatting.None); }); diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 9c375b72..2403aa38 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -205,7 +205,6 @@ public static class ContextListExt void LongRenderer(EmbedBuilder eb, IEnumerable page) { - var zone = ctx.System?.Zone ?? DateTimeZone.Utc; foreach (var m in page) { var profile = new StringBuilder($"**ID**: {m.Hid}"); @@ -231,11 +230,11 @@ public static class ContextListExt if ((opts.IncludeLastSwitch || opts.SortProperty == SortProperty.LastSwitch) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw)) - profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(zone)}"); + profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(ctx.Zone)}"); if ((opts.IncludeCreated || opts.SortProperty == SortProperty.CreationDate) && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created)) - profile.Append($"\n**Created on:** {created.FormatZoned(zone)}"); + profile.Append($"\n**Created on:** {created.FormatZoned(ctx.Zone)}"); if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatar) profile.Append($"\n**Avatar URL:** {avatar.TryGetCleanCdnUrl()}"); diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index c6bb978e..d356ab72 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -50,7 +50,7 @@ public class Member // Enforce per-system member limit var memberCount = await _repo.GetSystemMemberCount(ctx.System.Id); - var memberLimit = ctx.System.MemberLimitOverride ?? Limits.MaxMemberCount; + var memberLimit = ctx.Config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) throw Errors.MemberLimitReachedError(memberLimit); @@ -117,7 +117,7 @@ public class Member { var system = await _repo.GetSystem(target.System); await ctx.Reply( - embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); + embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system), ctx.Zone)); } public async Task Soulscream(Context ctx, PKMember target) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 5989965c..65c27e9d 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -314,7 +314,7 @@ public class MemberEdit LocalDate? birthday; if (birthdayStr == "today" || birthdayStr == "now") - birthday = SystemClock.Instance.InZone(ctx.System.Zone).GetCurrentDate(); + birthday = SystemClock.Instance.InZone(ctx.Zone).GetCurrentDate(); else birthday = DateUtils.ParseDate(birthdayStr, true); diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 74e6636f..4ded2da8 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -34,7 +34,7 @@ public class Random var randInt = randGen.Next(members.Count); await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, - ctx.LookupContextFor(ctx.System))); + ctx.LookupContextFor(ctx.System), ctx.Zone)); } public async Task Group(Context ctx) @@ -72,6 +72,6 @@ public class Random var randInt = randGen.Next(ms.Count); await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.Guild, - ctx.LookupContextFor(ctx.System))); + ctx.LookupContextFor(ctx.System), ctx.Zone)); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 52b4371a..b315d453 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -68,7 +68,7 @@ public class Switch var timeToMove = ctx.RemainderOrNull() ?? throw new PKSyntaxError("Must pass a date or time to move the switch to."); - var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.System.UiTz ?? "UTC"); + var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC"); var result = DateUtils.ParseDateTime(timeToMove, true, tz); if (result == null) throw Errors.InvalidDateTime(timeToMove); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 87743002..02670cf0 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -509,39 +509,6 @@ public class SystemEdit await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system."); } - public async Task SystemTimezone(Context ctx) - { - if (ctx.System == null) throw Errors.NoSystemError; - - if (await ctx.MatchClear()) - { - await _repo.UpdateSystem(ctx.System.Id, new SystemPatch { UiTz = "UTC" }); - - await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC)."); - return; - } - - var zoneStr = ctx.RemainderOrNull(); - if (zoneStr == null) - { - await ctx.Reply( - $"Your current system time zone is set to **{ctx.System.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.System)}** in that time zone. To change your system time zone, type `pk;s tz `."); - return; - } - - var zone = await FindTimeZone(ctx, zoneStr); - if (zone == null) throw Errors.InvalidTimeZone(zoneStr); - - var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone); - var msg = - $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?"; - if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled; - - await _repo.UpdateSystem(ctx.System.Id, new SystemPatch { UiTz = zone.Id }); - - await ctx.Reply($"System time zone changed to **{zone.Id}**."); - } - public async Task SystemPrivacy(Context ctx) { ctx.CheckSystem(); @@ -609,96 +576,4 @@ public class SystemEdit else await SetLevel(ctx.PopSystemPrivacySubject(), ctx.PopPrivacyLevel()); } - - public async Task SystemPing(Context ctx) - { - ctx.CheckSystem(); - - if (!ctx.HasNext()) - { - if (ctx.System.PingsEnabled) - await ctx.Reply( - "Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`."); - else - await ctx.Reply( - "Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`."); - } - else - { - if (ctx.Match("on", "enable")) - { - await _repo.UpdateSystem(ctx.System.Id, new SystemPatch { PingsEnabled = true }); - - await ctx.Reply("Reaction pings have now been enabled."); - } - - if (ctx.Match("off", "disable")) - { - await _repo.UpdateSystem(ctx.System.Id, new SystemPatch { PingsEnabled = false }); - - await ctx.Reply("Reaction pings have now been disabled."); - } - } - } - - public async Task FindTimeZone(Context ctx, string zoneStr) - { - // First, if we're given a flag emoji, we extract the flag emoji code from it. - zoneStr = StringUtils.ExtractCountryFlag(zoneStr) ?? zoneStr; - - // Then, we find all *locations* matching either the given country code or the country name. - var locations = TzdbDateTimeZoneSource.Default.Zone1970Locations; - var matchingLocations = locations.Where(l => l.Countries.Any(c => - string.Equals(c.Code, zoneStr, StringComparison.InvariantCultureIgnoreCase) || - string.Equals(c.Name, zoneStr, StringComparison.InvariantCultureIgnoreCase))); - - // Then, we find all (unique) time zone IDs that match. - var matchingZones = matchingLocations.Select(l => DateTimeZoneProviders.Tzdb.GetZoneOrNull(l.ZoneId)) - .Distinct().ToList(); - - // If the set of matching zones is empty (ie. we didn't find anything), we try a few other things. - if (matchingZones.Count == 0) - { - // First, we try to just find the time zone given directly and return that. - var givenZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(zoneStr); - if (givenZone != null) return givenZone; - - // If we didn't find anything there either, we try parsing the string as an offset, then - // find all possible zones that match that offset. For an offset like UTC+2, this doesn't *quite* - // work, since there are 57(!) matching zones (as of 2019-06-13) - but for less populated time zones - // this could work nicely. - var inputWithoutUtc = zoneStr.Replace("UTC", "").Replace("GMT", ""); - - var res = OffsetPattern.CreateWithInvariantCulture("+H").Parse(inputWithoutUtc); - if (!res.Success) res = OffsetPattern.CreateWithInvariantCulture("+H:mm").Parse(inputWithoutUtc); - - // If *this* didn't parse correctly, fuck it, bail. - if (!res.Success) return null; - var offset = res.Value; - - // To try to reduce the count, we go by locations from the 1970+ database instead of just the full database - // This elides regions that have been identical since 1970, omitting small distinctions due to Ancient History(tm). - var allZones = TzdbDateTimeZoneSource.Default.Zone1970Locations.Select(l => l.ZoneId).Distinct(); - matchingZones = allZones.Select(z => DateTimeZoneProviders.Tzdb.GetZoneOrNull(z)) - .Where(z => z.GetUtcOffset(SystemClock.Instance.GetCurrentInstant()) == offset).ToList(); - } - - // If we have a list of viable time zones, we ask the user which is correct. - - // If we only have one, return that one. - if (matchingZones.Count == 1) - return matchingZones.First(); - - // Otherwise, prompt and return! - return await ctx.Choose( - "There were multiple matches for your time zone query. Please select the region that matches you the closest:", - matchingZones, - z => - { - if (TzdbDateTimeZoneSource.Default.Aliases.Contains(z.Id)) - return $"**{z.Id}**, {string.Join(", ", TzdbDateTimeZoneSource.Default.Aliases[z.Id])}"; - - return $"**{z.Id}**"; - }); - } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 43b31be6..fd86a2ea 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -27,7 +27,7 @@ public class SystemFront var sw = await _repo.GetLatestSwitch(system.Id); if (sw == null) throw Errors.NoRegisteredSwitches; - await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone, ctx.LookupContextFor(system))); + await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, ctx.Zone, ctx.LookupContextFor(system))); } public async Task SystemFrontHistory(Context ctx, PKSystem system) @@ -77,12 +77,12 @@ public class SystemFront // Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one var switchDuration = lastSw.Value - sw.Timestamp; stringToAdd = - $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago, for {switchDuration.FormatDuration()})\n"; + $"**{membersStr}** ({sw.Timestamp.FormatZoned(ctx.Zone)}, {switchSince.FormatDuration()} ago, for {switchDuration.FormatDuration()})\n"; } else { stringToAdd = - $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago)\n"; + $"**{membersStr}** ({sw.Timestamp.FormatZoned(ctx.Zone)}, {switchSince.FormatDuration()} ago)\n"; } if (sb.Length + stringToAdd.Length >= 4096) @@ -113,7 +113,7 @@ public class SystemFront var now = SystemClock.Instance.GetCurrentInstant(); - var rangeStart = DateUtils.ParseDateTime(durationStr, true, system.Zone); + var rangeStart = DateUtils.ParseDateTime(durationStr, true, ctx.Zone); if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; @@ -127,7 +127,7 @@ public class SystemFront var showFlat = ctx.MatchFlag("flat"); var frontpercent = await _db.Execute(c => _repo.GetFrontBreakdown(c, system.Id, null, rangeStart.Value.ToInstant(), now)); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, system.Zone, + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, null, ctx.Zone, ctx.LookupContextFor(system), title.ToString(), ignoreNoFronters, showFlat)); } diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 76533a38..d1a5c022 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -126,7 +126,8 @@ public class MessageCreated: IEventHandler try { var system = ctx.SystemId != null ? await _repo.GetSystem(ctx.SystemId.Value) : null; - await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx)); + var config = ctx.SystemId != null ? await _repo.GetSystemConfig(ctx.SystemId.Value) : null; + await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, config, ctx)); } catch (PKError) { diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 8241a7a0..0e8da24b 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -9,6 +9,8 @@ using Myriad.Types; using PluralKit.Core; +using NodaTime; + using Serilog; namespace PluralKit.Bot; @@ -174,7 +176,8 @@ public class ReactionAdded: IEventHandler msg.System, msg.Member, guild, - LookupContext.ByNonOwner + LookupContext.ByNonOwner, + DateTimeZone.Utc ) }); @@ -199,7 +202,9 @@ public class ReactionAdded: IEventHandler var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages; if (member == null || !(await _cache.PermissionsFor(evt.ChannelId, member)).HasFlag(requiredPerms)) return; - if (msg.System.PingsEnabled) + var config = await _repo.GetSystemConfig(msg.System.Id); + + if (config.PingsEnabled) // If the system has pings enabled, go ahead await _rest.CreateMessage(evt.ChannelId, new MessageRequest { diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 71011cd5..50e4d454 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -72,6 +72,7 @@ public class BotModule: Module builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 26323d30..36154b14 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -64,7 +64,7 @@ public class EmbedService .Title(system.Name) .Thumbnail(new Embed.EmbedThumbnail(system.AvatarUrl.TryGetCleanCdnUrl())) .Footer(new Embed.EmbedFooter( - $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}")) + $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}")) .Color(color); if (system.DescriptionPrivacy.CanAccess(ctx)) @@ -139,7 +139,7 @@ public class EmbedService return embed.Build(); } - public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx) + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx, DateTimeZone zone) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -174,7 +174,7 @@ public class EmbedService // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) .Color(color) .Footer(new Embed.EmbedFooter( - $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}" : "")}")); + $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}")); if (member.DescriptionPrivacy.CanAccess(ctx)) eb.Image(new Embed.EmbedImage(member.BannerImage)); @@ -249,7 +249,7 @@ public class EmbedService .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx))) .Color(color) .Footer(new Embed.EmbedFooter( - $"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}")); + $"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(ctx.Zone)}")); if (target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System))) eb.Image(new Embed.EmbedImage(target.BannerImage)); diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 3c8e3f0f..810d0ba1 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -23,8 +23,9 @@ as $$ -- CTEs to query "static" (accessible only through args) data with - system as (select systems.*, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, allow_autoproxy as account_autoproxy from accounts + system as (select systems.*, config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, allow_autoproxy as account_autoproxy from accounts left join systems on systems.id = accounts.system + left join config on config.system = accounts.system left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id where accounts.uid = account_id), guild as (select * from servers where id = guild_id), @@ -48,11 +49,12 @@ as $$ coalesce(system.tag_enabled, true) as tag_enabled, system.avatar_url as system_avatar, system.account_autoproxy as allow_autoproxy, - system.latch_timeout as latch_timeout + config.latch_timeout as latch_timeout -- We need a "from" clause, so we just use some bogus data that's always present -- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data from (select 1) as _placeholder left join system on true + left join config on true left join guild on true left join last_message on true left join system_last_switch on system_last_switch.system = system.id diff --git a/PluralKit.Core/Database/Migrations/21.sql b/PluralKit.Core/Database/Migrations/21.sql new file mode 100644 index 00000000..269c9a7b --- /dev/null +++ b/PluralKit.Core/Database/Migrations/21.sql @@ -0,0 +1,29 @@ +-- schema version 21 +-- create `config` table + +create table config ( + system int primary key references systems(id) on delete cascade, + ui_tz text not null default 'UTC', + pings_enabled bool not null default true, + latch_timeout int, + member_limit_override int, + group_limit_override int +); + +insert into config select + id as system, + ui_tz, + pings_enabled, + latch_timeout, + member_limit_override, + group_limit_override +from systems; + +alter table systems + drop column ui_tz, + drop column pings_enabled, + drop column latch_timeout, + drop column member_limit_override, + drop column group_limit_override; + +update info set schema_version = 21; diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Config.cs b/PluralKit.Core/Database/Repository/ModelRepository.Config.cs new file mode 100644 index 00000000..980d3214 --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.Config.cs @@ -0,0 +1,23 @@ +using SqlKata; + +namespace PluralKit.Core; + +public partial class ModelRepository +{ + public Task GetSystemConfig(SystemId system) + => _db.QueryFirst(new Query("config").Where("system", system)); + + public async Task UpdateSystemConfig(SystemId system, SystemConfigPatch patch) + { + var query = patch.Apply(new Query("config").Where("system", system)); + var config = await _db.QueryFirst(query, "returning *"); + + _ = _dispatch.Dispatch(system, new UpdateDispatchData + { + Event = DispatchEvent.UPDATE_SETTINGS, + EventData = patch.ToJson() + }); + + return config; + } +} \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.System.cs b/PluralKit.Core/Database/Repository/ModelRepository.System.cs index 5f8c0456..77e30f15 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.System.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.System.cs @@ -1,4 +1,6 @@ #nullable enable +using Dapper; + using SqlKata; namespace PluralKit.Core; @@ -78,6 +80,8 @@ public partial class ModelRepository var system = await _db.QueryFirst(conn, query, "returning *"); _logger.Information("Created {SystemId}", system.Id); + await _db.Execute(conn => conn.QueryAsync("insert into config (system) value (@system)", new { system = system.Id })); + // no dispatch call here - system was just created, we don't have a webhook URL return system; } diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 48e9669c..4581ac19 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -9,7 +9,7 @@ namespace PluralKit.Core; internal class DatabaseMigrator { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 20; + private const int TargetSchemaVersion = 21; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Dispatch/DispatchModels.cs b/PluralKit.Core/Dispatch/DispatchModels.cs index 66e9def9..a2d3ecae 100644 --- a/PluralKit.Core/Dispatch/DispatchModels.cs +++ b/PluralKit.Core/Dispatch/DispatchModels.cs @@ -11,6 +11,7 @@ public enum DispatchEvent { PING, UPDATE_SYSTEM, + UPDATE_SETTINGS, CREATE_MEMBER, UPDATE_MEMBER, DELETE_MEMBER, diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index e1355af2..587a8e5c 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -46,18 +46,11 @@ public class PKSystem public string WebhookUrl { get; } public string WebhookToken { get; } public Instant Created { get; } - public string UiTz { get; set; } - public bool PingsEnabled { get; } - public int? LatchTimeout { get; } public PrivacyLevel DescriptionPrivacy { get; } public PrivacyLevel MemberListPrivacy { get; } public PrivacyLevel FrontPrivacy { get; } public PrivacyLevel FrontHistoryPrivacy { get; } public PrivacyLevel GroupListPrivacy { get; } - public int? MemberLimitOverride { get; } - public int? GroupLimitOverride { get; } - - [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); } public static class PKSystemExt @@ -84,7 +77,7 @@ public static class PKSystemExt { case APIVersion.V1: { - o.Add("tz", system.UiTz); + o.Add("tz", null); o.Add("description_privacy", ctx == LookupContext.ByOwner ? system.DescriptionPrivacy.ToJsonString() : null); @@ -98,7 +91,8 @@ public static class PKSystemExt } case APIVersion.V2: { - o.Add("timezone", system.UiTz); + // todo: remove this + o.Add("timezone", null); if (ctx == LookupContext.ByOwner) { diff --git a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs new file mode 100644 index 00000000..5344ec66 --- /dev/null +++ b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json.Linq; + +using NodaTime; + +using SqlKata; + +namespace PluralKit.Core; + +public class SystemConfigPatch: PatchObject +{ + public Partial UiTz { get; set; } + public Partial PingsEnabled { get; set; } + public Partial LatchTimeout { get; set; } + public Partial MemberLimitOverride { get; set; } + public Partial GroupLimitOverride { get; set; } + + public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper + .With("ui_tz", UiTz) + .With("pings_enabled", PingsEnabled) + .With("latch_timeout", LatchTimeout) + .With("member_limit_override", MemberLimitOverride) + .With("group_limit_override", GroupLimitOverride) + ); + + public new void AssertIsValid() + { + if (UiTz.IsPresent && DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz.Value) == null) + Errors.Add(new ValidationError("timezone")); + } + + public JObject ToJson() + { + var o = new JObject(); + + if (UiTz.IsPresent) + o.Add("timezone", UiTz.Value); + + if (PingsEnabled.IsPresent) + o.Add("pings_enabled", PingsEnabled.Value); + + if (LatchTimeout.IsPresent) + o.Add("latch_timeout", LatchTimeout.Value); + + if (MemberLimitOverride.IsPresent) + o.Add("member_limit", MemberLimitOverride.Value); + + if (GroupLimitOverride.IsPresent) + o.Add("group_limit", GroupLimitOverride.Value); + + return o; + } + + public static SystemConfigPatch FromJson(JObject o) + { + var patch = new SystemConfigPatch(); + + if (o.ContainsKey("timezone")) + patch.UiTz = o.Value("timezone"); + + if (o.ContainsKey("pings_enabled")) + patch.PingsEnabled = o.Value("pings_enabled"); + + if (o.ContainsKey("latch_timeout")) + patch.LatchTimeout = o.Value("latch_timeout"); + + return patch; + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index 7b5589ee..201e9b40 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -19,16 +19,11 @@ public class SystemPatch: PatchObject public Partial Token { get; set; } public Partial WebhookUrl { get; set; } public Partial WebhookToken { get; set; } - public Partial UiTz { get; set; } public Partial DescriptionPrivacy { get; set; } public Partial MemberListPrivacy { get; set; } public Partial GroupListPrivacy { get; set; } public Partial FrontPrivacy { get; set; } public Partial FrontHistoryPrivacy { get; set; } - public Partial PingsEnabled { get; set; } - public Partial LatchTimeout { get; set; } - public Partial MemberLimitOverride { get; set; } - public Partial GroupLimitOverride { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("name", Name) @@ -41,16 +36,11 @@ public class SystemPatch: PatchObject .With("token", Token) .With("webhook_url", WebhookUrl) .With("webhook_token", WebhookToken) - .With("ui_tz", UiTz) .With("description_privacy", DescriptionPrivacy) .With("member_list_privacy", MemberListPrivacy) .With("group_list_privacy", GroupListPrivacy) .With("front_privacy", FrontPrivacy) .With("front_history_privacy", FrontHistoryPrivacy) - .With("pings_enabled", PingsEnabled) - .With("latch_timeout", LatchTimeout) - .With("member_limit_override", MemberLimitOverride) - .With("group_limit_override", GroupLimitOverride) ); public new void AssertIsValid() @@ -69,8 +59,6 @@ public class SystemPatch: PatchObject s => MiscUtils.TryMatchUri(s, out var bannerUri)); if (Color.Value != null) AssertValid(Color.Value, "color", "^[0-9a-fA-F]{6}$"); - if (UiTz.IsPresent && DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz.Value) == null) - Errors.Add(new ValidationError("timezone")); } #nullable disable @@ -84,14 +72,11 @@ public class SystemPatch: PatchObject if (o.ContainsKey("avatar_url")) patch.AvatarUrl = o.Value("avatar_url").NullIfEmpty(); if (o.ContainsKey("banner")) patch.BannerImage = o.Value("banner").NullIfEmpty(); if (o.ContainsKey("color")) patch.Color = o.Value("color").NullIfEmpty(); - if (o.ContainsKey("timezone")) patch.UiTz = o.Value("timezone") ?? "UTC"; switch (v) { case APIVersion.V1: { - if (o.ContainsKey("tz")) patch.UiTz = o.Value("tz") ?? "UTC"; - if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy"); if (o.ContainsKey("member_list_privacy")) @@ -149,8 +134,6 @@ public class SystemPatch: PatchObject o.Add("banner", BannerImage.Value); if (Color.IsPresent) o.Add("color", Color.Value); - if (UiTz.IsPresent) - o.Add("timezone", UiTz.Value); if ( DescriptionPrivacy.IsPresent diff --git a/PluralKit.Core/Models/SystemConfig.cs b/PluralKit.Core/Models/SystemConfig.cs new file mode 100644 index 00000000..5a8c4ddd --- /dev/null +++ b/PluralKit.Core/Models/SystemConfig.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json.Linq; + +using NodaTime; + +namespace PluralKit.Core; + +public class SystemConfig +{ + public SystemId Id { get; } + public string UiTz { get; set; } + public bool PingsEnabled { get; } + public int? LatchTimeout { get; } + public int? MemberLimitOverride { get; } + public int? GroupLimitOverride { get; } + + public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); +} + +public static class SystemConfigExt +{ + public static JObject ToJson(this SystemConfig cfg) + { + var o = new JObject(); + + o.Add("timezone", cfg.UiTz); + o.Add("pings_enabled", cfg.PingsEnabled); + o.Add("latch_timeout", cfg.LatchTimeout); + o.Add("member_limit", cfg.MemberLimitOverride ?? Limits.MaxMemberCount); + o.Add("group_limit", cfg.GroupLimitOverride ?? Limits.MaxGroupCount); + + return o; + } +} \ No newline at end of file diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index d159b05a..ae9588ae 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -21,7 +21,7 @@ public class DataFileService _dispatch = dispatch; } - public async Task ExportSystem(PKSystem system) + public async Task ExportSystem(PKSystem system, string timezone) { await using var conn = await _db.Obtain(); @@ -30,7 +30,7 @@ public class DataFileService o.Merge(system.ToJson(LookupContext.ByOwner)); - o.Add("timezone", system.UiTz); + o.Add("timezone", timezone); o.Add("accounts", new JArray((await _repo.GetSystemAccounts(system.Id)).ToList())); o.Add("members", new JArray((await _repo.GetSystemMembers(system.Id).ToListAsync()).Select(m => diff --git a/PluralKit.Core/Utils/BulkImporter/BulkImporter.cs b/PluralKit.Core/Utils/BulkImporter/BulkImporter.cs index 848d4014..3bfdfc02 100644 --- a/PluralKit.Core/Utils/BulkImporter/BulkImporter.cs +++ b/PluralKit.Core/Utils/BulkImporter/BulkImporter.cs @@ -21,6 +21,7 @@ public partial class BulkImporter: IAsyncDisposable private ModelRepository _repo { get; init; } private PKSystem _system { get; set; } + private SystemConfig _cfg { get; set; } private IPKConnection _conn { get; init; } private IPKTransaction _tx { get; init; } @@ -60,6 +61,8 @@ public partial class BulkImporter: IAsyncDisposable importer._system = system; } + importer._cfg = await repo.GetSystemConfig(system.Id); + // Fetch all members in the system and log their names and hids var members = await conn.QueryAsync("select id, hid, name from members where system = @System", new { System = system.Id }); @@ -120,7 +123,7 @@ public partial class BulkImporter: IAsyncDisposable private async Task AssertMemberLimitNotReached(int newMembers) { - var memberLimit = _system.MemberLimitOverride ?? Limits.MaxMemberCount; + var memberLimit = _cfg.MemberLimitOverride ?? Limits.MaxMemberCount; var existingMembers = await _repo.GetSystemMemberCount(_system.Id); if (existingMembers + newMembers > memberLimit) throw new ImportException($"Import would exceed the maximum number of members ({memberLimit})."); @@ -128,7 +131,7 @@ public partial class BulkImporter: IAsyncDisposable private async Task AssertGroupLimitNotReached(int newGroups) { - var limit = _system.GroupLimitOverride ?? Limits.MaxGroupCount; + var limit = _cfg.GroupLimitOverride ?? Limits.MaxGroupCount; var existing = await _repo.GetSystemGroupCount(_system.Id); if (existing + newGroups > limit) throw new ImportException($"Import would exceed the maximum number of groups ({limit})."); diff --git a/PluralKit.Core/Utils/DateTimeFormats.cs b/PluralKit.Core/Utils/DateTimeFormats.cs index 35741d7e..c1f625ce 100644 --- a/PluralKit.Core/Utils/DateTimeFormats.cs +++ b/PluralKit.Core/Utils/DateTimeFormats.cs @@ -29,6 +29,5 @@ public static class DateTimeFormats public static string FormatExport(this LocalDate date) => DateExportFormat.Format(date); public static string FormatZoned(this ZonedDateTime zdt) => ZonedDateTimeFormat.Format(zdt); public static string FormatZoned(this Instant i, DateTimeZone zone) => i.InZone(zone).FormatZoned(); - public static string FormatZoned(this Instant i, PKSystem sys) => i.FormatZoned(sys.Zone); public static string FormatDuration(this Duration d) => DurationFormat.Format(d); } \ No newline at end of file diff --git a/docs/content/api/dispatch.md b/docs/content/api/dispatch.md index da72a8b2..03ff34fb 100644 --- a/docs/content/api/dispatch.md +++ b/docs/content/api/dispatch.md @@ -37,6 +37,7 @@ PluralKit will send invalid requests to your endpoint, with `PING` event type, o |---|---|---|---| |PING|PluralKit is checking if your webhook URL is working.|null|Reply with a 200 status code if the `signing_token` is correct, or a 401 status code if it is invalid.| |UPDATE_SYSTEM|your system was updated|[system object](/api/models#system-model) only containing modififed keys| +|UPDATE_SETTINGS|your bot settings were updated|[system settings object](/api/models#system-settings-model) only containing modified keys| |CREATE_MEMBER|a new member was created|[member object](/api/models#member-model) only containing `name` key|new member ID can be found in the top-level `id` key`| |UPDATE_MEMBER|a member was updated|[member object](/api/models#member-model) only containing modified keys|member ID can be found in the top-level `id` key`| |DELETE_MEMBER|a member was deleted|null|old member ID can be found in the top-level `id` key`| diff --git a/docs/content/api/endpoints.md b/docs/content/api/endpoints.md index 8d24b09f..e2880b70 100644 --- a/docs/content/api/endpoints.md +++ b/docs/content/api/endpoints.md @@ -28,6 +28,20 @@ Takes a partial [system object](/api/models#system-model). Returns a [system object](/api/models#system-model). +### Get System Settings + +GET `/systems/{systemRef}/settings` + +Returns a [system settings object](/api/models#system-settings-model). + +### Update System Settings + +PATCH `/systems/{systemRef}/settings` + +Takes a partial [system settings object](/api/models#system-settings-model). + +Returns a [system settings object](/api/models#system-settings-model). + ### Get System Guild Settings GET `/systems/@me/guilds/{guild_id}` diff --git a/docs/content/api/models.md b/docs/content/api/models.md index e1319b76..4de50cc2 100644 --- a/docs/content/api/models.md +++ b/docs/content/api/models.md @@ -28,7 +28,6 @@ Every PluralKit entity has two IDs: a short (5-character) ID and a longer UUID. |banner|?string|256-character limit, must be a publicly-accessible URL| |color|string|6-character hex code, no `#` at the beginning| |created|datetime|| -|timezone|string|defaults to `UTC`| |privacy|?system privacy object|| * System privacy keys: `description_privacy`, `member_list_privacy`, `group_list_privacy`, `front_privacy`, `front_history_privacy` @@ -100,6 +99,16 @@ Every PluralKit entity has two IDs: a short (5-character) ID and a longer UUID. |system|full System object|The system that proxied the message.| |member|full Member object|The member that proxied the message.| +### System settings model + +|key|type|notes| +|---|---|---| +|timezone|string|defaults to `UTC`| +|pings_enabled|boolean| +|latch_timeout|int?| +|member_limit|int|read-only, defaults to 1000| +|group_limit|int|read-only, defaults to 250| + ### System guild settings model |key|type|notes| diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 35cb0be7..7246c910 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -43,7 +43,6 @@ Some arguments indicate the use of specific Discord features. These include: - `pk;system privacy ` - Changes your systems privacy settings. - `pk;system tag [tag]` - Changes the system tag of your system. - `pk;system servertag [tag|-enable|-disable]` - Changes your system's tag in the current server, or disables it for the current server. -- `pk;system timezone [location]` - Changes the time zone of your system. - `pk;system proxy [server id] [on|off]` - Toggles message proxying for a specific server. - `pk;system delete` - Deletes your system. - `pk;system [system] fronter` - Shows the current fronter of a system. @@ -107,8 +106,12 @@ Some arguments indicate the use of specific Discord features. These include: ## Autoproxy commands - `pk;autoproxy [off|front|latch|]` - Sets your system's autoproxy mode for the current server. -- `pk;autoproxy timeout [|off|reset]` - Sets the latch timeout duration for your system. -- `pk;autoproxy account [on|off]` - Toggles autoproxy globally for the current account. + +## Config commands +- `pk;config timezone [location]` - Changes the time zone of your system. +- `pk;config ping ` - Changes your system's ping preferences. +- `pk;config autoproxy timeout [|off|reset]` - Sets the latch timeout duration for your system. +- `pk;config autoproxy account [on|off]` - Toggles autoproxy globally for the current account. ## Server owner commands *(all commands here require Manage Server permission)*