2026-10-04 11:03:14 +02:00
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
|
|
|
|
|
using collAnon.Client.Services;
|
|
|
|
|
|
|
|
|
|
using decePubClient.Extensions;
|
|
|
|
|
using decePubClient.Models;
|
|
|
|
|
using decePubClient.Models.Mastodon;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.ClientModels;
|
|
|
|
|
|
|
|
|
|
namespace decePubClient.Services
|
|
|
|
|
{
|
2026-10-07 11:56:59 +02:00
|
|
|
/// <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 what isn't posted yet; and knows what PrivaPub takes.
|
|
|
|
|
/// </summary>
|
2026-10-04 11:03:14 +02:00
|
|
|
public class MediaService(IHttpService httpService, CoalescingStringLocalizer localizer)
|
|
|
|
|
{
|
2026-10-07 11:56:59 +02:00
|
|
|
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)
|
2026-10-04 11:03:14 +02:00
|
|
|
{
|
|
|
|
|
using var response = await httpService.Post(APIs.Mastodon.POST_Media, (Func<HttpContent>)(() =>
|
|
|
|
|
{
|
2026-10-07 11:56:59 +02:00
|
|
|
var file = new StreamContent(media.File.OpenReadStream(maximumSize, cancellationToken));
|
|
|
|
|
file.Headers.ContentType = new MediaTypeHeaderValue(media.ContentType);
|
2026-10-04 11:03:14 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-10-07 11:56:59 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task<WebResult> Attachment(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|