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, }; internal 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); } public Pleroma(HttpClient client) { HttpClient = client; } internal Task Retry(Func reqFactory) { return Retry(reqFactory); } internal async Task Retry(Func reqFactory) { while (true) { HttpResponseMessage res = await HttpClient.SendAsync(reqFactory()); if (res.StatusCode == HttpStatusCode.NotFound) return default; if (res.Content == null) { if (typeof(T) == typeof(object)) return default; else throw new HttpRequestException("Server responded with no content"); } string text = await res.Content.ReadAsStringAsync(); if (res.StatusCode is >= (HttpStatusCode)300) { try { PleromaSimpleException? 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 a simple error } try { PleromaAggregateException? err = JsonSerializer.Deserialize(text, SerializerOptions); if (err != null && err.Text != null) throw err; } catch (JsonException) { //Not an aggregate error } throw new HttpRequestException(text); } 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"); } } /// /// Fetches the account of the pleroma client. /// 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)}")); /// /// 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)}")); /// /// 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")); /// /// 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")); /// /// 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) { string path = $"/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()); }); return Retry(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// Sets a private note for the given account. /// /// Account ID /// Account note body /// The new relationship with the given account. public Task SetUserNote(AccountID account, string comment) { MemoryStream mem = new MemoryStream(); using (Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true })) { writer.WriteStartObject(); writer.WritePropertyName("comment"); writer.WriteStringValue(comment); writer.WriteEndObject(); writer.Flush(); } 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; })!; } /// /// Gets the account's relationship with the given account. /// /// Account ID /// 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)}")); if (rels == null || rels.Length == 0) return null; else return rels[0]; } /// /// Posts a status. /// /// Text content of the status. /// The MIME type of the . /// Mark status and attached media as sensitive? /// 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. /// ID of the status being replied to, if status is a reply /// ID of the status being quoted. /// ISO 639 language code for this status. /// Array of Attachment ids to be attached as media. /// A list of nicknames (like lain@soykaf.club or lain on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the , only the people in the list will be addressed. The normal rules for post visibility are not affected by this and will still apply /// Visibility of the posted status. /// Thrown if something goes wrong while publishing the status. /// The newly published status if posting was successful. public Task PostStatus(string? content = null, string content_type = "text/plain", bool sensitive = false, int? expiresIn = null, StatusID? replyTo = null, StatusID? quoting = null, string? language = null, MediaID[]? attachments = null, string[]? to = null, StatusVisibility visibility = StatusVisibility.Public) { if (content == null && (attachments == null || attachments.Length == 0)) throw new ArgumentException("Cannot post nothing. Content and/or attachments must be provided."); MemoryStream mem = new MemoryStream(); using (Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true })) { writer.WriteStartObject(); if (content != null) { writer.WritePropertyName("status"); writer.WriteStringValue(content); writer.WritePropertyName("content_type"); writer.WriteStringValue(content_type); } if (sensitive) { writer.WritePropertyName("sensitive"); writer.WriteBooleanValue(true); } if (expiresIn.HasValue) { writer.WritePropertyName("expires_in"); writer.WriteNumberValue(expiresIn.Value); } if (replyTo.HasValue) { writer.WritePropertyName("in_reply_to_id"); writer.WriteStringValue(replyTo.Value.ID); } if (quoting.HasValue) { writer.WritePropertyName("quote_id"); writer.WriteStringValue(quoting.Value.ID); } if (language != null) { writer.WritePropertyName("language"); writer.WriteStringValue(language); } if (attachments != null) { writer.WritePropertyName("media_ids"); writer.WriteStartArray(); foreach (MediaID media in attachments) writer.WriteStringValue(media.ID); writer.WriteEndArray(); } if (to != null) { writer.WritePropertyName("to"); writer.WriteStartArray(); foreach (string name in to) writer.WriteStringValue(name); writer.WriteEndArray(); } if (visibility != StatusVisibility.Public) { writer.WritePropertyName("visibility"); writer.WriteStringValue(visibility.ToString().ToLowerInvariant()); } writer.WriteEndObject(); writer.Flush(); } 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 { CommonMIMEs.PNG => "image/png", CommonMIMEs.JPEG => "image/jpeg", CommonMIMEs.GIF => "image/gif", CommonMIMEs.MP4 => "video/mp4", CommonMIMEs.WEBM => "video/webm", CommonMIMEs.MOV => "video/quicktime", CommonMIMEs.WAV => "audio/vnd.wav", CommonMIMEs.MP3 => "audio/mpeg", CommonMIMEs.OGG => "audio/ogg", CommonMIMEs.Text => "text/plain", _ => throw new ArgumentException("Unknown common MIME", nameof(mime)), }; /// /// Uploads a file to the server. The resulting attachment can then be included in a status as an attachment. /// /// The MIME type of the file. /// An array containing the file's contents. /// A handle of the uploaded file, including its ID and URLs. public Task Upload(CommonMIMEs mime, byte[] file, string name = "file") => Upload(CommonMIMEString(mime), file, name); /// /// Uploads a file to the server. The resulting attachment can then be included in a status as an attachment. /// /// The MIME type of the file. /// An array containing the file's contents. /// A handle of the uploaded file, including its ID and URLs. public Task Upload(string mime, byte[] file, string name = "file") => Upload(mime, new ByteArrayContent(file), name); /// /// Uploads a file to the server. The resulting attachment can then be included in a status as an attachment. /// /// The MIME type of the file. /// A stream of the file's contents. /// A handle of the uploaded file, including its ID and URLs. public Task Upload(CommonMIMEs mime, Stream stream, string name = "file") => Upload(CommonMIMEString(mime), stream, name); /// /// Uploads a file to the server. The resulting attachment can then be included in a status as an attachment. /// /// The MIME type of the file. /// A stream of the file's contents. /// A handle of the uploaded file, including its ID and URLs. public Task Upload(string mime, Stream stream, string name = "file") => Upload(mime, new StreamContent(stream), name); async Task Upload(string mime, HttpContent content, string name) { return (await Retry(() => { content.Headers.ContentType = new MediaTypeHeaderValue(mime); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "\"file\"", FileName = $"\"{name}\"", }; MultipartFormDataContent form = [content]; HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/media") { Content = form }; return req; }))!; } /// /// Feature one of your own public statuses at the top of your profile /// /// The status to pin. /// The pinned status, if it exists. public Task Pin(StatusID status) { return Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/pin")); } /// /// Unfeature a status from the top of your profile /// /// The status to unpin. /// The unpinned status, if it exists. public Task Unpin(StatusID status) { return Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/statuses/{WebUtility.UrlEncode(status.ID)}/unpin")); } /// /// Reposts/boosts/shares a status. /// /// The status to reblog. /// The reblogged status, if it exists. public Task Reblog(StatusID status, StatusVisibility? visibility = null) { MemoryStream mem = new MemoryStream(); using (Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true })) { writer.WriteStartObject(); if (visibility.HasValue) { writer.WritePropertyName("visibility"); writer.WriteStringValue(visibility.Value.ToString().ToLowerInvariant()); } writer.WriteEndObject(); writer.Flush(); } 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; }); } /// /// Removes a reblog. /// /// 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"))!; /// /// Likes/favourites a status, adding it to the account's favourite list. /// /// 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"))!; /// /// 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"))!; /// /// 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 /// 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 with_muted = false, ReplyVisibility reply_visibility = ReplyVisibility.All, string? max_id = null, string? min_id = null, string? since_id = null, int offset = 0, int limit = 20) { string timelineName; switch (timeline) { case Timeline.Direct: timelineName = "direct"; break; case Timeline.Home: timelineName = "home"; break; case Timeline.Local: timelineName = "public"; local = true; break; case Timeline.Bubble: timelineName = "bubble"; break; case Timeline.All: timelineName = "public"; break; default: throw new ArgumentException("Invalid timeline", nameof(timeline)); } string path = $"/api/v1/timelines/{timelineName}" + CreateQuery(addPair => { if (local) addPair("local", "true"); if (instance != null) addPair("instance", instance); if (only_media) addPair("only_media", "true"); if (remote) addPair("remote", "true"); if (with_muted) addPair("with_muted", "true"); 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); if (offset > 0) addPair("offset", offset.ToString()); if (limit != 20) addPair("limit", limit.ToString()); }); return Retry(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// 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, bool only_media = false, bool with_muted = false, bool exclude_reblogs = false, bool exclude_replies = false, StatusVisibility[]? exclude_visibilities = null, string? max_id = null, string? min_id = null, string? since_id = null, int offset = 0, int limit = 20) { string path = $"/api/v1/accounts/{WebUtility.UrlEncode(account.ID)}/statuses" + CreateQuery(addPair => { if (pinned) addPair("pinned", "true"); if (tagged != null) addPair("tagged", tagged); if (only_media) addPair("only_media", "true"); if (with_muted) addPair("with_muted", "true"); if (exclude_reblogs) addPair("exclude_reblogs", "true"); if (exclude_replies) addPair("exclude_replies", "true"); if (exclude_visibilities != null) foreach (StatusVisibility visibility in exclude_visibilities) addPair("exclude_visibilities[]", 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); if (offset > 0) addPair("offset", offset.ToString()); if (limit != 20) addPair("limit", limit.ToString()); }); return Retry(() => new HttpRequestMessage(HttpMethod.Get, path)); } /// /// Fetches the context of a status. /// 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)}")); /// /// Deletes a status. /// 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. /// /// 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) { string path = "/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(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// Downloads an attachment. /// public async Task Download(Attachment attachment) { HttpResponseMessage res = await HttpClient.GetAsync(attachment.URL); res.EnsureSuccessStatusCode(); return await res.Content.ReadAsByteArrayAsync(); } /// /// Downloads an attachment as a . /// public async Task DownloadStream(Attachment attachment) { HttpResponseMessage res = await HttpClient.GetAsync(attachment.URL); res.EnsureSuccessStatusCode(); return await res.Content.ReadAsStreamAsync(); } /// /// Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and id values. /// /// 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 GetNotifications(NotificationType[]? exclude_types = null, string? account_id = null, StatusVisibility[]? exclude_visibilities = null, NotificationType[]? types = null, bool with_muted = false, string? max_id = null, string? min_id = null, string? since_id = null, int offset = 0, int limit = 20) { string path = "/api/v1/notifications" + CreateQuery(addPair => { if (exclude_types != null) foreach (NotificationType type in exclude_types) addPair("exclude_types[]", NotificationTypes.ToString(type)); if (account_id != null) addPair("account_id", account_id); if (exclude_visibilities != null) foreach (StatusVisibility visibility in exclude_visibilities) addPair("exclude_visibilities[]", visibility.ToString().ToLowerInvariant()); if (types != null) foreach (NotificationType type in types) addPair("types[]", NotificationTypes.ToString(type)); if (with_muted) addPair("with_muted", "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(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// View information about a notification with a given ID. /// /// Notification ID public Task GetNotification(NotificationID id) { return Retry(() => new HttpRequestMessage(HttpMethod.Get, $"/api/v1/notifications/{WebUtility.UrlEncode(id.ID)}")); } /// /// Clear a single notification from the server. /// /// Notification ID public Task Dismiss(NotificationID notification) { return Retry(() => new HttpRequestMessage(HttpMethod.Post, $"/api/v1/notifications/{WebUtility.UrlEncode(notification.ID)}/dismiss")); } /// /// Clears multiple notifications from the server. /// /// Array of notification IDs to dismiss public Task Dismiss(NotificationID[] notifications) { if (notifications.Length == 0) return Task.CompletedTask; string path = $"/api/v1/notifications/destroy_multiple" + CreateQuery(addPair => { foreach (NotificationID id in notifications) addPair("ids[]", id.ID); }); return Retry(() => new HttpRequestMessage(HttpMethod.Delete, path)); } /// /// Clears multiple notifications from the server. /// /// Array of notifications to dismiss public Task Dismiss(Notification[] notifications) { if (notifications.Length == 0) return Task.CompletedTask; string path = $"/api/v1/notifications/destroy_multiple" + CreateQuery(addPair => { foreach (NotificationID id in notifications) addPair("ids[]", id.ID); }); return Retry(() => new HttpRequestMessage(HttpMethod.Delete, path)); } /// /// Adds a reaction to a status. /// /// The status to react to. /// The unicode emoji or custom emoji. /// The new state of the status, if it exists. public Task React(StatusID status, string emoji) { return Retry(() => new HttpRequestMessage(HttpMethod.Put, $"/api/v1/pleroma/statuses/{WebUtility.UrlEncode(status.ID)}/reactions/{WebUtility.UrlEncode(emoji)}")); } } /// /// A pleroma client permitted to use the admin scope. /// public class PleromaAdmin : Pleroma { public PleromaAdmin(string host, string? authorization, string? userAgent = "Uwaa.Pleroma/0.0") : base(host, authorization, userAgent) { } /// /// Gets users known to the instance. /// /// Filter by local/external type /// Filter by account status /// Search users query /// Search by display name /// Search by email /// Page Number /// Number of users to return per page /// Filter by actor type /// Filter by tags public Task GetUsers(AccountType? type = null, AccountStatus? status = null, string? query = null, string? name = null, string? email = null, int? page = null, int? page_size = null, ActorType[]? actor_types = null, string[]? tags = null) { string path = "/api/v1/pleroma/admin/users" + CreateQuery(addPair => { string? typeStr = type switch { AccountType.Local => "local", AccountType.External => "external", null => null, _ => throw new ArgumentException("Invalid account type", nameof(type)), }; string? statusStr = status switch { AccountStatus.Active => "active", AccountStatus.Deactivated => "deactivated", AccountStatus.PendingApproval => "need_approval", AccountStatus.Unconfirmed => "unconfirmed", null => null, _ => throw new ArgumentException("Invalid account status", nameof(type)), }; if (type != null && status != null) addPair("filters", typeStr + "," + statusStr); else if (typeStr != null) addPair("filters", typeStr); else if (statusStr != null) addPair("filters", statusStr); if (query != null) addPair("query", query); if (name != null) addPair("name", name); if (email != null) addPair("email", email); if (page != null) addPair("page", page.Value.ToString()); if (page_size != null) addPair("page_size", page_size.Value.ToString()); if (actor_types != null) foreach (ActorType type in actor_types) addPair("actor_types[]", type.ToString()); if (tags != null) foreach (string tag in tags) addPair("tags[]", tag); }); return Retry(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// Modifies the sensitive and/or visibility of a status. /// /// The status ID to modify. /// If non-null, the status will be made sensitive (if true) or not sensitive (if false). /// If non-null, the status's visibility will be set to this. /// Returns the new state of the status. Returns null if the status doesn't exist. public Task ChangeScope(StatusID status, bool? sensitive = null, StatusVisibility? visibility = null) { MemoryStream mem = new MemoryStream(); using (Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true })) { writer.WriteStartObject(); if (sensitive.HasValue) { writer.WritePropertyName("sensitive"); writer.WriteBooleanValue(sensitive.Value); } if (visibility.HasValue) { writer.WritePropertyName("visibility"); writer.WriteStringValue(visibility.Value.ToString().ToLowerInvariant()); } writer.WriteEndObject(); writer.Flush(); } return Retry(() => { mem.Position = 0; HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/pleroma/admin/statuses/{WebUtility.UrlEncode(status.ID)}"); req.Content = new StreamContent(mem); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return req; }); } public Task GetModerationLog(int page = 1) { string path = "/api/v1/pleroma/admin/moderation_log" + CreateQuery(addPair => { addPair("page", page.ToString()); }); return Retry(() => new HttpRequestMessage(HttpMethod.Get, path))!; } /// /// Deactivates or deletes one or more users by nickname. /// /// If the user is pending approval, this will delete the user entirely. If the user is active, the user will be deactivated. Does nothing if the user is already deactivated. /// The nicknames of the users to deactivate or delete. /// An array of nicknames which were successfully deactivated or deleted. public Task DeleteUsers(params string[] nicknames) { MemoryStream mem = new MemoryStream(); using (Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true })) { writer.WriteStartObject(); writer.WriteStartArray("nicknames"); foreach (string nickname in nicknames) writer.WriteStringValue(nickname); writer.WriteEndArray(); writer.WriteEndObject(); writer.Flush(); } return Retry(() => { mem.Position = 0; HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Delete, "/api/v1/pleroma/admin/users"); req.Content = new StreamContent(mem); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return req; })!; } } [JsonConverter(typeof(EnumLowerCaseConverter))] public enum ActorType { Application, Group, Organization, Person, Service } [JsonConverter(typeof(EnumLowerCaseConverter))] public enum AccountType { Local, External, } [JsonConverter(typeof(EnumLowerCaseConverter))] public enum AccountStatus { Active, Deactivated, PendingApproval, Unconfirmed, }