using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using MongoDB.Entities; using PrivaPub.Domain.Privacy; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Rendering; using PrivaPub.Models.Federation; using PrivaPub.Models.Post; using PrivaPub.StaticServices; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Web.Pages { public sealed record PostView(string Title, string Warning, string ContentHtml, string Url, DateTime Published, bool Edited) { public static PostView From(PostEntity post, LocalActor author) => new( post.Title, post.SpoilerText ?? (post.HasContentWarning ? ActivityPubRenderer.ContentWarning : default), post.ContentHtml ?? ActivityPubRenderer.Html(post.Text), author.PostHtmlUrl(post.ID), DateTime.SpecifyKind(post.CreationDate, DateTimeKind.Utc), post.EditedAt.HasValue); } public abstract class PublicPageModel : PageModel { protected bool WantsActivityJson() { var accept = Request.Headers.Accept.ToString(); return accept.Contains("activity+json", StringComparison.OrdinalIgnoreCase) || accept.Contains("ld+json", StringComparison.OrdinalIgnoreCase); } protected void Harden() { Response.Headers["Content-Security-Policy"] = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"; Response.Headers["Referrer-Policy"] = "no-referrer"; Response.Headers["X-Content-Type-Options"] = "nosniff"; } } public class ProfileModel : PublicPageModel { const int PageSize = 20; readonly ILocalActorService _localActors; readonly DbEntities _dbEntities; public ProfileModel(ILocalActorService localActors, DbEntities dbEntities) { _localActors = localActors; _dbEntities = dbEntities; } public LocalActor Actor { get; private set; } public string BiographyHtml { get; private set; } public IReadOnlyList Posts { get; private set; } = Array.Empty(); public async Task OnGetAsync(string user, CancellationToken token) { Actor = await _localActors.FindByUserName(user, token); if (Actor is not { IsFederated: true, IsCircle: false } || Actor.Kind == LocalActorKind.Application) return NotFound(); if (WantsActivityJson()) return Redirect(Actor.Uri); Harden(); BiographyHtml = ActivityPubRenderer.Html(Actor.Summary); var posts = await (Actor.Kind == LocalActorKind.Group ? _dbEntities.Posts.Match(p => p.GroupId == Actor.Id) : _dbEntities.Posts.Match(p => p.GroupUserId == Actor.Id && !p.IsFederatedCopy)) .Match(VisibilityPolicy.IsPublic) .Match(p => p.ReblogOfPostId == null) .Sort(p => p.ID, Order.Descending) .Limit(PageSize) .ExecuteAsync(token); var authors = new Dictionary { [Actor.Id] = Actor }; var views = new List(); foreach (var post in posts) { if (post.IsFederatedCopy) continue; if (!authors.TryGetValue(post.GroupUserId, out var author)) authors[post.GroupUserId] = author = await _localActors.FindById(LocalActorKind.Person, post.GroupUserId, token); if (author != default) views.Add(PostView.From(post, author)); } Posts = views; return Page(); } } public class StatusModel : PublicPageModel { readonly ILocalActorService _localActors; readonly DbEntities _dbEntities; public StatusModel(ILocalActorService localActors, DbEntities dbEntities) { _localActors = localActors; _dbEntities = dbEntities; } public LocalActor Actor { get; private set; } public PostView Post { get; private set; } public string ObjectUri { get; private set; } public async Task OnGetAsync(string user, string id, CancellationToken token) { Actor = await _localActors.FindByUserName(user, token); if (Actor is not { IsFederated: true, Kind: LocalActorKind.Person }) return NotFound(); var post = await _dbEntities.Posts .Match(p => p.ID == id && p.GroupUserId == Actor.Id && !p.IsFederatedCopy && p.ReblogOfPostId == null) .Match(VisibilityPolicy.IsPublic) .ExecuteFirstAsync(token); if (post == default) return NotFound(); ObjectUri = Actor.PostUri(post.ID); if (WantsActivityJson()) return Redirect(ObjectUri); Harden(); Post = PostView.From(post, Actor); return Page(); } } }