2024-09-03 16:29:51 +02:00
|
|
|
using Coravel.Queuing.Interfaces;
|
2024-07-14 21:25:23 +02:00
|
|
|
using EntityFramework.Exceptions.Common;
|
2024-07-13 19:38:40 +02:00
|
|
|
using Foxnouns.Backend.Database;
|
|
|
|
using Foxnouns.Backend.Database.Models;
|
2024-09-03 16:29:51 +02:00
|
|
|
using Foxnouns.Backend.Extensions;
|
2024-07-14 21:25:23 +02:00
|
|
|
using Foxnouns.Backend.Jobs;
|
2024-07-13 19:38:40 +02:00
|
|
|
using Foxnouns.Backend.Middleware;
|
|
|
|
using Foxnouns.Backend.Services;
|
2024-07-14 21:25:23 +02:00
|
|
|
using Foxnouns.Backend.Utils;
|
2024-07-13 19:38:40 +02:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2024-07-14 21:41:16 +02:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2024-09-26 16:38:43 +02:00
|
|
|
using NodaTime;
|
2024-07-13 19:38:40 +02:00
|
|
|
|
|
|
|
namespace Foxnouns.Backend.Controllers;
|
|
|
|
|
|
|
|
[Route("/api/v2/users/{userRef}/members")]
|
|
|
|
public class MembersController(
|
|
|
|
ILogger logger,
|
|
|
|
DatabaseContext db,
|
2024-09-09 14:50:00 +02:00
|
|
|
MemberRendererService memberRenderer,
|
2024-07-14 21:41:16 +02:00
|
|
|
ISnowflakeGenerator snowflakeGenerator,
|
2024-09-09 14:50:00 +02:00
|
|
|
ObjectStorageService objectStorageService,
|
2024-09-26 16:38:43 +02:00
|
|
|
IQueue queue,
|
|
|
|
IClock clock) : ApiControllerBase
|
2024-07-13 19:38:40 +02:00
|
|
|
{
|
|
|
|
private readonly ILogger _logger = logger.ForContext<MembersController>();
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
[ProducesResponseType<IEnumerable<MemberRendererService.PartialMember>>(StatusCodes.Status200OK)]
|
2024-09-09 14:37:59 +02:00
|
|
|
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
|
2024-07-13 19:38:40 +02:00
|
|
|
{
|
2024-09-09 14:37:59 +02:00
|
|
|
var user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
2024-09-09 14:50:00 +02:00
|
|
|
return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken));
|
2024-07-13 19:38:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
[HttpGet("{memberRef}")]
|
|
|
|
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)]
|
2024-09-09 14:37:59 +02:00
|
|
|
public async Task<IActionResult> GetMemberAsync(string userRef, string memberRef, CancellationToken ct = default)
|
2024-07-13 19:38:40 +02:00
|
|
|
{
|
2024-09-09 14:37:59 +02:00
|
|
|
var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct);
|
2024-09-09 14:50:00 +02:00
|
|
|
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
2024-07-13 19:38:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
[HttpPost("/api/v2/users/@me/members")]
|
|
|
|
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)]
|
|
|
|
[Authorize("member.create")]
|
2024-09-25 19:48:05 +02:00
|
|
|
public async Task<IActionResult> CreateMemberAsync([FromBody] CreateMemberRequest req,
|
|
|
|
CancellationToken ct = default)
|
2024-07-13 19:38:40 +02:00
|
|
|
{
|
2024-07-14 21:25:23 +02:00
|
|
|
ValidationUtils.Validate([
|
|
|
|
("name", ValidationUtils.ValidateMemberName(req.Name)),
|
|
|
|
("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)),
|
|
|
|
("bio", ValidationUtils.ValidateBio(req.Bio)),
|
2024-08-22 15:13:46 +02:00
|
|
|
("avatar", ValidationUtils.ValidateAvatar(req.Avatar)),
|
2024-09-26 15:08:08 +02:00
|
|
|
.. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
|
|
|
.. ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names"),
|
|
|
|
.. ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences)
|
2024-07-14 21:25:23 +02:00
|
|
|
]);
|
2024-07-13 19:38:40 +02:00
|
|
|
|
|
|
|
var member = new Member
|
|
|
|
{
|
|
|
|
Id = snowflakeGenerator.GenerateSnowflake(),
|
2024-07-14 21:25:23 +02:00
|
|
|
User = CurrentUser!,
|
2024-07-13 19:38:40 +02:00
|
|
|
Name = req.Name,
|
2024-07-14 21:25:23 +02:00
|
|
|
DisplayName = req.DisplayName,
|
|
|
|
Bio = req.Bio,
|
2024-08-22 15:13:46 +02:00
|
|
|
Fields = req.Fields ?? [],
|
|
|
|
Names = req.Names ?? [],
|
|
|
|
Pronouns = req.Pronouns ?? [],
|
2024-07-14 21:25:23 +02:00
|
|
|
Unlisted = req.Unlisted ?? false
|
2024-07-13 19:38:40 +02:00
|
|
|
};
|
|
|
|
db.Add(member);
|
|
|
|
|
|
|
|
_logger.Debug("Creating member {MemberName} ({Id}) for {UserId}", member.Name, member.Id, CurrentUser!.Id);
|
|
|
|
|
2024-07-14 21:25:23 +02:00
|
|
|
try
|
|
|
|
{
|
2024-09-09 14:37:59 +02:00
|
|
|
await db.SaveChangesAsync(ct);
|
2024-07-14 21:25:23 +02:00
|
|
|
}
|
|
|
|
catch (UniqueConstraintException)
|
|
|
|
{
|
|
|
|
_logger.Debug("Could not create member {Id} due to name conflict", member.Id);
|
|
|
|
throw new ApiError.BadRequest("A member with that name already exists", "name", req.Name);
|
|
|
|
}
|
|
|
|
|
2024-09-03 16:29:51 +02:00
|
|
|
if (req.Avatar != null)
|
|
|
|
queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>(
|
|
|
|
new AvatarUpdatePayload(member.Id, req.Avatar));
|
2024-07-13 19:38:40 +02:00
|
|
|
|
2024-09-09 14:50:00 +02:00
|
|
|
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
2024-07-13 19:38:40 +02:00
|
|
|
}
|
|
|
|
|
2024-07-14 21:41:16 +02:00
|
|
|
[HttpDelete("/api/v2/users/@me/members/{memberRef}")]
|
|
|
|
[Authorize("member.update")]
|
2024-09-14 16:37:52 +02:00
|
|
|
public async Task<IActionResult> DeleteMemberAsync(string memberRef)
|
2024-07-14 21:41:16 +02:00
|
|
|
{
|
2024-09-14 16:37:52 +02:00
|
|
|
var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
2024-07-14 21:41:16 +02:00
|
|
|
var deleteCount = await db.Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id)
|
2024-09-14 16:37:52 +02:00
|
|
|
.ExecuteDeleteAsync();
|
2024-07-14 21:41:16 +02:00
|
|
|
if (deleteCount == 0)
|
|
|
|
{
|
|
|
|
_logger.Warning("Successfully resolved member {Id} but could not delete them", member.Id);
|
|
|
|
return NoContent();
|
|
|
|
}
|
|
|
|
|
2024-09-09 14:50:00 +02:00
|
|
|
if (member.Avatar != null) await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar);
|
2024-07-14 21:41:16 +02:00
|
|
|
return NoContent();
|
|
|
|
}
|
|
|
|
|
2024-08-22 15:13:46 +02:00
|
|
|
public record CreateMemberRequest(
|
|
|
|
string Name,
|
|
|
|
string? DisplayName,
|
|
|
|
string? Bio,
|
|
|
|
string? Avatar,
|
|
|
|
bool? Unlisted,
|
|
|
|
List<FieldEntry>? Names,
|
|
|
|
List<Pronoun>? Pronouns,
|
|
|
|
List<Field>? Fields);
|
2024-09-26 16:38:43 +02:00
|
|
|
|
|
|
|
[HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")]
|
|
|
|
[Authorize("member.update")]
|
|
|
|
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)]
|
|
|
|
public async Task<IActionResult> RerollSidAsync(string memberRef)
|
|
|
|
{
|
|
|
|
var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
|
|
|
|
|
|
|
var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1);
|
|
|
|
if (CurrentUser!.LastSidReroll > minTimeAgo)
|
|
|
|
throw new ApiError.BadRequest("Cannot reroll short ID yet");
|
|
|
|
|
2024-09-26 17:09:27 +02:00
|
|
|
// Using ExecuteUpdateAsync here as the new short ID is generated by the database
|
2024-09-26 16:38:43 +02:00
|
|
|
await db.Members.Where(m => m.Id == member.Id)
|
|
|
|
.ExecuteUpdateAsync(s => s
|
|
|
|
.SetProperty(m => m.Sid, _ => db.FindFreeMemberSid()));
|
|
|
|
|
2024-09-26 17:09:27 +02:00
|
|
|
await db.Users.Where(u => u.Id == CurrentUser.Id)
|
|
|
|
.ExecuteUpdateAsync(s => s
|
|
|
|
.SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant())
|
|
|
|
.SetProperty(u => u.LastActive, clock.GetCurrentInstant()));
|
2024-09-26 16:38:43 +02:00
|
|
|
|
2024-09-26 17:09:27 +02:00
|
|
|
// Re-fetch member to fetch the new sid
|
|
|
|
var updatedMember = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
2024-09-26 16:38:43 +02:00
|
|
|
return Ok(memberRenderer.RenderMember(updatedMember, CurrentToken));
|
|
|
|
}
|
2024-07-13 19:38:40 +02:00
|
|
|
}
|