[backend/masto-client] Render notifications (ISH-45)
This commit is contained in:
parent
32b0e1f3c7
commit
61938a4d5b
7 changed files with 169 additions and 15 deletions
|
@ -0,0 +1,47 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Attributes;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using static Iceshrimp.Backend.Core.Database.Tables.Notification;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
|
[MastodonApiController]
|
||||||
|
[Route("/api/v1/notifications")]
|
||||||
|
[Authenticate]
|
||||||
|
[EnableCors("mastodon")]
|
||||||
|
[EnableRateLimiting("sliding")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class NotificationController(DatabaseContext db, NotificationRenderer notificationRenderer) : Controller {
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize("read:notifications")]
|
||||||
|
[LinkPagination(40, 80)]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<Notification>))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||||
|
public async Task<IActionResult> GetNotifications(PaginationQuery query) {
|
||||||
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
var res = await db.Notifications
|
||||||
|
.IncludeCommonProperties()
|
||||||
|
.Where(p => p.Notifiee == user)
|
||||||
|
.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.PollEnded
|
||||||
|
|| p.Type == NotificationType.FollowRequestReceived)
|
||||||
|
.Paginate(query, ControllerContext)
|
||||||
|
.RenderAllForMastodonAsync(notificationRenderer);
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
}
|
|
@ -119,13 +119,13 @@ public class NoteRenderer(
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Account>> GetAccounts(IEnumerable<User> users) {
|
internal async Task<List<Account>> GetAccounts(IEnumerable<User> users) {
|
||||||
return (await userRenderer.RenderManyAsync(users.DistinctBy(p => p.Id))).ToList();
|
return (await userRenderer.RenderManyAsync(users.DistinctBy(p => p.Id))).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Status>> RenderManyAsync(IEnumerable<Note> notes) {
|
public async Task<IEnumerable<Status>> RenderManyAsync(IEnumerable<Note> notes, List<Account>? accounts = null) {
|
||||||
var noteList = notes.ToList();
|
var noteList = notes.ToList();
|
||||||
var accounts = await GetAccounts(noteList.Select(p => p.User));
|
accounts ??= await GetAccounts(noteList.Select(p => p.User));
|
||||||
var mentions = await GetMentions(noteList);
|
var mentions = await GetMentions(noteList);
|
||||||
var attachments = await GetAttachments(noteList);
|
var attachments = await GetAttachments(noteList);
|
||||||
return await noteList.Select(async p => await RenderAsync(p, accounts, mentions, attachments)).AwaitAllAsync();
|
return await noteList.Select(async p => await RenderAsync(p, accounts, mentions, attachments)).AwaitAllAsync();
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using Notification = Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities.Notification;
|
||||||
|
using DbNotification = Iceshrimp.Backend.Core.Database.Tables.Notification;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
|
||||||
|
public class NotificationRenderer(NoteRenderer noteRenderer, UserRenderer userRenderer) {
|
||||||
|
public async Task<Notification> RenderAsync(
|
||||||
|
DbNotification notification, List<Account>? accounts = null, IEnumerable<Status>? statuses = null
|
||||||
|
) {
|
||||||
|
var dbNotifier = notification.Notifier ?? throw new GracefulException("Notification has no notifier");
|
||||||
|
|
||||||
|
var note = notification.Note != null
|
||||||
|
? statuses?.FirstOrDefault(p => p.Id == notification.Note.Id) ??
|
||||||
|
await noteRenderer.RenderAsync(notification.Note, accounts)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ??
|
||||||
|
await userRenderer.RenderAsync(dbNotifier);
|
||||||
|
|
||||||
|
var res = new Notification {
|
||||||
|
Id = notification.Id,
|
||||||
|
Type = Notification.EncodeType(notification.Type),
|
||||||
|
Note = note,
|
||||||
|
Notifier = notifier,
|
||||||
|
CreatedAt = notification.CreatedAt.ToString("O")[..^5]
|
||||||
|
};
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Notification>> RenderManyAsync(IEnumerable<DbNotification> notifications) {
|
||||||
|
var notificationList = notifications.ToList();
|
||||||
|
|
||||||
|
var accounts = await noteRenderer.GetAccounts(notificationList.Where(p => p.Notifier != null)
|
||||||
|
.Select(p => p.Notifier)
|
||||||
|
.Concat(notificationList.Select(p => p.Notifiee))
|
||||||
|
.Cast<User>()
|
||||||
|
.DistinctBy(p => p.Id));
|
||||||
|
|
||||||
|
var notes = await noteRenderer.RenderManyAsync(notificationList.Where(p => p.Note != null)
|
||||||
|
.Select(p => p.Note)
|
||||||
|
.Cast<Note>()
|
||||||
|
.DistinctBy(p => p.Id), accounts);
|
||||||
|
|
||||||
|
return await notificationList
|
||||||
|
.Select(p => RenderAsync(p, accounts, notes))
|
||||||
|
.AwaitAllAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
using static Iceshrimp.Backend.Core.Database.Tables.Notification;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
|
public class Notification : IEntity {
|
||||||
|
[J("id")] public required string Id { get; set; }
|
||||||
|
[J("created_at")] public required string CreatedAt { get; set; }
|
||||||
|
[J("type")] public required string Type { get; set; }
|
||||||
|
[J("account")] public required Account Notifier { get; set; }
|
||||||
|
[J("status")] public required Status? Note { get; set; }
|
||||||
|
|
||||||
|
//TODO: [J("reaction")] public required Reaction? Reaction { get; set; }
|
||||||
|
|
||||||
|
public static string EncodeType(NotificationType type) {
|
||||||
|
return type switch {
|
||||||
|
NotificationType.Follow => "follow",
|
||||||
|
NotificationType.Mention => "mention",
|
||||||
|
NotificationType.Reply => "mention",
|
||||||
|
NotificationType.Renote => "renote",
|
||||||
|
NotificationType.Quote => "reblog",
|
||||||
|
NotificationType.Reaction => "favourite",
|
||||||
|
NotificationType.PollEnded => "poll",
|
||||||
|
NotificationType.FollowRequestReceived => "follow_request",
|
||||||
|
|
||||||
|
_ => throw new GracefulException($"Unsupported notification type: {type}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index("NotifieeId")]
|
[Index("NotifieeId")]
|
||||||
[Index("CreatedAt")]
|
[Index("CreatedAt")]
|
||||||
[Index("AppAccessTokenId")]
|
[Index("AppAccessTokenId")]
|
||||||
public class Notification {
|
public class Notification : IEntity {
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
[StringLength(32)]
|
[StringLength(32)]
|
||||||
|
|
|
@ -7,25 +7,38 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MastoNotification = Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities.Notification;
|
||||||
|
using Notification = Iceshrimp.Backend.Core.Database.Tables.Notification;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Extensions;
|
namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
public static class NoteQueryableExtensions {
|
public static class QueryableExtensions {
|
||||||
|
#pragma warning disable CS8602 // Dereference of a possibly null reference.
|
||||||
|
// Justification: in the context of nullable EF navigation properties, null values are ignored and therefore irrelevant.
|
||||||
|
// Source: https://learn.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types#navigating-and-including-nullable-relationships
|
||||||
|
|
||||||
public static IQueryable<Note> IncludeCommonProperties(this IQueryable<Note> query) {
|
public static IQueryable<Note> IncludeCommonProperties(this IQueryable<Note> query) {
|
||||||
return query.Include(p => p.User)
|
return query.Include(p => p.User.UserProfile)
|
||||||
.ThenInclude(p => p.UserProfile)
|
.Include(p => p.Renote.User.UserProfile)
|
||||||
.Include(p => p.Renote)
|
.Include(p => p.Reply.User.UserProfile);
|
||||||
.ThenInclude(p => p != null ? p.User : null)
|
|
||||||
.ThenInclude(p => p != null ? p.UserProfile : null)
|
|
||||||
.Include(p => p.Reply)
|
|
||||||
.ThenInclude(p => p != null ? p.User : null)
|
|
||||||
.ThenInclude(p => p != null ? p.UserProfile : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IQueryable<User> IncludeCommonProperties(this IQueryable<User> query) {
|
public static IQueryable<User> IncludeCommonProperties(this IQueryable<User> query) {
|
||||||
return query.Include(p => p.UserProfile);
|
return query.Include(p => p.UserProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<Notification> IncludeCommonProperties(this IQueryable<Notification> query) {
|
||||||
|
return query.Include(p => p.Notifiee.UserProfile)
|
||||||
|
.Include(p => p.Notifier.UserProfile)
|
||||||
|
.Include(p => p.Note.User.UserProfile)
|
||||||
|
.Include(p => p.Note.Renote.User.UserProfile)
|
||||||
|
.Include(p => p.Note.Reply.User.UserProfile)
|
||||||
|
.Include(p => p.FollowRequest.Follower.UserProfile)
|
||||||
|
.Include(p => p.FollowRequest.Followee.UserProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore CS8602 // Dereference of a possibly null reference.
|
||||||
|
|
||||||
public static IQueryable<T> Paginate<T>(
|
public static IQueryable<T> Paginate<T>(
|
||||||
this IQueryable<T> query,
|
this IQueryable<T> query,
|
||||||
PaginationQuery pq,
|
PaginationQuery pq,
|
||||||
|
@ -154,6 +167,12 @@ public static class NoteQueryableExtensions {
|
||||||
return (await renderer.RenderManyAsync(list)).ToList();
|
return (await renderer.RenderManyAsync(list)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<List<MastoNotification>> RenderAllForMastodonAsync(
|
||||||
|
this IQueryable<Notification> notifications, NotificationRenderer renderer) {
|
||||||
|
var list = await notifications.ToListAsync();
|
||||||
|
return (await renderer.RenderManyAsync(list)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public static IQueryable<Note> FilterByAccountStatusesRequest(this IQueryable<Note> query,
|
public static IQueryable<Note> FilterByAccountStatusesRequest(this IQueryable<Note> query,
|
||||||
AccountSchemas.AccountStatusesRequest request,
|
AccountSchemas.AccountStatusesRequest request,
|
||||||
User account) {
|
User account) {
|
||||||
|
|
|
@ -44,7 +44,8 @@ public static class ServiceExtensions {
|
||||||
.AddScoped<AuthenticationMiddleware>()
|
.AddScoped<AuthenticationMiddleware>()
|
||||||
.AddScoped<ErrorHandlerMiddleware>()
|
.AddScoped<ErrorHandlerMiddleware>()
|
||||||
.AddScoped<UserRenderer>()
|
.AddScoped<UserRenderer>()
|
||||||
.AddScoped<NoteRenderer>();
|
.AddScoped<NoteRenderer>()
|
||||||
|
.AddScoped<NotificationRenderer>();
|
||||||
|
|
||||||
// Singleton = instantiated once across application lifetime
|
// Singleton = instantiated once across application lifetime
|
||||||
services
|
services
|
||||||
|
|
Loading…
Add table
Reference in a new issue