| 
									
										
										
										
											2024-08-22 15:13:46 +02:00
										 |  |  | using System.Diagnostics.CodeAnalysis; | 
					
						
							| 
									
										
										
										
											2024-09-03 16:29:51 +02:00
										 |  |  | using Coravel.Queuing.Interfaces; | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | using Foxnouns.Backend.Database; | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  | using Foxnouns.Backend.Database.Models; | 
					
						
							| 
									
										
										
										
											2024-07-08 19:03:04 +02:00
										 |  |  | using Foxnouns.Backend.Jobs; | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | using Foxnouns.Backend.Middleware; | 
					
						
							| 
									
										
										
										
											2024-05-28 17:09:50 +02:00
										 |  |  | using Foxnouns.Backend.Services; | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  | using Foxnouns.Backend.Utils; | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | using Microsoft.AspNetCore.Mvc; | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  | using Microsoft.EntityFrameworkCore; | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | namespace Foxnouns.Backend.Controllers; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [Route("/api/v2/users")] | 
					
						
							| 
									
										
										
										
											2024-08-22 15:13:46 +02:00
										 |  |  | public class UsersController( | 
					
						
							|  |  |  |     DatabaseContext db, | 
					
						
							|  |  |  |     UserRendererService userRendererService, | 
					
						
							| 
									
										
										
										
											2024-09-03 16:29:51 +02:00
										 |  |  |     ISnowflakeGenerator snowflakeGenerator, | 
					
						
							|  |  |  |     IQueue queue) : ApiControllerBase | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | { | 
					
						
							|  |  |  |     [HttpGet("{userRef}")] | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | 
					
						
							| 
									
										
										
										
											2024-07-08 19:03:04 +02:00
										 |  |  |     public async Task<IActionResult> GetUserAsync(string userRef) | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-07-13 19:38:40 +02:00
										 |  |  |         var user = await db.ResolveUserAsync(userRef, CurrentToken); | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |         return await GetUserInnerAsync(user); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private async Task<IActionResult> GetUserInnerAsync(User user) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return Ok(await userRendererService.RenderUserAsync( | 
					
						
							|  |  |  |             user, | 
					
						
							|  |  |  |             selfUser: CurrentUser, | 
					
						
							|  |  |  |             token: CurrentToken, | 
					
						
							|  |  |  |             renderMembers: true, | 
					
						
							|  |  |  |             renderAuthMethods: true | 
					
						
							|  |  |  |         )); | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [HttpPatch("@me")] | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |     [Authorize("user.update")] | 
					
						
							|  |  |  |     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | 
					
						
							| 
									
										
										
										
											2024-07-08 19:03:04 +02:00
										 |  |  |     public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserRequest req) | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |         await using var tx = await db.Database.BeginTransactionAsync(); | 
					
						
							|  |  |  |         var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id); | 
					
						
							| 
									
										
										
										
											2024-07-14 16:44:41 +02:00
										 |  |  |         var errors = new List<(string, ValidationError?)>(); | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (req.Username != null && req.Username != user.Username) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-07-14 16:44:41 +02:00
										 |  |  |             errors.Add(("username", ValidationUtils.ValidateUsername(req.Username))); | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |             user.Username = req.Username; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (req.HasProperty(nameof(req.DisplayName))) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-07-14 16:44:41 +02:00
										 |  |  |             errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |             user.DisplayName = req.DisplayName; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (req.HasProperty(nameof(req.Bio))) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-07-14 16:44:41 +02:00
										 |  |  |             errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |             user.Bio = req.Bio; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (req.HasProperty(nameof(req.Avatar))) | 
					
						
							| 
									
										
										
										
											2024-07-14 16:44:41 +02:00
										 |  |  |             errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ValidationUtils.Validate(errors); | 
					
						
							|  |  |  |         // This is fired off regardless of whether the transaction is committed | 
					
						
							|  |  |  |         // (atomic operations are hard when combined with background jobs) | 
					
						
							|  |  |  |         // so it's in a separate block to the validation above. | 
					
						
							|  |  |  |         if (req.HasProperty(nameof(req.Avatar))) | 
					
						
							| 
									
										
										
										
											2024-09-03 16:29:51 +02:00
										 |  |  |             queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>( | 
					
						
							|  |  |  |                 new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); | 
					
						
							| 
									
										
										
										
											2024-07-08 19:03:04 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |         await db.SaveChangesAsync(); | 
					
						
							|  |  |  |         await tx.CommitAsync(); | 
					
						
							|  |  |  |         return Ok(await userRendererService.RenderUserAsync(user, CurrentUser, renderMembers: false, | 
					
						
							|  |  |  |             renderAuthMethods: false)); | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-22 15:13:46 +02:00
										 |  |  |     [HttpPatch("@me/custom-preferences")] | 
					
						
							|  |  |  |     [Authorize("user.update")] | 
					
						
							|  |  |  |     [ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)] | 
					
						
							|  |  |  |     public async Task<IActionResult> UpdateCustomPreferencesAsync([FromBody] List<CustomPreferencesUpdateRequest> req) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         ValidationUtils.Validate(ValidateCustomPreferences(req)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var user = await db.ResolveUserAsync(CurrentUser!.Id); | 
					
						
							|  |  |  |         var preferences = user.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)).ToDictionary(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach (var r in req) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             if (r.Id != null && preferences.ContainsKey(r.Id.Value)) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 preferences[r.Id.Value] = new User.CustomPreference | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     Favourite = r.Favourite, | 
					
						
							|  |  |  |                     Icon = r.Icon, | 
					
						
							|  |  |  |                     Muted = r.Muted, | 
					
						
							|  |  |  |                     Size = r.Size, | 
					
						
							|  |  |  |                     Tooltip = r.Tooltip | 
					
						
							|  |  |  |                 }; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 preferences[snowflakeGenerator.GenerateSnowflake()] = new User.CustomPreference | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     Favourite = r.Favourite, | 
					
						
							|  |  |  |                     Icon = r.Icon, | 
					
						
							|  |  |  |                     Muted = r.Muted, | 
					
						
							|  |  |  |                     Size = r.Size, | 
					
						
							|  |  |  |                     Tooltip = r.Tooltip | 
					
						
							|  |  |  |                 }; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         user.CustomPreferences = preferences; | 
					
						
							|  |  |  |         await db.SaveChangesAsync(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Ok(user.CustomPreferences); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] | 
					
						
							|  |  |  |     public class CustomPreferencesUpdateRequest | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         public Snowflake? Id { get; init; } | 
					
						
							|  |  |  |         public required string Icon { get; set; } | 
					
						
							|  |  |  |         public required string Tooltip { get; set; } | 
					
						
							|  |  |  |         public PreferenceSize Size { get; set; } | 
					
						
							|  |  |  |         public bool Muted { get; set; } | 
					
						
							|  |  |  |         public bool Favourite { get; set; } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private static List<(string, ValidationError?)> ValidateCustomPreferences( | 
					
						
							|  |  |  |         List<CustomPreferencesUpdateRequest> preferences) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         var errors = new List<(string, ValidationError?)>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (preferences.Count > 25) | 
					
						
							|  |  |  |             errors.Add(("custom_preferences", | 
					
						
							|  |  |  |                 ValidationError.LengthError("Too many custom preferences", 0, 25, preferences.Count))); | 
					
						
							|  |  |  |         if (preferences.Count > 50) return errors; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // TODO: validate individual preferences | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return errors; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 17:12:24 +02:00
										 |  |  |     public class UpdateUserRequest : PatchRequest | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         public string? Username { get; init; } | 
					
						
							|  |  |  |         public string? DisplayName { get; init; } | 
					
						
							|  |  |  |         public string? Bio { get; init; } | 
					
						
							|  |  |  |         public string? Avatar { get; init; } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-05-28 15:29:18 +02:00
										 |  |  | } |