implement proxied message and permcheck commands

This commit is contained in:
dusk 2025-10-03 02:21:12 +00:00
parent 2b304457cc
commit e4f38c76a9
No known key found for this signature in database
19 changed files with 233 additions and 155 deletions

View file

@ -8,6 +8,7 @@ public partial class CommandTree
{
return command switch
{
Commands.Explain => ctx.Execute<Help>(Explain, m => m.Explain(ctx)),
Commands.Help(_, var flags) => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx, flags.show_embed)),
Commands.HelpCommands => ctx.Reply(
"For the list of commands, see the website: <https://pluralkit.me/commands>"),
@ -236,6 +237,14 @@ public partial class CommandTree
Commands.AutoproxyLatch => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Latch())),
Commands.AutoproxyFront => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Front())),
Commands.AutoproxyMember(var param, _) => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Member(param.target))),
Commands.PermcheckChannel(var param, _) => ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx, param.target)),
Commands.PermcheckGuild(var param, _) => ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx, param.target)),
Commands.MessageProxyCheck(var param, _) => ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)),
Commands.MessageInfo(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)),
Commands.MessageAuthor(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)),
Commands.MessageDelete(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)),
Commands.MessageEdit(var param, var flags) => ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)),
Commands.MessageReproxy(var param, _) => ctx.Execute<ProxiedMessage>(MessageReproxy, m => m.ReproxyMessage(ctx, param.target.MessageId)),
_ =>
// this should only ever occur when deving if commands are not implemented...
ctx.Reply(
@ -251,16 +260,6 @@ public partial class CommandTree
return ctx.Execute<ImportExport>(Import, m => m.Import(ctx));
if (ctx.Match("export"))
return ctx.Execute<ImportExport>(Export, m => m.Export(ctx));
if (ctx.Match("explain"))
return ctx.Execute<Help>(Explain, m => m.Explain(ctx));
if (ctx.Match("message", "msg", "messageinfo"))
return ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx));
if (ctx.Match("edit", "e"))
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, false));
if (ctx.Match("x"))
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, true));
if (ctx.Match("reproxy", "rp", "crimes", "crime"))
return ctx.Execute<ProxiedMessage>(MessageReproxy, m => m.ReproxyMessage(ctx));
if (ctx.Match("log"))
if (ctx.Match("channel"))
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx), true);
@ -283,17 +282,8 @@ public partial class CommandTree
return ctx.Execute<ServerConfig>(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true);
else
return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`.");
if (ctx.Match("proxy"))
if (ctx.Match("debug"))
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
if (ctx.Match("invite")) return ctx.Execute<Misc>(Invite, m => m.Invite(ctx));
if (ctx.Match("stats", "status")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
if (ctx.Match("permcheck"))
return ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
if (ctx.Match("proxycheck"))
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
if (ctx.Match("debug"))
return HandleDebugCommand(ctx);
if (ctx.Match("admin"))
return HandleAdminCommand(ctx);
if (ctx.Match("dashboard", "dash"))
@ -372,26 +362,6 @@ public partial class CommandTree
await ctx.Reply($"{Emojis.Error} Unknown command.");
}
private async Task HandleDebugCommand(Context ctx)
{
var availableCommandsStr = "Available debug targets: `permissions`, `proxying`";
if (ctx.Match("permissions", "perms", "permcheck"))
if (ctx.Match("channel", "ch"))
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx));
else
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
else if (ctx.Match("channel"))
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx));
else if (ctx.Match("proxy", "proxying", "proxycheck"))
await ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
else if (!ctx.HasNext())
await ctx.Reply($"{Emojis.Error} You need to pass a command. {availableCommandsStr}");
else
await ctx.Reply(
$"{Emojis.Error} Unknown debug command {ctx.PeekArgument().AsCode()}. {availableCommandsStr}");
}
private async Task CommandHelpRoot(Context ctx)
{
if (!ctx.HasNext())

View file

@ -106,25 +106,24 @@ public static class ContextArgumentsExt
else return null;
}
public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId)
public static (ulong? messageId, ulong? channelId) GetRepliedTo(this Context ctx)
{
if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null)
return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId);
return (null, null);
}
var word = ctx.PeekArgument();
if (word == null)
return (null, null);
if (parseRawMessageId && ulong.TryParse(word, out var mid))
public static (ulong? messageId, ulong? channelId) ParseMessage(this Context ctx, string maybeMessageRef, bool parseRawMessageId)
{
if (parseRawMessageId && ulong.TryParse(maybeMessageRef, out var mid))
return (mid, null);
var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
var match = Regex.Match(maybeMessageRef, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
if (!match.Success)
return (null, null);
var channelId = ulong.Parse(match.Groups[1].Value);
var messageId = ulong.Parse(match.Groups[2].Value);
ctx.PopArgument();
return (messageId, channelId);
}
}

View file

@ -213,24 +213,4 @@ public static class ContextEntityArgumentsExt
ctx.PopArgument();
return channel;
}
public static async Task<Guild> ParseGuild(this Context ctx, string input)
{
if (!ulong.TryParse(input, out var id))
return null;
return await ctx.Rest.GetGuildOrNull(id);
}
public static async Task<Guild> MatchGuild(this Context ctx)
{
if (!ulong.TryParse(ctx.PeekArgument(), out var id))
return null;
var guild = await ctx.Rest.GetGuildOrNull(id);
if (guild != null)
ctx.PopArgument();
return guild;
}
}

View file

@ -100,6 +100,22 @@ public static class ContextParametersExt
);
}
public static async Task<Myriad.Types.Message.Reference?> ParamResolveMessage(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.MessageRef)?.message
);
}
public static async Task<Myriad.Types.Channel?> ParamResolveChannel(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.ChannelRef)?.channel
);
}
public static async Task<Myriad.Types.Guild?> ParamResolveGuild(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(

View file

@ -13,6 +13,8 @@ public abstract record Parameter()
public record GroupRef(PKGroup group): Parameter;
public record GroupRefs(List<PKGroup> groups): Parameter;
public record SystemRef(PKSystem system): Parameter;
public record MessageRef(Message.Reference message): Parameter;
public record ChannelRef(Channel channel): Parameter;
public record GuildRef(Guild guild): Parameter;
public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter;
public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter;
@ -118,8 +120,12 @@ public class Parameters
return new Parameter.Opaque(opaque.raw);
case uniffi.commands.Parameter.Avatar avatar:
return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar));
case uniffi.commands.Parameter.GuildRef guildRef:
return new Parameter.GuildRef(await ctx.ParseGuild(guildRef.guild) ?? throw new PKError($"Guild {guildRef.guild} not found"));
case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId):
return new Parameter.MessageRef(new Message.Reference(guildId, channelId, messageId));
case uniffi.commands.Parameter.ChannelRef(var channelId):
return new Parameter.ChannelRef(await ctx.Rest.GetChannelOrNull(channelId) ?? throw new PKError($"Channel {channelId} not found"));
case uniffi.commands.Parameter.GuildRef(var guildId):
return new Parameter.GuildRef(await ctx.Rest.GetGuildOrNull(guildId) ?? throw new PKError($"Guild {guildId} not found"));
}
return null;
}

View file

@ -13,10 +13,10 @@ public class Autoproxy
public abstract record Mode()
{
public record Off() : Mode;
public record Latch() : Mode;
public record Front() : Mode;
public record Member(PKMember member) : Mode;
public record Off(): Mode;
public record Latch(): Mode;
public record Front(): Mode;
public record Member(PKMember member): Mode;
}
public Autoproxy(IClock clock)

View file

@ -36,37 +36,11 @@ public class Checks
_cache = cache;
}
public async Task PermCheckGuild(Context ctx)
public async Task PermCheckGuild(Context ctx, Guild guild)
{
Guild guild;
GuildMemberPartial senderGuildUser = null;
if (ctx.Guild != null && !ctx.HasNext())
{
guild = ctx.Guild;
senderGuildUser = ctx.Member;
}
else
{
var guildIdStr = ctx.RemainderOrNull() ??
throw new PKSyntaxError("You must pass a server ID or run this command in a server.");
if (!ulong.TryParse(guildIdStr, out var guildId))
throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
try
{
guild = await _rest.GetGuild(guildId);
}
catch (ForbiddenException)
{
throw Errors.GuildNotFound(guildId);
}
if (guild != null)
senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id);
if (guild == null || senderGuildUser == null)
throw Errors.GuildNotFound(guildId);
}
var senderGuildUser = await _rest.GetGuildMember(guild.Id, ctx.Author.Id);
if (senderGuildUser == null)
throw Errors.GuildNotFound(guild.Id);
var guildMember = await _rest.GetGuildMember(guild.Id, _botConfig.ClientId);
@ -135,17 +109,13 @@ public class Checks
await ctx.Reply(embed: eb.Build());
}
public async Task PermCheckChannel(Context ctx)
public async Task PermCheckChannel(Context ctx, Channel channel)
{
if (!ctx.HasNext())
throw new PKSyntaxError("You need to specify a channel.");
var error = "Channel not found or you do not have permissions to access it.";
// todo: this breaks if channel is not in cache and bot does not have View Channel permissions
// with new cache it breaks if channel is not in current guild
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId == null)
if (channel.GuildId == null)
throw new PKError(error);
var guild = await _rest.GetGuildOrNull(channel.GuildId.Value);
@ -189,15 +159,17 @@ public class Checks
await ctx.Reply(embed: eb.Build());
}
public async Task MessageProxyCheck(Context ctx)
public async Task MessageProxyCheck(Context ctx, Message.Reference? messageReference)
{
if (!ctx.HasNext() && ctx.Message.MessageReference == null)
if (messageReference == null && ctx.Message.MessageReference == null)
throw new PKSyntaxError("You need to specify a message.");
var failedToGetMessage =
"Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you.";
var (messageId, channelId) = ctx.MatchMessage(false);
var (messageId, channelId) = ctx.GetRepliedTo();
if (messageReference != null)
(messageId, channelId) = (messageReference.MessageId, messageReference.ChannelId);
if (messageId == null || channelId == null)
throw new PKError(failedToGetMessage);

View file

@ -58,9 +58,9 @@ public class ProxiedMessage
_redisService = redisService;
}
public async Task ReproxyMessage(Context ctx)
public async Task ReproxyMessage(Context ctx, ulong? messageId)
{
var (msg, systemId) = await GetMessageToEdit(ctx, ReproxyTimeout, true);
var (msg, systemId) = await GetMessageToEdit(ctx, messageId, ReproxyTimeout, true);
if (ctx.System.Id != systemId)
throw new PKError("Can't reproxy a message sent by a different system.");
@ -93,9 +93,9 @@ public class ProxiedMessage
}
}
public async Task EditMessage(Context ctx, bool useRegex)
public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool mutateSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments)
{
var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false);
var (msg, systemId) = await GetMessageToEdit(ctx, messageId, EditTimeout, false);
if (ctx.System.Id != systemId)
throw new PKError("Can't edit a message sent by a different system.");
@ -104,21 +104,10 @@ public class ProxiedMessage
if (originalMsg == null)
throw new PKError("Could not edit message.");
// Regex flag
useRegex = useRegex || ctx.MatchFlag("regex", "x");
// Check if we should append or prepend
var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " ";
var append = ctx.MatchFlag("append", "a");
var prepend = ctx.MatchFlag("prepend", "p");
// Grab the original message content and new message content
var originalContent = originalMsg.Content;
var newContent = ctx.RemainderOrNull()?.NormalizeLineEndSpacing();
// Should we clear embeds?
var clearEmbeds = ctx.MatchFlag("clear-embed", "ce");
var clearAttachments = ctx.MatchFlag("clear-attachments", "ca");
if ((clearEmbeds || clearAttachments) && newContent == null)
newContent = originalMsg.Content!;
@ -249,14 +238,13 @@ public class ProxiedMessage
}
}
private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, Duration timeout, bool isReproxy)
private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, ulong? referencedMessage, Duration timeout, bool isReproxy)
{
var editType = isReproxy ? "reproxy" : "edit";
var editTypeAction = isReproxy ? "reproxied" : "edited";
PKMessage? msg = null;
var (referencedMessage, _) = ctx.MatchMessage(false);
if (referencedMessage != null)
{
await using var conn = await ctx.Database.Obtain();
@ -332,9 +320,8 @@ public class ProxiedMessage
return lastMessage;
}
public async Task GetMessage(Context ctx)
public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author)
{
var (messageId, _) = ctx.MatchMessage(true);
if (messageId == null)
{
if (!ctx.HasNext())
@ -342,8 +329,6 @@ public class ProxiedMessage
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
}
var isDelete = ctx.Match("delete") || ctx.MatchFlag("delete");
var message = await ctx.Repository.GetFullMessage(messageId.Value);
if (message == null)
{
@ -360,8 +345,6 @@ public class ProxiedMessage
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
var format = ctx.MatchFormat();
if (format != ReplyFormat.Standard)
{
var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid);
@ -423,7 +406,7 @@ public class ProxiedMessage
return;
}
if (ctx.Match("author") || ctx.MatchFlag("author"))
if (author)
{
var user = await _rest.GetUser(message.Message.Sender);
var eb = new EmbedBuilder()