Browse Source

fix: #1314 Don't parse tags within code blocks (#1318)

* implement a fix for tags being found in code blocks

still needs polish, consider this a rough draft

* refactor to reuse a local function

uses CheckWrappedInCode to check that there are no code blocks that surround the tag being parsed

* Add more test coverage of MessageHelper.ParseTags

* reset indexes for @ here mention

* add a test case to catch error fixed from prev commit

* wip commit of most test cases working

* fix the Enclosed in block util method

* code cleanup

* lint whitespace

* lint brackets for single line if blocks

* move messagehelpertests to the new unit test dir

* expose internals to the unit test project

this seems to have been breaking the build, since CI would build the merged branch, where rest wasn't exposed to the unit tests
tags/2.2.0
Chris Johnston Christopher F 5 years ago
parent
commit
c977f2ec9c
7 changed files with 177 additions and 7 deletions
  1. +3
    -2
      src/Discord.Net.Commands/AssemblyInfo.cs
  2. +1
    -0
      src/Discord.Net.Core/AssemblyInfo.cs
  3. +1
    -0
      src/Discord.Net.Rest/AssemblyInfo.cs
  4. +48
    -1
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  5. +3
    -2
      src/Discord.Net.WebSocket/AssemblyInfo.cs
  6. +3
    -2
      src/Discord.Net.Webhook/AssemblyInfo.cs
  7. +118
    -0
      test/Discord.Net.Tests.Unit/MessageHelperTests.cs

+ 3
- 2
src/Discord.Net.Commands/AssemblyInfo.cs View File

@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

+ 1
- 0
src/Discord.Net.Core/AssemblyInfo.cs View File

@@ -6,4 +6,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

+ 1
- 0
src/Discord.Net.Rest/AssemblyInfo.cs View File

@@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

[assembly: TypeForwardedTo(typeof(Discord.Embed))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))]


+ 48
- 1
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Model = Discord.API.Message;

@@ -108,14 +109,56 @@ namespace Discord.Rest
public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions)
{
var tags = ImmutableArray.CreateBuilder<ITag>();
int index = 0;
var codeIndex = 0;

var inlineRegex = new Regex(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);
var blockRegex = new Regex(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);

// checks if the tag being parsed is wrapped in code blocks
bool CheckWrappedCode()
{
// util to check if the index of a tag is within the bounds of the codeblock
bool EnclosedInBlock(Match m)
=> m.Groups[1].Index < index && index < m.Groups[2].Index;

// loop through all code blocks that are before the start of the tag
while (codeIndex < index)
{
var blockMatch = blockRegex.Match(text, codeIndex);
if (blockMatch.Success)
{
if (EnclosedInBlock(blockMatch))
return true;
// continue if the end of the current code was before the start of the tag
codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length;
if (codeIndex < index)
continue;
return false;
}
var inlineMatch = inlineRegex.Match(text, codeIndex);
if (inlineMatch.Success)
{
if (EnclosedInBlock(inlineMatch))
return true;
// continue if the end of the current code was before the start of the tag
codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length;
if (codeIndex < index)
continue;
return false;
}
return false;
}
return false;
}

while (true)
{
index = text.IndexOf('<', index);
if (index == -1) break;
int endIndex = text.IndexOf('>', index + 1);
if (endIndex == -1) break;
if (CheckWrappedCode()) break;
string content = text.Substring(index, endIndex - index + 1);

if (MentionUtils.TryParseUser(content, out ulong id))
@@ -158,10 +201,12 @@ namespace Discord.Rest
}

index = 0;
codeIndex = 0;
while (true)
{
index = text.IndexOf("@everyone", index);
if (index == -1) break;
if (CheckWrappedCode()) break;
var tagIndex = FindIndex(tags, index);
if (tagIndex.HasValue)
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole));
@@ -169,10 +214,12 @@ namespace Discord.Rest
}

index = 0;
codeIndex = 0;
while (true)
{
index = text.IndexOf("@here", index);
if (index == -1) break;
if (CheckWrappedCode()) break;
var tagIndex = FindIndex(tags, index);
if (tagIndex.HasValue)
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole));


+ 3
- 2
src/Discord.Net.WebSocket/AssemblyInfo.cs View File

@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

+ 3
- 2
src/Discord.Net.Webhook/AssemblyInfo.cs View File

@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

+ 118
- 0
test/Discord.Net.Tests.Unit/MessageHelperTests.cs View File

@@ -0,0 +1,118 @@
using Xunit;
using Discord.Rest;

