[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
|
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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue