diff --git a/PrivaPub.Tests/Http/MediaLifecycleTests.cs b/PrivaPub.Tests/Http/MediaLifecycleTests.cs index 72c6f17..c049d83 100644 --- a/PrivaPub.Tests/Http/MediaLifecycleTests.cs +++ b/PrivaPub.Tests/Http/MediaLifecycleTests.cs @@ -98,6 +98,29 @@ namespace PrivaPub.Tests.Http 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().Match(m => m.ProfileOfAvatarId == alice.Persona.Id && m.TrashedAt == null).ExecuteAnyAsync(Token)); + } + [Fact] public async Task A_removed_roots_media_and_pictures_are_trashed_and_its_scheduled_posts_dropped() { diff --git a/PrivaPub/Api/Mastodon/Controllers/AccountsController.cs b/PrivaPub/Api/Mastodon/Controllers/AccountsController.cs index ec3e426..5577713 100644 --- a/PrivaPub/Api/Mastodon/Controllers/AccountsController.cs +++ b/PrivaPub/Api/Mastodon/Controllers/AccountsController.cs @@ -123,6 +123,28 @@ namespace PrivaPub.Api.Mastodon.Controllers 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 DeleteAvatar([FromServices] IMediaService media, CancellationToken token) => DeletePicture(media, "avatar", token); + + [HttpDelete("/api/v1/profile/header"), Scope("write:accounts")] + public Task DeleteHeader([FromServices] IMediaService media, CancellationToken token) => DeletePicture(media, "header", token); + + async Task 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] public async Task Lookup(CancellationToken token) {