namespace Discord
{
/// <summary>
/// Tests for <see cref="MessageHelper"/> parsing.
/// </summary>
public class MessageHelperTests
{
/// <summary>
/// Tests that no tags are parsed while in code blocks
/// or inline code.
/// </summary>
[Theory]
[InlineData("`@everyone`")]
[InlineData("`<@163184946742034432>`")]
[InlineData("```@everyone```")]
[InlineData("```cs \n @everyone```")]
[InlineData("```cs <@163184946742034432> ```")]
[InlineData("``` test ``` ```cs <@163184946742034432> ```")]
[InlineData("`<:test:537920404019216384>`")]
[InlineData("``` @everyone `")] // discord client handles these weirdly
[InlineData("``` @everyone ``")]
[InlineData("` @here `")]
[InlineData("` @everyone @here <@163184946742034432> <@&163184946742034432> <#163184946742034432> <:test:537920404019216384> `")]
public void ParseTagsInCode(string testData)
{
// don't care that I'm passing in null channels/guilds/users
// as they shouldn't be required
var result = MessageHelper.ParseTags(testData, null, null, null);
Assert.Empty(result);
}

/// <summary> Tests parsing tags that surround inline code or a code block. </summary>
[Theory]
[InlineData("`` <@&163184946742034432>")]
[InlineData("``` code block 1 ``` ``` code block 2 ``` <@&163184946742034432>")]
[InlineData("` code block 1 ``` ` code block 2 ``` <@&163184946742034432>")]
[InlineData("<@&163184946742034432> ``` code block 1 ```")]
[InlineData("``` code ``` ``` code ``` @here ``` code ``` ``` more ```")]
[InlineData("``` code ``` @here ``` more ```")]
public void ParseTagsAroundCode(string testData)
{
// don't care that I'm passing in null channels/guilds/users
// as they shouldn't be required
var result = MessageHelper.ParseTags(testData, null, null, null);
Assert.NotEmpty(result);
}

[Theory]
[InlineData(@"\` @everyone \`")]
[InlineData(@"\`\`\` @everyone \`\`\`")]
[InlineData(@"hey\`\`\`@everyone\`\`\`!!")]
public void IgnoreEscapedCodeBlocks(string testData)
{
var result = MessageHelper.ParseTags(testData, null, null, null);
Assert.NotEmpty(result);
}

// cannot test parsing a user, as it uses the ReadOnlyCollection<IUser> arg.
// this could be done if mocked entities are merged in PR #1290

/// <summary> Tests parsing a mention of a role. </summary>
[Theory]
[InlineData("<@&163184946742034432>")]
[InlineData("**<@&163184946742034432>**")]
[InlineData("__<@&163184946742034432>__")]
[InlineData("<><@&163184946742034432>")]
public void ParseRole(string roleTag)
{
var result = MessageHelper.ParseTags(roleTag, null, null, null);
Assert.Contains(result, x => x.Type == TagType.RoleMention);
}

/// <summary> Tests parsing a channel. </summary>
[Theory]
[InlineData("<#429115823748284417>")]
[InlineData("**<#429115823748284417>**")]
[InlineData("<><#429115823748284417>")]
public void ParseChannel(string channelTag)
{
var result = MessageHelper.ParseTags(channelTag, null, null, null);
Assert.Contains(result, x => x.Type == TagType.ChannelMention);
}

/// <summary> Tests parsing an emoji. </summary>
[Theory]
[InlineData("<:test:537920404019216384>")]
[InlineData("**<:test:537920404019216384>**")]
[InlineData("<><:test:537920404019216384>")]
public void ParseEmoji(string emoji)
{
var result = MessageHelper.ParseTags(emoji, null, null, null);
Assert.Contains(result, x => x.Type == TagType.Emoji);
}

/// <summary> Tests parsing a mention of @everyone. </summary>
[Theory]
[InlineData("@everyone")]
[InlineData("**@everyone**")]
public void ParseEveryone(string everyone)
{
var result = MessageHelper.ParseTags(everyone, null, null, null);
Assert.Contains(result, x => x.Type == TagType.EveryoneMention);
}

/// <summary> Tests parsing a mention of @here. </summary>
[Theory]
[InlineData("@here")]
[InlineData("**@here**")]
public void ParseHere(string here)
{
var result = MessageHelper.ParseTags(here, null, null, null);
Assert.Contains(result, x => x.Type == TagType.HereMention);
}
}
}

Loading…
Cancel
Save