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 { /// /// 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. /// public class MediaService(IHttpService httpService, 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; /// What PrivaPub takes, read once a session (/api/v2/instance); its defaults while it can't be read. public async Task 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(cancellationToken) is { Configuration.MediaAttachments.SupportedMimeTypes.Count: > 0 } instance) return limits = instance.Configuration.MediaAttachments; return Fallback; } /// The attachment as Data; its Url is null while PrivaPub still processes it (follow it with Get). public async Task Upload(UploadMedia media, long maximumSize, CancellationToken cancellationToken = default) { using var response = await httpService.Post(APIs.Mastodon.POST_Media, (Func)(() => { 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); } /// An upload PrivaPub is processing: Data is the attachment once it is ready, null while it isn't; invalid /// when it can't be. public async Task 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); } /// Describes an upload not posted yet (a posted one is described again by editing its post). public async Task Describe(string mediaId, string description, CancellationToken cancellationToken = default) { using var response = await httpService.Put(APIs.Mastodon.PUT_M_Media(mediaId), new KeyValuePair[] { new("description", description ?? string.Empty) }, cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult(); } async Task Attachment(HttpResponseMessage response, CancellationToken cancellationToken) { var attachment = await response.MastodonReadFromJsonAsync(cancellationToken); return attachment?.Id is null ? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway) : new WebResult { Data = attachment }; } } }