[backend/federation] Refactor NoteServer to take a User param instead of an ASActor to save DB roundtrips
This commit is contained in:
parent
19ffbe7814
commit
c35d503c12
3 changed files with 65 additions and 76 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue