[backend/federation] Handle ASAnnounce activities
This commit is contained in:
parent
18af329ba6
commit
19ffbe7814
11 changed files with 100 additions and 12 deletions
|
@ -164,7 +164,12 @@ public class NoteRenderer(
|
|||
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));
|
||||
var mentions = await GetMentions(noteList);
|
||||
var attachments = await GetAttachments(noteList);
|
||||
|
|
|
@ -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<User>()
|
||||
.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<Note>()
|
||||
.DistinctBy(p => p.Id), user, accounts);
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -253,7 +253,8 @@ public static class QueryableExtensions
|
|||
)
|
||||
{
|
||||
var list = (await notes.ToListAsync())
|
||||
.EnforceRenoteReplyVisibility();
|
||||
.EnforceRenoteReplyVisibility()
|
||||
.ToList();
|
||||
return (await renderer.RenderManyAsync(list, user)).ToList();
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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<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 ASDelete() => Type = Types.Delete;
|
||||
|
|
|
@ -59,6 +59,8 @@ public class ASNote : ASObject
|
|||
[JC(typeof(ASAttachmentConverter))]
|
||||
public List<ASAttachment>? Attachments { get; set; }
|
||||
|
||||
public bool VerifiedFetch = false;
|
||||
|
||||
public Note.NoteVisibility GetVisibility(ASActor actor)
|
||||
{
|
||||
if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))
|
||||
|
|
|
@ -52,6 +52,7 @@ public class ASObject : ASObjectBase
|
|||
ASActivity.Types.Undo => token.ToObject<ASUndo>(),
|
||||
ASActivity.Types.Like => token.ToObject<ASLike>(),
|
||||
ASActivity.Types.Bite => token.ToObject<ASBite>(),
|
||||
ASActivity.Types.Announce => token.ToObject<ASAnnounce>(),
|
||||
_ => token.ToObject<ASObject>()
|
||||
};
|
||||
case JTokenType.Array:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue