feat: add pk;reproxy (#447)
				
					
				
			This commit is contained in:
		@@ -86,6 +86,7 @@ public partial class CommandTree
 | 
			
		||||
    public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying");
 | 
			
		||||
    public static Command Message = new Command("message", "message <id|link> [delete|author]", "Looks up a proxied message");
 | 
			
		||||
    public static Command MessageEdit = new Command("edit", "edit [link] <text>", "Edit a previously proxied message");
 | 
			
		||||
    public static Command MessageReproxy = new Command("reproxy", "reproxy [link] <member>", "Reproxy a previously proxied message using a different member");
 | 
			
		||||
    public static Command ProxyCheck = new Command("debug proxy", "debug proxy [link|reply]", "Checks why your message has not been proxied");
 | 
			
		||||
    public static Command LogChannel = new Command("log channel", "log channel <channel>", "Designates a channel to post proxied messages to");
 | 
			
		||||
    public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel");
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,8 @@ public partial class CommandTree
 | 
			
		||||
            return ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx));
 | 
			
		||||
        if (ctx.Match("edit", "e"))
 | 
			
		||||
            return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx));
 | 
			
		||||
        if (ctx.Match("reproxy", "rp"))
 | 
			
		||||
            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));
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ using Myriad.Types;
 | 
			
		||||
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
using App.Metrics;
 | 
			
		||||
 | 
			
		||||
using PluralKit.Core;
 | 
			
		||||
 | 
			
		||||
namespace PluralKit.Bot;
 | 
			
		||||
@@ -20,30 +22,70 @@ namespace PluralKit.Bot;
 | 
			
		||||
