[backend/federation] Handle note likes & unlikes (ISH-68)
This commit is contained in:
parent
e53e61e3ed
commit
5113f83c9f
17 changed files with 6248 additions and 42 deletions
|
@ -31,12 +31,13 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
var res = await db.Notifications
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Notifiee == user)
|
||||
.Where(p => p.Note != null)
|
||||
.Where(p => p.Type == NotificationType.Follow
|
||||
|| p.Type == NotificationType.Mention
|
||||
|| p.Type == NotificationType.Reply
|
||||
|| p.Type == NotificationType.Renote
|
||||
|| p.Type == NotificationType.Quote
|
||||
|| p.Type == NotificationType.Reaction
|
||||
|| p.Type == NotificationType.Like
|
||||
|| p.Type == NotificationType.PollEnded
|
||||
|| p.Type == NotificationType.FollowRequestReceived)
|
||||
.EnsureNoteVisibilityFor(p => p.Note, user)
|
||||
|
|
|
@ -20,6 +20,8 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe
|
|||
|
||||
var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ??
|
||||
await userRenderer.RenderAsync(dbNotifier);
|
||||
|
||||
//TODO: specially handle quotes
|
||||
|
||||
var res = new Notification {
|
||||
Id = notification.Id,
|
||||
|
|
|
@ -21,7 +21,7 @@ public class Notification : IEntity {
|
|||
NotificationType.Reply => "mention",
|
||||
NotificationType.Renote => "renote",
|
||||
NotificationType.Quote => "reblog",
|
||||
NotificationType.Reaction => "favourite",
|
||||
NotificationType.Like => "favourite",
|
||||
NotificationType.PollEnded => "poll",
|
||||
NotificationType.FollowRequestReceived => "follow_request",
|
||||
|
||||
|
|
6018
Iceshrimp.Backend/Core/Database/Migrations/20240214182733_AddLikeNotificationType.Designer.cs
generated
Normal file
6018
Iceshrimp.Backend/Core/Database/Migrations/20240214182733_AddLikeNotificationType.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,50 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLikeNotificationType : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.Annotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.Annotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app")
|
||||
.Annotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.Annotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.Annotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.Annotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.OldAnnotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.OldAnnotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app")
|
||||
.OldAnnotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.OldAnnotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:pg_trgm", ",,");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.Annotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.Annotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app")
|
||||
.Annotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.Annotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.Annotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.Annotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.OldAnnotation("Npgsql:Enum:antenna_src_enum", "home,all,users,list,group,instances")
|
||||
.OldAnnotation("Npgsql:Enum:note_visibility_enum", "public,home,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:notification_type_enum", "follow,mention,reply,renote,quote,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app")
|
||||
.OldAnnotation("Npgsql:Enum:page_visibility_enum", "public,followers,specified")
|
||||
.OldAnnotation("Npgsql:Enum:relay_status_enum", "requesting,accepted,rejected")
|
||||
.OldAnnotation("Npgsql:Enum:user_profile_ffvisibility_enum", "public,followers,private")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:pg_trgm", ",,");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "antenna_src_enum", new[] { "home", "all", "users", "list", "group", "instances" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "note_visibility_enum", new[] { "public", "home", "followers", "specified" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type_enum", new[] { "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type_enum", new[] { "follow", "mention", "reply", "renote", "quote", "like", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "page_visibility_enum", new[] { "public", "followers", "specified" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "relay_status_enum", new[] { "requesting", "accepted", "rejected" });
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_profile_ffvisibility_enum", new[] { "public", "followers", "private" });
|
||||
|
|
|
@ -43,7 +43,7 @@ public class Notification : IEntity {
|
|||
/// </summary>
|
||||
[Column("isRead")]
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
|
||||
[Column("type")] public NotificationType Type { get; set; }
|
||||
|
||||
[Column("noteId")] [StringLength(32)] public string? NoteId { get; set; }
|
||||
|
@ -109,6 +109,7 @@ public class Notification : IEntity {
|
|||
[PgName("reply")] Reply,
|
||||
[PgName("renote")] Renote,
|
||||
[PgName("quote")] Quote,
|
||||
[PgName("like")] Like,
|
||||
[PgName("reaction")] Reaction,
|
||||
[PgName("pollVote")] PollVote,
|
||||
[PgName("pollEnded")] PollEnded,
|
||||
|
|
8
Iceshrimp.Backend/Core/Events/NoteInteraction.cs
Normal file
8
Iceshrimp.Backend/Core/Events/NoteInteraction.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Events;
|
||||
|
||||
public class NoteInteraction {
|
||||
public required Note Note;
|
||||
public required User User;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
@ -12,6 +13,8 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameter",
|
||||
Justification = "We want to enforce AS types, so we can't use the base type here")]
|
||||
public class ActivityHandlerService(
|
||||
ILogger<ActivityHandlerService> logger,
|
||||
NoteService noteSvc,
|
||||
|
@ -31,9 +34,10 @@ public class ActivityHandlerService(
|
|||
throw GracefulException.UnprocessableEntity("Instance is blocked");
|
||||
if (activity.Object == null)
|
||||
throw GracefulException.UnprocessableEntity("Activity object is null");
|
||||
if (activity.Object.IsUnresolved)
|
||||
activity.Object = await resolver.ResolveObject(activity.Object) ??
|
||||
throw GracefulException.UnprocessableEntity("Failed to resolve activity object");
|
||||
|
||||
// Resolve object & children
|
||||
activity.Object = await resolver.ResolveObject(activity.Object) ??
|
||||
throw GracefulException.UnprocessableEntity("Failed to resolve activity object");
|
||||
|
||||
//TODO: validate inboxUserId
|
||||
|
||||
|
@ -64,34 +68,45 @@ public class ActivityHandlerService(
|
|||
throw GracefulException.UnprocessableEntity("Delete activity object is unknown or invalid");
|
||||
}
|
||||
case ASFollow: {
|
||||
if (activity.Object is not { } obj)
|
||||
if (activity.Object is not ASActor obj)
|
||||
throw GracefulException.UnprocessableEntity("Follow activity object is invalid");
|
||||
await FollowAsync(obj, activity.Actor, activity.Id);
|
||||
return;
|
||||
}
|
||||
case ASUnfollow: {
|
||||
if (activity.Object is not { } obj)
|
||||
if (activity.Object is not ASActor obj)
|
||||
throw GracefulException.UnprocessableEntity("Unfollow activity object is invalid");
|
||||
await UnfollowAsync(obj, activity.Actor);
|
||||
return;
|
||||
}
|
||||
case ASAccept: {
|
||||
if (activity.Object is not { } obj)
|
||||
if (activity.Object is not ASFollow obj)
|
||||
throw GracefulException.UnprocessableEntity("Accept activity object is invalid");
|
||||
await AcceptAsync(obj, activity.Actor);
|
||||
return;
|
||||
}
|
||||
case ASReject: {
|
||||
if (activity.Object is not { } obj)
|
||||
if (activity.Object is not ASFollow obj)
|
||||
throw GracefulException.UnprocessableEntity("Reject activity object is invalid");
|
||||
await RejectAsync(obj, activity.Actor);
|
||||
return;
|
||||
}
|
||||
case ASUndo: {
|
||||
//TODO: what other types of undo objects are there?
|
||||
if (activity.Object is not ASActivity { Type: ASActivity.Types.Follow, Object: not null } undoActivity)
|
||||
throw new NotImplementedException("Undo activity object is invalid");
|
||||
await UnfollowAsync(undoActivity.Object, activity.Actor);
|
||||
switch (activity.Object) {
|
||||
case ASFollow { Object: ASActor followee }:
|
||||
await UnfollowAsync(followee, activity.Actor);
|
||||
return;
|
||||
case ASLike { Object: ASNote likedNote }:
|
||||
await noteSvc.UnlikeNoteAsync(likedNote, activity.Actor);
|
||||
return;
|
||||
default:
|
||||
throw new NotImplementedException("Undo activity object is invalid");
|
||||
}
|
||||
}
|
||||
case ASLike: {
|
||||
if (activity.Object is not ASNote note)
|
||||
throw GracefulException.UnprocessableEntity("Like activity object is invalid");
|
||||
await noteSvc.LikeNoteAsync(note, activity.Actor);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
|
@ -100,7 +115,7 @@ public class ActivityHandlerService(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task FollowAsync(ASObject followeeActor, ASObject followerActor, string requestId) {
|
||||
private async Task FollowAsync(ASActor followeeActor, ASActor followerActor, string requestId) {
|
||||
var follower = await userResolver.ResolveAsync(followerActor.Id);
|
||||
var followee = await userResolver.ResolveAsync(followeeActor.Id);
|
||||
|
||||
|
@ -163,7 +178,7 @@ public class ActivityHandlerService(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task UnfollowAsync(ASObject followeeActor, ASObject followerActor) {
|
||||
private async Task UnfollowAsync(ASActor followeeActor, ASActor followerActor) {
|
||||
//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);
|
||||
|
@ -180,7 +195,7 @@ public class ActivityHandlerService(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task AcceptAsync(ASObject obj, ASObject actor) {
|
||||
private async Task AcceptAsync(ASFollow obj, ASActor actor) {
|
||||
var prefix = $"https://{config.Value.WebDomain}/follows/";
|
||||
if (!obj.Id.StartsWith(prefix))
|
||||
throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id");
|
||||
|
@ -220,8 +235,8 @@ public class ActivityHandlerService(
|
|||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task RejectAsync(ASObject obj, ASObject actor) {
|
||||
if (obj is not ASFollow { Actor: not null } follow)
|
||||
private async Task RejectAsync(ASFollow follow, ASActor actor) {
|
||||
if (follow is not { Actor: not null })
|
||||
throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object");
|
||||
|
||||
var resolvedActor = await userResolver.ResolveAsync(actor.Id);
|
||||
|
|
|
@ -9,9 +9,15 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
|||
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor",
|
||||
Justification = "We need IOptionsSnapshot for config hot reload")]
|
||||
public class FederationControlService(IOptionsSnapshot<Config.SecuritySection> options, DatabaseContext db) {
|
||||
public class FederationControlService(
|
||||
IOptionsSnapshot<Config.SecuritySection> options,
|
||||
IOptions<Config.InstanceSection> instance,
|
||||
DatabaseContext db
|
||||
) {
|
||||
//TODO: we need some level of caching here
|
||||
public async Task<bool> ShouldBlockAsync(params string[] hosts) {
|
||||
if (hosts.All(p => p == instance.Value.WebDomain || p == instance.Value.AccountDomain)) return false;
|
||||
|
||||
hosts = hosts.Select(p => p.StartsWith("http://") || p.StartsWith("https://") ? new Uri(p).Host : p)
|
||||
.Select(p => p.ToPunycode())
|
||||
.ToArray();
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
|
||||
|
@ -8,15 +10,26 @@ public class ObjectResolver(
|
|||
ILogger<ObjectResolver> logger,
|
||||
ActivityFetcherService fetchSvc,
|
||||
DatabaseContext db,
|
||||
FederationControlService federationCtrl
|
||||
FederationControlService federationCtrl,
|
||||
IOptions<Config.InstanceSection> config
|
||||
) {
|
||||
public async Task<ASObject?> ResolveObject(ASObjectBase baseObj) {
|
||||
if (baseObj is ASObject obj) return obj;
|
||||
public async Task<ASObject?> ResolveObject(ASObjectBase baseObj, int recurse = 5) {
|
||||
if (baseObj is ASActivity { Object.IsUnresolved: true } activity && recurse > 0) {
|
||||
activity.Object = await ResolveObject(activity.Object, --recurse);
|
||||
return await ResolveObject(activity, recurse);
|
||||
}
|
||||
if (baseObj is ASObject { IsUnresolved: false } obj)
|
||||
return obj;
|
||||
if (baseObj.Id == null) {
|
||||
logger.LogDebug("Refusing to resolve object with null id property");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/notes/"))
|
||||
return new ASNote { Id = baseObj.Id };
|
||||
if (baseObj.Id.StartsWith($"https://{config.Value.WebDomain}/users/"))
|
||||
return new ASActor { Id = baseObj.Id };
|
||||
|
||||
if (await federationCtrl.ShouldBlockAsync(baseObj.Id)) {
|
||||
logger.LogDebug("Instance is blocked");
|
||||
return null;
|
||||
|
|
|
@ -55,6 +55,10 @@ public class ASUndo : ASActivity {
|
|||
public ASUndo() => Type = Types.Undo;
|
||||
}
|
||||
|
||||
public class ASLike : ASActivity {
|
||||
public ASLike() => Type = Types.Like;
|
||||
}
|
||||
|
||||
//TODO: add the rest
|
||||
|
||||
public sealed class ASActivityConverter : ASSerializer.ListSingleObjectConverter<ASActivity>;
|
|
@ -8,7 +8,12 @@ using JR = Newtonsoft.Json.JsonRequiredAttribute;
|
|||
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
|
||||
public class ASObject : ASObjectBase {
|
||||
[J("@id")] [JR] public new required string Id { get; set; }
|
||||
[J("@id")]
|
||||
[JR]
|
||||
public new required string Id {
|
||||
get => base.Id ?? throw new NullReferenceException("base.Id should never be null on a required property");
|
||||
set => base.Id = value;
|
||||
}
|
||||
|
||||
[J("@type")]
|
||||
[JC(typeof(StringListSingleConverter))]
|
||||
|
@ -34,7 +39,7 @@ public class ASObject : ASObjectBase {
|
|||
ASActivity.Types.Accept => token.ToObject<ASAccept>(),
|
||||
ASActivity.Types.Reject => token.ToObject<ASReject>(),
|
||||
ASActivity.Types.Undo => token.ToObject<ASUndo>(),
|
||||
ASActivity.Types.Like => token.ToObject<ASActivity>(),
|
||||
ASActivity.Types.Like => token.ToObject<ASLike>(),
|
||||
_ => token.ToObject<ASObject>()
|
||||
},
|
||||
JTokenType.Array => Deserialize(token.First()),
|
||||
|
|
|
@ -1,11 +1,28 @@
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Events;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class EventService {
|
||||
public event EventHandler<Note> NotePublished;
|
||||
public event EventHandler<string> NoteDeleted;
|
||||
public event EventHandler<Note>? NotePublished;
|
||||
public event EventHandler<string>? NoteDeleted;
|
||||
public event EventHandler<NoteInteraction>? NoteLiked;
|
||||
public event EventHandler<NoteInteraction>? NoteUnliked;
|
||||
public event EventHandler<Notification>? Notification;
|
||||
|
||||
public void RaiseNotePublished(object? sender, Note note) => NotePublished.Invoke(sender, note);
|
||||
public void RaiseNoteDeleted(object? sender, Note note) => NoteDeleted.Invoke(sender, note.Id);
|
||||
public void RaiseNotePublished(object? sender, Note note) => NotePublished?.Invoke(sender, note);
|
||||
public void RaiseNoteDeleted(object? sender, Note note) => NoteDeleted?.Invoke(sender, note.Id);
|
||||
|
||||
public void RaiseNotification(object? sender, Notification notification) =>
|
||||
Notification?.Invoke(sender, notification);
|
||||
|
||||
public void RaiseNotifications(object? sender, IEnumerable<Notification> notifications) {
|
||||
foreach (var notification in notifications) Notification?.Invoke(sender, notification);
|
||||
}
|
||||
|
||||
public void RaiseNoteLiked(object? sender, Note note, User user) =>
|
||||
NoteLiked?.Invoke(sender, new NoteInteraction { Note = note, User = user });
|
||||
|
||||
public void RaiseNoteUnliked(object? sender, Note note, User user) =>
|
||||
NoteUnliked?.Invoke(sender, new NoteInteraction { Note = note, User = user });
|
||||
}
|
|
@ -303,7 +303,7 @@ public class NoteService(
|
|||
return result.Where(p => p != null).Cast<DriveFile>().ToList();
|
||||
}
|
||||
|
||||
public async Task<Note?> ResolveNoteAsync(string uri) {
|
||||
public async Task<Note?> ResolveNoteAsync(string uri, ASNote? fetchedNote = null) {
|
||||
//TODO: is this enough to prevent DoS attacks?
|
||||
if (_recursionLimit-- <= 0)
|
||||
throw GracefulException.UnprocessableEntity("Refusing to resolve threads this long");
|
||||
|
@ -312,13 +312,16 @@ public class NoteService(
|
|||
_resolverHistory.Add(uri);
|
||||
|
||||
var note = uri.StartsWith($"https://{config.Value.WebDomain}/notes/")
|
||||
? await db.Notes.FirstOrDefaultAsync(p => p.Id ==
|
||||
uri.Substring($"https://{config.Value.WebDomain}/notes/".Length))
|
||||
: await db.Notes.FirstOrDefaultAsync(p => p.Uri == uri);
|
||||
? await db.Notes.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Id ==
|
||||
uri.Substring($"https://{config.Value.WebDomain}/notes/".Length))
|
||||
: await db.Notes.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Uri == uri);
|
||||
|
||||
if (note != null) return note;
|
||||
|
||||
//TODO: should we fall back to a regular user's keypair if fetching with instance actor fails & a local user is following the actor?
|
||||
var fetchedNote = await fetchSvc.FetchNoteAsync(uri);
|
||||
fetchedNote ??= await fetchSvc.FetchNoteAsync(uri);
|
||||
if (fetchedNote?.AttributedTo is not [{ Id: not null } attrTo]) {
|
||||
logger.LogDebug("Invalid Note.AttributedTo, skipping");
|
||||
return null;
|
||||
|
@ -340,4 +343,48 @@ public class NoteService(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Note?> ResolveNoteAsync(ASNote note) {
|
||||
return await ResolveNoteAsync(note.Id, note);
|
||||
}
|
||||
|
||||
public async Task LikeNoteAsync(Note note, User user) {
|
||||
if (!await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user)) {
|
||||
var like = new NoteLike {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
User = user,
|
||||
Note = note
|
||||
};
|
||||
|
||||
await db.NoteLikes.AddAsync(like);
|
||||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseNoteLiked(this, note, user);
|
||||
await notificationSvc.GenerateLikeNotification(note, user);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnlikeNoteAsync(Note note, User user) {
|
||||
var count = await db.NoteLikes.Where(p => p.Note == note && p.User == user).ExecuteDeleteAsync();
|
||||
if (count == 0) return;
|
||||
eventSvc.RaiseNoteUnliked(this, note, user);
|
||||
await db.Notifications
|
||||
.Where(p => p.Type == Notification.NotificationType.Like && p.Notifiee == note.User &&
|
||||
p.Notifier == user)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
public async Task LikeNoteAsync(ASNote note, ASActor actor) {
|
||||
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot register like for unknown note");
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
|
||||
await LikeNoteAsync(dbNote, user);
|
||||
}
|
||||
|
||||
public async Task UnlikeNoteAsync(ASNote note, ASActor actor) {
|
||||
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot unregister like for unknown note");
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
|
||||
await UnlikeNoteAsync(dbNote, user);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,8 @@ namespace Iceshrimp.Backend.Core.Services;
|
|||
|
||||
public class NotificationService(
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
|
||||
DatabaseContext db
|
||||
DatabaseContext db,
|
||||
EventService eventSvc
|
||||
) {
|
||||
public async Task GenerateMentionNotifications(Note note, IReadOnlyCollection<string> mentionedLocalUserIds) {
|
||||
if (mentionedLocalUserIds.Count == 0) return;
|
||||
|
@ -21,10 +22,12 @@ public class NotificationService(
|
|||
NotifierId = note.UserId,
|
||||
NotifieeId = p,
|
||||
Type = Notification.NotificationType.Mention
|
||||
});
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await db.AddRangeAsync(notifications);
|
||||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseNotifications(this, notifications);
|
||||
}
|
||||
|
||||
public async Task GenerateReplyNotifications(Note note, IReadOnlyCollection<string> mentionedLocalUserIds) {
|
||||
|
@ -43,9 +46,29 @@ public class NotificationService(
|
|||
NotifierId = note.UserId,
|
||||
NotifieeId = p,
|
||||
Type = Notification.NotificationType.Reply
|
||||
});
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await db.AddRangeAsync(notifications);
|
||||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseNotifications(this, notifications);
|
||||
}
|
||||
|
||||
public async Task GenerateLikeNotification(Note note, User user) {
|
||||
if (note.UserHost != null) return;
|
||||
if (note.User == user) return;
|
||||
|
||||
var notification = new Notification {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Note = note,
|
||||
Notifiee = note.User,
|
||||
Notifier = user,
|
||||
Type = Notification.NotificationType.Like
|
||||
};
|
||||
|
||||
await db.AddAsync(notification);
|
||||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseNotification(this, notification);
|
||||
}
|
||||
}
|
|
@ -57,8 +57,4 @@
|
|||
<None Remove="migrate.sql"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Core\Events\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Loading…
Add table
Reference in a new issue