Swarm/DysonNetwork.Sphere/Post/PostController.cs
2025-05-01 14:59:28 +08:00

270 lines
10 KiB
C#

using System.ComponentModel.DataAnnotations;
using Casbin;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Permission;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Post;
[ApiController]
[Route("/posts")]
public class PostController(AppDatabase db, PostService ps, RelationshipService rels) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account.Account;
var userFriends = await rels.ListAccountFriends(currentUser!);
var totalCount = await db.Posts
.FilterWithVisibility(currentUser, userFriends, isListing: true)
.CountAsync();
var posts = await db.Posts
.Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost)
.Include(e => e.Attachments)
.Include(e => e.Categories)
.Include(e => e.Tags)
.Where(e => e.RepliedPostId == null)
.FilterWithVisibility(currentUser, userFriends, isListing: true)
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
.Skip(offset)
.Take(take)
.ToListAsync();
Response.Headers["X-Total"] = totalCount.ToString();
return Ok(posts);
}
[HttpGet("{id:long}")]
public async Task<ActionResult<Post>> GetPost(long id)
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account.Account;
var userFriends = await rels.ListAccountFriends(currentUser!);
var post = await db.Posts
.Where(e => e.Id == id)
.Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.RepliedPost)
.Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost)
.Include(e => e.Tags)
.Include(e => e.Categories)
.Include(e => e.Attachments)
.FilterWithVisibility(currentUser, userFriends)
.FirstOrDefaultAsync();
if (post is null) return NotFound();
return Ok(post);
}
[HttpGet("{id:long}/replies")]
public async Task<ActionResult<List<Post>>> ListReplies(long id, [FromQuery] int offset = 0,
[FromQuery] int take = 20)
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account.Account;
var userFriends = await rels.ListAccountFriends(currentUser!);
var post = await db.Posts
.Where(e => e.Id == id)
.FirstOrDefaultAsync();
if (post is null) return NotFound();
var totalCount = await db.Posts
.Where(e => e.RepliedPostId == post.Id)
.FilterWithVisibility(currentUser, userFriends, isListing: true)
.CountAsync();
var posts = await db.Posts
.Where(e => e.RepliedPostId == id)
.Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.ThreadedPost)
.Include(e => e.ForwardedPost)
.Include(e => e.Attachments)
.Include(e => e.Categories)
.Include(e => e.Tags)
.FilterWithVisibility(currentUser, userFriends, isListing: true)
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
.Skip(offset)
.Take(take)
.ToListAsync();
Response.Headers["X-Total"] = totalCount.ToString();
return Ok(posts);
}
public class PostRequest
{
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
public string? Content { get; set; }
public PostVisibility? Visibility { get; set; }
public PostType? Type { get; set; }
[MaxLength(16)] public List<string>? Tags { get; set; }
[MaxLength(8)] public List<string>? Categories { get; set; }
[MaxLength(32)] public List<string>? Attachments { get; set; }
public Dictionary<string, object>? Meta { get; set; }
public Instant? PublishedAt { get; set; }
public long? RepliedPostId { get; set; }
public long? ForwardedPostId { get; set; }
}
[HttpPost]
[RequiredPermission("global", "posts.create")]
public async Task<ActionResult<Post>> CreatePost(
[FromBody] PostRequest request,
[FromHeader(Name = "X-Pub")] string? publisherName
)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
Publisher? publisher;
if (publisherName is null)
{
// Use the first personal publisher
publisher = await db.Publishers.FirstOrDefaultAsync(e =>
e.AccountId == currentUser.Id && e.PublisherType == PublisherType.Individual);
}
else
{
publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == publisherName);
if (publisher is null) return BadRequest("Publisher was not found.");
var member =
await db.PublisherMembers.FirstOrDefaultAsync(e =>
e.AccountId == currentUser.Id && e.PublisherId == publisher.Id);
if (member is null) return StatusCode(403, "You even wasn't a member of the publisher you specified.");
if (member.Role < PublisherMemberRole.Editor)
return StatusCode(403, "You need at least be an editor to post as this publisher.");
}
if (publisher is null) return BadRequest("Publisher was not found.");
var post = new Post
{
Title = request.Title,
Description = request.Description,
Content = request.Content,
Visibility = request.Visibility ?? PostVisibility.Public,
PublishedAt = request.PublishedAt,
Type = request.Type ?? PostType.Moment,
Meta = request.Meta,
Publisher = publisher,
};
if (request.RepliedPostId is not null)
{
var repliedPost = await db.Posts.FindAsync(request.RepliedPostId.Value);
if (repliedPost is null) return BadRequest("Post replying to was not found.");
post.RepliedPost = repliedPost;
post.RepliedPostId = repliedPost.Id;
}
if (request.ForwardedPostId is not null)
{
var forwardedPost = await db.Posts.FindAsync(request.ForwardedPostId.Value);
if (forwardedPost is null) return BadRequest("Forwarded post was not found.");
post.ForwardedPost = forwardedPost;
post.ForwardedPostId = forwardedPost.Id;
}
try
{
post = await ps.PostAsync(
currentUser,
post,
attachments: request.Attachments,
tags: request.Tags,
categories: request.Categories
);
}
catch (InvalidOperationException err)
{
return BadRequest(err.Message);
}
return post;
}
[HttpPatch("{id:long}")]
public async Task<ActionResult<Post>> UpdatePost(long id, [FromBody] PostRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var post = await db.Posts
.Where(e => e.Id == id)
.Include(e => e.Publisher)
.Include(e => e.Publisher.Picture)
.Include(e => e.Publisher.Background)
.Include(e => e.Attachments)
.Include(e => e.Categories)
.Include(e => e.Tags)
.FirstOrDefaultAsync();
if (post is null) return NotFound();
var member = await db.PublisherMembers
.FirstOrDefaultAsync(e => e.AccountId == currentUser.Id && e.PublisherId == post.Publisher.Id);
if (member is null) return StatusCode(403, "You even wasn't a member of the publisher you specified.");
if (member.Role < PublisherMemberRole.Editor)
return StatusCode(403, "You need at least be an editor to edit this publisher's post.");
if (request.Title is not null) post.Title = request.Title;
if (request.Description is not null) post.Description = request.Description;
if (request.Content is not null) post.Content = request.Content;
if (request.Visibility is not null) post.Visibility = request.Visibility.Value;
if (request.Type is not null) post.Type = request.Type.Value;
if (request.Meta is not null) post.Meta = request.Meta;
try
{
post = await ps.UpdatePostAsync(
post,
attachments: request.Attachments,
tags: request.Tags,
categories: request.Categories,
publishedAt: request.PublishedAt
);
}
catch (InvalidOperationException err)
{
return BadRequest(err.Message);
}
return Ok(post);
}
[HttpDelete("{id:long}")]
public async Task<ActionResult<Post>> DeletePost(long id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var post = await db.Posts
.Where(e => e.Id == id)
.Include(e => e.Publisher)
.Include(e => e.Attachments)
.FirstOrDefaultAsync();
if (post is null) return NotFound();
var member = await db.PublisherMembers
.FirstOrDefaultAsync(e => e.AccountId == currentUser.Id && e.PublisherId == post.Publisher.Id);
if (member is null) return StatusCode(403, "You even wasn't a member of the publisher you specified.");
if (member.Role < PublisherMemberRole.Editor)
return StatusCode(403, "You need at least be an editor to delete the publisher's post.");
await ps.DeletePostAsync(post);
return NoContent();
}
}