diff --git a/Iceshrimp.Backend/Components/Helpers/AsyncComponentBase.cs b/Iceshrimp.Backend/Components/Helpers/AsyncComponentBase.cs new file mode 100644 index 00000000..8c3f25ef --- /dev/null +++ b/Iceshrimp.Backend/Components/Helpers/AsyncComponentBase.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Components; + +namespace Iceshrimp.Backend.Components.Helpers; + +/// +/// Overrides to allow for asynchronous actions to be performed in fully SSR pages before the page gets rendered +/// +public class AsyncComponentBase : ComponentBase +{ + private bool _initialized; + + public override Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + if (_initialized) return CallOnParametersSetAsync(); + _initialized = true; + + return RunInitAndSetParametersAsync(); + } + + private async Task RunInitAndSetParametersAsync() + { + OnInitialized(); + await OnInitializedAsync(); + await CallOnParametersSetAsync(); + } + + private async Task CallOnParametersSetAsync() + { + OnParametersSet(); + await OnParametersSetAsync(); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Attributes/PublicPreviewRouteFilter.cs b/Iceshrimp.Backend/Components/PublicPreview/Attributes/PublicPreviewRouteFilter.cs new file mode 100644 index 00000000..2549b59e --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Attributes/PublicPreviewRouteFilter.cs @@ -0,0 +1,42 @@ +using AngleSharp.Io; +using Microsoft.AspNetCore.Routing.Matching; + +namespace Iceshrimp.Backend.Components.PublicPreview.Attributes; + +public class PublicPreviewRouteMatcher : MatcherPolicy, IEndpointSelectorPolicy +{ + public override int Order => 99999; // That's ActionConstraintMatcherPolicy - 1 + + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return endpoints.Any(p => p.Metadata.GetMetadata() != null); + } + + public Task ApplyAsync(HttpContext ctx, CandidateSet candidates) + { + var applies = Enumerate(candidates) + .Any(p => p.Score >= 0 && p.Endpoint.Metadata.GetMetadata() != null); + if (!applies) return Task.CompletedTask; + + // Add Vary: Accept to the response headers to prevent caches serving the wrong response + ctx.Response.Headers.Append(HeaderNames.Vary, HeaderNames.Cookie); + + var hasCookie = ctx.Request.Cookies.ContainsKey("sessions"); + for (var i = 0; i < candidates.Count; i++) + { + var candidate = candidates[i]; + var hasAttr = candidate.Endpoint.Metadata.GetMetadata() != null; + candidates.SetValidity(i, !hasCookie || !hasAttr); + } + + return Task.CompletedTask; + } + + private static IEnumerable Enumerate(CandidateSet candidates) + { + for (var i = 0; i < candidates.Count; i++) yield return candidates[i]; + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class PublicPreviewRouteFilterAttribute : Attribute; \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor b/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor new file mode 100644 index 00000000..1b091e88 --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor @@ -0,0 +1,34 @@ +@using Iceshrimp.Backend.Components.PublicPreview.Schemas + + + + + + +
+ @if (Note.Text != null) + { + if (Note.Cw != null) + { +
+ @Note.Cw + @Note.Text +
+ } + else + { + @Note.Text + } + } +
+ +@code { + [Parameter, EditorRequired] public required PreviewNote Note { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor.css b/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor.css new file mode 100644 index 00000000..8b46be7e --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/NoteComponent.razor.css @@ -0,0 +1,11 @@ +.content { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #f7f7f7; + background-color: var(--background-alt); + padding: 10px; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Renderers/MfmRenderer.cs b/Iceshrimp.Backend/Components/PublicPreview/Renderers/MfmRenderer.cs new file mode 100644 index 00000000..83138ba5 --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Renderers/MfmRenderer.cs @@ -0,0 +1,23 @@ +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; +using Iceshrimp.Parsing; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Components.PublicPreview.Renderers; + +public class MfmRenderer(IOptions config) +{ + private readonly MfmConverter _converter = new(config); + + public async Task Render( + string? text, string? host, List mentions, List emoji, string rootElement + ) + { + if (text is null) return null; + var parsed = Mfm.parse(text); + var serialized = await _converter.ToHtmlAsync(parsed, mentions, host, emoji: emoji, rootElement: rootElement); + return new MarkupString(serialized); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Components/PublicPreview/Renderers/NoteRenderer.cs new file mode 100644 index 00000000..1c20bfba --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Renderers/NoteRenderer.cs @@ -0,0 +1,118 @@ +using Iceshrimp.Backend.Components.PublicPreview.Schemas; +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Components.PublicPreview.Renderers; + +public class NoteRenderer( + DatabaseContext db, + UserRenderer userRenderer, + MfmRenderer mfm, + IOptions instance, + IOptionsSnapshot security +) +{ + public async Task RenderOne(Note? note) + { + if (note == null) return null; + + var allNotes = ((Note?[]) [note, note.Reply, note.Renote]).NotNull().ToList(); + + var mentions = await GetMentions(allNotes); + var emoji = await GetEmoji(allNotes); + var users = await GetUsers(allNotes); + var attachments = await GetAttachments(allNotes); + + return await Render(note, users, mentions, emoji, attachments); + } + + private async Task Render( + Note note, List users, Dictionary> mentions, + Dictionary> emoji, Dictionary?> attachments + ) + { + var res = new PreviewNote + { + User = users.First(p => p.Id == note.User.Id), + Text = await mfm.Render(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span"), + Cw = note.Cw, + RawText = note.Text, + QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value), + QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false, + Attachments = attachments[note.Id], + CreatedAt = note.CreatedAt.ToDisplayStringTz(), + UpdatedAt = note.UpdatedAt?.ToDisplayStringTz() + }; + + return res; + } + + private async Task>> GetMentions(List notes) + { + var mentions = notes.SelectMany(n => n.Mentions).Distinct().ToList(); + if (mentions.Count == 0) return notes.ToDictionary>(p => p.Id, _ => []); + + var users = await db.Users.Where(p => mentions.Contains(p.Id)) + .ToDictionaryAsync(p => p.Id, + p => new Note.MentionedUser + { + Host = p.Host, + Uri = p.Uri ?? p.GetPublicUri(instance.Value), + Url = p.UserProfile?.Url, + Username = p.Username + }); + return notes.ToDictionary(p => p.Id, + p => users.Where(u => p.Mentions.Contains(u.Key)).Select(u => u.Value).ToList()); + } + + private async Task>> GetEmoji(List notes) + { + var ids = notes.SelectMany(n => n.Emojis).Distinct().ToList(); + if (ids.Count == 0) return notes.ToDictionary>(p => p.Id, _ => []); + + var emoji = await db.Emojis.Where(p => ids.Contains(p.Id)).ToListAsync(); + return notes.ToDictionary(p => p.Id, p => emoji.Where(e => p.Emojis.Contains(e.Id)).ToList()); + } + + private async Task> GetUsers(List notes) + { + if (notes is []) return []; + return await userRenderer.RenderMany(notes.Select(p => p.User).Distinct().ToList()); + } + + private async Task?>> GetAttachments(List notes) + { + if (security.Value.PublicPreview is Enums.PublicPreview.RestrictedNoMedia) + return notes.ToDictionary?>(p => p.Id, + p => p.FileIds is [] ? null : []); + + var ids = notes.SelectMany(p => p.FileIds).ToList(); + var files = await db.DriveFiles.Where(p => ids.Contains(p.Id)).ToListAsync(); + return notes.ToDictionary?>(p => p.Id, + p => files + .Where(f => p.FileIds.Contains(f.Id)) + .Select(f => new PreviewAttachment + { + MimeType = f.Type, + Url = f.AccessUrl, + Name = f.Name, + Alt = f.Comment + }) + .ToList()); + } + + public async Task> RenderMany(List notes) + { + if (notes is []) return []; + var allNotes = notes.SelectMany(p => [p, p.Renote, p.Reply]).NotNull().Distinct().ToList(); + var users = await GetUsers(allNotes); + var mentions = await GetMentions(allNotes); + var emoji = await GetEmoji(allNotes); + var attachments = await GetAttachments(allNotes); + return await notes.Select(p => Render(p, users, mentions, emoji, attachments)).AwaitAllAsync().ToListAsync(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Renderers/UserRenderer.cs b/Iceshrimp.Backend/Components/PublicPreview/Renderers/UserRenderer.cs new file mode 100644 index 00000000..cc74483a --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Renderers/UserRenderer.cs @@ -0,0 +1,68 @@ +using Iceshrimp.Backend.Components.PublicPreview.Schemas; +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Components.PublicPreview.Renderers; + +public class UserRenderer( + DatabaseContext db, + MfmRenderer mfm, + IOptions instance, + IOptionsSnapshot security +) +{ + public async Task RenderOne(User? user) + { + if (user == null) return null; + var emoji = await GetEmoji([user]); + return await Render(user, emoji); + } + + private async Task Render(User user, Dictionary> emoji) + { + var mentions = user.UserProfile?.Mentions ?? []; + + // @formatter:off + var res = new PreviewUser + { + Id = user.Id, + Username = user.Username, + Host = user.Host ?? instance.Value.AccountDomain, + Url = user.UserProfile?.Url ?? user.Uri ?? user.PublicUrlPath, + AvatarUrl = user.AvatarUrl ?? user.IdenticonUrlPath, + BannerUrl = user.BannerUrl, + RawDisplayName = user.DisplayName, + DisplayName = await mfm.Render(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"), + Bio = await mfm.Render(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"), + MovedToUri = user.MovedToUri + }; + // @formatter:on + + if (security.Value.PublicPreview is Enums.PublicPreview.RestrictedNoMedia) + { + res.AvatarUrl = user.IdenticonUrlPath; + res.BannerUrl = null; + } + + return res; + } + + private async Task>> GetEmoji(List users) + { + var ids = users.SelectMany(n => n.Emojis).Distinct().ToList(); + if (ids.Count == 0) return users.ToDictionary>(p => p.Id, _ => []); + + var emoji = await db.Emojis.Where(p => ids.Contains(p.Id)).ToListAsync(); + return users.ToDictionary(p => p.Id, p => emoji.Where(e => p.Emojis.Contains(e.Id)).ToList()); + } + + public async Task> RenderMany(List users) + { + var emoji = await GetEmoji(users); + return await users.Select(p => Render(p, emoji)).AwaitAllAsync().ToListAsync(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewNote.cs b/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewNote.cs new file mode 100644 index 00000000..76c99cf5 --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewNote.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components; + +namespace Iceshrimp.Backend.Components.PublicPreview.Schemas; + +public class PreviewNote +{ + public required PreviewUser User; + public required string? RawText; + public required MarkupString? Text; + public required string? Cw; + public required string? QuoteUrl; + public required bool QuoteInaccessible; + public required List? Attachments; + public required string CreatedAt; + public required string? UpdatedAt; +} + +public class PreviewAttachment +{ + public required string MimeType; + public required string Url; + public required string Name; + public required string? Alt; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewUser.cs b/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewUser.cs new file mode 100644 index 00000000..84c1171d --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/Schemas/PreviewUser.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Components; + +namespace Iceshrimp.Backend.Components.PublicPreview.Schemas; + +public class PreviewUser +{ + public required string Id; + public required string? RawDisplayName; + public required MarkupString? DisplayName; + public required MarkupString? Bio; + public required string Username; + public required string Host; + public required string Url; + public required string AvatarUrl; + public required string? BannerUrl; + public required string? MovedToUri; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor b/Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor new file mode 100644 index 00000000..d4d279f7 --- /dev/null +++ b/Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor @@ -0,0 +1,32 @@ +@using Iceshrimp.Backend.Components.PublicPreview.Schemas + +@if (Link) +{ + +} +else +{ + User avatar +} + +@code { + [Parameter, EditorRequired] public required PreviewUser User { get; set; } + [Parameter] public bool Link { get; set; } = true; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Pages/Note.cshtml.css b/Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor.css similarity index 70% rename from Iceshrimp.Backend/Pages/Note.cshtml.css rename to Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor.css index 8481cd32..d8400f88 100644 --- a/Iceshrimp.Backend/Pages/Note.cshtml.css +++ b/Iceshrimp.Backend/Components/PublicPreview/UserComponent.razor.css @@ -1,15 +1,3 @@ -.content { - display: flex; - flex-direction: column; - align-items: flex-start; - background-color: #f7f7f7; - background-color: var(--background-alt); - padding: 10px; - margin: 1em 0; - border-radius: 6px; - overflow: hidden; -} - .user { display: grid; grid-template-columns: repeat(auto-fill, minmax(3em, 3em)); @@ -45,4 +33,8 @@ .display-name { color: var(--text-bright); +} + +.host { + color: var(--text-dim); } \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/RootComponent.razor b/Iceshrimp.Backend/Components/RootComponent.razor new file mode 100644 index 00000000..e9cbf8fc --- /dev/null +++ b/Iceshrimp.Backend/Components/RootComponent.razor @@ -0,0 +1,38 @@ +@using System.Text.Encodings.Web +@using Iceshrimp.Backend.Core.Configuration +@using Iceshrimp.Backend.Core.Middleware +@using Microsoft.Extensions.Options +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@inject IOptions Instance +@preservewhitespace true +@attribute [RazorSsr] + + + + + + + + + + Iceshrimp.NET + + + + + + + +
+ Iceshrimp.NET v@(Instance.Value.Version) + + Login + +
+ + + +@code { + [CascadingParameter] public required HttpContext Context { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Components/RootComponent.razor.css b/Iceshrimp.Backend/Components/RootComponent.razor.css new file mode 100644 index 00000000..9618dd9a --- /dev/null +++ b/Iceshrimp.Backend/Components/RootComponent.razor.css @@ -0,0 +1,3 @@ +.float-right { + float: right; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Database/Tables/User.cs b/Iceshrimp.Backend/Core/Database/Tables/User.cs index 3da61447..1e430ce0 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/User.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/User.cs @@ -624,9 +624,11 @@ public class User : IEntity : throw new Exception("Cannot access PublicUri for remote user"); public string GetPublicUrl(string webDomain) => Host == null - ? $"https://{webDomain}/@{Username}" + ? $"https://{webDomain}{PublicUrlPath}" : throw new Exception("Cannot access PublicUrl for remote user"); + [Projectable] public string PublicUrlPath => $"/@{Username}"; + public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}"; } diff --git a/Iceshrimp.Backend/Core/Extensions/MvcBuilderExtensions.cs b/Iceshrimp.Backend/Core/Extensions/MvcBuilderExtensions.cs index e3a88c7e..806e1e28 100644 --- a/Iceshrimp.Backend/Core/Extensions/MvcBuilderExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/MvcBuilderExtensions.cs @@ -101,11 +101,6 @@ public static class MvcBuilderExtensions return builder; } - - public static IMvcBuilder AddRouteOverrides(this IMvcBuilder builder) - { - return builder.AddRazorPagesOptions(o => { o.Conventions.AddPageRoute("/User", "@{user}@{host}"); }); - } } /// diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 27304902..f61f71b1 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.RateLimiting; using System.Xml.Linq; +using Iceshrimp.Backend.Components.PublicPreview.Attributes; +using Iceshrimp.Backend.Components.PublicPreview.Renderers; using Iceshrimp.Backend.Controllers.Federation; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Web.Renderers; @@ -83,7 +85,9 @@ public static class ServiceExtensions .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped() + .AddScoped(); // Singleton = instantiated once across application lifetime services @@ -101,7 +105,10 @@ public static class ServiceExtensions .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); var config = configuration.GetSection("Storage").Get() ?? throw new Exception("Failed to read storage config section"); diff --git a/Iceshrimp.Backend/Core/Extensions/StringExtensions.cs b/Iceshrimp.Backend/Core/Extensions/StringExtensions.cs index 5b68e704..bd2c2ded 100644 --- a/Iceshrimp.Backend/Core/Extensions/StringExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/StringExtensions.cs @@ -19,6 +19,12 @@ public static class StringExtensions { return target[..Math.Min(target.Length, maxLength)]; } + + public static string TruncateEllipsis(this string target, int maxLength) + { + if (target.Length <= maxLength) return target; + return target[..(maxLength-3)] + "..."; + } private static string ToPunycode(this string target) { diff --git a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs index d900d701..4b6a54ed 100644 --- a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs @@ -27,7 +27,8 @@ public static class WebApplicationExtensions .UseMiddleware() .UseMiddleware() .UseMiddleware() - .UseMiddleware(); + .UseMiddleware() + .UseMiddleware(); } public static IApplicationBuilder UseSwaggerWithOptions(this WebApplication app) diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs index 3243cbc4..89ed7cd2 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs @@ -60,13 +60,13 @@ public class MfmConverter(IOptions config) public async Task ToHtmlAsync( IEnumerable nodes, List mentions, string? host, string? quoteUri = null, - bool quoteInaccessible = false, bool replyInaccessible = false, bool divAsRoot = false, + bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", List? emoji = null ) { var context = BrowsingContext.New(); var document = await context.OpenNewAsync(); - var element = document.CreateElement(divAsRoot ? "div" : "p"); + var element = document.CreateElement(rootElement); var nodeList = nodes.ToList(); var hasContent = nodeList.Count > 0; @@ -131,13 +131,13 @@ public class MfmConverter(IOptions config) public async Task ToHtmlAsync( string mfm, List mentions, string? host, string? quoteUri = null, - bool quoteInaccessible = false, bool replyInaccessible = false, bool divAsRoot = false, + bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", List? emoji = null ) { var nodes = MfmParser.Parse(mfm); return await ToHtmlAsync(nodes, mentions, host, quoteUri, quoteInaccessible, - replyInaccessible, divAsRoot, emoji); + replyInaccessible, rootElement, emoji); } private INode FromMfmNode( diff --git a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs index d7b32ef0..cf1c8a24 100644 --- a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs @@ -131,7 +131,6 @@ public class ErrorHandlerMiddleware( }; await WriteResponse(error); - //TODO: use the overload that takes an exception instead of printing it ourselves logger.LogError("Request encountered an unexpected error: {exception}", e); } @@ -167,7 +166,7 @@ public class ErrorHandlerMiddleware( { var model = new ErrorPageModel(payload); ctx.Response.ContentType = "text/html; charset=utf8"; - var stream = ctx.Response.BodyWriter.AsStream(); + var stream = ctx.Response.Body; await razor.RenderToStreamAsync("Shared/ErrorPage.cshtml", model, stream); return; } diff --git a/Iceshrimp.Backend/Core/Middleware/StripRazorJsInitMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/StripRazorJsInitMiddleware.cs new file mode 100644 index 00000000..fc9de131 --- /dev/null +++ b/Iceshrimp.Backend/Core/Middleware/StripRazorJsInitMiddleware.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using Microsoft.AspNetCore.Components.Endpoints; + +namespace Iceshrimp.Backend.Core.Middleware; + +public class StripRazorJsInitMiddleware : IMiddleware +{ + private static readonly byte[] Magic = "