diff --git a/Slon/Api/V1/Announcements.HC b/Slon/Api/V1/Announcements.HC new file mode 100644 index 0000000..7372d9b --- /dev/null +++ b/Slon/Api/V1/Announcements.HC @@ -0,0 +1,162 @@ +// Internally, "reactions" is stored as { "emoji": [ "account_id", "account_id", ...]} +// This is presented to the client as: "reactions": [{ "name": "emoji", "count": (count), "me": (true|false) }, ...] + +U0 @slon_api_v1_announcements_delete(SlonHttpSession* session) +{ + if (@slon_api_authorized(session)) { + SLON_AUTH_ACCOUNT_ID + + if (session->path_count() < 6) { + session->status(400); + return; + } + + I64 i; + U8* id = session->path(3); + U8* verb = session->path(4); + U8* emoji = @slon_http_decode_urlencoded_string(session, session->path(5)); + + JsonObject* announcement = @slon_api_announcement_by_id(id); + if (!announcement) { + @slon_free(session, emoji); + session->status(404); + return; + } + + if (!StrICmp("reactions", verb)) { + JsonArray* emoji_array = announcement->o("reactions")->a(emoji); + Bool save_announcements = FALSE; + if (emoji_array && emoji_array->contains(account_id)) { + for (i = 0; i < emoji_array->length; i++) { + if (!StrICmp(account_id, emoji_array->@(i))) { + emoji_array->remove(i); + if (!emoji_array->length) { + announcement->o("reactions")->unset(emoji); + } + @slon_db_save_announcements_to_disk; + break; + } + } + } + } + @slon_free(session, emoji); + session->send(SLON_EMPTY_JSON_OBJECT); + } else { + session->status(401); + } +} + +U0 @slon_api_v1_announcements_get(SlonHttpSession* session) +{ + if (@slon_api_authorized(session)) { + SLON_AUTH_ACCOUNT_ID + + JsonArray* announcements = Json.CreateArray(session->mem_task); + JsonArray* iter_array = db->a("announcements"); + JsonObject* announcement = NULL; + JsonKey* reaction_key = NULL; + JsonObject* reaction_object = NULL; + + I64 i; + for (i = 0; i < iter_array->length; i++) { + announcement = Json.Clone(iter_array->o(i), session->mem_task); + if (announcement->a("read_users")->contains(account_id)) { + announcement->set("read", TRUE, JSON_BOOLEAN); + } + announcement->unset("read_users"); + JsonArray* reactions_array = Json.CreateArray(session->mem_task); + reaction_key = announcement->o("reactions")->keys; + while (reaction_key) { + reaction_object = Json.CreateObject(session->mem_task); + reaction_object->set("name", reaction_key->name, JSON_STRING); + reaction_object->set("count", reaction_key->value(JsonArray*)->length, JSON_NUMBER); + reaction_object->set("me", reaction_key->value(JsonArray*)->contains(account_id), JSON_BOOLEAN); + reactions_array->append(reaction_object); + reaction_key = reaction_key->next; + } + announcement->set("reactions", reactions_array, JSON_ARRAY); + + announcements->append(announcement); + } + + session->send(announcements); + } else { + session->status(401); + } +} + +U0 @slon_api_v1_announcements_post(SlonHttpSession* session) +{ + if (@slon_api_authorized(session)) { + SLON_AUTH_ACCOUNT_ID + + if (session->path_count() < 5) { + session->status(400); + return; + } + + U8* id = session->path(3); + U8* verb = session->path(4); + + JsonObject* announcement = @slon_api_announcement_by_id(id); + if (!announcement) { + session->status(404); + return; + } + + if (!StrICmp("dismiss", verb)) { + if (!announcement->a("read_users")->contains(account_id)) { + announcement->a("read_users")->append(account_id); + @slon_db_save_announcements_to_disk; + } + } + + session->send(SLON_EMPTY_JSON_OBJECT); + } else { + session->status(401); + } +} + +U0 @slon_api_v1_announcements_put(SlonHttpSession* session) +{ + if (@slon_api_authorized(session)) { + SLON_AUTH_ACCOUNT_ID + + if (session->path_count() < 6) { + session->status(400); + return; + } + + U8* id = session->path(3); + U8* verb = session->path(4); + U8* emoji = @slon_http_decode_urlencoded_string(session, session->path(5)); + + JsonObject* announcement = @slon_api_announcement_by_id(id); + if (!announcement) { + @slon_free(session, emoji); + session->status(404); + return; + } + + if (!StrICmp("reactions", verb)) { + JsonArray* emoji_array = announcement->o("reactions")->a(emoji); + Bool save_announcements = FALSE; + if (!emoji_array) { + emoji_array = Json.CreateArray(slon_db_mem_task); + announcement->o("reactions")->set(emoji, emoji_array, JSON_ARRAY); + save_announcements = TRUE; + } + if (!emoji_array->contains(account_id)) { + emoji_array->append(account_id); + save_announcements = TRUE; + } + if (save_announcements) { + @slon_db_save_announcements_to_disk; + } + } + @slon_free(session, emoji); + session->send(SLON_EMPTY_JSON_OBJECT); + } else { + session->status(401); + } +} diff --git a/Slon/Endpoints/Delete/Announcements.HC b/Slon/Endpoints/Delete/Announcements.HC new file mode 100644 index 0000000..321c081 --- /dev/null +++ b/Slon/Endpoints/Delete/Announcements.HC @@ -0,0 +1,4 @@ +if (String.BeginsWith("/api/v1/announcements", session->path())) { + @slon_api_v1_announcements_delete(session); + return; +} diff --git a/Slon/Endpoints/Get/Announcements.HC b/Slon/Endpoints/Get/Announcements.HC new file mode 100644 index 0000000..4a971ce --- /dev/null +++ b/Slon/Endpoints/Get/Announcements.HC @@ -0,0 +1,4 @@ +if (!StrICmp("/api/v1/announcements", session->path())) { + @slon_api_v1_announcements_get(session); + return; +} diff --git a/Slon/Endpoints/Post/Announcements.HC b/Slon/Endpoints/Post/Announcements.HC new file mode 100644 index 0000000..80778c8 --- /dev/null +++ b/Slon/Endpoints/Post/Announcements.HC @@ -0,0 +1,4 @@ +if (String.BeginsWith("/api/v1/announcements", session->path())) { + @slon_api_v1_announcements_post(session); + return; +} diff --git a/Slon/Endpoints/Put/Announcements.HC b/Slon/Endpoints/Put/Announcements.HC new file mode 100644 index 0000000..4bffbbe --- /dev/null +++ b/Slon/Endpoints/Put/Announcements.HC @@ -0,0 +1,4 @@ +if (String.BeginsWith("/api/v1/announcements", session->path())) { + @slon_api_v1_announcements_put(session); + return; +} diff --git a/Slon/Http/AdminServer.HC b/Slon/Http/AdminServer.HC index e205778..6d1bbc2 100644 --- a/Slon/Http/AdminServer.HC +++ b/Slon/Http/AdminServer.HC @@ -248,6 +248,28 @@ U0 @slon_admin_delete_account(SlonHttpSession* session) session->send(SLON_EMPTY_JSON_OBJECT); } +U0 @slon_admin_delete_announcement(SlonHttpSession* session) +{ + SLON_SCRATCH_BUFFER_AND_REQUEST_JSON + no_warn scratch_buffer; + + if (!request_json->@("id")) + return; + I64 i; + JsonArray* announcements = db->a("announcements"); + JsonObject* announcement = NULL; + for (i = 0; i < announcements->length; i++) { + announcement = announcements->o(i); + if (announcement && !StrICmp(request_json->@("id"), announcement->@("id"))) { + AdamLog("deleting announcement %d\n", i); + announcements->remove(i); + break; + } + } + @slon_db_save_announcements_to_disk; + session->send(SLON_EMPTY_JSON_OBJECT); +} + U0 @slon_admin_new_account(SlonHttpSession* session) { SLON_SCRATCH_BUFFER_AND_REQUEST_JSON @@ -262,6 +284,38 @@ U0 @slon_admin_new_account(SlonHttpSession* session) } } +U0 @slon_admin_new_announcement(SlonHttpSession* session) +{ + SLON_SCRATCH_BUFFER_AND_REQUEST_JSON + + if (request_json->@("content")) { + U8* id = @slon_api_generate_unique_id(session); + U8* timestamp = @slon_api_timestamp_from_cdate(session, Now); + JsonObject* announcement = Json.CreateObject(slon_db_mem_task); + announcement->set("id", id, JSON_STRING); + announcement->set("content", request_json->@("content"), JSON_STRING); + announcement->set("starts_at", NULL, JSON_NULL); + announcement->set("ends_at", NULL, JSON_NULL); + announcement->set("all_day", FALSE, JSON_BOOLEAN); + announcement->set("published_at", timestamp, JSON_STRING); + announcement->set("updated_at", timestamp, JSON_STRING); + announcement->set("read", FALSE, JSON_BOOLEAN); + announcement->set("read_users", Json.CreateArray(slon_db_mem_task), JSON_ARRAY); + announcement->set("mentions", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY); + announcement->set("statuses", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY); + announcement->set("tags", Json.CreateArray(slon_db_mem_task), JSON_ARRAY); + announcement->set("emojis", Json.CreateArray(slon_db_mem_task), JSON_ARRAY); + // Internally, "reactions" is { "emoji": [ "account_id", "account_id", ... ]} + // This is presented to the client as: "reactions": [ { "name": "emoji", "count": xxx, "me": true/false }, ... ] + announcement->set("reactions", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); + db->a("announcements")->append(announcement); + @slon_db_save_announcements_to_disk; + @slon_free(session, id); + @slon_free(session, timestamp); + } + session->send(SLON_EMPTY_JSON_OBJECT); +} + U0 @slon_admin_manage_accounts(SlonHttpSession* session) { SLON_SCRATCH_BUFFER_AND_REQUEST_JSON @@ -324,14 +378,26 @@ U0 @slon_admin_server_get(SlonHttpSession* session) return; } + if (!StrICmp("/delete/announcement", session->path())) { + @slon_admin_delete_announcement(session); + return; + } + if (!StrICmp("/manage/accounts", session->path())) { @slon_admin_manage_accounts(session); return; } + + if (!StrICmp("/manage/announcements", session->path())) { + session->send(db->a("announcements")); + return; + } + if (!StrICmp("/manage/instance", session->path())) { session->send(db->o("instance")); return; } + if (!StrICmp("/manage/settings", session->path())) { session->send(db->o("settings")); return; @@ -395,6 +461,11 @@ U0 @slon_admin_server_post(SlonHttpSession* session) return; } + if (!StrICmp("/new/announcement", session->path())) { + @slon_admin_new_announcement(session); + return; + } + session->status(404); } diff --git a/Slon/Http/Server.HC b/Slon/Http/Server.HC index 491dc39..e866fdc 100644 --- a/Slon/Http/Server.HC +++ b/Slon/Http/Server.HC @@ -645,6 +645,7 @@ U0 @slon_http_handle_delete_request(SlonHttpSession* session) /* clang-format off */ + #include "Endpoints/Delete/Announcements"; #include "Endpoints/Delete/Statuses"; /* clang-format on */ @@ -664,6 +665,7 @@ U0 @slon_http_handle_get_request(SlonHttpSession* session) /* clang-format off */ #include "Endpoints/Get/Accounts"; + #include "Endpoints/Get/Announcements"; #include "Endpoints/Get/ActivityPub"; #include "Endpoints/Get/Blocks"; #include "Endpoints/Get/Bookmarks"; @@ -735,6 +737,7 @@ U0 @slon_http_handle_post_request(SlonHttpSession* session) /* clang-format off */ #include "Endpoints/Post/Accounts"; + #include "Endpoints/Post/Announcements"; #include "Endpoints/Post/ActivityPub"; #include "Endpoints/Post/Apps"; #include "Endpoints/Post/Markers"; @@ -763,6 +766,7 @@ U0 @slon_http_handle_put_request(SlonHttpSession* session) /* clang-format off */ + #include "Endpoints/Put/Announcements"; #include "Endpoints/Put/Media"; /* clang-format on */ diff --git a/Slon/MakeSlon.HC b/Slon/MakeSlon.HC index 0fd72f5..06b57ec 100644 --- a/Slon/MakeSlon.HC +++ b/Slon/MakeSlon.HC @@ -9,6 +9,7 @@ WinMax(Fs); #include "Modules/Api"; #include "Api/V1/Accounts"; +#include "Api/V1/Announcements"; #include "Api/V1/Apps"; #include "Api/V1/Blocks"; #include "Api/V1/Bookmarks"; diff --git a/Slon/Modules/Api.HC b/Slon/Modules/Api.HC index 67caa90..3b41186 100644 --- a/Slon/Modules/Api.HC +++ b/Slon/Modules/Api.HC @@ -133,6 +133,20 @@ JsonObject* @slon_api_account_by_remote_actor(U8* remote_actor) return NULL; } +JsonObject* @slon_api_announcement_by_id(U8* id) +{ + if (!id || !StrLen(id)) + return NULL; + JsonArray* announcements = db->a("announcements"); + I64 i; + for (i = 0; i < announcements->length; i++) { + if (!StrICmp(announcements->o(i)->@("id"), id)) { + return announcements->o(i); + } + } + return NULL; +} + U0 @slon_api_async_upload_to_catbox(SlonCatboxUpload* cb) { if (!cb) { diff --git a/Slon/Modules/Db.HC b/Slon/Modules/Db.HC index 9f2164c..2144049 100644 --- a/Slon/Modules/Db.HC +++ b/Slon/Modules/Db.HC @@ -51,6 +51,13 @@ U0 @slon_db_load_apps_from_disk() db->set("apps", Json.ParseFile(scratch_buffer, slon_db_mem_task), JSON_OBJECT); } +U0 @slon_db_load_announcements_from_disk() +{ + U8 scratch_buffer[256]; + StrPrint(scratch_buffer, "%s/announcements.json", SLON_DB_PATH); + db->set("announcements", Json.ParseFile(scratch_buffer, slon_db_mem_task), JSON_ARRAY); +} + U0 @slon_db_load_instance_from_disk() { U8 scratch_buffer[256]; @@ -230,6 +237,13 @@ U0 @slon_db_save_apps_to_disk() Json.DumpToFile(scratch_buffer, db->o("apps"), slon_db_mem_task); } +U0 @slon_db_save_announcements_to_disk() +{ + U8 scratch_buffer[256]; + StrPrint(scratch_buffer, "%s/announcements.json", SLON_DB_PATH); + Json.DumpToFile(scratch_buffer, db->a("announcements"), slon_db_mem_task); +} + U0 @slon_db_save_instance_to_disk() { U8 scratch_buffer[256]; @@ -328,6 +342,7 @@ U0 @slon_db_save_to_disk() { @slon_db_save_accounts_to_disk(); @slon_db_save_actors_to_disk(); + @slon_db_save_announcements_to_disk(); @slon_db_save_apps_to_disk(); @slon_db_save_followers_to_disk(); @slon_db_save_following_to_disk(); @@ -344,6 +359,7 @@ U0 @slon_db_load_from_defaults() { db->set("accounts", Json.CreateArray(slon_db_mem_task), JSON_ARRAY); db->set("actors", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); + db->set("announcements", Json.CreateArray(slon_db_mem_task), JSON_ARRAY); db->set("apps", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); db->set("idempotency_keys", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); db->set("private_keys", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); @@ -372,6 +388,7 @@ U0 @slon_db_load_from_disk() { @slon_db_load_accounts_from_disk(); @slon_db_load_actors_from_disk(); + @slon_db_load_announcements_from_disk(); @slon_db_load_apps_from_disk(); db->set("idempotency_keys", Json.CreateObject(slon_db_mem_task), JSON_OBJECT); @slon_db_load_private_keys_from_disk(); diff --git a/Slon/Static/html/admin/main.html b/Slon/Static/html/admin/main.html index e469bea..29d47c9 100644 --- a/Slon/Static/html/admin/main.html +++ b/Slon/Static/html/admin/main.html @@ -35,6 +35,7 @@ @@ -95,6 +96,25 @@ setContent(html); setActiveLink("accounts"); } + async function manageAnnouncements() { + clearActiveLinks(); + const request = new Request("/manage/announcements"); + const response = await fetch(request); + const announcements = await response.json(); + let html = "

