250 lines
8 KiB
C#
250 lines
8 KiB
C#
using Foxnouns.Backend.Database;
|
|
using Foxnouns.Backend.Database.Models;
|
|
using Foxnouns.Backend.Middleware;
|
|
using Foxnouns.Backend.Services;
|
|
using Foxnouns.Backend.Utils;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Foxnouns.Backend.Controllers.Moderation;
|
|
|
|
[Route("/api/v2/support")]
|
|
public class SupportController(
|
|
ILogger logger,
|
|
DatabaseContext db,
|
|
SupportRendererService supportRenderer,
|
|
ISnowflakeGenerator snowflakeGenerator
|
|
) : ApiControllerBase
|
|
{
|
|
private readonly ILogger _logger = logger.ForContext<SupportController>();
|
|
|
|
[HttpGet]
|
|
[ProducesResponseType<IEnumerable<SupportRendererService.ConversationResponse>>(
|
|
statusCode: StatusCodes.Status200OK
|
|
)]
|
|
[Authorize(":moderator")]
|
|
public async Task<IActionResult> GetOpenConversationsAsync(CancellationToken ct = default)
|
|
{
|
|
var conversations = await db
|
|
.SupportConversations.Where(c => c.Status == SupportConversationStatus.Open)
|
|
.OrderBy(c => c.Id)
|
|
.ToListAsync(ct);
|
|
|
|
return Ok(conversations.Select(supportRenderer.ToResponse));
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
[ProducesResponseType<SupportRendererService.ConversationResponse>(
|
|
statusCode: StatusCodes.Status200OK
|
|
)]
|
|
[Authorize("user.support")]
|
|
public async Task<IActionResult> GetConversationAsync(
|
|
Snowflake id,
|
|
CancellationToken ct = default
|
|
)
|
|
{
|
|
var (_, conversation) = await ResolveConversationAsync(id, ct);
|
|
|
|
return Ok(supportRenderer.ToResponse(conversation));
|
|
}
|
|
|
|
[HttpGet("{id}/messages")]
|
|
[ProducesResponseType<SupportRendererService.ConversationResponse>(
|
|
statusCode: StatusCodes.Status200OK
|
|
)]
|
|
[Authorize("user.support")]
|
|
public async Task<IActionResult> GetConversationMessagesAsync(
|
|
Snowflake id,
|
|
CancellationToken ct = default
|
|
)
|
|
{
|
|
var (isModerator, conversation) = await ResolveConversationAsync(id, ct);
|
|
|
|
var messages = await db
|
|
.SupportMessages.Where(m => m.ConversationId == conversation.Id)
|
|
.OrderBy(m => m.Id)
|
|
.ToListAsync(ct);
|
|
|
|
return Ok(messages.Select(m => supportRenderer.ToResponse(m, modView: isModerator)));
|
|
}
|
|
|
|
private const int MaxOpenConversations = 5;
|
|
private const int MinTitleLength = 5;
|
|
private const int MaxTitleLength = 512;
|
|
private const int MaxMessageLength = 4096;
|
|
|
|
[HttpPost]
|
|
[ProducesResponseType<SupportRendererService.ConversationResponse>(
|
|
statusCode: StatusCodes.Status200OK
|
|
)]
|
|
[Authorize("user.support")]
|
|
public async Task<IActionResult> CreateConversationAsync(
|
|
[FromBody] CreateConversationRequest req
|
|
)
|
|
{
|
|
var openCount = await db
|
|
.SupportConversations.Where(c =>
|
|
c.UserId == CurrentUser!.Id && c.Status == SupportConversationStatus.Open
|
|
)
|
|
.CountAsync();
|
|
if (openCount >= MaxOpenConversations)
|
|
throw new ApiError.BadRequest("Too many open conversations.");
|
|
|
|
req.Title.ValidateStringLength("title", MinTitleLength, MaxTitleLength);
|
|
req.InitialMessage.Content?.ValidateStringLength(
|
|
"initial_message.content",
|
|
0,
|
|
MaxMessageLength
|
|
);
|
|
|
|
var conversation = new SupportConversation
|
|
{
|
|
Id = snowflakeGenerator.GenerateSnowflake(),
|
|
User = CurrentUser!,
|
|
Title = req.Title,
|
|
};
|
|
|
|
var message = new SupportMessage
|
|
{
|
|
Id = snowflakeGenerator.GenerateSnowflake(),
|
|
ConversationId = conversation.Id,
|
|
UserId = CurrentUser!.Id,
|
|
IsAnonymous = false,
|
|
Content = req.InitialMessage.Content,
|
|
};
|
|
|
|
db.Add(conversation);
|
|
db.Add(message);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok(supportRenderer.ToResponse(conversation));
|
|
}
|
|
|
|
[HttpPost("{id}/messages")]
|
|
public async Task<IActionResult> CreateMessageAsync(
|
|
Snowflake id,
|
|
[FromBody] CreateMessageRequest req
|
|
)
|
|
{
|
|
var (isModerator, conversation) = await ResolveConversationAsync(id);
|
|
|
|
req.Content?.ValidateStringLength("content", 0, MaxMessageLength);
|
|
if (req.IsAnonymous && !isModerator)
|
|
{
|
|
throw new ApiError.BadRequest(
|
|
"Only moderators can mark a message as anonymous.",
|
|
"is_anonymous",
|
|
true
|
|
);
|
|
}
|
|
|
|
var message = new SupportMessage
|
|
{
|
|
Id = snowflakeGenerator.GenerateSnowflake(),
|
|
ConversationId = conversation.Id,
|
|
UserId = CurrentUser!.Id,
|
|
IsAnonymous = req.IsAnonymous,
|
|
Content = req.Content,
|
|
};
|
|
|
|
db.Add(message);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok(supportRenderer.ToResponse(message, modView: isModerator));
|
|
}
|
|
|
|
private async Task<(
|
|
bool IsModerator,
|
|
SupportConversation Conversation
|
|
)> ResolveConversationAsync(Snowflake id, CancellationToken ct = default)
|
|
{
|
|
var isModerator = CurrentUser!.Role is UserRole.Moderator or UserRole.Admin;
|
|
|
|
var conversation = await db.SupportConversations.FindAsync([id], ct);
|
|
if (conversation == null)
|
|
throw new ApiError.NotFound("Conversation not found");
|
|
if (!isModerator && conversation.UserId != CurrentUser.Id)
|
|
throw new ApiError.NotFound("Conversation not found");
|
|
|
|
return (isModerator, conversation);
|
|
}
|
|
|
|
[HttpPatch("{id}")]
|
|
[ProducesResponseType<SupportRendererService.ConversationResponse>(
|
|
statusCode: StatusCodes.Status200OK
|
|
)]
|
|
[Authorize("user.support")]
|
|
public async Task<IActionResult> UpdateConversationAsync(
|
|
Snowflake id,
|
|
[FromBody] UpdateConversationRequest req
|
|
)
|
|
{
|
|
var isModerator = CurrentUser!.Role is UserRole.Moderator or UserRole.Admin;
|
|
|
|
var conversation = await db.SupportConversations.FindAsync(id);
|
|
if (conversation == null)
|
|
throw new ApiError.NotFound("Conversation not found");
|
|
if (!isModerator && conversation.UserId != CurrentUser.Id)
|
|
throw new ApiError.NotFound("Conversation not found");
|
|
|
|
if (
|
|
!isModerator
|
|
&& (
|
|
req.AssignedTo != null
|
|
|| (req.Status != null && req.Status != SupportConversationStatus.Closed)
|
|
)
|
|
)
|
|
{
|
|
throw new ApiError.Forbidden("You can't change those options.");
|
|
}
|
|
|
|
if (req.Title != null)
|
|
{
|
|
req.Title.ValidateStringLength("title", MinTitleLength, MaxTitleLength);
|
|
conversation.Title = req.Title;
|
|
}
|
|
|
|
if (req.HasProperty(nameof(req.AssignedTo)))
|
|
{
|
|
if (req.AssignedTo == null)
|
|
conversation.AssignedToId = null;
|
|
else
|
|
{
|
|
var assignedToUser = await db.ResolveUserAsync(req.AssignedTo.Value);
|
|
if (assignedToUser.Role is not (UserRole.Admin or UserRole.Moderator))
|
|
{
|
|
throw new ApiError.BadRequest(
|
|
"That user is not a moderator.",
|
|
"assigned_to",
|
|
req.AssignedTo
|
|
);
|
|
}
|
|
|
|
conversation.AssignedToId = assignedToUser.Id;
|
|
}
|
|
}
|
|
|
|
if (req.Status != null)
|
|
conversation.Status = req.Status.Value;
|
|
|
|
db.Update(conversation);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok(supportRenderer.ToResponse(conversation));
|
|
}
|
|
|
|
public record CreateConversationRequest(string Title, CreateMessageRequest InitialMessage);
|
|
|
|
public record CreateMessageRequest(string? Content, bool IsAnonymous = false);
|
|
|
|
public class UpdateConversationRequest : PatchRequest
|
|
{
|
|
public string? Title { get; init; }
|
|
public Snowflake? AssignedTo { get; init; }
|
|
|
|
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
|
public SupportConversationStatus? Status { get; init; }
|
|
}
|
|
}
|