Build / Build (push) Successful in 51s
Starts the completion program (SocialPub docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17). - The localizer's namespace is decePubClient.Services; collAnon is gone from the repo. - Dead code: PagesBase, VConstants, SettingsOptions, ViewLanguage, unused APIs routes, InstanceService.Self, RequestStats.LastOf/AverageMs, PostAnon/DeleteTotallyAnon, unused AppStatusService methods and their main.js helpers (the app badge stays for Web Push), ExtensionMethods/SUtility leftovers (Markdig and BlazorZXingJs packages go), unused PublicCacheData/PageSettings/AppConfiguration fields, commented-out code. - The client log in IndexedDB, which nothing ever read, goes with the whole IndexedDB layer (DnetIndexedDb, IStorage): logs go to the browser console; main.js deletes the old "data" database once. - The solution names the real ClientModels project. Unused vendor assets (fontawesome, solid, open-iconic, rxjs) go. - robots.txt disallows everything; manifest.json and index.html say decePub; the service worker's cache name follows each publish's assets version, so old caches are purged. - MessageType is Visibility (member names kept: they are resx keys); ExpandMessage.razor is Thread.razor (same route). - i18n: dead keys removed, Italian "camera" is "punto di vista", DInputBase/DCalendar validation messages come from ErrorMessages, DataTransfer's spinner has its label. - CLAUDE.md follows: client storage, JS interop, PWA, the D fields' messages. Verified: dotnet build decePubClient.csproj and decePubClient.sln -c Release (0 errors, no new warnings); a Pasture publish served by the pasture's decepub peer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
91 lines
4.3 KiB
C#
91 lines
4.3 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 what isn't posted yet; and knows what PrivaPub takes.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <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();
|
|
}
|
|
|
|
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 };
|
|
}
|
|
}
|
|
}
|