From 19ffbe78144445e5bdd5de03313fb6c72d8ad909 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 21 Feb 2024 22:23:32 +0100 Subject: [PATCH] [backend/federation] Handle ASAnnounce activities --- .../Mastodon/Renderers/NoteRenderer.cs | 7 +++++- .../Renderers/NotificationRenderer.cs | 17 +++++++++++-- .../Schemas/Entities/NotificationEntity.cs | 7 +++--- .../Core/Extensions/QueryableExtensions.cs | 3 ++- .../ActivityPub/ActivityHandlerService.cs | 10 ++++++++ .../Federation/ActivityPub/ObjectResolver.cs | 11 +++++--- .../ActivityStreams/Types/ASActivity.cs | 25 +++++++++++++++++++ .../ActivityStreams/Types/ASNote.cs | 2 ++ .../ActivityStreams/Types/ASObject.cs | 1 + .../Core/Services/NoteService.cs | 6 +++++ .../Core/Services/NotificationService.cs | 23 +++++++++++++++++ 11 files changed, 100 insertions(+), 12 deletions(-) diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs index 6c50f97f..c98021fc 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs @@ -164,7 +164,12 @@ public class NoteRenderer( IEnumerable notes, User? user, List? accounts = null ) { - var noteList = notes.ToList(); + var noteList = notes.SelectMany(p => [p, p.Renote]) + .Where(p => p != null) + .Cast() + .DistinctBy(p => p.Id) + .ToList(); + accounts ??= await GetAccounts(noteList.Select(p => p.User)); var mentions = await GetMentions(noteList); var attachments = await GetAttachments(noteList); diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs index 5f316ca0..3a47a0f7 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs @@ -14,9 +14,16 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe { var dbNotifier = notification.Notifier ?? throw new GracefulException("Notification has no notifier"); + var targetNote = notification.Type == Notification.NotificationType.Renote + ? notification.Note?.Renote + : notification.Note; + + if (notification.Note != null && targetNote == null) + throw new Exception("targetNote must not be null at this stage"); + var note = notification.Note != null - ? statuses?.FirstOrDefault(p => p.Id == notification.Note.Id) ?? - await noteRenderer.RenderAsync(notification.Note, user, accounts) + ? statuses?.FirstOrDefault(p => p.Id == targetNote!.Id) ?? + await noteRenderer.RenderAsync(targetNote!, user, accounts) : null; var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ?? @@ -45,11 +52,17 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe var accounts = await noteRenderer.GetAccounts(notificationList.Where(p => p.Notifier != null) .Select(p => p.Notifier) .Concat(notificationList.Select(p => p.Notifiee)) + .Concat(notificationList + .Select(p => p.Note?.Renote?.User) + .Where(p => p != null)) .Cast() .DistinctBy(p => p.Id)); var notes = await noteRenderer.RenderManyAsync(notificationList.Where(p => p.Note != null) .Select(p => p.Note) + .Concat(notificationList + .Select(p => p.Note?.Renote) + .Where(p => p != null)) .Cast() .DistinctBy(p => p.Id), user, accounts); diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs index cf477aa6..7a5b38ef 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs @@ -22,8 +22,8 @@ public class NotificationEntity : IEntity NotificationType.Follow => "follow", NotificationType.Mention => "mention", NotificationType.Reply => "mention", - NotificationType.Renote => "renote", - NotificationType.Quote => "reblog", + NotificationType.Renote => "reblog", + NotificationType.Quote => "status", NotificationType.Like => "favourite", NotificationType.PollEnded => "poll", NotificationType.FollowRequestReceived => "follow_request", @@ -39,8 +39,7 @@ public class NotificationEntity : IEntity { "follow" => [NotificationType.Follow], "mention" => [NotificationType.Mention, NotificationType.Reply], - "renote" => [NotificationType.Renote], - "reblog" => [NotificationType.Quote], + "reblog" => [NotificationType.Renote, NotificationType.Quote], "favourite" => [NotificationType.Like], "poll" => [NotificationType.PollEnded], "follow_request" => [NotificationType.FollowRequestReceived], diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index 30d5ccf2..3ede745c 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -253,7 +253,8 @@ public static class QueryableExtensions ) { var list = (await notes.ToListAsync()) - .EnforceRenoteReplyVisibility(); + .EnforceRenoteReplyVisibility() + .ToList(); return (await renderer.RenderManyAsync(list, user)).ToList(); } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs index e8c6b5cc..f26ddfea 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs @@ -227,6 +227,16 @@ public class ActivityHandlerService( await notificationSvc.GenerateBiteNotification(dbBite); return; } + case ASAnnounce announce: + { + if (announce.Object is not ASNote note) + throw GracefulException.UnprocessableEntity("Invalid or unsupported announce object"); + + var dbNote = await noteSvc.ResolveNoteAsync(note.Id, note.VerifiedFetch ? note : null); + var renote = await noteSvc.CreateNoteAsync(resolvedActor, announce.GetVisibility(activity.Actor), renote: dbNote); + await notificationSvc.GenerateRenoteNotification(renote); + return; + } default: throw new NotImplementedException($"Activity type {activity.Type} is unknown"); } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs index d25cd24c..a73ba457 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs @@ -31,7 +31,7 @@ public class ObjectResolver( } if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/notes/")) - return new ASNote { Id = baseObj.Id }; + return new ASNote { Id = baseObj.Id, VerifiedFetch = true }; if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/users/")) return new ASActor { Id = baseObj.Id }; @@ -42,14 +42,17 @@ public class ObjectResolver( } if (await db.Notes.AnyAsync(p => p.Uri == baseObj.Id)) - return new ASNote { Id = baseObj.Id }; + return new ASNote { Id = baseObj.Id, VerifiedFetch = true }; if (await db.Users.AnyAsync(p => p.Uri == baseObj.Id)) return new ASActor { Id = baseObj.Id }; try { - var result = await fetchSvc.FetchActivityAsync(baseObj.Id); - return result.FirstOrDefault(); + var result = await fetchSvc.FetchActivityAsync(baseObj.Id); + var resolvedObj = result.FirstOrDefault(); + if (resolvedObj is not ASNote note) return resolvedObj; + note.VerifiedFetch = true; + return note; } catch (Exception e) { diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs index dfb78fec..c57fd7be 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs @@ -1,4 +1,5 @@ using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database.Tables; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; using JI = Newtonsoft.Json.JsonIgnoreAttribute; @@ -24,6 +25,7 @@ public class ASActivity : ASObject public const string Create = $"{Ns}#Create"; public const string Update = $"{Ns}#Update"; public const string Delete = $"{Ns}#Delete"; + public const string Announce = $"{Ns}#Announce"; public const string Follow = $"{Ns}#Follow"; public const string Unfollow = $"{Ns}#Unfollow"; public const string Accept = $"{Ns}#Accept"; @@ -59,6 +61,29 @@ public class ASCreate : ASActivity } } +public class ASAnnounce : ASActivity +{ + public ASAnnounce() => Type = Types.Announce; + + [J($"{Constants.ActivityStreamsNs}#to")] + public List? To { get; set; } + + [J($"{Constants.ActivityStreamsNs}#cc")] + public List? Cc { get; set; } + + public Note.NoteVisibility GetVisibility(ASActor actor) + { + if (To?.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public") ?? false) + return Note.NoteVisibility.Public; + if (Cc?.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public") ?? false) + return Note.NoteVisibility.Home; + if (To?.Any(p => p.Id is not null && p.Id == (actor.Followers?.Id ?? actor.Id + "/followers")) ?? false) + return Note.NoteVisibility.Followers; + + return Note.NoteVisibility.Specified; + } +} + public class ASDelete : ASActivity { public ASDelete() => Type = Types.Delete; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs index c72d0e5e..c373b82c 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs @@ -59,6 +59,8 @@ public class ASNote : ASObject [JC(typeof(ASAttachmentConverter))] public List? Attachments { get; set; } + public bool VerifiedFetch = false; + public Note.NoteVisibility GetVisibility(ASActor actor) { if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public")) diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs index de0d3cdf..9f5dc3b4 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs @@ -52,6 +52,7 @@ public class ASObject : ASObjectBase ASActivity.Types.Undo => token.ToObject(), ASActivity.Types.Like => token.ToObject(), ASActivity.Types.Bite => token.ToObject(), + ASActivity.Types.Announce => token.ToObject(), _ => token.ToObject() }; case JTokenType.Array: diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index a2f7a7e2..d156badd 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -91,11 +91,17 @@ public class NoteService( user.NotesCount++; if (reply != null) reply.RepliesCount++; + if (renote != null && !note.IsQuote) + if (!db.Notes.Any(p => p.UserId == user.Id && p.RenoteId == renote.Id && p.IsPureRenote)) + renote.RenoteCount++; + await db.AddAsync(note); await db.SaveChangesAsync(); eventSvc.RaiseNotePublished(this, note); await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds); + if (user.Host != null) return note; + var actor = userRenderer.RenderLite(user); var obj = await noteRenderer.RenderAsync(note, mentions); var activity = ActivityPub.ActivityRenderer.RenderCreate(obj, actor); diff --git a/Iceshrimp.Backend/Core/Services/NotificationService.cs b/Iceshrimp.Backend/Core/Services/NotificationService.cs index fc9780b1..65dd5f2c 100644 --- a/Iceshrimp.Backend/Core/Services/NotificationService.cs +++ b/Iceshrimp.Backend/Core/Services/NotificationService.cs @@ -179,4 +179,27 @@ public class NotificationService( await db.SaveChangesAsync(); eventSvc.RaiseNotification(this, notification); } + + [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] + public async Task GenerateRenoteNotification(Note note) + { + if (note.Renote is not { UserHost: null }) return; + if (!note.VisibilityIsPublicOrHome && + await db.Notes.AnyAsync(p => p.Id == note.Id && p.IsVisibleFor(note.Renote.User))) + return; + + var notification = new Notification + { + Id = IdHelpers.GenerateSlowflakeId(), + CreatedAt = DateTime.UtcNow, + Note = note, + Notifiee = note.Renote.User, + Notifier = note.User, + Type = Notification.NotificationType.Renote + }; + + await db.AddAsync(notification); + await db.SaveChangesAsync(); + eventSvc.RaiseNotification(this, notification); + } } \ No newline at end of file