236 lines
No EOL
7.7 KiB
C#
236 lines
No EOL
7.7 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Net;
|
|
using System.Net.Mime;
|
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
using Iceshrimp.Backend.Core.Database;
|
|
using Iceshrimp.Backend.Core.Database.Tables;
|
|
using Iceshrimp.Backend.Core.Extensions;
|
|
using Iceshrimp.Backend.Core.Helpers;
|
|
using Iceshrimp.Backend.Core.Middleware;
|
|
using Iceshrimp.Backend.Core.Services;
|
|
using Microsoft.AspNetCore.Cors;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|
|
|
[MastodonApiController]
|
|
[Route("/api/v1/lists")]
|
|
[Authenticate]
|
|
[EnableRateLimiting("sliding")]
|
|
[EnableCors("mastodon")]
|
|
[Produces(MediaTypeNames.Application.Json)]
|
|
public class ListController(DatabaseContext db, UserRenderer userRenderer, EventService eventSvc) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
[Authorize("read:lists")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
public async Task<IEnumerable<ListEntity>> GetLists()
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
|
|
return await db.UserLists
|
|
.Where(p => p.User == user)
|
|
.Select(p => new ListEntity
|
|
{
|
|
Id = p.Id,
|
|
Title = p.Name,
|
|
Exclusive = p.HideFromHomeTl
|
|
})
|
|
.ToListAsync();
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
[Authorize("read:lists")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
public async Task<ListEntity> GetList(string id)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
|
|
return await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.Select(p => new ListEntity
|
|
{
|
|
Id = p.Id,
|
|
Title = p.Name,
|
|
Exclusive = p.HideFromHomeTl
|
|
})
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
}
|
|
|
|
[HttpPost]
|
|
[Authorize("write:lists")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.UnprocessableEntity)]
|
|
public async Task<ListEntity> CreateList([FromHybrid] ListSchemas.ListCreationRequest request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Title))
|
|
throw GracefulException.UnprocessableEntity("Validation failed: Title can't be blank");
|
|
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = new UserList
|
|
{
|
|
Id = IdHelpers.GenerateSnowflakeId(),
|
|
CreatedAt = DateTime.UtcNow,
|
|
User = user,
|
|
Name = request.Title,
|
|
HideFromHomeTl = request.Exclusive
|
|
};
|
|
|
|
await db.AddAsync(list);
|
|
await db.SaveChangesAsync();
|
|
|
|
return new ListEntity
|
|
{
|
|
Id = list.Id,
|
|
Title = list.Name,
|
|
Exclusive = list.HideFromHomeTl
|
|
};
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
[Authorize("write:lists")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound, HttpStatusCode.UnprocessableEntity)]
|
|
public async Task<ListEntity> UpdateList(string id, [FromHybrid] ListSchemas.ListUpdateRequest request)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
if (request.Title != null && string.IsNullOrWhiteSpace(request.Title))
|
|
throw GracefulException.UnprocessableEntity("Validation failed: Title can't be blank");
|
|
|
|
list.Name = request.Title ?? list.Name;
|
|
list.HideFromHomeTl = request.Exclusive ?? list.HideFromHomeTl;
|
|
|
|
db.Update(list);
|
|
await db.SaveChangesAsync();
|
|
|
|
return new ListEntity
|
|
{
|
|
Id = list.Id,
|
|
Title = list.Name,
|
|
Exclusive = list.HideFromHomeTl
|
|
};
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
[Authorize("write:lists")]
|
|
[OverrideResultType<object>]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
public async Task<object> DeleteList(string id)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
db.Remove(list);
|
|
await db.SaveChangesAsync();
|
|
eventSvc.RaiseListMembersUpdated(this, list);
|
|
return new object();
|
|
}
|
|
|
|
[LinkPagination(40, 80)]
|
|
[HttpGet("{id}/accounts")]
|
|
[Authorize("read:lists")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
public async Task<List<AccountEntity>> GetListMembers(string id, MastodonPaginationQuery pq)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
return pq.Limit == 0
|
|
? await db.UserListMembers
|
|
.Where(p => p.UserList == list)
|
|
.Include(p => p.User.UserProfile)
|
|
.Select(p => p.User)
|
|
.RenderAllForMastodonAsync(userRenderer, user)
|
|
: await db.UserListMembers
|
|
.Where(p => p.UserList == list)
|
|
.Paginate(pq, ControllerContext)
|
|
.Include(p => p.User.UserProfile)
|
|
.Select(p => p.User)
|
|
.RenderAllForMastodonAsync(userRenderer, user);
|
|
}
|
|
|
|
[HttpPost("{id}/accounts")]
|
|
[Authorize("write:lists")]
|
|
[OverrideResultType<object>]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound, HttpStatusCode.UnprocessableEntity)]
|
|
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
|
public async Task<object> AddListMember(string id, [FromHybrid] ListSchemas.ListUpdateMembersRequest request)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
var subjects = await db.Users.Where(p => request.AccountIds.Contains(p.Id) && p.IsFollowedBy(user))
|
|
.Select(p => p.Id)
|
|
.ToListAsync();
|
|
|
|
if (subjects.Count == 0 || subjects.Count != request.AccountIds.Count)
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
if (await db.UserListMembers.AnyAsync(p => subjects.Contains(p.UserId) && p.UserList == list))
|
|
throw GracefulException.UnprocessableEntity("Validation failed: Account has already been taken");
|
|
|
|
var memberships = subjects.Select(subject => new UserListMember
|
|
{
|
|
Id = IdHelpers.GenerateSnowflakeId(),
|
|
CreatedAt = DateTime.UtcNow,
|
|
UserList = list,
|
|
UserId = subject
|
|
});
|
|
|
|
await db.AddRangeAsync(memberships);
|
|
await db.SaveChangesAsync();
|
|
|
|
eventSvc.RaiseListMembersUpdated(this, list);
|
|
|
|
return new object();
|
|
}
|
|
|
|
[HttpDelete("{id}/accounts")]
|
|
[Authorize("write:lists")]
|
|
[OverrideResultType<object>]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
public async Task<object> RemoveListMember(
|
|
string id, [FromHybrid] ListSchemas.ListUpdateMembersRequest request
|
|
)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var list = await db.UserLists
|
|
.Where(p => p.User == user && p.Id == id)
|
|
.FirstOrDefaultAsync() ??
|
|
throw GracefulException.RecordNotFound();
|
|
|
|
await db.UserListMembers
|
|
.Where(p => p.UserList == list && request.AccountIds.Contains(p.UserId))
|
|
.ExecuteDeleteAsync();
|
|
|
|
eventSvc.RaiseListMembersUpdated(this, list);
|
|
|
|
return new object();
|
|
}
|
|
} |