diff --git a/Pleroma.Test.sln b/Pleroma.Test.sln
new file mode 100644
index 0000000..ffa733b
--- /dev/null
+++ b/Pleroma.Test.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35027.167
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pleroma.Test", "Pleroma.Test\Pleroma.Test.csproj", "{38EB55E0-55FC-4D18-ADFC-28C3DC45DD97}"
+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
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {38EB55E0-55FC-4D18-ADFC-28C3DC45DD97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {38EB55E0-55FC-4D18-ADFC-28C3DC45DD97}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {38EB55E0-55FC-4D18-ADFC-28C3DC45DD97}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {38EB55E0-55FC-4D18-ADFC-28C3DC45DD97}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9E897444-04AE-4063-9945-9964F502994F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {80040410-D102-4350-92BE-D3E98286B1C3}
+ EndGlobalSection
+EndGlobal
diff --git a/Pleroma.Test/Pleroma.Test.csproj b/Pleroma.Test/Pleroma.Test.csproj
new file mode 100644
index 0000000..f319bc2
--- /dev/null
+++ b/Pleroma.Test/Pleroma.Test.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net8.0
+ disable
+ enable
+ Uwaa.Pleroma.Test
+ Uwaa.Pleroma.Test
+
+
+
+
+
+
diff --git a/Pleroma.Test/Program.cs b/Pleroma.Test/Program.cs
new file mode 100644
index 0000000..64b0644
--- /dev/null
+++ b/Pleroma.Test/Program.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Uwaa.Pleroma.Test;
+
+static class Program
+{
+ static void Main(string[] args)
+ {
+ MainInline().Wait();
+ }
+
+ static async Task MainInline()
+ {
+ Pleroma client = new Pleroma("alpine1.local", "Bearer Y52OX25r7rp3Lyqa-oTibk5_4sLapEKBIsxa5vWMRtw");
+ 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)
+ {
+ Console.Write('\t');
+ Console.Write(status.CreatedAt.ToShortDateString());
+ Console.Write(' ');
+ Console.Write(status.CreatedAt.ToLongTimeString());
+ Console.Write(' ');
+ Console.WriteLine(status);
+ }
+ Console.ReadKey();
+ }
+}
diff --git a/Pleroma.sln b/Pleroma.sln
new file mode 100644
index 0000000..bacad6e
--- /dev/null
+++ b/Pleroma.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35027.167
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pleroma", "Pleroma\Pleroma.csproj", "{2BDD2FEF-2B99-47AD-9C3B-B380DD8E6BB9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTTP", "HTTP\HTTP.csproj", "{8986A015-8FF2-42E3-9D74-58C8A1BABC44}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2BDD2FEF-2B99-47AD-9C3B-B380DD8E6BB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2BDD2FEF-2B99-47AD-9C3B-B380DD8E6BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2BDD2FEF-2B99-47AD-9C3B-B380DD8E6BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2BDD2FEF-2B99-47AD-9C3B-B380DD8E6BB9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8986A015-8FF2-42E3-9D74-58C8A1BABC44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8986A015-8FF2-42E3-9D74-58C8A1BABC44}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8986A015-8FF2-42E3-9D74-58C8A1BABC44}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8986A015-8FF2-42E3-9D74-58C8A1BABC44}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8D29C553-2975-44E6-9862-43D1D9ABEB75}
+ EndGlobalSection
+EndGlobal
diff --git a/Pleroma/API/PublishStatus.cs b/Pleroma/API/PublishStatus.cs
new file mode 100644
index 0000000..7e4bb1c
--- /dev/null
+++ b/Pleroma/API/PublishStatus.cs
@@ -0,0 +1,76 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma.API;
+
+[JsonConverter(typeof(PublishStatusConverter))]
+public class PublishStatus
+{
+ public string Content { get; set; }
+
+ public bool Sensitive { get; set; }
+
+ public int? ExpiresIn { get; set; } = null!;
+
+ public string? ReplyTo { get; set; }
+
+ 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.Visibility != StatusVisibility.Public)
+ {
+ writer.WritePropertyName("visibility");
+ writer.WriteStringValue(status.Visibility.ToString().ToLowerInvariant());
+ }
+
+ writer.WriteEndObject();
+ }
+}
\ No newline at end of file
diff --git a/Pleroma/Models/Account.cs b/Pleroma/Models/Account.cs
new file mode 100644
index 0000000..0ac1b11
--- /dev/null
+++ b/Pleroma/Models/Account.cs
@@ -0,0 +1,32 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class Account
+{
+ [JsonPropertyName("id")]
+ public string ID { get; set; } = null!;
+
+ [JsonPropertyName("bot")]
+ public bool Bot { get; set; }
+
+ [JsonPropertyName("display_name")]
+ public string DisplayName { get; set; } = null!;
+
+ [JsonPropertyName("username")]
+ public string Username { get; set; } = null!;
+
+ [JsonPropertyName("statuses_count")]
+ public uint StatusesCount { get; set; }
+
+ [JsonPropertyName("url")]
+ public string URL { get; set; } = null!;
+
+ [JsonPropertyName("followers_count")]
+ public uint Followers { get; set; }
+
+ [JsonPropertyName("following_count")]
+ public uint Following { get; set; }
+
+ public override string ToString() => $"@{Username}";
+}
diff --git a/Pleroma/Models/Context.cs b/Pleroma/Models/Context.cs
new file mode 100644
index 0000000..6d68f57
--- /dev/null
+++ b/Pleroma/Models/Context.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class Context
+{
+ [JsonPropertyName("ancestors")]
+ public Status[] Ancestors { get; set; } = null!;
+
+ [JsonPropertyName("descendants")]
+ public Status[] Descendants { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Pleroma/Models/PleromaException.cs b/Pleroma/Models/PleromaException.cs
new file mode 100644
index 0000000..c92c71d
--- /dev/null
+++ b/Pleroma/Models/PleromaException.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class PleromaException : Exception
+{
+ [JsonPropertyName("error")]
+ public string Text { get; set; } = null!;
+
+ public override string Message => Text;
+
+ public override string ToString() => Text;
+}
diff --git a/Pleroma/Models/PleromaObject.cs b/Pleroma/Models/PleromaObject.cs
new file mode 100644
index 0000000..f8d51b0
--- /dev/null
+++ b/Pleroma/Models/PleromaObject.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class PleromaObject
+{
+ [JsonPropertyName("content")]
+ public Content Content { get; set; } = null!;
+
+ [JsonPropertyName("local")]
+ public bool Local { get; set; }
+}
+
+public class Content
+{
+ [JsonPropertyName("text/plain")]
+ public string Plain { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Pleroma/Models/Status.cs b/Pleroma/Models/Status.cs
new file mode 100644
index 0000000..925266a
--- /dev/null
+++ b/Pleroma/Models/Status.cs
@@ -0,0 +1,54 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class Status
+{
+ [JsonPropertyName("id")]
+ public string ID { get; set; } = null!;
+
+ [JsonPropertyName("content")]
+ public string HtmlContent { get; set; } = null!;
+
+ [JsonPropertyName("account")]
+ public Account Account { get; set; } = null!;
+
+ [JsonPropertyName("created_at")]
+ public DateTime CreatedAt { get; set; }
+
+ [JsonPropertyName("pleroma")]
+ public PleromaObject Pleroma { get; set; } = null!;
+
+ [JsonPropertyName("in_reply_to_account_id")]
+ public string? ReplyToAccount { get; set; } = null;
+
+ [JsonPropertyName("in_reply_to_id")]
+ public string? ReplyToStatus { get; set; } = null;
+
+ [JsonPropertyName("mentions")]
+ public Mention[] Mentions { get; set; } = Array.Empty();
+
+ [JsonPropertyName("language")]
+ public string? Language { get; set; }
+
+ [JsonPropertyName("pinned")]
+ public bool Pinned { get; set; }
+
+ [JsonIgnore]
+ public string Content => Pleroma?.Content.Plain ?? HtmlContent;
+
+ public bool CheckMention(string id)
+ {
+ return ReplyToAccount == id || Mentions.Any(m => m.ID == id);
+ }
+
+ public override string ToString() => $"{Account?.Username ?? "unknown"}: \"{Content}\"";
+}
+
+public class Mention
+{
+ public static implicit operator string(Mention mention) => mention.ID;
+
+ [JsonPropertyName("id")]
+ public string ID { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Pleroma/Pleroma.cs b/Pleroma/Pleroma.cs
new file mode 100644
index 0000000..cc859f9
--- /dev/null
+++ b/Pleroma/Pleroma.cs
@@ -0,0 +1,156 @@
+using System.Text.Json;
+using System.Threading.Tasks;
+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) where T : class
+ {
+ req.Fields.UserAgent = UserAgent;
+ req.Fields.Authorization = Authorization;
+
+ 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)
+ 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.
+ /// The status, if posting was successful.
+ public 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];
+ return RequestJSON(req)!;
+ }
+
+ ///
+ /// 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);
+ }
+}
diff --git a/Pleroma/Pleroma.csproj b/Pleroma/Pleroma.csproj
new file mode 100644
index 0000000..0295dd5
--- /dev/null
+++ b/Pleroma/Pleroma.csproj
@@ -0,0 +1,20 @@
+
+
+ net8.0
+ disable
+ enable
+ Uwaa.Pleroma
+ Uwaa.Pleroma
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pleroma/README.md b/Pleroma/README.md
new file mode 100644
index 0000000..9326b60
--- /dev/null
+++ b/Pleroma/README.md
@@ -0,0 +1 @@
+Simple pleroma API client for .NET
\ No newline at end of file
diff --git a/Pleroma/StatusVisibility.cs b/Pleroma/StatusVisibility.cs
new file mode 100644
index 0000000..370169a
--- /dev/null
+++ b/Pleroma/StatusVisibility.cs
@@ -0,0 +1,10 @@
+namespace Uwaa.Pleroma;
+
+public enum StatusVisibility
+{
+ Public,
+ Unlisted,
+ Private,
+ Local,
+ Direct,
+}