[backend/core] Emit notification on note edit for users that have interacted with the note

This commit is contained in:
Laura Hausmann 2024-02-17 05:30:28 +01:00
parent be00d5237f
commit 6044cdb52c
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
13 changed files with 12275 additions and 7 deletions

View file

@ -40,7 +40,8 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
p.Type == NotificationType.Quote || p.Type == NotificationType.Quote ||
p.Type == NotificationType.Like || p.Type == NotificationType.Like ||
p.Type == NotificationType.PollEnded || p.Type == NotificationType.PollEnded ||
p.Type == NotificationType.FollowRequestReceived) p.Type == NotificationType.FollowRequestReceived ||
p.Type == NotificationType.Edit)
.EnsureNoteVisibilityFor(p => p.Note, user) .EnsureNoteVisibilityFor(p => p.Note, user)
.FilterBlocked(p => p.Notifier, user) .FilterBlocked(p => p.Notifier, user)
.FilterBlocked(p => p.Note, user) .FilterBlocked(p => p.Note, user)

View file

@ -27,6 +27,7 @@ public class NotificationEntity : IEntity
NotificationType.Like => "favourite", NotificationType.Like => "favourite",
NotificationType.PollEnded => "poll", NotificationType.PollEnded => "poll",
NotificationType.FollowRequestReceived => "follow_request", NotificationType.FollowRequestReceived => "follow_request",
NotificationType.Edit => "update",
_ => throw new GracefulException($"Unsupported notification type: {type}") _ => throw new GracefulException($"Unsupported notification type: {type}")
}; };

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Iceshrimp.Backend.Core.Database.Migrations
{
/// <inheritdoc />
public partial class AddEditNotificationType : 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,edit")
.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", ",,");
}
/// <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,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,like,reaction,pollVote,pollEnded,receiveFollowRequest,followRequestAccepted,groupInvited,app,edit")
.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", ",,");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,122 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Iceshrimp.Backend.Core.Database.Migrations
{
/// <inheritdoc />
public partial class RenameNoteBookmarksTable2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_note_favorite_note_noteId",
table: "note_favorite");
migrationBuilder.DropForeignKey(
name: "FK_note_favorite_user_userId",
table: "note_favorite");
migrationBuilder.DropPrimaryKey(
name: "PK_note_favorite",
table: "note_favorite");
migrationBuilder.RenameTable(
name: "note_favorite",
newName: "note_bookmark");
migrationBuilder.RenameIndex(
name: "IX_note_favorite_userId_noteId",
table: "note_bookmark",
newName: "IX_note_bookmark_userId_noteId");
migrationBuilder.RenameIndex(
name: "IX_note_favorite_userId",
table: "note_bookmark",
newName: "IX_note_bookmark_userId");
migrationBuilder.RenameIndex(
name: "IX_note_favorite_noteId",
table: "note_bookmark",
newName: "IX_note_bookmark_noteId");
migrationBuilder.AddPrimaryKey(
name: "PK_note_bookmark",
table: "note_bookmark",
column: "id");
migrationBuilder.AddForeignKey(
name: "FK_note_bookmark_note_noteId",
table: "note_bookmark",
column: "noteId",
principalTable: "note",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_note_bookmark_user_userId",
table: "note_bookmark",
column: "userId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_note_bookmark_note_noteId",
table: "note_bookmark");
migrationBuilder.DropForeignKey(
name: "FK_note_bookmark_user_userId",
table: "note_bookmark");
migrationBuilder.DropPrimaryKey(
name: "PK_note_bookmark",
table: "note_bookmark");
migrationBuilder.RenameTable(
name: "note_bookmark",
newName: "note_favorite");
migrationBuilder.RenameIndex(
name: "IX_note_bookmark_userId_noteId",
table: "note_favorite",
newName: "IX_note_favorite_userId_noteId");
migrationBuilder.RenameIndex(
name: "IX_note_bookmark_userId",
table: "note_favorite",
newName: "IX_note_favorite_userId");
migrationBuilder.RenameIndex(
name: "IX_note_bookmark_noteId",
table: "note_favorite",
newName: "IX_note_favorite_noteId");
migrationBuilder.AddPrimaryKey(
name: "PK_note_favorite",
table: "note_favorite",
column: "id");
migrationBuilder.AddForeignKey(
name: "FK_note_favorite_note_noteId",
table: "note_favorite",
column: "noteId",
principalTable: "note",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_note_favorite_user_userId",
table: "note_favorite",
column: "userId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -25,7 +25,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "antenna_src_enum", new[] { "home", "all", "users", "list", "group", "instances" }); 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, "note_visibility_enum", new[] { "public", "home", "followers", "specified" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type_enum", new[] { "follow", "mention", "reply", "renote", "quote", "like", "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", "edit" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "page_visibility_enum", new[] { "public", "followers", "specified" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "page_visibility_enum", new[] { "public", "followers", "specified" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "relay_status_enum", new[] { "requesting", "accepted", "rejected" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "relay_status_enum", new[] { "requesting", "accepted", "rejected" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_profile_ffvisibility_enum", new[] { "public", "followers", "private" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_profile_ffvisibility_enum", new[] { "public", "followers", "private" });
@ -2640,7 +2640,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.HasIndex("UserId", "NoteId") b.HasIndex("UserId", "NoteId")
.IsUnique(); .IsUnique();
b.ToTable("note_favorite"); b.ToTable("note_bookmark");
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteEdit", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteEdit", b =>
@ -3490,7 +3490,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.ToTable("promo_read"); b.ToTable("promo_read");
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistrationTicket", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistrationInvite", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.HasMaxLength(32) .HasMaxLength(32)

View file

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Database.Tables; namespace Iceshrimp.Backend.Core.Database.Tables;
[Table("note_favorite")] [Table("note_bookmark")]
[Index("UserId", "NoteId", IsUnique = true)] [Index("UserId", "NoteId", IsUnique = true)]
[Index("UserId")] [Index("UserId")]
public class NoteBookmark public class NoteBookmark

View file

@ -29,7 +29,8 @@ public class Notification : IEntity
[PgName("receiveFollowRequest")] FollowRequestReceived, [PgName("receiveFollowRequest")] FollowRequestReceived,
[PgName("followRequestAccepted")] FollowRequestAccepted, [PgName("followRequestAccepted")] FollowRequestAccepted,
[PgName("groupInvited")] GroupInvited, [PgName("groupInvited")] GroupInvited,
[PgName("app")] App [PgName("app")] App,
[PgName("edit")] Edit
} }
/// <summary> /// <summary>

View file

@ -384,12 +384,20 @@ public class User : IEntity
[InverseProperty(nameof(NoteBookmark.User))] [InverseProperty(nameof(NoteBookmark.User))]
public virtual ICollection<NoteBookmark> NoteBookmarks { get; set; } = new List<NoteBookmark>(); public virtual ICollection<NoteBookmark> NoteBookmarks { get; set; } = new List<NoteBookmark>();
[NotMapped] [Projectable] public virtual IEnumerable<Note> BookmarkedNotes => NoteBookmarks.Select(p => p.Note);
[InverseProperty(nameof(NoteReaction.User))] [InverseProperty(nameof(NoteReaction.User))]
public virtual ICollection<NoteLike> NoteLikes { get; set; } = new List<NoteLike>(); public virtual ICollection<NoteLike> NoteLikes { get; set; } = new List<NoteLike>();
[NotMapped] [Projectable] public virtual IEnumerable<Note> LikedNotes => NoteLikes.Select(p => p.Note);
[InverseProperty(nameof(NoteReaction.User))] [InverseProperty(nameof(NoteReaction.User))]
public virtual ICollection<NoteReaction> NoteReactions { get; set; } = new List<NoteReaction>(); public virtual ICollection<NoteReaction> NoteReactions { get; set; } = new List<NoteReaction>();
[NotMapped]
[Projectable]
public virtual IEnumerable<Note> ReactedNotes => NoteReactions.Select(p => p.Note).Distinct();
[InverseProperty(nameof(NoteThreadMuting.User))] [InverseProperty(nameof(NoteThreadMuting.User))]
public virtual ICollection<NoteThreadMuting> NoteThreadMutings { get; set; } = new List<NoteThreadMuting>(); public virtual ICollection<NoteThreadMuting> NoteThreadMutings { get; set; } = new List<NoteThreadMuting>();
@ -530,6 +538,29 @@ public class User : IEntity
[Projectable] [Projectable]
public bool HasPinned(Note note) => PinnedNotes.Contains(note); public bool HasPinned(Note note) => PinnedNotes.Contains(note);
[Projectable]
public bool HasBookmarked(Note note) => BookmarkedNotes.Contains(note);
[Projectable]
public bool HasLiked(Note note) => LikedNotes.Contains(note);
[Projectable]
public bool HasReacted(Note note) => ReactedNotes.Contains(note);
[Projectable]
public bool HasRenoted(Note note) => Notes.Any(p => p.Renote == note);
[Projectable]
public bool HasReplied(Note note) => Notes.Any(p => p.Reply == note);
[Projectable]
public bool HasInteractedWith(Note note) =>
HasLiked(note) ||
HasReacted(note) ||
HasBookmarked(note) ||
HasReplied(note) ||
HasRenoted(note);
public User WithPrecomputedBlockStatus(bool blocking, bool blockedBy) public User WithPrecomputedBlockStatus(bool blocking, bool blockedBy)
{ {
PrecomputedIsBlocking = blocking; PrecomputedIsBlocking = blocking;

View file

@ -99,7 +99,7 @@ public class ASUpdate : ASActivity
[J($"{Constants.ActivityStreamsNs}#cc")] [J($"{Constants.ActivityStreamsNs}#cc")]
public List<ASObjectBase>? Cc { get; set; } public List<ASObjectBase>? Cc { get; set; }
[J($"{Constants.ActivityStreamsNs}#object")] [JI]
public new ASObject? Object public new ASObject? Object
{ {
get => base.Object; get => base.Object;

View file

@ -179,6 +179,7 @@ public class NoteService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds); await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds);
await notificationSvc.GenerateReplyNotifications(note, mentionedLocalUserIds); await notificationSvc.GenerateReplyNotifications(note, mentionedLocalUserIds);
await notificationSvc.GenerateEditNotifications(note);
eventSvc.RaiseNoteUpdated(this, note); eventSvc.RaiseNoteUpdated(this, note);
var actor = userRenderer.RenderLite(note.User); var actor = userRenderer.RenderLite(note.User);
@ -405,6 +406,7 @@ public class NoteService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notificationSvc.GenerateMentionNotifications(dbNote, mentionedLocalUserIds); await notificationSvc.GenerateMentionNotifications(dbNote, mentionedLocalUserIds);
await notificationSvc.GenerateReplyNotifications(dbNote, mentionedLocalUserIds); await notificationSvc.GenerateReplyNotifications(dbNote, mentionedLocalUserIds);
await notificationSvc.GenerateEditNotifications(dbNote);
eventSvc.RaiseNoteUpdated(this, dbNote); eventSvc.RaiseNoteUpdated(this, dbNote);
return dbNote; return dbNote;
} }

View file

@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
@ -59,6 +60,31 @@ public class NotificationService(
eventSvc.RaiseNotifications(this, notifications); eventSvc.RaiseNotifications(this, notifications);
} }
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall",
Justification = "Projectable functions are very much translatable")]
public async Task GenerateEditNotifications(Note note)
{
var notifications = await db.Users
.Where(p => p.Host == null && p != note.User && p.HasInteractedWith(note))
.Select(p => new Notification
{
Id = IdHelpers.GenerateSlowflakeId(DateTime.UtcNow),
CreatedAt = DateTime.UtcNow,
Note = note,
NotifierId = note.UserId,
Notifiee = p,
Type = Notification.NotificationType.Edit
})
.ToListAsync();
if (notifications.Count == 0)
return;
await db.AddRangeAsync(notifications);
await db.SaveChangesAsync();
eventSvc.RaiseNotifications(this, notifications);
}
public async Task GenerateLikeNotification(Note note, User user) public async Task GenerateLikeNotification(Note note, User user)
{ {
if (note.UserHost != null) return; if (note.UserHost != null) return;