diff --git a/Pleroma/CommonMIMEs.cs b/Pleroma/CommonMIMEs.cs new file mode 100644 index 0000000..6e35cb3 --- /dev/null +++ b/Pleroma/CommonMIMEs.cs @@ -0,0 +1,18 @@ +namespace Uwaa.Pleroma; + +public enum CommonMIMEs +{ + PNG, + JPEG, + GIF, + + MP4, + WEBM, + MOV, + + WAV, + MP3, + OGG, + + Text, +} diff --git a/Pleroma/Models/Attachment.cs b/Pleroma/Models/Attachment.cs index 62f4ff1..b95b15e 100644 --- a/Pleroma/Models/Attachment.cs +++ b/Pleroma/Models/Attachment.cs @@ -49,6 +49,16 @@ public class Attachment /// [JsonPropertyName("url")] public string URL { get; set; } = null!; + + /// + /// Downloads the attachment. + /// + public async Task Download() + { + using HttpClient client = new HttpClient(); + HttpResponseMessage res = await client.GetAsync(URL); + return await res.Content.ReadAsByteArrayAsync(); + } } [JsonConverter(typeof(EnumLowerCaseConverter))] @@ -76,4 +86,31 @@ public class PleromaAttachmentData /// [JsonPropertyName("name")] public string Name { get; set; } = null!; +} + +[JsonConverter(typeof(MediaIDConverter))] +public readonly struct MediaID(string id) +{ + public static implicit operator MediaID(string id) => new MediaID(id); + + public static implicit operator MediaID(Attachment attachment) => new MediaID(attachment.ID); + + public readonly string ID = id; + + public override string ToString() => ID; +} + +class MediaIDConverter : JsonConverter +{ + public override bool CanConvert(Type type) => type.IsAssignableTo(typeof(MediaID)); + + public override MediaID Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new MediaID(reader.GetString() ?? throw new NullReferenceException("Expected a string, got null")); + } + + public override void Write(Utf8JsonWriter writer, MediaID value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ID); + } } \ No newline at end of file diff --git a/Pleroma/Models/PleromaException.cs b/Pleroma/Models/PleromaException.cs index 3c13a69..e5d5ada 100644 --- a/Pleroma/Models/PleromaException.cs +++ b/Pleroma/Models/PleromaException.cs @@ -7,5 +7,25 @@ public class PleromaException : Exception public override string Message => Text; - public override string ToString() => Text; + public override string ToString() => Message; } + +public class PleromaAggregateException : Exception +{ + [JsonPropertyName("errors")] + public PleromaInnerException[] Text { get; set; } = null!; + + public override string Message => string.Join("\n", (object?[])Text); + + public override string ToString() => Message; +} + +public class PleromaInnerException : Exception +{ + [JsonPropertyName("detail")] + public string Text { get; set; } = null!; + + public override string Message => Text; + + public override string ToString() => Message; +} \ No newline at end of file diff --git a/Pleroma/Pleroma.cs b/Pleroma/Pleroma.cs index d6dc489..cb8fb65 100644 --- a/Pleroma/Pleroma.cs +++ b/Pleroma/Pleroma.cs @@ -65,7 +65,7 @@ public class Pleroma string text = await res.Content.ReadAsStringAsync(); - if (res.StatusCode is >= (HttpStatusCode)400 and < (HttpStatusCode)600) + if (res.StatusCode is >= (HttpStatusCode)300) { try { @@ -87,6 +87,17 @@ public class Pleroma { //Not an error } + + try + { + PleromaAggregateException? err = JsonSerializer.Deserialize(text, SerializerOptions); + if (err != null && err.Text != null) + throw err; + } + catch (JsonException) + { + //Not an error + } } if (res.StatusCode is >= (HttpStatusCode)200 and < (HttpStatusCode)300) @@ -179,28 +190,36 @@ public class Pleroma /// 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. /// Visibility of the posted status. - /// Thrown if something goes wrong while uploading the status. - /// The status if posting was successful. - public Task PostStatus(string content, + /// Thrown if something goes wrong while publishing the status. + /// The newly published status if posting was successful. + public Task PostStatus(string? content = null, bool sensitive = false, int? expiresIn = null, string? replyTo = null, string? quoting = null, string? language = null, + MediaID[]? attachments = 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(); { Utf8JsonWriter writer = new Utf8JsonWriter(mem, new JsonWriterOptions() { SkipValidation = true }); writer.WriteStartObject(); - writer.WritePropertyName("status"); - writer.WriteStringValue(content); + if (content != null) + { + writer.WritePropertyName("status"); + writer.WriteStringValue(content); - writer.WritePropertyName("content-type"); - writer.WriteStringValue("text/plain"); + writer.WritePropertyName("content-type"); + writer.WriteStringValue("text/plain"); + } if (sensitive) { @@ -232,6 +251,15 @@ public class Pleroma writer.WriteStringValue(language); } + if (attachments != null) + { + writer.WritePropertyName("media_ids"); + writer.WriteStartArray(); + foreach (MediaID media in attachments) + writer.WriteStringValue(media.ID); + writer.WriteEndArray(); + } + if (visibility != StatusVisibility.Public) { writer.WritePropertyName("visibility"); @@ -250,6 +278,71 @@ public class Pleroma return Retry(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) => Upload(CommonMIMEString(mime), file); + + /// + /// 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) => Upload(mime, new ByteArrayContent(file)); + + /// + /// 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) => Upload(CommonMIMEString(mime), stream); + + /// + /// 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) => Upload(mime, new StreamContent(stream)); + + async Task Upload(string mime, HttpContent content) + { + content.Headers.ContentType = new MediaTypeHeaderValue(mime); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "\"file\"", + FileName = $"\"{Random.Shared.NextInt64()}\"", + }; + + MultipartFormDataContent form = new MultipartFormDataContent(); + form.Add(content); + + HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/media"); + req.Content = form; + + return (await Retry(req))!; + } + /// /// Reposts/boosts/shares a status. ///