Merge branch 'feat/webhooks' into main

This commit is contained in:
spiral 2021-11-25 17:15:42 -05:00
commit b8e2ebd470
No known key found for this signature in database
GPG key ID: A6059F0CA0E1BD31
34 changed files with 920 additions and 39 deletions

View file

@ -0,0 +1,7 @@
-- schema version 20: insert date
-- add outgoing webhook to systems
alter table systems add column webhook_url text;
alter table systems add column webhook_token text;
update info set schema_version = 20;

View file

@ -10,6 +10,7 @@ namespace PluralKit.Core
{
_logger.Information("Updated account {accountId}: {@AccountPatch}", id, patch);
var query = patch.Apply(new Query("accounts").Where("uid", id));
_ = _dispatch.Dispatch(id, patch);
await _db.ExecuteQuery(query, extraSql: "returning *");
}
}

View file

@ -2,12 +2,20 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using SqlKata;
namespace PluralKit.Core
{
public partial class ModelRepository
{
public Task<PKGroup?> GetGroup(GroupId id)
{
var query = new Query("groups").Where("id", id);
return _db.QueryFirst<PKGroup?>(query);
}
public Task<PKGroup?> GetGroupByName(SystemId system, string name)
{
var query = new Query("groups").Where("system", system).WhereRaw("lower(name) = lower(?)", name.ToLower());
@ -60,18 +68,31 @@ namespace PluralKit.Core
return group;
}
public Task<PKGroup> UpdateGroup(GroupId id, GroupPatch patch, IPKConnection? conn = null)
public async Task<PKGroup> UpdateGroup(GroupId id, GroupPatch patch, IPKConnection? conn = null)
{
_logger.Information("Updated {GroupId}: {@GroupPatch}", id, patch);
var query = patch.Apply(new Query("groups").Where("id", id));
return _db.QueryFirst<PKGroup>(conn, query, extraSql: "returning *");
var group = await _db.QueryFirst<PKGroup>(conn, query, extraSql: "returning *");
if (conn == null)
_ = _dispatch.Dispatch(id, new()
{
Event = DispatchEvent.UPDATE_GROUP,
EventData = patch.ToJson(),
});
return group;
}
public Task DeleteGroup(GroupId group)
public async Task DeleteGroup(GroupId group)
{
var oldGroup = await GetGroup(group);
_logger.Information("Deleted {GroupId}", group);
var query = new Query("groups").AsDelete().Where("id", group);
return _db.ExecuteQuery(query);
await _db.ExecuteQuery(query);
if (oldGroup != null)
_ = _dispatch.Dispatch(oldGroup.System, oldGroup.Uuid, DispatchEvent.DELETE_GROUP);
}
}
}

View file

@ -39,14 +39,15 @@ namespace PluralKit.Core
);
}
public Task<SystemGuildSettings> UpdateSystemGuild(SystemId system, ulong guild, SystemGuildPatch patch)
public async Task<SystemGuildSettings> UpdateSystemGuild(SystemId system, ulong guild, SystemGuildPatch patch)
{
_logger.Information("Updated {SystemId} in guild {GuildId}: {@SystemGuildPatch}", system, guild, patch);
var query = patch.Apply(new Query("system_guild").Where("system", system).Where("guild", guild));
return _db.QueryFirst<SystemGuildSettings>(query, extraSql: "returning *");
var settings = await _db.QueryFirst<SystemGuildSettings>(query, extraSql: "returning *");
_ = _dispatch.Dispatch(system, guild, patch);
return settings;
}
public Task<MemberGuildSettings> GetMemberGuild(ulong guild, MemberId member, bool defaultInsert = true)
{
if (!defaultInsert)
@ -69,6 +70,7 @@ namespace PluralKit.Core
{
_logger.Information("Updated {MemberId} in guild {GuildId}: {@MemberGuildPatch}", member, guild, patch);
var query = patch.Apply(new Query("member_guild").Where("member", member).Where("guild", guild));
_ = _dispatch.Dispatch(member, guild, patch);
return _db.QueryFirst<MemberGuildSettings>(query, extraSql: "returning *");
}
}