Announcements

"; + if (announcements.length) { + html += announcements.length + " announcement(s)
"; + html += ""; + for (let i = 0; i < announcements.length; i++) { + html += ""; + } + } else { + html += "No announcements
"; + } + html += "

"; + setContent(html); + setActiveLink("announcements"); + } async function manageInstance() { clearActiveLinks(); const request = new Request("/manage/instance"); @@ -182,6 +202,14 @@ const response = await fetch(request); manageSettings(); } + async function confirmDeleteAnnouncement(id) { + if (confirm("Are you sure you want to delete announcement id " + id + " ?")) { + const request = new Request("/delete/announcement?id=" + id); + const response = await fetch(request); + const empty_json = await response.json(); + manageAnnouncements(); + } + } async function confirmDeleteUser(user, id) { if (confirm("Are you sure you want to delete '" + user + "' ?")) { const request = new Request("/delete/account?id=" + id); @@ -190,6 +218,23 @@ manageAccounts(0); } } + function createNewAnnouncement() { + clearActiveLinks(); + let html = "

New Announcement

"; + + html += "
"; + + html += "
"; + html += ""; + html += "
"; + + html += "
"; + html += "
" + html += ""; + + setContent(html); + setActiveLink("announcements"); + } function manageNewUser() { clearActiveLinks(); let html = "

New User

"; @@ -216,6 +261,21 @@ setContent(html); setActiveLink("accounts"); } + async function saveNewAnnouncement() { + let data = {"content": document.getElementById("announcement-content").value}; + const request = new Request("/new/announcement", { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify(data) + }); + const response = await fetch(request); + const json = await response.json(); + if (!Object.keys(json).length) { + manageAnnouncements(); + } else { + alert(JSON.stringify(json)); + } + } async function saveNewUser() { let data = {}; let fields = document.getElementsByTagName("input");
idpublished_atupdated_atcontentdelete
" + announcements[i]["id"] + "" + announcements[i]["published_at"] + "" + announcements[i]["updated_at"] + "" + announcements[i]["content"] + "