Merge pull request #150 from xSke/feature/autoproxy
Implement autoproxy. Closes #149.
This commit is contained in:
		@@ -111,6 +111,7 @@ namespace PluralKit.Bot
 | 
			
		||||
            .AddTransient<HelpCommands>()
 | 
			
		||||
            .AddTransient<ModCommands>()
 | 
			
		||||
            .AddTransient<MiscCommands>()
 | 
			
		||||
            .AddTransient<AutoproxyCommands>()
 | 
			
		||||
            
 | 
			
		||||
            .AddTransient<EmbedService>()
 | 
			
		||||
            .AddTransient<ProxyService>()
 | 
			
		||||
@@ -120,6 +121,7 @@ namespace PluralKit.Bot
 | 
			
		||||
 | 
			
		||||
            .AddTransient<ProxyCacheService>()
 | 
			
		||||
            .AddSingleton<WebhookCacheService>()
 | 
			
		||||
            .AddSingleton<AutoproxyCacheService>()
 | 
			
		||||
            .AddSingleton<ShardInfoService>()
 | 
			
		||||
            .AddSingleton<CpuStatService>()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								PluralKit.Bot/Commands/AutoproxyCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								PluralKit.Bot/Commands/AutoproxyCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
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;
 | 
			
		||||
        private AutoproxyCacheService _cache;
 | 
			
		||||
 | 
			
		||||
        public AutoproxyCommands(IDataStore data, AutoproxyCacheService cache)
 | 
			
		||||
        {
 | 
			
		||||
            _data = data;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 <member>` 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 _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id);
 | 
			
		||||
                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 _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.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 _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.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 _cache.FlushCacheForSystem(ctx.System, ctx.Guild.Id);
 | 
			
		||||
            await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<Embed> 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 <member>** - 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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 <description|members|fronter|fronthistory> <public|private>", "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 <member>", "Looks up information about a member");
 | 
			
		||||
        public static Command MemberNew = new Command("member new", "member new <name>", "Creates a new member");
 | 
			
		||||
        public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "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<AutoproxyCommands>(Autoproxy, m => m.Autoproxy(ctx));
 | 
			
		||||
            if (ctx.Match("link"))
 | 
			
		||||
                return ctx.Execute<LinkCommands>(Link, m => m.LinkSystem(ctx));
 | 
			
		||||
            if (ctx.Match("unlink"))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								PluralKit.Bot/Services/AutoproxyCacheService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								PluralKit.Bot/Services/AutoproxyCacheService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<AutoproxyCacheResult> 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<AutoproxyCacheResult> FetchSettings(ulong account, ulong guild, ICacheEntry entry)
 | 
			
		||||
        {
 | 
			
		||||
            using var conn = await _conn.Obtain();
 | 
			
		||||
            var data = (await conn.QueryAsync<SystemGuildSettings, PKSystem, PKMember, AutoproxyCacheResult>(
 | 
			
		||||
                "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}";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<ProxyService>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -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<ProxyMatch> 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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								PluralKit.Core/Migrations/3.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								PluralKit.Core/Migrations/3.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
-- 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 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 default null;
 | 
			
		||||
 | 
			
		||||
update info set schema_version = 3;
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -68,6 +77,9 @@ namespace PluralKit {
 | 
			
		||||
    public class SystemGuildSettings
 | 
			
		||||
    {
 | 
			
		||||
        public bool ProxyEnabled { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
        public AutoproxyMode AutoproxyMode { get; set; } = AutoproxyMode.Off;
 | 
			
		||||
        public int? AutoproxyMember { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class MemberGuildSettings
 | 
			
		||||
@@ -267,12 +279,13 @@ namespace PluralKit {
 | 
			
		||||
        /// Saves a posted message to the database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="senderAccount">The ID of the account that sent the original trigger message.</param>
 | 
			
		||||
        /// <param name="guildId">The ID of the guild the message was posted to.</param>
 | 
			
		||||
        /// <param name="channelId">The ID of the channel the message was posted to.</param>
 | 
			
		||||
        /// <param name="postedMessageId">The ID of the message posted by the webhook.</param>
 | 
			
		||||
        /// <param name="triggerMessageId">The ID of the original trigger message containing the proxy tags.</param>
 | 
			
		||||
        /// <param name="proxiedMember">The member (and by extension system) that was proxied.</param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        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);
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes a message from the data store.
 | 
			
		||||
@@ -286,6 +299,12 @@ namespace PluralKit {
 | 
			
		||||
        /// <param name="postedMessageIds">The IDs of the webhook messages to delete.</param>
 | 
			
		||||
        Task DeleteMessagesBulk(IEnumerable<ulong> postedMessageIds);
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the most recent message sent by a given account in a given guild.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The full message object, or null if none was found.</returns>
 | 
			
		||||
        Task<FullMessage> GetLastMessageInGuild(ulong account, ulong guild);
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets switches from a system.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@@ -334,6 +353,12 @@ namespace PluralKit {
 | 
			
		||||
        /// <param name="periodEnd"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        Task<FrontBreakdown> GetFrontBreakdown(PKSystem system, Instant periodStart, Instant periodEnd);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first listed fronter in a system.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The first fronter, or null if none are registered.</returns>
 | 
			
		||||
        Task<PKMember> GetFirstFronter(PKSystem system);
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers a switch with the given members in the given system.
 | 
			
		||||
@@ -434,11 +459,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
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -661,10 +688,11 @@ namespace PluralKit {
 | 
			
		||||
            using (var conn = await _conn.Obtain())
 | 
			
		||||
                return await conn.ExecuteScalarAsync<ulong>("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,
 | 
			
		||||
@@ -703,6 +731,17 @@ namespace PluralKit {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<FullMessage> GetLastMessageInGuild(ulong account, ulong guild)
 | 
			
		||||
        {
 | 
			
		||||
            using var conn = await _conn.Obtain();
 | 
			
		||||
            return (await conn.QueryAsync<PKMessage, PKMember, PKSystem, FullMessage>("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<ulong> GetTotalMessages()
 | 
			
		||||
        {
 | 
			
		||||
            using (var conn = await _conn.Obtain())
 | 
			
		||||
@@ -767,6 +806,15 @@ namespace PluralKit {
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<PKMember> 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<PKMember> members)
 | 
			
		||||
        {
 | 
			
		||||
            // Use a transaction here since we're doing multiple executed commands in one
 | 
			
		||||
 
 | 
			
		||||
@@ -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 `<member>` is a member name (in "quotes" if multiple words) or 5-letter ID:
 | 
			
		||||
 | 
			
		||||
    pk;autoproxy <member>
 | 
			
		||||
 | 
			
		||||
## 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ Words in \<angle brackets> 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 <account>` - Links this system to a different account.
 | 
			
		||||
- `pk;unlink [account]` - Unlinks an account from this system.
 | 
			
		||||
## Member commands
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user