From 46d5fdc1afa4818dee23b22c818bf03322893e47 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 23 May 2024 23:11:51 +0200 Subject: [PATCH] [backend/api] Add user follow, unfollow & lookup api methods (ISH-346) --- .../Controllers/UserController.cs | 81 +++++++++++++++++-- .../ControllerModels/UserControllerModel.cs | 9 +++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/Iceshrimp.Backend/Controllers/UserController.cs b/Iceshrimp.Backend/Controllers/UserController.cs index 1246218c..1cb1e31a 100644 --- a/Iceshrimp.Backend/Controllers/UserController.cs +++ b/Iceshrimp.Backend/Controllers/UserController.cs @@ -7,6 +7,7 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Middleware; +using Iceshrimp.Backend.Core.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; @@ -17,17 +18,18 @@ namespace Iceshrimp.Backend.Controllers; [Authenticate] [Authorize] [EnableRateLimiting("sliding")] -[Route("/api/iceshrimp/users/{id}")] +[Route("/api/iceshrimp/users")] [Produces(MediaTypeNames.Application.Json)] public class UserController( DatabaseContext db, UserRenderer userRenderer, NoteRenderer noteRenderer, UserProfileRenderer userProfileRenderer, - ActivityPub.UserResolver userResolver + ActivityPub.UserResolver userResolver, + UserService userSvc ) : ControllerBase { - [HttpGet] + [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] public async Task GetUser(string id) @@ -39,12 +41,27 @@ public class UserController( return Ok(await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user))); } - [HttpGet("profile")] + [HttpGet("lookup")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] + public async Task LookupUser([FromQuery] string username, [FromQuery] string? host) + { + username = username.ToLowerInvariant(); + host = host?.ToLowerInvariant(); + + var user = await db.Users.IncludeCommonProperties() + .FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == host) ?? + throw GracefulException.NotFound("User not found"); + + return Ok(await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user))); + } + + [HttpGet("{id}/profile")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserProfileResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] public async Task GetUserProfile(string id) { - var localUser = HttpContext.GetUser(); + var localUser = HttpContext.GetUserOrFail(); var user = await db.Users.IncludeCommonProperties() .FirstOrDefaultAsync(p => p.Id == id) ?? throw GracefulException.NotFound("User not found"); @@ -52,13 +69,13 @@ public class UserController( return Ok(await userProfileRenderer.RenderOne(await userResolver.GetUpdatedUser(user), localUser)); } - [HttpGet("notes")] + [HttpGet("{id}/notes")] [LinkPagination(20, 80)] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] public async Task GetUserNotes(string id, PaginationQuery pq) { - var localUser = HttpContext.GetUser(); + var localUser = HttpContext.GetUserOrFail(); var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ?? throw GracefulException.NotFound("User not found"); @@ -74,4 +91,54 @@ public class UserController( return Ok(await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), localUser, Filter.FilterContext.Accounts)); } + + [HttpPost("{id}/follow")] + [Authenticate] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] + public async Task FollowUser(string id) + { + var user = HttpContext.GetUserOrFail(); + if (user.Id == id) + throw GracefulException.BadRequest("You cannot follow yourself"); + + var followee = await db.Users.IncludeCommonProperties() + .Where(p => p.Id == id) + .PrecomputeRelationshipData(user) + .FirstOrDefaultAsync() ?? + throw GracefulException.RecordNotFound(); + + if ((followee.PrecomputedIsBlockedBy ?? true) || (followee.PrecomputedIsBlocking ?? true)) + throw GracefulException.Forbidden("This action is not allowed"); + + if (!(followee.PrecomputedIsFollowedBy ?? false) && !(followee.PrecomputedIsRequestedBy ?? false)) + await userSvc.FollowUserAsync(user, followee); + + return Ok(); + } + + [HttpPost("{id}/unfollow")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] + public async Task UnfollowUser(string id) + { + var user = HttpContext.GetUserOrFail(); + if (user.Id == id) + throw GracefulException.BadRequest("You cannot unfollow yourself"); + + var followee = await db.Users + .Where(p => p.Id == id) + .IncludeCommonProperties() + .PrecomputeRelationshipData(user) + .FirstOrDefaultAsync() ?? + throw GracefulException.RecordNotFound(); + + await userSvc.UnfollowUserAsync(user, followee); + + return Ok(); + } } \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/ControllerModels/UserControllerModel.cs b/Iceshrimp.Frontend/Core/ControllerModels/UserControllerModel.cs index 7951930c..e203555a 100644 --- a/Iceshrimp.Frontend/Core/ControllerModels/UserControllerModel.cs +++ b/Iceshrimp.Frontend/Core/ControllerModels/UserControllerModel.cs @@ -1,6 +1,7 @@ using Iceshrimp.Frontend.Core.Miscellaneous; using Iceshrimp.Frontend.Core.Services; using Iceshrimp.Shared.Schemas; +using Microsoft.AspNetCore.Http; namespace Iceshrimp.Frontend.Core.ControllerModels; @@ -15,4 +16,12 @@ internal class UserControllerModel(ApiClient api) [LinkPagination(20, 80)] public Task?> GetUserNotes(string id, PaginationQuery pq) => api.CallNullable>(HttpMethod.Get, $"/users/{id}/notes", pq); + + public Task LookupUser(string username, string? host) + { + var query = new QueryString(); + query.Add("username", username); + if (host != null) query.Add("host", host); + return api.CallNullable(HttpMethod.Get, "/users/lookup", query); + } } \ No newline at end of file