pleroma: stuff

This commit is contained in:
uwaa 2024-12-21 19:01:57 +00:00
parent b96992d7f0
commit 97e7d5db5d
5 changed files with 177 additions and 73 deletions

21
Pleroma/ASObject.cs Normal file
View file

@ -0,0 +1,21 @@
namespace Uwaa.Pleroma;
/// <summary>
/// Base class for ActivityStreams objects.
/// </summary>
public class ASObject
{
/// <summary>
/// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
/// </summary>
[JsonPropertyName("id")]
public string ID { get; set; } = null!;
public override string ToString() => ID;
public override bool Equals(object? obj) => Equals(obj as ASObject);
public bool Equals(ASObject? other) => other is not null && ID == other.ID;
public static bool operator ==(ASObject? left, ASObject? right) => EqualityComparer<ASObject>.Default.Equals(left, right);
public static bool operator !=(ASObject? left, ASObject? right) => !(left == right);
public override int GetHashCode() => ID.GetHashCode();
}

View file

@ -1,13 +1,7 @@
namespace Uwaa.Pleroma;
public class Account
public class Account : ASObject
{
/// <summary>
/// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
/// </summary>
[JsonPropertyName("id")]
public string ID { get; set; } = null!;
[JsonPropertyName("bot")]
public bool Bot { get; set; }
@ -74,6 +68,17 @@ public readonly struct AccountID(string id)
public readonly string ID = id;
public override string ToString() => ID;
public static bool operator ==(AccountID left, AccountID right) => left.Equals(right);
public static bool operator !=(AccountID left, AccountID right) => !(left == right);
public override bool Equals(object? obj) => (obj is AccountID id && Equals(id)) || (obj is Account status && Equals(status));
public bool Equals(AccountID other) => ID == other.ID;
public override int GetHashCode() => ID.GetHashCode();
}
class AccountIDConverter : JsonConverter<AccountID>

View file

