diff --git a/Iceshrimp.Backend/Controllers/Mastodon/NotificationController.cs b/Iceshrimp.Backend/Controllers/Mastodon/NotificationController.cs index 916faad6..1a3ea8de 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/NotificationController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/NotificationController.cs @@ -34,6 +34,8 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not ) { var user = HttpContext.GetUserOrFail(); + var isPleroma = HttpContext.GetOauthToken()!.IsPleroma; + return await db.Notifications .IncludeCommonProperties() .Where(p => p.Notifiee == user) @@ -54,7 +56,7 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not .FilterMutedThreads(user, db) .Paginate(p => p.MastoId, query, ControllerContext) .PrecomputeNoteVisibilities(user) - .RenderAllForMastodonAsync(notificationRenderer, user); + .RenderAllForMastodonAsync(notificationRenderer, user, isPleroma); } [HttpGet("{id:long}")] @@ -64,6 +66,8 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not public async Task GetNotification(long id) { var user = HttpContext.GetUserOrFail(); + var isPleroma = HttpContext.GetOauthToken()!.IsPleroma; + var notification = await db.Notifications .IncludeCommonProperties() .Where(p => p.Notifiee == user && p.MastoId == id) @@ -72,7 +76,7 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not .FirstOrDefaultAsync() ?? throw GracefulException.RecordNotFound(); - var res = await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note), user); + var res = await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note), user, isPleroma); return res; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs index 21704446..0e0ebec6 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NotificationRenderer.cs @@ -1,14 +1,17 @@ using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Middleware; +using Iceshrimp.Backend.Core.Services; +using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers; -public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRenderer) +public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer, UserRenderer userRenderer) { public async Task RenderAsync( - Notification notification, User user, List? accounts = null, + Notification notification, User user, bool isPleroma, List? accounts = null, IEnumerable? statuses = null ) { @@ -25,20 +28,35 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ?? await userRenderer.RenderAsync(dbNotifier); + string? emojiUrl = null; + if (notification.Reaction != null && EmojiService.IsCustomEmoji(notification.Reaction)) { + var parts = notification.Reaction.Trim(':').Split('@'); + emojiUrl = await db.Emojis.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null)).Select(e => e.PublicUrl).FirstOrDefaultAsync(); + } + var res = new NotificationEntity { Id = notification.MastoId.ToString(), - Type = NotificationEntity.EncodeType(notification.Type), + Type = NotificationEntity.EncodeType(notification.Type, isPleroma), Note = note, Notifier = notifier, - CreatedAt = notification.CreatedAt.ToStringIso8601Like() + CreatedAt = notification.CreatedAt.ToStringIso8601Like(), + + Emoji = notification.Reaction, + EmojiUrl = emojiUrl, + + Pleroma = new() { + // TODO: stub + IsMuted = false, + IsSeen = notification.IsRead + } }; return res; } public async Task> RenderManyAsync( - IEnumerable notifications, User user + IEnumerable notifications, User user, bool isPleroma ) { var notificationList = notifications.ToList(); @@ -64,7 +82,7 @@ public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRe user, Filter.FilterContext.Notifications, accounts); return await notificationList - .Select(p => RenderAsync(p, user, accounts, notes)) + .Select(p => RenderAsync(p, user, isPleroma, accounts, notes)) .AwaitAllAsync(); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs index f215c87d..bb9903cb 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/NotificationEntity.cs @@ -2,6 +2,7 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Middleware; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using static Iceshrimp.Backend.Core.Database.Tables.Notification; +using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; @@ -13,9 +14,12 @@ public class NotificationEntity : IEntity [J("status")] public required StatusEntity? Note { get; set; } [J("id")] public required string Id { get; set; } - //TODO: [J("reaction")] public required Reaction? Reaction { get; set; } - public static string EncodeType(NotificationType type) + [J("emoji")] public string? Emoji { get; set; } + [J("emoji_url")] public string? EmojiUrl { get; set; } + [J("pleroma")] public required PleromaNotificationExtensions Pleroma { get; set; } + + public static string EncodeType(NotificationType type, bool isPleroma) { return type switch { @@ -28,7 +32,9 @@ public class NotificationEntity : IEntity NotificationType.PollEnded => "poll", NotificationType.FollowRequestReceived => "follow_request", NotificationType.Edit => "update", - NotificationType.Reaction => "reaction", + + NotificationType.Reaction when isPleroma => "pleroma:emoji_reaction", + NotificationType.Reaction when !isPleroma => "reaction", _ => throw new GracefulException($"Unsupported notification type: {type}") }; @@ -46,6 +52,7 @@ public class NotificationEntity : IEntity "follow_request" => [NotificationType.FollowRequestReceived], "update" => [NotificationType.Edit], "reaction" => [NotificationType.Reaction], + "pleroma:emoji_reaction" => [NotificationType.Reaction], _ => [] }; } diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Streaming/Channels/UserChannel.cs b/Iceshrimp.Backend/Controllers/Mastodon/Streaming/Channels/UserChannel.cs index bf7449e0..1fbae971 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Streaming/Channels/UserChannel.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Streaming/Channels/UserChannel.cs @@ -183,7 +183,7 @@ public class UserChannel(WebSocketConnection connection, bool notificationsOnly) NotificationEntity rendered; try { - rendered = await renderer.RenderAsync(notification, connection.Token.User); + rendered = await renderer.RenderAsync(notification, connection.Token.User, connection.Token.IsPleroma); } catch (GracefulException) { diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaNotificationExtensions.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaNotificationExtensions.cs new file mode 100644 index 00000000..c84f29dc --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaNotificationExtensions.cs @@ -0,0 +1,9 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +public class PleromaNotificationExtensions +{ + [J("is_muted")] public required bool IsMuted { get; set; } + [J("is_seen")] public required bool IsSeen { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index 4c4bd625..60ea060a 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -523,13 +523,13 @@ public static class QueryableExtensions } public static async Task> RenderAllForMastodonAsync( - this IQueryable notifications, NotificationRenderer renderer, User user + this IQueryable notifications, NotificationRenderer renderer, User user, bool isPleroma ) { var list = (await notifications.ToListAsync()) .EnforceRenoteReplyVisibility(p => p.Note) .ToList(); - return (await renderer.RenderManyAsync(list, user)).ToList(); + return (await renderer.RenderManyAsync(list, user, isPleroma)).ToList(); } public static IQueryable FilterByAccountStatusesRequest( diff --git a/Iceshrimp.Backend/Core/Services/EmojiService.cs b/Iceshrimp.Backend/Core/Services/EmojiService.cs index edeb809e..08f937cb 100644 --- a/Iceshrimp.Backend/Core/Services/EmojiService.cs +++ b/Iceshrimp.Backend/Core/Services/EmojiService.cs @@ -230,6 +230,8 @@ public partial class EmojiService( return emoji; } + public static bool IsCustomEmoji(string s) => CustomEmojiRegex().IsMatch(s); + [GeneratedRegex(@"^:?([\w+-]+)(?:@\.)?:?$", RegexOptions.Compiled)] private static partial Regex CustomEmojiRegex(); diff --git a/Iceshrimp.Backend/Core/Services/NotificationService.cs b/Iceshrimp.Backend/Core/Services/NotificationService.cs index b1f572da..8a5cc1d3 100644 --- a/Iceshrimp.Backend/Core/Services/NotificationService.cs +++ b/Iceshrimp.Backend/Core/Services/NotificationService.cs @@ -134,7 +134,8 @@ public class NotificationService( Note = reaction.Note, Notifiee = reaction.Note.User, Notifier = reaction.User, - Type = Notification.NotificationType.Reaction + Type = Notification.NotificationType.Reaction, + Reaction = reaction.Reaction }; await db.AddAsync(notification); diff --git a/Iceshrimp.Backend/Core/Services/PushService.cs b/Iceshrimp.Backend/Core/Services/PushService.cs index 1f9e4841..65e4d672 100644 --- a/Iceshrimp.Backend/Core/Services/PushService.cs +++ b/Iceshrimp.Backend/Core/Services/PushService.cs @@ -33,12 +33,14 @@ public class PushService( private async void MastodonPushHandler(object? _, Notification notification) { + // TODO: do not hardcode isPleroma + try { await using var scope = scopeFactory.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); - var type = NotificationEntity.EncodeType(notification.Type); + var type = NotificationEntity.EncodeType(notification.Type, isPleroma: false); var subscriptions = await db.PushSubscriptions.Where(p => p.User == notification.Notifiee) .Include(pushSubscription => pushSubscription.OauthToken) .Where(p => p.Types.Contains(type)) @@ -74,7 +76,7 @@ public class PushService( p.Followee == notification.Notifiee); var renderer = scope.ServiceProvider.GetRequiredService(); - var rendered = await renderer.RenderAsync(notification, notification.Notifiee); + var rendered = await renderer.RenderAsync(notification, notification.Notifiee, isPleroma: false); var name = rendered.Notifier.DisplayName; var subject = rendered.Type switch {