| @@ -225,7 +225,7 @@ namespace Discord.Commands | |||||
| return false; | return false; | ||||
| } | } | ||||
| public SearchResult Search(IMessage message, int argPos) => Search(message, message.RawText.Substring(argPos)); | |||||
| public SearchResult Search(IMessage message, int argPos) => Search(message, message.Text.Substring(argPos)); | |||||
| public SearchResult Search(IMessage message, string input) | public SearchResult Search(IMessage message, string input) | ||||
| { | { | ||||
| string lowerInput = input.ToLowerInvariant(); | string lowerInput = input.ToLowerInvariant(); | ||||
| @@ -237,7 +237,7 @@ namespace Discord.Commands | |||||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | ||||
| } | } | ||||
| public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.RawText.Substring(argPos)); | |||||
| public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.Text.Substring(argPos)); | |||||
| public async Task<IResult> Execute(IMessage message, string input) | public async Task<IResult> Execute(IMessage message, string input) | ||||
| { | { | ||||
| var searchResult = Search(message, input); | var searchResult = Search(message, input); | ||||
| @@ -4,7 +4,7 @@ | |||||
| { | { | ||||
| public static bool HasCharPrefix(this IMessage msg, char c, ref int argPos) | public static bool HasCharPrefix(this IMessage msg, char c, ref int argPos) | ||||
| { | { | ||||
| var text = msg.RawText; | |||||
| var text = msg.Text; | |||||
| if (text.Length > 0 && text[0] == c) | if (text.Length > 0 && text[0] == c) | ||||
| { | { | ||||
| argPos = 1; | argPos = 1; | ||||
| @@ -14,7 +14,7 @@ | |||||
| } | } | ||||
| public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) | public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) | ||||
| { | { | ||||
| var text = msg.RawText; | |||||
| var text = msg.Text; | |||||
| //str = str + ' '; | //str = str + ' '; | ||||
| if (text.StartsWith(str)) | if (text.StartsWith(str)) | ||||
| { | { | ||||
| @@ -25,7 +25,7 @@ | |||||
| } | } | ||||
| public static bool HasMentionPrefix(this IMessage msg, IUser user, ref int argPos) | public static bool HasMentionPrefix(this IMessage msg, IUser user, ref int argPos) | ||||
| { | { | ||||
| var text = msg.RawText; | |||||
| var text = msg.Text; | |||||
| string mention = user.Mention + ' '; | string mention = user.Mention + ' '; | ||||
| if (text.StartsWith(mention)) | if (text.StartsWith(mention)) | ||||
| { | { | ||||
| @@ -13,9 +13,7 @@ namespace Discord | |||||
| bool IsTTS { get; } | bool IsTTS { get; } | ||||
| /// <summary> Returns true if this message was added to its channel's pinned messages. </summary> | /// <summary> Returns true if this message was added to its channel's pinned messages. </summary> | ||||
| bool IsPinned { get; } | bool IsPinned { get; } | ||||
| /// <summary> Returns the original, unprocessed text for this message. </summary> | |||||
| string RawText { get; } | |||||
| /// <summary> Returns the text for this message after mention processing. </summary> | |||||
| /// <summary> Returns the text for this message. </summary> | |||||
| string Text { get; } | string Text { get; } | ||||
| /// <summary> Gets the time this message was sent. </summary> | /// <summary> Gets the time this message was sent. </summary> | ||||
| DateTimeOffset Timestamp { get; } | DateTimeOffset Timestamp { get; } | ||||
| @@ -41,5 +39,10 @@ namespace Discord | |||||
| Task PinAsync(); | Task PinAsync(); | ||||
| /// <summary> Removes this message from its channel's pinned messages. </summary> | /// <summary> Removes this message from its channel's pinned messages. </summary> | ||||
| Task UnpinAsync(); | Task UnpinAsync(); | ||||
| /// <summary> Transforms this message's text into a human readable form, resolving things like mentions to that object's name. </summary> | |||||
| string Resolve(int startIndex, int length, UserResolveMode userMode = UserResolveMode.NameOnly); | |||||
| /// <summary> Transforms this message's text into a human readable form, resolving things like mentions to that object's name. </summary> | |||||
| string Resolve(UserResolveMode userMode = UserResolveMode.NameOnly); | |||||
| } | } | ||||
| } | } | ||||
| @@ -16,7 +16,6 @@ namespace Discord | |||||
| private long? _editedTimestampTicks; | private long? _editedTimestampTicks; | ||||
| public bool IsTTS { get; private set; } | public bool IsTTS { get; private set; } | ||||
| public string RawText { get; private set; } | |||||
| public string Text { get; private set; } | public string Text { get; private set; } | ||||
| public bool IsPinned { get; private set; } | public bool IsPinned { get; private set; } | ||||
| @@ -111,29 +110,15 @@ namespace Discord | |||||
| if (model.Content.IsSpecified) | if (model.Content.IsSpecified) | ||||
| { | { | ||||
| RawText = model.Content.Value; | |||||
| var text = model.Content.Value; | |||||
| if (guildChannel != null) | if (guildChannel != null) | ||||
| { | { | ||||
| var orderedMentionedUsers = ImmutableArray.CreateBuilder<IUser>(5); | |||||
| Text = MentionUtils.CleanUserMentions(RawText, Channel.IsAttached ? Channel : null, MentionedUsers, orderedMentionedUsers); | |||||
| MentionedUsers = orderedMentionedUsers.ToImmutable(); | |||||
| var roles = ImmutableArray.CreateBuilder<IRole>(5); | |||||
| Text = MentionUtils.CleanRoleMentions(Text, guildChannel.Guild, roles); | |||||
| MentionedRoles = roles.ToImmutable(); | |||||
| if (guildChannel.IsAttached) //It's too expensive to do a channel lookup in REST mode | |||||
| { | |||||
| var channelIds = ImmutableArray.CreateBuilder<ulong>(5); | |||||
| Text = MentionUtils.CleanChannelMentions(Text, guildChannel.Guild, channelIds); | |||||
| MentionedChannelIds = channelIds.ToImmutable(); | |||||
| } | |||||
| else | |||||
| MentionedChannelIds = MentionUtils.GetChannelMentions(RawText); | |||||
| MentionedUsers = MentionUtils.GetUserMentions(text, Channel.IsAttached ? Channel : null, MentionedUsers); | |||||
| MentionedChannelIds = MentionUtils.GetChannelMentions(text, guildChannel.Guild); | |||||
| MentionedRoles = MentionUtils.GetRoleMentions(text, guildChannel.Guild); | |||||
| } | } | ||||
| else | |||||
| Text = RawText; | |||||
| Text = text; | |||||
| } | } | ||||
| } | } | ||||
| @@ -168,16 +153,32 @@ namespace Discord | |||||
| else | else | ||||
| await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> Adds this message to its channel's pinned messages. </summary> | |||||
| public async Task PinAsync() | public async Task PinAsync() | ||||
| { | { | ||||
| await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); | await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> Removes this message from its channel's pinned messages. </summary> | |||||
| public async Task UnpinAsync() | public async Task UnpinAsync() | ||||
| { | { | ||||
| await Discord.ApiClient.RemovePinAsync(Channel.Id, Id).ConfigureAwait(false); | await Discord.ApiClient.RemovePinAsync(Channel.Id, Id).ConfigureAwait(false); | ||||
| } | } | ||||
| public string Resolve(int startIndex, int length, UserResolveMode userMode = UserResolveMode.NameOnly) | |||||
| => Resolve(Text.Substring(startIndex, length), userMode); | |||||
| public string Resolve(UserResolveMode userMode = UserResolveMode.NameOnly) | |||||
| => Resolve(Text, userMode); | |||||
| private string Resolve(string text, UserResolveMode userMode = UserResolveMode.NameOnly) | |||||
| { | |||||
| var guild = (Channel as IGuildChannel)?.Guild; | |||||
| text = MentionUtils.ResolveUserMentions(text, Channel, MentionedUsers, userMode); | |||||
| if (guild != null) | |||||
| { | |||||
| if (guild.IsAttached) //It's too expensive to do a channel lookup in REST mode | |||||
| text = MentionUtils.ResolveChannelMentions(text, guild); | |||||
| text = MentionUtils.ResolveRoleMentions(text, guild, MentionedRoles); | |||||
| } | |||||
| return text; | |||||
| } | |||||
| public override string ToString() => Text; | public override string ToString() => Text; | ||||
| private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; | private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; | ||||
| @@ -0,0 +1,8 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public enum UserResolveMode | |||||
| { | |||||
| NameOnly = 0, | |||||
| NameAndDiscriminator | |||||
| } | |||||
| } | |||||
| @@ -65,6 +65,7 @@ namespace Discord | |||||
| channelId = 0; | channelId = 0; | ||||
| return false; | return false; | ||||
| } | } | ||||
| /// <summary> Parses a provided role mention string. </summary> | /// <summary> Parses a provided role mention string. </summary> | ||||
| public static ulong ParseRole(string mentionText) | public static ulong ParseRole(string mentionText) | ||||
| { | { | ||||
| @@ -87,44 +88,72 @@ namespace Discord | |||||
| roleId = 0; | roleId = 0; | ||||
| return false; | return false; | ||||
| } | } | ||||
| internal static ImmutableArray<IUser> GetUserMentions(string text, IMessageChannel channel, IReadOnlyCollection<IUser> fallbackUsers) | |||||
| { | |||||
| var matches = _userRegex.Matches(text); | |||||
| var builder = ImmutableArray.CreateBuilder<IUser>(matches.Count); | |||||
| foreach (var match in matches.OfType<Match>()) | |||||
| { | |||||
| ulong id; | |||||
| if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||||
| { | |||||
| IUser user = null; | |||||
| if (channel != null) | |||||
| user = channel.GetUserAsync(id).GetAwaiter().GetResult() as IUser; | |||||
| if (user == null) | |||||
| { | |||||
| foreach (var fallbackUser in fallbackUsers) | |||||
| { | |||||
| if (fallbackUser.Id == id) | |||||
| { | |||||
| user = fallbackUser; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| /// <summary> Gets the ids of all users mentioned in a provided text.</summary> | |||||
| public static ImmutableArray<ulong> GetUserMentions(string text) => GetMentions(text, _userRegex).ToImmutable(); | |||||
| /// <summary> Gets the ids of all channels mentioned in a provided text.</summary> | |||||
| public static ImmutableArray<ulong> GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToImmutable(); | |||||
| /// <summary> Gets the ids of all roles mentioned in a provided text.</summary> | |||||
| public static ImmutableArray<ulong> GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToImmutable(); | |||||
| private static ImmutableArray<ulong>.Builder GetMentions(string text, Regex regex) | |||||
| if (user != null) | |||||
| builder.Add(user); | |||||
| } | |||||
| } | |||||
| return builder.ToImmutable(); | |||||
| } | |||||
| internal static ImmutableArray<ulong> GetChannelMentions(string text, IGuild guild) | |||||
| { | { | ||||
| var matches = regex.Matches(text); | |||||
| var matches = _channelRegex.Matches(text); | |||||
| var builder = ImmutableArray.CreateBuilder<ulong>(matches.Count); | var builder = ImmutableArray.CreateBuilder<ulong>(matches.Count); | ||||
| foreach (var match in matches.OfType<Match>()) | foreach (var match in matches.OfType<Match>()) | ||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | ||||
| { | |||||
| /*var channel = guild.GetChannelAsync(id).GetAwaiter().GetResult(); | |||||
| if (channel != null) | |||||
| builder.Add(channel);*/ | |||||
| builder.Add(id); | builder.Add(id); | ||||
| } | |||||
| } | } | ||||
| return builder; | |||||
| return builder.ToImmutable(); | |||||
| } | } | ||||
| /*internal static string CleanUserMentions(string text, ImmutableArray<User> mentions) | |||||
| internal static ImmutableArray<IRole> GetRoleMentions(string text, IGuild guild) | |||||
| { | { | ||||
| return _userRegex.Replace(text, new MatchEvaluator(e => | |||||
| var matches = _roleRegex.Matches(text); | |||||
| var builder = ImmutableArray.CreateBuilder<IRole>(matches.Count); | |||||
| foreach (var match in matches.OfType<Match>()) | |||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||||
| if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||||
| { | { | ||||
| for (int i = 0; i < mentions.Length; i++) | |||||
| { | |||||
| var mention = mentions[i]; | |||||
| if (mention.Id == id) | |||||
| return '@' + mention.Username; | |||||
| } | |||||
| var role = guild.GetRole(id); | |||||
| if (role != null) | |||||
| builder.Add(role); | |||||
| } | } | ||||
| return e.Value; | |||||
| })); | |||||
| }*/ | |||||
| internal static string CleanUserMentions(string text, IMessageChannel channel, IReadOnlyCollection<IUser> fallbackUsers, ImmutableArray<IUser>.Builder mentions = null) | |||||
| } | |||||
| return builder.ToImmutable(); | |||||
| } | |||||
| internal static string ResolveUserMentions(string text, IMessageChannel channel, IReadOnlyCollection<IUser> mentions, UserResolveMode mode) | |||||
| { | { | ||||
| return _userRegex.Replace(text, new MatchEvaluator(e => | return _userRegex.Replace(text, new MatchEvaluator(e => | ||||
| { | { | ||||
| @@ -132,67 +161,71 @@ namespace Discord | |||||
| if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | ||||
| { | { | ||||
| IUser user = null; | IUser user = null; | ||||
| if (channel != null) | |||||
| user = channel.GetUserAsync(id).GetAwaiter().GetResult() as IUser; | |||||
| if (user == null) | |||||
| foreach (var mention in mentions) | |||||
| { | { | ||||
| foreach (var fallbackUser in fallbackUsers) | |||||
| if (mention.Id == id) | |||||
| { | { | ||||
| if (fallbackUser.Id == id) | |||||
| { | |||||
| user = fallbackUser; | |||||
| break; | |||||
| } | |||||
| user = mention; | |||||
| break; | |||||
| } | } | ||||
| } | } | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| mentions.Add(user); | |||||
| string name = user.Username; | |||||
| var guildUser = user as IGuildUser; | |||||
| if (e.Value[2] == '!') | if (e.Value[2] == '!') | ||||
| { | { | ||||
| var guildUser = user as IGuildUser; | |||||
| if (guildUser != null && guildUser.Nickname != null) | if (guildUser != null && guildUser.Nickname != null) | ||||
| return '@' + guildUser.Nickname; | |||||
| name = guildUser.Nickname; | |||||
| } | |||||
| switch (mode) | |||||
| { | |||||
| case UserResolveMode.NameOnly: | |||||
| default: | |||||
| return $"@{name}"; | |||||
| case UserResolveMode.NameAndDiscriminator: | |||||
| return $"@{name}#{user.Discriminator}"; | |||||
| } | } | ||||
| return '@' + user.Username; | |||||
| } | } | ||||
| } | } | ||||
| return e.Value; | return e.Value; | ||||
| })); | })); | ||||
| } | } | ||||
| internal static string CleanChannelMentions(string text, IGuild guild, ImmutableArray<ulong>.Builder mentions = null) | |||||
| internal static string ResolveChannelMentions(string text, IGuild guild) | |||||
| { | { | ||||
| return _channelRegex.Replace(text, new MatchEvaluator(e => | return _channelRegex.Replace(text, new MatchEvaluator(e => | ||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | ||||
| { | { | ||||
| var channel = guild.GetChannelAsync(id).GetAwaiter().GetResult() as IGuildChannel; | |||||
| IGuildChannel channel = null; | |||||
| channel = guild.GetChannelAsync(id).GetAwaiter().GetResult(); | |||||
| if (channel != null) | if (channel != null) | ||||
| { | |||||
| if (mentions != null) | |||||
| mentions.Add(channel.Id); | |||||
| return '#' + channel.Name; | return '#' + channel.Name; | ||||
| } | |||||
| } | } | ||||
| return e.Value; | return e.Value; | ||||
| })); | })); | ||||
| } | } | ||||
| internal static string CleanRoleMentions(string text, IGuild guild, ImmutableArray<IRole>.Builder mentions = null) | |||||
| internal static string ResolveRoleMentions(string text, IGuild guild, IReadOnlyCollection<IRole> mentions) | |||||
| { | { | ||||
| return _roleRegex.Replace(text, new MatchEvaluator(e => | return _roleRegex.Replace(text, new MatchEvaluator(e => | ||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | ||||
| { | { | ||||
| var role = guild.GetRole(id); | |||||
| if (role != null) | |||||
| IRole role = null; | |||||
| foreach (var mention in mentions) | |||||
| { | { | ||||
| if (mentions != null) | |||||
| mentions.Add(role); | |||||
| return '@' + role.Name; | |||||
| if (mention.Id == id) | |||||
| { | |||||
| role = mention; | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| if (role != null) | |||||
| return '@' + role.Name; | |||||
| } | } | ||||
| return e.Value; | return e.Value; | ||||
| })); | })); | ||||