diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 8671b0b9..08601749 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -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 [delete|author]", "Looks up a proxied message"); public static Command MessageEdit = new Command("edit", "edit [link] ", "Edit a previously proxied message"); + public static Command MessageReproxy = new Command("reproxy", "reproxy [link] ", "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 ", "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"); diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 70559808..a5453474 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -48,6 +48,8 @@ public partial class CommandTree return ctx.Execute(Message, m => m.GetMessage(ctx)); if (ctx.Match("edit", "e")) return ctx.Execute(MessageEdit, m => m.EditMessage(ctx)); + if (ctx.Match("reproxy", "rp")) + return ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx)); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 3e8edf15..65d47608 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -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 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 GetMessageToEdit(Context ctx) + private async Task 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 FindRecentMessage(Context ctx) + private async Task 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; diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 81a7debd..1015f41d 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -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 diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 07a9839d..d395a190 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -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 ` - Checks why your message has not been proxied. - `pk;edit [message link|reply] ` - 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] ` - 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 ` - Links your system to a different account. - `pk;unlink [account]` - Unlinks an account from your system.