diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index b6fa5b21..d6ba6764 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -38,6 +38,7 @@ public static class ServiceExtensions { .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 73cd0d55..aa9ebcd0 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -15,8 +15,9 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Core.Services; -using MentionQuad = +using MentionQuintuple = (List mentionedUserIds, + List mentionedLocalUserIds, List mentions, List remoteMentions, Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping); @@ -34,7 +35,8 @@ public class NoteService( UserRenderer userRenderer, MentionsResolver mentionsResolver, MfmConverter mfmConverter, - DriveService driveSvc + DriveService driveSvc, + NotificationService notificationSvc ) { private readonly List _resolverHistory = []; private int _recursionLimit = 100; @@ -49,7 +51,8 @@ public class NoteService( if (text is { Length: > 100000 }) throw GracefulException.BadRequest("Text cannot be longer than 100.000 characters"); - var (mentionedUserIds, mentions, remoteMentions, splitDomainMapping) = await ResolveNoteMentionsAsync(text); + var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) = + await ResolveNoteMentionsAsync(text); if (text != null) text = mentionsResolver.ResolveMentions(text, null, mentions, splitDomainMapping); @@ -80,6 +83,7 @@ public class NoteService( user.NotesCount++; await db.AddAsync(note); await db.SaveChangesAsync(); + await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds); var obj = await noteRenderer.RenderAsync(note, mentions); var activity = ActivityRenderer.RenderCreate(obj, actor); @@ -159,7 +163,8 @@ public class NoteService( //TODO: resolve anything related to the note as well (attachments, emoji, etc) - var (mentionedUserIds, mentions, remoteMentions, splitDomainMapping) = await ResolveNoteMentionsAsync(note); + var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) = + await ResolveNoteMentionsAsync(note); var dbNote = new Note { Id = IdHelpers.GenerateSlowflakeId(), @@ -205,11 +210,13 @@ public class NoteService( user.NotesCount++; await db.Notes.AddAsync(dbNote); await db.SaveChangesAsync(); + await notificationSvc.GenerateMentionNotifications(dbNote, mentionedLocalUserIds); + await notificationSvc.GenerateReplyNotifications(dbNote, mentionedLocalUserIds); logger.LogDebug("Note {id} created successfully", dbNote.Id); return dbNote; } - private async Task ResolveNoteMentionsAsync(ASNote note) { + private async Task ResolveNoteMentionsAsync(ASNote note) { var mentionTags = note.Tags?.OfType().Where(p => p.Href != null) ?? []; var users = await mentionTags .Select(async p => { @@ -225,7 +232,7 @@ public class NoteService( return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList()); } - private async Task ResolveNoteMentionsAsync(string? text) { + private async Task ResolveNoteMentionsAsync(string? text) { var users = text != null ? await MfmParser.Parse(text) .SelectMany(p => p.Children.Append(p)) @@ -245,8 +252,9 @@ public class NoteService( return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList()); } - private MentionQuad ResolveNoteMentions(IReadOnlyCollection users) { - var userIds = users.Select(p => p.Id).Distinct().ToList(); + private MentionQuintuple ResolveNoteMentions(IReadOnlyCollection users) { + var userIds = users.Select(p => p.Id).Distinct().ToList(); + var localUserIds = users.Where(p => p.Host == null).Select(p => p.Id).Distinct().ToList(); var remoteUsers = users.Where(p => p is { Host: not null, Uri: not null }) .ToList(); @@ -274,7 +282,7 @@ public class NoteService( var mentions = remoteMentions.Concat(localMentions).ToList(); - return (userIds, mentions, remoteMentions, splitDomainMapping); + return (userIds, localUserIds, mentions, remoteMentions, splitDomainMapping); } private async Task> ProcessAttachmentsAsync( @@ -283,7 +291,8 @@ public class NoteService( if (attachments is not { Count: > 0 }) return []; var result = await attachments .OfType() - .Select(p => driveSvc.StoreFile(p.Url?.Id, user, p.Sensitive ?? sensitive, p.Description, p.MediaType)) + .Select(p => driveSvc.StoreFile(p.Url?.Id, user, p.Sensitive ?? sensitive, p.Description, + p.MediaType)) .AwaitAllNoConcurrencyAsync(); return result.Where(p => p != null).Cast().ToList(); diff --git a/Iceshrimp.Backend/Core/Services/NotificationService.cs b/Iceshrimp.Backend/Core/Services/NotificationService.cs new file mode 100644 index 00000000..eafd193d --- /dev/null +++ b/Iceshrimp.Backend/Core/Services/NotificationService.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Helpers; + +namespace Iceshrimp.Backend.Core.Services; + +public class NotificationService( + [SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")] + DatabaseContext db +) { + public async Task GenerateMentionNotifications(Note note, IReadOnlyCollection mentionedLocalUserIds) { + if (mentionedLocalUserIds.Count == 0) return; + + var notifications = mentionedLocalUserIds + .Where(p => p != note.UserId) + .Select(p => new Notification { + Id = IdHelpers.GenerateSlowflakeId(), + CreatedAt = DateTime.UtcNow, + Note = note, + NotifierId = note.UserId, + NotifieeId = p, + Type = Notification.NotificationType.Mention + }); + + await db.AddRangeAsync(notifications); + await db.SaveChangesAsync(); + } + + public async Task GenerateReplyNotifications(Note note, IReadOnlyCollection mentionedLocalUserIds) { + if (note.Visibility != Note.NoteVisibility.Specified) return; + if (note.VisibleUserIds.Count == 0) return; + + var users = mentionedLocalUserIds.Concat(note.VisibleUserIds).Distinct().Except(mentionedLocalUserIds).ToList(); + if (users.Count == 0) return; + + var notifications = users + .Where(p => p != note.UserId) + .Select(p => new Notification { + Id = IdHelpers.GenerateSlowflakeId(), + CreatedAt = DateTime.UtcNow, + Note = note, + NotifierId = note.UserId, + NotifieeId = p, + Type = Notification.NotificationType.Reply + }); + + await db.AddRangeAsync(notifications); + await db.SaveChangesAsync(); + } +} \ No newline at end of file