From bf70a6e3e12ac6f02b140a9bebbfa415b5e46394 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 23 Jan 2020 20:19:50 +0100 Subject: [PATCH 1/5] Add schema changes for autoproxy --- PluralKit.Core/Migrations/3.sql | 13 +++++++++++++ PluralKit.Core/Stores.cs | 11 +++++++++++ 2 files changed, 24 insertions(+) create mode 100644 PluralKit.Core/Migrations/3.sql diff --git a/PluralKit.Core/Migrations/3.sql b/PluralKit.Core/Migrations/3.sql new file mode 100644 index 00000000..2dc0660b --- /dev/null +++ b/PluralKit.Core/Migrations/3.sql @@ -0,0 +1,13 @@ +-- Same sort of psuedo-enum due to Dapper limitations. See 2.sql. +-- 1 = autoproxy off +-- 2 = front mode (first fronter) +-- 3 = latch mode (last proxyer) +-- 4 = member mode (specific member) +alter table system_guild add column autoproxy_mode int check (autoproxy_mode in (1, 2, 3, 4)) not null default 1; + +-- for member mode +alter table system_guild add column autoproxy_member nullable references members (id) on delete set null; + +-- for latch mode +-- not *really* nullable, null just means old (pre-schema-change) data. +alter table messages add column guild bigint nullable default null; \ No newline at end of file diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index bc398083..4ce97068 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -9,6 +9,14 @@ using NodaTime; using Serilog; namespace PluralKit { + public enum AutoproxyMode + { + Off = 1, + Front = 2, + Latch = 3, + Member = 4 + } + public class FullMessage { public PKMessage Message; @@ -19,6 +27,7 @@ namespace PluralKit { public struct PKMessage { public ulong Mid; + public ulong? Guild; // null value means "no data" (ie. from before this field being added) public ulong Channel; public ulong Sender; public ulong? OriginalMid; @@ -63,6 +72,8 @@ namespace PluralKit { public ulong? LogChannel { get; set; } public ISet LogBlacklist { get; set; } public ISet Blacklist { get; set; } + public AutoproxyMode AutoproxyMode { get; set; } + public int AutoproxyMember { get; set; } } public class SystemGuildSettings From ca37c7e6cac3d24517c1257426500ccc36004170 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 23 Jan 2020 20:29:22 +0100 Subject: [PATCH 2/5] =?UTF-8?q?Put=20the=20autoproxy=20settings=20in=20the?= =?UTF-8?q?=20correct=20class=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PluralKit.Core/Stores.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index 4ce97068..64f70394 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -72,13 +72,14 @@ namespace PluralKit { public ulong? LogChannel { get; set; } public ISet LogBlacklist { get; set; } public ISet Blacklist { get; set; } - public AutoproxyMode AutoproxyMode { get; set; } - public int AutoproxyMember { get; set; } } public class SystemGuildSettings { public bool ProxyEnabled { get; set; } = true; + + public AutoproxyMode AutoproxyMode { get; set; } + public int AutoproxyMember { get; set; } } public class MemberGuildSettings From 57bc576de6dd242c33e8e582191f5c9c78b15132 Mon Sep 17 00:00:00 2001 From: Ske Date: Thu, 23 Jan 2020 21:20:22 +0100 Subject: [PATCH 3/5] Add autoproxy management commands --- PluralKit.Bot/Bot.cs | 1 + PluralKit.Bot/Commands/AutoproxyCommands.cs | 139 ++++++++++++++++++++ PluralKit.Bot/Commands/CommandTree.cs | 3 + PluralKit.Core/Migrations/3.sql | 6 +- PluralKit.Core/SchemaService.cs | 2 +- PluralKit.Core/Stores.cs | 12 +- 6 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 PluralKit.Bot/Commands/AutoproxyCommands.cs diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index da3ed512..cd20b114 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -111,6 +111,7 @@ namespace PluralKit.Bot .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() diff --git a/PluralKit.Bot/Commands/AutoproxyCommands.cs b/PluralKit.Bot/Commands/AutoproxyCommands.cs new file mode 100644 index 00000000..66416d30 --- /dev/null +++ b/PluralKit.Bot/Commands/AutoproxyCommands.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Discord; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class AutoproxyCommands + { + private IDataStore _data; + + public AutoproxyCommands(IDataStore data) + { + _data = data; + } + + public async Task Autoproxy(Context ctx) + { + ctx.CheckSystem().CheckGuildContext(); + + if (ctx.Match("off", "stop", "cancel", "no")) + await AutoproxyOff(ctx); + else if (ctx.Match("latch", "last", "proxy", "stick", "sticky")) + await AutoproxyLatch(ctx); + else if (ctx.Match("front", "fronter", "switch")) + await AutoproxyFront(ctx); + 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)); + else + throw new PKSyntaxError($"Invalid autoproxy mode `{ctx.PopArgument().EscapeMarkdown()}`."); + } + + private async Task AutoproxyOff(Context ctx) + { + var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + if (settings.AutoproxyMode == AutoproxyMode.Off) + { + await ctx.Reply($"{Emojis.Note} Autoproxy is already off in this server."); + } + else + { + settings.AutoproxyMode = AutoproxyMode.Off; + settings.AutoproxyMember = null; + await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await ctx.Reply($"{Emojis.Success} Autoproxy turned off in this server."); + } + } + + private async Task AutoproxyLatch(Context ctx) + { + var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + 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`."); + } + else + { + settings.AutoproxyMode = AutoproxyMode.Latch; + settings.AutoproxyMember = null; + await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await ctx.Reply($"{Emojis.Success} Autoproxy set to latch mode in this server. Messages will now be autoproxied using the *last-proxied member* in this server."); + } + } + + private async Task AutoproxyFront(Context ctx) + { + var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + 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`."); + } + else + { + settings.AutoproxyMode = AutoproxyMode.Front; + settings.AutoproxyMember = null; + await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await ctx.Reply($"{Emojis.Success} Autoproxy set to front mode in this server. Messages will now be autoproxied using the *current first fronter*, if any."); + } + } + + private async Task AutoproxyMember(Context ctx, PKMember member) + { + ctx.CheckOwnMember(member); + + var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + settings.AutoproxyMode = AutoproxyMode.Member; + settings.AutoproxyMember = member.Id; + await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + + await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server."); + } + + private async Task CreateAutoproxyStatusEmbed(Context ctx) + { + var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + + var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy ** - Autoproxies as a specific member"; + var eb = new EmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); + + switch (settings.AutoproxyMode) { + case AutoproxyMode.Off: eb.WithDescription($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); + break; + case AutoproxyMode.Front: { + var lastSwitch = await _data.GetLatestSwitch(ctx.System); + if (lastSwitch == null) + eb.WithDescription("Autoproxy is currently set to **front mode** in this server, but you have no registered switches. Use the `pk;switch` command to log one."); + else + { + var firstMember = await _data.GetSwitchMembers(lastSwitch).FirstOrDefaultAsync(); + eb.WithDescription(firstMember == null + ? "Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered." + : $"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{firstMember.Name.EscapeMarkdown()}** (`{firstMember.Hid}`). To disable, type `pk;autoproxy off`."); + } + + break; + } + // AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up + case AutoproxyMode.Member when settings.AutoproxyMember != null: { + var member = await _data.GetMemberById(settings.AutoproxyMember.Value); + eb.WithDescription($"Autoproxy is active for member **{member.Name}** (`{member.Hid}`) in this server. To disable, type `pk;autoproxy off`."); + break; + } + case AutoproxyMode.Latch: + eb.WithDescription($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`."); + break; + default: throw new ArgumentOutOfRangeException(); + } + + return eb.Build(); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index d0489150..d004f3f4 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -23,6 +23,7 @@ namespace PluralKit.Bot.Commands public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history"); public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown"); public static Command SystemPrivacy = new Command("system privacy", "system privacy ", "Changes your system's privacy settings"); + public static Command Autoproxy = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for this server"); public static Command MemberInfo = new Command("member", "member ", "Looks up information about a member"); public static Command MemberNew = new Command("member new", "member new ", "Creates a new member"); public static Command MemberRename = new Command("member rename", "member rename ", "Renames a member"); @@ -87,6 +88,8 @@ namespace PluralKit.Bot.Commands return HandleMemberCommand(ctx); if (ctx.Match("switch", "sw")) return HandleSwitchCommand(ctx); + if (ctx.Match("ap", "autoproxy", "auto")) + return ctx.Execute(Autoproxy, m => m.Autoproxy(ctx)); if (ctx.Match("link")) return ctx.Execute(Link, m => m.LinkSystem(ctx)); if (ctx.Match("unlink")) diff --git a/PluralKit.Core/Migrations/3.sql b/PluralKit.Core/Migrations/3.sql index 2dc0660b..29cabda4 100644 --- a/PluralKit.Core/Migrations/3.sql +++ b/PluralKit.Core/Migrations/3.sql @@ -6,8 +6,10 @@ alter table system_guild add column autoproxy_mode int check (autoproxy_mode in (1, 2, 3, 4)) not null default 1; -- for member mode -alter table system_guild add column autoproxy_member nullable references members (id) on delete set null; +alter table system_guild add column autoproxy_member int references members (id) on delete set null; -- for latch mode -- not *really* nullable, null just means old (pre-schema-change) data. -alter table messages add column guild bigint nullable default null; \ No newline at end of file +alter table messages add column guild bigint default null; + +update info set schema_version = 3; \ No newline at end of file diff --git a/PluralKit.Core/SchemaService.cs b/PluralKit.Core/SchemaService.cs index abcb34de..3be9938c 100644 --- a/PluralKit.Core/SchemaService.cs +++ b/PluralKit.Core/SchemaService.cs @@ -10,7 +10,7 @@ using Serilog; namespace PluralKit { public class SchemaService { - private const int TargetSchemaVersion = 2; + private const int TargetSchemaVersion = 3; private DbConnectionFactory _conn; private ILogger _logger; diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index 64f70394..0801b4cc 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -77,9 +77,9 @@ namespace PluralKit { public class SystemGuildSettings { public bool ProxyEnabled { get; set; } = true; - - public AutoproxyMode AutoproxyMode { get; set; } - public int AutoproxyMember { get; set; } + + public AutoproxyMode AutoproxyMode { get; set; } = AutoproxyMode.Off; + public int? AutoproxyMember { get; set; } } public class MemberGuildSettings @@ -446,11 +446,13 @@ namespace PluralKit { public async Task SetSystemGuildSettings(PKSystem system, ulong guild, SystemGuildSettings settings) { using (var conn = await _conn.Obtain()) - await conn.ExecuteAsync("insert into system_guild (system, guild, proxy_enabled) values (@System, @Guild, @ProxyEnabled) on conflict (system, guild) do update set proxy_enabled = @ProxyEnabled", new + await conn.ExecuteAsync("insert into system_guild (system, guild, proxy_enabled, autoproxy_mode, autoproxy_member) values (@System, @Guild, @ProxyEnabled, @AutoproxyMode, @AutoproxyMember) on conflict (system, guild) do update set proxy_enabled = @ProxyEnabled, autoproxy_mode = @AutoproxyMode, autoproxy_member = @AutoproxyMember", new { System = system.Id, Guild = guild, - ProxyEnabled = settings.ProxyEnabled + settings.ProxyEnabled, + settings.AutoproxyMode, + settings.AutoproxyMember }); } From 83cfb3eb4615fcb64ebb39cf047c1307f60dde0b Mon Sep 17 00:00:00 2001 From: Ske Date: Fri, 24 Jan 2020 20:28:48 +0100 Subject: [PATCH 4/5] Add autoproxy functionality --- PluralKit.Bot/Bot.cs | 1 + PluralKit.Bot/Commands/AutoproxyCommands.cs | 9 ++- .../Services/AutoproxyCacheService.cs | 73 ++++++++++++++++++ PluralKit.Bot/Services/ProxyService.cs | 74 +++++++++++++++++-- PluralKit.Core/Stores.cs | 40 +++++++++- 5 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 PluralKit.Bot/Services/AutoproxyCacheService.cs diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index cd20b114..353bcabf 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -121,6 +121,7 @@ namespace PluralKit.Bot .AddTransient() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/PluralKit.Bot/Commands/AutoproxyCommands.cs b/PluralKit.Bot/Commands/AutoproxyCommands.cs index 66416d30..e748e6de 100644 --- a/PluralKit.Bot/Commands/AutoproxyCommands.cs +++ b/PluralKit.Bot/Commands/AutoproxyCommands.cs @@ -11,10 +11,12 @@ namespace PluralKit.Bot.Commands public class AutoproxyCommands { private IDataStore _data; + private AutoproxyCacheService _cache; - public AutoproxyCommands(IDataStore data) + public AutoproxyCommands(IDataStore data, AutoproxyCacheService cache) { _data = data; + _cache = cache; } public async Task Autoproxy(Context ctx) @@ -49,6 +51,7 @@ namespace PluralKit.Bot.Commands settings.AutoproxyMode = AutoproxyMode.Off; settings.AutoproxyMember = null; await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id); await ctx.Reply($"{Emojis.Success} Autoproxy turned off in this server."); } } @@ -65,6 +68,7 @@ namespace PluralKit.Bot.Commands settings.AutoproxyMode = AutoproxyMode.Latch; settings.AutoproxyMember = null; await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id); await ctx.Reply($"{Emojis.Success} Autoproxy set to latch mode in this server. Messages will now be autoproxied using the *last-proxied member* in this server."); } } @@ -81,6 +85,7 @@ namespace PluralKit.Bot.Commands settings.AutoproxyMode = AutoproxyMode.Front; settings.AutoproxyMember = null; await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id); await ctx.Reply($"{Emojis.Success} Autoproxy set to front mode in this server. Messages will now be autoproxied using the *current first fronter*, if any."); } } @@ -93,7 +98,7 @@ namespace PluralKit.Bot.Commands settings.AutoproxyMode = AutoproxyMode.Member; settings.AutoproxyMember = member.Id; await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); - + await _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id); await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server."); } diff --git a/PluralKit.Bot/Services/AutoproxyCacheService.cs b/PluralKit.Bot/Services/AutoproxyCacheService.cs new file mode 100644 index 00000000..d3f043a0 --- /dev/null +++ b/PluralKit.Bot/Services/AutoproxyCacheService.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Dapper; + +using Microsoft.Extensions.Caching.Memory; + +namespace PluralKit.Bot +{ + public class AutoproxyCacheResult + { + public SystemGuildSettings GuildSettings; + public PKSystem System; + public PKMember AutoproxyMember; + } + public class AutoproxyCacheService + { + private IMemoryCache _cache; + private IDataStore _data; + private DbConnectionFactory _conn; + + public AutoproxyCacheService(IMemoryCache cache, DbConnectionFactory conn, IDataStore data) + { + _cache = cache; + _conn = conn; + _data = data; + } + + public async Task GetGuildSettings(ulong account, ulong guild) => + await _cache.GetOrCreateAsync(GetKey(account, guild), entry => FetchSettings(account, guild, entry)); + + public async Task FlushCacheForSystem(PKSystem system, ulong guild) + { + foreach (var account in await _data.GetSystemAccounts(system)) + FlushCacheFor(account, guild); + } + + public void FlushCacheFor(ulong account, ulong guild) => + _cache.Remove(GetKey(account, guild)); + + private async Task FetchSettings(ulong account, ulong guild, ICacheEntry entry) + { + using var conn = await _conn.Obtain(); + var data = (await conn.QueryAsync( + "select system_guild.*, systems.*, members.* from accounts inner join systems on systems.id = accounts.system inner join system_guild on system_guild.system = systems.id left join members on system_guild.autoproxy_member = members.id where accounts.uid = @Uid and system_guild.guild = @Guild", + (guildSettings, system, autoproxyMember) => new AutoproxyCacheResult + { + GuildSettings = guildSettings, + System = system, + AutoproxyMember = autoproxyMember + }, + new {Uid = account, Guild = guild})).FirstOrDefault(); + + if (data != null) + { + // Long expiry for accounts with no system/settings registered + entry.SetSlidingExpiration(TimeSpan.FromMinutes(5)); + entry.SetAbsoluteExpiration(TimeSpan.FromHours(1)); + } + else + { + // Shorter expiry if they already have settings + entry.SetSlidingExpiration(TimeSpan.FromMinutes(1)); + entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); + } + + return data; + } + + private string GetKey(ulong account, ulong guild) => $"_system_guild_{account}_{guild}"; + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Services/ProxyService.cs b/PluralKit.Bot/Services/ProxyService.cs index 56252d07..1c00de37 100644 --- a/PluralKit.Bot/Services/ProxyService.cs +++ b/PluralKit.Bot/Services/ProxyService.cs @@ -9,6 +9,9 @@ using Discord; using Discord.Net; using Discord.WebSocket; +using NodaTime; +using NodaTime.Extensions; + using PluralKit.Core; using Serilog; @@ -18,7 +21,7 @@ namespace PluralKit.Bot class ProxyMatch { public PKMember Member; public PKSystem System; - public ProxyTag ProxyTags; + public ProxyTag? ProxyTags; public string InnerText; } @@ -31,8 +34,9 @@ namespace PluralKit.Bot private ILogger _logger; private WebhookExecutorService _webhookExecutor; private ProxyCacheService _cache; + private AutoproxyCacheService _autoproxyCache; - public ProxyService(IDiscordClient client, LogChannelService logChannel, IDataStore data, EmbedService embeds, ILogger logger, ProxyCacheService cache, WebhookExecutorService webhookExecutor, DbConnectionFactory conn) + public ProxyService(IDiscordClient client, LogChannelService logChannel, IDataStore data, EmbedService embeds, ILogger logger, ProxyCacheService cache, WebhookExecutorService webhookExecutor, DbConnectionFactory conn, AutoproxyCacheService autoproxyCache) { _client = client; _logChannel = logChannel; @@ -41,6 +45,7 @@ namespace PluralKit.Bot _cache = cache; _webhookExecutor = webhookExecutor; _conn = conn; + _autoproxyCache = autoproxyCache; _logger = logger.ForContext(); } @@ -92,8 +97,13 @@ namespace PluralKit.Bot if (!(message.Channel is ITextChannel channel)) return; // Find a member with proxy tags matching the message - var results = await _cache.GetResultsFor(message.Author.Id); + var results = (await _cache.GetResultsFor(message.Author.Id)).ToList(); var match = GetProxyTagMatch(message.Content, results); + + // If we didn't get a match by proxy tags, try to get one by autoproxy + if (match == null) match = await GetAutoproxyMatch(message, channel); + + // If we still haven't found any, just yeet if (match == null) return; // Gather all "extra" data from DB at once @@ -122,8 +132,9 @@ namespace PluralKit.Bot if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName); // Add the proxy tags into the proxied message if that option is enabled - var messageContents = match.Member.KeepProxy - ? $"{match.ProxyTags.Prefix}{match.InnerText}{match.ProxyTags.Suffix}" + // Also check if the member has any proxy tags - some cases autoproxy can return a member with no tags + var messageContents = (match.Member.KeepProxy && match.ProxyTags.HasValue) + ? $"{match.ProxyTags.Value.Prefix}{match.InnerText}{match.ProxyTags.Value.Suffix}" : match.InnerText; // Sanitize @everyone, but only if the original user wouldn't have permission to @@ -138,7 +149,7 @@ namespace PluralKit.Bot ); // Store the message in the database, and log it in the log channel (if applicable) - await _data.AddMessage(message.Author.Id, hookMessageId, message.Channel.Id, message.Id, match.Member); + await _data.AddMessage(message.Author.Id, hookMessageId, channel.GuildId, message.Channel.Id, message.Id, match.Member); await _logChannel.LogMessage(match.System, match.Member, hookMessageId, message.Id, message.Channel as IGuildChannel, message.Author, match.InnerText, aux.Guild); // Wait a second or so before deleting the original message @@ -155,6 +166,57 @@ namespace PluralKit.Bot } } + private async Task GetAutoproxyMatch(IMessage message, IGuildChannel channel) + { + // For now we use a backslash as an "escape character", subject to change later + if ((message.Content ?? "").TrimStart().StartsWith("\"")) return null; + + // Fetch info from the cache, bail if we don't have anything (either no system or no autoproxy settings - AP defaults to off so this works) + var autoproxyCache = await _autoproxyCache.GetGuildSettings(message.Author.Id, channel.GuildId); + if (autoproxyCache == null) return null; + + PKMember member = null; + // Figure out which member to proxy as + switch (autoproxyCache.GuildSettings.AutoproxyMode) + { + case AutoproxyMode.Off: + // Autoproxy off, bail + return null; + case AutoproxyMode.Front: + // Front mode: just use the current first fronter + member = await _data.GetFirstFronter(autoproxyCache.System); + break; + case AutoproxyMode.Latch: + // Latch mode: find last proxied message, use *that* member + var msg = await _data.GetLastMessageInGuild(message.Author.Id, channel.GuildId); + if (msg == null) return null; // No message found + + // If the message is older than 6 hours, ignore it and force the sender to "refresh" a proxy + // This can be revised in the future, it's a preliminary value. + var timestamp = SnowflakeUtils.FromSnowflake(msg.Message.Mid).ToInstant(); + var timeSince = SystemClock.Instance.GetCurrentInstant() - timestamp; + if (timeSince > Duration.FromHours(6)) return null; + + member = msg.Member; + break; + case AutoproxyMode.Member: + // Member mode: just use that member + member = autoproxyCache.AutoproxyMember; + break; + } + + // If we haven't found the member (eg. front mode w/ no fronter), bail again + if (member == null) return null; + return new ProxyMatch + { + System = autoproxyCache.System, + Member = member, + // Autoproxying members with no proxy tags is possible, return the correct result + ProxyTags = member.ProxyTags.Count > 0 ? member.ProxyTags.First() : (ProxyTag?) null, + InnerText = message.Content + }; + } + private static string SanitizeEveryoneMaybe(IMessage message, string messageContents) { var senderPermissions = ((IGuildUser) message.Author).GetPermissions(message.Channel as IGuildChannel); diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index 0801b4cc..5c53ce78 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -279,12 +279,13 @@ namespace PluralKit { /// Saves a posted message to the database. /// /// The ID of the account that sent the original trigger message. + /// The ID of the guild the message was posted to. /// The ID of the channel the message was posted to. /// The ID of the message posted by the webhook. /// The ID of the original trigger message containing the proxy tags. /// The member (and by extension system) that was proxied. /// - Task AddMessage(ulong senderAccount, ulong channelId, ulong postedMessageId, ulong triggerMessageId, PKMember proxiedMember); + Task AddMessage(ulong senderAccount, ulong guildId, ulong channelId, ulong postedMessageId, ulong triggerMessageId, PKMember proxiedMember); /// /// Deletes a message from the data store. @@ -298,6 +299,12 @@ namespace PluralKit { /// The IDs of the webhook messages to delete. Task DeleteMessagesBulk(IEnumerable postedMessageIds); + /// + /// Gets the most recent message sent by a given account in a given guild. + /// + /// The full message object, or null if none was found. + Task GetLastMessageInGuild(ulong account, ulong guild); + /// /// Gets switches from a system. /// @@ -346,6 +353,12 @@ namespace PluralKit { /// /// Task GetFrontBreakdown(PKSystem system, Instant periodStart, Instant periodEnd); + + /// + /// Gets the first listed fronter in a system. + /// + /// The first fronter, or null if none are registered. + Task GetFirstFronter(PKSystem system); /// /// Registers a switch with the given members in the given system. @@ -675,10 +688,11 @@ namespace PluralKit { using (var conn = await _conn.Obtain()) return await conn.ExecuteScalarAsync("select count(id) from members"); } - public async Task AddMessage(ulong senderId, ulong messageId, ulong channelId, ulong originalMessage, PKMember member) { + public async Task AddMessage(ulong senderId, ulong messageId, ulong guildId, ulong channelId, ulong originalMessage, PKMember member) { using (var conn = await _conn.Obtain()) - await conn.ExecuteAsync("insert into messages(mid, channel, member, sender, original_mid) values(@MessageId, @ChannelId, @MemberId, @SenderId, @OriginalMid)", new { + await conn.ExecuteAsync("insert into messages(mid, guild, channel, member, sender, original_mid) values(@MessageId, @GuildId, @ChannelId, @MemberId, @SenderId, @OriginalMid)", new { MessageId = messageId, + GuildId = guildId, ChannelId = channelId, MemberId = member.Id, SenderId = senderId, @@ -717,6 +731,17 @@ namespace PluralKit { } } + public async Task GetLastMessageInGuild(ulong account, ulong guild) + { + using var conn = await _conn.Obtain(); + return (await conn.QueryAsync("select messages.*, members.*, systems.* from messages, members, systems where messages.guild = @Guild and messages.sender = @Uid and messages.member = members.id and systems.id = members.system order by mid desc limit 1", (msg, member, system) => new FullMessage + { + Message = msg, + System = system, + Member = member + }, new { Uid = account, Guild = guild })).FirstOrDefault(); + } + public async Task GetTotalMessages() { using (var conn = await _conn.Obtain()) @@ -781,6 +806,15 @@ namespace PluralKit { }; } + public async Task GetFirstFronter(PKSystem system) + { + // TODO: move to extension method since it doesn't rely on internals + var lastSwitch = await GetLatestSwitch(system); + if (lastSwitch == null) return null; + + return await GetSwitchMembers(lastSwitch).FirstOrDefaultAsync(); + } + public async Task AddSwitch(PKSystem system, IEnumerable members) { // Use a transaction here since we're doing multiple executed commands in one From 01c4e998761490b8c63b248058794fde92da449d Mon Sep 17 00:00:00 2001 From: Ske Date: Sat, 25 Jan 2020 16:21:52 +0100 Subject: [PATCH 5/5] Document autoproxy functionality --- docs/1-user-guide.md | 32 ++++++++++++++++++++++++++++++++ docs/2-command-list.md | 1 + docs/6-tips-and-tricks.md | 1 + 3 files changed, 34 insertions(+) diff --git a/docs/1-user-guide.md b/docs/1-user-guide.md index 956b464d..29731cd1 100644 --- a/docs/1-user-guide.md +++ b/docs/1-user-guide.md @@ -311,6 +311,38 @@ Since the messages will be posted by PluralKit's webhook, there's no way to dele To delete a PluralKit-proxied message, you can react to it with the ❌ emoji. Note that this only works if the message has been sent from your own account. +### Autoproxying +The bot's *autoproxy* feature allows you to have messages be proxied without directly including the proxy tags. Autoproxy can be set up in various ways. There are three autoproxy modes currently implemented: + +To see your system's current autoproxy settings, simply use the command: + pk;autoproxy + +To disable autoproxying for the current server, use the command: + pk;autoproxy off + +*(hint: `pk;autoproxy` can be shortened to `pk;ap` in all related commands)* + +#### Front mode +This autoproxy mode will proxy messages as the current *first* fronter of the system. If you register a switch with `Alice` and `Bob`, messages without proxy tags will be autoproxied as `Alice`. +To enable front-mode autoproxying for a given server, use the following command: + + pk;autoproxy front + +#### Latch mode +This autoproxy mode will essentially "continue" previous proxy tags. If you proxy a message with `Alice`'s proxy tags, messages posted afterwards will be proxied as Alice. Proxying again with someone else's proxy tags, say, `Bob`, will cause messages *from then on* to be proxied as Bob. +In other words, it means proxy tags become "sticky". This will carry over across all channels in the same server. + +To enable latch-mode autoproxying for a given server, use the following command: + + pk;autoproxy latch + +#### Member mode +This autoproxy mode will autoproxy for a specific selected member, irrelevant of past proxies or fronters. + +To enable member-mode autoproxying for a given server, use the following command, where `` is a member name (in "quotes" if multiple words) or 5-letter ID: + + pk;autoproxy + ## Managing switches PluralKit allows you to log member switches through the bot. Essentially, this means you can mark one or more members as *the current fronter(s)* for the duration until the next switch. diff --git a/docs/2-command-list.md b/docs/2-command-list.md index 8a2ac6d5..9ee20111 100644 --- a/docs/2-command-list.md +++ b/docs/2-command-list.md @@ -24,6 +24,7 @@ Words in \ are *required parameters*. Words in [square brackets] - `pk;system [id] frontpercent [timeframe]` - Shows the aggregated front history of a system within a given time frame. - `pk;system [id] list` - Shows a paginated list of a system's members. - `pk;system [id] list full` - Shows a paginated list of a system's members, with increased detail. +- `pk;autoproxy [off|front|latch|member]` - Updates the system's autoproxy settings for a given server. - `pk;link ` - Links this system to a different account. - `pk;unlink [account]` - Unlinks an account from this system. ## Member commands diff --git a/docs/6-tips-and-tricks.md b/docs/6-tips-and-tricks.md index a0a4fe20..8fe45976 100644 --- a/docs/6-tips-and-tricks.md +++ b/docs/6-tips-and-tricks.md @@ -21,6 +21,7 @@ PluralKit has a couple of useful command shorthands to reduce the typing: |pk;member new|pk;m n| |pk;switch|pk;sw| |pk;message|pk;msg| +|pk;autoproxy|pk;ap| ## Permission checker command If you're having issues with PluralKit not proxying, it may be an issue with your server's channel permission setup.