From 9865f3dde71f04ac14d1fff2be6938d1fcbec9c1 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 15 Dec 2024 00:11:31 +0100 Subject: [PATCH] [backend/core] Replace MFM line endings during user/note ingestion This saves significant amounts of time & memory during parsing. --- .../Controllers/Mastodon/StatusController.cs | 8 +- .../ActivityPub/MentionsResolver.cs | 2 +- .../Helpers/LibMfm/Conversion/MfmConverter.cs | 17 +- .../Core/Services/EmojiService.cs | 4 +- .../Core/Services/NoteService.cs | 112 ++++++------- .../Services/UserProfileMentionsResolver.cs | 6 +- .../Core/Services/UserService.cs | 149 +++++++++--------- Iceshrimp.Backend/Iceshrimp.Backend.csproj | 2 +- Iceshrimp.Frontend/Iceshrimp.Frontend.csproj | 2 +- 9 files changed, 154 insertions(+), 148 deletions(-) diff --git a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs index bc3d9989..babcbf53 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs @@ -400,17 +400,15 @@ public class StatusController( if (token.AutoDetectQuotes && request.Text != null) { - var parsed = MfmParser.Parse(request.Text).AsEnumerable(); - quoteUri = MfmParser.Parse(request.Text).LastOrDefault() switch + var parsed = MfmParser.Parse(request.Text); + quoteUri = parsed.LastOrDefault() switch { MfmUrlNode urlNode => urlNode.Url, MfmLinkNode linkNode => linkNode.Url, _ => quoteUri }; - if (quoteUri != null) - parsed = parsed.SkipLast(1); - newText = parsed.Serialize(); + newText = quoteUri != null ? parsed.SkipLast(1).Serialize() : parsed.Serialize(); } if (request is { Sensitive: true, MediaIds.Count: > 0 }) diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs index 4232fd01..86077798 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs @@ -21,7 +21,7 @@ public class MentionsResolver(IOptions config) : ISingle SplitDomainMapping splitDomainMapping ) { - var nodes = MfmParser.Parse(mfm); + var nodes = MfmParser.Parse(mfm.ReplaceLineEndings("\n")); ResolveMentions(nodes.AsSpan(), host, mentionCache, splitDomainMapping); return nodes.Serialize(); } diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs index 3e3736b4..ca19e875 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs @@ -59,6 +59,9 @@ public class MfmConverter( // Ensure compatibility with AP servers that send non-breaking space characters instead of regular spaces html = html.Replace("\u00A0", " "); + // Ensure compatibility with AP servers that send CRLF or CR instead of LF-style newlines + html = html.ReplaceLineEndings("\n"); + var dom = await new HtmlParser().ParseDocumentAsync(html); if (dom.Body == null) return new HtmlMfmData("", media); @@ -87,7 +90,7 @@ public class MfmConverter( } public async Task ToHtmlAsync( - IEnumerable nodes, List mentions, string? host, string? quoteUri = null, + IMfmNode[] nodes, List mentions, string? host, string? quoteUri = null, bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", List? emoji = null, List? media = null ) @@ -95,8 +98,7 @@ public class MfmConverter( var context = BrowsingContext.New(); var document = await context.OpenNewAsync(); var element = document.CreateElement(rootElement); - var nodeList = nodes.ToList(); - var hasContent = nodeList.Count > 0; + var hasContent = nodes.Length > 0; if (replyInaccessible) { @@ -115,7 +117,8 @@ public class MfmConverter( } var usedMedia = new List(); - foreach (var node in nodeList) element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); + foreach (var node in nodes) + element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); if (quoteUri != null) { @@ -170,7 +173,8 @@ public class MfmConverter( } private INode FromMfmNode( - IDocument document, IMfmNode node, List mentions, string? host, List usedMedia, + IDocument document, IMfmNode node, List mentions, string? host, + List usedMedia, List? emoji = null, List? media = null ) { @@ -413,7 +417,8 @@ public class MfmConverter( List? emoji = null, List? media = null ) { - foreach (var node in parent.Children) element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); + foreach (var node in parent.Children) + element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); } private IElement CreateInlineFormattingElement(IDocument document, string name) diff --git a/Iceshrimp.Backend/Core/Services/EmojiService.cs b/Iceshrimp.Backend/Core/Services/EmojiService.cs index 9bf927e6..e81060e2 100644 --- a/Iceshrimp.Backend/Core/Services/EmojiService.cs +++ b/Iceshrimp.Backend/Core/Services/EmojiService.cs @@ -187,7 +187,7 @@ public partial class EmojiService( return await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == name); } - public async Task> ResolveEmojiAsync(IEnumerable nodes) + public async Task> ResolveEmojiAsync(IMfmNode[] nodes) { var list = new List(); ResolveChildren(nodes, ref list); @@ -195,7 +195,7 @@ public partial class EmojiService( } private static void ResolveChildren( - IEnumerable nodes, ref List list + Span nodes, ref List list ) { foreach (var node in nodes) diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 6ff49ebd..05f4f8f4 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -58,6 +58,7 @@ public class NoteService( public required User User; public required Note.NoteVisibility Visibility; public string? Text; + public IMfmNode[]? ParsedText; public string? Cw; public Note? Reply; public Note? Renote; @@ -78,6 +79,7 @@ public class NoteService( { public required Note Note; public string? Text; + public IMfmNode[]? ParsedText; public string? Cw; public IReadOnlyCollection? Attachments; public Poll? Poll; @@ -126,6 +128,7 @@ public class NoteService( if (data.Poll is { Choices.Count: < 2 }) throw GracefulException.UnprocessableEntity("Polls must have at least two options"); + data.ParsedText = data.Text != null ? MfmParser.Parse(data.Text.ReplaceLineEndings("\n")) : null; policySvc.CallRewriteHooks(data, IRewritePolicy.HookLocationEnum.PreLogic); if (!data.LocalOnly && (data.Renote is { LocalOnly: true } || data.Reply is { LocalOnly: true })) @@ -154,7 +157,7 @@ public class NoteService( } var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) = - data.ResolvedMentions ?? await ResolveNoteMentionsAsync(data.Text); + data.ResolvedMentions ?? await ResolveNoteMentionsAsync(data.ParsedText); // ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall if (mentionedUserIds.Count > 0) @@ -167,18 +170,17 @@ public class NoteService( throw GracefulException.Forbidden($"You're not allowed to interact with @{blockAcct}"); } - IMfmNode[]? nodes = null; if (data.Text != null && string.IsNullOrWhiteSpace(data.Text)) { - data.Text = null; + data.Text = null; + data.ParsedText = null; } else if (data.Text != null) { - nodes = MfmParser.Parse(data.Text); - mentionsResolver.ResolveMentions(nodes.AsSpan(), data.User.Host, mentions, splitDomainMapping); - data.Text = nodes.Serialize(); + mentionsResolver.ResolveMentions(data.ParsedText, data.User.Host, mentions, splitDomainMapping); } + data.Cw = data.Cw?.Trim(); if (data.Cw != null && string.IsNullOrWhiteSpace(data.Cw)) data.Cw = null; @@ -196,7 +198,7 @@ public class NoteService( .ExecuteUpdateAsync(p => p.SetProperty(i => i.IsSensitive, _ => true)); } - var tags = ResolveHashtags(data.Text, data.ASNote); + var tags = ResolveHashtags(data.ParsedText, data.ASNote); if (tags.Count > 0 && data.Text != null && data.ASNote != null) { // @formatter:off @@ -211,9 +213,9 @@ public class NoteService( ? data.Reply?.UserId : data.Reply.MastoReplyUserId ?? data.Reply.ReplyUserId ?? data.Reply.UserId; - if (data.Emoji == null && data.User.IsLocalUser && nodes != null) + if (data.Emoji == null && data.User.IsLocalUser && data.ParsedText != null) { - data.Emoji = (await emojiSvc.ResolveEmojiAsync(nodes)).Select(p => p.Id).ToList(); + data.Emoji = (await emojiSvc.ResolveEmojiAsync(data.ParsedText)).Select(p => p.Id).ToList(); } List visibleUserIds = []; @@ -299,8 +301,8 @@ public class NoteService( Id = noteId, Uri = data.Uri, Url = data.Url, - Text = data.Text?.Trim(), - Cw = data.Cw?.Trim(), + Text = data.ParsedText?.Serialize(), + Cw = data.Cw, Reply = data.Reply, ReplyUserId = data.Reply?.UserId, MastoReplyUserId = mastoReplyUserId, @@ -485,7 +487,7 @@ public class NoteService( } } - private List GetInlineMediaUrls(IEnumerable mfm) + private List GetInlineMediaUrls(Span mfm) { List urls = []; @@ -503,7 +505,6 @@ public class NoteService( return urls; } - public async Task UpdateNoteAsync(NoteUpdateData data) { logger.LogDebug("Processing note update for note {id}", data.Note.Id); @@ -529,6 +530,7 @@ public class NoteService( FileIds = note.FileIds }; + data.ParsedText = data.Text != null ? MfmParser.Parse(data.Text.ReplaceLineEndings("\n")) : null; policySvc.CallRewriteHooks(data, IRewritePolicy.HookLocationEnum.PreLogic); var previousMentionedLocalUserIds = await db.Users.Where(p => note.Mentions.Contains(p.Id) && p.IsLocalUser) @@ -540,20 +542,19 @@ public class NoteService( .ToListAsync(); var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) = - data.ResolvedMentions ?? await ResolveNoteMentionsAsync(data.Text); + data.ResolvedMentions ?? await ResolveNoteMentionsAsync(data.ParsedText); - IMfmNode[]? nodes = null; if (data.Text != null && string.IsNullOrWhiteSpace(data.Text)) { - data.Text = null; + data.Text = null; + data.ParsedText = null; } else if (data.Text != null) { - nodes = MfmParser.Parse(data.Text); - mentionsResolver.ResolveMentions(nodes.AsSpan(), note.User.Host, mentions, splitDomainMapping); - data.Text = nodes.Serialize(); + mentionsResolver.ResolveMentions(data.ParsedText, note.User.Host, mentions, splitDomainMapping); } + data.Cw = data.Cw?.Trim(); if (data.Cw != null && string.IsNullOrWhiteSpace(data.Cw)) data.Cw = null; @@ -571,14 +572,10 @@ public class NoteService( // ReSharper restore EntityFramework.UnsupportedServerSideFunctionCall mentionedLocalUserIds = mentionedLocalUserIds.Except(previousMentionedLocalUserIds).ToList(); - note.Text = data.Text?.Trim(); - note.Cw = data.Cw?.Trim(); - note.Tags = ResolveHashtags(data.Text, data.ASNote); + note.Tags = ResolveHashtags(data.ParsedText, data.ASNote); - if (note.User.IsLocalUser && nodes != null) - { - data.Emoji = (await emojiSvc.ResolveEmojiAsync(nodes)).Select(p => p.Id).ToList(); - } + if (note.User.IsLocalUser && data.ParsedText != null) + data.Emoji = (await emojiSvc.ResolveEmojiAsync(data.ParsedText)).Select(p => p.Id).ToList(); if (data.Emoji != null && !note.Emojis.IsEquivalent(data.Emoji)) note.Emojis = data.Emoji; @@ -621,33 +618,34 @@ public class NoteService( note.VisibleUserIds.AddRange(missing); } - note.Text = mentionsResolver.ResolveMentions(data.Text, note.UserHost, mentions, splitDomainMapping); + mentionsResolver.ResolveMentions(data.ParsedText, note.UserHost, mentions, splitDomainMapping); } var attachments = data.Attachments?.ToList() ?? []; - var inlineMediaUrls = nodes != null ? GetInlineMediaUrls(nodes) : []; + var inlineMediaUrls = data.ParsedText != null ? GetInlineMediaUrls(data.ParsedText) : []; var newMediaUrls = data.Attachments?.Select(p => p.Url) ?? []; var missingUrls = inlineMediaUrls.Except(newMediaUrls).ToArray(); if (missingUrls.Length > 0) { var missingAttachments = await db.DriveFiles - .Where(p => missingUrls.Contains(p.PublicUrl ?? p.Url) && p.UserId == note.UserId) + .Where(p => missingUrls.Contains(p.PublicUrl ?? p.Url) + && p.UserId == note.UserId) .ToArrayAsync(); attachments.AddRange(missingAttachments); } //TODO: handle updated alt text et al - var fileIds = attachments?.Select(p => p.Id).ToList() ?? []; + var fileIds = attachments.Select(p => p.Id).ToList(); if (!note.FileIds.IsEquivalent(fileIds)) { note.FileIds = fileIds; - note.AttachedFileTypes = attachments?.Select(p => p.Type).ToList() ?? []; + note.AttachedFileTypes = attachments.Select(p => p.Type).ToList(); - var combinedAltText = attachments?.Select(p => p.Comment).Where(c => c != null); - note.CombinedAltText = combinedAltText != null ? string.Join(' ', combinedAltText) : null; + var combinedAltText = attachments.Select(p => p.Comment).Where(c => c != null); + note.CombinedAltText = string.Join(' ', combinedAltText); } var isPollEdited = false; @@ -722,6 +720,8 @@ public class NoteService( } } + note.Text = data.Text = data.ParsedText?.Serialize(); + var isEdit = data.ASNote is not ASQuestion || poll == null || isPollEdited @@ -985,9 +985,9 @@ public class NoteService( (text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions); } - var cw = note.Summary; - var url = note.Url?.Link; - var uri = note.Id; + var cw = note.Summary; + var url = note.Url?.Link; + var uri = note.Id; if (note is ASQuestion question) { @@ -1074,7 +1074,7 @@ public class NoteService( if (text == null) (text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions); - var cw = note.Summary; + var cw = note.Summary; Poll? poll = null; @@ -1134,15 +1134,16 @@ public class NoteService( return ResolveNoteMentions(users.NotNull().ToList()); } - private async Task ResolveNoteMentionsAsync(string? text) + private async Task ResolveNoteMentionsAsync(IMfmNode[]? parsedText) { - if (text == null) + if (parsedText == null) return ResolveNoteMentions([]); - var mentions = MfmParser.Parse(text) - .SelectMany(p => p.Children.Append(p)) - .OfType() - .DistinctBy(p => p.Acct) - .ToArray(); + + var mentions = parsedText + .SelectMany(p => p.Children.Append(p)) + .OfType() + .DistinctBy(p => p.Acct) + .ToArray(); if (mentions.Length > 100) throw GracefulException.UnprocessableEntity("Refusing to process note with more than 100 mentions"); @@ -1153,19 +1154,19 @@ public class NoteService( return ResolveNoteMentions(users.NotNull().ToList()); } - private List ResolveHashtags(string? text, ASNote? note = null) + private List ResolveHashtags(IMfmNode[]? parsedText, ASNote? note = null) { List tags = []; - if (text != null) + if (parsedText != null) { - tags = MfmParser.Parse(text) - .SelectMany(p => p.Children.Append(p)) - .OfType() - .Select(p => p.Hashtag.ToLowerInvariant()) - .Select(p => p.Trim('#')) - .Distinct() - .ToList(); + tags = parsedText + .SelectMany(p => p.Children.Append(p)) + .OfType() + .Select(p => p.Hashtag.ToLowerInvariant()) + .Select(p => p.Trim('#')) + .Distinct() + .ToList(); } var extracted = note?.Tags?.OfType() @@ -1174,7 +1175,7 @@ public class NoteService( .Cast() .Select(p => p.Trim('#')) .Distinct() - .ToList(); + .ToArray(); if (extracted != null) tags.AddRange(extracted); @@ -1248,8 +1249,7 @@ public class NoteService( .DistinctBy(p => p.Src) .Select(p => new ASDocument { - Url = new ASLink(p.Src), - Description = p.Alt, + Url = new ASLink(p.Src), Description = p.Alt, })); } diff --git a/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.cs b/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.cs index 7ad012fe..12855c2a 100644 --- a/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.cs +++ b/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.cs @@ -37,7 +37,7 @@ public class UserProfileMentionsResolver( if (actor.MkSummary != null) { - var nodes = MfmParser.Parse(actor.MkSummary); + var nodes = MfmParser.Parse(actor.MkSummary.ReplaceLineEndings("\n")); mentionNodes = EnumerateMentions(nodes); } @@ -84,7 +84,7 @@ public class UserProfileMentionsResolver( .Cast() .ToList(); - var nodes = input.SelectMany(p => MfmParser.Parse(p)); + var nodes = input.SelectMany(p => MfmParser.Parse(p.ReplaceLineEndings("\n"))).ToArray(); var mentionNodes = EnumerateMentions(nodes); var users = await mentionNodes .DistinctBy(p => p.Acct) @@ -106,7 +106,7 @@ public class UserProfileMentionsResolver( [SuppressMessage("ReSharper", "ReturnTypeCanBeEnumerable.Local", Justification = "Roslyn inspection says this hurts performance")] - private static List EnumerateMentions(IEnumerable nodes) + private static List EnumerateMentions(Span nodes) { var list = new List(); diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 063564cc..55bed91b 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -101,8 +101,8 @@ public class UserService( return await db.Users .IncludeCommonProperties() - .FirstOrDefaultAsync(p => p.UsernameLower == tuple.Username.ToLowerInvariant() && - p.Host == tuple.Host); + .FirstOrDefaultAsync(p => p.UsernameLower == tuple.Username.ToLowerInvariant() + && p.Host == tuple.Host); } public async Task CreateUserAsync(string uri, string acct) @@ -131,8 +131,8 @@ public class UserService( actor.NormalizeAndValidate(uri); - user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == actor.Username!.ToLowerInvariant() && - p.Host == host); + user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == actor.Username!.ToLowerInvariant() + && p.Host == host); if (user is not null) throw GracefulException .UnprocessableEntity($"A user with acct @{user.UsernameLower}@{user.Host} already exists: {user.Uri}"); @@ -155,16 +155,16 @@ public class UserService( .AwaitAllAsync() : null; - var bio = actor.MkSummary ?? (await MfmConverter.FromHtmlAsync(actor.Summary)).Mfm; - - var tags = ResolveHashtags(bio, actor); + var bio = actor.MkSummary?.ReplaceLineEndings("\n").Trim() + ?? (await MfmConverter.FromHtmlAsync(actor.Summary)).Mfm; + var tags = ResolveHashtags(MfmParser.Parse(bio), actor); user = new User { Id = IdHelpers.GenerateSnowflakeId(), CreatedAt = DateTime.UtcNow, LastFetchedAt = followupTaskSvc.IsBackgroundWorker.Value ? null : DateTime.UtcNow, - DisplayName = actor.DisplayName, + DisplayName = actor.DisplayName?.ReplaceLineEndings("\n").Trim(), IsLocked = actor.IsLocked ?? false, IsBot = actor.IsBot, Username = actor.Username!, @@ -253,8 +253,8 @@ public class UserService( public async Task UpdateUserAsync(string id) { - var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ?? - throw new Exception("Cannot update nonexistent user"); + var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) + ?? throw new Exception("Cannot update nonexistent user"); return await UpdateUserAsync(user, force: true); } @@ -283,7 +283,7 @@ public class UserService( user.LastFetchedAt = DateTime.UtcNow; // If we don't do this we'll overwrite the value with the previous one user.Inbox = actor.Inbox?.Link; user.SharedInbox = actor.SharedInbox?.Link ?? actor.Endpoints?.SharedInbox?.Id; - user.DisplayName = actor.DisplayName; + user.DisplayName = actor.DisplayName?.ReplaceLineEndings("\n").Trim(); user.IsLocked = actor.IsLocked ?? false; user.IsBot = actor.IsBot; user.MovedToUri = actor.MovedTo?.Link; @@ -294,8 +294,9 @@ public class UserService( user.Featured = actor.Featured?.Id; var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType().ToList(), - user.Host ?? - throw new Exception("User host must not be null at this stage")); + user.Host + ?? throw new + Exception("User host must not be null at this stage")); var fields = actor.Attachments != null ? await actor.Attachments @@ -316,7 +317,8 @@ public class UserService( var processPendingDeletes = await ResolveAvatarAndBannerAsync(user, actor); - user.UserProfile.Description = actor.MkSummary ?? (await MfmConverter.FromHtmlAsync(actor.Summary)).Mfm; + user.UserProfile.Description = actor.MkSummary?.ReplaceLineEndings("\n").Trim() + ?? (await MfmConverter.FromHtmlAsync(actor.Summary)).Mfm; //user.UserProfile.Birthday = TODO; //user.UserProfile.Location = TODO; user.UserProfile.Fields = fields?.ToArray() ?? []; @@ -325,7 +327,7 @@ public class UserService( user.UserProfile.MentionsResolved = false; - user.Tags = ResolveHashtags(user.UserProfile.Description, actor); + user.Tags = ResolveHashtags(MfmParser.Parse(user.UserProfile.Description), actor); user.Host = await UpdateUserHostAsync(user); db.Update(user); @@ -341,24 +343,25 @@ public class UserService( if (user.IsRemoteUser) throw new Exception("This method is only valid for local users"); if (user.UserProfile == null) throw new Exception("user.UserProfile must not be null at this stage"); - user.Tags = ResolveHashtags(user.UserProfile.Description); + user.DisplayName = user.DisplayName?.ReplaceLineEndings("\n").Trim(); + user.UserProfile.Description = user.UserProfile.Description?.ReplaceLineEndings("\n").Trim(); + var parsedName = user.DisplayName != null ? MfmParser.Parse(user.DisplayName) : null; + var parsedBio = user.UserProfile.Description != null ? MfmParser.Parse(user.UserProfile.Description) : null; + + user.Tags = parsedBio != null ? ResolveHashtags(parsedBio) : []; user.Emojis = []; - if (user.UserProfile.Description != null) - { - var nodes = MfmParser.Parse(user.UserProfile.Description); - user.Emojis.AddRange((await emojiSvc.ResolveEmojiAsync(nodes)).Select(p => p.Id).ToList()); - } + if (parsedBio != null) + user.Emojis.AddRange((await emojiSvc.ResolveEmojiAsync(parsedBio)).Select(p => p.Id).ToList()); + if (parsedName != null) + user.Emojis.AddRange((await emojiSvc.ResolveEmojiAsync(parsedName)).Select(p => p.Id).ToList()); - if (user.DisplayName != null) + if (user.UserProfile.Fields is { Length: > 0 } fields) { - var nodes = MfmParser.Parse(user.DisplayName); - user.Emojis.AddRange((await emojiSvc.ResolveEmojiAsync(nodes)).Select(p => p.Id).ToList()); - } + foreach (var field in fields) + field.Name = field.Name.ReplaceLineEndings("\n").Trim(); - if (user.UserProfile.Fields.Length != 0) - { var input = user.UserProfile.Fields.Select(p => $"{p.Name} {p.Value}"); var nodes = MfmParser.Parse(string.Join('\n', input)); user.Emojis.AddRange((await emojiSvc.ResolveEmojiAsync(nodes)).Select(p => p.Id).ToList()); @@ -392,8 +395,8 @@ public class UserService( throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server"); if (security.Value.Registrations == Enums.Registrations.Invite && invite == null) throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code"); - if (security.Value.Registrations == Enums.Registrations.Invite && - !await db.RegistrationInvites.AnyAsync(p => p.Code == invite)) + if (security.Value.Registrations == Enums.Registrations.Invite + && !await db.RegistrationInvites.AnyAsync(p => p.Code == invite)) throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid"); if (!Regex.IsMatch(username, @"^\w+$")) throw new GracefulException(HttpStatusCode.BadRequest, "Username must only contain letters and numbers"); @@ -638,9 +641,9 @@ public class UserService( // Clean up notifications await db.Notifications - .Where(p => p.Type == Notification.NotificationType.FollowRequestReceived && - p.Notifiee == request.Followee && - p.Notifier == request.Follower) + .Where(p => p.Type == Notification.NotificationType.FollowRequestReceived + && p.Notifiee == request.Followee + && p.Notifier == request.Follower) .ExecuteDeleteAsync(); } @@ -658,13 +661,13 @@ public class UserService( // Clean up notifications await db.Notifications - .Where(p => ((p.Type == Notification.NotificationType.FollowRequestReceived || - p.Type == Notification.NotificationType.Follow) && - p.Notifiee == request.Followee && - p.Notifier == request.Follower) || - (p.Type == Notification.NotificationType.FollowRequestAccepted && - p.Notifiee == request.Follower && - p.Notifier == request.Followee)) + .Where(p => ((p.Type == Notification.NotificationType.FollowRequestReceived + || p.Type == Notification.NotificationType.Follow) + && p.Notifiee == request.Followee + && p.Notifier == request.Follower) + || (p.Type == Notification.NotificationType.FollowRequestAccepted + && p.Notifiee == request.Follower + && p.Notifier == request.Followee)) .ExecuteDeleteAsync(); } @@ -734,11 +737,12 @@ public class UserService( // Otherwise, create a new request and insert it into the database else { - var autoAccept = followee.IsLocalUser && - await db.Followings.AnyAsync(p => p.Follower == followee && - p.Followee == follower && - p.Follower.UserSettings != null && - p.Follower.UserSettings.AutoAcceptFollowed); + var autoAccept = followee.IsLocalUser + && await db.Followings.AnyAsync(p => p.Follower == followee + && p.Followee == follower + && p.Follower.UserSettings != null + && p.Follower.UserSettings + .AutoAcceptFollowed); // Followee has auto accept enabled & is already following the follower user if (autoAccept) @@ -926,15 +930,15 @@ public class UserService( /// public async Task UnfollowUserAsync(User user, User followee) { - if (((followee.PrecomputedIsFollowedBy ?? false) || (followee.PrecomputedIsRequestedBy ?? false)) && - followee.IsRemoteUser) + if (((followee.PrecomputedIsFollowedBy ?? false) || (followee.PrecomputedIsRequestedBy ?? false)) + && followee.IsRemoteUser) { var relationshipId = await db.Followings.Where(p => p.Follower == user && p.Followee == followee) .Select(p => p.RelationshipId) - .FirstOrDefaultAsync() ?? - await db.FollowRequests.Where(p => p.Follower == user && p.Followee == followee) - .Select(p => p.RelationshipId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync() + ?? await db.FollowRequests.Where(p => p.Follower == user && p.Followee == followee) + .Select(p => p.RelationshipId) + .FirstOrDefaultAsync(); var activity = activityRenderer.RenderUnfollow(user, followee, relationshipId); await deliverSvc.DeliverToAsync(activity, user, followee); @@ -980,12 +984,12 @@ public class UserService( // Clean up notifications await db.Notifications - .Where(p => (p.Type == Notification.NotificationType.FollowRequestAccepted && - p.Notifiee == user && - p.Notifier == followee) || - (p.Type == Notification.NotificationType.Follow && - p.Notifiee == followee && - p.Notifier == user)) + .Where(p => (p.Type == Notification.NotificationType.FollowRequestAccepted + && p.Notifiee == user + && p.Notifier == followee) + || (p.Type == Notification.NotificationType.Follow + && p.Notifiee == followee + && p.Notifier == user)) .ExecuteDeleteAsync(); // Clean up user list memberships @@ -1103,19 +1107,19 @@ public class UserService( return user; } - private List ResolveHashtags(string? text, ASActor? actor = null) + private List ResolveHashtags(IMfmNode[]? parsedText, ASActor? actor = null) { List tags = []; - if (text != null) + if (parsedText != null) { - tags = MfmParser.Parse(text) - .SelectMany(p => p.Children.Append(p)) - .OfType() - .Select(p => p.Hashtag.ToLowerInvariant()) - .Select(p => p.Trim('#')) - .Distinct() - .ToList(); + tags = parsedText + .SelectMany(p => p.Children.Append(p)) + .OfType() + .Select(p => p.Hashtag.ToLowerInvariant()) + .Select(p => p.Trim('#')) + .Distinct() + .ToList(); } var extracted = actor?.Tags?.OfType() @@ -1220,13 +1224,12 @@ public class UserService( .SetProperty(i => i.FollowingCount, i => i.FollowingCount - cnt1)); // clean up notifications - await db.Notifications.Where(p => ((p.Notifiee == blocker && - p.Notifier == blockee) || - (p.Notifiee == blockee && - p.Notifier == blocker)) && - (p.Type == Notification.NotificationType.Follow || - p.Type == Notification.NotificationType.FollowRequestAccepted || - p.Type == Notification.NotificationType.FollowRequestReceived)) + await db.Notifications + .Where(p => ((p.Notifiee == blocker && p.Notifier == blockee) + || (p.Notifiee == blockee && p.Notifier == blocker)) + && (p.Type == Notification.NotificationType.Follow + || p.Type == Notification.NotificationType.FollowRequestAccepted + || p.Type == Notification.NotificationType.FollowRequestReceived)) .ExecuteDeleteAsync(); await db.AddAsync(blocking); @@ -1439,4 +1442,4 @@ public class UserService( user.IsSuspended = false; await db.SaveChangesAsync(); } -} \ No newline at end of file +} diff --git a/Iceshrimp.Backend/Iceshrimp.Backend.csproj b/Iceshrimp.Backend/Iceshrimp.Backend.csproj index d71bba0c..147105bf 100644 --- a/Iceshrimp.Backend/Iceshrimp.Backend.csproj +++ b/Iceshrimp.Backend/Iceshrimp.Backend.csproj @@ -52,7 +52,7 @@ - + diff --git a/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj b/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj index 8adb9a35..3c6d6501 100644 --- a/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj +++ b/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj @@ -27,7 +27,7 @@ - +