View file

@ -1,7 +1,10 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using SqlKata;
namespace PluralKit.Core
@ -46,6 +49,15 @@ namespace PluralKit.Core
return _db.QueryFirst<PKMember?>(query);
}
public Task<IEnumerable<Guid>> GetMemberGuids(IEnumerable<MemberId> ids)
{
var query = new Query("members")
.Select("uuid")
.WhereIn("id", ids);
return _db.Query<Guid>(query);
}
public async Task<PKMember> CreateMember(SystemId systemId, string memberName, IPKConnection? conn = null)
{
var query = new Query("members").AsInsert(new
@ -64,14 +76,27 @@ namespace PluralKit.Core
{
_logger.Information("Updated {MemberId}: {@MemberPatch}", id, patch);
var query = patch.Apply(new Query("members").Where("id", id));
if (conn == null)
_ = _dispatch.Dispatch(id, new()
{
Event = DispatchEvent.UPDATE_MEMBER,
EventData = patch.ToJson(),
});
return _db.QueryFirst<PKMember>(conn, query, extraSql: "returning *");
}
public Task DeleteMember(MemberId id)
public async Task DeleteMember(MemberId id)
{
var oldMember = await GetMember(id);
_logger.Information("Deleted {MemberId}", id);
var query = new Query("members").AsDelete().Where("id", id);
return _db.ExecuteQuery(query);
await _db.ExecuteQuery(query);
// shh, compiler
if (oldMember != null)
_ = _dispatch.Dispatch(oldMember.System, oldMember.Uuid, DispatchEvent.DELETE_MEMBER);
}
}
}

View file