@ -1,13 +1,7 @@
namespace Uwaa.Pleroma;
public class Status
public class Status : ASObject
{
/// <summary>
/// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
/// </summary>
[JsonPropertyName("id")]
public string ID { get; set; } = null!;
/// <summary>
/// HTML-encoded status content
/// </summary>
@ -149,10 +143,10 @@ public class Status
/// <summary>
/// Returns true if a status mentions or replies to a user.
/// </summary>
/// <param name="id">The ID of the user.</param>
public bool IsInteracting(string id)
/// <param name="user">The ID of the user.</param>
public bool IsInteracting(AccountID user)
{
return ReplyToAccount == id || Mentions.Any(m => m.ID == id);
return ReplyToAccount == user.ID || Mentions.Any(m => m.ID == user.ID);
}
public override string ToString() => $"{Account?.Username ?? "unknown"}: \"{Content}\"";
@ -164,20 +158,23 @@ public class Status
public class PleromaStatusData
{
[JsonPropertyName("content")]
public Content Content { get; set; } = null!;
public PleromaStatusContent Content { get; set; } = null!;
[JsonPropertyName("local")]
public bool Local { get; set; }
[JsonPropertyName("quote_id")]
public string? QuoteID { get; set; }
}
public class Content
public class PleromaStatusContent
{
[JsonPropertyName("text/plain")]
public string Plain { get; set; } = null!;
}
[JsonConverter(typeof(StatusIDConverter))]
public readonly struct StatusID(string id)
public readonly struct StatusID(string id) : IEquatable<StatusID>
{
public static implicit operator StatusID(string id) => new StatusID(id);
@ -186,6 +183,17 @@ public readonly struct StatusID(string id)
public readonly string ID = id;
public override string ToString() => ID;
public static bool operator ==(StatusID left, StatusID right) => left.Equals(right);
public static bool operator !=(StatusID left, StatusID right) => !(left == right);
public override bool Equals(object? obj) => (obj is StatusID id && Equals(id)) || (obj is Status status && Equals(status));
public bool Equals(StatusID other) => ID == other.ID;
public override int GetHashCode() => ID.GetHashCode();
}
class StatusIDConverter : JsonConverter<StatusID>

View file

@ -51,11 +51,11 @@ public class Pleroma
HttpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization);
}
async Task<T?> Retry<T>(HttpRequestMessage req)
async Task<T?> Retry<T>(Func<HttpRequestMessage> reqFactory)
{
while (true)
{
HttpResponseMessage res = await HttpClient.SendAsync(req);
HttpResponseMessage res = await HttpClient.SendAsync(reqFactory());
if (res.StatusCode == HttpStatusCode.NotFound)
return default;
@ -110,33 +110,59 @@ public class Pleroma
/// <summary>
/// Fetches the account of the pleroma client.
/// </summary>
public Task<Account> GetAccount() => Retry<Account>(new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
public Task<Account> GetAccount() => Retry<Account>(() => new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
/// <summary>
/// Fetches an account by ID.
/// </summary>
/// <param name="id">Account ID to fetch</param>
public Task<Account?> GetAccountByID(string id) => Retry<Account>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(id)}"));
public Task<Account?> GetAccountByID(string id) => Retry<Account>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(id)}"));
/// <summary>
/// Fetches an account by nickname.
/// </summary>
/// <param name="acct">User nickname</param>
public Task<Account?> GetAccountByName(string acct) => Retry<Account>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/lookup?acct={WebUtility.UrlEncode(acct)}"));
public Task<Account?> GetAccountByName(string acct) => Retry<Account>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/lookup?acct={WebUtility.UrlEncode(acct)}"));
/// <summary>
/// Follows the given account.
/// </summary>
/// <param name="id">Account to follow</param>
/// <returns>The new relationship, if the provided account exists.</returns>
public Task<Relationship?> Follow(AccountID account) => Retry<Relationship>(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/follow"));
public Task<Relationship?> Follow(AccountID account) => Retry<Relationship>(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/follow"));
/// <summary>
/// Unfollows the given account.
/// </summary>
/// <param name="id">Account to unfollow</param>
/// <returns>The new state of the relationship, if the provided account exists.</returns>
public Task<Relationship?> Unfollow(AccountID account) => Retry<Relationship>(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/unfollow"));
public Task<Relationship?> Unfollow(AccountID account) => Retry<Relationship>(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/unfollow"));
/// <summary>
/// Gets accounts which the given account is following, if network is not hidden by the account owner.
/// </summary>
/// <param name="account">Account to get the follows of.</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>
public Task<Account[]> GetFollowing(AccountID account,
string? max_id = null,
string? min_id = null,
string? since_id = null,
int offset = 0,
int limit = 20)
{
return Retry<Account[]>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{account.ID}/following" + CreateQuery(addPair =>
{
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());
})))!;
}
/// <summary>
/// Sets a private note for the given account.
@ -159,12 +185,14 @@ public class Pleroma
writer.Flush();
}
return Retry<Relationship>(() =>
{
mem.Position = 0;
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/note");
req.Content = new StreamContent(mem);
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return Retry<Relationship>(req)!;
return req;
})!;
}
/// <summary>
@ -174,7 +202,7 @@ public class Pleroma
/// <returns>A relationship object for the requested account, if the account exists.</returns>
public async Task<Relationship?> GetRelationship(AccountID account)
{
Relationship[]? rels = await Retry<Relationship[]>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/relationships?id={WebUtility.UrlEncode(account.ID)}"));
Relationship[]? rels = await Retry<Relationship[]>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/relationships?id={WebUtility.UrlEncode(account.ID)}"));
if (rels == null || rels.Length == 0)
return null;
else
@ -270,12 +298,14 @@ public class Pleroma
writer.Flush();
}
return Retry<Status>(() =>
{
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)!;
return req;
})!;
}
static string CommonMIMEString(CommonMIMEs mime) => mime switch
@ -326,6 +356,8 @@ public class Pleroma
public Task<Attachment> Upload(string mime, Stream stream) => Upload(mime, new StreamContent(stream));
async Task<Attachment> Upload(string mime, HttpContent content)
{
return (await Retry<Attachment>(() =>
{
content.Headers.ContentType = new MediaTypeHeaderValue(mime);
content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
@ -339,8 +371,8 @@ public class Pleroma
{
Content = form
};
return (await Retry<Attachment>(req))!;
return req;
}))!;
}
/// <summary>
@ -366,12 +398,14 @@ public class Pleroma
writer.Flush();
}
return Retry<Status>(() =>
{
mem.Position = 0;
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/reblog");
req.Content = new StreamContent(mem);
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return Retry<Status>(req);
return req;
});
}
/// <summary>
@ -379,7 +413,7 @@ public class Pleroma
/// </summary>
/// <param name="status">The reblogged status to undo.</param>
/// <returns>The status, if it exists.</returns>
public Task<Status?> Unreblog(StatusID status) => Retry<Status>(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unreblog"))!;
public Task<Status?> Unreblog(StatusID status) => Retry<Status>(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unreblog"))!;
/// <summary>
@ -387,26 +421,39 @@ public class Pleroma
/// </summary>
/// <param name="status">The status to favourite.</param>
/// <returns>The favourited status, if it exists.</returns>
public Task<Status?> Favourite(StatusID status) => Retry<Status>(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/favourite"))!;
public Task<Status?> Favourite(StatusID status) => Retry<Status>(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/favourite"))!;
/// <summary>
/// Unfavourites a favorited status, removing it from the account's favourite list.
/// </summary>
/// <param name="status">The status to unfavourite.</param>
/// <returns>The status, if it exists.</returns>
public Task<Status?> Unfavourite(StatusID status) => Retry<Status>(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unfavourite"))!;
public Task<Status?> Unfavourite(StatusID status) => Retry<Status>(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unfavourite"))!;
/// <summary>
/// Fetches the latest statuses from a standard timeline.
/// </summary>
/// <param name="timeline">The timeline to fetch.</param>
/// <param name="local">Show only local statuses?</param>
/// <param name="remote">Show only remote statuses?</param>
/// <param name="instance">Show only statuses from the given domain</param>
/// <param name="only_media">Show only statuses with media attached?</param>
/// <param name="with_muted">Include activities by muted users</param>
/// <param name="exclude_visibilities">Exclude the statuses with the given visibilities</param>
/// <param name="reply_visibility">Filter replies.</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>
public Task<Status[]> GetTimeline(Timeline timeline,
bool local = false,
bool remote = false,
string? instance = null,
bool only_media = false,
bool remote = false,
bool with_muted = false,
StatusVisibility[]? exclude_visibilities = null,
ReplyVisibility? reply_visibility = null,
ReplyVisibility reply_visibility = ReplyVisibility.All,
string? max_id = null,
string? min_id = null,
string? since_id = null,
@ -437,7 +484,7 @@ public class Pleroma
throw new ArgumentException("Invalid timeline", nameof(timeline));
}
return Retry<Status[]>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/timelines/{timelineName}" + CreateQuery(addPair =>
return Retry<Status[]>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/timelines/{timelineName}" + CreateQuery(addPair =>
{
if (local) addPair("local", "true");
if (instance != null) addPair("instance", instance);
@ -445,7 +492,7 @@ public class Pleroma
if (remote) addPair("remote", "true");
if (with_muted) addPair("with_muted", "true");
if (exclude_visibilities != null) addPair("exclude_visibilities", JsonSerializer.Serialize(exclude_visibilities));
if (reply_visibility.HasValue) addPair("reply_visibility", reply_visibility.Value.ToString().ToLowerInvariant());
if (reply_visibility != ReplyVisibility.All) addPair("reply_visibility", reply_visibility.ToString().ToLowerInvariant());
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);
@ -458,6 +505,19 @@ public class Pleroma
/// <summary>
/// Fetches the latest statuses from a user's timeline.
/// </summary>
/// <param name="account">The user to fetch the timeline of.</param>
/// <param name="pinned">Include only pinned statuses</param>
/// <param name="tagged">With tag</param>
/// <param name="only_media">Show only statuses with media attached?</param>
/// <param name="with_muted">Include activities by muted users</param>
/// <param name="exclude_reblogs">Exclude reblogs</param>
/// <param name="exclude_replies">Exclude replies</param>
/// <param name="exclude_visibilities">Exclude the statuses with the given visibilities</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>
public Task<Status[]?> GetTimeline(AccountID account,
bool pinned = false,
string? tagged = null,
@ -473,7 +533,7 @@ public class Pleroma
int limit = 20)
{
return Retry<Status[]>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/statuses" + CreateQuery(addPair =>
return Retry<Status[]>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/statuses" + CreateQuery(addPair =>
{
if (pinned) addPair("pinned", "true");
if (tagged != null) addPair("tagged", tagged);
@ -493,17 +553,17 @@ public class Pleroma
/// <summary>
/// Fetches the context of a status.
/// </summary>
public Task<Context?> GetContext(StatusID status) => Retry<Context>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/context"));
public Task<Context?> GetContext(StatusID status) => Retry<Context>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/context"));
/// <summary>
/// Fetches a status by ID.
/// </summary>
public Task<Status?> GetStatus(string id) => Retry<Status>(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(id)}"));
public Task<Status?> GetStatus(string id) => Retry<Status>(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(id)}"));
/// <summary>
/// Deletes a status.
/// </summary>
public Task<Status?> Delete(StatusID status) => Retry<Status>(new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}"));
public Task<Status?> Delete(StatusID status) => Retry<Status>(() => new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}"));
/// <summary>
/// Searches for an accounts, hashtags, and/or statuses.
@ -518,7 +578,6 @@ public class Pleroma
/// <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,
@ -530,7 +589,7 @@ public class Pleroma
int offset = 0,
int limit = 20)
{
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, "/api/v2/search" + CreateQuery(addPair =>
return Retry<SearchResults>(() => new HttpRequestMessage(HttpMethod.Get, "/api/v2/search" + CreateQuery(addPair =>
{
addPair("q", query);
if (account_id != null) addPair("account_id", account_id);
@ -542,7 +601,6 @@ public class Pleroma
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)!;
})))!;
}
}

View file

@ -3,6 +3,18 @@
[JsonConverter(typeof(EnumLowerCaseConverter<ReplyVisibility>))]
public enum ReplyVisibility
{
/// <summary>
/// Shows all replies.
/// </summary>
All,
/// <summary>
/// Replies directed to you or users you follow.
/// </summary>
Following,
/// <summary>
/// Replies directed to you.
/// </summary>
Self,
}