diff --git a/Pleroma.Test/Program.cs b/Pleroma.Test/Program.cs
index 64b0644..1e93413 100644
--- a/Pleroma.Test/Program.cs
+++ b/Pleroma.Test/Program.cs
@@ -12,7 +12,7 @@ static class Program
static async Task MainInline()
{
- Pleroma client = new Pleroma("alpine1.local", "Bearer Y52OX25r7rp3Lyqa-oTibk5_4sLapEKBIsxa5vWMRtw");
+ Pleroma client = new Pleroma("localhost", "Bearer abcdefghijklmnopqrstuvwxyz");
Account account = await client.GetAccount();
Status[] statuses = await client.GetTimeline();
diff --git a/Pleroma/API/PublishStatus.cs b/Pleroma/API/PublishStatus.cs
index 7e4bb1c..3df81e9 100644
--- a/Pleroma/API/PublishStatus.cs
+++ b/Pleroma/API/PublishStatus.cs
@@ -6,14 +6,39 @@ namespace Uwaa.Pleroma.API;
[JsonConverter(typeof(PublishStatusConverter))]
public class PublishStatus
{
+ ///
+ /// Text content of the status.
+ ///
public string Content { get; set; }
+ ///
+ /// Mark status and attached media as sensitive?
+ ///
public bool Sensitive { get; set; }
+ ///
+ /// The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.
+ ///
public int? ExpiresIn { get; set; } = null!;
+ ///
+ /// ID of the status being replied to, if status is a reply
+ ///
public string? ReplyTo { get; set; }
+ ///
+ /// ID of the status being quoted.
+ ///
+ public string? Quoting { get; set; }
+
+ ///
+ /// ISO 639 language code for this status.
+ ///
+ public string? Language { get; set; }
+
+ ///
+ /// Visibility of the posted status.
+ ///
public StatusVisibility Visibility { get; set; } = StatusVisibility.Public;
///
@@ -65,6 +90,18 @@ class PublishStatusConverter : JsonConverter
writer.WriteStringValue(status.ReplyTo);
}
+ if (status.Quoting != null)
+ {
+ writer.WritePropertyName("quote_id");
+ writer.WriteStringValue(status.Quoting);
+ }
+
+ if (status.Language != null)
+ {
+ writer.WritePropertyName("language");
+ writer.WriteStringValue(status.Language);
+ }
+
if (status.Visibility != StatusVisibility.Public)
{
writer.WritePropertyName("visibility");
diff --git a/Pleroma/EnumLowerCaseConverter.cs b/Pleroma/EnumLowerCaseConverter.cs
new file mode 100644
index 0000000..038dfa7
--- /dev/null
+++ b/Pleroma/EnumLowerCaseConverter.cs
@@ -0,0 +1,22 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+///
+/// Converts to and from enum values in lowercase.
+///
+class EnumLowerCaseConverter : JsonConverter where T : struct
+{
+ public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
+
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return Enum.Parse(reader.GetString() ?? throw new NullReferenceException("Expected a string, got null"), true);
+ }
+
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString()?.ToLowerInvariant());
+ }
+}
\ No newline at end of file
diff --git a/Pleroma/Models/Account.cs b/Pleroma/Models/Account.cs
index 0ac1b11..e8e86c3 100644
--- a/Pleroma/Models/Account.cs
+++ b/Pleroma/Models/Account.cs
@@ -4,6 +4,9 @@ namespace Uwaa.Pleroma;
public class Account
{
+ ///
+ /// 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!;
diff --git a/Pleroma/Models/Hashtag.cs b/Pleroma/Models/Hashtag.cs
new file mode 100644
index 0000000..d27c885
--- /dev/null
+++ b/Pleroma/Models/Hashtag.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class Hashtag
+{
+ ///
+ /// The value of the hashtag after the # sign
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = null!;
+
+ ///
+ /// A link to the hashtag on the instance
+ ///
+ [JsonPropertyName("url")]
+ public string URL { get; set; } = null!;
+
+ public override string ToString() => $"#{Name}";
+}
\ No newline at end of file
diff --git a/Pleroma/Models/SearchResults.cs b/Pleroma/Models/SearchResults.cs
new file mode 100644
index 0000000..e560572
--- /dev/null
+++ b/Pleroma/Models/SearchResults.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace Uwaa.Pleroma;
+
+public class SearchResults
+{
+ ///
+ /// Accounts which match the search query.
+ ///
+ [JsonPropertyName("accounts")]
+ public Account[]? Accounts { get; set; }
+
+ ///
+ /// Hashtags which match the search query.
+ ///
+ [JsonPropertyName("hashtags")]
+ public Hashtag[]? Hashtags { get; set; }
+
+ ///
+ /// Statuses which match the search query.
+ ///
+ [JsonPropertyName("statuses")]
+ public Status[]? Statuses { get; set; }
+}
diff --git a/Pleroma/Models/Status.cs b/Pleroma/Models/Status.cs
index b8ae5a5..a243972 100644
--- a/Pleroma/Models/Status.cs
+++ b/Pleroma/Models/Status.cs
@@ -4,36 +4,72 @@ namespace Uwaa.Pleroma;
public class Status
{
+ ///
+ /// 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
+ ///
[JsonPropertyName("content")]
public string HtmlContent { get; set; } = null!;
+ ///
+ /// The account that authored this status
+ ///
[JsonPropertyName("account")]
public Account Account { get; set; } = null!;
+ ///
+ /// The date when this status was created
+ ///
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("pleroma")]
public PleromaObject Pleroma { get; set; } = null!;
+ ///
+ /// ID of the account being replied to
+ ///
[JsonPropertyName("in_reply_to_account_id")]
public string? ReplyToAccount { get; set; } = null;
+ ///
+ /// ID of the status being replied
+ ///
[JsonPropertyName("in_reply_to_id")]
public string? ReplyToStatus { get; set; } = null;
+ ///
+ /// Mentions of users within the status content
+ ///
[JsonPropertyName("mentions")]
public Mention[] Mentions { get; set; } = Array.Empty();
+ ///
+ /// Primary language of this status
+ ///
[JsonPropertyName("language")]
public string? Language { get; set; }
+ ///
+ /// Have you pinned this status? Only appears if the status is pinnable.
+ ///
[JsonPropertyName("pinned")]
public bool Pinned { get; set; }
+ ///
+ /// Visibility of this status
+ ///
+ [JsonPropertyName("visibility")]
+ public StatusVisibility Visibility { get; set; }
+
+ ///
+ /// Plain text content of that status.
+ ///
[JsonIgnore]
public string Content => Pleroma?.Content?.Plain ?? HtmlContent;
@@ -49,6 +85,29 @@ public class Mention
{
public static implicit operator string(Mention mention) => mention.ID;
+ ///
+ /// The webfinger acct: URI of the mentioned user. Equivalent to username for local users, or username@domain for remote users.
+ ///
+ [JsonPropertyName("acct")]
+ public string Account { get; set; } = null!;
+
+ ///
+ /// The account id of the mentioned user
+ ///
[JsonPropertyName("id")]
public string ID { get; set; } = null!;
-}
\ No newline at end of file
+
+ ///
+ /// The location of the mentioned user's profile
+ ///
+ [JsonPropertyName("url")]
+ public string URL { get; set; } = null!;
+
+ ///
+ /// The username of the mentioned user
+ ///
+ [JsonPropertyName("username")]
+ public string Username { get; set; } = null!;
+
+ public override string ToString() => $"@{Account}";
+}
diff --git a/Pleroma/Pleroma.cs b/Pleroma/Pleroma.cs
index deee4cf..e1d7dcd 100644
--- a/Pleroma/Pleroma.cs
+++ b/Pleroma/Pleroma.cs
@@ -40,134 +40,118 @@ public class Pleroma
Authorization = authorization;
}
- async Task RequestJSONRetry(HttpRequest req)
- {
- while (true)
- {
- try
- {
- await RequestJSON(req);
- return;
- }
- catch (PleromaException e)
- {
- if (e.Text == "Throttled")
- {
- await Task.Delay(5000);
- continue;
- }
- else
- throw;
- }
- }
- }
-
- async Task RequestJSONRetry(HttpRequest req) where T : class
- {
- while (true)
- {
- try
- {
- return await RequestJSON(req);
- }
- catch (PleromaException e)
- {
- if (e.Text == "Throttled")
- {
- await Task.Delay(5000);
- continue;
- }
- else
- throw;
- }
- }
- }
-
async Task RequestJSON(HttpRequest req)
{
req.Fields.UserAgent = UserAgent;
req.Fields.Authorization = Authorization;
-
- HttpResponse res = await HttpClient.Request(Host, true, req);
-
- if (res.StatusCode == 404)
- return;
-
- 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)
+ while (true)
{
- try
- {
- PleromaException? err = JsonSerializer.Deserialize(text, SerializerOptions);
- if (err != null && err.Text != null)
- throw err;
- }
- catch (JsonException)
- {
- //Not an error
- }
- }
+ HttpResponse res = await HttpClient.Request(Host, true, req);
- if (res.StatusCode is not >= 200 or not < 300)
- throw new HttpException("Unknown error occurred");
+ if (res.StatusCode == 404)
+ return;
+
+ if (res.Content.HasValue)
+ {
+ if (res.Fields.ContentType.HasValue && !res.Fields.ContentType.Value.Match(JsonMIMEType))
+ throw new HttpException(res.Content.Value.AsText);
+
+ if (res.StatusCode is >= 400 and < 600)
+ {
+ try
+ {
+ string text = res.Content.Value.AsText;
+ 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 not >= 200 or not < 300)
+ throw new HttpException("Unknown error occurred");
+ }
}
async Task RequestJSON(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)
+ req.Fields.Accept = [JsonMIMEType];
+ while (true)
{
- try
- {
- PleromaException? err = JsonSerializer.Deserialize(text, SerializerOptions);
- if (err != null && err.Text != null)
- throw err;
- }
- catch (JsonException)
- {
- //Not an error
- }
- }
+ HttpResponse res = await HttpClient.Request(Host, true, req);
- if (res.StatusCode is >= 200 and < 300)
- return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpException("Couldn't deserialize response");
- else
- throw new HttpException("Unknown error occurred");
+ 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(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 >= 200 and < 300)
+ return JsonSerializer.Deserialize(text, SerializerOptions) ?? throw new HttpException("Couldn't deserialize response");
+ else
+ throw new HttpException("Unknown error occurred");
+ }
}
///
/// Posts a status.
///
/// The status data to send, including the content, visibility, etc.
- /// The status, if posting was successful.
- public Task PostStatus(PublishStatus status)
+ /// Thrown if something goes wrong while uploading the status.
+ /// The status if posting was successful.
+ public async Task 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 RequestJSONRetry(req)!;
+ Status result = (await RequestJSON(req))!;
+ status.OnPublish?.Invoke();
+ return result;
}
///
@@ -178,7 +162,7 @@ public class Pleroma
//TODO: Parameters and selecting different timelines (home, public, bubble)
HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/timelines/public");
req.Fields.Accept = [ JsonMIMEType ];
- return RequestJSONRetry(req)!;
+ return RequestJSON(req)!;
}
///
@@ -194,7 +178,7 @@ public class Pleroma
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{account_id}/statuses");
req.Fields.Accept = [JsonMIMEType];
- return RequestJSONRetry(req)!;
+ return RequestJSON(req)!;
}
///
@@ -204,7 +188,7 @@ public class Pleroma
{
HttpRequest req = new HttpRequest(HttpMethod.GET, "/api/v1/accounts/verify_credentials");
req.Fields.Accept = [JsonMIMEType];
- return RequestJSONRetry(req)!;
+ return RequestJSON(req)!;
}
///
@@ -214,7 +198,7 @@ public class Pleroma
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/accounts/{id}");
req.Fields.Accept = [JsonMIMEType];
- return RequestJSONRetry(req);
+ return RequestJSON(req);
}
///
@@ -230,7 +214,17 @@ public class Pleroma
{
HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}/context");
req.Fields.Accept = [JsonMIMEType];
- return RequestJSONRetry(req);
+ return RequestJSON(req);
+ }
+
+ ///
+ /// Fetches a status.
+ ///
+ public Task GetStatus(string status_id)
+ {
+ HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v1/statuses/{status_id}");
+ req.Fields.Accept = [JsonMIMEType];
+ return RequestJSON(req);
}
///
@@ -246,6 +240,46 @@ public class Pleroma
{
HttpRequest req = new HttpRequest(HttpMethod.DELETE, $"/api/v1/statuses/{status_id}");
req.Fields.Accept = [JsonMIMEType];
- return RequestJSONRetry(req);
+ return RequestJSON(req);
+ }
+
+ ///
+ /// Searches for an accounts, hashtags, and/or statuses.
+ ///
+ /// What to search for
+ /// If provided, statuses returned will be authored only by this account
+ /// Search type
+ /// Attempt WebFinger lookup
+ /// Only include accounts that the user is following
+ /// 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 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)
+ {
+ HttpRequest req = new HttpRequest(HttpMethod.GET, $"/api/v2/search");
+ req.Fields.Accept = [JsonMIMEType];
+ req.Query["q"] = query;
+ if (account_id != null) req.Query["account_id"] = account_id;
+ if (type != SearchType.All) req.Query["type"] = type.ToString().ToLowerInvariant();
+ if (resolve) req.Query["resolve"] = "true";
+ if (following) req.Query["following"] = "true";
+ if (max_id != null) req.Query["max_id"] = max_id;
+ if (min_id != null) req.Query["min_id"] = min_id;
+ if (since_id != null) req.Query["since_id"] = since_id;
+ if (offset > 0) req.Query["offset"] = offset.ToString();
+ if (limit != 20) req.Query["limit"] = limit.ToString();
+ return RequestJSON(req)!;
}
}
diff --git a/Pleroma/SearchType.cs b/Pleroma/SearchType.cs
new file mode 100644
index 0000000..5ada74f
--- /dev/null
+++ b/Pleroma/SearchType.cs
@@ -0,0 +1,9 @@
+namespace Uwaa.Pleroma;
+
+public enum SearchType
+{
+ All,
+ Accounts,
+ Hashtags,
+ Statuses
+}
\ No newline at end of file
diff --git a/Pleroma/StatusVisibility.cs b/Pleroma/StatusVisibility.cs
index 370169a..00acd77 100644
--- a/Pleroma/StatusVisibility.cs
+++ b/Pleroma/StatusVisibility.cs
@@ -1,10 +1,14 @@
-namespace Uwaa.Pleroma;
+using System.Text.Json.Serialization;
+namespace Uwaa.Pleroma;
+
+[JsonConverter(typeof(EnumLowerCaseConverter))]
public enum StatusVisibility
{
Public,
Unlisted,
- Private,
Local,
+ Private,
Direct,
+ List
}