using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using WindowsGotify.Models; namespace WindowsGotify.Services; public sealed class GotifyApiService { private readonly AppConfig _config; private readonly Logger _logger; private readonly HttpClient _httpClient; private readonly object _httpLock = new(); private string _appliedBaseUrl = ""; private string _appliedToken = ""; public GotifyApiService(AppConfig config, Logger logger) { _config = config; _logger = logger; _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(15) }; SyncAuthHeaders(); } /// /// 检测 URL/Token 是否变了,变了就刷新 HttpClient 的 Authorization。 /// 注意:HttpClient.BaseAddress 构造后只读,所以我们用绝对 URI 绕过。 /// private void SyncAuthHeaders() { var baseUrl = (_config.Gotify.Url ?? "").TrimEnd('/'); var token = _config.Gotify.ClientToken ?? ""; if (_appliedBaseUrl == baseUrl && _appliedToken == token) return; lock (_httpLock) { if (_appliedBaseUrl == baseUrl && _appliedToken == token) return; // 显式给默认 header;实际请求走绝对 URI,所以 BaseAddress 永远为空 _httpClient.DefaultRequestHeaders.Authorization = string.IsNullOrEmpty(token) ? null : new AuthenticationHeaderValue("Bearer", token); _appliedBaseUrl = baseUrl; _appliedToken = token; } } private Uri BuildUri(string path) { var baseUrl = (_config.Gotify.Url ?? "").TrimEnd('/'); if (string.IsNullOrEmpty(baseUrl)) throw new InvalidOperationException("Gotify URL 未配置。请在主窗填入 URL 并保存。"); return new Uri(baseUrl + "/" + path); } /// /// 拉取 lastId 之后的增量消息(去重补齐用)。 /// public async Task> GetMessagesSinceAsync(long sinceId, int limit, CancellationToken ct) { SyncAuthHeaders(); var path = $"message?limit={Math.Clamp(limit, 1, 100)}&since={Math.Max(0, sinceId)}"; using var response = await _httpClient.GetAsync(BuildUri(path), ct); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(ct); var page = await JsonSerializer.DeserializeAsync(stream, cancellationToken: ct); return page?.Messages ?? new List(); } /// /// 拉取最近 N 条消息(历史查看用)。since=0 让服务器从最新往旧返回。 /// public async Task> GetRecentMessagesAsync(int limit, CancellationToken ct) { SyncAuthHeaders(); var path = $"message?limit={Math.Clamp(limit, 1, 100)}&since=0"; using var response = await _httpClient.GetAsync(BuildUri(path), ct); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(ct); var page = await JsonSerializer.DeserializeAsync(stream, cancellationToken: ct); var list = page?.Messages ?? new List(); // Gotify 的 since=0 行为:返回数据库里所有消息中的 limit 条;按 id 降序再取一次保证正确 return list.OrderByDescending(m => m.Id).Take(Math.Clamp(limit, 1, 100)).ToList(); } /// /// 拉取所有 Gotify Application,供 UI 用来把消息 AppId 解析为应用名。 /// public async Task> GetApplicationsAsync(CancellationToken ct) { SyncAuthHeaders(); var path = "application?limit=999"; using var response = await _httpClient.GetAsync(BuildUri(path), ct); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(ct); var list = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: ct); return list ?? new List(); } }