@ -5,6 +5,8 @@ using System.Threading.Tasks;
using Dapper;
using Newtonsoft.Json.Linq;
using NodaTime;
using NpgsqlTypes;
@ -42,8 +44,19 @@ namespace PluralKit.Core
await tx.CommitAsync();
_logger.Information("Created {SwitchId} in {SystemId}: {Members}", sw.Id, system, members);
_ = _dispatch.Dispatch(sw.Id, new()
{
Event = DispatchEvent.CREATE_SWITCH,
EventData = JObject.FromObject(new
{
id = sw.Uuid.ToString(),
timestamp = sw.Timestamp.FormatExport(),
members = await GetMemberGuids(members),
}),
});
return sw;
}
public async Task EditSwitch(IPKConnection conn, SwitchId switchId, IReadOnlyCollection<MemberId> members)
{
// Use a transaction here since we're doing multiple executed commands in one
@ -69,28 +82,52 @@ namespace PluralKit.Core
// Finally we commit the tx, since the using block will otherwise rollback it
await tx.CommitAsync();
_ = _dispatch.Dispatch(switchId, new()
{
Event = DispatchEvent.UPDATE_SWITCH_MEMBERS,
EventData = JObject.FromObject(new
{
members = await GetMemberGuids(members),
}),
});
_logger.Information("Updated {SwitchId} members: {Members}", switchId, members);
}
public Task MoveSwitch(SwitchId id, Instant time)
public async Task MoveSwitch(SwitchId id, Instant time)
{
_logger.Information("Updated {SwitchId} timestamp: {SwitchTimestamp}", id, time);
var query = new Query("switches").AsUpdate(new { timestamp = time }).Where("id", id);
return _db.ExecuteQuery(query);
await _db.ExecuteQuery(query);
_ = _dispatch.Dispatch(id, new()
{
Event = DispatchEvent.UPDATE_SWITCH,
EventData = JObject.FromObject(new
{
timestamp = time.FormatExport(),
}),
});
}
public Task DeleteSwitch(SwitchId id)
public async Task DeleteSwitch(SwitchId id)
{
_logger.Information("Deleted {Switch}", id);
var existingSwitch = await GetSwitch(id);
var query = new Query("switches").AsDelete().Where("id", id);
return _db.ExecuteQuery(query);
await _db.ExecuteQuery(query);
_logger.Information("Deleted {Switch}", id);
_ = _dispatch.Dispatch(existingSwitch.System, existingSwitch.Uuid, DispatchEvent.DELETE_SWITCH);
}
public Task DeleteAllSwitches(SystemId system)
public async Task DeleteAllSwitches(SystemId system)
{
_logger.Information("Deleted all switches in {SystemId}", system);
var query = new Query("switches").AsDelete().Where("system", system);
return _db.ExecuteQuery(query);
await _db.ExecuteQuery(query);
_ = _dispatch.Dispatch(system, new UpdateDispatchData()
{
Event = DispatchEvent.DELETE_ALL_SWITCHES
});
}
public IAsyncEnumerable<PKSwitch> GetSwitches(SystemId system)
@ -100,6 +137,9 @@ namespace PluralKit.Core
return _db.QueryStream<PKSwitch>(query);
}
public Task<PKSwitch?> GetSwitch(SwitchId id)
=> _db.QueryFirst<PKSwitch?>(new Query("switches").Where("id", id));
public Task<PKSwitch> GetSwitchByUuid(Guid uuid)
{
var query = new Query("switches").Where("uuid", uuid);

View file

@ -78,17 +78,27 @@ namespace PluralKit.Core
});
var system = await _db.QueryFirst<PKSystem>(conn, query, extraSql: "returning *");
_logger.Information("Created {SystemId}", system.Id);
// no dispatch call here - system was just created, we don't have a webhook URL
return system;
}
public Task<PKSystem> UpdateSystem(SystemId id, SystemPatch patch, IPKConnection? conn = null)
public async Task<PKSystem> UpdateSystem(SystemId id, SystemPatch patch, IPKConnection? conn = null)
{
_logger.Information("Updated {SystemId}: {@SystemPatch}", id, patch);
var query = patch.Apply(new Query("systems").Where("id", id));
return _db.QueryFirst<PKSystem>(conn, query, extraSql: "returning *");
var res = await _db.QueryFirst<PKSystem>(conn, query, extraSql: "returning *");
_ = _dispatch.Dispatch(id, new UpdateDispatchData()
{
Event = DispatchEvent.UPDATE_SYSTEM,
EventData = patch.ToJson(),
});
return res;
}
public Task AddAccount(SystemId system, ulong accountId, IPKConnection? conn = null)
public async Task AddAccount(SystemId system, ulong accountId, IPKConnection? conn = null)
{
// We have "on conflict do nothing" since linking an account when it's already linked to the same system is idempotent
// This is used in import/export, although the pk;link command checks for this case beforehand
@ -100,7 +110,13 @@ namespace PluralKit.Core
});
_logger.Information("Linked account {UserId} to {SystemId}", accountId, system);
return _db.ExecuteQuery(conn, query, extraSql: "on conflict do nothing");
await _db.ExecuteQuery(conn, query, extraSql: "on conflict do nothing");
_ = _dispatch.Dispatch(system, new UpdateDispatchData()
{
Event = DispatchEvent.LINK_ACCOUNT,
EntityId = accountId.ToString(),
});
}
public async Task RemoveAccount(SystemId system, ulong accountId)
@ -108,6 +124,11 @@ namespace PluralKit.Core
var query = new Query("accounts").AsDelete().Where("uid", accountId).Where("system", system);
await _db.ExecuteQuery(query);
_logger.Information("Unlinked account {UserId} from {SystemId}", accountId, system);
_ = _dispatch.Dispatch(system, new UpdateDispatchData()
{
Event = DispatchEvent.UNLINK_ACCOUNT,
EntityId = accountId.ToString(),
});
}
public Task DeleteSystem(SystemId id)

View file

