using System.Net;
using System.Net.Http.Headers;
using System.Text;
namespace Uwaa.Pleroma;
delegate void AddPair(string key, string value);
///
/// A pleroma client.
///
public class Pleroma
{
static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
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();
}
public HttpClient HttpClient;
public Pleroma(string host, string? authorization, string? userAgent = "Uwaa.Pleroma/0.0")
{
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 Retry(HttpRequestMessage req)
{
while (true)
{
HttpResponseMessage res = await HttpClient.SendAsync(req);
if (res.StatusCode == HttpStatusCode.NotFound)
return default;
if (res.Content == null)
throw new HttpRequestException("Server responded with no content");
string text = await res.Content.ReadAsStringAsync();
if (res.StatusCode is >= (HttpStatusCode)400 and < (HttpStatusCode)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 >= (HttpStatusCode)200 and < (HttpStatusCode)300)
return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpRequestException("Couldn't deserialize response");
else
throw new HttpRequestException("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 Task PostStatus(string content,
bool sensitive = false,
int? expiresIn = null,
string? replyTo = null,
string? quoting = null,
string? language = null,
StatusVisibility visibility = StatusVisibility.Public)
{
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)!;
}
///
/// Fetches the latest statuses from the public timeline.
///
public Task GetTimeline()
{
//TODO: Parameters and selecting different timelines (home, public, bubble)
return Retry(new HttpRequestMessage(HttpMethod.Get, "/api/v1/timelines/public"))!;
}
///
/// 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)
{
return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{account_id}/statuses"))!;
}
///
/// Fetches the account of the pleroma client.
///
public Task GetAccount()
{
return Retry(new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
}
///
/// Fetches an account.
///
public Task GetAccount(string id)
{
return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{id}"));
}
///
/// 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)
{
return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}/context"));
}
///
/// Fetches a status.
///
public Task GetStatus(string status_id)
{
return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}"));
}
///
/// Deletes a status.
///
public Task Delete(Status status)
=> Delete(status.ID);
///
/// Deletes a status by ID.
///
public Task Delete(string status_id)
{
//TODO: Test
return Retry