pleroma API client init

This commit is contained in:
uwaa 2024-11-22 08:55:08 +00:00
parent 13192a1972
commit ce757177c6
14 changed files with 506 additions and 0 deletions

37
Pleroma.Test.sln Normal file
View file

@ -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

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Uwaa.Pleroma.Test</RootNamespace>
<AssemblyName>Uwaa.Pleroma.Test</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Pleroma\Pleroma.csproj" />
</ItemGroup>
</Project>

32
Pleroma.Test/Program.cs Normal file
View file

@ -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();
}
}

31
Pleroma.sln Normal file
View file

@ -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

View file

@ -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;
/// <summary>
/// Called after the status has been successfully published.
/// </summary>
[JsonIgnore]
public Action? OnPublish;
public PublishStatus(string content)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
}
}
class PublishStatusConverter : JsonConverter<PublishStatus>
{
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();
}
}

32
Pleroma/Models/Account.cs Normal file
View file

@ -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}";
}

12
Pleroma/Models/Context.cs Normal file
View file

@ -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!;
}

View file

@ -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;
}

View file

@ -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!;
}

54
Pleroma/Models/Status.cs Normal file
View file

@ -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<Mention>();
[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!;
}

156
Pleroma/Pleroma.cs Normal file
View file

@ -0,0 +1,156 @@
using System.Text.Json;
using System.Threading.Tasks;
using Uwaa.HTTP;
using Uwaa.Pleroma.API;
namespace Uwaa.Pleroma;
/// <summary>
/// A pleroma client.
/// </summary>
public class Pleroma
{
static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
};
static readonly MIMEType JsonMIMEType = new("application", "json");
/// <summary>
/// The hostname of the pleroma instance.
/// </summary>
public string Host;
/// <summary>
/// The full token, including the "Bearer" string.
/// </summary>
public string Authorization;
/// <summary>
/// The user agent string.
/// </summary>
public string UserAgent = "Uwaa.Pleroma/0.0";
public Pleroma(string host, string authorization)
{
Host = host;
Authorization = authorization;
}
async Task<T?> RequestJSON<T>(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<PleromaException>(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<T>(text, SerializerOptions) ?? throw new HttpException("Couldn't deserialize response");
else
throw new HttpException("Unknown error occurred");
}
/// <summary>
/// Posts a status.
/// </summary>
/// <param name="status">The status data to send, including the content, visibility, etc.</param>
/// <returns>The status, if posting was successful.</returns>
public Task<Status> 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<Status>(req)!;
}
/// <summary>
/// Fetches the latest statuses from the public timeline.
/// </summary>
public Task<Status[]> 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<Status[]>(req)!;
}
/// <summary>
/// Fetches the latest statuses from a user's timeline.
/// </summary>
public Task<Status[]> GetTimeline(Account account)
=> GetTimeline(account.ID);
/// <summary>
/// Fetches the latest statuses from a user's timeline.
/// </summary>
public Task<Status[]> GetTimeline(string account_id)
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{account_id}/statuses");
req.Fields.Accept = [JsonMIMEType];
return RequestJSON<Status[]>(req)!;
}
/// <summary>
/// Fetches the account of the pleroma client.
/// </summary>
public Task<Account> GetAccount()
{
HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/accounts/verify_credentials");
req.Fields.Accept = [JsonMIMEType];
return RequestJSON<Account>(req)!;
}
/// <summary>
/// Fetches an account.
/// </summary>
public Task<Account?> GetAccount(string id)
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{id}");
req.Fields.Accept = [JsonMIMEType];
return RequestJSON<Account>(req);
}
/// <summary>
/// Fetches the context of a status.
/// </summary>
public Task<Context?> GetContext(Status status)
=> GetContext(status.ID);
/// <summary>
/// Fetches the context of a status.
/// </summary>
public Task<Context?> GetContext(string status_id)
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}/context");
req.Fields.Accept = [JsonMIMEType];
return RequestJSON<Context>(req);
}
}

20
Pleroma/Pleroma.csproj Normal file
View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Uwaa.Pleroma</RootNamespace>
<AssemblyName>Uwaa.Pleroma</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\HTTP\HTTP.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="System" />
<Using Include="System.Collections.Generic" />
<Using Include="System.IO" />
<Using Include="System.Linq" />
</ItemGroup>
</Project>

1
Pleroma/README.md Normal file
View file

@ -0,0 +1 @@
Simple pleroma API client for .NET

View file

@ -0,0 +1,10 @@
namespace Uwaa.Pleroma;
public enum StatusVisibility
{
Public,
Unlisted,
Private,
Local,
Direct,
}