feat: rewrite database schema for localized autoproxy

This commit is contained in:
spiral 2022-03-21 23:43:33 -04:00
parent ca108813b7
commit 982812333b
No known key found for this signature in database
GPG Key ID: 244A11E4B0BCF40E
16 changed files with 245 additions and 188 deletions

View File

@ -24,11 +24,7 @@ public class DiscordControllerV2: PKControllerBase
if (settings == null) if (settings == null)
throw Errors.SystemGuildNotFound; throw Errors.SystemGuildNotFound;
PKMember member = null; return Ok(settings.ToJson());
if (settings.AutoproxyMember != null)
member = await _repo.GetMember(settings.AutoproxyMember.Value);
return Ok(settings.ToJson(member?.Uuid.ToString()));
} }
[HttpPatch("systems/{systemRef}/guilds/{guild_id}")] [HttpPatch("systems/{systemRef}/guilds/{guild_id}")]
@ -42,61 +38,14 @@ public class DiscordControllerV2: PKControllerBase
if (settings == null) if (settings == null)
throw Errors.SystemGuildNotFound; throw Errors.SystemGuildNotFound;
MemberId? memberId = null; var patch = SystemGuildPatch.FromJson(data);
if (data.ContainsKey("autoproxy_member"))
{
if (data["autoproxy_member"].Type != JTokenType.Null)
{
var member = await ResolveMember(data.Value<string>("autoproxy_member"));
if (member == null)
throw Errors.MemberNotFound;
memberId = member.Id;
}
}
else
{
memberId = settings.AutoproxyMember;
}
var patch = SystemGuildPatch.FromJson(data, memberId);
patch.AssertIsValid(); patch.AssertIsValid();
if (patch.Errors.Count > 0) if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors); 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); var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch);
return Ok(newSettings.ToJson());
PKMember? newMember = null;
if (newSettings.AutoproxyMember != null)
newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value);
return Ok(newSettings.ToJson(newMember?.Hid));
} }
[HttpGet("members/{memberRef}/guilds/{guild_id}")] [HttpGet("members/{memberRef}/guilds/{guild_id}")]

View File

