feat: pk;config

This commit is contained in:
spiral
2021-11-29 21:35:21 -05:00
parent d195c80d92
commit 56d07e0f2d
41 changed files with 648 additions and 313 deletions

View File

@@ -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

View 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;

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -11,6 +11,7 @@ public enum DispatchEvent
{
PING,
UPDATE_SYSTEM,
UPDATE_SETTINGS,
CREATE_MEMBER,
UPDATE_MEMBER,
DELETE_MEMBER,

View File

@@ -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)
{

View 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;
}
}

View File

@@ -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

View 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;
}
}

View File

@@ -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 =>

View File

@@ -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}).");

View File

@@ -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);
}