Skip to content

Commit 4a0f0cd

Browse files
author
davidwei
committed
Add RemoveNoteLinks method to sanitize note links before Mastodon sync
1 parent acfdbee commit 4a0f0cd

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

src/HappyNotes.Common/StringExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static partial class StringExtensions
1111
private static readonly Regex NoteId = _NoteId();
1212
private static readonly Regex ImagePattern = _ImagePattern();
1313
private static readonly Regex ImagesSuffixPattern = _ImagesSuffixPattern();
14+
private static readonly Regex LeadingNoteLinks = _LeadingNoteLinks();
15+
private static readonly Regex TrailingNoteLinks = _TrailingNoteLinks();
1416
private static readonly Converter MarkdownConverter = new();
1517

1618
/// <summary>
@@ -31,6 +33,12 @@ public static partial class StringExtensions
3133
[GeneratedRegex(@"(?<=(?:^|[^\\]))@[1-9][0-9]{0,31}(?=[^\d]|$)", RegexOptions.Singleline)]
3234
private static partial Regex _NoteId();
3335

36+
[GeneratedRegex(@"^(?:@[1-9][0-9]{0,31}\s*)+", RegexOptions.Singleline)]
37+
private static partial Regex _LeadingNoteLinks();
38+
39+
[GeneratedRegex(@"\s*(?:@[1-9][0-9]{0,31}\s*)+$", RegexOptions.Singleline)]
40+
private static partial Regex _TrailingNoteLinks();
41+
3442
public static bool IsLong(this string? str)
3543
{
3644
var content = str?.Trim() ?? string.Empty;
@@ -132,6 +140,19 @@ public static string RemoveImageReference(this string? markdownInput)
132140
return markdownInput;
133141
}
134142

143+
public static string RemoveNoteLinks(this string? text)
144+
{
145+
if (text is null) return string.Empty;
146+
147+
// Remove leading note links (@123 @456 at the start)
148+
text = LeadingNoteLinks.Replace(text, string.Empty);
149+
150+
// Remove trailing note links (@123 @456 at the end)
151+
text = TrailingNoteLinks.Replace(text, string.Empty);
152+
153+
return text.Trim();
154+
}
155+
135156
[GeneratedRegex(@"!\[(.*?)\]\((.*?)\)", RegexOptions.Compiled)]
136157
private static partial Regex _ImagePattern();
137158