public class ProxiedMessage
 | 
			
		||||
{
 | 
			
		||||
    private static readonly Duration EditTimeout = Duration.FromMinutes(10);
 | 
			
		||||
    private static readonly Duration ReproxyTimeout = Duration.FromMinutes(1);
 | 
			
		||||
 | 
			
		||||
    // private readonly IDiscordCache _cache;
 | 
			
		||||
    private readonly IClock _clock;
 | 
			
		||||
    private readonly ModelRepository _repo;
 | 
			
		||||
    private readonly IMetrics _metrics;
 | 
			
		||||
 | 
			
		||||
    private readonly EmbedService _embeds;
 | 
			
		||||
    private readonly LogChannelService _logChannel;
 | 
			
		||||
    private readonly DiscordApiClient _rest;
 | 
			
		||||
    private readonly WebhookExecutorService _webhookExecutor;
 | 
			
		||||
    private readonly ProxyService _proxy;
 | 
			
		||||
 | 
			
		||||
    public ProxiedMessage(EmbedService embeds, IClock clock,
 | 
			
		||||
                          DiscordApiClient rest,
 | 
			
		||||
    public ProxiedMessage(EmbedService embeds,
 | 
			
		||||
                          DiscordApiClient rest, IMetrics metrics, ModelRepository repo, ProxyService proxy,
 | 
			
		||||
                          WebhookExecutorService webhookExecutor, LogChannelService logChannel, IDiscordCache cache)
 | 
			
		||||
    {
 | 
			
		||||
        _embeds = embeds;
 | 
			
		||||
        _clock = clock;
 | 
			
		||||
        _rest = rest;
 | 
			
		||||
        _webhookExecutor = webhookExecutor;
 | 
			
		||||
        _repo = repo;
 | 
			
		||||
        _logChannel = logChannel;
 | 
			
		||||
        // _cache = cache;
 | 
			
		||||
        _metrics = metrics;
 | 
			
		||||
        _proxy =  proxy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ReproxyMessage(Context ctx)
 | 
			
		||||
    {
 | 
			
		||||
        var msg = await GetMessageToEdit(ctx, ReproxyTimeout, true);
 | 
			
		||||
 | 
			
		||||
        if (ctx.System.Id != msg.System?.Id)
 | 
			
		||||
            throw new PKError("Can't reproxy a message sent by a different system.");
 | 
			
		||||
 | 
			
		||||
        // Get target member ID
 | 
			
		||||
        var target = await ctx.MatchMember(restrictToSystem: ctx.System.Id);
 | 
			
		||||
        if (target == null)
 | 
			
		||||
            throw new PKError("Could not find a member to reproxy the message with.");
 | 
			
		||||
 | 
			
		||||
        // Fetch members and get the ProxyMember for `target`
 | 
			
		||||
        List <ProxyMember> members;
 | 
			
		||||
        using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime))
 | 
			
		||||
            members = (await _repo.GetProxyMembers(ctx.Author.Id, msg.Message.Guild!.Value)).ToList();
 | 
			
		||||
        var match = members.Find(x => x.Id == target.Id);
 | 
			
		||||
        if (match == null)
 | 
			
		||||
            throw new PKError("Could not find a member to reproxy the message with.");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _proxy.ExecuteReproxy(ctx.Message, msg.Message, match);
 | 
			
		||||
 | 
			
		||||
            if (ctx.Guild == null)
 | 
			
		||||
                await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success });
 | 
			
		||||
            if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages))
 | 
			
		||||
                await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id);
 | 
			
		||||
        }
 | 
			
		||||
        catch (NotFoundException)
 | 
			
		||||
        {
 | 
			
		||||
            throw new PKError("Could not reproxy message.");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task EditMessage(Context ctx)
 | 
			
		||||
    {
 | 
			
		||||
        var msg = await GetMessageToEdit(ctx);
 | 
			
		||||
        var msg = await GetMessageToEdit(ctx, EditTimeout, false);
 | 
			
		||||
 | 
			
		||||
        if (ctx.System.Id != msg.System?.Id)
 | 
			
		||||
            throw new PKError("Can't edit a message sent by a different system.");
 | 
			
		||||
@@ -93,8 +135,11 @@ public class ProxiedMessage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<FullMessage> GetMessageToEdit(Context ctx)
 | 
			
		||||
    private async Task<FullMessage> GetMessageToEdit(Context ctx, Duration timeout, bool isReproxy)
 | 
			
		||||
    {
 | 
			
		||||
        var editType = isReproxy ? "reproxy" : "edit";
 | 
			
		||||
        var editTypeAction = isReproxy ? "reproxied" : "edited";
 | 
			
		||||
 | 
			
		||||
        // todo: is it correct to get a connection here?
 | 
			
		||||
        await using var conn = await ctx.Database.Obtain();
 | 
			
		||||
        FullMessage? msg = null;
 | 
			
		||||
@@ -110,15 +155,15 @@ public class ProxiedMessage
 | 
			
		||||
        if (msg == null)
 | 
			
		||||
        {
 | 
			
		||||
            if (ctx.Guild == null)
 | 
			
		||||
                throw new PKSyntaxError("You must use a message link to edit messages in DMs.");
 | 
			
		||||
                throw new PKSyntaxError($"You must use a message link to {editType} messages in DMs.");
 | 
			
		||||
 | 
			
		||||
            var recent = await FindRecentMessage(ctx);
 | 
			
		||||
            var recent = await FindRecentMessage(ctx, timeout);
 | 
			
		||||
            if (recent == null)
 | 
			
		||||
                throw new PKSyntaxError("Could not find a recent message to edit.");
 | 
			
		||||
                throw new PKSyntaxError($"Could not find a recent message to {editType}.");
 | 
			
		||||
 | 
			
		||||
            msg = await ctx.Repository.GetMessage(conn, recent.Mid);
 | 
			
		||||
            if (msg == null)
 | 
			
		||||
                throw new PKSyntaxError("Could not find a recent message to edit.");
 | 
			
		||||
                throw new PKSyntaxError($"Could not find a recent message to {editType}.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (msg.Message.Channel != ctx.Channel.Id)
 | 
			
		||||
@@ -136,17 +181,21 @@ public class ProxiedMessage
 | 
			
		||||
                throw new PKError(error);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var msgTimestamp = DiscordUtils.SnowflakeToInstant(msg.Message.Mid);
 | 
			
		||||
        if (isReproxy && SystemClock.Instance.GetCurrentInstant() - msgTimestamp > timeout)
 | 
			
		||||
            throw new PKError($"The message is too old to be {editTypeAction}.");
 | 
			
		||||
 | 
			
		||||
        return msg;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<PKMessage?> FindRecentMessage(Context ctx)
 | 
			
		||||
    private async Task<PKMessage?> FindRecentMessage(Context ctx, Duration timeout)
 | 
			
		||||
    {
 | 
			
		||||
        var lastMessage = await ctx.Repository.GetLastMessage(ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id);
 | 
			
		||||
        if (lastMessage == null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Mid);
 | 
			
		||||
        if (_clock.GetCurrentInstant() - timestamp > EditTimeout)
 | 
			
		||||
        if (SystemClock.Instance.GetCurrentInstant() - timestamp > timeout)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return lastMessage;
 | 
			
		||||
 
 | 
			
		||||
@@ -188,6 +188,60 @@ public class ProxyService
 | 
			
		||||
        await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ExecuteReproxy(Message trigger, PKMessage msg, ProxyMember member)
 | 
			
		||||
    {
 | 
			
		||||
        var originalMsg = await _rest.GetMessageOrNull(msg.Channel, msg.Mid);
 | 
			
		||||
        if (originalMsg == null)
 | 
			
		||||
            throw new PKError("Could not reproxy message.");
 | 
			
		||||
 | 
			
		||||
        // Get a MessageContext for the original message
 | 
			
		||||
        MessageContext ctx =
 | 
			
		||||
            await _repo.GetMessageContext(msg.Sender, msg.Guild!.Value, msg.Channel);
 | 
			
		||||
 | 
			
		||||
        // Make sure proxying is enabled here
 | 
			
		||||
        if (ctx.InBlacklist)
 | 
			
		||||
            throw new ProxyChecksFailedException(
 | 
			
		||||
                "Proxying was disabled in this channel by a server administrator (via the proxy blacklist).");
 | 
			
		||||
 | 
			
		||||
        var match = new ProxyMatch
 | 
			
		||||
        {
 | 
			
		||||
            Member = member,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var messageChannel = await _rest.GetChannelOrNull(msg.Channel!);
 | 
			
		||||
        var rootChannel = await _rest.GetChannelOrNull(messageChannel.IsThread() ? messageChannel.ParentId!.Value : messageChannel.Id);
 | 
			
		||||
        var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
 | 
			
		||||
        var guild = await _rest.GetGuildOrNull(msg.Guild!.Value);
 | 
			
		||||
 | 
			
		||||
        // Grab user permissions
 | 
			
		||||
        var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, trigger.Author.Id, null);
 | 
			
		||||
        var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone);
 | 
			
		||||
 | 
			
		||||
        // Make sure user has permissions to send messages
 | 
			
		||||
        if (!senderPermissions.HasFlag(PermissionSet.SendMessages))
 | 
			
		||||
            throw new PKError("You don't have permission to send messages in the channel that message is in.");
 | 
			
		||||
 | 
			
		||||
        // Send the reproxied webhook
 | 
			
		||||
        var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest
 | 
			
		||||
        {
 | 
			
		||||
            GuildId = guild.Id,
 | 
			
		||||
            ChannelId = rootChannel.Id,
 | 
			
		||||
            ThreadId = threadId,
 | 
			
		||||
            Name = match.Member.ProxyName(ctx),
 | 
			
		||||
            AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
 | 
			
		||||
            Content = originalMsg.Content!,
 | 
			
		||||
            Attachments = originalMsg.Attachments!,
 | 
			
		||||
            FileSizeLimit = guild.FileSizeLimit(),
 | 
			
		||||
            Embeds = originalMsg.Embeds!.ToArray(),
 | 
			
		||||
            Stickers = originalMsg.StickerItems!,
 | 
			
		||||
            AllowEveryone = allowEveryone
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, msg.Guild!.Value, null);
 | 
			
		||||
        await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match, deletePrevious: false);
 | 
			
		||||
        await _rest.DeleteMessage(originalMsg.ChannelId!, originalMsg.Id!);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<(string?, string?)> FetchReferencedMessageAuthorInfo(Message trigger, Message referenced)
 | 
			
		||||
    {
 | 
			
		||||
        if (referenced.WebhookId != null)
 | 
			
		||||
@@ -308,7 +362,8 @@ public class ProxyService
 | 
			
		||||
        => message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\");
 | 
			
		||||
 | 
			
		||||
    private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings,
 | 
			
		||||
                                                Message triggerMessage, Message proxyMessage, ProxyMatch match)
 | 
			
		||||
                                                  Message triggerMessage, Message proxyMessage, ProxyMatch match,
 | 
			
		||||
                                                  bool deletePrevious = true)
 | 
			
		||||
    {
 | 
			
		||||
        var sentMessage = new PKMessage
 | 
			
		||||
        {
 | 
			
		||||
@@ -338,6 +393,9 @@ public class ProxyService
 | 
			
		||||
 | 
			
		||||
        async Task DeleteProxyTriggerMessage()
 | 
			
		||||
        {
 | 
			
		||||
            if (!deletePrevious)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // Wait a second or so before deleting the original message
 | 
			
		||||
            await Task.Delay(MessageDeletionDelay);
 | 
			
		||||
            try
 | 
			
		||||
 
 | 
			
		||||
@@ -134,6 +134,7 @@ Some arguments indicate the use of specific Discord features. These include:
 | 
			
		||||
- `pk;debug permissions [server id]` - [Checks the given server's permission setup](/staff/permissions/#permission-checker-command) to check if it's compatible with PluralKit.
 | 
			
		||||
- `pk;debug proxying <message link|reply>` - Checks why your message has not been proxied.
 | 
			
		||||
- `pk;edit [message link|reply] <new content>` - Edits a proxied message. Without an explicit message target, will target the last message proxied by your system in the current channel. **Does not support message IDs!**
 | 
			
		||||
- `pk;reproxy [message link|reply] <member name|ID>` - Reproxies a message using a different member. Without an explicit message target, will target the last message proxied by your system in the current channel.
 | 
			
		||||
- `pk;link <account>` - Links your system to a different account.
 | 
			
		||||
- `pk;unlink [account]` - Unlinks an account from your system.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user