diff --git a/src/Discord.Net.Core/Entities/Messages/Mentions/ChannelMentionHandling.cs b/src/Discord.Net.Core/Entities/Messages/Mentions/ChannelMentionHandling.cs index 39f9baa6a..ea1b91688 100644 --- a/src/Discord.Net.Core/Entities/Messages/Mentions/ChannelMentionHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/Mentions/ChannelMentionHandling.cs @@ -4,6 +4,7 @@ { Ignore = 0, Remove, - Name + Name, + Sanitize } } diff --git a/src/Discord.Net.Core/Entities/Messages/Mentions/RoleMentionHandling.cs b/src/Discord.Net.Core/Entities/Messages/Mentions/RoleMentionHandling.cs index 466cf1fd8..94d4a382f 100644 --- a/src/Discord.Net.Core/Entities/Messages/Mentions/RoleMentionHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/Mentions/RoleMentionHandling.cs @@ -4,6 +4,7 @@ { Ignore = 0, Remove, - Name + Name, + Sanitize } } diff --git a/src/Discord.Net.Core/Entities/Messages/Mentions/UserMentionHandling.cs b/src/Discord.Net.Core/Entities/Messages/Mentions/UserMentionHandling.cs index b31a994a2..42914f393 100644 --- a/src/Discord.Net.Core/Entities/Messages/Mentions/UserMentionHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/Mentions/UserMentionHandling.cs @@ -5,6 +5,7 @@ Ignore = 0, Remove, Name, - NameAndDiscriminator + NameAndDiscriminator, + Sanitize } } diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index acfe35a03..51463cdae 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -1,14 +1,27 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; namespace Discord { public static class MentionUtils { + private const char SanitizeChar = '\x200b'; + + private static readonly Regex _userRegex = new Regex(@"<@!?([0-9]+)>", RegexOptions.Compiled); + private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); + private static readonly Regex _roleRegex = new Regex(@"<@&([0-9]+)>", RegexOptions.Compiled); + //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) - public static string MentionUser(ulong id) => MentionsHelper.MentionUser(id, true); - public static string MentionChannel(ulong id) => MentionsHelper.MentionChannel(id); - public static string MentionRole(ulong id) => MentionsHelper.MentionRole(id); + internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; + public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); + internal static string MentionChannel(string id) => $"<#{id}>"; + public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); + internal static string MentionRole(string id) => $"<@&{id}>"; + public static string MentionRole(ulong id) => MentionRole(id.ToString()); /// Parses a provided user mention string. public static ulong ParseUser(string mentionText) @@ -81,5 +94,191 @@ namespace Discord roleId = 0; return false; } + + internal static ImmutableArray GetUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentionedUsers) + where TUser : class, IUser + { + var matches = _userRegex.Matches(text); + var builder = ImmutableArray.CreateBuilder(matches.Count); + foreach (var match in matches.OfType()) + { + ulong id; + if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + TUser user = null; + + //Verify this user was actually mentioned + foreach (var userMention in mentionedUsers) + { + if (userMention.Id == id) + { + user = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as TUser; + if (user == null) //User not found, fallback to basic mention info + user = userMention; + break; + } + } + + if (user != null) + builder.Add(user); + } + } + return builder.ToImmutable(); + } + internal static ImmutableArray GetChannelMentions(string text, IGuild guild) + { + var matches = _channelRegex.Matches(text); + var builder = ImmutableArray.CreateBuilder(matches.Count); + foreach (var match in matches.OfType()) + { + ulong id; + if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + builder.Add(id); + } + return builder.ToImmutable(); + } + internal static ImmutableArray GetRoleMentions(string text, IGuild guild) + where TRole : class, IRole + { + if (guild == null) + return ImmutableArray.Create(); + + var matches = _roleRegex.Matches(text); + var builder = ImmutableArray.CreateBuilder(matches.Count); + foreach (var match in matches.OfType()) + { + ulong id; + if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var role = guild.GetRole(id) as TRole; + if (role != null) + builder.Add(role); + } + } + return builder.ToImmutable(); + } + + internal static string ResolveUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentions, UserMentionHandling mode) + { + if (mode == UserMentionHandling.Ignore) return text; + + return _userRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + IUser user = null; + foreach (var mention in mentions) + { + if (mention.Id == id) + { + user = mention; + break; + } + } + if (user != null) + { + string name = user.Username; + + var guildUser = user as IGuildUser; + if (e.Value[2] == '!') + { + if (guildUser != null && guildUser.Nickname != null) + name = guildUser.Nickname; + } + + switch (mode) + { + case UserMentionHandling.Name: + return $"@{name}"; + case UserMentionHandling.NameAndDiscriminator: + return $"@{name}#{user.Discriminator}"; + case UserMentionHandling.Sanitize: + return MentionUser($"{SanitizeChar}{id}"); + case UserMentionHandling.Remove: + default: + return ""; + } + } + } + return e.Value; + })); + } + internal static string ResolveChannelMentions(string text, IGuild guild, ChannelMentionHandling mode) + { + if (mode == ChannelMentionHandling.Ignore) return text; + + return _channelRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + switch (mode) + { + case ChannelMentionHandling.Name: + IGuildChannel channel = null; + channel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + if (channel != null) + return $"#{channel.Name}"; + else + return $"#deleted-channel"; + case ChannelMentionHandling.Sanitize: + return MentionChannel($"{SanitizeChar}{id}"); + case ChannelMentionHandling.Remove: + default: + return ""; + } + } + return e.Value; + })); + } + internal static string ResolveRoleMentions(string text, IReadOnlyCollection mentions, RoleMentionHandling mode) + { + if (mode == RoleMentionHandling.Ignore) return text; + + return _roleRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + switch (mode) + { + case RoleMentionHandling.Name: + IRole role = null; + foreach (var mention in mentions) + { + if (mention.Id == id) + { + role = mention; + break; + } + } + if (role != null) + return $"{role.Name}"; + else + return $"deleted-role"; + case RoleMentionHandling.Sanitize: + return MentionRole($"{SanitizeChar}{id}"); + case RoleMentionHandling.Remove: + default: + return ""; + } + } + return e.Value; + })); + } + internal static string ResolveEveryoneMentions(string text, EveryoneMentionHandling mode) + { + if (mode == EveryoneMentionHandling.Ignore) return text; + + switch (mode) + { + case EveryoneMentionHandling.Sanitize: + return text.Replace("@everyone", $"@{SanitizeChar}everyone").Replace("@here", $"@{SanitizeChar}here"); + case EveryoneMentionHandling.Remove: + default: + return text.Replace("@everyone", "").Replace("@here", ""); + } + } } } diff --git a/src/Discord.Net.Core/Utils/MentionsHelper.cs b/src/Discord.Net.Core/Utils/MentionsHelper.cs deleted file mode 100644 index 269e235aa..000000000 --- a/src/Discord.Net.Core/Utils/MentionsHelper.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Discord -{ - internal static class MentionsHelper - { - private static readonly Regex _userRegex = new Regex(@"<@!?([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _roleRegex = new Regex(@"<@&([0-9]+)>", RegexOptions.Compiled); - - //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) - internal static string MentionUser(ulong id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; - internal static string MentionChannel(ulong id) => $"<#{id}>"; - internal static string MentionRole(ulong id) => $"<@&{id}>"; - - internal static ImmutableArray GetUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentionedUsers) - where TUser : class, IUser - { - var matches = _userRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - TUser user = null; - - //Verify this user was actually mentioned - foreach (var userMention in mentionedUsers) - { - if (userMention.Id == id) - { - user = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as TUser; - if (user == null) //User not found, fallback to basic mention info - user = userMention; - break; - } - } - - if (user != null) - builder.Add(user); - } - } - return builder.ToImmutable(); - } - internal static ImmutableArray GetChannelMentions(string text, IGuild guild) - { - var matches = _channelRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - builder.Add(id); - } - return builder.ToImmutable(); - } - internal static ImmutableArray GetRoleMentions(string text, IGuild guild) - where TRole : class, IRole - { - if (guild == null) - return ImmutableArray.Create(); - - var matches = _roleRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - var role = guild.GetRole(id) as TRole; - if (role != null) - builder.Add(role); - } - } - return builder.ToImmutable(); - } - - internal static string ResolveUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentions, UserMentionHandling mode) - { - if (mode == UserMentionHandling.Ignore) return text; - - return _userRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - IUser user = null; - foreach (var mention in mentions) - { - if (mention.Id == id) - { - user = mention; - break; - } - } - if (user != null) - { - string name = user.Username; - - var guildUser = user as IGuildUser; - if (e.Value[2] == '!') - { - if (guildUser != null && guildUser.Nickname != null) - name = guildUser.Nickname; - } - - switch (mode) - { - case UserMentionHandling.Remove: - default: - return ""; - case UserMentionHandling.Name: - return $"@{name}"; - case UserMentionHandling.NameAndDiscriminator: - return $"@{name}#{user.Discriminator}"; - } - } - } - return e.Value; - })); - } - internal static string ResolveChannelMentions(string text, IGuild guild, ChannelMentionHandling mode) - { - if (mode == ChannelMentionHandling.Ignore) return text; - - return _channelRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - switch (mode) - { - case ChannelMentionHandling.Remove: - return ""; - case ChannelMentionHandling.Name: - IGuildChannel channel = null; - channel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - if (channel != null) - return $"#{channel.Name}"; - else - return $"#deleted-channel"; - } - } - return e.Value; - })); - } - internal static string ResolveRoleMentions(string text, IReadOnlyCollection mentions, RoleMentionHandling mode) - { - if (mode == RoleMentionHandling.Ignore) return text; - - return _roleRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - switch (mode) - { - case RoleMentionHandling.Remove: - return ""; - case RoleMentionHandling.Name: - IRole role = null; - foreach (var mention in mentions) - { - if (mention.Id == id) - { - role = mention; - break; - } - } - if (role != null) - return $"@{role.Name}"; - else - return $"@deleted-role"; - } - } - return e.Value; - })); - } - internal static string ResolveEveryoneMentions(string text, EveryoneMentionHandling mode) - { - if (mode == EveryoneMentionHandling.Ignore) return text; - - switch (mode) - { - case EveryoneMentionHandling.Sanitize: - return text.Replace("@everyone", "@\x200beveryone").Replace("@here", "@\x200bhere"); - case EveryoneMentionHandling.Remove: - default: - return text.Replace("@everyone", "").Replace("@here", ""); - } - } - } -} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index b1da6c0b5..e21295a92 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -102,9 +102,9 @@ namespace Discord.Rest { var text = model.Content.Value; - _mentionedUsers = MentionsHelper.GetUserMentions(text, null, mentions); - _mentionedChannelIds = MentionsHelper.GetChannelMentions(text, null); - _mentionedRoles = MentionsHelper.GetRoleMentions(text, null); + _mentionedUsers = MentionUtils.GetUserMentions(text, null, mentions); + _mentionedChannelIds = MentionUtils.GetChannelMentions(text, null); + _mentionedRoles = MentionUtils.GetRoleMentions(text, null); model.Content = text; } } @@ -128,10 +128,10 @@ namespace Discord.Rest public string Resolve(string text, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) { - text = MentionsHelper.ResolveUserMentions(text, null, MentionedUsers, userHandling); - text = MentionsHelper.ResolveChannelMentions(text, null, channelHandling); - text = MentionsHelper.ResolveRoleMentions(text, MentionedRoles, roleHandling); - text = MentionsHelper.ResolveEveryoneMentions(text, everyoneHandling); + text = MentionUtils.ResolveUserMentions(text, null, MentionedUsers, userHandling); + text = MentionUtils.ResolveChannelMentions(text, null, channelHandling); + text = MentionUtils.ResolveRoleMentions(text, MentionedRoles, roleHandling); + text = MentionUtils.ResolveEveryoneMentions(text, everyoneHandling); return text; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs index a33d1f412..05ec87f56 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; -using System.Linq; namespace Discord.API.Gateway { @@ -14,6 +13,6 @@ namespace Discord.API.Gateway public int Limit { get; set; } [JsonProperty("guild_id")] - private ulong[] GuildIds { get; set; } + public IEnumerable GuildIds { get; set; } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 042248152..683f6ec3d 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -105,9 +105,9 @@ namespace Discord.WebSocket var text = model.Content.Value; var guild = (Channel as SocketGuildChannel)?.Guild; - _mentionedUsers = MentionsHelper.GetUserMentions(text, Channel, mentions); - _mentionedChannelIds = MentionsHelper.GetChannelMentions(text, guild); - _mentionedRoles = MentionsHelper.GetRoleMentions(text, guild); + _mentionedUsers = MentionUtils.GetUserMentions(text, Channel, mentions); + _mentionedChannelIds = MentionUtils.GetChannelMentions(text, guild); + _mentionedRoles = MentionUtils.GetRoleMentions(text, guild); model.Content = text; } } @@ -131,10 +131,10 @@ namespace Discord.WebSocket public string Resolve(string text, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) { - text = MentionsHelper.ResolveUserMentions(text, null, MentionedUsers, userHandling); - text = MentionsHelper.ResolveChannelMentions(text, null, channelHandling); - text = MentionsHelper.ResolveRoleMentions(text, MentionedRoles, roleHandling); - text = MentionsHelper.ResolveEveryoneMentions(text, everyoneHandling); + text = MentionUtils.ResolveUserMentions(text, null, MentionedUsers, userHandling); + text = MentionUtils.ResolveChannelMentions(text, null, channelHandling); + text = MentionUtils.ResolveRoleMentions(text, MentionedRoles, roleHandling); + text = MentionUtils.ResolveEveryoneMentions(text, everyoneHandling); return text; }