src/HappyNotes.Services/MastodonTootService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public async Task<Status> SendTootAsync(string instanceUrl, string accessToken,
2424

2525
try
2626
{
27+
// Remove note links (@123) from beginning and end before sending to Mastodon
28+
text = text.RemoveNoteLinks();
2729
var fullText = _GetFullText(text, isMarkdown);
2830
if (fullText.Length > Constants.MastodonTootLength)
2931
{
@@ -66,6 +68,7 @@ private async Task<Status> _SendLongTootAsPhotoAsync(string instanceUrl, string
6668
logger.LogDebug("Converting long text to image for {InstanceUrl}, textLength: {TextLength}",
6769
instanceUrl, longText.Length);
6870

71+
// Note: longText has already been sanitized by RemoveNoteLinks() in the caller
6972
var client = new MastodonClient(instanceUrl, accessToken);
7073
var filePath = Path.GetTempFileName();
7174

@@ -113,6 +116,8 @@ public async Task<Status> EditTootAsync(
113116

114117
try
115118
{
119+
// Remove note links (@123) from beginning and end before editing on Mastodon
120+
newText = newText.RemoveNoteLinks();
116121
var fullText = _GetFullText(newText, isMarkdown);
117122
if (fullText.Length > Constants.MastodonTootLength)
118123
{
@@ -226,6 +231,7 @@ private async Task<Status> _EditLongTootAsPhotoAsync(
226231
logger.LogDebug("Editing long toot as photo for {InstanceUrl}, tootId: {TootId}, textLength: {TextLength}",
227232
instanceUrl, tootId, longText.Length);
228233

234+
// Note: longText has already been sanitized by RemoveNoteLinks() in the caller
229235
try
230236
{
231237
var client = new MastodonClient(instanceUrl, accessToken);

tests/HappyNotes.Common.Tests/StringExtensionsTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,28 @@ public void IsHtmlTest(string input, bool expected)
133133
Assert.That(result, Is.EqualTo(expected));
134134
}
135135

136+
[TestCase("@123 This is content", "This is content")]
137+
[TestCase("@123 @456 This is content", "This is content")]
138+
[TestCase("This is content @123", "This is content")]
139+
[TestCase("This is content @123 @456", "This is content")]
140+
[TestCase("@123 This is content @456", "This is content")]
141+
[TestCase("@123 @456 This is content @789", "This is content")]
142+
[TestCase("This is content", "This is content")]
143+
[TestCase("@123", "")]
144+
[TestCase("@123 @456 @789", "")]
145+
[TestCase(null, "")]
146+
[TestCase("", "")]
147+
[TestCase("This @123 is content", "This @123 is content")]
148+
[TestCase(@"\@123 This is content", @"\@123 This is content")]
149+
[TestCase("@123\nThis is content", "This is content")]
150+
[TestCase("This is content\n@123", "This is content")]
151+
public void RemoveNoteLinksTest(string input, string expected)
152+
{
153+
// Act
154+
var result = input.RemoveNoteLinks();
155+
156+
// Assert
157+
Assert.That(result, Is.EqualTo(expected));
158+
}
159+
136160
}

tests/HappyNotes.Services.Tests/MastodonSyncNoteServiceTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,49 @@ public async Task SyncNewNote_ShouldSyncNoteToMastodon(string fullContent, bool
9595
_mockSyncQueueService.Verify(s => s.EnqueueAsync(It.IsAny<string>(), It.IsAny<SyncTask<object>>()), Times.Never);
9696
}
9797
}
98+
99+
[TestCase("@123 This is content", "This is content")]
100+
[TestCase("@123 @456 This is content", "This is content")]
101+
[TestCase("This is content @123", "This is content")]
102+
[TestCase("@123 This is content @456", "This is content")]
103+
[TestCase("This @123 is content", "This @123 is content")]
104+
public async Task SyncNewNote_WithNoteLinks_ShouldEnqueueWithOriginalContent(string fullContent, string expectedCleanContent)
105+
{
106+
// Arrange
107+
var note = new Note
108+
{
109+
Id = 100,
110+
UserId = 1,
111+
IsPrivate = false,
112+
TagList = []
113+
};
114+
115+
var mastodonUserAccounts = new List<MastodonUserAccount>
116+
{
117+
new()
118+
{
119+
Id = 1,
120+
UserId = 1,
121+
AccessToken = TextEncryptionHelper.Encrypt("test token", "test_key"),
122+
InstanceUrl = "https://mastodon.instance",
123+
SyncType = MastodonSyncType.All
124+
}
125+
};
126+
127+
_mockMastodonUserAccountCacheService
128+
.Setup(s => s.GetAsync(note.UserId))
129+
.ReturnsAsync(mastodonUserAccounts);
130+
131+
// Act
132+
await _mastodonSyncNoteService.SyncNewNote(note, fullContent);
133+
134+
// Assert - The original content should be enqueued (cleaning happens in MastodonTootService)
135+
_mockSyncQueueService.Verify(s => s.EnqueueAsync("mastodon",
136+
It.Is<SyncTask<MastodonSyncPayload>>(task =>
137+
task.Action == "CREATE" &&
138+
task.EntityId == note.Id &&
139+
task.UserId == note.UserId &&
140+
((MastodonSyncPayload)task.Payload).FullContent == fullContent)),
141+
Times.Once);
142+
}
98143
}

0 commit comments

Comments
 (0)