@ -6,10 +6,12 @@ namespace PluralKit.Core
{
private readonly ILogger _logger;
private readonly IDatabase _db;
public ModelRepository(ILogger logger, IDatabase db)
private readonly DispatchService _dispatch;
public ModelRepository(ILogger logger, IDatabase db, DispatchService dispatch)
{
_logger = logger.ForContext<ModelRepository>();
_db = db;
_dispatch = dispatch;
}
}
}

View file

@ -12,7 +12,7 @@ namespace PluralKit.Core
internal class DatabaseMigrator
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 19;
private const int TargetSchemaVersion = 20;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)

View file

@ -0,0 +1,117 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
namespace PluralKit.Core
{
public enum DispatchEvent
{
PING,
UPDATE_SYSTEM,
CREATE_MEMBER,
UPDATE_MEMBER,
DELETE_MEMBER,
CREATE_GROUP,
UPDATE_GROUP,
UPDATE_GROUP_MEMBERS,
DELETE_GROUP,
LINK_ACCOUNT,
UNLINK_ACCOUNT,
UPDATE_SYSTEM_GUILD,
UPDATE_MEMBER_GUILD,
CREATE_MESSAGE,
CREATE_SWITCH,
UPDATE_SWITCH,
UPDATE_SWITCH_MEMBERS,
DELETE_SWITCH,
DELETE_ALL_SWITCHES,
SUCCESSFUL_IMPORT,
}
public struct UpdateDispatchData
{
public DispatchEvent Event;
public string SystemId;
public string? EntityId;
public ulong? GuildId;
public string SigningToken;
public JObject? EventData;
}
public static class DispatchExt
{
public static StringContent GetPayloadBody(this UpdateDispatchData data)
{
var o = new JObject();
o.Add("type", data.Event.ToString());
o.Add("signing_token", data.SigningToken);
o.Add("system_id", data.SystemId);
o.Add("id", data.EntityId);
o.Add("guild_id", data.GuildId);
o.Add("data", data.EventData);
return new StringContent(JsonConvert.SerializeObject(o));
}
public static JObject ToDispatchJson(this PKMessage msg, string memberRef)
{
var o = new JObject();
o.Add("timestamp", Instant.FromUnixTimeMilliseconds((long)(msg.Mid >> 22) + 1420070400000).FormatExport());
o.Add("id", msg.Mid.ToString());
o.Add("original", msg.OriginalMid.ToString());
o.Add("sender", msg.Sender.ToString());
o.Add("channel", msg.Channel.ToString());
o.Add("member", memberRef);
return o;
}
public static async Task<bool> ValidateUri(string url)
{
IPHostEntry host = null;
try
{
var uri = new Uri(url);
host = await Dns.GetHostEntryAsync(uri.DnsSafeHost);
}
catch (Exception)
{
return false;
}
if (host == null || host.AddressList.Length == 0)
return false;
#pragma warning disable CS0618
foreach (var address in host.AddressList.Where(address => address.AddressFamily is AddressFamily.InterNetwork))
{
if ((address.Address & 0x7f000000) == 0x7f000000) // 127.0/8
return false;
if ((address.Address & 0x0a000000) == 0x0a000000) // 10.0/8
return false;
if ((address.Address & 0xa9fe0000) == 0xa9fe0000) // 169.254/16
return false;
if ((address.Address & 0xac100000) == 0xac100000) // 172.16/12
return false;
}
if (host.AddressList.Any(address => address.IsIPv6LinkLocal))
return false;
// we only support IPv4 in prod :(
return host.AddressList.Any(address => address.AddressFamily is AddressFamily.InterNetwork);
}
}
}

View file

