a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[backend/api] Add notifications endpoints (ISH-202)

+203 -7
+1 -6
Iceshrimp.Backend/Controllers/Mastodon/NotificationController.cs
··· 54 54 .PrecomputeNoteVisibilities(user) 55 55 .RenderAllForMastodonAsync(notificationRenderer, user); 56 56 57 - //TODO: handle mutes 58 - //TODO: handle reply/renote visibility 59 - 60 57 return Ok(res); 61 58 } 62 59 63 - [HttpGet("{id}")] 60 + [HttpGet("{id:long}")] 64 61 [Authorize("read:notifications")] 65 62 [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationEntity>))] 66 63 [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] ··· 74 71 .PrecomputeNoteVisibilities(user) 75 72 .FirstOrDefaultAsync() ?? 76 73 throw GracefulException.RecordNotFound(); 77 - 78 - //TODO: handle reply/renote visibility 79 74 80 75 var res = await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note), user); 81 76
+96
Iceshrimp.Backend/Controllers/NotificationController.cs
··· 1 + using System.Net.Mime; 2 + using Iceshrimp.Backend.Controllers.Attributes; 3 + using Iceshrimp.Backend.Controllers.Renderers; 4 + using Iceshrimp.Backend.Controllers.Schemas; 5 + using Iceshrimp.Backend.Core.Database; 6 + using Iceshrimp.Backend.Core.Database.Tables; 7 + using Iceshrimp.Backend.Core.Extensions; 8 + using Iceshrimp.Backend.Core.Middleware; 9 + using Iceshrimp.Backend.Core.Services; 10 + using Microsoft.AspNetCore.Mvc; 11 + using Microsoft.AspNetCore.RateLimiting; 12 + using Microsoft.EntityFrameworkCore; 13 + 14 + namespace Iceshrimp.Backend.Controllers; 15 + 16 + [ApiController] 17 + [Authenticate, Authorize] 18 + [EnableRateLimiting("sliding")] 19 + [Route("/api/iceshrimp/v1/notification")] 20 + [Produces(MediaTypeNames.Application.Json)] 21 + public class NotificationController(DatabaseContext db, NotificationRenderer notificationRenderer) : ControllerBase 22 + { 23 + [HttpGet] 24 + [LinkPagination(20, 80)] 25 + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationResponse>))] 26 + public async Task<IActionResult> GetNotifications(PaginationQuery query) 27 + { 28 + var user = HttpContext.GetUserOrFail(); 29 + var notifications = await db.Notifications 30 + .Where(p => p.Notifiee == user) 31 + .IncludeCommonProperties() 32 + .EnsureNoteVisibilityFor(p => p.Note, user) 33 + .FilterBlocked(p => p.Notifier, user) 34 + .FilterBlocked(p => p.Note, user) 35 + .Paginate(query, ControllerContext) 36 + .PrecomputeNoteVisibilities(user) 37 + .ToListAsync(); 38 + 39 + return Ok(await notificationRenderer.RenderMany(notifications.EnforceRenoteReplyVisibility(p => p.Note), user)); 40 + } 41 + 42 + [HttpPost("{id}/read")] 43 + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))] 44 + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] 45 + public async Task<IActionResult> MarkNotificationAsRead(string id) 46 + { 47 + var user = HttpContext.GetUserOrFail(); 48 + var notification = await db.Notifications.FirstOrDefaultAsync(p => p.Notifiee == user && p.Id == id) ?? 49 + throw GracefulException.NotFound("Notification not found"); 50 + 51 + if (!notification.IsRead) 52 + { 53 + notification.IsRead = true; 54 + await db.SaveChangesAsync(); 55 + } 56 + 57 + return Ok(new object()); 58 + } 59 + 60 + [HttpPost("read")] 61 + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))] 62 + public async Task<IActionResult> MarkAllNotificationsAsRead() 63 + { 64 + var user = HttpContext.GetUserOrFail(); 65 + await db.Notifications.Where(p => p.Notifiee == user && !p.IsRead) 66 + .ExecuteUpdateAsync(p => p.SetProperty(n => n.IsRead, true)); 67 + 68 + return Ok(new object()); 69 + } 70 + 71 + [HttpDelete("{id}")] 72 + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))] 73 + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] 74 + public async Task<IActionResult> DeleteNotification(string id) 75 + { 76 + var user = HttpContext.GetUserOrFail(); 77 + var notification = await db.Notifications.FirstOrDefaultAsync(p => p.Notifiee == user && p.Id == id) ?? 78 + throw GracefulException.NotFound("Notification not found"); 79 + 80 + db.Remove(notification); 81 + await db.SaveChangesAsync(); 82 + 83 + return Ok(new object()); 84 + } 85 + 86 + [HttpDelete] 87 + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))] 88 + public async Task<IActionResult> DeleteAllNotifications() 89 + { 90 + var user = HttpContext.GetUserOrFail(); 91 + await db.Notifications.Where(p => p.Notifiee == user) 92 + .ExecuteDeleteAsync(); 93 + 94 + return Ok(new object()); 95 + } 96 + }
+87
Iceshrimp.Backend/Controllers/Renderers/NotificationRenderer.cs
··· 1 + using Iceshrimp.Backend.Controllers.Schemas; 2 + using Iceshrimp.Backend.Core.Database; 3 + using Iceshrimp.Backend.Core.Database.Tables; 4 + using Iceshrimp.Backend.Core.Extensions; 5 + using Iceshrimp.Backend.Core.Services; 6 + using Microsoft.EntityFrameworkCore; 7 + 8 + namespace Iceshrimp.Backend.Controllers.Renderers; 9 + 10 + public class NotificationRenderer( 11 + UserRenderer userRenderer, 12 + NoteRenderer noteRenderer 13 + ) 14 + { 15 + public async Task<NotificationResponse> RenderOne( 16 + Notification notification, User localUser, NotificationRendererDto? data = null 17 + ) 18 + { 19 + var user = notification.Notifier != null 20 + ? (data?.Users ?? await GetUsers([notification])).First(p => p.Id == notification.Notifier.Id) 21 + : null; 22 + 23 + var note = notification.Note != null 24 + ? (data?.Notes ?? await GetNotes([notification], localUser)).First(p => p.Id == notification.Note.Id) 25 + : null; 26 + 27 + return new NotificationResponse 28 + { 29 + Id = notification.Id, 30 + Read = notification.IsRead, 31 + CreatedAt = notification.CreatedAt.ToStringIso8601Like(), 32 + User = user, 33 + Note = note, 34 + Type = RenderType(notification.Type) 35 + }; 36 + } 37 + 38 + private static string RenderType(Notification.NotificationType type) => type switch 39 + { 40 + Notification.NotificationType.Follow => "follow", 41 + Notification.NotificationType.Mention => "mention", 42 + Notification.NotificationType.Reply => "reply", 43 + Notification.NotificationType.Renote => "renote", 44 + Notification.NotificationType.Quote => "quote", 45 + Notification.NotificationType.Like => "like", 46 + Notification.NotificationType.Reaction => "reaction", 47 + Notification.NotificationType.PollVote => "pollVote", 48 + Notification.NotificationType.PollEnded => "pollEnded", 49 + Notification.NotificationType.FollowRequestReceived => "followRequestReceived", 50 + Notification.NotificationType.FollowRequestAccepted => "followRequestAccepted", 51 + Notification.NotificationType.GroupInvited => "groupInvited", 52 + Notification.NotificationType.App => "app", 53 + Notification.NotificationType.Edit => "edit", 54 + Notification.NotificationType.Bite => "bite", 55 + 56 + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) 57 + }; 58 + 59 + private async Task<List<UserResponse>> GetUsers(IEnumerable<Notification> notifications) 60 + { 61 + var users = notifications.Select(p => p.Notifier).OfType<User>().DistinctBy(p => p.Id); 62 + return await userRenderer.RenderMany(users).ToListAsync(); 63 + } 64 + 65 + private async Task<List<NoteResponse>> GetNotes(IEnumerable<Notification> notifications, User user) 66 + { 67 + var notes = notifications.Select(p => p.Note).OfType<Note>().DistinctBy(p => p.Id); 68 + return await noteRenderer.RenderMany(notes, user).ToListAsync(); 69 + } 70 + 71 + public async Task<IEnumerable<NotificationResponse>> RenderMany(IEnumerable<Notification> notifications, User user) 72 + { 73 + var notificationsList = notifications.ToList(); 74 + var data = new NotificationRendererDto 75 + { 76 + Users = await GetUsers(notificationsList), Notes = await GetNotes(notificationsList, user) 77 + }; 78 + 79 + return await notificationsList.Select(p => RenderOne(p, user, data)).AwaitAllAsync(); 80 + } 81 + 82 + public class NotificationRendererDto 83 + { 84 + public List<UserResponse>? Users; 85 + public List<NoteResponse>? Notes; 86 + } 87 + }
+14
Iceshrimp.Backend/Controllers/Schemas/NotificationResponse.cs
··· 1 + using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; 2 + using JI = System.Text.Json.Serialization.JsonIgnoreAttribute; 3 + 4 + namespace Iceshrimp.Backend.Controllers.Schemas; 5 + 6 + public class NotificationResponse 7 + { 8 + [J("id")] public required string Id { get; set; } 9 + [J("type")] public required string Type { get; set; } 10 + [J("read")] public required bool Read { get; set; } 11 + [J("createdAt")] public required string CreatedAt { get; set; } 12 + [J("note")] public NoteResponse? Note { get; set; } 13 + [J("user")] public UserResponse? User { get; set; } 14 + }
+5 -1
Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs
··· 17 17 using Microsoft.OpenApi.Models; 18 18 using StackExchange.Redis; 19 19 using NoteRenderer = Iceshrimp.Backend.Controllers.Renderers.NoteRenderer; 20 + using NotificationRenderer = Iceshrimp.Backend.Controllers.Renderers.NotificationRenderer; 20 21 using UserRenderer = Iceshrimp.Backend.Controllers.Renderers.UserRenderer; 21 22 22 23 namespace Iceshrimp.Backend.Core.Extensions; ··· 54 55 .AddScoped<ErrorHandlerMiddleware>() 55 56 .AddScoped<Controllers.Mastodon.Renderers.UserRenderer>() 56 57 .AddScoped<Controllers.Mastodon.Renderers.NoteRenderer>() 58 + .AddScoped<Controllers.Mastodon.Renderers.NotificationRenderer>() 57 59 .AddScoped<PollRenderer>() 58 60 .AddScoped<PollService>() 59 61 .AddScoped<NoteRenderer>() ··· 227 229 context.HttpContext.Response.ContentType = "application/json"; 228 230 var res = new ErrorResponse 229 231 { 230 - Error = "Too Many Requests", StatusCode = 429, RequestId = context.HttpContext.TraceIdentifier 232 + Error = "Too Many Requests", 233 + StatusCode = 429, 234 + RequestId = context.HttpContext.TraceIdentifier 231 235 }; 232 236 await context.HttpContext.Response.WriteAsJsonAsync(res, token); 233 237 };