2024-12-16 04:50:54 +01:00
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
using System.Text;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
|
|
|
|
namespace Uwaa.Pleroma;
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
delegate void AddPair(string key, string value);
|
|
|
|
|
|
2024-11-22 09:55:08 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// A pleroma client.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class Pleroma
|
|
|
|
|
{
|
|
|
|
|
static readonly JsonSerializerOptions SerializerOptions = new()
|
|
|
|
|
{
|
|
|
|
|
PropertyNameCaseInsensitive = true,
|
2024-12-16 04:50:54 +01:00
|
|
|
|
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
2024-11-22 09:55:08 +01:00
|
|
|
|
};
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
static string CreateQuery(Action<AddPair> 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();
|
|
|
|
|
}
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
public HttpClient HttpClient;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
2024-12-16 04:54:10 +01:00
|
|
|
|
public Pleroma(string host, string? authorization, string? userAgent = "Uwaa.Pleroma/0.0")
|
2024-11-30 14:14:00 +01:00
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
UriBuilder builder = new UriBuilder();
|
|
|
|
|
builder.Scheme = "https";
|
|
|
|
|
builder.Host = host;
|
|
|
|
|
|
|
|
|
|
HttpClient = new HttpClient();
|
2024-12-16 04:54:10 +01:00
|
|
|
|
HttpClient.DefaultRequestHeaders.Add("User-agent", userAgent);
|
2024-12-16 04:50:54 +01:00
|
|
|
|
HttpClient.BaseAddress = builder.Uri;
|
|
|
|
|
HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
|
|
|
if (authorization != null)
|
|
|
|
|
HttpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization);
|
2024-11-30 14:14:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
async Task<T?> Retry<T>(HttpRequestMessage req)
|
2024-11-22 09:55:08 +01:00
|
|
|
|
{
|
2024-12-11 03:49:46 +01:00
|
|
|
|
while (true)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
HttpResponseMessage res = await HttpClient.SendAsync(req);
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
if (res.StatusCode == HttpStatusCode.NotFound)
|
|
|
|
|
return default;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
if (res.Content == null)
|
|
|
|
|
throw new HttpRequestException("Server responded with no content");
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
string text = await res.Content.ReadAsStringAsync();
|
2024-11-22 09:55:08 +01:00
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
if (res.StatusCode is >= (HttpStatusCode)400 and < (HttpStatusCode)600)
|
2024-11-22 09:55:08 +01:00
|
|
|
|
{
|
2024-12-11 03:49:46 +01:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
PleromaException? err = JsonSerializer.Deserialize<PleromaException>(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
|
|
|
|
|
}
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
if (res.StatusCode is >= (HttpStatusCode)200 and < (HttpStatusCode)300)
|
|
|
|
|
return JsonSerializer.Deserialize<T>(text, SerializerOptions) ?? throw new HttpRequestException("Couldn't deserialize response");
|
2024-12-11 03:49:46 +01:00
|
|
|
|
else
|
2024-12-16 04:50:54 +01:00
|
|
|
|
throw new HttpRequestException("Unknown error occurred");
|
2024-12-11 03:49:46 +01:00
|
|
|
|
}
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Posts a status.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="status">The status data to send, including the content, visibility, etc.</param>
|
2024-12-16 04:50:54 +01:00
|
|
|
|
/// <exception cref="HttpRequestException">Thrown if something goes wrong while uploading the status.</exception>
|
2024-12-11 03:49:46 +01:00
|
|
|
|
/// <returns>The status if posting was successful.</returns>
|
2024-12-16 04:50:54 +01:00
|
|
|
|
public Task<Status> PostStatus(string content,
|
|
|
|
|
bool sensitive = false,
|
|
|
|
|
int? expiresIn = null,
|
|
|
|
|
string? replyTo = null,
|
|
|
|
|
string? quoting = null,
|
|
|
|
|
string? language = null,
|
|
|
|
|
StatusVisibility visibility = StatusVisibility.Public)
|
2024-11-22 09:55:08 +01:00
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
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<Status>(req)!;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fetches the latest statuses from the public timeline.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<Status[]> GetTimeline()
|
|
|
|
|
{
|
|
|
|
|
//TODO: Parameters and selecting different timelines (home, public, bubble)
|
2024-12-16 04:50:54 +01:00
|
|
|
|
|
|
|
|
|
return Retry<Status[]>(new HttpRequestMessage(HttpMethod.Get, "/api/v1/timelines/public"))!;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-12-16 04:50:54 +01:00
|
|
|
|
|
2024-11-22 09:55:08 +01:00
|
|
|
|
/// <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)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<Status[]>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{account_id}/statuses"))!;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fetches the account of the pleroma client.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<Account> GetAccount()
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<Account>(new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fetches an account.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<Account?> GetAccount(string id)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<Account?>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{id}"));
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<Context?>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}/context"));
|
2024-12-11 03:49:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fetches a status.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<Status?> GetStatus(string status_id)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<Status?>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{status_id}"));
|
2024-11-30 14:14:00 +01:00
|
|
|
|
}
|
2024-11-30 14:14:20 +01:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Deletes a status.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task Delete(Status status)
|
|
|
|
|
=> Delete(status.ID);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Deletes a status by ID.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task Delete(string status_id)
|
|
|
|
|
{
|
2024-12-16 04:54:10 +01:00
|
|
|
|
//TODO: Test
|
2024-12-16 04:50:54 +01:00
|
|
|
|
return Retry<object?>(new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{status_id}"));
|
2024-12-11 03:49:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Searches for an accounts, hashtags, and/or statuses.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="query">What to search for</param>
|
|
|
|
|
/// <param name="account_id">If provided, statuses returned will be authored only by this account</param>
|
|
|
|
|
/// <param name="type">Search type</param>
|
|
|
|
|
/// <param name="resolve">Attempt WebFinger lookup</param>
|
|
|
|
|
/// <param name="following">Only include accounts that the user is following</param>
|
|
|
|
|
/// <param name="max_id">Return items older than this ID</param>
|
|
|
|
|
/// <param name="min_id">Return the oldest items newer than this ID</param>
|
|
|
|
|
/// <param name="since_id">Return the newest items newer than this ID</param>
|
|
|
|
|
/// <param name="offset">Return items past this number of items</param>
|
|
|
|
|
/// <param name="limit">Maximum number of items to return. Will be ignored if it's more than 40</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public Task<SearchResults> 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)
|
|
|
|
|
{
|
2024-12-16 04:50:54 +01:00
|
|
|
|
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<SearchResults>(req)!;
|
2024-11-22 09:55:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|