using Uwaa.HTTP; using Uwaa.Pleroma.API; namespace Uwaa.Pleroma; /// /// A pleroma client. /// public class Pleroma { static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNameCaseInsensitive = true, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, }; static readonly MIMEType JsonMIMEType = new("application", "json"); /// /// The hostname of the pleroma instance. /// public string Host; /// /// The full token, including the "Bearer" string. /// public string Authorization; /// /// The user agent string. /// public string UserAgent = "Uwaa.Pleroma/0.0"; public Pleroma(string host, string authorization) { Host = host; Authorization = authorization; } async Task RequestJSON(HttpRequest req) { req.Fields.UserAgent = UserAgent; req.Fields.Authorization = Authorization; while (true) { HttpResponse res = await HttpClient.Request(Host, true, req); if (res.StatusCode == 404) return; if (res.Content.HasValue) { if (res.Fields.ContentType.HasValue && !res.Fields.ContentType.Value.Match(JsonMIMEType)) throw new HttpException(res.Content.Value.AsText); if (res.StatusCode is >= 400 and < 600) { try { string text = res.Content.Value.AsText; PleromaException? err = JsonSerializer.Deserialize(text, SerializerOptions); if (err != null && err.Text != null) { if (err.Text == "Throttled") { await Task.Delay(5000); continue; //Retry } else { throw err; } } } catch (JsonException) { //Not an error } } } if (res.StatusCode is not >= 200 or not < 300) throw new HttpException("Unknown error occurred"); } } async Task RequestJSON(HttpRequest req) where T : class { req.Fields.UserAgent = UserAgent; req.Fields.Authorization = Authorization; req.Fields.Accept = [JsonMIMEType]; while (true) { HttpResponse res = await HttpClient.Request(Host, true, req); if (res.StatusCode == 404) return null; if (!res.Fields.ContentType.HasValue || !res.Fields.ContentType.Value.Match(JsonMIMEType)) throw new HttpException("Server did not respond with JSON" + (res.Content.HasValue ? ", got: " + res.Content.Value.AsText : null)); if (!res.Content.HasValue) throw new HttpException("Server responded with no content"); string text = res.Content.Value.AsText; if (res.StatusCode is >= 400 and < 600) { try { PleromaException? err = JsonSerializer.Deserialize(text, SerializerOptions); if (err != null && err.Text != null) { if (err.Text == "Throttled") { await Task.Delay(5000); continue; //Retry } else { throw err; } } } catch (JsonException) { //Not an error } } if (res.StatusCode is >= 200 and < 300) return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpException("Couldn't deserialize response"); else throw new HttpException("Unknown error occurred"); } } /// /// Posts a status. /// /// The status data to send, including the content, visibility, etc. /// Thrown if something goes wrong while uploading the status. /// The status if posting was successful. public async Task PostStatus(PublishStatus status) { HttpRequest req = new HttpRequest(HttpMethod.POST, "/api/v1/statuses"); req.Content = new HttpContent(JsonMIMEType, JsonSerializer.SerializeToUtf8Bytes(status, SerializerOptions)); req.Fields.Accept = [JsonMIMEType]; Status result = (await RequestJSON(req))!; status.OnPublish?.Invoke(); return result; } /// /// Fetches the latest statuses from the public timeline. /// public Task GetTimeline() { //TODO: Parameters and selecting different timelines (home, public, bubble) HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/timelines/public"); req.Fields.Accept = [ JsonMIMEType ]; return RequestJSON(req)!; } /// /// Fetches the latest statuses from a user's timeline. /// public Task GetTimeline(Account account) => GetTimeline(account.ID); /// /// Fetches the latest statuses from a user's timeline. /// public Task GetTimeline(string account_id) { HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{account_id}/statuses"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req)!; } /// /// Fetches the account of the pleroma client. /// public Task GetAccount() { HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/accounts/verify_credentials"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req)!; } /// /// Fetches an account. /// public Task GetAccount(string id) { HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{id}"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req); } /// /// Fetches the context of a status. /// public Task GetContext(Status status) => GetContext(status.ID); /// /// Fetches the context of a status. /// public Task GetContext(string status_id) { HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}/context"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req); } /// /// Fetches a status. /// public Task GetStatus(string status_id) { HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req); } /// /// Deletes a status. /// public Task Delete(Status status) => Delete(status.ID); /// /// Deletes a status by ID. /// public Task Delete(string status_id) { HttpRequest req = new HttpRequest(HttpMethod.DELETE, $"/api/v1/statuses/{status_id}"); req.Fields.Accept = [JsonMIMEType]; return RequestJSON(req); } /// /// Searches for an accounts, hashtags, and/or statuses. /// /// What to search for /// If provided, statuses returned will be authored only by this account /// Search type /// Attempt WebFinger lookup /// Only include accounts that the user is following /// Return items older than this ID /// Return the oldest items newer than this ID /// Return the newest items newer than this ID /// Return items past this number of items /// Maximum number of items to return. Will be ignored if it's more than 40 /// public Task Search(string query, string? account_id = null, SearchType type = SearchType.All, bool resolve = false, bool following = false, string? max_id = null, string? min_id = null, string? since_id = null, int offset = 0, int limit = 20) { HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v2/search"); req.Fields.Accept = [JsonMIMEType]; req.Query["q"] = query; if (account_id != null) req.Query["account_id"] = account_id; if (type != SearchType.All) req.Query["type"] = type.ToString().ToLowerInvariant(); if (resolve) req.Query["resolve"] = "true"; if (following) req.Query["following"] = "true"; if (max_id != null) req.Query["max_id"] = max_id; if (min_id != null) req.Query["min_id"] = min_id; if (since_id != null) req.Query["since_id"] = since_id; if (offset > 0) req.Query["offset"] = offset.ToString(); if (limit != 20) req.Query["limit"] = limit.ToString(); return RequestJSON(req)!; } }