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);
|
||||
|
||||
Reference in New Issue
Block a user