[backend/core] Replace MFM line endings during user/note ingestion

This saves significant amounts of time & memory during parsing.
This commit is contained in:
Laura Hausmann 2024-12-15 00:11:31 +01:00
parent a6b5b4c69f
commit 9865f3dde7
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
9 changed files with 154 additions and 148 deletions

View file

@ -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 })

View file

@ -21,7 +21,7 @@ public class MentionsResolver(IOptions<Config.InstanceSection> 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();
}

View file

@ -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<MfmHtmlData> ToHtmlAsync(
IEnumerable<IMfmNode> nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
List<Emoji>? emoji = null, List<MfmInlineMedia>? 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<MfmInlineMedia>();
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<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
IDocument document, IMfmNode node, List<Note.MentionedUser> mentions, string? host,
List<MfmInlineMedia> usedMedia,
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
)
{
@ -413,7 +417,8 @@ public class MfmConverter(
List<Emoji>? emoji = null, List<MfmInlineMedia>? 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)

View file

@ -187,7 +187,7 @@ public partial class EmojiService(
return await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == name);
}
public async Task<List<Emoji>> ResolveEmojiAsync(IEnumerable<IMfmNode> nodes)
public async Task<List<Emoji>> ResolveEmojiAsync(IMfmNode[] nodes)
{
var list = new List<MfmEmojiCodeNode>();
ResolveChildren(nodes, ref list);
@ -195,7 +195,7 @@ public partial class EmojiService(
}
private static void ResolveChildren(
IEnumerable<IMfmNode> nodes, ref List<MfmEmojiCodeNode> list
Span<IMfmNode> nodes, ref List<MfmEmojiCodeNode> list
)
{
foreach (var node in nodes)

View file

@ -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<DriveFile>? 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<string> 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<string> GetInlineMediaUrls(IEnumerable<IMfmNode> mfm)
private List<string> GetInlineMediaUrls(Span<IMfmNode> mfm)
{
List<string> urls = [];
@ -503,7 +505,6 @@ public class NoteService(
return urls;
}
public async Task<Note> 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<NoteMentionData> ResolveNoteMentionsAsync(string? text)
private async Task<NoteMentionData> ResolveNoteMentionsAsync(IMfmNode[]? parsedText)
{
if (text == null)
if (parsedText == null)
return ResolveNoteMentions([]);
var mentions = MfmParser.Parse(text)
.SelectMany(p => p.Children.Append(p))
.OfType<MfmMentionNode>()
.DistinctBy(p => p.Acct)
.ToArray();
var mentions = parsedText
.SelectMany(p => p.Children.Append(p))
.OfType<MfmMentionNode>()
.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<string> ResolveHashtags(string? text, ASNote? note = null)
private List<string> ResolveHashtags(IMfmNode[]? parsedText, ASNote? note = null)
{
List<string> tags = [];
if (text != null)
if (parsedText != null)
{
tags = MfmParser.Parse(text)
.SelectMany(p => p.Children.Append(p))
.OfType<MfmHashtagNode>()
.Select(p => p.Hashtag.ToLowerInvariant())
.Select(p => p.Trim('#'))
.Distinct()
.ToList();
tags = parsedText
.SelectMany(p => p.Children.Append(p))
.OfType<MfmHashtagNode>()
.Select(p => p.Hashtag.ToLowerInvariant())
.Select(p => p.Trim('#'))
.Distinct()
.ToList();
}
var extracted = note?.Tags?.OfType<ASHashtag>()
@ -1174,7 +1175,7 @@ public class NoteService(
.Cast<string>()
.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,
}));
}

View file

@ -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<string>()
.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<MfmMentionNode> EnumerateMentions(IEnumerable<IMfmNode> nodes)
private static List<MfmMentionNode> EnumerateMentions(Span<IMfmNode> nodes)
{
var list = new List<MfmMentionNode>();

View file

@ -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<User> 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<User> 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<ASEmoji>().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(
/// </remarks>
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<string> ResolveHashtags(string? text, ASActor? actor = null)
private List<string> ResolveHashtags(IMfmNode[]? parsedText, ASActor? actor = null)
{
List<string> tags = [];
if (text != null)
if (parsedText != null)
{
tags = MfmParser.Parse(text)
.SelectMany(p => p.Children.Append(p))
.OfType<MfmHashtagNode>()
.Select(p => p.Hashtag.ToLowerInvariant())
.Select(p => p.Trim('#'))
.Distinct()
.ToList();
tags = parsedText
.SelectMany(p => p.Children.Append(p))
.OfType<MfmHashtagNode>()
.Select(p => p.Hashtag.ToLowerInvariant())
.Select(p => p.Trim('#'))
.Distinct()
.ToList();
}
var extracted = actor?.Tags?.OfType<ASHashtag>()
@ -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();
}
}
}

View file

@ -52,7 +52,7 @@
<PackageReference Include="Ulid" Version="1.3.4" />
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.3" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.4" />
<PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" />
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />
<PackageReference Include="NetVips" Version="3.0.0" />

View file

@ -27,7 +27,7 @@
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="BlazorIntersectionObserver" Version="3.1.0" />
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.3" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />