// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using XidNet; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] public class FlagsController( DatabaseContext db, UserRendererService userRenderer, ISnowflakeGenerator snowflakeGenerator, IQueue queue ) : ApiControllerBase { [HttpGet] [Limit(UsableByDeletedUsers = true)] [Authorize("user.read_flags")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) { List flags = await db .PrideFlags.Where(f => f.UserId == CurrentUser!.Id) .OrderBy(f => f.Name) .ThenBy(f => f.Id) .ToListAsync(ct); return Ok(flags.Select(userRenderer.RenderPrideFlag)); } public const int MaxFlagCount = 500; [HttpPost] [Authorize("user.update_flags")] [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public async Task CreateFlagAsync([FromBody] CreateFlagRequest req) { int flagCount = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).CountAsync(); if (flagCount >= MaxFlagCount) throw new ApiError.BadRequest("Maximum number of flags reached"); ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); var flag = new PrideFlag { Id = snowflakeGenerator.GenerateSnowflake(), LegacyId = Xid.NewXid().ToString(), UserId = CurrentUser!.Id, Name = req.Name, Description = req.Description, }; db.Add(flag); await db.SaveChangesAsync(); queue.QueueInvocableWithPayload( new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image) ); return Accepted(userRenderer.RenderPrideFlag(flag)); } [HttpPatch("{id}")] [Authorize("user.create_flags")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id ); if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); if (req.Name != null) flag.Name = req.Name; if (req.HasProperty(nameof(req.Description))) flag.Description = req.Description; db.Update(flag); await db.SaveChangesAsync(); return Ok(userRenderer.RenderPrideFlag(flag)); } [HttpDelete("{id}")] [Authorize("user.update_flags")] public async Task DeleteFlagAsync(Snowflake id) { PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id ); if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); db.PrideFlags.Remove(flag); await db.SaveChangesAsync(); return NoContent(); } private static List<(string, ValidationError?)> ValidateFlag( string? name, string? description, string? imageData ) { var errors = new List<(string, ValidationError?)>(); if (name != null) { switch (name.Length) { case < 1: errors.Add( ( "name", ValidationError.LengthError("Name is too short", 1, 100, name.Length) ) ); break; case > 100: errors.Add( ( "name", ValidationError.LengthError("Name is too long", 1, 100, name.Length) ) ); break; } } if (description != null) { switch (description.Length) { case < 1: errors.Add( ( "description", ValidationError.LengthError( "Description is too short", 1, 100, description.Length ) ) ); break; case > 500: errors.Add( ( "description", ValidationError.LengthError( "Description is too long", 1, 100, description.Length ) ) ); break; } } if (imageData != null) { switch (imageData.Length) { case 0: errors.Add( ( "image", ValidationError.GenericValidationError("Image cannot be empty", null) ) ); break; case > 1_500_000: errors.Add( ( "image", ValidationError.GenericValidationError("Image is too large", null) ) ); break; } } return errors; } }