using System.Net; using decePubClient.Extensions; using decePubClient.Models; using PrivaPub.ClientModels; using PrivaPub.ClientModels.Admin; namespace decePubClient.Services { /// /// 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. /// public class AdminService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer) { /// The newest 100 reports, open or resolved. public async Task> 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>(cancellationToken) ?? []; await LogFailure(response, nameof(Reports), cancellationToken); return []; } public async Task 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); } /// 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. public async Task Log(string before, int limit, CancellationToken cancellationToken = default) { var parameters = new List { $"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(cancellationToken); await LogFailure(response, nameof(Log), cancellationToken); return null; } /// The servers blocked for everyone here, by name. public async Task> ServerBlocks(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_AdminDomainBlocks, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync>(cancellationToken) ?? []; await LogFailure(response, nameof(ServerBlocks), cancellationToken); return []; } /// Blocks a server for everyone, or changes how; Data is the block. public async Task 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(cancellationToken) }; } public async Task 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); } /// The server's backups, what runs and the last restore; null on failure. public async Task Backups(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_AdminBackups, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync(cancellationToken); await LogFailure(response, nameof(Backups), cancellationToken); return null; } /// Starts a backup; it runs on while the list says so. public async Task 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 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); } /// A download link for a backup, for the password; Data is the ViewBackupTicket. public async Task 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(cancellationToken) }; } /// Asks for a backup to be restored: PrivaPub stops within seconds and restores it as it starts again. public async Task 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); } /// An upload to send a backup's pieces to (transfer.js); Data is the ViewBackupUpload. public async Task 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(cancellationToken) }; } /// The whole upload read into a backup; Data is the ViewBackup. public async Task 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(cancellationToken) }; } public async Task 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 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(cancellationToken); await LogFailure(response, nameof(Overview), cancellationToken); return null; } /// One page of the servers we exchanged with; sort is traffic (default), failures, latency or host. public async Task 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(cancellationToken); await LogFailure(response, nameof(Hosts), cancellationToken); return null; } public async Task Crawler(CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.ClientApi.GET_StatisticsCrawler, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode) return await response.DefaultReadFromJsonAsync(cancellationToken); await LogFailure(response, nameof(Crawler), cancellationToken); return null; } /// 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. public async Task 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(cancellationToken); if (response.StatusCode is not HttpStatusCode.NotFound) await LogFailure(response, nameof(Host), cancellationToken); return null; } /// Asks PrivaPub to read the server's NodeInfo and instance description again (a queued job, answered 202). public async Task 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); } /// 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. public async Task> Events(string host, string channel, string outcome, string reason, DateTime? before, int limit, CancellationToken cancellationToken = default) { var parameters = new List { $"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>(cancellationToken) ?? []; await LogFailure(response, nameof(Events), cancellationToken); return []; } /// The server's own days, oldest first: its client API, the documents it served and its ledger. public async Task> 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>(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); } } }