2026-10-05 06:20:18 +02:00
|
|
|
using System.Net;
|
|
|
|
|
|
|
|
|
|
using decePubClient.Extensions;
|
|
|
|
|
using decePubClient.Models;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.ClientModels;
|
|
|
|
|
using PrivaPub.ClientModels.Admin;
|
|
|
|
|
|
|
|
|
|
namespace decePubClient.Services
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
2026-10-17 04:32:57 +02:00
|
|
|
/// 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.
|
2026-10-05 06:20:18 +02:00
|
|
|
/// </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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 04:32:57 +02:00
|
|
|
/// <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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 06:20:18 +02:00
|
|
|
/// <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);
|
2026-10-07 12:45:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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);
|
2026-10-05 06:20:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 04:02:20 +02:00
|
|
|
/// <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 [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 06:20:18 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|