2026-10-01 08:05:56 +02:00
using Microsoft.AspNetCore.Authorization ;
2023-02-19 00:43:43 +01:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.Extensions.Localization ;
using PrivaPub.ClientModels ;
using PrivaPub.Extensions ;
using PrivaPub.Resources ;
using PrivaPub.Services.ClientToServer.Private ;
2026-10-17 05:23:45 +02:00
using PrivaPub.ClientModels.User ;
2023-02-19 00:43:43 +01:00
using PrivaPub.ClientModels.User.Avatar ;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/avatar/private"),
Authorize(Policy = Policies.IsUser)]
public class PrivateAvatarController : ControllerBase
{
readonly ILogger < PrivateAvatarController > _logger ;
readonly IPrivateAvatarUsersService _privateAvatarUsersService ;
readonly IStringLocalizer _localizer ;
public PrivateAvatarController ( IPrivateAvatarUsersService privateAvatarUsersService ,
IStringLocalizer < GenericRes > localizer ,
ILogger < PrivateAvatarController > logger )
{
_privateAvatarUsersService = privateAvatarUsersService ;
_localizer = localizer ;
_logger = logger ;
}
2026-10-01 08:05:56 +02:00
[HttpGet, Route("/clientapi/avatar/private/list")]
public async Task < IActionResult > GetAvatars ( CancellationToken token )
{
var result = await _privateAvatarUsersService . GetRootAvatars ( User . GetUserId (), token );
return result . IsValid ? Ok ( result . Data ) : StatusCode ( result . StatusCode , result );
}
2023-02-19 00:43:43 +01:00
[HttpPost, Route("/clientapi/avatar/private/insert")]
public async Task < IActionResult > InsertAvatar ( InsertAvatarForm model )
{
var result = new WebResult ();
if (! ModelState . IsValid )
return BadRequest ( result . Invalidate ( _localizer [ "Invalid model." ]));
try
{
model . RootId = User . GetUserId ();
result = await _privateAvatarUsersService . InsertAvatar ( model );
if (! result . IsValid )
return StatusCode ( result . StatusCode , result );
return Ok ( result . Data );
}
catch ( Exception ex )
{
_logger . LogError ( ex , $"{nameof(PrivateAvatarController)}.{nameof(InsertAvatar)}()" );
2026-10-01 10:58:39 +02:00
return BadRequest ( result . Invalidate ( _localizer [ "Something went wrong." ]));
2023-02-19 00:43:43 +01:00
}
}
2026-10-17 05:23:45 +02:00
// A login deletes one of its personas, with its password again (R6): everything public it had goes, the groups it owns
// with it, as when the whole login is deleted (PersonaRemoval). A suspended persona may be deleted too.
[HttpPost, Route("/clientapi/avatar/private/delete"), Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Infrastructure.RateLimiting.Accounts)]
public async Task < IActionResult > DeleteAvatar ( DeletePersonaForm form , [ FromServices ] Services . IRootUsersService users ,
[FromServices] Services . IPersonaRemoval personas , [ FromServices ] StaticServices . DbEntities dbEntities , CancellationToken token )
{
if (! ModelState . IsValid )
return BadRequest ( new WebResult (). Invalidate ( _localizer [ "Invalid model." ]));
var rootId = User . GetUserId ();
if (! await users . PasswordMatches ( rootId , form . Password ))
return StatusCode ( StatusCodes . Status403Forbidden , new WebResult (). Invalidate ( _localizer [ Services . RootUsersService . NoMatch ], StatusCodes . Status403Forbidden ));
if (! await dbEntities . RootToAvatars . Match ( ra => ra . RootId == rootId && ra . AvatarId == form . AvatarId ). ExecuteAnyAsync ( token )
|| ! await personas . Remove ( form . AvatarId , CancellationToken . None ))
return NotFound ( new WebResult (). Invalidate ( _localizer [ "Avatar not found." ], StatusCodes . Status404NotFound ));
return Ok ();
}
2026-10-18 08:38:11 +02:00
// how the persona's clients show what others post (R8): Mastodon's reading preferences, which its API only reads
[HttpGet, Route("/clientapi/avatar/private/preferences")]
public async Task < IActionResult > Preferences ([ FromQuery ] string avatarId , [ FromServices ] StaticServices . DbEntities dbEntities , CancellationToken token )
{
var avatar = await OwnPersona ( avatarId , dbEntities , token );
return avatar == default
? NotFound ( new WebResult (). Invalidate ( _localizer [ "Avatar not found." ], StatusCodes . Status404NotFound ))
: Ok ( PreferencesOf ( avatar ));
}
[HttpPost, Route("/clientapi/avatar/private/preferences")]
public async Task < IActionResult > SavePreferences ( PersonaPreferencesForm form , [ FromServices ] StaticServices . DbEntities dbEntities , CancellationToken token )
{
if (! ModelState . IsValid )
return BadRequest ( new WebResult (). Invalidate ( _localizer [ "Invalid model." ]));
if (! Models . User . ExpandMediaModes . IsKnown ( form . ExpandMedia ))
return UnprocessableEntity ( new WebResult (). Invalidate ( _localizer [ "Media are shown as default, show_all or hide_all." ], StatusCodes . Status422UnprocessableEntity ));
var avatar = await OwnPersona ( form . AvatarId , dbEntities , token );
if ( avatar == default )
return NotFound ( new WebResult (). Invalidate ( _localizer [ "Avatar not found." ], StatusCodes . Status404NotFound ));
await MongoDB . Entities . DB . Default . Update < Models . User . Avatar >(). MatchID ( avatar . ID )
. Modify ( a => a . Settings . ExpandMedia , form . ExpandMedia )
. Modify ( a => a . Settings . ExpandSpoilers , form . ExpandSpoilers )
. ExecuteAsync ( token );
avatar . Settings . ExpandMedia = form . ExpandMedia ;
avatar . Settings . ExpandSpoilers = form . ExpandSpoilers ;
return Ok ( PreferencesOf ( avatar ));
}
// one of the login's own personas, not deleted (a suspended one keeps its preferences for when it is back)
async Task < Models . User . Avatar > OwnPersona ( string avatarId , StaticServices . DbEntities dbEntities , CancellationToken token )
{
var rootId = User . GetUserId ();
if ( string . IsNullOrEmpty ( avatarId ) || ! await dbEntities . RootToAvatars . Match ( ra => ra . RootId == rootId && ra . AvatarId == avatarId ). ExecuteAnyAsync ( token ))
return default ;
return await dbEntities . Avatars . MatchID ( avatarId ). Match ( a => ! a . DeletionAt . HasValue ). ExecuteFirstAsync ( token );
}
static ViewPersonaPreferences PreferencesOf ( Models . User . Avatar avatar ) => new ()
{
AvatarId = avatar . ID ,
ExpandMedia = Models . User . ExpandMediaModes . IsKnown ( avatar . Settings . ExpandMedia ) ? avatar . Settings . ExpandMedia : Models . User . ExpandMediaModes . Default ,
ExpandSpoilers = avatar . Settings . ExpandSpoilers
};
2023-02-19 00:43:43 +01:00
[HttpPost, Route("/clientapi/avatar/private/update")]
public async Task < IActionResult > UpdateAvatar ( UpdateAvatarForm model )
{
var result = new WebResult ();
if (! ModelState . IsValid )
return BadRequest ( result . Invalidate ( _localizer [ "Invalid model." ]));
try
{
model . RootId = User . GetUserId ();
result = await _privateAvatarUsersService . UpdateAvatar ( model );
if (! result . IsValid )
return StatusCode ( result . StatusCode , result );
return Ok ( result . Data );
}
catch ( Exception ex )
{
_logger . LogError ( ex , $"{nameof(PrivateAvatarController)}.{nameof(UpdateAvatar)}()" );
2026-10-01 10:58:39 +02:00
return BadRequest ( result . Invalidate ( _localizer [ "Something went wrong." ]));
2023-02-19 00:43:43 +01:00
}
}
}
}