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
243 lines
13 KiB
C#
243 lines
13 KiB
C#
using System.Net;
|
|
|
|
using decePubClient.Extensions;
|
|
using decePubClient.Models;
|
|
|
|
using PrivaPub.ClientModels;
|
|
using PrivaPub.ClientModels.Admin;
|
|
|
|
namespace decePubClient.Services
|
|
{
|
|
/// <summary>
|
|
/// What PrivaPub's moderators and administrators do as the root: reports and the staff's log (/clientapi/moderator),
|
|
/// servers blocked for the whole instance, the federation statistics (the overview, the servers, one server's days and
|
|
/// events, the ledger's events, the server's own days) and the backups (/clientapi/admin). The server's own settings,
|
|
/// announcements and relays are InstanceAdminService's. PrivaPub checks the root's role on every call. Lists come back
|
|
/// empty and values null on failure; nothing throws.
|
|
/// </summary>
|
|
public class AdminService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
|
{
|
|
/// <summary>The newest 100 reports, open or resolved.</summary>
|
|
public async Task<List<ViewReport>> Reports(bool resolved, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_ModeratorReports, [$"resolved={(resolved ? "true" : "false")}"],
|
|
cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<List<ViewReport>>(cancellationToken) ?? [];
|
|
|
|
await LogFailure(response, nameof(Reports), cancellationToken);
|
|
return [];
|
|
}
|
|
|
|
public async Task<WebResult> Resolve(string reportId, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_ResolveReport(reportId), cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>One page of the staff's log, newest first: those before `before` (the previous page's Next) when given.
|
|
/// Administrators read all of it with login names; a moderator reads what isn't about a login. Null on failure.</summary>
|
|
public async Task<ViewAdminActionsPage> Log(string before, int limit, CancellationToken cancellationToken = default)
|
|
{
|
|
var parameters = new List<string> { $"limit={limit}" };
|
|
if (before is { Length: > 0 })
|
|
parameters.Add($"before={Uri.EscapeDataString(before)}");
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_ModeratorLog, [.. parameters], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewAdminActionsPage>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Log), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>The servers blocked for everyone here, by name.</summary>
|
|
public async Task<List<ViewDomainBlock>> ServerBlocks(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_AdminDomainBlocks, cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<List<ViewDomainBlock>>(cancellationToken) ?? [];
|
|
|
|
await LogFailure(response, nameof(ServerBlocks), cancellationToken);
|
|
return [];
|
|
}
|
|
|
|
/// <summary>Blocks a server for everyone, or changes how; Data is the block.</summary>
|
|
public async Task<WebResult> BlockServer(DomainBlockForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_AdminDomainBlock, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewDomainBlock>(cancellationToken) };
|
|
}
|
|
|
|
public async Task<WebResult> UnblockServer(string domain, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminDomainUnblock(domain), cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>The server's backups, what runs and the last restore; null on failure.</summary>
|
|
public async Task<ViewBackups> Backups(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_AdminBackups, cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewBackups>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Backups), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Starts a backup; it runs on while the list says so.</summary>
|
|
public async Task<WebResult> BackUp(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_AdminBackup, cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
public async Task<WebResult> DeleteBackup(string id, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Delete(APIs.ClientApi.DELETE_M_AdminBackup(id), cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>A download link for a backup, for the password; Data is the ViewBackupTicket.</summary>
|
|
public async Task<WebResult> BackupTicket(string id, string password, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminBackupTicket(id), new BackupPasswordForm { Password = password },
|
|
cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewBackupTicket>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>Asks for a backup to be restored: PrivaPub stops within seconds and restores it as it starts again.</summary>
|
|
public async Task<WebResult> RestoreBackup(string id, RestoreBackupForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminBackupRestore(id), form, cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>An upload to send a backup's pieces to (transfer.js); Data is the ViewBackupUpload.</summary>
|
|
public async Task<WebResult> StartBackupUpload(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_AdminBackupUpload, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewBackupUpload>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>The whole upload read into a backup; Data is the ViewBackup.</summary>
|
|
public async Task<WebResult> FinishBackupUpload(string id, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminBackupUploadFinish(id), cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewBackup>(cancellationToken) };
|
|
}
|
|
|
|
public async Task<WebResult> CancelBackupUpload(string id, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Delete(APIs.ClientApi.DELETE_M_AdminBackupUpload(id), cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
public async Task<ViewStatisticsOverview> Overview(int days, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsOverview, [$"days={days}"], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewStatisticsOverview>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Overview), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>One page of the servers we exchanged with; sort is traffic (default), failures, latency or host.</summary>
|
|
public async Task<ViewHostsPage> Hosts(int days, string sort, int page, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsHosts,
|
|
[$"days={days}", $"sort={Uri.EscapeDataString(sort ?? "")}", $"page={page}", "limit=50"], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewHostsPage>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Hosts), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
public async Task<ViewCrawler> Crawler(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsCrawler, cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewCrawler>(cancellationToken);
|
|
|
|
await LogFailure(response, nameof(Crawler), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>One server: what it says of itself, its days (counters keyed as the ledger keys them) and its latest 100
|
|
/// events; null when PrivaPub knows nothing of it or it cannot be read.</summary>
|
|
public async Task<ViewHostDetail> Host(string host, int days, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_M_StatisticsHost(host), [$"days={days}"], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<ViewHostDetail>(cancellationToken);
|
|
|
|
if (response.StatusCode is not HttpStatusCode.NotFound)
|
|
await LogFailure(response, nameof(Host), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Asks PrivaPub to read the server's NodeInfo and instance description again (a queued job, answered 202).</summary>
|
|
public async Task<WebResult> Describe(string host, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_M_StatisticsDescribe(host), cancellationToken: cancellationToken);
|
|
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>The ledger's events, newest first, those before `before` when given (the next page starts at the last
|
|
/// one's time); each filter is left out when null.</summary>
|
|
public async Task<List<ViewInteractionEvent>> Events(string host, string channel, string outcome, string reason, DateTime? before, int limit,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var parameters = new List<string> { $"limit={limit}" };
|
|
if (host is { Length: > 0 })
|
|
parameters.Add($"host={Uri.EscapeDataString(host)}");
|
|
if (channel is { Length: > 0 })
|
|
parameters.Add($"channel={Uri.EscapeDataString(channel)}");
|
|
if (outcome is { Length: > 0 })
|
|
parameters.Add($"outcome={Uri.EscapeDataString(outcome)}");
|
|
if (reason is { Length: > 0 })
|
|
parameters.Add($"reason={Uri.EscapeDataString(reason)}");
|
|
if (before is { } at)
|
|
parameters.Add($"before={Uri.EscapeDataString(at.ToUniversalTime().ToString("o", System.Globalization.CultureInfo.InvariantCulture))}");
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsEvents, [.. parameters], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<List<ViewInteractionEvent>>(cancellationToken) ?? [];
|
|
|
|
await LogFailure(response, nameof(Events), cancellationToken);
|
|
return [];
|
|
}
|
|
|
|
/// <summary>The server's own days, oldest first: its client API, the documents it served and its ledger.</summary>
|
|
public async Task<List<ViewServerDay>> ServerDays(int days, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsServer, [$"days={days}"], cancellationToken: cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.DefaultReadFromJsonAsync<List<ViewServerDay>>(cancellationToken) ?? [];
|
|
|
|
await LogFailure(response, nameof(ServerDays), cancellationToken);
|
|
return [];
|
|
}
|
|
|
|
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(AdminService)}.{caller}", cancellationToken);
|
|
}
|
|
}
|
|
}
|