diff --git a/PluralKit.API/Controllers/v2/DiscordControllerV2.cs b/PluralKit.API/Controllers/v2/DiscordControllerV2.cs index 89d3d806..a0cde510 100644 --- a/PluralKit.API/Controllers/v2/DiscordControllerV2.cs +++ b/PluralKit.API/Controllers/v2/DiscordControllerV2.cs @@ -24,11 +24,7 @@ public class DiscordControllerV2: PKControllerBase if (settings == null) throw Errors.SystemGuildNotFound; - PKMember member = null; - if (settings.AutoproxyMember != null) - member = await _repo.GetMember(settings.AutoproxyMember.Value); - - return Ok(settings.ToJson(member?.Uuid.ToString())); + return Ok(settings.ToJson()); } [HttpPatch("systems/{systemRef}/guilds/{guild_id}")] @@ -42,61 +38,14 @@ public class DiscordControllerV2: PKControllerBase if (settings == null) throw Errors.SystemGuildNotFound; - MemberId? memberId = null; - if (data.ContainsKey("autoproxy_member")) - { - if (data["autoproxy_member"].Type != JTokenType.Null) - { - var member = await ResolveMember(data.Value("autoproxy_member")); - if (member == null) - throw Errors.MemberNotFound; - - memberId = member.Id; - } - } - else - { - memberId = settings.AutoproxyMember; - } - - var patch = SystemGuildPatch.FromJson(data, memberId); + var patch = SystemGuildPatch.FromJson(data); patch.AssertIsValid(); if (patch.Errors.Count > 0) throw new ModelParseError(patch.Errors); - // this is less than great, but at least it's legible - if (patch.AutoproxyMember.Value == null) - { - if (patch.AutoproxyMode.IsPresent) - { - if (patch.AutoproxyMode.Value == AutoproxyMode.Member) - throw Errors.MissingAutoproxyMember; - } - else if (settings.AutoproxyMode == AutoproxyMode.Member) - { - throw Errors.MissingAutoproxyMember; - } - } - else - { - if (patch.AutoproxyMode.IsPresent) - { - if (patch.AutoproxyMode.Value == AutoproxyMode.Latch) - throw Errors.PatchLatchMemberError; - } - else if (settings.AutoproxyMode == AutoproxyMode.Latch) - { - throw Errors.PatchLatchMemberError; - } - } - var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch); - - PKMember? newMember = null; - if (newSettings.AutoproxyMember != null) - newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value); - return Ok(newSettings.ToJson(newMember?.Hid)); + return Ok(newSettings.ToJson()); } [HttpGet("members/{memberRef}/guilds/{guild_id}")] diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 2101b505..db0af3ba 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -16,25 +16,29 @@ public class Autoproxy // no need to check account here, it's already done at CommandTree ctx.CheckGuildContext(); + // for now, just for guild + // this also creates settings if there are none present + var settings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, ctx.Guild.Id, null); + if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) - await AutoproxyOff(ctx); + await AutoproxyOff(ctx, settings); else if (ctx.Match("latch", "last", "proxy", "stick", "sticky")) - await AutoproxyLatch(ctx); + await AutoproxyLatch(ctx, settings); else if (ctx.Match("front", "fronter", "switch")) - await AutoproxyFront(ctx); + await AutoproxyFront(ctx, settings); else if (ctx.Match("member")) throw new PKSyntaxError("Member-mode autoproxy must target a specific member. Use the `pk;autoproxy ` command, where `member` is the name or ID of a member in your system."); else if (await ctx.MatchMember() is PKMember member) await AutoproxyMember(ctx, member); else if (!ctx.HasNext()) - await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx)); + await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings)); else throw new PKSyntaxError($"Invalid autoproxy mode {ctx.PopArgument().AsCode()}."); } - private async Task AutoproxyOff(Context ctx) + private async Task AutoproxyOff(Context ctx, AutoproxySettings settings) { - if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Off) + if (settings.AutoproxyMode == AutoproxyMode.Off) { await ctx.Reply($"{Emojis.Note} Autoproxy is already off in this server."); } @@ -45,9 +49,9 @@ public class Autoproxy } } - private async Task AutoproxyLatch(Context ctx) + private async Task AutoproxyLatch(Context ctx, AutoproxySettings settings) { - if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Latch) + if (settings.AutoproxyMode == AutoproxyMode.Latch) { await ctx.Reply($"{Emojis.Note} Autoproxy is already set to latch mode in this server. If you want to disable autoproxying, use `pk;autoproxy off`."); } @@ -58,9 +62,9 @@ public class Autoproxy } } - private async Task AutoproxyFront(Context ctx) + private async Task AutoproxyFront(Context ctx, AutoproxySettings settings) { - if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Front) + if (settings.AutoproxyMode == AutoproxyMode.Front) { await ctx.Reply($"{Emojis.Note} Autoproxy is already set to front mode in this server. If you want to disable autoproxying, use `pk;autoproxy off`."); } @@ -75,11 +79,13 @@ public class Autoproxy { ctx.CheckOwnMember(member); + // todo: why does this not throw an error if the member is already set + await UpdateAutoproxy(ctx, AutoproxyMode.Member, member.Id); await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server."); } - private async Task CreateAutoproxyStatusEmbed(Context ctx) + private async Task CreateAutoproxyStatusEmbed(Context ctx, AutoproxySettings settings) { var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member" + "\n**pk;autoproxy front** - Autoproxies as current (first) fronter" @@ -88,14 +94,16 @@ public class Autoproxy .Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); var fronters = ctx.MessageContext.LastSwitchMembers; - var relevantMember = ctx.MessageContext.AutoproxyMode switch + var relevantMember = settings.AutoproxyMode switch { AutoproxyMode.Front => fronters.Length > 0 ? await ctx.Repository.GetMember(fronters[0]) : null, - AutoproxyMode.Member when ctx.MessageContext.AutoproxyMember.HasValue => await ctx.Repository.GetMember(ctx.MessageContext.AutoproxyMember.Value), + AutoproxyMode.Member when settings.AutoproxyMember.HasValue => await ctx.Repository.GetMember(settings.AutoproxyMember.Value), _ => null }; - switch (ctx.MessageContext.AutoproxyMode) + Console.WriteLine(settings.AutoproxyMode); + + switch (settings.AutoproxyMode) { case AutoproxyMode.Off: eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); @@ -136,9 +144,7 @@ public class Autoproxy private async Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) { - await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id); - - var patch = new SystemGuildPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember }; - await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, patch); + var patch = new AutoproxyPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember }; + await ctx.Repository.UpdateAutoproxy(ctx.System.Id, ctx.Guild.Id, null, patch); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index cbfa79ae..05623a4d 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -259,11 +259,16 @@ public class Checks var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, msg.ChannelId); var members = (await ctx.Repository.GetProxyMembers(msg.Author.Id, channel.GuildId.Value)).ToList(); + // for now this is just server + var autoproxySettings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, channel.GuildId.Value, null); + + // todo: match unlatch + // Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message. try { _proxy.ShouldProxy(channel, msg, context); - _matcher.TryMatch(context, members, out var match, msg.Content, msg.Attachments.Length > 0, + _matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, context.AllowAutoproxy); await ctx.Reply("I'm not sure why this message was not proxied, sorry."); diff --git a/PluralKit.Bot/Proxy/ProxyMatcher.cs b/PluralKit.Bot/Proxy/ProxyMatcher.cs index f7579bd2..9c4a5b68 100644 --- a/PluralKit.Bot/Proxy/ProxyMatcher.cs +++ b/PluralKit.Bot/Proxy/ProxyMatcher.cs @@ -18,12 +18,12 @@ public class ProxyMatcher _clock = clock; } - public bool TryMatch(MessageContext ctx, IReadOnlyCollection members, out ProxyMatch match, + public bool TryMatch(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection members, out ProxyMatch match, string messageContent, bool hasAttachments, bool allowAutoproxy) { if (TryMatchTags(members, messageContent, hasAttachments, out match)) return true; - if (allowAutoproxy && TryMatchAutoproxy(ctx, members, messageContent, out match)) return true; + if (allowAutoproxy && TryMatchAutoproxy(ctx, settings, members, messageContent, out match)) return true; return false; } @@ -37,7 +37,7 @@ public class ProxyMatcher return hasAttachments || match.Content.Trim().Length > 0; } - private bool TryMatchAutoproxy(MessageContext ctx, IReadOnlyCollection members, + private bool TryMatchAutoproxy(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection members, string messageContent, out ProxyMatch match) { @@ -49,41 +49,41 @@ public class ProxyMatcher "This message matches none of your proxy tags, and it was not autoproxied because it starts with a backslash (`\\`)."); // Find the member we should autoproxy (null if none) - var member = ctx.AutoproxyMode switch + var member = settings.AutoproxyMode switch { - AutoproxyMode.Member when ctx.AutoproxyMember != null => - members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), + AutoproxyMode.Member when settings.AutoproxyMember != null => + members.FirstOrDefault(m => m.Id == settings.AutoproxyMember), AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), - AutoproxyMode.Latch when ctx.LastMessageMember != null => - members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value), + AutoproxyMode.Latch when settings.AutoproxyMember != null => + members.FirstOrDefault(m => m.Id == settings.AutoproxyMember.Value), _ => null }; // Throw an error if the member is null, message varies depending on autoproxy mode if (member == null) { - if (ctx.AutoproxyMode == AutoproxyMode.Front) + if (settings.AutoproxyMode == AutoproxyMode.Front) throw new ProxyService.ProxyChecksFailedException( "You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch ` to log a new switch."); - if (ctx.AutoproxyMode == AutoproxyMode.Member) + if (settings.AutoproxyMode == AutoproxyMode.Member) throw new ProxyService.ProxyChecksFailedException( "You are using member-specific autoproxy with an invalid member. Was this member deleted?"); - if (ctx.AutoproxyMode == AutoproxyMode.Latch) + if (settings.AutoproxyMode == AutoproxyMode.Latch) throw new ProxyService.ProxyChecksFailedException( "You are using autoproxy latch, but have not sent any messages yet in this server. Please send a message using proxy tags first."); throw new ProxyService.ProxyChecksFailedException( "This message matches none of your proxy tags and autoproxy is not enabled."); } - if (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy) + if (settings.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy) throw new ProxyService.ProxyChecksFailedException( "This member has autoproxy disabled. To enable it, use `pk;m autoproxy on`."); // Moved the IsLatchExpired() check to here, so that an expired latch and a latch without any previous messages throw different errors - if (ctx.AutoproxyMode == AutoproxyMode.Latch && IsLatchExpired(ctx)) + if (settings.AutoproxyMode == AutoproxyMode.Latch && IsLatchExpired(ctx, settings)) throw new ProxyService.ProxyChecksFailedException( "Latch-mode autoproxy has timed out. Please send a new message using proxy tags."); @@ -99,16 +99,14 @@ public class ProxyMatcher return true; } - private bool IsLatchExpired(MessageContext ctx) + private bool IsLatchExpired(MessageContext ctx, AutoproxySettings settings) { - if (ctx.LastMessage == null) return true; if (ctx.LatchTimeout == 0) return false; var timeout = ctx.LatchTimeout.HasValue ? Duration.FromSeconds(ctx.LatchTimeout.Value) : DefaultLatchExpiryTime; - var timestamp = DiscordUtils.SnowflakeToInstant(ctx.LastMessage.Value); - return _clock.GetCurrentInstant() - timestamp > timeout; + return _clock.GetCurrentInstant() - settings.LastLatchTimestamp > timeout; } } \ No newline at end of file diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index e9666e2b..64c9b067 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -32,10 +32,11 @@ public class ProxyService private readonly ModelRepository _repo; private readonly DiscordApiClient _rest; private readonly WebhookExecutorService _webhookExecutor; + private readonly NodaTime.IClock _clock; public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor, DispatchService dispatch, IDatabase db, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, - IDiscordCache cache, DiscordApiClient rest, LastMessageCacheService lastMessage) + NodaTime.IClock clock, IDiscordCache cache, DiscordApiClient rest, LastMessageCacheService lastMessage) { _logChannel = logChannel; _webhookExecutor = webhookExecutor; @@ -47,6 +48,7 @@ public class ProxyService _cache = cache; _lastMessage = lastMessage; _rest = rest; + _clock = clock; _logger = logger.ForContext(); } @@ -56,6 +58,17 @@ public class ProxyService if (!ShouldProxy(channel, message, ctx)) return false; + var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null); + + if (autoproxySettings.AutoproxyMode == AutoproxyMode.Latch && IsUnlatch(message)) + { + // "unlatch" + await _repo.UpdateAutoproxy(ctx.SystemId.Value, guild.Id, null, new() { + AutoproxyMember = null + }); + return false; + } + var rootChannel = await _cache.GetRootChannel(message.ChannelId); List members; @@ -63,7 +76,7 @@ public class ProxyService using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime)) members = (await _repo.GetProxyMembers(message.Author.Id, message.GuildId!.Value)).ToList(); - if (!_matcher.TryMatch(ctx, members, out var match, message.Content, message.Attachments.Length > 0, + if (!_matcher.TryMatch(ctx, autoproxySettings, members, out var match, message.Content, message.Attachments.Length > 0, allowAutoproxy)) return false; // this is hopefully temporary, so not putting it into a separate method @@ -84,7 +97,7 @@ public class ProxyService var allowEmbeds = senderPermissions.HasFlag(PermissionSet.EmbedLinks); // Everything's in order, we can execute the proxy! - await ExecuteProxy(message, ctx, match, allowEveryone, allowEmbeds); + await ExecuteProxy(message, ctx, autoproxySettings, match, allowEveryone, allowEmbeds); return true; } @@ -129,7 +142,7 @@ public class ProxyService return true; } - private async Task ExecuteProxy(Message trigger, MessageContext ctx, + private async Task ExecuteProxy(Message trigger, MessageContext ctx, AutoproxySettings autoproxySettings, ProxyMatch match, bool allowEveryone, bool allowEmbeds) { // Create reply embed @@ -171,7 +184,7 @@ public class ProxyService Stickers = trigger.StickerItems, AllowEveryone = allowEveryone }); - await HandleProxyExecutedActions(ctx, trigger, proxyMessage, match); + await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match); } private async Task<(string?, string?)> FetchReferencedMessageAuthorInfo(Message trigger, Message referenced) @@ -290,7 +303,11 @@ public class ProxyService private string FixSameNameInner(string name) => $"{name}\u17b5"; - private async Task HandleProxyExecutedActions(MessageContext ctx, Message triggerMessage, Message proxyMessage, ProxyMatch match) + public static bool IsUnlatch(Message message) + => message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\"); + + private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings, + Message triggerMessage, Message proxyMessage, ProxyMatch match) { var sentMessage = new PKMessage { @@ -308,6 +325,14 @@ public class ProxyService Task LogMessageToChannel() => _logChannel.LogMessage(ctx, sentMessage, triggerMessage, proxyMessage).AsTask(); + Task SaveLatchAutoproxy() => autoproxySettings.AutoproxyMode == AutoproxyMode.Latch + ? _repo.UpdateAutoproxy(ctx.SystemId.Value, triggerMessage.GuildId, null, new() + { + AutoproxyMember = match.Member.Id, + LastLatchTimestamp = _clock.GetCurrentInstant(), + }) + : Task.CompletedTask; + Task DispatchWebhook() => _dispatch.Dispatch(ctx.SystemId.Value, sentMessage); async Task DeleteProxyTriggerMessage() @@ -333,6 +358,7 @@ public class ProxyService DeleteProxyTriggerMessage(), SaveMessageInDatabase(), LogMessageToChannel(), + SaveLatchAutoproxy(), DispatchWebhook() ); } diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 790b8138..ef4e6daa 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -15,10 +15,6 @@ public class MessageContext public bool InLogBlacklist { get; } public bool LogCleanupEnabled { get; } public bool ProxyEnabled { get; } - public AutoproxyMode AutoproxyMode { get; } - public MemberId? AutoproxyMember { get; } - public ulong? LastMessage { get; } - public MemberId? LastMessageMember { get; } public SwitchId? LastSwitch { get; } public MemberId[] LastSwitchMembers { get; } = new MemberId[0]; public Instant? LastSwitchTimestamp { get; } diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 16d2acac..3c7857fe 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -6,10 +6,6 @@ in_log_blacklist bool, log_cleanup_enabled bool, proxy_enabled bool, - autoproxy_mode int, - autoproxy_member int, - last_message bigint, - last_message_member int, last_switch int, last_switch_members int[], last_switch_timestamp timestamp, @@ -28,8 +24,7 @@ as $$ left join system_config on system_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), - last_message as (select * from messages where messages.guild = guild_id and messages.sender = account_id order by mid desc limit 1) + guild as (select * from servers where id = guild_id) select system.id as system_id, guild.log_channel, @@ -37,10 +32,6 @@ as $$ (channel_id = any(guild.log_blacklist)) as in_log_blacklist, coalesce(guild.log_cleanup_enabled, false), coalesce(system_guild.proxy_enabled, true) as proxy_enabled, - coalesce(system_guild.autoproxy_mode, 1) as autoproxy_mode, - system_guild.autoproxy_member, - last_message.mid as last_message, - last_message.member as last_message_member, system_last_switch.switch as last_switch, system_last_switch.members as last_switch_members, system_last_switch.timestamp as last_switch_timestamp, @@ -55,7 +46,6 @@ as $$ from (select 1) as _placeholder left join system 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 left join system_guild on system_guild.system = system.id and system_guild.guild = guild_id $$ language sql stable rows 1; diff --git a/PluralKit.Core/Database/Migrations/27.sql b/PluralKit.Core/Database/Migrations/27.sql new file mode 100644 index 00000000..a344da73 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/27.sql @@ -0,0 +1,36 @@ +-- schema version 27 +-- autoproxy locations + +-- mode pseudo-enum: (copied from 3.sql) +-- 1 = autoproxy off +-- 2 = front mode (first fronter) +-- 3 = latch mode (last proxyer) +-- 4 = member mode (specific member) + +create table autoproxy ( + system int references systems(id) on delete cascade, + channel_id bigint, + guild_id bigint, + autoproxy_mode int check (mode in (1, 2, 3, 4)) not null default 1, + autoproxy_member int references members(id) on delete set null, + last_latch_timestamp timestamp, + check ( + (channel_id = 0 and guild_id = 0) + or (channel_id != 0 and guild_id = 0) + or (channel_id = 0 and guild_id != 0) + ), + primary key (system, channel_id, guild_id) +); + +insert into autoproxy select + system, + 0 as channel_id, + guild as guild_id, + autoproxy_mode, + autoproxy_member +from system_guild; + +alter table system_guild drop column autoproxy_mode; +alter table system_guild drop column autoproxy_member; + +update info set schema_version = 27; \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs b/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs new file mode 100644 index 00000000..5e0ecf64 --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs @@ -0,0 +1,35 @@ +using Dapper; + +using SqlKata; + +namespace PluralKit.Core; + +public partial class ModelRepository +{ + public async Task UpdateAutoproxy(SystemId system, ulong? guildId, ulong? channelId, AutoproxyPatch patch) + { + var locationStr = guildId != null ? "guild" : (channelId != null ? "channel" : "global"); + _logger.Information("Updated autoproxy for {SystemId} in location {location}: {@AutoproxyPatch}", system, locationStr, patch); + + var query = patch.Apply(new Query("autoproxy") + .Where("system", system) + .Where("guild_id", guildId ?? 0) + .Where("channel_id", channelId ?? 0) + ); + _ = _dispatch.Dispatch(system, guildId, channelId, patch); + await _db.ExecuteQuery(query); + } + + // todo: this might break with differently scoped autoproxy + public async Task GetAutoproxySettings(SystemId system, ulong? guildId, ulong? channelId) + => await _db.QueryFirst(new Query("autoproxy").AsInsert(new { + system = system, + guild_id = guildId ?? 0, + channel_id = channelId ?? 0, + }) + .Where("system", system) + .Where("guild_id", guildId ?? 0) + .Where("channel_id", channelId ?? 0), + "on conflict (system, guild_id, channel_id) do update set system = $1 returning *" + ); +} \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 777f3eb6..ed22a618 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 = 26; + private const int TargetSchemaVersion = 27; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Dispatch/DispatchService.cs b/PluralKit.Core/Dispatch/DispatchService.cs index 48cdfd3d..ce0a2697 100644 --- a/PluralKit.Core/Dispatch/DispatchService.cs +++ b/PluralKit.Core/Dispatch/DispatchService.cs @@ -38,6 +38,12 @@ public class DispatchService } } + public Task Dispatch(SystemId systemId, ulong? guildId, ulong? channelId, AutoproxyPatch patch) + { + // todo + return Task.CompletedTask; + } + public async Task Dispatch(SystemId systemId, UpdateDispatchData data) { if (data.EventData != null && data.EventData.Count == 0) @@ -159,18 +165,11 @@ public class DispatchService 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.EventData = patch.ToJson(memberRef, guild_id); + data.EventData = patch.ToJson(guild_id); _logger.Debug("Dispatching webhook for system {SystemId} in guild {GuildId}", system.Id, guild_id); await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody()); diff --git a/PluralKit.Core/Models/Autoproxy.cs b/PluralKit.Core/Models/Autoproxy.cs new file mode 100644 index 00000000..a8df8078 --- /dev/null +++ b/PluralKit.Core/Models/Autoproxy.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json.Linq; + +using NodaTime; + +namespace PluralKit.Core; + +public enum AutoproxyMode +{ + Off = 1, + Front = 2, + Latch = 3, + Member = 4 +} + +public class AutoproxySettings +{ + public AutoproxyMode AutoproxyMode { get; } + public MemberId? AutoproxyMember { get; } + public Instant LastLatchTimestamp { get; } +} + +public static class AutoproxyExt +{ + public static JObject ToJson(this AutoproxySettings settings, string? memberHid = null) + { + var o = new JObject(); + + // tbd + o.Add("autoproxy_mode", settings.AutoproxyMode.ToString().ToLower()); + o.Add("autoproxy_member", memberHid); + + return o; + } + + public static (AutoproxyMode?, ValidationError?) ParseAutoproxyMode(this JToken o) + { + if (o.Type == JTokenType.Null) + return (AutoproxyMode.Off, null); + if (o.Type != JTokenType.String) + return (null, new ValidationError("autoproxy_mode")); + + var value = o.Value(); + + switch (value) + { + case "off": + return (AutoproxyMode.Off, null); + case "front": + return (AutoproxyMode.Front, null); + case "latch": + return (AutoproxyMode.Latch, null); + case "member": + return (AutoproxyMode.Member, null); + default: + return (null, + new ValidationError("autoproxy_mode", $"Value '{value}' is not a valid autoproxy mode.")); + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/AutoproxyPatch.cs b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs new file mode 100644 index 00000000..4a6a036d --- /dev/null +++ b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; + +using NodaTime; + +using SqlKata; + +namespace PluralKit.Core; + +public class AutoproxyPatch : PatchObject +{ + public Partial AutoproxyMode { get; set; } + public Partial AutoproxyMember { get; set; } + + public Partial LastLatchTimestamp { get; set; } + + public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper + .With("autoproxy_mode", AutoproxyMode) + .With("autoproxy_member", AutoproxyMember) + .With("last_latch_timestamp", LastLatchTimestamp) + ); +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs index 2309a62d..628877e9 100644 --- a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs @@ -9,15 +9,11 @@ namespace PluralKit.Core; public class SystemGuildPatch: PatchObject { public Partial ProxyEnabled { get; set; } - public Partial AutoproxyMode { get; set; } - public Partial AutoproxyMember { get; set; } public Partial Tag { get; set; } public Partial TagEnabled { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("proxy_enabled", ProxyEnabled) - .With("autoproxy_mode", AutoproxyMode) - .With("autoproxy_member", AutoproxyMember) .With("tag", Tag) .With("tag_enabled", TagEnabled) ); @@ -29,24 +25,13 @@ public class SystemGuildPatch: PatchObject } #nullable disable - public static SystemGuildPatch FromJson(JObject o, MemberId? memberId) + public static SystemGuildPatch FromJson(JObject o) { var patch = new SystemGuildPatch(); if (o.ContainsKey("proxying_enabled") && o["proxying_enabled"].Type != JTokenType.Null) patch.ProxyEnabled = o.Value("proxying_enabled"); - if (o.ContainsKey("autoproxy_mode")) - { - var (val, err) = o["autoproxy_mode"].ParseAutoproxyMode(); - if (err != null) - patch.Errors.Add(err); - else - patch.AutoproxyMode = val.Value; - } - - patch.AutoproxyMember = memberId; - if (o.ContainsKey("tag")) patch.Tag = o.Value("tag").NullIfEmpty(); @@ -56,7 +41,7 @@ public class SystemGuildPatch: PatchObject return patch; } - public JObject ToJson(string memberRef, ulong guild_id) + public JObject ToJson(ulong guild_id) { var o = new JObject(); @@ -65,12 +50,6 @@ public class SystemGuildPatch: PatchObject 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); diff --git a/PluralKit.Core/Models/SystemGuildSettings.cs b/PluralKit.Core/Models/SystemGuildSettings.cs index 07905b4c..e8b88e92 100644 --- a/PluralKit.Core/Models/SystemGuildSettings.cs +++ b/PluralKit.Core/Models/SystemGuildSettings.cs @@ -1,68 +1,26 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; namespace PluralKit.Core; -[JsonConverter(typeof(StringEnumConverter))] -public enum AutoproxyMode -{ - Off = 1, - Front = 2, - Latch = 3, - Member = 4 -} - public class SystemGuildSettings { public ulong Guild { get; } public SystemId System { get; } public bool ProxyEnabled { get; } = true; - - public AutoproxyMode AutoproxyMode { get; } = AutoproxyMode.Off; - public MemberId? AutoproxyMember { get; } - public string? Tag { get; } public bool TagEnabled { get; } } public static class SystemGuildExt { - public static JObject ToJson(this SystemGuildSettings settings, string? memberHid = null) + public static JObject ToJson(this SystemGuildSettings settings) { var o = new JObject(); o.Add("proxying_enabled", settings.ProxyEnabled); - o.Add("autoproxy_mode", settings.AutoproxyMode.ToString().ToLower()); - o.Add("autoproxy_member", memberHid); o.Add("tag", settings.Tag); o.Add("tag_enabled", settings.TagEnabled); return o; } - - public static (AutoproxyMode?, ValidationError?) ParseAutoproxyMode(this JToken o) - { - if (o.Type == JTokenType.Null) - return (AutoproxyMode.Off, null); - if (o.Type != JTokenType.String) - return (null, new ValidationError("autoproxy_mode")); - - var value = o.Value(); - - switch (value) - { - case "off": - return (AutoproxyMode.Off, null); - case "front": - return (AutoproxyMode.Front, null); - case "latch": - return (AutoproxyMode.Latch, null); - case "member": - return (AutoproxyMode.Member, null); - default: - return (null, - new ValidationError("autoproxy_mode", $"Value '{value}' is not a valid autoproxy mode.")); - } - } } \ No newline at end of file diff --git a/docs/content/api/models.md b/docs/content/api/models.md index f8737a63..124af596 100644 --- a/docs/content/api/models.md +++ b/docs/content/api/models.md @@ -120,11 +120,10 @@ Every PluralKit entity has two IDs: a short (5-character) ID and a longer UUID. |---|---|---| |guild_id|snowflake|only sent if the guild ID isn't already known (in dispatch payloads)| |proxying_enabled|boolean|| -|autoproxy_mode|autoproxy mode enum|| -|autoproxy_member|?string|must be set if autoproxy_mode is `member`| |tag|?string|79-character limit| |tag_enabled|boolean|| + ### Member guild settings model