Merge remote-tracking branch 'origin/main'

This commit is contained in:
spiral 2022-11-24 06:32:59 +00:00
commit 07845edee6
15 changed files with 222 additions and 20 deletions

View File

@ -117,10 +117,18 @@ public partial class CommandTree
await ctx.Execute<Admin>(Admin, a => a.UpdateMemberId(ctx));
else if (ctx.Match("ugid", "updategroupid"))
await ctx.Execute<Admin>(Admin, a => a.UpdateGroupId(ctx));
else if (ctx.Match("rsid", "rerollsystemid"))
await ctx.Execute<Admin>(Admin, a => a.RerollSystemId(ctx));
else if (ctx.Match("rmid", "rerollmemberid"))
await ctx.Execute<Admin>(Admin, a => a.RerollMemberId(ctx));
else if (ctx.Match("rgid", "rerollgroupid"))
await ctx.Execute<Admin>(Admin, a => a.RerollGroupId(ctx));
else if (ctx.Match("uml", "updatememberlimit"))
await ctx.Execute<Admin>(Admin, a => a.SystemMemberLimit(ctx));
else if (ctx.Match("ugl", "updategrouplimit"))
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
else if (ctx.Match("sr", "systemrecover"))
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
else
await ctx.Reply($"{Emojis.Error} Unknown command.");
}
@ -511,6 +519,8 @@ public partial class CommandTree
return ctx.Execute<Config>(null, m => m.GroupDefaultPrivacy(ctx));
if (ctx.MatchMultiple(new[] { "show" }, new[] { "private" }) || ctx.Match("sp"))
return ctx.Execute<Config>(null, m => m.ShowPrivateInfo(ctx));
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "case" }))
return ctx.Execute<Config>(null, m => m.CaseSensitiveProxyTags(ctx));
// todo: maybe add the list of configuration keys here?
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings.");

View File

@ -1,5 +1,11 @@
using System.Text.RegularExpressions;
using Dapper;
using SqlKata;
using Myriad.Rest;
using Myriad.Types;
using PluralKit.Core;
namespace PluralKit.Bot;
@ -7,10 +13,12 @@ namespace PluralKit.Bot;
public class Admin
{
private readonly BotConfig _botConfig;
private readonly DiscordApiClient _rest;
public Admin(BotConfig botConfig)
public Admin(BotConfig botConfig, DiscordApiClient rest)
{
_botConfig = botConfig;
_rest = rest;
}
public async Task UpdateSystemId(Context ctx)
@ -87,6 +95,74 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollSystemId(Context ctx)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll"))
throw new PKError("ID change cancelled.");
var query = new Query("systems").AsUpdate(new
{
hid = new UnsafeLiteral("find_free_system_hid()"),
})
.Where("id", target.Id);
var newHid = await ctx.Database.QueryFirst<string>(query, "returning hid");
await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollMemberId(Context ctx)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchMember();
if (target == null)
throw new PKError("Unknown member.");
if (!await ctx.PromptYesNo(
$"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?",
"Reroll"
))
throw new PKError("ID change cancelled.");
var query = new Query("members").AsUpdate(new
{
hid = new UnsafeLiteral("find_free_member_hid()"),
})
.Where("id", target.Id);
var newHid = await ctx.Database.QueryFirst<string>(query, "returning hid");
await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollGroupId(Context ctx)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchGroup();
if (target == null)
throw new PKError("Unknown group.");
if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?",
"Change"
))
throw new PKError("ID change cancelled.");
var query = new Query("groups").AsUpdate(new
{
hid = new UnsafeLiteral("find_free_group_hid()"),
})
.Where("id", target.Id);
var newHid = await ctx.Database.QueryFirst<string>(query, "returning hid");
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task SystemMemberLimit(Context ctx)
{
ctx.AssertBotAdmin();
@ -142,4 +218,52 @@ public class Admin
await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { GroupLimitOverride = newLimit });
await ctx.Reply($"{Emojis.Success} Group limit updated.");
}
public async Task SystemRecover(Context ctx)
{
ctx.AssertBotAdmin();
var rerollToken = ctx.MatchFlag("rt", "reroll-token");
var systemToken = ctx.PopArgument();
var systemId = await ctx.Database.Execute(conn => conn.QuerySingleOrDefaultAsync<SystemId?>(
"select id from systems where token = @token",
new { token = systemToken }
));
if (systemId == null)
throw new PKError("Could not retrieve a system with that token.");
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to associate the system with (either ID or @mention).");
var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id);
if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount);
var system = await ctx.Repository.GetSystem(systemId.Value!);
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account"))
throw new PKError("System recovery cancelled.");
await ctx.Repository.AddAccount(system.Id, account.Id);
if (rerollToken)
await ctx.Repository.UpdateSystem(system.Id, new SystemPatch { Token = StringUtils.GenerateToken() });
if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages))
await _rest.DeleteMessage(ctx.Message);
await ctx.Reply(null, new Embed
{
Title = "System recovered",
Description = $"{account.NameAndMention()} has been linked to system `{system.Hid}`.",
Fields = new Embed.Field[]
{
new Embed.Field("Token rerolled?", rerollToken ? "yes" : "no", true),
new Embed.Field("Actioned by", ctx.Author.NameAndMention(), true),
},
Color = DiscordUtils.Green,
});
}
}

View File

@ -243,7 +243,7 @@ public class Checks
try
{
_proxy.ShouldProxy(channel, msg, context);
_matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true);
_matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true, ctx.Config.CaseSensitiveProxyTags);
await ctx.Reply("I'm not sure why this message was not proxied, sorry.");
}

View File

@ -88,6 +88,13 @@ public class Config
Limits.MaxGroupCount.ToString()
));
items.Add(new(
"Case sensitive proxy tags",
"If proxy tags should be case sensitive",
EnabledDisabled(ctx.Config.CaseSensitiveProxyTags),
"enabled"
));
await ctx.Paginate<PaginatedConfigItem>(
items.ToAsyncEnumerable(),
items.Count,
@ -383,4 +390,27 @@ public class Config
await ctx.Reply("Private information will now be **hidden** when looking up your own info. Use the `-private` flag to show it.");
}
}
public async Task CaseSensitiveProxyTags(Context ctx)
{
if (!ctx.HasNext())
{
if (ctx.Config.CaseSensitiveProxyTags) { await ctx.Reply("Proxy tags are currently case sensitive"); }
else { await ctx.Reply("Proxy tags are currently case insensitive"); }
return;
}
if (ctx.MatchToggle(true))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = true });
await ctx.Reply("Proxy tags are now case sensitive");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = false });
await ctx.Reply("Proxy tags are now case insensitive");
}
}
}

View File

