[backend/federation] Refactor NoteServer to take a User param instead of an ASActor to save DB roundtrips

This commit is contained in:
Laura Hausmann 2024-02-22 00:14:25 +01:00
parent 19ffbe7814
commit c35d503c12
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 65 additions and 76 deletions

View file

@ -65,7 +65,7 @@ public class ActivityHandlerService(
//TODO: should we handle other types of creates? //TODO: should we handle other types of creates?
if (activity.Object is not ASNote note) if (activity.Object is not ASNote note)
throw GracefulException.UnprocessableEntity("Create activity object is invalid"); throw GracefulException.UnprocessableEntity("Create activity object is invalid");
await noteSvc.ProcessNoteAsync(note, activity.Actor); await noteSvc.ProcessNoteAsync(note, resolvedActor);
return; return;
} }
case ASDelete: case ASDelete:
@ -86,7 +86,7 @@ public class ActivityHandlerService(
throw GracefulException.UnprocessableEntity("Delete activity object is invalid"); throw GracefulException.UnprocessableEntity("Delete activity object is invalid");
if (await db.Notes.AnyAsync(p => p.Uri == tombstone.Id)) if (await db.Notes.AnyAsync(p => p.Uri == tombstone.Id))
{ {
await noteSvc.DeleteNoteAsync(tombstone, activity.Actor); await noteSvc.DeleteNoteAsync(tombstone, resolvedActor);
return; return;
} }
@ -105,28 +105,28 @@ public class ActivityHandlerService(
{ {
if (activity.Object is not ASActor obj) if (activity.Object is not ASActor obj)
throw GracefulException.UnprocessableEntity("Follow activity object is invalid"); throw GracefulException.UnprocessableEntity("Follow activity object is invalid");
await FollowAsync(obj, activity.Actor, activity.Id); await FollowAsync(obj, activity.Actor, resolvedActor, activity.Id);
return; return;
} }
case ASUnfollow: case ASUnfollow:
{ {
if (activity.Object is not ASActor obj) if (activity.Object is not ASActor obj)
throw GracefulException.UnprocessableEntity("Unfollow activity object is invalid"); throw GracefulException.UnprocessableEntity("Unfollow activity object is invalid");
await UnfollowAsync(obj, activity.Actor); await UnfollowAsync(obj, resolvedActor);
return; return;
} }
case ASAccept: case ASAccept:
{ {
if (activity.Object is not ASFollow obj) if (activity.Object is not ASFollow obj)
throw GracefulException.UnprocessableEntity("Accept activity object is invalid"); throw GracefulException.UnprocessableEntity("Accept activity object is invalid");
await AcceptAsync(obj, activity.Actor); await AcceptAsync(obj, resolvedActor);
return; return;
} }
case ASReject: case ASReject:
{ {
if (activity.Object is not ASFollow obj) if (activity.Object is not ASFollow obj)
throw GracefulException.UnprocessableEntity("Reject activity object is invalid"); throw GracefulException.UnprocessableEntity("Reject activity object is invalid");
await RejectAsync(obj, activity.Actor); await RejectAsync(obj, resolvedActor);
return; return;
} }
case ASUndo: case ASUndo:
@ -134,10 +134,10 @@ public class ActivityHandlerService(
switch (activity.Object) switch (activity.Object)
{ {
case ASFollow { Object: ASActor followee }: case ASFollow { Object: ASActor followee }:
await UnfollowAsync(followee, activity.Actor); await UnfollowAsync(followee, resolvedActor);
return; return;
case ASLike { Object: ASNote likedNote }: case ASLike { Object: ASNote likedNote }:
await noteSvc.UnlikeNoteAsync(likedNote, activity.Actor); await noteSvc.UnlikeNoteAsync(likedNote, resolvedActor);
return; return;
default: default:
throw GracefulException.UnprocessableEntity("Undo activity object is invalid"); throw GracefulException.UnprocessableEntity("Undo activity object is invalid");
@ -147,7 +147,7 @@ public class ActivityHandlerService(
{ {
if (activity.Object is not ASNote note) if (activity.Object is not ASNote note)
throw GracefulException.UnprocessableEntity("Like activity object is invalid"); throw GracefulException.UnprocessableEntity("Like activity object is invalid");
await noteSvc.LikeNoteAsync(note, activity.Actor); await noteSvc.LikeNoteAsync(note, resolvedActor);
return; return;
} }
case ASUpdate: case ASUpdate:
@ -160,7 +160,7 @@ public class ActivityHandlerService(
await userSvc.UpdateUserAsync(resolvedActor, actor); await userSvc.UpdateUserAsync(resolvedActor, actor);
return; return;
case ASNote note: case ASNote note:
await noteSvc.ProcessNoteUpdateAsync(note, activity.Actor, resolvedActor); await noteSvc.ProcessNoteUpdateAsync(note, resolvedActor);
return; return;
default: default:
throw GracefulException.UnprocessableEntity("Update activity object is invalid"); throw GracefulException.UnprocessableEntity("Update activity object is invalid");
@ -233,7 +233,8 @@ public class ActivityHandlerService(
throw GracefulException.UnprocessableEntity("Invalid or unsupported announce object"); throw GracefulException.UnprocessableEntity("Invalid or unsupported announce object");
var dbNote = await noteSvc.ResolveNoteAsync(note.Id, note.VerifiedFetch ? note : null); var dbNote = await noteSvc.ResolveNoteAsync(note.Id, note.VerifiedFetch ? note : null);
var renote = await noteSvc.CreateNoteAsync(resolvedActor, announce.GetVisibility(activity.Actor), renote: dbNote); var renote = await noteSvc.CreateNoteAsync(resolvedActor, announce.GetVisibility(activity.Actor),
renote: dbNote);
await notificationSvc.GenerateRenoteNotification(renote); await notificationSvc.GenerateRenoteNotification(renote);
return; return;
} }
@ -244,9 +245,8 @@ public class ActivityHandlerService(
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall",
Justification = "Projectable functions can very much be translated to SQL")] Justification = "Projectable functions can very much be translated to SQL")]
private async Task FollowAsync(ASActor followeeActor, ASActor followerActor, string requestId) private async Task FollowAsync(ASActor followeeActor, ASActor followerActor, User follower, string requestId)
{ {
var follower = await userResolver.ResolveAsync(followerActor.Id);
var followee = await userResolver.ResolveAsync(followeeActor.Id); var followee = await userResolver.ResolveAsync(followeeActor.Id);
if (followee.Host != null) throw new Exception("Cannot process follow for remote followee"); if (followee.Host != null) throw new Exception("Cannot process follow for remote followee");
@ -324,10 +324,9 @@ public class ActivityHandlerService(
} }
} }
private async Task UnfollowAsync(ASActor followeeActor, ASActor followerActor) private async Task UnfollowAsync(ASActor followeeActor, User follower)
{ {
//TODO: send reject? or do we not want to copy that part of the old ap core //TODO: send reject? or do we not want to copy that part of the old ap core
var follower = await userResolver.ResolveAsync(followerActor.Id);
var followee = await userResolver.ResolveAsync(followeeActor.Id); var followee = await userResolver.ResolveAsync(followeeActor.Id);
await db.FollowRequests.Where(p => p.Follower == follower && p.Followee == followee).ExecuteDeleteAsync(); await db.FollowRequests.Where(p => p.Follower == follower && p.Followee == followee).ExecuteDeleteAsync();
@ -350,33 +349,32 @@ public class ActivityHandlerService(
} }
} }
private async Task AcceptAsync(ASFollow obj, ASActor actor) private async Task AcceptAsync(ASFollow obj, User actor)
{ {
var prefix = $"https://{config.Value.WebDomain}/follows/"; var prefix = $"https://{config.Value.WebDomain}/follows/";
if (!obj.Id.StartsWith(prefix)) if (!obj.Id.StartsWith(prefix))
throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id"); throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id");
var resolvedActor = await userResolver.ResolveAsync(actor.Id); var ids = obj.Id[prefix.Length..].TrimEnd('/').Split("/");
var ids = obj.Id[prefix.Length..].TrimEnd('/').Split("/"); if (ids.Length != 2 || ids[1] != actor.Id)
if (ids.Length != 2 || ids[1] != resolvedActor.Id)
throw GracefulException throw GracefulException
.UnprocessableEntity($"Actor id '{resolvedActor.Id}' doesn't match followee id '{ids[1]}'"); .UnprocessableEntity($"Actor id '{actor.Id}' doesn't match followee id '{ids[1]}'");
var request = await db.FollowRequests var request = await db.FollowRequests
.Include(p => p.Follower.UserProfile) .Include(p => p.Follower.UserProfile)
.Include(p => p.Followee.UserProfile) .Include(p => p.Followee.UserProfile)
.FirstOrDefaultAsync(p => p.Followee == resolvedActor && p.FollowerId == ids[0]); .FirstOrDefaultAsync(p => p.Followee == actor && p.FollowerId == ids[0]);
if (request == null) if (request == null)
throw GracefulException throw GracefulException
.UnprocessableEntity($"No follow request matching follower '{ids[0]}' and followee '{resolvedActor.Id}' found"); .UnprocessableEntity($"No follow request matching follower '{ids[0]}' and followee '{actor.Id}' found");
var following = new Following var following = new Following
{ {
Id = IdHelpers.GenerateSlowflakeId(), Id = IdHelpers.GenerateSlowflakeId(),
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
Follower = request.Follower, Follower = request.Follower,
Followee = resolvedActor, Followee = actor,
FollowerHost = request.FollowerHost, FollowerHost = request.FollowerHost,
FolloweeHost = request.FolloweeHost, FolloweeHost = request.FolloweeHost,
FollowerInbox = request.FollowerInbox, FollowerInbox = request.FollowerInbox,
@ -385,7 +383,7 @@ public class ActivityHandlerService(
FolloweeSharedInbox = request.FolloweeSharedInbox FolloweeSharedInbox = request.FolloweeSharedInbox
}; };
resolvedActor.FollowersCount++; actor.FollowersCount++;
request.Follower.FollowingCount++; request.Follower.FollowingCount++;
db.Remove(request); db.Remove(request);
@ -394,23 +392,22 @@ public class ActivityHandlerService(
await notificationSvc.GenerateFollowRequestAcceptedNotification(request); await notificationSvc.GenerateFollowRequestAcceptedNotification(request);
} }
private async Task RejectAsync(ASFollow follow, ASActor actor) private async Task RejectAsync(ASFollow follow, User actor)
{ {
if (follow is not { Actor: not null }) if (follow is not { Actor: not null })
throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object"); throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object");
var resolvedActor = await userResolver.ResolveAsync(actor.Id);
var resolvedFollower = await userResolver.ResolveAsync(follow.Actor.Id); var resolvedFollower = await userResolver.ResolveAsync(follow.Actor.Id);
if (resolvedFollower is not { Host: null }) if (resolvedFollower is not { Host: null })
throw GracefulException.UnprocessableEntity("Refusing to reject remote follow"); throw GracefulException.UnprocessableEntity("Refusing to reject remote follow");
await db.FollowRequests.Where(p => p.Followee == resolvedActor && p.Follower == resolvedFollower) await db.FollowRequests.Where(p => p.Followee == actor && p.Follower == resolvedFollower)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
var count = await db.Followings.Where(p => p.Followee == resolvedActor && p.Follower == resolvedFollower) var count = await db.Followings.Where(p => p.Followee == actor && p.Follower == resolvedFollower)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
if (count > 0) if (count > 0)
{ {
resolvedActor.FollowersCount -= count; actor.FollowersCount -= count;
resolvedFollower.FollowingCount -= count; resolvedFollower.FollowingCount -= count;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
@ -418,7 +415,7 @@ public class ActivityHandlerService(
await db.Notifications await db.Notifications
.Where(p => p.Type == Notification.NotificationType.FollowRequestAccepted) .Where(p => p.Type == Notification.NotificationType.FollowRequestAccepted)
.Where(p => p.Notifiee == resolvedFollower && .Where(p => p.Notifiee == resolvedFollower &&
p.Notifier == resolvedActor) p.Notifier == actor)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
} }
} }

View file

@ -61,25 +61,28 @@ public class ASNote : ASObject
public bool VerifiedFetch = false; public bool VerifiedFetch = false;
public Note.NoteVisibility GetVisibility(ASActor actor) public Note.NoteVisibility GetVisibility(User actor)
{ {
if (actor.Host == null) throw new Exception("Can't get recipients for local actor");
if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public")) if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))
return Note.NoteVisibility.Public; return Note.NoteVisibility.Public;
if (Cc.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public")) if (Cc.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))
return Note.NoteVisibility.Home; return Note.NoteVisibility.Home;
if (To.Any(p => p.Id is not null && p.Id == (actor.Followers?.Id ?? actor.Id + "/followers"))) if (To.Any(p => p.Id is not null && p.Id == (actor.FollowersUri ?? actor.Uri + "/followers")))
return Note.NoteVisibility.Followers; return Note.NoteVisibility.Followers;
return Note.NoteVisibility.Specified; return Note.NoteVisibility.Specified;
} }
public List<string> GetRecipients(ASActor actor) public List<string> GetRecipients(User actor)
{ {
if (actor.Host == null) throw new Exception("Can't get recipients for local actor");
return To.Concat(Cc) return To.Concat(Cc)
.Select(p => p.Id) .Select(p => p.Id)
.Distinct() .Distinct()
.Where(p => p != $"{Constants.ActivityStreamsNs}#Public" && .Where(p => p != $"{Constants.ActivityStreamsNs}#Public" &&
p != (actor.Followers?.Id ?? actor.Id + "/followers")) p != (actor.FollowersUri ?? actor.Uri + "/followers"))
.Where(p => p != null) .Where(p => p != null)
.Select(p => p!) .Select(p => p!)
.ToList(); .ToList();

View file

@ -228,7 +228,7 @@ public class NoteService(
await deliverSvc.DeliverToFollowersAsync(activity, note.User, recipients); await deliverSvc.DeliverToFollowersAsync(activity, note.User, recipients);
} }
public async Task DeleteNoteAsync(ASTombstone note, ASActor actor) public async Task DeleteNoteAsync(ASTombstone note, User actor)
{ {
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery (it doesn't know about IncludeCommonProperties()) // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery (it doesn't know about IncludeCommonProperties())
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id); var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
@ -238,24 +238,22 @@ public class NoteService(
return; return;
} }
var user = await userResolver.ResolveAsync(actor.Id);
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above) // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above)
if (dbNote.User != user) if (dbNote.User != actor)
{ {
logger.LogDebug("Note '{id}' isn't owned by actor requesting its deletion, skipping", note.Id); logger.LogDebug("Note '{id}' isn't owned by actor requesting its deletion, skipping", note.Id);
return; return;
} }
logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, user.Id); logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, actor.Id);
user.NotesCount--; actor.NotesCount--;
db.Remove(dbNote); db.Remove(dbNote);
eventSvc.RaiseNoteDeleted(this, dbNote); eventSvc.RaiseNoteDeleted(this, dbNote);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
public async Task<Note> ProcessNoteAsync(ASNote note, ASActor actor) public async Task<Note> ProcessNoteAsync(ASNote note, User actor)
{ {
var dbHit = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id); var dbHit = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
@ -269,15 +267,12 @@ public class NoteService(
_resolverHistory.Add(note.Id); _resolverHistory.Add(note.Id);
logger.LogDebug("Creating note: {id}", note.Id); logger.LogDebug("Creating note: {id}", note.Id);
var user = await userResolver.ResolveAsync(actor.Id);
logger.LogDebug("Resolved user to {userId}", user.Id);
// Validate note // Validate note
if (note.AttributedTo is not { Count: 1 } || note.AttributedTo[0].Id != user.Uri) if (note.AttributedTo is not { Count: 1 } || note.AttributedTo[0].Id != actor.Uri)
throw GracefulException.UnprocessableEntity("User.Uri doesn't match Note.AttributedTo"); throw GracefulException.UnprocessableEntity("User.Uri doesn't match Note.AttributedTo");
if (user.Uri == null) if (actor.Uri == null)
throw GracefulException.UnprocessableEntity("User.Uri is null"); throw GracefulException.UnprocessableEntity("User.Uri is null");
if (new Uri(note.Id).IdnHost != new Uri(user.Uri).IdnHost) if (new Uri(note.Id).IdnHost != new Uri(actor.Uri).IdnHost)
throw GracefulException.UnprocessableEntity("User.Uri host doesn't match Note.Id host"); throw GracefulException.UnprocessableEntity("User.Uri host doesn't match Note.Id host");
if (!note.Id.StartsWith("https://")) if (!note.Id.StartsWith("https://"))
throw GracefulException.UnprocessableEntity("Note.Id schema is invalid"); throw GracefulException.UnprocessableEntity("Note.Id schema is invalid");
@ -285,7 +280,7 @@ public class NoteService(
throw GracefulException.UnprocessableEntity("Note.Url schema is invalid"); throw GracefulException.UnprocessableEntity("Note.Url schema is invalid");
if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3)) if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3))
throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical"); throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical");
if (user.IsSuspended) if (actor.IsSuspended)
throw GracefulException.Forbidden("User is suspended"); throw GracefulException.Forbidden("User is suspended");
//TODO: resolve anything related to the note as well (attachments, emoji, etc) //TODO: resolve anything related to the note as well (attachments, emoji, etc)
@ -303,9 +298,9 @@ public class NoteService(
Url = note.Url?.Id, //FIXME: this doesn't seem to work yet Url = note.Url?.Id, //FIXME: this doesn't seem to work yet
Text = note.MkContent ?? await mfmConverter.FromHtmlAsync(note.Content, mentions), Text = note.MkContent ?? await mfmConverter.FromHtmlAsync(note.Content, mentions),
Cw = note.Summary, Cw = note.Summary,
UserId = user.Id, UserId = actor.Id,
CreatedAt = createdAt, CreatedAt = createdAt,
UserHost = user.Host, UserHost = actor.Host,
Visibility = note.GetVisibility(actor), Visibility = note.GetVisibility(actor),
Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id) : null Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id) : null
}; };
@ -343,14 +338,14 @@ public class NoteService(
} }
var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null; var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null;
var files = await ProcessAttachmentsAsync(note.Attachments, user, sensitive); var files = await ProcessAttachmentsAsync(note.Attachments, actor, sensitive);
if (files.Count != 0) if (files.Count != 0)
{ {
dbNote.FileIds = files.Select(p => p.Id).ToList(); dbNote.FileIds = files.Select(p => p.Id).ToList();
dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList(); dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList();
} }
user.NotesCount++; actor.NotesCount++;
if (dbNote.Reply != null) dbNote.Reply.RepliesCount++; if (dbNote.Reply != null) dbNote.Reply.RepliesCount++;
await db.Notes.AddAsync(dbNote); await db.Notes.AddAsync(dbNote);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -364,13 +359,12 @@ public class NoteService(
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", [SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage",
Justification = "Inspection doesn't understand IncludeCommonProperties()")] Justification = "Inspection doesn't understand IncludeCommonProperties()")]
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")] [SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")]
public async Task<Note> ProcessNoteUpdateAsync(ASNote note, ASActor actor, User? resolvedActor = null) public async Task<Note> ProcessNoteUpdateAsync(ASNote note, User actor)
{ {
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id); var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
if (dbNote == null) return await ProcessNoteAsync(note, actor); if (dbNote == null) return await ProcessNoteAsync(note, actor);
resolvedActor ??= await userResolver.ResolveAsync(actor.Id); if (dbNote.User != actor)
if (dbNote.User != resolvedActor)
throw GracefulException.UnprocessableEntity("Refusing to update note of user other than actor"); throw GracefulException.UnprocessableEntity("Refusing to update note of user other than actor");
if (dbNote.User.IsSuspended) if (dbNote.User.IsSuspended)
throw GracefulException.Forbidden("User is suspended"); throw GracefulException.Forbidden("User is suspended");
@ -425,7 +419,7 @@ public class NoteService(
//TODO: handle updated alt text et al //TODO: handle updated alt text et al
var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null; var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null;
var files = await ProcessAttachmentsAsync(note.Attachments, resolvedActor, sensitive); var files = await ProcessAttachmentsAsync(note.Attachments, actor, sensitive);
dbNote.FileIds = files.Select(p => p.Id).ToList(); dbNote.FileIds = files.Select(p => p.Id).ToList();
dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList(); dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList();
@ -574,8 +568,7 @@ public class NoteService(
if (res != null) return res; if (res != null) return res;
} }
//TODO: we don't need to fetch the actor every time, we can use userResolver here var actor = await userResolver.ResolveAsync(attrTo.Id);
var actor = await fetchSvc.FetchActorAsync(attrTo.Id);
try try
{ {
@ -615,38 +608,34 @@ public class NoteService(
} }
} }
public async Task UnlikeNoteAsync(Note note, User user) public async Task UnlikeNoteAsync(Note note, User actor)
{ {
var count = await db.NoteLikes.Where(p => p.Note == note && p.User == user).ExecuteDeleteAsync(); var count = await db.NoteLikes.Where(p => p.Note == note && p.User == actor).ExecuteDeleteAsync();
if (count == 0) return; if (count == 0) return;
if (user.Host == null && note.UserHost != null) if (actor.Host == null && note.UserHost != null)
{ {
var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(user), var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(actor),
activityRenderer.RenderLike(note, user)); activityRenderer.RenderLike(note, actor));
await deliverSvc.DeliverToFollowersAsync(activity, user, [note.User]); await deliverSvc.DeliverToFollowersAsync(activity, actor, [note.User]);
} }
eventSvc.RaiseNoteUnliked(this, note, user); eventSvc.RaiseNoteUnliked(this, note, actor);
await db.Notifications await db.Notifications
.Where(p => p.Type == Notification.NotificationType.Like && .Where(p => p.Type == Notification.NotificationType.Like &&
p.Notifiee == note.User && p.Notifiee == note.User &&
p.Notifier == user) p.Notifier == actor)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
} }
public async Task LikeNoteAsync(ASNote note, ASActor actor) public async Task LikeNoteAsync(ASNote note, User actor)
{ {
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot register like for unknown note"); var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot register like for unknown note");
var user = await userResolver.ResolveAsync(actor.Id); await LikeNoteAsync(dbNote, actor);
await LikeNoteAsync(dbNote, user);
} }
public async Task UnlikeNoteAsync(ASNote note, ASActor actor) public async Task UnlikeNoteAsync(ASNote note, User actor)
{ {
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot unregister like for unknown note"); var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot unregister like for unknown note");
var user = await userResolver.ResolveAsync(actor.Id); await UnlikeNoteAsync(dbNote, actor);
await UnlikeNoteAsync(dbNote, user);
} }
} }