Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
07845edee6
@ -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.");
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -28,4 +28,5 @@ public class MessageContext
|
||||
public string? SystemAvatar { get; }
|
||||
public bool AllowAutoproxy { get; }
|
||||
public int? LatchTimeout { get; }
|
||||
public bool CaseSensitiveProxyTags { get; }
|
||||
}
|
@ -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
|
||||
|
5
PluralKit.Core/Database/Migrations/31.sql
Normal file
5
PluralKit.Core/Database/Migrations/31.sql
Normal 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;
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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)*
|
||||
|
Loading…
Reference in New Issue
Block a user