[backend/federation] Generate mention & reply notifications (ISH-45)

This commit is contained in:
Laura Hausmann 2024-02-13 03:33:41 +01:00
parent 82b7b93681
commit c46573ae37
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 71 additions and 10 deletions

View file

@ -38,6 +38,7 @@ public static class ServiceExtensions {
.AddScoped<WebFingerService>() .AddScoped<WebFingerService>()
.AddScoped<SystemUserService>() .AddScoped<SystemUserService>()
.AddScoped<DriveService>() .AddScoped<DriveService>()
.AddScoped<NotificationService>()
.AddScoped<DatabaseMaintenanceService>() .AddScoped<DatabaseMaintenanceService>()
.AddScoped<AuthorizedFetchMiddleware>() .AddScoped<AuthorizedFetchMiddleware>()
.AddScoped<AuthenticationMiddleware>() .AddScoped<AuthenticationMiddleware>()

View file

@ -15,8 +15,9 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
using MentionQuad = using MentionQuintuple =
(List<string> mentionedUserIds, (List<string> mentionedUserIds,
List<string> mentionedLocalUserIds,
List<Note.MentionedUser> mentions, List<Note.MentionedUser> mentions,
List<Note.MentionedUser> remoteMentions, List<Note.MentionedUser> remoteMentions,
Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping); Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping);
@ -34,7 +35,8 @@ public class NoteService(
UserRenderer userRenderer, UserRenderer userRenderer,
MentionsResolver mentionsResolver, MentionsResolver mentionsResolver,
MfmConverter mfmConverter, MfmConverter mfmConverter,
DriveService driveSvc DriveService driveSvc,
NotificationService notificationSvc
) { ) {
private readonly List<string> _resolverHistory = []; private readonly List<string> _resolverHistory = [];
private int _recursionLimit = 100; private int _recursionLimit = 100;
@ -49,7 +51,8 @@ public class NoteService(
if (text is { Length: > 100000 }) if (text is { Length: > 100000 })
throw GracefulException.BadRequest("Text cannot be longer than 100.000 characters"); 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) if (text != null)
text = mentionsResolver.ResolveMentions(text, null, mentions, splitDomainMapping); text = mentionsResolver.ResolveMentions(text, null, mentions, splitDomainMapping);
@ -80,6 +83,7 @@ public class NoteService(
user.NotesCount++; user.NotesCount++;
await db.AddAsync(note); await db.AddAsync(note);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notificationSvc.GenerateMentionNotifications(note, mentionedLocalUserIds);
var obj = await noteRenderer.RenderAsync(note, mentions); var obj = await noteRenderer.RenderAsync(note, mentions);
var activity = ActivityRenderer.RenderCreate(obj, actor); 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) //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 { var dbNote = new Note {
Id = IdHelpers.GenerateSlowflakeId(), Id = IdHelpers.GenerateSlowflakeId(),
@ -205,11 +210,13 @@ public class NoteService(
user.NotesCount++; user.NotesCount++;
await db.Notes.AddAsync(dbNote); await db.Notes.AddAsync(dbNote);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notificationSvc.GenerateMentionNotifications(dbNote, mentionedLocalUserIds);
await notificationSvc.GenerateReplyNotifications(dbNote, mentionedLocalUserIds);
logger.LogDebug("Note {id} created successfully", dbNote.Id); logger.LogDebug("Note {id} created successfully", dbNote.Id);
return dbNote; return dbNote;
} }
private async Task<MentionQuad> ResolveNoteMentionsAsync(ASNote note) { private async Task<MentionQuintuple> ResolveNoteMentionsAsync(ASNote note) {
var mentionTags = note.Tags?.OfType<ASMention>().Where(p => p.Href != null) ?? []; var mentionTags = note.Tags?.OfType<ASMention>().Where(p => p.Href != null) ?? [];
var users = await mentionTags var users = await mentionTags
.Select(async p => { .Select(async p => {
@ -225,7 +232,7 @@ public class NoteService(
return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList()); return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList());
} }
private async Task<MentionQuad> ResolveNoteMentionsAsync(string? text) { private async Task<MentionQuintuple> ResolveNoteMentionsAsync(string? text) {
var users = text != null var users = text != null
? await MfmParser.Parse(text) ? await MfmParser.Parse(text)
.SelectMany(p => p.Children.Append(p)) .SelectMany(p => p.Children.Append(p))
@ -245,8 +252,9 @@ public class NoteService(
return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList()); return ResolveNoteMentions(users.Where(p => p != null).Select(p => p!).ToList());
} }
private MentionQuad ResolveNoteMentions(IReadOnlyCollection<User> users) { private MentionQuintuple ResolveNoteMentions(IReadOnlyCollection<User> users) {
var userIds = users.Select(p => p.Id).Distinct().ToList(); 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 }) var remoteUsers = users.Where(p => p is { Host: not null, Uri: not null })
.ToList(); .ToList();
@ -274,7 +282,7 @@ public class NoteService(
var mentions = remoteMentions.Concat(localMentions).ToList(); var mentions = remoteMentions.Concat(localMentions).ToList();
return (userIds, mentions, remoteMentions, splitDomainMapping); return (userIds, localUserIds, mentions, remoteMentions, splitDomainMapping);
} }
private async Task<List<DriveFile>> ProcessAttachmentsAsync( private async Task<List<DriveFile>> ProcessAttachmentsAsync(
@ -283,7 +291,8 @@ public class NoteService(
if (attachments is not { Count: > 0 }) return []; if (attachments is not { Count: > 0 }) return [];
var result = await attachments var result = await attachments
.OfType<ASDocument>() .OfType<ASDocument>()
.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(); .AwaitAllNoConcurrencyAsync();
return result.Where(p => p != null).Cast<DriveFile>().ToList(); return result.Where(p => p != null).Cast<DriveFile>().ToList();

View file

@ -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<string> 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<string> 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();
}
}