A persona's avatar and header can be removed
Mastodon's DELETE /api/v1/profile/avatar and /api/v1/profile/header: the picture goes to the trash (its file stops being served at once), the profile shows the default again, followers are sent the updated profile, and the answer is the account with its source. The profile Update is the one a persona that never had a picture sends, so nothing new leaves PrivaPub. MediaLifecycleTests: the avatar, then the header, removed and no longer served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
3ca29603ed
commit
8fecf7de2d
2 files changed
+45
No files matched your search
@@ -98,6 +98,29 @@ namespace PrivaPub.Tests.Http
|
|||||||
Assert.Equal(second, (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Ok().Body.Text("avatar"));
|
Assert.Equal(second, (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Ok().Body.Text("avatar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mastodon's DELETE /api/v1/profile/avatar and /header: the picture goes, the default shows again
|
||||||
|
[Fact]
|
||||||
|
public async Task A_picture_is_removed_and_the_default_shows_again()
|
||||||
|
{
|
||||||
|
var alice = await _host.Mastodon($"pic{Guid.NewGuid():N}"[..12]);
|
||||||
|
var form = MastodonHelpers.Multipart(("avatar", MastodonHelpers.JpegWithMetadata(200, 200), "image/jpeg", "me.jpg"),
|
||||||
|
("header", MastodonHelpers.JpegWithMetadata(900, 300), "image/jpeg", "top.jpg"));
|
||||||
|
var set = (await alice.Client.Exchange(new HttpRequestMessage(HttpMethod.Patch, "/api/v1/accounts/update_credentials") { Content = form })).Ok();
|
||||||
|
var avatar = set.Body.Text("avatar");
|
||||||
|
var header = set.Body.Text("header");
|
||||||
|
|
||||||
|
var removed = (await alice.Client.Delete("/api/v1/profile/avatar")).Ok();
|
||||||
|
|
||||||
|
Assert.Contains("missing", removed.Body.Text("avatar"));
|
||||||
|
Assert.Equal(header, removed.Body.Text("header"));
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, await Served(avatar));
|
||||||
|
Assert.Equal(HttpStatusCode.OK, await Served(header));
|
||||||
|
|
||||||
|
Assert.Contains("missing", (await alice.Client.Delete("/api/v1/profile/header")).Ok().Body.Text("header"));
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, await Served(header));
|
||||||
|
Assert.False(await DB.Default.Find<MediaAttachment>().Match(m => m.ProfileOfAvatarId == alice.Persona.Id && m.TrashedAt == null).ExecuteAnyAsync(Token));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task A_removed_roots_media_and_pictures_are_trashed_and_its_scheduled_posts_dropped()
|
public async Task A_removed_roots_media_and_pictures_are_trashed_and_its_scheduled_posts_dropped()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,28 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
|||||||
return Json(await _mapper.Local(actor, withSource: true, token));
|
return Json(await _mapper.Local(actor, withSource: true, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mastodon's: the picture goes (to the trash), the profile shows the default again, and followers are told
|
||||||
|
[HttpDelete("/api/v1/profile/avatar"), Scope("write:accounts")]
|
||||||
|
public Task<IActionResult> DeleteAvatar([FromServices] IMediaService media, CancellationToken token) => DeletePicture(media, "avatar", token);
|
||||||
|
|
||||||
|
[HttpDelete("/api/v1/profile/header"), Scope("write:accounts")]
|
||||||
|
public Task<IActionResult> DeleteHeader([FromServices] IMediaService media, CancellationToken token) => DeletePicture(media, "header", token);
|
||||||
|
|
||||||
|
async Task<IActionResult> DeletePicture(IMediaService media, string kind, CancellationToken token)
|
||||||
|
{
|
||||||
|
var avatar = await _dbEntities.Avatars.MatchID(Me.Id).ExecuteFirstAsync(token);
|
||||||
|
if (kind == "avatar")
|
||||||
|
avatar.PictureURL = default;
|
||||||
|
else
|
||||||
|
avatar.ThumbnailURL = default;
|
||||||
|
avatar.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await DB.Default.SaveAsync(avatar, token);
|
||||||
|
await media.Trash(m => m.ProfileOfAvatarId == avatar.ID && m.Kind == kind, "removed", token);
|
||||||
|
var actor = _localActors.FromAvatar(avatar);
|
||||||
|
await _outbox.PublishProfile(actor, token);
|
||||||
|
return Json(await _mapper.Local(actor, withSource: true, token));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("/api/v1/accounts/lookup"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
[HttpGet("/api/v1/accounts/lookup"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||||
public async Task<IActionResult> Lookup(CancellationToken token)
|
public async Task<IActionResult> Lookup(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user