From a7649a00c8943382b2fc571fe32b4ca0c557c91d Mon Sep 17 00:00:00 2001 From: Alec Murphy Date: Tue, 4 Mar 2025 13:26:35 -0500 Subject: [PATCH] Everywhere: Use Catbox API asynchronously Fixes #2 --- Slon/Api/V1/Media.HC | 23 ++++++++ Slon/Api/V1/Statuses.HC | 2 +- Slon/Api/V2/Media.HC | 103 ++++++++++-------------------------- Slon/Endpoints/Get/Media.HC | 4 ++ Slon/Http/Server.HC | 1 + Slon/Modules/Api.HC | 71 +++++++++++++++---------- 6 files changed, 102 insertions(+), 102 deletions(-) create mode 100644 Slon/Endpoints/Get/Media.HC diff --git a/Slon/Api/V1/Media.HC b/Slon/Api/V1/Media.HC index 6f07b47..6e76c54 100644 --- a/Slon/Api/V1/Media.HC +++ b/Slon/Api/V1/Media.HC @@ -1,3 +1,26 @@ +U0 @slon_api_v1_media_get(SlonHttpSession* session) +{ + if (@slon_api_authorized(session)) { + if (session->path_count() < 4) { + session->status(400); + return; + } + U8* id = session->path(3); + if (db->o("media")->o(id)) { + if (db->o("media")->o(id)->@("url", TRUE)(JsonKey*)->type == JSON_NULL) { + session->send(db->o("media")->o(id)); + session->status(206); + } else { + session->send(db->o("media")->o(id)); + } + } else { + session->status(404); + } + } else { + session->status(401); + } +} + U0 @slon_api_v1_media_put(SlonHttpSession* session) { SLON_SCRATCH_BUFFER_AND_REQUEST_JSON diff --git a/Slon/Api/V1/Statuses.HC b/Slon/Api/V1/Statuses.HC index 608aa36..f845013 100644 --- a/Slon/Api/V1/Statuses.HC +++ b/Slon/Api/V1/Statuses.HC @@ -196,7 +196,7 @@ U0 @slon_api_v1_statuses_delete(SlonHttpSession* session) while (*(attachment_url_ptr - 1) != '/') { --attachment_url_ptr; } - @slon_api_delete_from_catbox(session, attachment_url_ptr); + Spawn(&@slon_api_async_delete_from_catbox, StrNew(attachment_url_ptr, adam_task), "SlonAsyncCatboxDelete"); } } } diff --git a/Slon/Api/V2/Media.HC b/Slon/Api/V2/Media.HC index 0543bd4..a1ac678 100644 --- a/Slon/Api/V2/Media.HC +++ b/Slon/Api/V2/Media.HC @@ -6,66 +6,25 @@ U0 @slon_api_v2_media_post(SlonHttpSession* session) no_warn request_json; if (@slon_api_authorized(session)) { - U8* data = session->request->data; - // Advance to Content-Disposition for file attachment - data = StrFind("filename=", data); - - if (!data) { + if (!request_json->@("file")) { session->status(400); return; } - if (!StrFind("\r\n\r\n", data)) { + SlonMultipartFile* file = request_json->@("file"); + if (!file->buffer || !file->size || !file->content_type) { session->status(400); return; } - // Mark beginning of file data - U8* file_ptr = StrFind("\r\n\r\n", data) + 4; - - // NULL terminate Content-Type - StrFind("\r\n\r\n", data)[0] = NULL; - - U8* mime_type = StrFind("Content-Type: ", data); - if (!mime_type) { - session->status(400); - return; - } - mime_type += 14; // StrLen("Content-Type: ") - - if (!String.BeginsWith("image/", mime_type)) { - session->status(400); - return; - } - - U8* boundary = StrFind("boundary=", session->header("content-type")) + 9; - I64 content_length = Str2I64(session->header("content-length")); - // Strip begin double-quotes and ending CRLF, double-quotes - while (boundary[0] == '"') - boundary++; - // Rstrip EOL - while (boundary[StrLen(boundary) - 1] == '\"' || boundary[StrLen(boundary) - 1] == ' ' || boundary[StrLen(boundary) - 1] == '\r' || boundary[StrLen(boundary) - 1] == '\n') - boundary[StrLen(boundary) - 1] = NULL; - - // Get file size - StrPrint(scratch_buffer, "\r\n--%s", boundary); - I64 file_size = 0; - I64 scratch_buffer_len = StrLen(scratch_buffer); - while (file_size < content_length && MemCmp(file_ptr + file_size, scratch_buffer, scratch_buffer_len)) { - ++file_size; - } - - // File size is non-zero and within bounds - if (!file_size || file_size >= content_length) { - session->status(400); - return; - } + U8* media_id = @slon_api_generate_unique_id(session); + U8* media_file_ext = StrFind("/", file->content_type) + 1; I32 width = 0; I32 height = 0; I32 comp = 0; - I32 code = @stbi_info_from_memory(file_ptr, file_size, &width, &height, &comp); + I32 code = @stbi_info_from_memory(file->buffer, file->size, &width, &height, &comp); // Buffer contains a valid image file if (code != 1) { @@ -73,37 +32,33 @@ U0 @slon_api_v2_media_post(SlonHttpSession* session) return; } - U8* media_id = @slon_api_generate_unique_id(session); - U8* media_file_ext = StrFind("/", mime_type) + 1; - // Write image file to RAM disk StrPrint(scratch_buffer, "%s/%s.%s", SLON_MEDIA_PATH, media_id, media_file_ext); - FileWrite(scratch_buffer, file_ptr, file_size); + FileWrite(scratch_buffer, file->buffer, file->size); - // Then, upload to Catbox - U8* media_url = @slon_api_upload_to_catbox(session, scratch_buffer); - if (media_url) { - JsonObject* media_object = Json.CreateObject(); - media_object->set("id", media_id, JSON_STRING); - media_object->set("type", "image", JSON_STRING); - media_object->set("url", media_url, JSON_STRING); - media_object->set("preview_url", NULL, JSON_NULL); - media_object->set("remote_url", NULL, JSON_NULL); - media_object->set("meta", Json.CreateObject(), JSON_OBJECT); - media_object->o("meta")->set("original", Json.CreateObject(), JSON_OBJECT); - media_object->o("meta")->o("original")->set("width", width, JSON_NUMBER); - media_object->o("meta")->o("original")->set("height", height, JSON_NUMBER); - media_object->set("description", NULL, JSON_NULL); - 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); - } + // Create media object + JsonObject* media_object = Json.CreateObject(); + media_object->set("id", media_id, JSON_STRING); + media_object->set("type", "image", JSON_STRING); + media_object->set("url", NULL, JSON_NULL); + media_object->set("preview_url", NULL, JSON_NULL); + media_object->set("remote_url", NULL, JSON_NULL); + media_object->set("meta", Json.CreateObject(), JSON_OBJECT); + media_object->o("meta")->set("original", Json.CreateObject(), JSON_OBJECT); + media_object->o("meta")->o("original")->set("width", width, JSON_NUMBER); + media_object->o("meta")->o("original")->set("height", height, JSON_NUMBER); + media_object->set("description", NULL, JSON_NULL); + media_object->set("blurhash", NULL, JSON_NULL); + db->o("media")->set(media_id, media_object, JSON_OBJECT); - // Delete image from RAM disk - Del(scratch_buffer); + // Then, async upload the image file to Catbox + SlonCatboxUpload* cb = CAlloc(sizeof(SlonCatboxUpload), adam_task); + cb->key = media_object->@("url", TRUE); + cb->filepath = StrNew(scratch_buffer, adam_task); + Spawn(&@slon_api_async_upload_to_catbox, cb, "SlonAsyncCatboxUpload"); + + session->send(media_object); + session->status(202); @slon_free(session, media_id); } else { diff --git a/Slon/Endpoints/Get/Media.HC b/Slon/Endpoints/Get/Media.HC new file mode 100644 index 0000000..a291c55 --- /dev/null +++ b/Slon/Endpoints/Get/Media.HC @@ -0,0 +1,4 @@ +if (String.BeginsWith("/api/v1/media", session->path())) { + @slon_api_v1_media_get(session); + return; +} diff --git a/Slon/Http/Server.HC b/Slon/Http/Server.HC index e5b171e..4aed0e0 100644 --- a/Slon/Http/Server.HC +++ b/Slon/Http/Server.HC @@ -692,6 +692,7 @@ U0 @slon_http_handle_get_request(SlonHttpSession* session) #include "Endpoints/Get/FollowedTags"; #include "Endpoints/Get/Instance"; #include "Endpoints/Get/Markers"; + #include "Endpoints/Get/Media"; #include "Endpoints/Get/Notifications"; #include "Endpoints/Get/NodeInfo"; #include "Endpoints/Get/OAuth"; diff --git a/Slon/Modules/Api.HC b/Slon/Modules/Api.HC index c6d19c5..c82c0e8 100644 --- a/Slon/Modules/Api.HC +++ b/Slon/Modules/Api.HC @@ -3,6 +3,11 @@ extern @http_response* @slon_activitypub_signed_request(U8* url_string, U8* fetch_buffer, JsonObject* request_object = NULL, I64 verb = SLON_HTTP_VERB_POST, U8* signatory = NULL); +class SlonCatboxUpload { + JsonKey* key; + U8* filepath; +}; + Bool @slon_api_authorized(SlonHttpSession* session) { return session->auth > 0; @@ -115,19 +120,24 @@ JsonObject* @slon_api_account_by_remote_actor(U8* remote_actor) return NULL; } -U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) +U0 @slon_api_async_upload_to_catbox(SlonCatboxUpload* cb) { - if (!session || !filepath) { - return NULL; + if (!cb) { + return; } + if (!cb->key || !cb->filepath || !FileFind(cb->filepath)) { + Free(cb); + return; + } + + U8* filepath = cb->filepath; I64 data_size = 0; U8* data = FileRead(filepath, &data_size); - U8* image_url = NULL; // build the multipart/form-data payload - U8* payload = @slon_calloc(session, 4096 + data_size); + U8* payload = CAlloc(4096 + data_size, adam_task); I64 payload_size = 0; U8* boundary = "----------SlonFormBoundary00"; @@ -137,12 +147,16 @@ U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) 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 random_filename[64]; + U64 id = ((CDate2Unix(Now) + SLON_API_LOCAL_TIME_OFFSET) * 1000) << 16; + id += RandU64 & 0xffff; + StrPrint(random_filename, "%d", id); + 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; @@ -151,7 +165,7 @@ U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) payload_size += StrLen(boundary); // build the http headers - U8* headers = @slon_calloc(session, 4096); + U8* headers = CAlloc(4096, adam_task); 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"); @@ -159,7 +173,7 @@ U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) 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); + U8* send_buffer = CAlloc(send_buffer_size, adam_task); MemCpy(send_buffer, headers, StrLen(headers)); MemCpy(send_buffer + StrLen(headers), payload, payload_size); @@ -183,7 +197,7 @@ U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) I64 bytes_received = 0; I64 response_buffer_size = 0; - U8* response_buffer = @slon_calloc(session, 4096); + U8* response_buffer = CAlloc(4096, adam_task); while (!bytes_received) { bytes_received = s->receive(response_buffer + response_buffer_size, 4096); @@ -199,28 +213,30 @@ U8* @slon_api_upload_to_catbox(SlonHttpSession* session, U8* filepath) url_ptr = StrFind("\r\n", url_ptr) + 2; StrFind("\r\n", url_ptr)[0] = NULL; - image_url = @slon_strnew(session, url_ptr); + cb->key->value = StrNew(url_ptr, adam_task); + cb->key->type = JSON_STRING; slon_api_upload_to_catbox_failed: - @slon_free(session, response_buffer); - @slon_free(session, send_buffer); - @slon_free(session, headers); - @slon_free(session, payload); + Free(response_buffer); + Free(send_buffer); + Free(headers); + Free(payload); Free(data); - - return image_url; + Del(cb->filepath); + Free(cb->filepath); + Free(cb); } -U0 @slon_api_delete_from_catbox(SlonHttpSession* session, U8* filename) +U0 @slon_api_async_delete_from_catbox(U8* filename) { - if (!session || !filename) { + if (!filename) { return; } // build the multipart/form-data payload - U8* payload = @slon_calloc(session, 4096); + U8* payload = CAlloc(4096, adam_task); I64 payload_size = 0; U8* boundary = "----------SlonFormBoundary00"; @@ -234,7 +250,7 @@ U0 @slon_api_delete_from_catbox(SlonHttpSession* session, U8* filename) payload_size = StrLen(payload); // build the http headers - U8* headers = @slon_calloc(session, 4096); + U8* headers = CAlloc(4096, adam_task); 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"); @@ -242,7 +258,7 @@ U0 @slon_api_delete_from_catbox(SlonHttpSession* session, U8* filename) 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); + U8* send_buffer = CAlloc(send_buffer_size, adam_task); MemCpy(send_buffer, headers, StrLen(headers)); MemCpy(send_buffer + StrLen(headers), payload, payload_size); @@ -255,7 +271,7 @@ U0 @slon_api_delete_from_catbox(SlonHttpSession* session, U8* filename) I64 bytes_received = 0; I64 response_buffer_size = 0; - U8* response_buffer = @slon_calloc(session, 4096); + U8* response_buffer = CAlloc(4096, adam_task); while (!bytes_received) { bytes_received = s->receive(response_buffer + response_buffer_size, 4096); @@ -264,8 +280,9 @@ U0 @slon_api_delete_from_catbox(SlonHttpSession* session, U8* filename) s->close(); - @slon_free(session, response_buffer); - @slon_free(session, send_buffer); - @slon_free(session, payload); - @slon_free(session, headers); + Free(response_buffer); + Free(send_buffer); + Free(payload); + Free(headers); + Free(filename); }