@ -0,0 +1,232 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Autofac;
using Serilog;
namespace PluralKit.Core
{
public class DispatchService
{
private readonly ILogger _logger;
private readonly ILifetimeScope _provider;
private readonly HttpClient _client = new();
public DispatchService(ILogger logger, ILifetimeScope provider, CoreConfig cfg)
{
_logger = logger;
_provider = provider;
}
public async Task DoPostRequest(SystemId system, string webhookUrl, HttpContent content, bool isVerify = false)
{
if (!await DispatchExt.ValidateUri(webhookUrl))
{
_logger.Warning("Failed to dispatch webhook for system {SystemId}: URL is invalid or points to a private address", system);
return;
}
try
{
await _client.PostAsync(webhookUrl, content);
}
catch (HttpRequestException e)
{
if (isVerify)
throw;
else
_logger.Error("Could not dispatch webhook request!", e);
}
}
public async Task Dispatch(SystemId systemId, UpdateDispatchData data)
{
if (data.EventData != null && data.EventData.Count == 0)
return;
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystem(systemId);
if (system.WebhookUrl == null)
return;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
_logger.Debug("Dispatching webhook for system {SystemId}", systemId);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(MemberId memberId, UpdateDispatchData data)
{
if (data.EventData != null && data.EventData.Count == 0)
return;
var repo = _provider.Resolve<ModelRepository>();
var member = await repo.GetMember(memberId);
var system = await repo.GetSystem(member.System);
if (system.WebhookUrl == null)
return;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EntityId = member.Uuid.ToString();
_logger.Debug("Dispatching webhook for member {MemberId} (system {SystemId})", memberId, system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(GroupId groupId, UpdateDispatchData data)
{
if (data.EventData != null && data.EventData.Count == 0)
return;
var repo = _provider.Resolve<ModelRepository>();
var group = await repo.GetGroup(groupId);
var system = await repo.GetSystem(group.System);
if (system.WebhookUrl == null)
return;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EntityId = group.Uuid.ToString();
_logger.Debug("Dispatching webhook for group {GroupId} (system {SystemId})", groupId, system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(Dictionary<GroupId, MemberId> dict, DispatchEvent evt)
{
var repo = _provider.Resolve<ModelRepository>();
var g = await repo.GetGroup(dict.Keys.FirstOrDefault());
var system = await repo.GetSystem(g.System);
if (system.WebhookUrl == null)
return;
var data = new UpdateDispatchData();
data.Event = DispatchEvent.UPDATE_GROUP_MEMBERS;
data.SystemId = system.Uuid.ToString();
_logger.Debug("Dispatching webhook for group member update (system {SystemId})", system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(SwitchId swId, UpdateDispatchData data)
{
var repo = _provider.Resolve<ModelRepository>();
var sw = await repo.GetSwitch(swId);
var system = await repo.GetSystem(sw.System);
if (system.WebhookUrl == null)
return;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EntityId = sw.Uuid.ToString();
_logger.Debug("Dispatching webhook for switch {SwitchId} (system {SystemId})", sw.Id, system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(SystemId systemId, PKMessage newMessage)
{
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystem(systemId);
if (system.WebhookUrl == null)
return;
var member = await repo.GetMember(newMessage.Member);
var data = new UpdateDispatchData();
data.Event = DispatchEvent.CREATE_MESSAGE;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EventData = newMessage.ToDispatchJson(member.Uuid.ToString());
_logger.Debug("Dispatching webhook for message create (system {SystemId})", system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(SystemId systemId, ulong guild_id, SystemGuildPatch patch)
{
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystem(systemId);
if (system.WebhookUrl == null)
return;
string memberRef = null;
if (patch.AutoproxyMember.Value != null)
{
var member = await repo.GetMember(patch.AutoproxyMember.Value.Value);
memberRef = member.Uuid.ToString();
}
var data = new UpdateDispatchData();
data.Event = DispatchEvent.UPDATE_SYSTEM_GUILD;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.GuildId = guild_id;
data.EventData = patch.ToJson(memberRef);
_logger.Debug("Dispatching webhook for system {SystemId} in guild {GuildId}", system.Id, guild_id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(MemberId memberId, ulong guild_id, MemberGuildPatch patch)
{
var repo = _provider.Resolve<ModelRepository>();
var member = await repo.GetMember(memberId);
var system = await repo.GetSystem(member.System);
if (system.WebhookUrl == null)
return;
var data = new UpdateDispatchData();
data.Event = DispatchEvent.UPDATE_MEMBER_GUILD;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EntityId = member.Uuid.ToString();
data.GuildId = guild_id;
data.EventData = patch.ToJson();
_logger.Debug("Dispatching webhook for member {MemberId} (system {SystemId}) in guild {GuildId}", member.Id, system.Id, guild_id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(ulong accountId, AccountPatch patch)
{
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystemByAccount(accountId);
if (system.WebhookUrl == null)
return;
var data = new UpdateDispatchData();
data.Event = DispatchEvent.UPDATE_MEMBER_GUILD;
data.SigningToken = system.WebhookToken;
data.EntityId = accountId.ToString();
data.EventData = patch.ToJson();
_logger.Debug("Dispatching webhook for account {AccountId} (system {SystemId})", accountId, system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
public async Task Dispatch(SystemId systemId, Guid uuid, DispatchEvent evt)
{
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystem(systemId);
if (system.WebhookUrl == null)
return;
var data = new UpdateDispatchData();
data.Event = evt;
data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString();
data.EntityId = uuid.ToString();
_logger.Debug("Dispatching webhook for entity delete (system {SystemId})", system.Id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
}
}
}

View file

@ -46,6 +46,8 @@ namespace PluralKit.Core
public string BannerImage { get; }
public string Color { get; }
public string Token { get; }
public string WebhookUrl { get; }
public string WebhookToken { get; }
public Instant Created { get; }
public string UiTz { get; set; }
public bool PingsEnabled { get; }
@ -100,6 +102,10 @@ namespace PluralKit.Core
if (ctx == LookupContext.ByOwner)
{
// todo: should this be moved to a different JSON model?
o.Add("webhook_url", system.WebhookUrl);
o.Add("webhook_token", system.WebhookToken);
var p = new JObject();
p.Add("description_privacy", system.DescriptionPrivacy.ToJsonString());

View file

@ -1,5 +1,7 @@
using SqlKata;
using Newtonsoft.Json.Linq;
namespace PluralKit.Core
{
public class AccountPatch: PatchObject
@ -9,5 +11,15 @@ namespace PluralKit.Core
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
.With("allow_autoproxy", AllowAutoproxy)
);
public JObject ToJson()
{
var o = new JObject();
if (AllowAutoproxy.IsPresent)
o.Add("allow_autoproxy", AllowAutoproxy.Value);
return o;
}
}
}

View file

@ -91,5 +91,51 @@ namespace PluralKit.Core
return patch;
}
public JObject ToJson()
{
var o = new JObject();
if (Name.IsPresent)
o.Add("name", Name.Value);
if (Hid.IsPresent)
o.Add("id", Hid.Value);
if (DisplayName.IsPresent)
o.Add("display_name", DisplayName.Value);
if (Description.IsPresent)
o.Add("description", Description.Value);
if (Icon.IsPresent)
o.Add("icon", Icon.Value);
if (BannerImage.IsPresent)
o.Add("banner", BannerImage.Value);
if (Color.IsPresent)
o.Add("color", Color.Value);
if (
DescriptionPrivacy.IsPresent
|| IconPrivacy.IsPresent
|| ListPrivacy.IsPresent
|| Visibility.IsPresent
)
{
var p = new JObject();
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (IconPrivacy.IsPresent)
p.Add("icon_privacy", IconPrivacy.Value.ToJsonString());
if (ListPrivacy.IsPresent)
p.Add("list_privacy", ListPrivacy.Value.ToJsonString());
if (Visibility.IsPresent)
p.Add("visibilithy", Visibility.Value.ToJsonString());
o.Add("privacy", p);
}
return o;
}
}
}

View file

@ -38,5 +38,18 @@ namespace PluralKit.Core
return patch;
}
public JObject ToJson()
{
var o = new JObject();
if (DisplayName.IsPresent)
o.Add("display_name", DisplayName.Value);
if (AvatarUrl.IsPresent)
o.Add("avatar_url", AvatarUrl.Value);
return o;
}
}
}

View file

@ -192,5 +192,77 @@ namespace PluralKit.Core
return patch;
}
public JObject ToJson()
{
var o = new JObject();
if (Name.IsPresent)
o.Add("name", Name.Value);
if (Hid.IsPresent)
o.Add("id", Hid.Value);
if (DisplayName.IsPresent)
o.Add("display_name", DisplayName.Value);
if (AvatarUrl.IsPresent)
o.Add("avatar_url", AvatarUrl.Value);
if (BannerImage.IsPresent)
o.Add("banner", BannerImage.Value);
if (Color.IsPresent)
o.Add("color", Color.Value);
if (Birthday.IsPresent)
o.Add("birthday", Birthday.Value?.FormatExport());
if (Pronouns.IsPresent)
o.Add("pronouns", Pronouns.Value);
if (Description.IsPresent)
o.Add("description", Description.Value);
if (ProxyTags.IsPresent)
{
var tagArray = new JArray();
foreach (var tag in ProxyTags.Value)
tagArray.Add(new JObject { { "prefix", tag.Prefix }, { "suffix", tag.Suffix } });
o.Add("proxy_tags", tagArray);
}
if (KeepProxy.IsPresent)
o.Add("keep_proxy", KeepProxy.Value);
if (
Visibility.IsPresent
|| NamePrivacy.IsPresent
|| DescriptionPrivacy.IsPresent
|| PronounPrivacy.IsPresent
|| BirthdayPrivacy.IsPresent
|| AvatarPrivacy.IsPresent
|| MetadataPrivacy.IsPresent
)
{
var p = new JObject();
if (Visibility.IsPresent)
p.Add("visibility", Visibility.Value.ToJsonString());
if (NamePrivacy.IsPresent)
p.Add("name_privacy", NamePrivacy.Value.ToJsonString());
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (PronounPrivacy.IsPresent)
p.Add("pronoun_privacy", PronounPrivacy.Value.ToJsonString());
if (BirthdayPrivacy.IsPresent)
p.Add("birthday_privacy", BirthdayPrivacy.Value.ToJsonString());
if (AvatarPrivacy.IsPresent)
p.Add("avatar_privacy", AvatarPrivacy.Value.ToJsonString());
if (MetadataPrivacy.IsPresent)
p.Add("metadata_privacy", MetadataPrivacy.Value.ToJsonString());
o.Add("privacy", p);
}
return o;
}
}
}

View file

@ -55,5 +55,27 @@ namespace PluralKit.Core
return patch;
}
public JObject ToJson(string memberRef)
{
var o = new JObject();
if (ProxyEnabled.IsPresent)
o.Add("proxying_enabled", ProxyEnabled.Value);
if (AutoproxyMode.IsPresent)
o.Add("autoproxy_mode", AutoproxyMode.Value.ToString().ToLower());
if (AutoproxyMember.IsPresent)
o.Add("autoproxy_member", memberRef);
if (Tag.IsPresent)
o.Add("tag", Tag.Value);
if (TagEnabled.IsPresent)
o.Add("tag_enabled", TagEnabled.Value);
return o;
}
}
}

View file

@ -20,6 +20,8 @@ namespace PluralKit.Core
public Partial<string?> BannerImage { get; set; }
public Partial<string?> Color { get; set; }
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; }
@ -40,6 +42,8 @@ namespace PluralKit.Core
.With("banner_image", BannerImage)
.With("color", Color)
.With("token", Token)
.With("webhook_url", WebhookUrl)
.With("webhook_token", WebhookToken)
.With("ui_tz", UiTz)
.With("description_privacy", DescriptionPrivacy)
.With("member_list_privacy", MemberListPrivacy)
@ -126,5 +130,57 @@ namespace PluralKit.Core
return patch;
}
public JObject ToJson()
{
var o = new JObject();
if (Name.IsPresent)
o.Add("name", Name.Value);
if (Hid.IsPresent)
o.Add("id", Hid.Value);
if (Description.IsPresent)
o.Add("description", Description.Value);
if (Tag.IsPresent)
o.Add("tag", Tag.Value);
if (AvatarUrl.IsPresent)
o.Add("avatar_url", AvatarUrl.Value);
if (BannerImage.IsPresent)
o.Add("banner", BannerImage.Value);
if (Color.IsPresent)
o.Add("color", Color.Value);
if (UiTz.IsPresent)
o.Add("timezone", UiTz.Value);
if (
DescriptionPrivacy.IsPresent
|| MemberListPrivacy.IsPresent
|| GroupListPrivacy.IsPresent
|| FrontPrivacy.IsPresent
|| FrontHistoryPrivacy.IsPresent
)
{
var p = new JObject();
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (MemberListPrivacy.IsPresent)
p.Add("member_list_privacy", MemberListPrivacy.Value.ToJsonString());
if (GroupListPrivacy.IsPresent)
p.Add("group_list_privacy", GroupListPrivacy.Value.ToJsonString());
if (FrontPrivacy.IsPresent)
p.Add("front_privacy", FrontPrivacy.Value.ToJsonString());
if (FrontHistoryPrivacy.IsPresent)
p.Add("front_history_privacy", FrontHistoryPrivacy.Value.ToJsonString());
o.Add("privacy", p);
}
return o;
}
}
}

View file

@ -14,6 +14,8 @@ namespace PluralKit.Core
builder.RegisterType<Database>().As<IDatabase>().SingleInstance();
builder.RegisterType<ModelRepository>().AsSelf().SingleInstance();
builder.RegisterType<DispatchService>().AsSelf().SingleInstance();
builder.Populate(new ServiceCollection().AddMemoryCache());
}
}

