using System.Net; using System.Net.Http.Headers; using decePubClient.Extensions; using decePubClient.Models; using Microsoft.AspNetCore.Components.Forms; using PrivaPub.ClientModels; using PrivaPub.ClientModels.Admin; namespace decePubClient.Services { /// /// The server's own settings from the administrator's page (PrivaPub's InstanceAdminController, /clientapi/admin): what /// it says of itself, its rules, its thumbnail, its media limits, whether its weekly activity is published, its /// announcements and the relays it subscribes to. PrivaPub checks the administrator against its database on every call. /// A save replaces everything the form carries (a rule left out is removed), so SaveSettings always starts from the /// settings as PrivaPub has them now and changes only what the caller changes. Lists come back empty and values null on /// failure; nothing throws. /// public class InstanceAdminService(IHttpService httpService, AboutService aboutService, ILoggingService logger, CoalescingStringLocalizer localizer) { /// The settings as the page shows them, deleted rules too; null on failure. public async Task Settings(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_AdminSettings, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync(cancellationToken); await LogFailure(response, nameof(Settings), cancellationToken); return null; } /// The form that saves the settings exactly as they are: the rules not deleted in their order, the media /// overrides, every text. public static ServerSettingsForm FormOf(ViewServerSettings settings) => new() { Title = settings.Title, ShortDescription = settings.ShortDescription, Description = settings.Description, ContactEmail = settings.ContactEmail, ContactAccountId = settings.ContactAccountId, PrivacyPolicy = settings.PrivacyPolicy, TermsOfService = settings.TermsOfService, TermsEffectiveAt = settings.TermsEffectiveAt, Rules = [.. settings.Rules.Where(r => !r.IsDeleted).OrderBy(r => r.Position).Select(r => new ServerRuleForm { Id = r.Id, Text = r.Text, Hint = r.Hint })], Media = new() { QuotaBytesPerRoot = settings.Media.QuotaBytesPerRoot, MaxImageBytes = settings.Media.MaxImageBytes, MaxVideoBytes = settings.Media.MaxVideoBytes, MaxSeconds = settings.Media.MaxSeconds }, PublishActivity = settings.PublishActivity }; /// /// Reads the settings as PrivaPub has them now, lets `change` edit the form made of them, and saves the whole form; /// Data is the ViewServerSettings saved. What another panel or another administrator saved meanwhile stays. /// public async Task SaveSettings(Action change, CancellationToken cancellationToken = default) { using var current = await httpService.Get(APIs.ClientApi.GET_AdminSettings, cancellationToken: cancellationToken); if (!current.IsSuccessStatusCode) return await current.Content.ReadWebResult(current.StatusCode, localizer, cancellationToken); if (await current.DefaultReadFromJsonAsync(cancellationToken) is not { } latest) return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway); var form = FormOf(latest); change(form); using var response = await httpService.Post(APIs.ClientApi.POST_AdminSettings, form, cancellationToken: cancellationToken); return await Saved(response, cancellationToken); } /// A new thumbnail, streamed from the browser's file; PrivaPub checks it, strips it and fits it within /// 1200×630. Data is the ViewServerSettings. public async Task SaveThumbnail(IBrowserFile file, long maximumSize, CancellationToken cancellationToken = default) { using var response = await httpService.Post(APIs.ClientApi.POST_AdminThumbnail, (Func)(() => { var picture = new StreamContent(file.OpenReadStream(maximumSize, cancellationToken)); picture.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); return new MultipartFormDataContent { { picture, "file", file.Name } }; }), cancellationToken: cancellationToken); return await Saved(response, cancellationToken); } /// Back to the default picture; the uploaded one is trashed. Data is the ViewServerSettings. public async Task RemoveThumbnail(CancellationToken cancellationToken = default) { using var response = await httpService.Delete(APIs.ClientApi.DELETE_AdminThumbnail, cancellationToken: cancellationToken); return await Saved(response, cancellationToken); } /// Every announcement, published or not, with its reads and reactions. public async Task> Announcements(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_AdminAnnouncements, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(Announcements), cancellationToken); return []; } /// Writes a new announcement (no Id) or changes one; Data is the ViewAnnouncement. public async Task SaveAnnouncement(AnnouncementForm form, CancellationToken cancellationToken = default) { var route = form.Id is { Length: > 0 } ? APIs.ClientApi.POST_AdminAnnouncementUpdate : APIs.ClientApi.POST_AdminAnnouncementInsert; using var response = await httpService.Post(route, form, cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult { Data = await response.DefaultReadFromJsonAsync(cancellationToken) }; } /// Deletes an announcement, its reads and reactions with it; every persona's stream hears it go. public async Task DeleteAnnouncement(string id, CancellationToken cancellationToken = default) { using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminAnnouncementDelete(id), cancellationToken: cancellationToken); return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); } /// The relays the server subscribes to: the configuration's, then the settings'. public async Task> Relays(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_AdminRelays, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(Relays), cancellationToken); return []; } /// Subscribes to a relay by its actor's or inbox's https address. PrivaPub answers 202 and subscribes in the /// background, so the list it answers with (Data) shows the relay before its state does; read it again shortly. public async Task AddRelay(string address, CancellationToken cancellationToken = default) { using var response = await httpService.Post(APIs.ClientApi.POST_AdminRelayInsert, new RelayForm { Address = address?.Trim() }, cancellationToken: cancellationToken); return await RelayList(response, cancellationToken); } /// Unsubscribes from a relay the settings named (a configured one is refused with 409); answered 202 like /// AddRelay. public async Task RemoveRelay(string address, CancellationToken cancellationToken = default) { using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminRelayDelete(address), cancellationToken: cancellationToken); return await RelayList(response, cancellationToken); } async Task Saved(HttpResponseMessage response, CancellationToken cancellationToken) { if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); // the title and the rules everyone reads have changed aboutService.Forget(); return new WebResult { Data = await response.DefaultReadFromJsonAsync(cancellationToken) }; } async Task RelayList(HttpResponseMessage response, CancellationToken cancellationToken) { if (!response.IsSuccessStatusCode) return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); return new WebResult { Data = await response.DefaultReadFromJsonAsync>(cancellationToken) ?? [] }; } async Task LogFailure(HttpResponseMessage response, string caller, CancellationToken cancellationToken) { var result = await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); if (result.StatusCode is not ((int)HttpStatusCode.Gone or (int)HttpStatusCode.Forbidden)) await logger.ProcessWarning(result.ToString(localizer), $"{nameof(InstanceAdminService)}.{caller}", cancellationToken); } } }