@ -16,25 +16,29 @@ public class Autoproxy
// no need to check account here, it's already done at CommandTree // no need to check account here, it's already done at CommandTree
ctx.CheckGuildContext(); 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")) 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")) else if (ctx.Match("latch", "last", "proxy", "stick", "sticky"))
await AutoproxyLatch(ctx); await AutoproxyLatch(ctx, settings);
else if (ctx.Match("front", "fronter", "switch")) else if (ctx.Match("front", "fronter", "switch"))
await AutoproxyFront(ctx); await AutoproxyFront(ctx, settings);
else if (ctx.Match("member")) else if (ctx.Match("member"))
throw new PKSyntaxError("Member-mode autoproxy must target a specific member. Use the `pk;autoproxy <member>` command, where `member` is the name or ID of a member in your system."); throw new PKSyntaxError("Member-mode autoproxy must target a specific member. Use the `pk;autoproxy <member>` command, where `member` is the name or ID of a member in your system.");
else if (await ctx.MatchMember() is PKMember member) else if (await ctx.MatchMember() is PKMember member)
await AutoproxyMember(ctx, member); await AutoproxyMember(ctx, member);
else if (!ctx.HasNext()) else if (!ctx.HasNext())
await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx)); await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings));
else else
throw new PKSyntaxError($"Invalid autoproxy mode {ctx.PopArgument().AsCode()}."); 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."); 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`."); 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`."); 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); 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 UpdateAutoproxy(ctx, AutoproxyMode.Member, member.Id);
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server."); await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server.");
} }
private async Task<Embed> CreateAutoproxyStatusEmbed(Context ctx) private async Task<Embed> CreateAutoproxyStatusEmbed(Context ctx, AutoproxySettings settings)
{ {
var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member" var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member"
+ "\n**pk;autoproxy front** - Autoproxies as current (first) fronter" + "\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()})"); .Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})");
var fronters = ctx.MessageContext.LastSwitchMembers; 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.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 _ => null
}; };
switch (ctx.MessageContext.AutoproxyMode) Console.WriteLine(settings.AutoproxyMode);
switch (settings.AutoproxyMode)
{ {
case AutoproxyMode.Off: case AutoproxyMode.Off:
eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); 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) private async Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember)
{ {
await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id); var patch = new AutoproxyPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember };
await ctx.Repository.UpdateAutoproxy(ctx.System.Id, ctx.Guild.Id, null, patch);
var patch = new SystemGuildPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember };
await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, patch);
} }
} }

View File

@ -259,11 +259,16 @@ public class Checks
var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, msg.ChannelId); 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(); 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. // Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message.
try try
{ {
_proxy.ShouldProxy(channel, msg, context); _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); context.AllowAutoproxy);
await ctx.Reply("I'm not sure why this message was not proxied, sorry."); await ctx.Reply("I'm not sure why this message was not proxied, sorry.");

View File

@ -18,12 +18,12 @@ public class ProxyMatcher
_clock = clock; _clock = clock;
} }
public bool TryMatch(MessageContext ctx, IReadOnlyCollection<ProxyMember> members, out ProxyMatch match, public bool TryMatch(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection<ProxyMember> members, out ProxyMatch match,
string messageContent, string messageContent,
bool hasAttachments, bool allowAutoproxy) bool hasAttachments, bool allowAutoproxy)
{ {
if (TryMatchTags(members, messageContent, hasAttachments, out match)) return true; 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; return false;
} }
@ -37,7 +37,7 @@ public class ProxyMatcher
return hasAttachments || match.Content.Trim().Length > 0; return hasAttachments || match.Content.Trim().Length > 0;
} }
private bool TryMatchAutoproxy(MessageContext ctx, IReadOnlyCollection<ProxyMember> members, private bool TryMatchAutoproxy(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection<ProxyMember> members,
string messageContent, string messageContent,
out ProxyMatch match) 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 (`\\`)."); "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) // 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 => AutoproxyMode.Member when settings.AutoproxyMember != null =>
members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), members.FirstOrDefault(m => m.Id == settings.AutoproxyMember),
AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 =>
members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]),
AutoproxyMode.Latch when ctx.LastMessageMember != null => AutoproxyMode.Latch when settings.AutoproxyMember != null =>
members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value), members.FirstOrDefault(m => m.Id == settings.AutoproxyMember.Value),
_ => null _ => null
}; };
// Throw an error if the member is null, message varies depending on autoproxy mode // Throw an error if the member is null, message varies depending on autoproxy mode
if (member == null) if (member == null)
{ {
if (ctx.AutoproxyMode == AutoproxyMode.Front) if (settings.AutoproxyMode == AutoproxyMode.Front)
throw new ProxyService.ProxyChecksFailedException( throw new ProxyService.ProxyChecksFailedException(
"You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch <member>` to log a new switch."); "You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch <member>` to log a new switch.");
if (ctx.AutoproxyMode == AutoproxyMode.Member) if (settings.AutoproxyMode == AutoproxyMode.Member)
throw new ProxyService.ProxyChecksFailedException( throw new ProxyService.ProxyChecksFailedException(
"You are using member-specific autoproxy with an invalid member. Was this member deleted?"); "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( 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."); "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( throw new ProxyService.ProxyChecksFailedException(
"This message matches none of your proxy tags and autoproxy is not enabled."); "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( throw new ProxyService.ProxyChecksFailedException(
"This member has autoproxy disabled. To enable it, use `pk;m <member> autoproxy on`."); "This member has autoproxy disabled. To enable it, use `pk;m <member> autoproxy on`.");
// Moved the IsLatchExpired() check to here, so that an expired latch and a latch without any previous messages throw different errors // 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( throw new ProxyService.ProxyChecksFailedException(
"Latch-mode autoproxy has timed out. Please send a new message using proxy tags."); "Latch-mode autoproxy has timed out. Please send a new message using proxy tags.");
@ -99,16 +99,14 @@ public class ProxyMatcher
return true; 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; if (ctx.LatchTimeout == 0) return false;
var timeout = ctx.LatchTimeout.HasValue var timeout = ctx.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.LatchTimeout.Value) ? Duration.FromSeconds(ctx.LatchTimeout.Value)
: DefaultLatchExpiryTime; : DefaultLatchExpiryTime;
var timestamp = DiscordUtils.SnowflakeToInstant(ctx.LastMessage.Value); return _clock.GetCurrentInstant() - settings.LastLatchTimestamp > timeout;
return _clock.GetCurrentInstant() - timestamp > timeout;
} }
} }

View File

@ -32,10 +32,11 @@ public class ProxyService
private readonly ModelRepository _repo; private readonly ModelRepository _repo;
private readonly DiscordApiClient _rest; private readonly DiscordApiClient _rest;
private readonly WebhookExecutorService _webhookExecutor; private readonly WebhookExecutorService _webhookExecutor;
private readonly NodaTime.IClock _clock;
public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor, public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor,
DispatchService dispatch, IDatabase db, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, 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; _logChannel = logChannel;
_webhookExecutor = webhookExecutor; _webhookExecutor = webhookExecutor;
@ -47,6 +48,7 @@ public class ProxyService
_cache = cache; _cache = cache;
_lastMessage = lastMessage; _lastMessage = lastMessage;
_rest = rest; _rest = rest;
_clock = clock;
_logger = logger.ForContext<ProxyService>(); _logger = logger.ForContext<ProxyService>();
} }
@ -56,6 +58,17 @@ public class ProxyService
if (!ShouldProxy(channel, message, ctx)) if (!ShouldProxy(channel, message, ctx))
return false; 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); var rootChannel = await _cache.GetRootChannel(message.ChannelId);
List<ProxyMember> members; List<ProxyMember> members;
@ -63,7 +76,7 @@ public class ProxyService
using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime)) using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime))
members = (await _repo.GetProxyMembers(message.Author.Id, message.GuildId!.Value)).ToList(); 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; allowAutoproxy)) return false;
// this is hopefully temporary, so not putting it into a separate method // 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); var allowEmbeds = senderPermissions.HasFlag(PermissionSet.EmbedLinks);
// Everything's in order, we can execute the proxy! // 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; return true;
} }
@ -129,7 +142,7 @@ public class ProxyService
return true; 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) ProxyMatch match, bool allowEveryone, bool allowEmbeds)
{ {
// Create reply embed // Create reply embed
@ -171,7 +184,7 @@ public class ProxyService
Stickers = trigger.StickerItems, Stickers = trigger.StickerItems,
AllowEveryone = allowEveryone 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) private async Task<(string?, string?)> FetchReferencedMessageAuthorInfo(Message trigger, Message referenced)
@ -290,7 +303,11 @@ public class ProxyService
private string FixSameNameInner(string name) private string FixSameNameInner(string name)
=> $"{name}\u17b5"; => $"{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 var sentMessage = new PKMessage
{ {
@ -308,6 +325,14 @@ public class ProxyService
Task LogMessageToChannel() => Task LogMessageToChannel() =>
_logChannel.LogMessage(ctx, sentMessage, triggerMessage, proxyMessage).AsTask(); _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); Task DispatchWebhook() => _dispatch.Dispatch(ctx.SystemId.Value, sentMessage);
async Task DeleteProxyTriggerMessage() async Task DeleteProxyTriggerMessage()
@ -333,6 +358,7 @@ public class ProxyService
DeleteProxyTriggerMessage(), DeleteProxyTriggerMessage(),
SaveMessageInDatabase(), SaveMessageInDatabase(),
LogMessageToChannel(), LogMessageToChannel(),
SaveLatchAutoproxy(),
DispatchWebhook() DispatchWebhook()
); );
} }

View File

@ -15,10 +15,6 @@ public class MessageContext
public bool InLogBlacklist { get; } public bool InLogBlacklist { get; }
public bool LogCleanupEnabled { get; } public bool LogCleanupEnabled { get; }
public bool ProxyEnabled { 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 SwitchId? LastSwitch { get; }
public MemberId[] LastSwitchMembers { get; } = new MemberId[0]; public MemberId[] LastSwitchMembers { get; } = new MemberId[0];
public Instant? LastSwitchTimestamp { get; } public Instant? LastSwitchTimestamp { get; }

View File

@ -6,10 +6,6 @@
in_log_blacklist bool, in_log_blacklist bool,
log_cleanup_enabled bool, log_cleanup_enabled bool,
proxy_enabled bool, proxy_enabled bool,
autoproxy_mode int,
autoproxy_member int,
last_message bigint,
last_message_member int,
last_switch int, last_switch int,
last_switch_members int[], last_switch_members int[],
last_switch_timestamp timestamp, last_switch_timestamp timestamp,
@ -28,8 +24,7 @@ as $$
left join system_config on system_config.system = accounts.system 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 left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id
where accounts.uid = account_id), where accounts.uid = account_id),
guild as (select * from servers where id = guild_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)
select select
system.id as system_id, system.id as system_id,
guild.log_channel, guild.log_channel,
@ -37,10 +32,6 @@ as $$
(channel_id = any(guild.log_blacklist)) as in_log_blacklist, (channel_id = any(guild.log_blacklist)) as in_log_blacklist,
coalesce(guild.log_cleanup_enabled, false), coalesce(guild.log_cleanup_enabled, false),
coalesce(system_guild.proxy_enabled, true) as proxy_enabled, 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.switch as last_switch,
system_last_switch.members as last_switch_members, system_last_switch.members as last_switch_members,
system_last_switch.timestamp as last_switch_timestamp, system_last_switch.timestamp as last_switch_timestamp,
@ -55,7 +46,6 @@ as $$
from (select 1) as _placeholder from (select 1) as _placeholder
left join system on true left join system on true
left join guild 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_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 left join system_guild on system_guild.system = system.id and system_guild.guild = guild_id
$$ language sql stable rows 1; $$ language sql stable rows 1;

View File

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

View File

@ -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<AutoproxySettings> GetAutoproxySettings(SystemId system, ulong? guildId, ulong? channelId)
=> await _db.QueryFirst<AutoproxySettings>(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 *"
);
}

View File

@ -9,7 +9,7 @@ namespace PluralKit.Core;
internal class DatabaseMigrator internal class DatabaseMigrator
{ {
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files 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; private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger) public DatabaseMigrator(ILogger logger)

View File

@ -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) public async Task Dispatch(SystemId systemId, UpdateDispatchData data)
{ {
if (data.EventData != null && data.EventData.Count == 0) if (data.EventData != null && data.EventData.Count == 0)
@ -159,18 +165,11 @@ public class DispatchService
if (system.WebhookUrl == null) if (system.WebhookUrl == null)
return; 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(); var data = new UpdateDispatchData();
data.Event = DispatchEvent.UPDATE_SYSTEM_GUILD; data.Event = DispatchEvent.UPDATE_SYSTEM_GUILD;
data.SigningToken = system.WebhookToken; data.SigningToken = system.WebhookToken;
data.SystemId = system.Uuid.ToString(); 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); _logger.Debug("Dispatching webhook for system {SystemId} in guild {GuildId}", system.Id, guild_id);
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody()); await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());

View File

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

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
using NodaTime;
using SqlKata;
namespace PluralKit.Core;
public class AutoproxyPatch : PatchObject
{
public Partial<AutoproxyMode> AutoproxyMode { get; set; }
public Partial<MemberId?> AutoproxyMember { get; set; }
public Partial<Instant> 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)
);
}

View File

@ -9,15 +9,11 @@ namespace PluralKit.Core;
public class SystemGuildPatch: PatchObject public class SystemGuildPatch: PatchObject
{ {
public Partial<bool> ProxyEnabled { get; set; } public Partial<bool> ProxyEnabled { get; set; }
public Partial<AutoproxyMode> AutoproxyMode { get; set; }
public Partial<MemberId?> AutoproxyMember { get; set; }
public Partial<string?> Tag { get; set; } public Partial<string?> Tag { get; set; }
public Partial<bool?> TagEnabled { get; set; } public Partial<bool?> TagEnabled { get; set; }
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
.With("proxy_enabled", ProxyEnabled) .With("proxy_enabled", ProxyEnabled)
.With("autoproxy_mode", AutoproxyMode)
.With("autoproxy_member", AutoproxyMember)
.With("tag", Tag) .With("tag", Tag)
.With("tag_enabled", TagEnabled) .With("tag_enabled", TagEnabled)
); );
@ -29,24 +25,13 @@ public class SystemGuildPatch: PatchObject
} }
#nullable disable #nullable disable
public static SystemGuildPatch FromJson(JObject o, MemberId? memberId) public static SystemGuildPatch FromJson(JObject o)
{ {
var patch = new SystemGuildPatch(); var patch = new SystemGuildPatch();
if (o.ContainsKey("proxying_enabled") && o["proxying_enabled"].Type != JTokenType.Null) if (o.ContainsKey("proxying_enabled") && o["proxying_enabled"].Type != JTokenType.Null)
patch.ProxyEnabled = o.Value<bool>("proxying_enabled"); patch.ProxyEnabled = o.Value<bool>("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")) if (o.ContainsKey("tag"))
patch.Tag = o.Value<string>("tag").NullIfEmpty(); patch.Tag = o.Value<string>("tag").NullIfEmpty();
@ -56,7 +41,7 @@ public class SystemGuildPatch: PatchObject
return patch; return patch;
} }
public JObject ToJson(string memberRef, ulong guild_id) public JObject ToJson(ulong guild_id)
{ {
var o = new JObject(); var o = new JObject();
@ -65,12 +50,6 @@ public class SystemGuildPatch: PatchObject
if (ProxyEnabled.IsPresent) if (ProxyEnabled.IsPresent)
o.Add("proxying_enabled", ProxyEnabled.Value); 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) if (Tag.IsPresent)
o.Add("tag", Tag.Value); o.Add("tag", Tag.Value);

View File

@ -1,68 +1,26 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace PluralKit.Core; namespace PluralKit.Core;
[JsonConverter(typeof(StringEnumConverter))]
public enum AutoproxyMode
{
Off = 1,
Front = 2,
Latch = 3,
Member = 4
}
public class SystemGuildSettings public class SystemGuildSettings
{ {
public ulong Guild { get; } public ulong Guild { get; }
public SystemId System { get; } public SystemId System { get; }
public bool ProxyEnabled { get; } = true; public bool ProxyEnabled { get; } = true;
public AutoproxyMode AutoproxyMode { get; } = AutoproxyMode.Off;
public MemberId? AutoproxyMember { get; }
public string? Tag { get; } public string? Tag { get; }
public bool TagEnabled { get; } public bool TagEnabled { get; }
} }
public static class SystemGuildExt 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(); var o = new JObject();
o.Add("proxying_enabled", settings.ProxyEnabled); 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", settings.Tag);
o.Add("tag_enabled", settings.TagEnabled); o.Add("tag_enabled", settings.TagEnabled);
return o; 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<string>();
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."));
}
}
} }

View File

@ -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)| |guild_id|snowflake|only sent if the guild ID isn't already known (in dispatch payloads)|
|proxying_enabled|boolean|| |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|?string|79-character limit|
|tag_enabled|boolean|| |tag_enabled|boolean||
<!--
#### Autoproxy mode enum #### Autoproxy mode enum
|key|description| |key|description|
@ -133,6 +132,7 @@ Every PluralKit entity has two IDs: a short (5-character) ID and a longer UUID.
|front|autoproxy is set to the first member in the current fronters list, or disabled if the current switch contains no members| |front|autoproxy is set to the first member in the current fronters list, or disabled if the current switch contains no members|
|latch|autoproxy is set to the last member who sent a proxied message in the server| |latch|autoproxy is set to the last member who sent a proxied message in the server|
|member|autoproxy is set to a specific member (see `autoproxy_member` key)| |member|autoproxy is set to a specific member (see `autoproxy_member` key)|
-->
### Member guild settings model ### Member guild settings model