View file

@ -18,12 +18,14 @@ namespace PluralKit.Core
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly ILogger _logger;
private readonly DispatchService _dispatch;
public DataFileService(IDatabase db, ModelRepository repo, ILogger logger)
public DataFileService(IDatabase db, ModelRepository repo, ILogger logger, DispatchService dispatch)
{
_db = db;
_repo = repo;
_logger = logger;
_dispatch = dispatch;
}
public async Task<JObject> ExportSystem(PKSystem system)
@ -72,7 +74,7 @@ namespace PluralKit.Core
await using var conn = await _db.Obtain();
await using var tx = await conn.BeginTransactionAsync();
return await BulkImporter.PerformImport(conn, tx, _repo, _logger, userId, system, importFile, confirmFunc);
return await BulkImporter.PerformImport(conn, tx, _repo, _logger, _dispatch, userId, system, importFile, confirmFunc);
}
}

View file

@ -34,7 +34,7 @@ namespace PluralKit.Core
private ImportResultNew _result = new();
internal static async Task<ImportResultNew> PerformImport(IPKConnection conn, IPKTransaction tx, ModelRepository repo, ILogger logger,
ulong userId, PKSystem? system, JObject importFile, Func<string, Task> confirmFunc)
DispatchService dispatch, ulong userId, PKSystem? system, JObject importFile, Func<string, Task> confirmFunc)
{
await using var importer = new BulkImporter()
{
@ -82,6 +82,11 @@ namespace PluralKit.Core
throw new ImportException("File type is unknown.");
importer._result.Success = true;
await tx.CommitAsync();
_ = dispatch.Dispatch(system.Id, new UpdateDispatchData()
{
Event = DispatchEvent.SUCCESSFUL_IMPORT
});
}
catch (ImportException e)
{