diff --git a/Pleroma.Test.sln b/Pleroma.Test.sln index ffa733b..0195110 100644 --- a/Pleroma.Test.sln +++ b/Pleroma.Test.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pleroma.Test", "Pleroma.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pleroma", "Pleroma\Pleroma.csproj", "{9E897444-04AE-4063-9945-9964F502994F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTTP", "HTTP\HTTP.csproj", "{7F622DB8-062D-4D21-8419-F3E5C7DAEC79}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,10 +21,6 @@ Global {9E897444-04AE-4063-9945-9964F502994F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9E897444-04AE-4063-9945-9964F502994F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E897444-04AE-4063-9945-9964F502994F}.Release|Any CPU.Build.0 = Release|Any CPU - {7F622DB8-062D-4D21-8419-F3E5C7DAEC79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F622DB8-062D-4D21-8419-F3E5C7DAEC79}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F622DB8-062D-4D21-8419-F3E5C7DAEC79}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F622DB8-062D-4D21-8419-F3E5C7DAEC79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Pleroma.Test/Program.cs b/Pleroma.Test/Program.cs index 1e93413..8bbc48e 100644 --- a/Pleroma.Test/Program.cs +++ b/Pleroma.Test/Program.cs @@ -12,13 +12,7 @@ static class Program static async Task MainInline() { - Pleroma client = new Pleroma("localhost", "Bearer abcdefghijklmnopqrstuvwxyz"); - Account account = await client.GetAccount(); - Status[] statuses = await client.GetTimeline(); - - Console.WriteLine($"Account: {account} ({account.ID})"); - Console.WriteLine("Public statuses:"); - foreach (Status status in statuses) + static void PrintStatus(Status status) { Console.Write('\t'); Console.Write(status.CreatedAt.ToShortDateString()); @@ -27,6 +21,30 @@ static class Program Console.Write(' '); Console.WriteLine(status); } + + Pleroma client = new Pleroma("localhost", "Bearer abcdefghijklmnopqrstuvwxyz"); + Account account = await client.GetAccount(); + Status[] statuses = await client.GetTimeline(); + + Console.WriteLine($"Account: {account} ({account.ID})"); + Console.WriteLine("Public statuses:"); + foreach (Status status in statuses) + PrintStatus(status); + + Console.WriteLine(); + + SearchResults search = await client.Search("bepis", type: SearchType.Statuses, limit: 10); + if (search.Statuses == null || search.Statuses.Length == 0) + { + Console.WriteLine("No search results"); + } + else + { + Console.WriteLine("Search results for \"bepis\":"); + foreach (Status status in search.Statuses) + PrintStatus(status); + } + Console.ReadKey(); } } diff --git a/Pleroma/API/PublishStatus.cs b/Pleroma/API/PublishStatus.cs deleted file mode 100644 index 18926c7..0000000 --- a/Pleroma/API/PublishStatus.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace Uwaa.Pleroma.API; - -[JsonConverter(typeof(PublishStatusConverter))] -public class PublishStatus -{ - /// - /// Text content of the status. - /// - public string Content { get; set; } - - /// - /// Mark status and attached media as sensitive? - /// - public bool Sensitive { get; set; } - - /// - /// The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour. - /// - public int? ExpiresIn { get; set; } = null!; - - /// - /// ID of the status being replied to, if status is a reply - /// - public string? ReplyTo { get; set; } - - /// - /// ID of the status being quoted. - /// - public string? Quoting { get; set; } - - /// - /// ISO 639 language code for this status. - /// - public string? Language { get; set; } - - /// - /// Visibility of the posted status. - /// - public StatusVisibility Visibility { get; set; } = StatusVisibility.Public; - - /// - /// Called after the status has been successfully published. - /// - [JsonIgnore] - public Action? OnPublish; - - public PublishStatus(string content) - { - Content = content ?? throw new ArgumentNullException(nameof(content)); - } -} - -class PublishStatusConverter : JsonConverter -{ - public sealed override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(PublishStatus); - - public override PublishStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write(Utf8JsonWriter writer, PublishStatus status, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - writer.WritePropertyName("status"); - writer.WriteStringValue(status.Content); - - writer.WritePropertyName("content-type"); - writer.WriteStringValue("text/plain"); - - if (status.Sensitive) - { - writer.WritePropertyName("sensitive"); - writer.WriteBooleanValue(true); - } - - if (status.ExpiresIn.HasValue) - { - writer.WritePropertyName("expires_in"); - writer.WriteNumberValue(status.ExpiresIn.Value); - } - - if (status.ReplyTo != null) - { - writer.WritePropertyName("in_reply_to_id"); - writer.WriteStringValue(status.ReplyTo); - } - - if (status.Quoting != null) - { - writer.WritePropertyName("quote_id"); - writer.WriteStringValue(status.Quoting); - } - - if (status.Language != null) - { - writer.WritePropertyName("language"); - writer.WriteStringValue(status.Language); - } - - if (status.Visibility != StatusVisibility.Public) - { - writer.WritePropertyName("visibility"); - writer.WriteStringValue(status.Visibility.ToString().ToLowerInvariant()); - } - - writer.WriteEndObject(); - } -} \ No newline at end of file diff --git a/Pleroma/Pleroma.cs b/Pleroma/Pleroma.cs index 081a7dd..669c167 100644 --- a/Pleroma/Pleroma.cs +++ b/Pleroma/Pleroma.cs @@ -1,8 +1,11 @@ -using Uwaa.HTTP; -using Uwaa.Pleroma.API; +using System.Net; +using System.Net.Http.Headers; +using System.Text; namespace Uwaa.Pleroma; +delegate void AddPair(string key, string value); + /// /// A pleroma client. /// @@ -11,101 +14,63 @@ public class Pleroma static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, + NumberHandling = JsonNumberHandling.AllowReadingFromString, }; - static readonly MIMEType JsonMIMEType = new("application", "json"); + static string CreateQuery(Action generator) + { + StringBuilder sb = new StringBuilder(); + void addPair(string key, string value) + { + if (sb.Length == 0) + sb.Append('?'); + else + sb.Append('&'); + sb.Append(WebUtility.UrlEncode(key)); + sb.Append('='); + sb.Append(WebUtility.UrlEncode(value)); + } + generator(addPair); + return sb.ToString(); + } - /// - /// 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) + public HttpClient HttpClient; + + public Pleroma(string host, string? authorization) { - Host = host; - Authorization = authorization; + UriBuilder builder = new UriBuilder(); + builder.Scheme = "https"; + builder.Host = host; + + HttpClient = new HttpClient(); + HttpClient.DefaultRequestHeaders.Add("User-agent", UserAgent); + HttpClient.BaseAddress = builder.Uri; + HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + if (authorization != null) + HttpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization); } - async Task RequestJSON(HttpRequest req) + async Task Retry(HttpRequestMessage req) { - req.Fields.UserAgent = UserAgent; - req.Fields.Authorization = Authorization; while (true) { - HttpResponse res = await HttpClient.Request(Host, true, req); + HttpResponseMessage res = await HttpClient.SendAsync(req); - if (res.StatusCode == 404) - return; + if (res.StatusCode == HttpStatusCode.NotFound) + return default; - if (res.Content.HasValue) - { - if (res.Fields.ContentType.HasValue && !res.Fields.ContentType.Value.Match(JsonMIMEType)) - throw new HttpException(res.Content.Value.AsText); + if (res.Content == null) + throw new HttpRequestException("Server responded with no content"); - 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 - } - } - } + string text = await res.Content.ReadAsStringAsync(); - 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) + if (res.StatusCode is >= (HttpStatusCode)400 and < (HttpStatusCode)600) { try { @@ -129,10 +94,10 @@ public class Pleroma } } - if (res.StatusCode is >= 200 and < 300) - return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpException("Couldn't deserialize response"); + if (res.StatusCode is >= (HttpStatusCode)200 and < (HttpStatusCode)300) + return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpRequestException("Couldn't deserialize response"); else - throw new HttpException("Unknown error occurred"); + throw new HttpRequestException("Unknown error occurred"); } } @@ -140,16 +105,74 @@ public class Pleroma /// Posts a status. /// /// The status data to send, including the content, visibility, etc. - /// Thrown if something goes wrong while uploading the status. + /// Thrown if something goes wrong while uploading the status. /// The status if posting was successful. - public async Task PostStatus(PublishStatus status) + public Task PostStatus(string content, + bool sensitive = false, + int? expiresIn = null, + string? replyTo = null, + string? quoting = null, + string? language = null, + StatusVisibility visibility = StatusVisibility.Public) { - 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; + MemoryStream mem = new MemoryStream(); + + { + Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true }); + writer.WriteStartObject(); + + writer.WritePropertyName("status"); + writer.WriteStringValue(content); + + writer.WritePropertyName("content-type"); + writer.WriteStringValue("text/plain"); + + if (sensitive) + { + writer.WritePropertyName("sensitive"); + writer.WriteBooleanValue(true); + } + + if (expiresIn.HasValue) + { + writer.WritePropertyName("expires_in"); + writer.WriteNumberValue(expiresIn.Value); + } + + if (replyTo != null) + { + writer.WritePropertyName("in_reply_to_id"); + writer.WriteStringValue(replyTo); + } + + if (quoting != null) + { + writer.WritePropertyName("quote_id"); + writer.WriteStringValue(quoting); + } + + if (language != null) + { + writer.WritePropertyName("language"); + writer.WriteStringValue(language); + } + + if (visibility != StatusVisibility.Public) + { + writer.WritePropertyName("visibility"); + writer.WriteStringValue(visibility.ToString().ToLowerInvariant()); + } + + writer.WriteEndObject(); + writer.Flush(); + } + + mem.Position = 0; + HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/statuses"); + req.Content = new StreamContent(mem); + req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + return Retry(req)!; } /// @@ -158,11 +181,11 @@ public class Pleroma 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)!; + + return Retry(new HttpRequestMessage(HttpMethod.Get, "/api/v1/timelines/public"))!; } + /// /// Fetches the latest statuses from a user's timeline. /// @@ -174,9 +197,7 @@ public class Pleroma /// 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)!; + return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{account_id}/statuses"))!; } /// @@ -184,9 +205,7 @@ public class Pleroma /// public Task GetAccount() { - HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/accounts/verify_credentials"); - req.Fields.Accept = [JsonMIMEType]; - return RequestJSON(req)!; + return Retry(new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!; } /// @@ -194,9 +213,7 @@ public class Pleroma /// public Task GetAccount(string id) { - HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{id}"); - req.Fields.Accept = [JsonMIMEType]; - return RequestJSON(req); + return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{id}")); } /// @@ -210,9 +227,7 @@ public class Pleroma /// 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); + return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}/context")); } /// @@ -220,9 +235,7 @@ public class Pleroma /// public Task GetStatus(string status_id) { - HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}"); - req.Fields.Accept = [JsonMIMEType]; - return RequestJSON(req); + return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}")); } /// @@ -236,9 +249,7 @@ public class Pleroma /// public Task Delete(string status_id) { - HttpRequest req = new HttpRequest(HttpMethod.DELETE, $"/api/v1/statuses/{status_id}"); - req.Fields.Accept = [JsonMIMEType]; - return RequestJSON(req); + return Retry(new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{status_id}")); } /// @@ -266,18 +277,19 @@ public class Pleroma 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)!; + HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, "/api/v2/search" + CreateQuery(addPair => + { + addPair("q", query); + if (account_id != null) addPair("account_id", account_id); + if (type != SearchType.All) addPair("type", type.ToString().ToLowerInvariant()); + if (resolve) addPair("resolve", "true"); + if (following) addPair("following", "true"); + if (max_id != null) addPair("max_id", max_id); + if (min_id != null) addPair("min_id", min_id); + if (since_id != null) addPair("since_id", since_id); + if (offset > 0) addPair("offset", offset.ToString()); + if (limit != 20) addPair("limit", limit.ToString()); + })); + return Retry(req)!; } } diff --git a/Pleroma/Pleroma.csproj b/Pleroma/Pleroma.csproj index a7c45d8..6544107 100644 --- a/Pleroma/Pleroma.csproj +++ b/Pleroma/Pleroma.csproj @@ -1,6 +1,6 @@  - disable + true Uwaa.Pleroma Uwaa.Pleroma @@ -8,15 +8,6 @@ - - - - - - - - -