diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 6c99d0ce..9fbf18ed 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -91,7 +91,7 @@ namespace PluralKit.Bot // Check if message starts with the command prefix if (content.StartsWith("pk;", StringComparison.InvariantCultureIgnoreCase)) argPos = 3; else if (content.StartsWith("pk!", StringComparison.InvariantCultureIgnoreCase)) argPos = 3; - else if (StringUtils.HasMentionPrefix(content, ref argPos, out var id)) // Set argPos to the proper value + else if (DiscordUtils.HasMentionPrefix(content, ref argPos, out var id)) // Set argPos to the proper value if (id != _client.CurrentUser.Id) // But undo it if it's someone else's ping argPos = -1; diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 354bcee8..3403d459 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 0ec95374..cf684811 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -56,9 +56,12 @@ namespace PluralKit.Bot // Permission check after proxy match so we don't get spammed when not actually proxying if (!await CheckBotPermissionsOrError(message.Channel)) return false; if (!CheckProxyNameBoundsOrError(match.Member.ProxyName(ctx))) return false; + + // Check if we can mention everyone/here + var allowEveryone = (message.Channel.PermissionsInSync(message.Author) & Permissions.MentionEveryone) != 0; // Everything's in order, we can execute the proxy! - await ExecuteProxy(conn, message, ctx, match); + await ExecuteProxy(conn, message, ctx, match, allowEveryone); return true; } @@ -85,14 +88,13 @@ namespace PluralKit.Bot } private async Task ExecuteProxy(IPKConnection conn, DiscordMessage trigger, MessageContext ctx, - ProxyMatch match) + ProxyMatch match, bool allowEveryone) { // Send the webhook var id = await _webhookExecutor.ExecuteWebhook(trigger.Channel, match.Member.ProxyName(ctx), match.Member.ProxyAvatar(ctx), - match.ProxyContent, trigger.Attachments); - - + match.ProxyContent, trigger.Attachments, allowEveryone); + Task SaveMessage() => _data.AddMessage(conn, trigger.Author.Id, trigger.Channel.GuildId, trigger.Channel.Id, id, trigger.Id, match.Member.Id); Task LogMessage() => _logChannel.LogMessage(ctx, match, trigger, id).AsTask(); async Task DeleteMessage() diff --git a/PluralKit.Bot/Proxy/ProxyTagParser.cs b/PluralKit.Bot/Proxy/ProxyTagParser.cs index a598b030..6ff39215 100644 --- a/PluralKit.Bot/Proxy/ProxyTagParser.cs +++ b/PluralKit.Bot/Proxy/ProxyTagParser.cs @@ -80,7 +80,7 @@ namespace PluralKit.Bot private string? ExtractLeadingMention(ref string input) { var mentionPos = 0; - if (!StringUtils.HasMentionPrefix(input, ref mentionPos, out _)) return null; + if (!DiscordUtils.HasMentionPrefix(input, ref mentionPos, out _)) return null; var leadingMention = input.Substring(0, mentionPos); input = input.Substring(mentionPos); diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 62032ec9..8ba7047c 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -42,13 +42,13 @@ namespace PluralKit.Bot _logger = logger.ForContext(); } - public async Task ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList attachments) + public async Task ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList attachments, bool allowEveryone) { _logger.Verbose("Invoking webhook in channel {Channel}", channel.Id); // Get a webhook, execute it var webhook = await _webhookCache.GetWebhook(channel); - var id = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments); + var id = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments, allowEveryone); // Log the relevant metrics _metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied); @@ -59,11 +59,14 @@ namespace PluralKit.Bot } private async Task ExecuteWebhookInner(DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content, - IReadOnlyList attachments, bool hasRetried = false) + IReadOnlyList attachments, bool allowEveryone, bool hasRetried = false) { + content = content.Truncate(2000); + var dwb = new DiscordWebhookBuilder(); dwb.WithUsername(FixClyde(name).Truncate(80)); - dwb.WithContent(content.Truncate(2000)); + dwb.WithContent(content); + dwb.AddMentions(content.ParseAllMentions(allowEveryone)); if (avatarUrl != null) dwb.WithAvatarUrl(avatarUrl); var attachmentChunks = ChunkAttachmentsOrThrow(attachments, 8 * 1024 * 1024); @@ -95,7 +98,7 @@ namespace PluralKit.Bot _logger.Warning("Error invoking webhook {Webhook} in channel {Channel}", webhook.Id, webhook.ChannelId); var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(channel, webhook); - return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, hasRetried: true); + return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, allowEveryone, hasRetried: true); } throw; diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index bd493abd..a470c43d 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using System.Threading.Tasks; using DSharpPlus; @@ -124,6 +126,63 @@ namespace PluralKit.Bot .GetProperty("UserCache", BindingFlags.Instance | BindingFlags.NonPublic) ?.GetValue(client); return cache != null && cache.TryGetValue(id, out user); + } + + private static readonly Regex USER_MENTION = new Regex("<@!?(\\d{17,19})>"); + private static readonly Regex ROLE_MENTION = new Regex("<@&(\\d{17,19})>"); + private static readonly Regex EVERYONE_HERE_MENTION = new Regex("@(everyone|here)"); + public static DiscordColor? ToDiscordColor(this string color) + { + if (int.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) + return new DiscordColor(colorInt); + throw new ArgumentException($"Invalid color string '{color}'."); + } + + public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId) + { + mentionId = 0; + + // Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix + if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@')) + return false; + int num = content.IndexOf('>'); + if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !TryParseMention(content.Substring(0, num + 1), out mentionId)) + return false; + argPos = num + 2; + return true; + } + + public static bool TryParseMention(this string potentialMention, out ulong id) + { + if (ulong.TryParse(potentialMention, out id)) return true; + + var match = USER_MENTION.Match(potentialMention); + if (match.Success && match.Index == 0 && match.Length == potentialMention.Length) + { + id = ulong.Parse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture); + return true; + } + + return false; + } + + public static IEnumerable ParseAllMentions(this string input, bool allowEveryone = false) + { + var mentions = new List(); + mentions.AddRange(USER_MENTION.Matches(input) + .Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention)); + mentions.AddRange(ROLE_MENTION.Matches(input) + .Select(x => new RoleMention(ulong.Parse(x.Groups[1].Value)) as IMention)); + if (EVERYONE_HERE_MENTION.IsMatch(input) && allowEveryone) + mentions.Add(new EveryoneMention()); + return mentions; + } + + public static string EscapeMarkdown(this string input) + { + Regex pattern = new Regex(@"[*_~>`(||)\\]", RegexOptions.Multiline); + if (input != null) return pattern.Replace(input, @"\$&"); + else return input; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Utils/StringUtils.cs b/PluralKit.Bot/Utils/StringUtils.cs deleted file mode 100644 index 7423faa0..00000000 --- a/PluralKit.Bot/Utils/StringUtils.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; -using System.Text.RegularExpressions; - -using DSharpPlus.Entities; - -namespace PluralKit.Bot -{ - public static class StringUtils - { - private static readonly Regex USER_MENTION = new Regex("^<@!?(\\d{17,19})>$"); - public static DiscordColor? ToDiscordColor(this string color) - { - if (int.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) - return new DiscordColor(colorInt); - throw new ArgumentException($"Invalid color string '{color}'."); - } - - public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId) - { - mentionId = 0; - - // Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix - if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@')) - return false; - int num = content.IndexOf('>'); - if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !TryParseMention(content.Substring(0, num + 1), out mentionId)) - return false; - argPos = num + 2; - return true; - } - - public static bool TryParseMention(this string potentialMention, out ulong id) - { - if (ulong.TryParse(potentialMention, out id)) return true; - - var match = USER_MENTION.Match(potentialMention); - if (match.Success) - { - id = ulong.Parse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture); - return true; - } - - return false; - } - - public static string EscapeMarkdown(this string input) - { - Regex pattern = new Regex(@"[*_~>`(||)\\]", RegexOptions.Multiline); - if (input != null) return pattern.Replace(input, @"\$&"); - else return input; - } - } -} \ No newline at end of file