Everywhere: Use Catbox API asynchronously

Fixes #2
This commit is contained in:
Alec Murphy 2025-03-04 13:26:35 -05:00
parent 04dce33aca
commit a7649a00c8
6 changed files with 102 additions and 102 deletions

View file

@ -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

View file

@ -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");
}
}
}

View file

@ -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 {

View file

@ -0,0 +1,4 @@
if (String.BeginsWith("/api/v1/media", session->path())) {
@slon_api_v1_media_get(session);
return;
}

View file

@ -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";

View file

@ -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);
}