Slon/Api/{V1,V2}/Media: Implement PUT /api/v1/media, POST /api/v2/media
This commit is contained in:
parent
95aecb9fb1
commit
b104551bbd
9 changed files with 284 additions and 3 deletions
21
Slon/Api/V1/Media.HC
Normal file
21
Slon/Api/V1/Media.HC
Normal file
|
@ -0,0 +1,21 @@
|
|||
U0 @slon_api_v1_media_put(SlonHttpSession* session)
|
||||
{
|
||||
SLON_SCRATCH_BUFFER_AND_REQUEST_JSON
|
||||
no_warn scratch_buffer;
|
||||
|
||||
if (@slon_api_authorized(session)) {
|
||||
if (session->path_count() < 4) {
|
||||
session->status(400);
|
||||
return;
|
||||
}
|
||||
U8* id = session->path(3);
|
||||
if (db->o("media")->@(id)) {
|
||||
db->o("media")->o(id)->set("description", request_json->@("description"), JSON_STRING);
|
||||
session->send(db->o("media")->o(id));
|
||||
} else {
|
||||
session->status(404);
|
||||
}
|
||||
} else {
|
||||
session->status(401);
|
||||
}
|
||||
}
|
|
@ -256,6 +256,7 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session)
|
|||
// IceCubesApp lets us post with +: media_attachments, replies_count, spoiler_text, sensitive
|
||||
|
||||
JsonObject* status = Json.CreateObject();
|
||||
JsonArray* media_attachments = NULL;
|
||||
status->set("id", id, JSON_STRING);
|
||||
status->set("created_at", created_at, JSON_STRING);
|
||||
status->set("content", request_json->@("status"), JSON_STRING);
|
||||
|
@ -269,7 +270,19 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session)
|
|||
status->set("emojis", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
status->set("tags", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
status->set("mentions", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
status->set("media_attachments", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
if (request_json->@("media_ids") && request_json->a("media_ids")->length) {
|
||||
I64 i;
|
||||
media_attachments = Json.CreateArray();
|
||||
for (i = 0; i < request_json->a("media_ids")->length; i++) {
|
||||
U8* media_id = request_json->a("media_ids")->@(i);
|
||||
if (media_id && db->o("media")->o(media_id)) {
|
||||
media_attachments->append(Json.CreateItem(db->o("media")->o(media_id), JSON_OBJECT));
|
||||
}
|
||||
}
|
||||
status->set("media_attachments", media_attachments, JSON_ARRAY);
|
||||
} else {
|
||||
status->set("media_attachments", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
}
|
||||
status->set("replies_count", 0, JSON_NUMBER);
|
||||
status->set("spoiler_text", "", JSON_STRING);
|
||||
status->set("sensitive", sensitive, JSON_BOOLEAN);
|
||||
|
@ -292,6 +305,9 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session)
|
|||
|
||||
session->send(status);
|
||||
|
||||
if (media_attachments) {
|
||||
Json.Delete(media_attachments);
|
||||
}
|
||||
Json.Delete(status_app);
|
||||
Json.Delete(account_object);
|
||||
Json.Delete(app_object);
|
||||
|
|
176
Slon/Api/V2/Media.HC
Normal file
176
Slon/Api/V2/Media.HC
Normal file
|
@ -0,0 +1,176 @@
|
|||
U8* @slon_api_v2_media_upload(SlonHttpSession* session, U8* filepath)
|
||||
{
|
||||
I64 data_size = 0;
|
||||
U8* data = FileRead(filepath, &data_size);
|
||||
|
||||
// build the multipart/form-data payload
|
||||
|
||||
U8* payload = @slon_calloc(session, 4096 + data_size);
|
||||
I64 payload_size = 0;
|
||||
|
||||
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);
|
||||
payload_size = StrLen(payload);
|
||||
|
||||
MemCpy(payload + payload_size, data, data_size);
|
||||
payload_size += data_size;
|
||||
StrPrint(payload + payload_size, "\r\n--%s--\r\n", boundary);
|
||||
payload_size += 8;
|
||||
payload_size += StrLen(boundary);
|
||||
|
||||
// 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);
|
||||
|
||||
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)
|
||||
Sleep(1);
|
||||
|
||||
s->send(send_buffer, send_buffer_size);
|
||||
|
||||
I64 bytes_received = -1;
|
||||
while (bytes_received != 0) {
|
||||
bytes_received = s->receive(response_buffer + response_buffer_size, 16384);
|
||||
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);
|
||||
|
||||
@slon_free(session, response_buffer);
|
||||
@slon_free(session, send_buffer);
|
||||
@slon_free(session, headers);
|
||||
@slon_free(session, payload);
|
||||
Free(data);
|
||||
|
||||
return obj->@("url");
|
||||
}
|
||||
|
||||
U0 @slon_api_v2_media_post(SlonHttpSession* session)
|
||||
{
|
||||
// NOTE: We only support images at the moment
|
||||
|
||||
SLON_SCRATCH_BUFFER_AND_REQUEST_JSON
|
||||
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) {
|
||||
session->status(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StrFind("\r\n\r\n", data)) {
|
||||
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;
|
||||
}
|
||||
|
||||
I32 width = 0;
|
||||
I32 height = 0;
|
||||
I32 comp = 0;
|
||||
I32 code = @stbi_info_from_memory(file_ptr, file_size, &width, &height, &comp);
|
||||
|
||||
// Buffer contains a valid image file
|
||||
if (code != 1) {
|
||||
session->status(400);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
} else {
|
||||
session->status(400);
|
||||
}
|
||||
|
||||
// Delete image from RAM disk
|
||||
Del(scratch_buffer);
|
||||
|
||||
@slon_free(session, media_id);
|
||||
} else {
|
||||
session->status(401);
|
||||
}
|
||||
}
|
4
Slon/Endpoints/Post/Media.HC
Normal file
4
Slon/Endpoints/Post/Media.HC
Normal file
|
@ -0,0 +1,4 @@
|
|||
if (!StrICmp("/api/v2/media", session->path())) {
|
||||
@slon_api_v2_media_post(session);
|
||||
return;
|
||||
}
|
4
Slon/Endpoints/Put/Media.HC
Normal file
4
Slon/Endpoints/Put/Media.HC
Normal file
|
@ -0,0 +1,4 @@
|
|||
if (String.BeginsWith("/api/v1/media", session->path())) {
|
||||
@slon_api_v1_media_put(session);
|
||||
return;
|
||||
}
|
|
@ -482,6 +482,11 @@ U0 @slon_http_parse_request_as_form_urlencoded(SlonHttpSession* session)
|
|||
|
||||
U0 @slon_http_parse_request_as_multipart_form_data(SlonHttpSession* session)
|
||||
{
|
||||
if (StrFind("; filename=", session->request->data)) {
|
||||
// Skip parsing - this is a media upload
|
||||
session->request->json = Json.Parse("{}");
|
||||
return;
|
||||
}
|
||||
U8* json_string = @slon_http_json_string_from_multipart_form_data(session, session->request->data);
|
||||
session->request->json = Json.Parse(json_string);
|
||||
@slon_free(session, json_string);
|
||||
|
@ -589,6 +594,7 @@ U0 @slon_http_handle_post_request(SlonHttpSession* session)
|
|||
#include "Endpoints/Post/ActivityPub";
|
||||
#include "Endpoints/Post/Apps";
|
||||
#include "Endpoints/Post/Markers";
|
||||
#include "Endpoints/Post/Media";
|
||||
#include "Endpoints/Post/OAuth";
|
||||
#include "Endpoints/Post/Statuses";
|
||||
|
||||
|
@ -597,6 +603,29 @@ U0 @slon_http_handle_post_request(SlonHttpSession* session)
|
|||
session->status(404);
|
||||
}
|
||||
|
||||
U0 @slon_http_handle_put_request(SlonHttpSession* session)
|
||||
{
|
||||
if (StrFind("json", session->header("content-type")) > 0) {
|
||||
@slon_http_parse_request_as_json(session);
|
||||
}
|
||||
if (String.BeginsWith("application/x-www-form-urlencoded", session->header("content-type"))) {
|
||||
@slon_http_parse_request_as_form_urlencoded(session);
|
||||
}
|
||||
if (String.BeginsWith("multipart/form-data", session->header("content-type"))) {
|
||||
@slon_http_parse_request_as_multipart_form_data(session);
|
||||
}
|
||||
|
||||
SLON_DEBUG_PRINT_REQUEST_JSON
|
||||
|
||||
/* clang-format off */
|
||||
|
||||
#include "Endpoints/Put/Media";
|
||||
|
||||
/* clang-format on */
|
||||
|
||||
session->status(404);
|
||||
}
|
||||
|
||||
U0 @slon_http_handle_request(SlonHttpSession* session)
|
||||
{
|
||||
|
||||
|
@ -618,6 +647,9 @@ U0 @slon_http_handle_request(SlonHttpSession* session)
|
|||
case SLON_HTTP_VERB_POST:
|
||||
@slon_http_handle_post_request(session);
|
||||
break;
|
||||
case SLON_HTTP_VERB_PUT:
|
||||
@slon_http_handle_put_request(session);
|
||||
break;
|
||||
default:
|
||||
session->status(405);
|
||||
}
|
||||
|
|
|
@ -19,12 +19,14 @@ WinMax(Fs);
|
|||
#include "Api/V1/FollowRequests";
|
||||
#include "Api/V1/FollowedTags";
|
||||
#include "Api/V1/Markers";
|
||||
#include "Api/V1/Media";
|
||||
#include "Api/V1/Notifications";
|
||||
#include "Api/V1/Statuses";
|
||||
#include "Api/V1/Timelines";
|
||||
|
||||
#include "Api/V2/Filters";
|
||||
#include "Api/V2/Instance";
|
||||
#include "Api/V2/Media";
|
||||
#include "Api/V2/Search";
|
||||
#include "Api/V2/Suggestions";
|
||||
|
||||
|
|
|
@ -433,6 +433,8 @@ U0 @slon_activitypub_async_accept_request(JsonObject* o)
|
|||
U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest)
|
||||
{
|
||||
Sleep(1000);
|
||||
I64 i;
|
||||
|
||||
U8 scratch_buffer[2048];
|
||||
|
||||
U8* this_actor = StrNew(status->@("uri"), adam_task);
|
||||
|
@ -466,7 +468,6 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest)
|
|||
// lookup status uri in user's home timeline
|
||||
JsonArray* lookup_array = db->o("timelines")->o("home")->a(status->o("account")->@("id"));
|
||||
if (lookup_array) {
|
||||
I64 i;
|
||||
for (i = 0; i < lookup_array->length; i++) {
|
||||
if (!StrICmp(status->@("in_reply_to_id"), lookup_array->o(i)->@("id"))) {
|
||||
note_object->set("inReplyTo", lookup_array->o(i)->@("uri"), JSON_STRING);
|
||||
|
@ -483,7 +484,26 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest)
|
|||
JsonObject* content_map = Json.CreateObject();
|
||||
content_map->set("en", status->@("content"), JSON_STRING);
|
||||
note_object->set("contentMap", content_map, JSON_OBJECT);
|
||||
note_object->set("attachment", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
JsonArray* attachment_array = NULL;
|
||||
if (status->@("media_attachments") && status->a("media_attachments")->length) {
|
||||
attachment_array = Json.CreateArray();
|
||||
JsonArray* media_attachments = status->a("media_attachments");
|
||||
JsonObject* masto_attachment = NULL;
|
||||
JsonObject* ap_attachment = NULL;
|
||||
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);
|
||||
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);
|
||||
ap_attachment->set("height", masto_attachment->o("meta")->o("original")->@("height"), JSON_NUMBER);
|
||||
attachment_array->append(Json.CreateItem(ap_attachment, JSON_OBJECT));
|
||||
}
|
||||
note_object->set("attachment", attachment_array, JSON_ARRAY);
|
||||
} else {
|
||||
note_object->set("attachment", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
}
|
||||
note_object->set("tag", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||
note_object->set("replies", SLON_EMPTY_JSON_OBJECT, JSON_OBJECT);
|
||||
note_object->set("likes", SLON_EMPTY_JSON_OBJECT, JSON_OBJECT);
|
||||
|
@ -494,6 +514,9 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest)
|
|||
StrPrint(scratch_buffer, "%s/inbox", dest);
|
||||
@slon_activitypub_signed_request(scratch_buffer, fetch_buffer, create_object);
|
||||
Free(fetch_buffer);
|
||||
if (attachment_array) {
|
||||
Json.Delete(attachment_array);
|
||||
}
|
||||
Json.Delete(create_object);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#define SLON_MISSING_ACCOUNT_AVATAR "https://slon-project.org/images/avatar-missing.png"
|
||||
|
||||
#define SLON_DB_PATH "A:/db"
|
||||
#define SLON_MEDIA_PATH "B:/media"
|
||||
|
||||
JsonObject* db = Json.CreateObject();
|
||||
|
||||
|
@ -279,6 +280,7 @@ U0 @slon_db_load_from_defaults()
|
|||
db->set("following", Json.CreateObject(), JSON_OBJECT);
|
||||
db->set("instance", Json.ParseFile("M:/Slon/Static/defaults/instance.json"), JSON_OBJECT);
|
||||
db->set("markers", Json.CreateObject(), JSON_OBJECT);
|
||||
db->set("media", Json.CreateObject(), JSON_OBJECT);
|
||||
db->set("statuses", Json.CreateObject(), JSON_OBJECT);
|
||||
db->set("timelines", Json.CreateObject(), JSON_OBJECT);
|
||||
db->o("timelines")->set("home", Json.CreateObject(), JSON_OBJECT);
|
||||
|
@ -305,6 +307,7 @@ U0 @slon_db_load_from_disk()
|
|||
@slon_db_load_following_from_disk();
|
||||
@slon_db_load_instance_from_disk();
|
||||
@slon_db_load_markers_from_disk();
|
||||
db->set("media", Json.CreateObject(), JSON_OBJECT);
|
||||
@slon_db_load_oauth_from_disk();
|
||||
@slon_db_load_statuses_from_disk();
|
||||
@slon_db_load_timelines_from_disk();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue