From 27fa0f8aaddd58cfcc66e25b9e3d19fda6855df8 Mon Sep 17 00:00:00 2001 From: Alec Murphy Date: Sun, 2 Mar 2025 15:08:39 -0500 Subject: [PATCH] Slon/Api/V2/Media: Add support for Catbox API --- README.md | 2 + Slon/Api/V1/Statuses.HC | 76 +++++++++++++++++++++++++++++++++++ Slon/Api/V2/Media.HC | 79 ++++++++++++++++++++++++++----------- Slon/Modules/ActivityPub.HC | 2 +- 4 files changed, 135 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0d41c78..2440779 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Slon is developed to run from a standard TempleOS installation, or directly from Slon includes its own Virtio net and block device drivers, TCP/IPv4 network stack, and various supplemental libraries, all loaded at runtime, making it suitable for QEMU/KVM based cloud environments. +Image uploads powered by [Catbox](https://catbox.moe) (Support Catbox: [Patreon](https://patreon.com/catbox) | [Ko-fi](https://ko-fi.com/catboxmoe) | [Merch](https://store.catbox.moe)) + Slon utilizes the following third-party libraries: - [`stb_image.h`](https://github.com/nothings/stb/blob/master/stb_image.h) and [`stb_image_write.h`](https://github.com/nothings/stb/blob/master/stb_image_write.h) (for optional image processing) diff --git a/Slon/Api/V1/Statuses.HC b/Slon/Api/V1/Statuses.HC index b09a33f..e82f107 100644 --- a/Slon/Api/V1/Statuses.HC +++ b/Slon/Api/V1/Statuses.HC @@ -1,6 +1,64 @@ U0 (*@slon_api_status_create_fedi)(JsonObject* status) = NULL; U0 (*@slon_api_status_delete_fedi)(JsonObject* status) = NULL; +U0 @slon_api_v1_statuses_delete_file_from_catbox(SlonHttpSession* session, U8* filename) +{ + if (!session || !filename) { + return; + } + + // build the multipart/form-data payload + + U8* payload = @slon_calloc(session, 4096); + I64 payload_size = 0; + + U8* boundary = "----------SlonFormBoundary00"; + StrPrint(payload, "--%s\r\n", boundary); + + String.Append(payload, "Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s\r\n", "reqtype", "deletefiles", boundary); + if (db->o("settings")->@("catbox_userhash")) { + String.Append(payload, "Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s\r\n", "userhash", db->o("settings")->@("catbox_userhash"), boundary); + } + String.Append(payload, "Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s\r\n", "files", filename, boundary); + payload_size = StrLen(payload); + + // build the http headers + U8* headers = @slon_calloc(session, 4096); + String.Append(headers, "POST /user/api.php HTTP/1.1\r\n"); + String.Append(headers, "Host: catbox.moe\r\n"); + String.Append(headers, "User-Agent: slon/1.0\r\n"); + String.Append(headers, "Content-Length: %d\r\n", payload_size); + String.Append(headers, "Content-Type: multipart/form-data; boundary=%s\r\n\r\n", boundary); + + I64 send_buffer_size = StrLen(headers) + payload_size; + U8* send_buffer = @slon_calloc(session, send_buffer_size); + + MemCpy(send_buffer, headers, StrLen(headers)); + MemCpy(send_buffer + StrLen(headers), payload, payload_size); + + TlsSocket* s = @tls_socket_create("catbox.moe", 443); + while (!@tls_established(s->ctx)) + Sleep(1); + + s->send(send_buffer, send_buffer_size); + + I64 bytes_received = 0; + I64 response_buffer_size = 0; + U8* response_buffer = @slon_calloc(session, 4096); + + while (!bytes_received) { + bytes_received = s->receive(response_buffer + response_buffer_size, 4096); + response_buffer_size += bytes_received; + } + + s->close(); + + @slon_free(session, response_buffer); + @slon_free(session, send_buffer); + @slon_free(session, payload); + @slon_free(session, headers); +} + JsonObject* @slon_api_v1_statuses_lookup_by_id(U8* id, JsonArray* statuses) { if (!id || !statuses) { @@ -131,13 +189,31 @@ U0 @slon_api_v1_statuses_delete(SlonHttpSession* session) U8* id = session->path(3); JsonObject* status; JsonObject* fedi_status; + JsonArray* media_attachments = NULL; + JsonObject* attachment = NULL; + U8* attachment_url_ptr = NULL; I64 i; + I64 j; for (i = 0; i < statuses->length; i++) { status = statuses->@(i); if (!StrICmp(status->@("id"), id)) { fedi_status = Json.Clone(status); status->set("deleted", TRUE, JSON_BOOLEAN); + media_attachments = status->a("media_attachments"); + if (db->o("settings")->@("catbox_userhash") && media_attachments && media_attachments->length) { + for (j = 0; j < media_attachments->length; j++) { + attachment = media_attachments->@(j); + attachment_url_ptr = attachment->@("url"); + if (attachment_url_ptr) { + attachment_url_ptr += StrLen(attachment_url_ptr) - 1; + while (*(attachment_url_ptr - 1) != '/') { + --attachment_url_ptr; + } + @slon_api_v1_statuses_delete_file_from_catbox(session, attachment_url_ptr); + } + } + } @slon_db_save_statuses_to_disk; @slon_db_instance_decrement_status_count; @slon_db_save_instance_to_disk; diff --git a/Slon/Api/V2/Media.HC b/Slon/Api/V2/Media.HC index 26f54e6..8512002 100644 --- a/Slon/Api/V2/Media.HC +++ b/Slon/Api/V2/Media.HC @@ -1,7 +1,12 @@ -U8* @slon_api_v2_media_upload(SlonHttpSession* session, U8* filepath) +U8* @slon_api_v2_media_upload_to_catbox(SlonHttpSession* session, U8* filepath) { + if (!session || !filepath) { + return NULL; + } + I64 data_size = 0; U8* data = FileRead(filepath, &data_size); + U8* image_url = NULL; // build the multipart/form-data payload @@ -10,9 +15,17 @@ U8* @slon_api_v2_media_upload(SlonHttpSession* session, U8* filepath) U8* boundary = "----------SlonFormBoundary00"; StrPrint(payload, "--%s\r\n", boundary); - String.Append(payload, "Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n"); - String.Append(payload, "Content-Type: image/%s\r\n\r\n", StrFind(".", filepath) + 1); + + String.Append(payload, "Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s\r\n", "reqtype", "fileupload", boundary); + if (db->o("settings")->@("catbox_userhash")) { + String.Append(payload, "Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s\r\n", "userhash", db->o("settings")->@("catbox_userhash"), boundary); + } + U8* random_filename = @slon_api_generate_unique_id(session); + U8* ext = StrFind(".", filepath) + 1; + String.Append(payload, "Content-Disposition: form-data; name=\"fileToUpload\"; filename=\"%s.%s\"\r\n", random_filename, ext); + String.Append(payload, "Content-Type: image/%s\r\n\r\n", ext); payload_size = StrLen(payload); + @slon_free(session, random_filename); MemCpy(payload + payload_size, data, data_size); payload_size += data_size; @@ -22,35 +35,56 @@ U8* @slon_api_v2_media_upload(SlonHttpSession* session, U8* filepath) // build the http headers U8* headers = @slon_calloc(session, 4096); - String.Append(headers, "POST / HTTP/1.0\r\n"); - String.Append(headers, "Content-Type: multipart/form-data; boundary=%s\r\n", boundary); - String.Append(headers, "Content-Length: %d\r\n\r\n", payload_size); + String.Append(headers, "POST /user/api.php HTTP/1.1\r\n"); + String.Append(headers, "Host: catbox.moe\r\n"); + String.Append(headers, "User-Agent: slon/1.0\r\n"); + String.Append(headers, "Content-Length: %d\r\n", payload_size); + String.Append(headers, "Content-Type: multipart/form-data; boundary=%s\r\n\r\n", boundary); I64 send_buffer_size = StrLen(headers) + payload_size; - I64 response_buffer_size = 0; - U8* send_buffer = @slon_calloc(session, send_buffer_size); - U8* response_buffer = @slon_calloc(session, 16384); MemCpy(send_buffer, headers, StrLen(headers)); MemCpy(send_buffer + StrLen(headers), payload, payload_size); - TlsSocket* s = @tcp_socket_create("10.20.0.254", 5558); - while (s->state != TCP_SOCKET_STATE_ESTABLISHED) + TlsSocket* s = @tls_socket_create("catbox.moe", 443); + while (!@tls_established(s->ctx)) Sleep(1); - s->send(send_buffer, send_buffer_size); + I64 left = send_buffer_size; + I64 sent = 0; + I64 chunk_size = 0; + while (left) { + chunk_size = 2048; + if (chunk_size > left) + chunk_size = left; + s->send(send_buffer + sent, chunk_size); + left -= chunk_size; + sent += chunk_size; + Sleep(10); + } - I64 bytes_received = -1; - while (bytes_received != 0) { - bytes_received = s->receive(response_buffer + response_buffer_size, 16384); + I64 bytes_received = 0; + I64 response_buffer_size = 0; + U8* response_buffer = @slon_calloc(session, 4096); + + while (!bytes_received) { + bytes_received = s->receive(response_buffer + response_buffer_size, 4096); response_buffer_size += bytes_received; } - response_buffer[response_buffer_size] = NULL; - s->close(); - JsonObject* obj = Json.Parse(StrFind("\r\n\r\n", response_buffer) + 4); + + U8* url_ptr = StrFind("\r\n\r\n", response_buffer) + 4; + if (url_ptr < 0x10) { + goto slon_api_v2_media_upload_to_catbox_failed; + } + url_ptr = StrFind("\r\n", url_ptr) + 2; + StrFind("\r\n", url_ptr)[0] = NULL; + + image_url = @slon_strnew(session, url_ptr); + +slon_api_v2_media_upload_to_catbox_failed: @slon_free(session, response_buffer); @slon_free(session, send_buffer); @@ -58,7 +92,7 @@ U8* @slon_api_v2_media_upload(SlonHttpSession* session, U8* filepath) @slon_free(session, payload); Free(data); - return obj->@("url"); + return image_url; } U0 @slon_api_v2_media_post(SlonHttpSession* session) @@ -143,10 +177,8 @@ U0 @slon_api_v2_media_post(SlonHttpSession* session) StrPrint(scratch_buffer, "%s/%s.%s", SLON_MEDIA_PATH, media_id, media_file_ext); FileWrite(scratch_buffer, file_ptr, file_size); - // Then, upload to image host - // NOTE: Replace @slon_api_v2_media_upload(session, filepath) with a function that uploads to your desired image host. - // An example upload function is provided. - U8* media_url = @slon_api_v2_media_upload(session, scratch_buffer); + // Then, upload to Catbox + U8* media_url = @slon_api_v2_media_upload_to_catbox(session, scratch_buffer); if (media_url) { JsonObject* media_object = Json.CreateObject(); media_object->set("id", media_id, JSON_STRING); @@ -162,6 +194,7 @@ U0 @slon_api_v2_media_post(SlonHttpSession* session) media_object->set("blurhash", NULL, JSON_NULL); db->o("media")->set(media_id, media_object, JSON_OBJECT); session->send(media_object); + @slon_free(session, media_url); } else { session->status(400); } diff --git a/Slon/Modules/ActivityPub.HC b/Slon/Modules/ActivityPub.HC index 90d7020..ea8e277 100644 --- a/Slon/Modules/ActivityPub.HC +++ b/Slon/Modules/ActivityPub.HC @@ -493,7 +493,7 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest) for (i = 0; i < media_attachments->length; i++) { masto_attachment = media_attachments->@(i); ap_attachment = Json.CreateObject(); - StrPrint(scratch_buffer, "image/%s", StrFind(".", StrFind("/images/", masto_attachment->@("url")) + 8) + 1); + StrPrint(scratch_buffer, "image/%s", StrFind(".", StrFind("catbox.moe/", masto_attachment->@("url")) + 11) + 1); ap_attachment->set("mediaType", scratch_buffer, JSON_STRING); ap_attachment->set("url", masto_attachment->@("url"), JSON_STRING); ap_attachment->set("width", masto_attachment->o("meta")->o("original")->@("width"), JSON_NUMBER);