R5 the server's own settings in decePub: instance, rules, announcements, relays, media limits, the staff's log, About
Build / Build (push) Successful in 1m5s
Build / Build (push) Successful in 1m5s
Administration's Pleroma placeholders give way to real sections: Instance (title, descriptions, contact, thumbnail, privacy policy, terms, the weekly activity), Rules, Announcements, Relays, Media limits and the Moderation log, next to Reports, blocked servers (now published or obfuscated), statistics and backups; moderators see Reports and the log. Every save sends the whole settings form built from the latest read, since PrivaPub replaces what it carries. /about shows the server's documents to anyone. The report dialog picks the rules a violation breaks, and reports show them. Announcements show above the feed tabs, dismissed and reacted to, live from the user stream. Login names the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
b8d7858acb
commit
4c88980374
33 files changed
+3743
-239
No files matched your search
@@ -0,0 +1,178 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class InstanceAdminService(IHttpService httpService, AboutService aboutService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
||||
{
|
||||
/// <summary>The settings as the page shows them, deleted rules too; null on failure.</summary>
|
||||
public async Task<ViewServerSettings> Settings(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_AdminSettings, cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<ViewServerSettings>(cancellationToken);
|
||||
|
||||
await LogFailure(response, nameof(Settings), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>The form that saves the settings exactly as they are: the rules not deleted in their order, the media
|
||||
/// overrides, every text.</summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public async Task<WebResult> SaveSettings(Action<ServerSettingsForm> 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<ViewServerSettings>(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);
|
||||
}
|
||||
|
||||
/// <summary>A new thumbnail, streamed from the browser's file; PrivaPub checks it, strips it and fits it within
|
||||
/// 1200×630. Data is the ViewServerSettings.</summary>
|
||||
public async Task<WebResult> SaveThumbnail(IBrowserFile file, long maximumSize, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_AdminThumbnail, (Func<HttpContent>)(() =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Back to the default picture; the uploaded one is trashed. Data is the ViewServerSettings.</summary>
|
||||
public async Task<WebResult> RemoveThumbnail(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Delete(APIs.ClientApi.DELETE_AdminThumbnail, cancellationToken: cancellationToken);
|
||||
return await Saved(response, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Every announcement, published or not, with its reads and reactions.</summary>
|
||||
public async Task<List<ViewAnnouncement>> Announcements(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_AdminAnnouncements, cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<List<ViewAnnouncement>>(cancellationToken) ?? [];
|
||||
|
||||
await LogFailure(response, nameof(Announcements), cancellationToken);
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>Writes a new announcement (no Id) or changes one; Data is the ViewAnnouncement.</summary>
|
||||
public async Task<WebResult> 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<ViewAnnouncement>(cancellationToken) };
|
||||
}
|
||||
|
||||
/// <summary>Deletes an announcement, its reads and reactions with it; every persona's stream hears it go.</summary>
|
||||
public async Task<WebResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>The relays the server subscribes to: the configuration's, then the settings'.</summary>
|
||||
public async Task<List<ViewRelay>> Relays(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_AdminRelays, cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<List<ViewRelay>>(cancellationToken) ?? [];
|
||||
|
||||
await LogFailure(response, nameof(Relays), cancellationToken);
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
public async Task<WebResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from a relay the settings named (a configured one is refused with 409); answered 202 like
|
||||
/// AddRelay.</summary>
|
||||
public async Task<WebResult> 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<WebResult> 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<ViewServerSettings>(cancellationToken) };
|
||||
}
|
||||
|
||||
async Task<WebResult> RelayList(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
|
||||
return new WebResult { Data = await response.DefaultReadFromJsonAsync<List<ViewRelay>>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user