[backend/federation] Handle ASAnnounce activities

This commit is contained in:
Laura Hausmann 2024-02-21 22:23:32 +01:00
parent 18af329ba6
commit 19ffbe7814
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
11 changed files with 100 additions and 12 deletions

View file

@ -164,7 +164,12 @@ public class NoteRenderer(
IEnumerable<Note> notes, User? user, List<AccountEntity>? accounts = null IEnumerable<Note> notes, User? user, List<AccountEntity>? accounts = null
) )
{ {
var noteList = notes.ToList(); var noteList = notes.SelectMany<Note, Note?>(p => [p, p.Renote])
.Where(p => p != null)
.Cast<Note>()
.DistinctBy(p => p.Id)
.ToList();
accounts ??= await GetAccounts(noteList.Select(p => p.User)); accounts ??= await GetAccounts(noteList.Select(p => p.User));
var mentions = await GetMentions(noteList); var mentions = await GetMentions(noteList);
var attachments = await GetAttachments(noteList); var attachments = await GetAttachments(noteList);

View file

@ -14,9 +14,16 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe
{ {
var dbNotifier = notification.Notifier ?? throw new GracefulException("Notification has no notifier"); 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 var note = notification.Note != null
? statuses?.FirstOrDefault(p => p.Id == notification.Note.Id) ?? ? statuses?.FirstOrDefault(p => p.Id == targetNote!.Id) ??
await noteRenderer.RenderAsync(notification.Note, user, accounts) await noteRenderer.RenderAsync(targetNote!, user, accounts)
: null; : null;
var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ?? 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) var accounts = await noteRenderer.GetAccounts(notificationList.Where(p => p.Notifier != null)
.Select(p => p.Notifier) .Select(p => p.Notifier)
.Concat(notificationList.Select(p => p.Notifiee)) .Concat(notificationList.Select(p => p.Notifiee))
.Concat(notificationList
.Select(p => p.Note?.Renote?.User)
.Where(p => p != null))
.Cast<User>() .Cast<User>()
.DistinctBy(p => p.Id)); .DistinctBy(p => p.Id));
var notes = await noteRenderer.RenderManyAsync(notificationList.Where(p => p.Note != null) var notes = await noteRenderer.RenderManyAsync(notificationList.Where(p => p.Note != null)
.Select(p => p.Note) .Select(p => p.Note)
.Concat(notificationList
.Select(p => p.Note?.Renote)
.Where(p => p != null))
.Cast<Note>() .Cast<Note>()
.DistinctBy(p => p.Id), user, accounts); .DistinctBy(p => p.Id), user, accounts);

View file

@ -22,8 +22,8 @@ public class NotificationEntity : IEntity
NotificationType.Follow => "follow", NotificationType.Follow => "follow",
NotificationType.Mention => "mention", NotificationType.Mention => "mention",
NotificationType.Reply => "mention", NotificationType.Reply => "mention",
NotificationType.Renote => "renote", NotificationType.Renote => "reblog",
NotificationType.Quote => "reblog", NotificationType.Quote => "status",
NotificationType.Like => "favourite", NotificationType.Like => "favourite",
NotificationType.PollEnded => "poll", NotificationType.PollEnded => "poll",
NotificationType.FollowRequestReceived => "follow_request", NotificationType.FollowRequestReceived => "follow_request",
@ -39,8 +39,7 @@ public class NotificationEntity : IEntity
{ {
"follow" => [NotificationType.Follow], "follow" => [NotificationType.Follow],
"mention" => [NotificationType.Mention, NotificationType.Reply], "mention" => [NotificationType.Mention, NotificationType.Reply],
"renote" => [NotificationType.Renote], "reblog" => [NotificationType.Renote, NotificationType.Quote],
"reblog" => [NotificationType.Quote],
"favourite" => [NotificationType.Like], "favourite" => [NotificationType.Like],
"poll" => [NotificationType.PollEnded], "poll" => [NotificationType.PollEnded],
"follow_request" => [NotificationType.FollowRequestReceived], "follow_request" => [NotificationType.FollowRequestReceived],

View file

@ -253,7 +253,8 @@ public static class QueryableExtensions
) )
{ {
var list = (await notes.ToListAsync()) var list = (await notes.ToListAsync())
.EnforceRenoteReplyVisibility(); .EnforceRenoteReplyVisibility()
.ToList();
return (await renderer.RenderManyAsync(list, user)).ToList(); return (await renderer.RenderManyAsync(list, user)).ToList();
} }

View file

@ -227,6 +227,16 @@ public class ActivityHandlerService(
await notificationSvc.GenerateBiteNotification(dbBite); await notificationSvc.GenerateBiteNotification(dbBite);
return; 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: default:
throw new NotImplementedException($"Activity type {activity.Type} is unknown"); throw new NotImplementedException($"Activity type {activity.Type} is unknown");
} }

View file

@ -31,7 +31,7 @@ public class ObjectResolver(
} }
if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/notes/")) 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/")) if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/users/"))
return new ASActor { Id = baseObj.Id }; return new ASActor { Id = baseObj.Id };
@ -42,14 +42,17 @@ public class ObjectResolver(
} }
if (await db.Notes.AnyAsync(p => p.Uri == baseObj.Id)) 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)) if (await db.Users.AnyAsync(p => p.Uri == baseObj.Id))
return new ASActor { Id = baseObj.Id }; return new ASActor { Id = baseObj.Id };
try try
{ {
var result = await fetchSvc.FetchActivityAsync(baseObj.Id); var result = await fetchSvc.FetchActivityAsync(baseObj.Id);
return result.FirstOrDefault(); var resolvedObj = result.FirstOrDefault();
if (resolvedObj is not ASNote note) return resolvedObj;
note.VerifiedFetch = true;
return note;
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -1,4 +1,5 @@
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using J = Newtonsoft.Json.JsonPropertyAttribute; using J = Newtonsoft.Json.JsonPropertyAttribute;
using JC = Newtonsoft.Json.JsonConverterAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute;
using JI = Newtonsoft.Json.JsonIgnoreAttribute; using JI = Newtonsoft.Json.JsonIgnoreAttribute;
@ -24,6 +25,7 @@ public class ASActivity : ASObject
public const string Create = $"{Ns}#Create"; public const string Create = $"{Ns}#Create";
public const string Update = $"{Ns}#Update"; public const string Update = $"{Ns}#Update";
public const string Delete = $"{Ns}#Delete"; public const string Delete = $"{Ns}#Delete";
public const string Announce = $"{Ns}#Announce";
public const string Follow = $"{Ns}#Follow"; public const string Follow = $"{Ns}#Follow";
public const string Unfollow = $"{Ns}#Unfollow"; public const string Unfollow = $"{Ns}#Unfollow";
public const string Accept = $"{Ns}#Accept"; 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<ASObjectBase>? To { get; set; }
[J($"{Constants.ActivityStreamsNs}#cc")]
public List<ASObjectBase>? 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 class ASDelete : ASActivity
{ {
public ASDelete() => Type = Types.Delete; public ASDelete() => Type = Types.Delete;

View file

@ -59,6 +59,8 @@ public class ASNote : ASObject
[JC(typeof(ASAttachmentConverter))] [JC(typeof(ASAttachmentConverter))]
public List<ASAttachment>? Attachments { get; set; } public List<ASAttachment>? Attachments { get; set; }
public bool VerifiedFetch = false;
public Note.NoteVisibility GetVisibility(ASActor actor) public Note.NoteVisibility GetVisibility(ASActor actor)
{ {
if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public")) if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))

View file

@ -52,6 +52,7 @@ public class ASObject : ASObjectBase
ASActivity.Types.Undo => token.ToObject<ASUndo>(), ASActivity.Types.Undo => token.ToObject<ASUndo>(),
ASActivity.Types.Like => token.ToObject<ASLike>(), ASActivity.Types.Like => token.ToObject<ASLike>(),
ASActivity.Types.Bite => token.ToObject<ASBite>(), ASActivity.Types.Bite => token.ToObject<ASBite>(),
ASActivity.Types.Announce => token.ToObject<ASAnnounce>(),
_ => token.ToObject<ASObject>() _ => token.ToObject<ASObject>()
}; };
case JTokenType.Array: case JTokenType.Array:

View file

@ -91,11 +91,17 @@ public class NoteService(
user.NotesCount++; user.NotesCount++;
if (reply != null) reply.RepliesCount++; 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.AddAsync(note);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
eventSvc.RaiseNotePublished(this, note); eventSvc.RaiseNotePublished(this, note);
await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds); await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds);
if (user.Host != null) return note;
var actor = userRenderer.RenderLite(user); var actor = userRenderer.RenderLite(user);
var obj = await noteRenderer.RenderAsync(note, mentions); var obj = await noteRenderer.RenderAsync(note, mentions);
var activity = ActivityPub.ActivityRenderer.RenderCreate(obj, actor); var activity = ActivityPub.ActivityRenderer.RenderCreate(obj, actor);

View file

@ -179,4 +179,27 @@ public class NotificationService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
eventSvc.RaiseNotification(this, notification); 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);
}
} }