@ -20,17 +20,17 @@ public class ProxyMatcher
public bool TryMatch(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection<ProxyMember> members, out ProxyMatch match,
string messageContent,
bool hasAttachments, bool allowAutoproxy)
bool hasAttachments, bool allowAutoproxy, bool caseSensitive)
{
if (TryMatchTags(members, messageContent, hasAttachments, out match)) return true;
if (TryMatchTags(members, messageContent, hasAttachments, caseSensitive, out match)) return true;
if (allowAutoproxy && TryMatchAutoproxy(ctx, settings, members, messageContent, out match)) return true;
return false;
}
private bool TryMatchTags(IReadOnlyCollection<ProxyMember> members, string messageContent, bool hasAttachments,
out ProxyMatch match)
bool caseSensitive, out ProxyMatch match)
{
if (!_parser.TryMatch(members, messageContent, out match)) return false;
if (!_parser.TryMatch(members, messageContent, caseSensitive, out match)) return false;
// Edge case: If we got a match with blank inner text, we'd normally just send w/ attachments
// However, if there are no attachments, the user probably intended something else, so we "un-match" and proceed to autoproxy

View File

@ -80,7 +80,7 @@ public class ProxyService
members = (await _repo.GetProxyMembers(message.Author.Id, message.GuildId!.Value)).ToList();
if (!_matcher.TryMatch(ctx, autoproxySettings, members, out var match, message.Content, message.Attachments.Length > 0,
allowAutoproxy)) return false;
allowAutoproxy, ctx.CaseSensitiveProxyTags)) return false;
// this is hopefully temporary, so not putting it into a separate method
if (message.Content != null && message.Content.Length > 2000)
@ -210,8 +210,9 @@ public class ProxyService
"Proxying was disabled in this channel by a server administrator (via the proxy blacklist).");
var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, msg.Guild!.Value, null);
var config = await _repo.GetSystemConfig(ctx.SystemId.Value);
var prevMatched = _matcher.TryMatch(ctx, autoproxySettings, members, out var prevMatch, originalMsg.Content,
originalMsg.Attachments.Length > 0, false);
originalMsg.Attachments.Length > 0, false, ctx.CaseSensitiveProxyTags);
var match = new ProxyMatch
{

View File

@ -10,7 +10,7 @@ public class ProxyTagParser
private readonly Regex prefixPattern = new(@"^<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>");
private readonly Regex suffixPattern = new(@"<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>$");
public bool TryMatch(IEnumerable<ProxyMember> members, string? input, out ProxyMatch result)
public bool TryMatch(IEnumerable<ProxyMember> members, string? input, bool caseSensitive, out ProxyMatch result)
{
result = default;
@ -42,7 +42,7 @@ public class ProxyTagParser
if (tag.Suffix == ">" && suffixPattern.IsMatch(input)) continue;
// Can we match with these tags?
if (TryMatchTagsInner(input, tag, out result.Content))
if (TryMatchTagsInner(input, tag, caseSensitive, out result.Content))
{
// If we extracted a leading mention before, add that back now
if (leadingMention != null) result.Content = $"{leadingMention} {result.Content}";
@ -56,7 +56,7 @@ public class ProxyTagParser
return false;
}
private bool TryMatchTagsInner(string input, ProxyTag tag, out string inner)
private bool TryMatchTagsInner(string input, ProxyTag tag, bool caseSensitive, out string inner)
{
inner = "";
@ -64,9 +64,14 @@ public class ProxyTagParser
var prefix = tag.Prefix ?? "";
var suffix = tag.Suffix ?? "";
var comparision = caseSensitive
? StringComparison.CurrentCulture
: StringComparison.CurrentCultureIgnoreCase;
// Check if our input starts/ends with the tags
var isMatch = input.Length >= prefix.Length + suffix.Length
&& input.StartsWith(prefix) && input.EndsWith(suffix);
&& input.StartsWith(prefix, comparision)
&& input.EndsWith(suffix, comparision);
// Special case: image-only proxies + proxy tags with spaces
// Trim everything, then see if we have a "contentless tag pair" (normally disallowed, but OK if we have an attachment)

View File

@ -28,4 +28,5 @@ public class MessageContext
public string? SystemAvatar { get; }
public bool AllowAutoproxy { get; }
public int? LatchTimeout { get; }
public bool CaseSensitiveProxyTags { get; }
}

View File

@ -14,12 +14,14 @@
tag_enabled bool,
system_avatar text,
allow_autoproxy bool,
latch_timeout integer
latch_timeout integer,
case_sensitive_proxy_tags bool
)
as $$
-- CTEs to query "static" (accessible only through args) data
with
system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, allow_autoproxy as account_autoproxy from accounts
system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled,
allow_autoproxy as account_autoproxy, system_config.case_sensitive_proxy_tags from accounts
left join systems on systems.id = 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
@ -40,7 +42,8 @@ as $$
coalesce(system.tag_enabled, true) as tag_enabled,
system.avatar_url as system_avatar,
system.account_autoproxy as allow_autoproxy,
system.latch_timeout as latch_timeout
system.latch_timeout as latch_timeout,
system.case_sensitive_proxy_tags as case_sensitive_proxy_tags
-- We need a "from" clause, so we just use some bogus data that's always present
-- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data
from (select 1) as _placeholder

View File

@ -0,0 +1,5 @@
-- schema version 31
alter table system_config add column case_sensitive_proxy_tags boolean not null default true;
update info set schema_version = 31;

View File

@ -9,7 +9,7 @@ namespace PluralKit.Core;
internal class DatabaseMigrator
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 30;
private const int TargetSchemaVersion = 31;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)

View File

@ -17,6 +17,7 @@ public class SystemConfigPatch: PatchObject
public Partial<int?> MemberLimitOverride { get; set; }
public Partial<int?> GroupLimitOverride { get; set; }
public Partial<string[]> DescriptionTemplates { get; set; }
public Partial<bool> CaseSensitiveProxyTags { get; set; }
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
@ -29,6 +30,7 @@ public class SystemConfigPatch: PatchObject
.With("member_limit_override", MemberLimitOverride)
.With("group_limit_override", GroupLimitOverride)
.With("description_templates", DescriptionTemplates)
.With("case_sensitive_proxy_tags", CaseSensitiveProxyTags)
);
public new void AssertIsValid()
@ -78,6 +80,9 @@ public class SystemConfigPatch: PatchObject
if (DescriptionTemplates.IsPresent)
o.Add("description_templates", JArray.FromObject(DescriptionTemplates.Value));
if (CaseSensitiveProxyTags.IsPresent)
o.Add("case_sensitive_proxy_tags", CaseSensitiveProxyTags.Value);
return o;
}
@ -103,6 +108,9 @@ public class SystemConfigPatch: PatchObject
if (o.ContainsKey("description_templates"))
patch.DescriptionTemplates = o.Value<JArray>("description_templates").Select(x => x.Value<string>()).ToArray();
if (o.ContainsKey("case_sensitive_proxy_tags"))
patch.CaseSensitiveProxyTags = o.Value<bool>("case_sensitive_proxy_tags");
return patch;
}
}

View File

@ -18,6 +18,8 @@ public class SystemConfig
public ICollection<string> DescriptionTemplates { get; }
public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
public bool CaseSensitiveProxyTags { get; set; }
}
public static class SystemConfigExt
@ -34,6 +36,7 @@ public static class SystemConfigExt
o.Add("show_private_info", cfg.ShowPrivateInfo);
o.Add("member_limit", cfg.MemberLimitOverride ?? Limits.MaxMemberCount);
o.Add("group_limit", cfg.GroupLimitOverride ?? Limits.MaxGroupCount);
o.Add("case_sensitive_proxy_tags", cfg.CaseSensitiveProxyTags);
o.Add("description_templates", JArray.FromObject(cfg.DescriptionTemplates));

View File

@ -9,9 +9,10 @@ namespace PluralKit.Tests;
public class ProxyTagParserTests
{
internal static ProxyMatch AssertMatch(IEnumerable<ProxyMember> members, string input, string? name = null,
string? prefix = null, string? suffix = null, string? content = null)
string? prefix = null, string? suffix = null, string? content = null,
bool caseSensitive = true)
{
Assert.True(new ProxyTagParser().TryMatch(members, input, out var result));
Assert.True(new ProxyTagParser().TryMatch(members, input, caseSensitive, out var result));
if (name != null) Assert.Equal(name, result.Member.Name);
if (prefix != null) Assert.Equal(prefix, result.ProxyTags?.Prefix);
if (suffix != null) Assert.Equal(suffix, result.ProxyTags?.Suffix);
@ -19,9 +20,9 @@ public class ProxyTagParserTests
return result;
}
internal static void AssertNoMatch(IEnumerable<ProxyMember> members, string? input)
internal static void AssertNoMatch(IEnumerable<ProxyMember> members, string? input, bool caseSensitive = true)
{
Assert.False(new ProxyTagParser().TryMatch(members, input, out _));
Assert.False(new ProxyTagParser().TryMatch(members, input, caseSensitive, out _));
}
public class Basics
@ -46,6 +47,16 @@ public class ProxyTagParserTests
public void StringWithTagsMatch(string input) =>
AssertMatch(members, input);
[Theory]
[InlineData("a:tag with lowercase prefix")]
public void StringWithLowercaseUsingDefaultConfigMatchesNothing(string input) =>
AssertNoMatch(members, input);
[Theory]
[InlineData("a:tag with lowercase prefix")]
public void StringWithLowercaseUsingCaseInsensitiveConfigMatches(string input) =>
AssertMatch(members, input, caseSensitive: false);
[Theory]
[InlineData("[john's tags]", "John")]
[InlineData("{bob's tags}", "Bob")]

View File

@ -117,6 +117,7 @@ Some arguments indicate the use of specific Discord features. These include:
- `pk;config ping <enable|disable>` - Changes your system's ping preferences.
- `pk;config autoproxy timeout [<duration>|off|reset]` - Sets the latch timeout duration for your system.
- `pk;config autoproxy account [on|off]` - Toggles autoproxy globally for the current account.
- `pk;config proxy case [on|off]` - Toggles case sensitive proxy tags for your system.
## Server owner commands
*(all commands here require Manage Server permission)*