feat: pk;config
This commit is contained in:
@@ -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
|
||||
|
||||
29
PluralKit.Core/Database/Migrations/21.sql
Normal file
29
PluralKit.Core/Database/Migrations/21.sql
Normal file
@@ -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;
|
||||
23
PluralKit.Core/Database/Repository/ModelRepository.Config.cs
Normal file
23
PluralKit.Core/Database/Repository/ModelRepository.Config.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using SqlKata;
|
||||
|
||||
namespace PluralKit.Core;
|
||||
|
||||
public partial class ModelRepository
|
||||
{
|
||||
public Task<SystemConfig> GetSystemConfig(SystemId system)
|
||||
=> _db.QueryFirst<SystemConfig>(new Query("config").Where("system", system));
|
||||
|
||||
public async Task<SystemConfig> UpdateSystemConfig(SystemId system, SystemConfigPatch patch)
|
||||
{
|
||||
var query = patch.Apply(new Query("config").Where("system", system));
|
||||
var config = await _db.QueryFirst<SystemConfig>(query, "returning *");
|
||||
|
||||
_ = _dispatch.Dispatch(system, new UpdateDispatchData
|
||||
{
|
||||
Event = DispatchEvent.UPDATE_SETTINGS,
|
||||
EventData = patch.ToJson()
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -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<PKSystem>(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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,7 @@ public enum DispatchEvent
|
||||
{
|
||||
PING,
|
||||
UPDATE_SYSTEM,
|
||||
UPDATE_SETTINGS,
|
||||
CREATE_MEMBER,
|
||||
UPDATE_MEMBER,
|
||||
DELETE_MEMBER,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
68
PluralKit.Core/Models/Patch/SystemConfigPatch.cs
Normal file
68
PluralKit.Core/Models/Patch/SystemConfigPatch.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using SqlKata;
|
||||
|
||||
namespace PluralKit.Core;
|
||||
|
||||
public class SystemConfigPatch: PatchObject
|
||||
{
|
||||
public Partial<string> UiTz { get; set; }
|
||||
public Partial<bool> PingsEnabled { get; set; }
|
||||
public Partial<int?> LatchTimeout { get; set; }
|
||||
public Partial<int?> MemberLimitOverride { get; set; }
|
||||
public Partial<int?> 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<string>("timezone");
|
||||
|
||||
if (o.ContainsKey("pings_enabled"))
|
||||
patch.PingsEnabled = o.Value<bool>("pings_enabled");
|
||||
|
||||
if (o.ContainsKey("latch_timeout"))
|
||||
patch.LatchTimeout = o.Value<int>("latch_timeout");
|
||||
|
||||
return patch;
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,11 @@ public class SystemPatch: PatchObject
|
||||
public Partial<string?> Token { get; set; }
|
||||
public Partial<string?> WebhookUrl { get; set; }
|
||||
public Partial<string?> WebhookToken { get; set; }
|
||||
public Partial<string> UiTz { get; set; }
|
||||
public Partial<PrivacyLevel> DescriptionPrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> MemberListPrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> GroupListPrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> FrontPrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> FrontHistoryPrivacy { get; set; }
|
||||
public Partial<bool> PingsEnabled { get; set; }
|
||||
public Partial<int?> LatchTimeout { get; set; }
|
||||
public Partial<int?> MemberLimitOverride { get; set; }
|
||||
public Partial<int?> 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<string>("avatar_url").NullIfEmpty();
|
||||
if (o.ContainsKey("banner")) patch.BannerImage = o.Value<string>("banner").NullIfEmpty();
|
||||
if (o.ContainsKey("color")) patch.Color = o.Value<string>("color").NullIfEmpty();
|
||||
if (o.ContainsKey("timezone")) patch.UiTz = o.Value<string>("timezone") ?? "UTC";
|
||||
|
||||
switch (v)
|
||||
{
|
||||
case APIVersion.V1:
|
||||
{
|
||||
if (o.ContainsKey("tz")) patch.UiTz = o.Value<string>("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
|
||||
|
||||
33
PluralKit.Core/Models/SystemConfig.cs
Normal file
33
PluralKit.Core/Models/SystemConfig.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class DataFileService
|
||||
_dispatch = dispatch;
|
||||
}
|
||||
|
||||
public async Task<JObject> ExportSystem(PKSystem system)
|
||||
public async Task<JObject> 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 =>
|
||||
|
||||
@@ -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<PKMember>("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}).");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user