Remove Casbin package references, configurations, and unused imports across multiple files. This change simplifies the codebase by eliminating unnecessary dependencies and reducing complexity. ✨ add new chat features and improve message handling Introduce new chat features including message notifications, nicknames, and improved message handling. Enhance the WebSocket service to support new packet handlers and improve message delivery. 🗃️ add new migrations for chat-related changes Add new migrations to support the latest chat features, including changes to chat members, messages, and reactions. These migrations ensure the database schema is up-to-date with the latest code changes.
272 lines
10 KiB
C#
272 lines
10 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using System.Text.Json;
|
|
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 = currentUser is null ? [] : 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();
|
|
posts = PostService.TruncatePostContent(posts);
|
|
|
|
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 = currentUser is null ? [] : 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 = currentUser is null ? [] : 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();
|
|
posts = PostService.TruncatePostContent(posts);
|
|
|
|
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 JsonDocument? 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();
|
|
}
|
|
} |