diff --git a/Pleroma/ASObject.cs b/Pleroma/ASObject.cs
new file mode 100644
index 0000000..26aefc1
--- /dev/null
+++ b/Pleroma/ASObject.cs
@@ -0,0 +1,21 @@
+namespace Uwaa.Pleroma;
+
+///
+/// Base class for ActivityStreams objects.
+///
+public class ASObject
+{
+ ///
+ /// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
+ ///
+ [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.Default.Equals(left, right);
+ public static bool operator !=(ASObject? left, ASObject? right) => !(left == right);
+ public override int GetHashCode() => ID.GetHashCode();
+}
diff --git a/Pleroma/Models/Account.cs b/Pleroma/Models/Account.cs
index dd23e3b..f294904 100644
--- a/Pleroma/Models/Account.cs
+++ b/Pleroma/Models/Account.cs
@@ -1,13 +1,7 @@
namespace Uwaa.Pleroma;
-public class Account
+public class Account : ASObject
{
- ///
- /// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
- ///
- [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
diff --git a/Pleroma/Models/Status.cs b/Pleroma/Models/Status.cs
index e702655..679546e 100644
--- a/Pleroma/Models/Status.cs
+++ b/Pleroma/Models/Status.cs
@@ -1,13 +1,7 @@
namespace Uwaa.Pleroma;
-public class Status
+public class Status : ASObject
{
- ///
- /// Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
- ///
- [JsonPropertyName("id")]
- public string ID { get; set; } = null!;
-
///
/// HTML-encoded status content
///
@@ -149,10 +143,10 @@ public class Status
///
/// Returns true if a status mentions or replies to a user.
///
- /// The ID of the user.
- public bool IsInteracting(string id)
+ /// The ID of the user.
+ 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
{
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
diff --git a/Pleroma/Pleroma.cs b/Pleroma/Pleroma.cs
index f18ddae..daafea4 100644
--- a/Pleroma/Pleroma.cs
+++ b/Pleroma/Pleroma.cs
@@ -51,11 +51,11 @@ public class Pleroma
HttpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization);
}
- async Task Retry(HttpRequestMessage req)
+ async Task Retry(Func 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
///
/// Fetches the account of the pleroma client.
///
- public Task GetAccount() => Retry(new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
+ public Task GetAccount() => Retry(() => new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/verify_credentials"))!;
///
/// Fetches an account by ID.
///
/// Account ID to fetch
- public Task GetAccountByID(string id) => Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(id)}"));
+ public Task GetAccountByID(string id) => Retry(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(id)}"));
///
/// Fetches an account by nickname.
///
/// User nickname
- public Task GetAccountByName(string acct) => Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/lookup?acct={WebUtility.UrlEncode(acct)}"));
+ public Task GetAccountByName(string acct) => Retry(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/lookup?acct={WebUtility.UrlEncode(acct)}"));
///
/// Follows the given account.
///
/// Account to follow
/// The new relationship, if the provided account exists.
- public Task Follow(AccountID account) => Retry(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/follow"));
+ public Task Follow(AccountID account) => Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/follow"));
///
/// Unfollows the given account.
///
/// Account to unfollow
/// The new state of the relationship, if the provided account exists.
- public Task Unfollow(AccountID account) => Retry(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/unfollow"));
+ public Task Unfollow(AccountID account) => Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/unfollow"));
+
+ ///
+ /// Gets accounts which the given account is following, if network is not hidden by the account owner.
+ ///
+ /// Account to get the follows of.
+ /// Return items older than this ID
+ /// Return the oldest items newer than this ID
+ /// Return the newest items newer than this ID
+ /// Return items past this number of items
+ /// Maximum number of items to return. Will be ignored if it's more than 40
+ public Task GetFollowing(AccountID account,
+ string? max_id = null,
+ string? min_id = null,
+ string? since_id = null,
+ int offset = 0,
+ int limit = 20)
+ {
+ return Retry(() => 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());
+ })))!;
+ }
///
/// Sets a private note for the given account.
@@ -159,12 +185,14 @@ public class Pleroma
writer.Flush();
}
- 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(req)!;
+ return Retry(() =>
+ {
+ 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 req;
+ })!;
}
///
@@ -174,7 +202,7 @@ public class Pleroma
/// A relationship object for the requested account, if the account exists.
public async Task GetRelationship(AccountID account)
{
- Relationship[]? rels = await Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/relationships?id={WebUtility.UrlEncode(account.ID)}"));
+ Relationship[]? rels = await Retry(() => 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();
}
- 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)!;
+ return Retry(() =>
+ {
+ 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 req;
+ })!;
}
static string CommonMIMEString(CommonMIMEs mime) => mime switch
@@ -327,20 +357,22 @@ public class Pleroma
async Task Upload(string mime, HttpContent content)
{
- content.Headers.ContentType = new MediaTypeHeaderValue(mime);
- content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
+ return (await Retry(() =>
{
- Name = "\"file\"",
- FileName = $"\"{Random.Shared.NextInt64()}\"",
- };
+ content.Headers.ContentType = new MediaTypeHeaderValue(mime);
+ content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
+ {
+ Name = "\"file\"",
+ FileName = $"\"{Random.Shared.NextInt64()}\"",
+ };
- MultipartFormDataContent form = [content];
- HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/media")
- {
- Content = form
- };
-
- return (await Retry(req))!;
+ MultipartFormDataContent form = [content];
+ HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/media")
+ {
+ Content = form
+ };
+ return req;
+ }))!;
}
///
@@ -366,12 +398,14 @@ public class Pleroma
writer.Flush();
}
- 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(req);
+ return Retry(() =>
+ {
+ 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 req;
+ });
}
///
@@ -379,7 +413,7 @@ public class Pleroma
///
/// The reblogged status to undo.
/// The status, if it exists.
- public Task Unreblog(StatusID status) => Retry(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unreblog"))!;
+ public Task Unreblog(StatusID status) => Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unreblog"))!;
///
@@ -387,26 +421,39 @@ public class Pleroma
///
/// The status to favourite.
/// The favourited status, if it exists.
- public Task Favourite(StatusID status) => Retry(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/favourite"))!;
+ public Task Favourite(StatusID status) => Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/favourite"))!;
///
/// Unfavourites a favorited status, removing it from the account's favourite list.
///
/// The status to unfavourite.
/// The status, if it exists.
- public Task Unfavourite(StatusID status) => Retry(new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unfavourite"))!;
+ public Task Unfavourite(StatusID status) => Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unfavourite"))!;
///
/// Fetches the latest statuses from a standard timeline.
///
+ /// The timeline to fetch.
+ /// Show only local statuses?
+ /// Show only remote statuses?
+ /// Show only statuses from the given domain
+ /// Show only statuses with media attached?
+ /// Include activities by muted users
+ /// Exclude the statuses with the given visibilities
+ /// Filter replies.
+ /// Return items older than this ID
+ /// Return the oldest items newer than this ID
+ /// Return the newest items newer than this ID
+ /// Return items past this number of items
+ /// Maximum number of items to return. Will be ignored if it's more than 40
public Task 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(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/timelines/{timelineName}" + CreateQuery(addPair =>
+ return Retry(() => 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
///
/// Fetches the latest statuses from a user's timeline.
///
+ /// The user to fetch the timeline of.
+ /// Include only pinned statuses
+ /// With tag
+ /// Show only statuses with media attached?
+ /// Include activities by muted users
+ /// Exclude reblogs
+ /// Exclude replies
+ /// Exclude the statuses with the given visibilities
+ /// Return items older than this ID
+ /// Return the oldest items newer than this ID
+ /// Return the newest items newer than this ID
+ /// Return items past this number of items
+ /// Maximum number of items to return. Will be ignored if it's more than 40
public Task GetTimeline(AccountID account,
bool pinned = false,
string? tagged = null,
@@ -473,7 +533,7 @@ public class Pleroma
int limit = 20)
{
- return Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/statuses" + CreateQuery(addPair =>
+ return Retry(() => 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
///
/// Fetches the context of a status.
///
- public Task GetContext(StatusID status) => Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/context"));
+ public Task GetContext(StatusID status) => Retry(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/context"));
///
/// Fetches a status by ID.
///
- public Task GetStatus(string id) => Retry(new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(id)}"));
+ public Task GetStatus(string id) => Retry(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/statuses/{WebUtility.UrlEncode(id)}"));
///
/// Deletes a status.
///
- public Task Delete(StatusID status) => Retry(new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}"));
+ public Task Delete(StatusID status) => Retry(() => new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}"));
///
/// Searches for an accounts, hashtags, and/or statuses.
@@ -518,7 +578,6 @@ public class Pleroma
/// Return the newest items newer than this ID
/// Return items past this number of items
/// Maximum number of items to return. Will be ignored if it's more than 40
- ///
public Task 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(() => 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(req)!;
+ })))!;
}
}
diff --git a/Pleroma/ReplyVisibility.cs b/Pleroma/ReplyVisibility.cs
index 2cbfc01..13b3c18 100644
--- a/Pleroma/ReplyVisibility.cs
+++ b/Pleroma/ReplyVisibility.cs
@@ -3,6 +3,18 @@
[JsonConverter(typeof(EnumLowerCaseConverter))]
public enum ReplyVisibility
{
+ ///
+ /// Shows all replies.
+ ///
+ All,
+
+ ///
+ /// Replies directed to you or users you follow.
+ ///
Following,
+
+ ///
+ /// Replies directed to you.
+ ///
Self,
}
\ No newline at end of file