Build / Build (push) Successful in 1m8s
Follow requests answered on the persona's own profile and from the Notifications tab; its followers and the accounts it follows listed there, a follower removed (asked first). Downvotes on threadiverse posts, after the public-vote notice. A notification dismissed from its line. "Filter this post" from a card's menu, and the Filters page showing the posts a filter holds. Who may quote the persona's own posts, and revoking a quote of one. Statistics: a server in detail with "describe again", the ledger's events, the server's own days. The composer deletes an upload taken out before posting. MediaType.Documents goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
106 lines
5.1 KiB
C#
106 lines
5.1 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
|
|
using decePubClient.Extensions;
|
|
using decePubClient.Models;
|
|
using decePubClient.Models.Mastodon;
|
|
|
|
using PrivaPub.ClientModels;
|
|
|
|
namespace decePubClient.Services
|
|
{
|
|
/// <summary>
|
|
/// Uploads to PrivaPub (/api/v2/media, multipart), streamed from the browser's file; follows what it is still processing
|
|
/// (audio and video: 202, then 206 until ready); describes and deletes what isn't posted yet; and knows what PrivaPub
|
|
/// takes.
|
|
/// </summary>
|
|
public class MediaService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
|
{
|
|
static readonly MediaConfiguration Fallback = new()
|
|
{
|
|
SupportedMimeTypes = ["image/jpeg", "image/png", "image/gif", "image/webp", "image/avif", "video/mp4", "video/quicktime", "video/webm",
|
|
"audio/mpeg", "audio/mp3", "audio/ogg", "audio/wav", "audio/x-wav", "audio/mp4", "audio/x-m4a", "audio/flac", "audio/webm"],
|
|
ImageSizeLimit = 16 * 1024 * 1024,
|
|
VideoSizeLimit = 99 * 1024 * 1024
|
|
};
|
|
|
|
MediaConfiguration limits;
|
|
|
|
/// <summary>What PrivaPub takes, read once a session (/api/v2/instance); its defaults while it can't be read.</summary>
|
|
public async Task<MediaConfiguration> Limits(CancellationToken cancellationToken = default)
|
|
{
|
|
if (limits is not null)
|
|
return limits;
|
|
using var response = await httpService.GetAnon(APIs.Mastodon.GET_InstanceV2, cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode
|
|
&& await response.MastodonReadFromJsonAsync<InstanceV2>(cancellationToken) is { Configuration.MediaAttachments.SupportedMimeTypes.Count: > 0 } instance)
|
|
return limits = instance.Configuration.MediaAttachments;
|
|
return Fallback;
|
|
}
|
|
|
|
/// <summary>The attachment as Data; its Url is null while PrivaPub still processes it (follow it with Get).</summary>
|
|
public async Task<WebResult> Upload(UploadMedia media, long maximumSize, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.Mastodon.POST_Media, (Func<HttpContent>)(() =>
|
|
{
|
|
var file = new StreamContent(media.File.OpenReadStream(maximumSize, cancellationToken));
|
|
file.Headers.ContentType = new MediaTypeHeaderValue(media.ContentType);
|
|
var content = new MultipartFormDataContent { { file, "file", media.FileName } };
|
|
if (media.AltText is { Length: > 0 } description)
|
|
content.Add(new StringContent(description), "description");
|
|
return content;
|
|
}), cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return await Attachment(response, cancellationToken);
|
|
}
|
|
|
|
/// <summary>An upload PrivaPub is processing: Data is the attachment once it is ready, null while it isn't; invalid
|
|
/// when it can't be.</summary>
|
|
public async Task<WebResult> Get(string mediaId, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_M_Media(mediaId), cancellationToken: cancellationToken);
|
|
if (response.StatusCode == HttpStatusCode.PartialContent)
|
|
return new WebResult();
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return await Attachment(response, cancellationToken);
|
|
}
|
|
|
|
/// <summary>Describes an upload not posted yet (a posted one is described again by editing its post).</summary>
|
|
public async Task<WebResult> Describe(string mediaId, string description, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Put(APIs.Mastodon.PUT_M_Media(mediaId),
|
|
new KeyValuePair<string, string>[] { new("description", description ?? string.Empty) }, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult();
|
|
}
|
|
|
|
/// <summary>Deletes an upload of the persona's that no post holds (DELETE /api/v1/media/:id; PrivaPub refuses a posted
|
|
/// one with 422): the composer drops what was removed before posting. A failure is logged.</summary>
|
|
public async Task<WebResult> Discard(string mediaId, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Delete(APIs.Mastodon.DELETE_M_Media(mediaId), cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return new WebResult();
|
|
|
|
var result = await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
if (result.StatusCode is not (int)HttpStatusCode.Gone)
|
|
await logger.ProcessWarning(result.ToString(localizer), $"{nameof(MediaService)}.{nameof(Discard)}", cancellationToken);
|
|
return result;
|
|
}
|
|
|
|
async Task<WebResult> Attachment(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
{
|
|
var attachment = await response.MastodonReadFromJsonAsync<MediaAttachment>(cancellationToken);
|
|
return attachment?.Id is null
|
|
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway)
|
|
: new WebResult { Data = attachment };
|
|
}
|
|
}
|
|
}
|