diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..2480ecf --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Always +BreakBeforeBraces: Attach +PointerAlignment: Left +AllowShortIfStatementsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AlwaysBreakTemplateDeclarations: Yes +SpacesInParentheses: false +SpaceBeforeParens: ControlStatements +SpaceAfterCStyleCast: true +KeepEmptyLinesAtTheStartOfBlocks: false +ColumnLimit: 100 +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignTrailingComments: true +IncludeBlocks: Regroup +ReflowComments: true +... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2a16c9a..04b4cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ +*DS_Store* *.log *.db *.yaml -audio/ data/ sessions/ -img/ -fonts/ tmp/ cmake-* *build* diff --git a/CMakeLists.txt b/CMakeLists.txt index 7898c57..e76c7a2 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ set(PROJECT_SOURCE_FILES src/av_io.cpp src/wadinfo.cpp src/dol.cpp + src/post_handlers.cpp ) include_directories(include) @@ -67,6 +68,11 @@ endif() add_executable(${PROJECT_NAME} ${PROJECT_SOURCE_FILES}) +target_compile_options(${PROJECT_NAME} PRIVATE + $<$:/W4 /WX> + $<$>:-Wall -Wextra -Werror> +) + target_link_libraries(${PROJECT_NAME} PRIVATE Boost::system yaml-cpp::yaml-cpp diff --git a/audio/click.wav b/audio/click.wav new file mode 100644 index 0000000..06c849e Binary files /dev/null and b/audio/click.wav differ diff --git a/css/ff.css b/css/ff.css index 375d5fe..73f25fa 100644 --- a/css/ff.css +++ b/css/ff.css @@ -3,8 +3,31 @@ src: url('../fonts/font.woff2') format('woff2'); } +.floating_window { + position: fixed; + min-width: 300px; + min-height: 100px; + max-width: 90vw; + max-height: 90vh; + + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + border-radius: 10px; + padding: 10px; + z-index: 9999; + text-align: center; + + overflow-y: auto; + overflow-x: hidden; + + box-sizing: border-box; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + /* buttons */ -#login-button, #login-window { +#login-button, #login-window, #tos-window { background-color: #a9def9; color: #000; } @@ -12,7 +35,7 @@ background-color: #ff99c8; color: #000; } -#logout-button { +#logout-button, #logout-window { background-color: #f08080; color: #000; } @@ -36,6 +59,14 @@ background-color: #000000; color: #fff; } +#forum-button { + background-color: #f0f0f0; + color: #000; +} +.forum_window { + background-color: #f0f0f0; + color: #000; +} #sandbox-button, #sandbox-window, #file-window { background-color: #c4c4c4; color: #000; @@ -52,6 +83,25 @@ background-color: #fcf6bd; color: #000; } +#browse-window, #sandbox-window { + min-width: 90%; + min-height: 90%; +} + +#announcements-window, #announcement-window, #create-announcement-window { + min-width: 60%; + min-height: 60%; +} + +#view-window, #file-window, .forum_window { + min-width: 70%; + min-height: 70%; +} + +#profile-window, #edit-profile-window, #terms-window { + min-width: 50%; + min-height: 50%; +} .announcement_div { background-color: #fff; color: #000; @@ -106,16 +156,6 @@ body { overflow: auto; } -.link_box { - background-color: #f0f0f0; /* default, if you don't override through classes or IDs */ - color: #000000; /* default, if you don't override through classes or IDs */ - border-radius: 10px; - padding: 10px; - margin-top: 10px; - min-width: 300px; - text-align: center; -} - .link_box_container { display: flex; flex-direction: column; @@ -132,7 +172,13 @@ body { } .link_box { + background-color: #f0f0f0; /* default, if you don't override through classes or IDs */ + color: #000000; /* default, if you don't override through classes or IDs */ + margin-top: 10px; + min-width: 300px; + text-align: center; flex: 1 1 calc(50% - 1rem); + box-sizing: border-box; padding: 1rem; border: 1px solid #ccc; @@ -140,6 +186,15 @@ body { cursor: pointer; transition: background 0.3s; max-width: 100%; + + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + position: relative; + + width: 250px; + height: 100px; + + overflow: hidden; } @media (max-width: 768px) { @@ -158,32 +213,6 @@ body { font-size: 1rem; } - -.floating_window { - position: fixed; - min-width: 300px; - min-height: 100px; - max-width: 90vw; - max-height: 90vh; - - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - border-radius: 10px; - padding: 10px; - z-index: 9999; - text-align: center; - - overflow-y: auto; - overflow-x: hidden; - - background-color: #70d6ff; - color: #000000; - - box-sizing: border-box; -} - .file_div { background-color: #fafafa; color: #000000; @@ -193,26 +222,6 @@ body { margin: 10px auto; } -#browse-window, #sandbox-window { - min-width: 90%; - min-height: 90%; -} - -#announcements-window, #announcement-window, #create-announcement-window { - min-width: 60%; - min-height: 60%; -} - -#view-window, #file-window { - min-width: 70%; - min-height: 70%; -} - -#profile-window, #edit-profile-window, #terms-window { - min-width: 50%; - min-height: 50%; -} - .view_floating_window_comment_meta { display: flex; align-items: center; @@ -231,7 +240,6 @@ body { } } - .view_floating_window_comment_logo { width: 24px; height: 24px; @@ -313,6 +321,60 @@ body { color: #000; } +.post-file { + background-color: #fafafa; + color: #000000; + border-radius: 10px; + padding: 10px; + width: 80%; + margin: 10px auto; + overflow: hidden; +} + +.forum-topic { + background-color: #fafafa; + color: #000000; + border-radius: 10px; + width: 80%; + margin: 10px auto; +} + +.forum-post { + background-color: aliceblue; + color: #000000; + border-radius: 10px; + width: 80%; + margin: 10px auto; +} + +.post-comment { + background-color: aliceblue; + border-radius: 5px; + padding: 5px; + margin-bottom: 10px; +} + +#comments_div { + max-height: 500px; + overflow-y: auto; +} + +.post-comment-file { + background-color: #fafafa; + color: #7289da; + padding: 5px; +} + +.forum-topic:hover, .forum-post:hover, .post-file:hover { + transform: scale(1.025); + transition: transform 0.3s ease; +} + +button:hover { + transform: scale(1.025); + transition: transform 0.3s ease; +} + input, button, select, textarea { background-color: #fafafa; color: #000000; diff --git a/fonts/font.woff2 b/fonts/font.woff2 new file mode 100644 index 0000000..dca2c58 Binary files /dev/null and b/fonts/font.woff2 differ diff --git a/http/create_topic.http b/http/create_topic.http new file mode 100644 index 0000000..8bb7b60 --- /dev/null +++ b/http/create_topic.http @@ -0,0 +1,11 @@ +POST /api/create_topic HTTP/1.1 +Host: localhost:8080 +Content-Type: application/json + +{ + "title": "New Topic", + "description": "This is a new topic created for testing purposes.", + + "username": "jacob", + "key": "baR5ZE7v07z5MygYKlHuWUPJbB0pDDkfmLFNbMuna6lrrwxOH96zQXJojUrUtHq2" +} \ No newline at end of file diff --git a/http/rate_forwarder.http b/http/rate_forwarder.http index 10c590e..1ef825e 100644 --- a/http/rate_forwarder.http +++ b/http/rate_forwarder.http @@ -5,5 +5,5 @@ Content-Type: application/json "forwarder_id": "jRF5k1YB", "rating": 5, "username": "jacob", - "key": "DQFLbDpU8tmK0uWrVi3vxXeL0iVCOf5PhXH9YgAz9wpOBaYOmovXBBvE7aeLLHVO" + "key": "baR5ZE7v07z5MygYKlHuWUPJbB0pDDkfmLFNbMuna6lrrwxOH96zQXJojUrUtHq2" } diff --git a/img/announcements.svg b/img/announcements.svg new file mode 100644 index 0000000..75ccc32 --- /dev/null +++ b/img/announcements.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/img/background-logo-1.png b/img/background-logo-1.png new file mode 100644 index 0000000..b47890e Binary files /dev/null and b/img/background-logo-1.png differ diff --git a/img/coin.svg b/img/coin.svg new file mode 100644 index 0000000..e74911f --- /dev/null +++ b/img/coin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/discord.svg b/img/discord.svg new file mode 100644 index 0000000..465e56d --- /dev/null +++ b/img/discord.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/img/favicon.svg b/img/favicon.svg new file mode 100644 index 0000000..5e3930d --- /dev/null +++ b/img/favicon.svg @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/grab-moving.png b/img/grab-moving.png new file mode 100644 index 0000000..01ab1e4 Binary files /dev/null and b/img/grab-moving.png differ diff --git a/img/grab.png b/img/grab.png new file mode 100644 index 0000000..01ab1e4 Binary files /dev/null and b/img/grab.png differ diff --git a/img/hammer.svg b/img/hammer.svg new file mode 100644 index 0000000..cb38918 --- /dev/null +++ b/img/hammer.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/img/logo.svg b/img/logo.svg new file mode 100644 index 0000000..5e3930d --- /dev/null +++ b/img/logo.svg @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/messages.svg b/img/messages.svg new file mode 100644 index 0000000..a165504 --- /dev/null +++ b/img/messages.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/img/original_cursors/grab copy.png b/img/original_cursors/grab copy.png new file mode 100644 index 0000000..c5e5a9a Binary files /dev/null and b/img/original_cursors/grab copy.png differ diff --git a/img/original_cursors/grab-moving copy.png b/img/original_cursors/grab-moving copy.png new file mode 100644 index 0000000..c5e5a9a Binary files /dev/null and b/img/original_cursors/grab-moving copy.png differ diff --git a/img/original_cursors/pointer copy.png b/img/original_cursors/pointer copy.png new file mode 100644 index 0000000..ee125bc Binary files /dev/null and b/img/original_cursors/pointer copy.png differ diff --git a/img/original_cursors/pointer-moving copy.png b/img/original_cursors/pointer-moving copy.png new file mode 100644 index 0000000..ee125bc Binary files /dev/null and b/img/original_cursors/pointer-moving copy.png differ diff --git a/img/pen.svg b/img/pen.svg new file mode 100644 index 0000000..384b352 --- /dev/null +++ b/img/pen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/pointer-moving.png b/img/pointer-moving.png new file mode 100644 index 0000000..a36e70a Binary files /dev/null and b/img/pointer-moving.png differ diff --git a/img/pointer.png b/img/pointer.png new file mode 100644 index 0000000..a36e70a Binary files /dev/null and b/img/pointer.png differ diff --git a/img/question-mark-block.svg b/img/question-mark-block.svg new file mode 100644 index 0000000..89614d4 --- /dev/null +++ b/img/question-mark-block.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/img/retro-star.svg b/img/retro-star.svg new file mode 100644 index 0000000..daf36d3 --- /dev/null +++ b/img/retro-star.svg @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/img/shovel.svg b/img/shovel.svg new file mode 100644 index 0000000..99ba33f --- /dev/null +++ b/img/shovel.svg @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/img/star.svg b/img/star.svg new file mode 100644 index 0000000..7d54606 --- /dev/null +++ b/img/star.svg @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/img/wave.svg b/img/wave.svg new file mode 100644 index 0000000..36a5d29 --- /dev/null +++ b/img/wave.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/include/account_creation_status_enum.hpp b/include/account_creation_status_enum.hpp new file mode 100755 index 0000000..7aa39d4 --- /dev/null +++ b/include/account_creation_status_enum.hpp @@ -0,0 +1,17 @@ +#pragma once + +namespace ff { + enum class AccountCreationStatus { + Success, + Failure, + UsernameExists, + UsernameTooShort, + UsernameTooLong, + PasswordTooShort, + PasswordTooLong, + InvalidUsername, + InvalidPassword, + InvalidEmail, + EmailExists, + }; +} // namespace ff diff --git a/include/cache_manager.hpp b/include/cache_manager.hpp new file mode 100755 index 0000000..04df16a --- /dev/null +++ b/include/cache_manager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace ff { + class CacheManager { + using FileContent = std::string; + using FileName = std::string; + std::vector> cache{}; + public: + explicit CacheManager() = default; + ~CacheManager() = default; + + [[nodiscard]] FileContent open_file(const FileName& fp, const bool cache = true) { //NOLINT + if (cache && settings.cache_static) { + for (const auto& it : this->cache) if (it.first == fp) return it.second; + if (!std::filesystem::exists(fp)) { + throw std::runtime_error{"File does not exist."}; + } + std::ifstream file{fp}; + const std::string& data = {std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; + this->cache.emplace_back(fp, data); + return data; + } + + std::ifstream file{fp}; + return {std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; + } + }; +} // namespace ff diff --git a/include/database.hpp b/include/database.hpp new file mode 100755 index 0000000..80daf85 --- /dev/null +++ b/include/database.hpp @@ -0,0 +1,70 @@ +#pragma once + +#define LIMHAMN_DATABASE_IMPL +#include + +namespace ff { + class database { +#if FF_ENABLE_SQLITE + limhamn::database::sqlite3_database sqlite{}; +#define SQLITE_HANDLE this->sqlite +#endif +#if FF_ENABLE_POSTGRESQL + limhamn::database::postgresql_database postgres{}; +#define POSTGRES_HANDLE this->postgres +#endif +#ifndef SQLITE_HANDLE +#define SQLITE_HANDLE this->postgres +#endif +#ifndef POSTGRES_HANDLE +#define POSTGRES_HANDLE this->sqlite +#endif + + bool enabled_type = false; // false = sqlite, true = postgres + public: + explicit database(bool type) : enabled_type(type) {} + std::vector> query(const std::string& query) { + if (!this->enabled_type) { + return SQLITE_HANDLE.query(query); + } else { + return POSTGRES_HANDLE.query(query); + } + } + bool exec(const std::string& query) { + if (!this->enabled_type) { + return SQLITE_HANDLE.exec(query); + } else { + return POSTGRES_HANDLE.exec(query); + } + } + template + std::vector> query(const std::string& query, Args... args) { + if (!this->enabled_type) { + return SQLITE_HANDLE.query(query, args...); + } else { + return POSTGRES_HANDLE.query(query, args...); + } + } + template + bool exec(const std::string& query, Args... args) { + if (!this->enabled_type) { + return SQLITE_HANDLE.exec(query, args...); + } else { + return POSTGRES_HANDLE.exec(query, args...); + } + } + [[nodiscard]] bool good() const { + return this->enabled_type ? POSTGRES_HANDLE.good() : SQLITE_HANDLE.good(); + } +#if FF_ENABLE_SQLITE + limhamn::database::sqlite3_database& get_sqlite() { + return this->sqlite; + } +#endif +#if FF_ENABLE_POSTGRESQL + limhamn::database::postgresql_database& get_postgres() { + return this->postgres; + } +#endif + }; +} // namespace ff diff --git a/include/endpoint_handlers.hpp b/include/endpoint_handlers.hpp new file mode 100755 index 0000000..9dc2907 --- /dev/null +++ b/include/endpoint_handlers.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace ff { + limhamn::http::server::response handle_try_upload_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_try_upload_post_comment_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_root_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_try_setup_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_setup_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_virtual_favicon_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_virtual_stylesheet_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_virtual_script_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_try_register_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_try_login_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_try_upload_forwarder_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_try_upload_file_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_forwarder_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_file_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_forwarders_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_files_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_set_approval_for_uploads_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_update_profile_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_profile_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_announcements_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_announcement(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_edit_announcement_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_create_announcement_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_rate_forwarder_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_rate_file_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_comment_forwarder_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_comment_file_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_comment_forwarder_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_comment_file_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_stay_logged_in(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_try_logout_endpoint(const limhamn::http::server::request& request, database& db); + + limhamn::http::server::response handle_api_create_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_edit_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_close_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_posts_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_comment_post_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_comment_post_endpoint(const limhamn::http::server::request& request, database& db); + + limhamn::http::server::response handle_api_create_topic_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_delete_topic_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_get_topics_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_edit_topic_endpoint(const limhamn::http::server::request& request, database& db); + limhamn::http::server::response handle_api_close_topic_endpoint(const limhamn::http::server::request& request, database& db); +} // namespace ff diff --git a/include/ff.hpp b/include/ff.hpp index 41fa649..11c3386 100755 --- a/include/ff.hpp +++ b/include/ff.hpp @@ -1,344 +1,31 @@ #pragma once #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #define LIMHAMN_LOGGER_IMPL -#define LIMHAMN_DATABASE_IMPL - -#include #include #define LIMHAMN_HTTP_SERVER_IMPL #define LIMHAMN_HTTP_UTILS_IMPL #include namespace ff { - struct Settings { -#ifndef FF_DEBUG - std::string access_file{"/var/log/ff/access.log"}; - std::string warning_file{"/var/log/ff/warning.log"}; - std::string error_file{"/var/log/ff/error.log"}; - std::string notice_file{"/var/log/ff/notice.log"}; - bool output_to_std{false}; - bool halt_on_error{false}; - std::string sqlite_database_file{"/var/db/ff/ff.db"}; - std::string session_directory{"/var/lib/ff/sessions"}; - std::string data_directory{"/var/lib/ff/data"}; - std::string temp_directory{"/var/tmp/ff"}; - std::string html_file{"/etc/ff/html/index.html"}; - std::string css_file{"/etc/ff/css/ff.css"}; - std::string script_file{"/etc/ff/js/ff.js"}; - std::string favicon_file{"/etc/ff/img/favicon.svg"}; - bool public_registration{true}; - std::vector> custom_paths{ - {"/img/pointer.png", "/etc/ff/img/pointer.png"}, - {"/img/pointer-moving.png", "/etc/ff/img/pointer-moving.png"}, - {"/img/grab.png", "/etc/ff/img/grab.png"}, - {"/img/grab-moving.png", "/etc/ff/img/grab-moving.png"}, - {"/img/background-logo-1.png", "/etc/ff/img/background-logo-1.png"}, - {"/img/discord.svg", "/etc/ff/img/discord.svg"}, - {"/img/star.svg", "/etc/ff/img/star.svg"}, - {"/img/pen.svg", "/etc/ff/img/pen.svg"}, - {"/img/logo.svg", "/etc/ff/img/logo.svg"}, - {"/img/announcements.svg", "/etc/ff/img/announcements.svg"}, - {"/fonts/font.woff2", "/etc/ff/fonts/font.woff2"}, - {"/audio/click.wav", "/etc/ff/audio/click.wav"}, - {"/img/favicon.ico", "/etc/ff/img/favicon.ico"}, - }; - int64_t max_request_size{250 * 1024 * 1024}; // 250mb - std::string site_url{"https://forwarderfactory.com"}; - bool enable_email_verification{true}; -#else - std::string access_file{"./access.log"}; - std::string warning_file{"./warning.log"}; - std::string error_file{"./error.log"}; - std::string notice_file{"./notice.log"}; - std::string sqlite_database_file{"./ff-debug.db"}; - bool output_to_std{true}; - bool halt_on_error{false}; - std::string session_directory{"./sessions"}; - std::string data_directory{"./data"}; - std::string temp_directory{"./tmp"}; - std::string html_file{"./html/index.html"}; - std::string css_file{"./css/ff.css"}; - std::string script_file{"./js/ff.js"}; - std::string favicon_file{"./img/favicon.svg"}; - bool public_registration{true}; - std::vector> custom_paths{ - {"/img/pointer.png", "./img/pointer.png"}, - {"/img/pointer-moving.png", "./img/pointer-moving.png"}, - {"/img/grab.png", "./img/grab.png"}, - {"/img/grab-moving.png", "./img/grab-moving.png"}, - {"/img/background-logo-1.png", "./img/background-logo-1.png"}, - {"/img/discord.svg", "./img/discord.svg"}, - {"/img/star.svg", "./img/star.svg"}, - {"/img/pen.svg", "./img/pen.svg"}, - {"/img/logo.svg", "./img/logo.svg"}, - {"/img/announcements.svg", "./img/announcements.svg"}, - {"/fonts/font.woff2", "./fonts/font.woff2"}, - {"/audio/click.wav", "./audio/click.wav"}, - }; - int64_t max_request_size{1024 * 1024 * 1024}; - std::string site_url{"http://localhost:8080"}; - bool enable_email_verification{false}; -#endif - int port{8080}; - bool log_access_to_file{true}; - bool log_warning_to_file{true}; - bool log_error_to_file{true}; - bool log_notice_to_file{true}; - std::size_t password_min_length{8}; - std::size_t password_max_length{64}; - std::size_t username_min_length{3}; - std::size_t username_max_length{32}; - std::string allowed_characters{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}; - bool allow_all_characters{false}; - std::string session_cookie_name{"ff_session"}; - std::string title{"Forwarder Factory"}; - std::string description{"Forwarder Factory is a community dedicated to preserving and sharing Nintendo- and Wii-related content."}; - int default_user_type{0}; - bool preview_files{true}; - std::string email_username{}; - std::string email_password{}; - std::string email_from{}; - std::string smtp_server{}; - int smtp_port{465}; - std::string psql_username{"postgres"}; - std::string psql_password{"postgrespasswordhere"}; - std::string psql_database{"ff"}; - std::string psql_host{"localhost"}; - int psql_port{5432}; - bool enabled_database{false}; - bool trust_x_forwarded_for{false}; - int rate_limit{100}; - std::vector blacklisted_ips{}; - std::vector whitelisted_ips{"127.0.0.1"}; - int64_t max_file_size_hash{1024 * 1024 * 1024}; - bool cache_static{false}; - bool cache_exists{false}; - bool convert_images_to_webp{true}; - bool convert_videos_to_webm{false}; - }; - - enum class AccountCreationStatus { - Success, - Failure, - UsernameExists, - UsernameTooShort, - UsernameTooLong, - PasswordTooShort, - PasswordTooLong, - InvalidUsername, - InvalidPassword, - InvalidEmail, - EmailExists, - }; - - enum class UploadStatus { - Success, - Failure, - InvalidCreds, - NoFile, - TooLarge, - }; - - enum class LoginStatus { - Success, - Failure, - Inactive, - InvalidUsername, - InvalidPassword, - Banned, - }; - - enum class ProfileUpdateStatus { - Success, - Failure, - InvalidCreds, - InvalidJson, - InvalidIcon, - }; - - enum class UserType : int { - Undefined = -1, - User = 0, - Administrator = 1, - }; - - struct FileConstruct { - std::string path{}; - std::string name{}; - std::string username{}; - std::string ip_address{}; - std::string user_agent{}; - }; - - struct RetrievedFile { - std::string path{}; - std::string name{}; - }; - - struct UserProperties { - std::string username{}; /* only filled in if cookie is valid */ - std::string ip_address{}; - std::string user_agent{}; - }; - - inline Settings settings{}; - - class StaticExists { - std::vector> paths{}; - public: - explicit StaticExists() = default; - ~StaticExists() = default; - [[nodiscard]] bool is_file(const std::string& path, const bool cache = true) { - if (settings.cache_exists && cache) { - for (const auto& it : this->paths) if (it.first == path) return it.second; - paths.emplace_back(path, std::filesystem::exists(path)); - return paths.back().second; - } else { - return std::filesystem::exists(path); - } - } - - }; - - class CacheManager { - using FileContent = std::string; - using FileName = std::string; - std::vector> cache{}; - public: - explicit CacheManager() = default; - ~CacheManager() = default; - - [[nodiscard]] FileContent open_file(const FileName& fp, const bool cache = true) { //NOLINT - if (cache && settings.cache_static) { - for (const auto& it : this->cache) if (it.first == fp) return it.second; - if (!std::filesystem::exists(fp)) { - throw std::runtime_error{"File does not exist."}; - } - std::ifstream file{fp}; - const std::string& data = {std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; - this->cache.emplace_back(fp, data); - return data; - } - - std::ifstream file{fp}; - return {std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; - } - }; - inline limhamn::logger::logger logger{}; inline CacheManager cache_manager{}; - inline StaticExists static_exists{}; inline bool fatal{false}; - - class database { -#if FF_ENABLE_SQLITE - limhamn::database::sqlite3_database sqlite{}; -#define SQLITE_HANDLE this->sqlite -#endif -#if FF_ENABLE_POSTGRESQL - limhamn::database::postgresql_database postgres{}; -#define POSTGRES_HANDLE this->postgres -#endif -#ifndef SQLITE_HANDLE -#define SQLITE_HANDLE this->postgres -#endif -#ifndef POSTGRES_HANDLE -#define POSTGRES_HANDLE this->sqlite -#endif - - bool enabled_type = false; // false = sqlite, true = postgres - public: - explicit database(bool type) : enabled_type(type) {} - std::vector> query(const std::string& query) { - if (!this->enabled_type) { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "SQLite3 query: {{" + query + "}}\n"); -#endif - return SQLITE_HANDLE.query(query); - } else { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "PostgreSQL query: {{" + query + "}}\n"); -#endif - return POSTGRES_HANDLE.query(query); - } - } - bool exec(const std::string& query) { - if (!this->enabled_type) { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "SQLite3 exec: {{" + query + "}}\n"); -#endif - return SQLITE_HANDLE.exec(query); - } else { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "PostgreSQL exec: {{" + query + "}}\n"); -#endif - return POSTGRES_HANDLE.exec(query); - } - } - template - std::vector> query(const std::string& query, Args... args) { - if (!this->enabled_type) { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "SQLite3 query: {{" + query + "}}\n"); -#endif - return SQLITE_HANDLE.query(query, args...); - } else { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "PostgreSQL query: {{" + query + "}}\n"); -#endif - return POSTGRES_HANDLE.query(query, args...); - } - } - template - bool exec(const std::string& query, Args... args) { - if (!this->enabled_type) { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "SQLite3 exec: {{" + query + "}}\n"); -#endif - return SQLITE_HANDLE.exec(query, args...); - } else { -#if FF_DEBUG - logger.write_to_log(limhamn::logger::type::notice, "PostgreSQL exec: {{" + query + "}}\n"); -#endif - return POSTGRES_HANDLE.exec(query, args...); - } - } - [[nodiscard]] bool good() const { - return this->enabled_type ? POSTGRES_HANDLE.good() : SQLITE_HANDLE.good(); - } -#if FF_ENABLE_SQLITE - limhamn::database::sqlite3_database& get_sqlite() { - return this->sqlite; - } -#endif -#if FF_ENABLE_POSTGRESQL - limhamn::database::postgresql_database& get_postgres() { - return this->postgres; - } -#endif - }; - - struct WADInfo { - std::string title{}; - std::string title_id{}; - std::string full_title_id{}; - unsigned int ios{}; - std::string region{}; - int version{}; - int blocks{}; - bool supports_vwii{false}; - }; - - WADInfo get_info_from_wad(const std::string& wad_path); - void replace_dol_in_wad(const std::string& wad, const std::string& dol); - void set_ios_in_wad(const std::string& wad, int ios); - void set_title_id_in_wad(const std::string& wad, const std::string& title_id); - - inline static const std::string virtual_stylesheet_path = "/css/index.css"; - inline static const std::string virtual_font_path = "/fonts/font.ttf"; - inline static const std::string virtual_favicon_path = "/img/favicon.svg"; - inline static const std::string virtual_script_path = "/js/index.js"; + inline static const std::string virtual_stylesheet_path{"/css/index.css"}; + inline static const std::string virtual_font_path{"/fonts/font.ttf"}; + inline static const std::string virtual_favicon_path{"/img/favicon.svg"}; + inline static const std::string virtual_script_path{"/js/index.js"}; inline bool needs_setup{false}; void start_server(); @@ -388,34 +75,4 @@ namespace ff { void create_patched_dol(const std::string& path, const std::string& output_path); bool is_file(database& db, const std::string& file_key); - - limhamn::http::server::response handle_root_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_try_setup_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_setup_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_virtual_favicon_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_virtual_stylesheet_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_virtual_script_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_try_register_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_try_login_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_try_upload_forwarder_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_try_upload_file_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_delete_forwarder_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_delete_file_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_get_forwarders_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_get_files_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_set_approval_for_uploads_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_update_profile(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_get_profile(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_get_announcements(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_delete_announcement(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_edit_announcement(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_create_announcement(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_rate_forwarder_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_rate_file_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_comment_forwarder_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_comment_file_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_delete_comment_forwarder_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_delete_comment_file_endpoint(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_stay_logged_in(const limhamn::http::server::request& request, database& db); - limhamn::http::server::response handle_api_try_logout_endpoint(const limhamn::http::server::request& request, database& db); } // namespace ff diff --git a/include/file_construct_struct.hpp b/include/file_construct_struct.hpp new file mode 100755 index 0000000..4340530 --- /dev/null +++ b/include/file_construct_struct.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace ff { + struct FileConstruct { + std::string path{}; + std::string name{}; + std::string username{}; + std::string ip_address{}; + std::string user_agent{}; + }; +} // namespace ff diff --git a/include/login_status_enum.hpp b/include/login_status_enum.hpp new file mode 100755 index 0000000..f4330c5 --- /dev/null +++ b/include/login_status_enum.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace ff { + enum class LoginStatus { + Success, + Failure, + Inactive, + InvalidUsername, + InvalidPassword, + Banned, + }; +} // namespace ff diff --git a/include/profile_update_status_enum.hpp b/include/profile_update_status_enum.hpp new file mode 100755 index 0000000..529e536 --- /dev/null +++ b/include/profile_update_status_enum.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace ff { + enum class ProfileUpdateStatus { + Success, + Failure, + InvalidCreds, + InvalidJson, + InvalidIcon, + }; +} // namespace ff diff --git a/include/retrieved_file_struct.hpp b/include/retrieved_file_struct.hpp new file mode 100755 index 0000000..903d2bd --- /dev/null +++ b/include/retrieved_file_struct.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace ff { + struct RetrievedFile { + std::string path{}; + std::string name{}; + }; +} // namespace ff diff --git a/include/settings.hpp b/include/settings.hpp new file mode 100755 index 0000000..e7a7ba8 --- /dev/null +++ b/include/settings.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +namespace ff { + struct Settings { +#ifndef FF_DEBUG + std::string access_file{"/var/log/ff/access.log"}; + std::string warning_file{"/var/log/ff/warning.log"}; + std::string error_file{"/var/log/ff/error.log"}; + std::string notice_file{"/var/log/ff/notice.log"}; + bool output_to_std{false}; + bool halt_on_error{false}; + std::string sqlite_database_file{"/var/db/ff/ff.db"}; + std::string session_directory{"/var/lib/ff/sessions"}; + std::string data_directory{"/var/lib/ff/data"}; + std::string temp_directory{"/var/tmp/ff"}; + std::string html_file{"/etc/ff/html/index.html"}; + std::string css_file{"/etc/ff/css/ff.css"}; + std::string script_file{"/etc/ff/js/ff.js"}; + std::string favicon_file{"/etc/ff/img/favicon.svg"}; + bool public_registration{true}; + std::vector> custom_paths{ + {"/img/pointer.png", "/etc/ff/img/pointer.png"}, + {"/img/pointer-moving.png", "/etc/ff/img/pointer-moving.png"}, + {"/img/grab.png", "/etc/ff/img/grab.png"}, + {"/img/grab-moving.png", "/etc/ff/img/grab-moving.png"}, + {"/img/background-logo-1.png", "/etc/ff/img/background-logo-1.png"}, + {"/img/discord.svg", "/etc/ff/img/discord.svg"}, + {"/img/messages.svg", "/etc/ff/img/messages.svg"}, + {"/img/shovel.svg", "/etc/ff/img/shovel.svg"}, + {"/img/question-mark-block.svg", "/etc/ff/img/question-mark-block.svg"}, + {"/img/coin.svg", "/etc/ff/img/coin.svg"}, + {"/img/hammer.svg", "/etc/ff/img/hammer.svg"}, + {"/img/star.svg", "/etc/ff/img/star.svg"}, + {"/img/retro-star.svg", "/etc/ff/img/retro-star.svg"}, + {"/img/wave.svg", "/etc/ff/img/wave.svg"}, + {"/img/pen.svg", "/etc/ff/img/pen.svg"}, + {"/img/logo.svg", "/etc/ff/img/logo.svg"}, + {"/img/announcements.svg", "/etc/ff/img/announcements.svg"}, + {"/fonts/font.woff2", "/etc/ff/fonts/font.woff2"}, + {"/audio/click.wav", "/etc/ff/audio/click.wav"}, + {"/img/favicon.ico", "/etc/ff/img/favicon.ico"}, + }; + int64_t max_request_size{250 * 1024 * 1024}; // 250mb + std::string site_url{"https://forwarderfactory.com"}; + bool enable_email_verification{true}; +#else + std::string access_file{"./access.log"}; + std::string warning_file{"./warning.log"}; + std::string error_file{"./error.log"}; + std::string notice_file{"./notice.log"}; + std::string sqlite_database_file{"./ff-debug.db"}; + bool output_to_std{true}; + bool halt_on_error{false}; + std::string session_directory{"./sessions"}; + std::string data_directory{"./data"}; + std::string temp_directory{"./tmp"}; + std::string html_file{"./html/index.html"}; + std::string css_file{"./css/ff.css"}; + std::string script_file{"./js/ff.js"}; + std::string favicon_file{"./img/favicon.svg"}; + bool public_registration{true}; + std::vector> custom_paths{ + {"/img/pointer.png", "./img/pointer.png"}, + {"/img/pointer-moving.png", "./img/pointer-moving.png"}, + {"/img/grab.png", "./img/grab.png"}, + {"/img/grab-moving.png", "./img/grab-moving.png"}, + {"/img/background-logo-1.png", "./img/background-logo-1.png"}, + {"/img/discord.svg", "./img/discord.svg"}, + {"/img/messages.svg", "./img/messages.svg"}, + {"/img/shovel.svg", "./img/shovel.svg"}, + {"/img/question-mark-block.svg", "./img/question-mark-block.svg"}, + {"/img/hammer.svg", "./img/hammer.svg"}, + {"/img/coin.svg", "./img/coin.svg"}, + {"/img/star.svg", "./img/star.svg"}, + {"/img/wave.svg", "./img/wave.svg"}, + {"/img/retro-star.svg", "./img/retro-star.svg"}, + {"/img/pen.svg", "./img/pen.svg"}, + {"/img/logo.svg", "./img/logo.svg"}, + {"/img/announcements.svg", "./img/announcements.svg"}, + {"/fonts/font.woff2", "./fonts/font.woff2"}, + {"/audio/click.wav", "./audio/click.wav"}, + }; + int64_t max_request_size{1024 * 1024 * 1024}; + std::string site_url{"http://localhost:8080"}; + bool enable_email_verification{false}; +#endif + int port{8080}; + bool log_access_to_file{true}; + bool log_warning_to_file{true}; + bool log_error_to_file{true}; + bool log_notice_to_file{true}; + std::size_t password_min_length{8}; + std::size_t password_max_length{64}; + std::size_t username_min_length{3}; + std::size_t username_max_length{32}; + std::string allowed_characters{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}; + bool allow_all_characters{false}; + std::string session_cookie_name{"ff_session"}; + std::string title{"Forwarder Factory"}; + std::string description{"Forwarder Factory is a community dedicated to preserving and sharing Nintendo- and Wii-related content."}; + int default_user_type{0}; + bool preview_files{true}; + std::string email_username{}; + std::string email_password{}; + std::string email_from{}; + std::string smtp_server{}; + int smtp_port{465}; + std::string psql_username{"postgres"}; + std::string psql_password{"postgrespasswordhere"}; + std::string psql_database{"ff"}; + std::string psql_host{"localhost"}; + int psql_port{5432}; + bool enabled_database{false}; + bool trust_x_forwarded_for{false}; + int rate_limit{100}; + std::vector blacklisted_ips{}; + std::vector whitelisted_ips{"127.0.0.1"}; + int64_t max_file_size_hash{1024 * 1024 * 1024}; + bool cache_static{false}; + bool cache_exists{false}; + bool convert_images_to_webp{true}; + bool convert_videos_to_webm{false}; + bool topics_require_admin{false}; + }; + + inline Settings settings{}; +} // namespace ff diff --git a/include/static_exists.hpp b/include/static_exists.hpp new file mode 100755 index 0000000..26098d7 --- /dev/null +++ b/include/static_exists.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace ff { + class StaticExists { + std::vector> paths{}; + public: + explicit StaticExists() = default; + ~StaticExists() = default; + [[nodiscard]] bool is_file(const std::string& path, const bool cache = true) { + if (settings.cache_exists && cache) { + for (const auto& it : this->paths) if (it.first == path) return it.second; + paths.emplace_back(path, std::filesystem::exists(path)); + return paths.back().second; + } else { + return std::filesystem::exists(path); + } + } + + }; + + inline StaticExists static_exists{}; +} // namespace ff diff --git a/include/upload_status_enum.hpp b/include/upload_status_enum.hpp new file mode 100755 index 0000000..50bd041 --- /dev/null +++ b/include/upload_status_enum.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace ff { + enum class UploadStatus { + Success, + Failure, + InvalidCreds, + NoFile, + TooLarge, + }; +} // namespace ff diff --git a/include/user_properties_struct.hpp b/include/user_properties_struct.hpp new file mode 100755 index 0000000..d9f7ccd --- /dev/null +++ b/include/user_properties_struct.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace ff { + struct UserProperties { + std::string username{}; /* only filled in if cookie is valid */ + std::string ip_address{}; + std::string user_agent{}; + }; +} // namespace ff diff --git a/include/user_type_enum.hpp b/include/user_type_enum.hpp new file mode 100755 index 0000000..b6be20c --- /dev/null +++ b/include/user_type_enum.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace ff { + enum class UserType : int { + Undefined = -1, + User = 0, + Administrator = 1, + }; +} // namespace ff diff --git a/include/wad_info.hpp b/include/wad_info.hpp new file mode 100755 index 0000000..8f21bbc --- /dev/null +++ b/include/wad_info.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace ff { + struct WADInfo { + std::string title{}; + std::string title_id{}; + std::string full_title_id{}; + unsigned int ios{}; + std::string region{}; + int version{}; + int blocks{}; + bool supports_vwii{false}; + }; + + WADInfo get_info_from_wad(const std::string& wad_path); + void replace_dol_in_wad(const std::string& wad, const std::string& dol); + void set_ios_in_wad(const std::string& wad, int ios); + void set_title_id_in_wad(const std::string& wad, const std::string& title_id); +} // namespace ff diff --git a/js/ff.js b/js/ff.js index 3b41f76..2459804 100644 --- a/js/ff.js +++ b/js/ff.js @@ -74,7 +74,8 @@ function WSCBackgroundRepeatingSpawner(speed = 0.5, creation_interval = 8000) { document.body.style.overflow = 'hidden'; - const cachedImageSrc = '/img/background-logo-1.png'; + const cachedImageSrc = '/img/logo.svg'; + //const cachedImageSrc = '/img/background-logo-1.png'; function createImage(initialX, initialY) { const img = document.createElement('img'); @@ -87,7 +88,7 @@ function WSCBackgroundRepeatingSpawner(speed = 0.5, creation_interval = 8000) { img.style.userSelect = 'none'; img.draggable = false; img.style.filter = `hue-rotate(${Math.random() * 360}deg)`; - img.style.width = '150px'; + img.style.width = '75px'; img.style.height = 'auto'; container.appendChild(img); @@ -174,6 +175,7 @@ function hide_all_windows() { while (windows[i].firstChild) { windows[i].removeChild(windows[i].firstChild); } + windows[i].remove(); } const grids = document.getElementsByClassName('grid'); @@ -184,21 +186,21 @@ function hide_all_windows() { // hide #browse-search and #browse-filter-button if they exist const search = document.getElementById('sandbox-search'); if (search) { - search.style.display = 'none'; + search.remove(); } const filter = document.getElementById('sandbox-filter-button'); if (filter) { - filter.style.display = 'none'; + filter.remove(); } // hide #browse-search and #browse-filter-button if they exist const browse_search = document.getElementById('browse-search'); if (browse_search) { - browse_search.style.display = 'none'; + browse_search.remove(); } const browse_filter = document.getElementById('browse-filter-button'); if (browse_filter) { - browse_filter.style.display = 'none'; + browse_filter.remove(); } // show title if hidden @@ -260,18 +262,16 @@ function hide_initial() { class WindowProperties { constructor({ - back_button = null, + classes = [], close_button = true, moveable = false, - close_on_click_outside = false, close_on_escape = true, remove_existing = true, function_on_close = null } = {}) { - this.back_button = back_button; + this.classes = classes; this.close_button = close_button; this.moveable = moveable; - this.close_on_click_outside = close_on_click_outside; this.close_on_escape = close_on_escape; this.remove_existing = remove_existing; this.function_on_close = function_on_close; @@ -282,10 +282,10 @@ function create_window(id, prop = new WindowProperties()){ if (prop.remove_existing) { const windows = document.getElementsByClassName('floating_window'); for (let i = 0; i < windows.length; i++) { - windows[i].style.display = 'none'; while (windows[i].firstChild) { windows[i].removeChild(windows[i].firstChild); } + windows[i].remove(); } } // remove existing with same id always @@ -298,7 +298,15 @@ function create_window(id, prop = new WindowProperties()){ } const window = document.createElement('div'); window.className = 'floating_window'; + + if (prop.classes && prop.classes.length > 0) { + for (const cls of prop.classes) { + window.classList.add(cls); + } + } + window.id = id; + /* if (prop.close_on_click_outside) { window.onclick = (event) => { if (event.target === window) { @@ -306,10 +314,17 @@ function create_window(id, prop = new WindowProperties()){ prop.function_on_close(); return; } - hide_all_windows(); + //hide_all_windows(); + window.remove(); + const windows = document.getElementsByClassName('floating_window'); + if (windows.length === 0) { + hide_all_windows(); + } } } } + */ + if (prop.close_on_escape) { document.onkeydown = (event) => { if (event.key === 'Escape') { @@ -317,7 +332,12 @@ function create_window(id, prop = new WindowProperties()){ prop.function_on_close(); return; } - hide_all_windows(); + + window.remove(); + const windows = document.getElementsByClassName('floating_window'); + if (windows.length === 0) { + hide_all_windows(); + } } } } @@ -365,29 +385,14 @@ function create_window(id, prop = new WindowProperties()){ prop.function_on_close(); return; } - hide_all_windows(); - } - - window.appendChild(close); - } - if (prop.back_button) { - const back = document.createElement('a'); - back.innerHTML = '←'; - back.id = 'window-back'; - back.style.position = 'fixed'; - back.style.padding = '10px'; - back.style.top = '0'; - back.style.left = '0'; - back.style.textDecoration = 'none'; - back.style.color = 'black'; - back.onclick = () => { - play_click(); - if (prop.function_on_close) { - prop.function_on_close(); + window.remove(); + const windows = document.getElementsByClassName('floating_window'); + if (windows.length === 0) { + hide_all_windows(); } } - window.appendChild(back); + window.appendChild(close); } document.body.appendChild(window); @@ -397,28 +402,31 @@ function create_window(id, prop = new WindowProperties()){ function show_terms() { play_click(); - const terms = create_window('terms-window'); + const terms = create_window('tos-window'); const title = document.createElement('h1'); title.innerHTML = 'Terms of Service'; const paragraph = document.createElement('p'); - let terms_str = "By contributing, logging in and/or registering to this website (hereinafter referred to as 'Forwarder Factory'), you agree to the following terms of service:"; - terms_str += "
1. Forwarder Factory, its contributors and its members shall not be held responsible for any damages caused by the use of files, software, information, or any other content found on this website."; - terms_str += "
2. Forwarder Factory reserves the right to remove any content at any time for any reason, including but not limited to, a DMCA takedown request."; - terms_str += "
3. Forwarder Factory reserves the right to ban users who upload harmful, malicious or otherwise dangerous content."; - terms_str += "
4. Forwarder Factory reserves the right to change these terms at any time without notice."; - terms_str += "
5. Forwarder Factory cannot guarantee it will keep your data safe, and it shall not be held responsible for any data breaches. We therefore highly urge our users not to use the same password on Forwarder Factory as they do on other websites. Data breaches are not expected, but they are possible, and we want to be transparent about it."; - terms_str += "
6. Any data you submit to Forwarder Factory may be stored indefinitely, until a request for deletion is received. Forwarder Factory will not sell your data to third parties."; - terms_str += " In the event of a data breach, we will attempt to notify all affected users as soon as possible, within reason."; - terms_str += "
European Union: By using this website, you agree to the use of cookies. We only use cookies for session management, not for tracking or advertising purposes. You have the right to request that your data be deleted at any time, and we will comply with any such requests. Send requests in the form of an email to contact@forwarderfactory.com."; - terms_str += "
Note: Forwarder Factory is not affiliated with Nintendo. All trademarks are property of their respective owners."; - terms_str += "
Forwarder Factory is hosted in Sweden, and is therefore subject to Swedish law. Uploads that do not comply with Swedish law will be removed on sight, as we are legally required to do so."; - - paragraph.innerHTML = terms_str; + paragraph.innerHTML = 'Loading terms...'; terms.appendChild(title); terms.appendChild(paragraph); + + fetch('https://raw.githubusercontent.com/ForwarderFactory/documents/refs/heads/master/terms-of-service.txt') + .then(response => { + if (!response.ok) { + throw new Error('network response was not ok.'); + } + return response.text(); + }) + .then(text => { + paragraph.innerHTML = text.replace(/\n/g, '
'); + }) + .catch(error => { + paragraph.innerHTML = 'failed to load terms of service.'; + console.error('error fetching terms:', error); + }); } function show_login(_error = "") { @@ -4688,11 +4696,10 @@ function show_browse(uploader = '') { icon.className = 'preview-icon scaling-in hidden'; // hidden initially icon.style.cursor = 'pointer'; - // Create spinner element const spinner = document.createElement('div'); spinner.className = 'loading-spinner'; - iconWrapper.innerHTML = ''; // Clear previous content + iconWrapper.innerHTML = ''; iconWrapper.appendChild(icon); iconWrapper.appendChild(spinner); @@ -5190,26 +5197,1164 @@ function show_admin() { unfinished.className = 'admin-unfinished'; admin.appendChild(unfinished); } -const generate_stars = (n, w) => { + +function generate_stars(n, w) { + const size = 4; + const win_box = w.getBoundingClientRect(); + for (let i = 0; i < n; i++) { const star = document.createElement('div'); star.className = 'star'; star.style.position = 'absolute'; - star.style.width = '4px'; - star.style.height = '4px'; + star.style.width = `${size}px`; + star.style.height = `${size}px`; star.style.backgroundColor = 'white'; star.style.borderRadius = '50%'; - star.style.top = `${Math.random() * 100}%`; - star.style.left = `${Math.random() * 100}%`; + + const max_top = w.clientHeight - size; + const max_left = w.clientWidth - size; + + star.style.top = `${Math.random() * max_top}px`; + star.style.left = `${Math.random() * max_left}px`; + star.style.setProperty('--random-x', Math.random() - 0.5); star.style.setProperty('--random-y', Math.random() - 0.5); + star.style.animationDuration = `${Math.random() * 10 + 10}s`; w.appendChild(star); } } +function generate_sprites(container, url, img_properties = {}, size = 24, distance = 48) { + const wrapper = document.createElement('div'); + const random_string = Math.random().toString(36).substring(2, 15); + wrapper.id = 'tiled-wrapper-' + random_string; + + Object.assign(wrapper.style, { + position: 'absolute', + top: `-${distance}px`, + left: `-${distance}px`, + width: `calc(100% + ${distance * 2}px)`, + height: `calc(100% + ${distance * 2}px)`, + pointerEvents: 'none', + overflow: 'hidden', + willChange: "transform", + zIndex: '-1' + }); + + const animate = (el) => { + let start = null; + + const step = (timestamp) => { + if (!start) start = timestamp; + + const progress = (timestamp - start) % 4000; + const fraction = progress / 4000; + + const x = -distance * fraction; + const y = distance * fraction; + + el.style.transform = `translate(${x}px, ${y}px)`; + requestAnimationFrame(step); + } + + requestAnimationFrame(step); + } + + animate(wrapper); + + const cols = Math.ceil((container.offsetWidth + distance * 2) / distance); + const rows = Math.ceil((container.offsetHeight + distance * 2) / distance); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + if ((x + y) % 2 === 0) { + const sprite = document.createElement('div'); + sprite.className = 'sprite'; + sprite.style.left = `${x * distance}px`; + sprite.style.top = `${y * distance}px`; + sprite.style.position = "absolute"; + sprite.style.width = `${size}px`; + sprite.style.height = `${size}px`; + sprite.style.backgroundImage = `url(${url})`; + sprite.style.backgroundSize = "cover"; + + const filters = []; + + if (img_properties.invert) filters.push('invert(1)'); + if (img_properties.random_colors) filters.push(`hue-rotate(${Math.random() * 360}deg)`); + if (img_properties.hue !== undefined) filters.push(`hue-rotate(${img_properties.hue}deg)`); + if (img_properties.monochrome) filters.push('grayscale(1)'); + + if (filters.length > 0) sprite.style.filter = filters.join(' '); + + sprite.style.opacity = img_properties.opacity !== undefined ? img_properties.opacity : 0.25; + + wrapper.appendChild(sprite); + } + } + } + + container.prepend(wrapper); +} + + +function get_posts(topic_id, start_index = 0, end_index = -1) { + if (topic_id == null || topic_id === '') { + return Promise.resolve([]); + } + + const url = '/api/get_posts'; + const requestBody = JSON.stringify({ topic_id: topic_id, start_index: start_index, end_index: end_index }); + return fetch(url, { + method: 'POST', + body: requestBody, + headers: { + 'Content-Type': 'application/json' + } + }) + .then (response => response.text()) + .then (text => { + const json = JSON.parse(text); + if (json.error) { + console.error(json.error); + return Promise.resolve([]); + } + + return json.posts; + }) + .catch(error => { + console.error('Error fetching posts:', error); + return Promise.resolve([]); + }); +} + +function get_topics(start_index = 0, end_index = -1) { + // fetch /api/get_topics + const url = '/api/get_topics'; + const requestBody = JSON.stringify({ start_index: start_index, end_index: end_index }); + return fetch(url, { + method: 'POST', + body: requestBody, + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.text()) + .then(text => { + const json = JSON.parse(text); + if (json.error) { + console.error(json.error); + return []; + } + + return json.topics; + }) + .catch(error => { + console.error('Error fetching topics:', error); + return []; + }); +} + +function show_post(post_id, topic_id = '') { + hide_all_windows(); + set_path('/post'); + + if (post_id === null || post_id === '') { + console.error('post_id is null'); + return; + } + + // get the post from the server + const url = '/api/get_posts'; + const requestBody = JSON.stringify({ post_id: post_id, topic_id: topic_id }); + fetch(url, { + method: 'POST', + body: requestBody, + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.text()) + .then(text => { + const json = JSON.parse(text); + if (json.error) { + console.error(json.error); + return; + } + + const posts = json.posts; + let post; + posts.forEach(p => { + if (p.identifier === post_id) { + post = p; + } + }); + + if (post === undefined) { + console.error(`Post with ID ${post_id} not found`); + return; + } + + set_path('/post/' + post_id); + + const post_window = create_window('post-window-' + (topic_id || "root") + ("-" + post_id || "root"), { classes: ["forum_window"], close_button: true, back_button: null, close_on_escape: true, function_on_close: () => { + hide_all_windows(); + show_topic(topic_id || post.topic_id); + } + }); + + const title = document.createElement('h1'); + title.innerHTML = post.title || 'No title'; + title.className = 'post-title'; + + const author = document.createElement('p'); + author.innerHTML = `Posted by ${post.created_by} on ${new Date(post.created_at).toLocaleDateString()}`; + author.className = 'post-author'; + + const content = document.createElement('div'); + content.innerHTML = post.text || 'No content'; + content.className = 'post-content'; + + post_window.appendChild(title); + post_window.appendChild(author); + post_window.appendChild(content); + + // in a grid, show files. they're in data/x/download_key. We can display data/x/filename + if (post.data && post.data.length > 0) { + const files_grid = document.createElement('div'); + files_grid.className = 'post-files-grid'; + + post.data.forEach(file => { + const file_div = document.createElement('div'); + file_div.className = 'post-file'; + file_div.id = file.download_key; + + const file_name = document.createElement('p'); + file_name.innerHTML = file.filename || 'No filename'; + file_name.className = 'post-file-name'; + file_name.onclick = () => { + play_click(); + window.location.href = `/download/${file.download_key}`; + } + file_name.style.color = 'blue'; + + file_div.appendChild(file_name); + files_grid.appendChild(file_div); + }); + + post_window.appendChild(files_grid); + } + + const reply_h2 = document.createElement('h2'); + reply_h2.innerHTML = 'Reply to this post'; + reply_h2.className = 'post-reply-button'; + + const reply_textarea = document.createElement('textarea'); + reply_textarea.className = 'post-reply-textarea'; + reply_textarea.placeholder = 'Write your reply here...'; + reply_textarea.rows = 5; + reply_textarea.cols = 50; + + const file_uploads = document.createElement('input'); + file_uploads.type = 'file'; + file_uploads.multiple = true; + file_uploads.className = 'post-file-upload'; + file_uploads.accept = '*/*'; + file_uploads.style.marginRight = '10px'; + + const reply_button = document.createElement('button'); + reply_button.innerHTML = 'Reply'; + reply_button.className = 'post-reply-button'; + reply_button.onclick = () => { + play_click(); + + // 1. must be sent to /api/comment_post + // 2. must be as a multipart, with the json being name="json" + // 3. the json must contain the post_id, topic_id, text + const formData = new FormData(); + formData.append('json', JSON.stringify({ + "post_id": post_id, + "topic_id": topic_id, + "comment": reply_textarea.value, + })); + + if (file_uploads.files.length > 0) { + for (let i = 0; i < file_uploads.files.length; i++) { + formData.append('file-' + i, file_uploads.files[i]); + } + } + + fetch('/api/comment_post', { + method: 'POST', + body: formData, + headers: { + 'Accept': 'application/json' + } + }) + .then(response => response.text()) + .then(text => { + show_post(post_id, topic_id); + }); + } + + // must not be closed, or admin, to reply + if (get_cookie('user_type') === "1" || post.open) { + post_window.appendChild(reply_h2); + post_window.appendChild(reply_textarea); + post_window.appendChild(document.createElement('br')); + post_window.appendChild(file_uploads); + post_window.appendChild(reply_button); + } + + const replies_h2 = document.createElement('h2'); + replies_h2.innerHTML = 'Replies'; + replies_h2.className = 'post-replies-title'; + + post_window.appendChild(replies_h2); + + // now print all comments for this post + // they're in the comments array + if (post.comments && post.comments.length > 0) { + const search_input = document.createElement('input'); + + search_input.type = 'text'; + search_input.id = 'comment_search_input'; + search_input.placeholder = 'Search comments...'; + search_input.className = 'post-comment-search'; + search_input.style.margin = '5px'; + + post_window.appendChild(search_input); + + search_input.addEventListener('input', (event) => { + const raw_query = event.target.value.trim().toLowerCase(); + search_query = raw_query; + + if (!raw_query) { + filtered_comments = []; + load_comments(1); + return; + } + + const filters = { + author: null, + content: null, + file: null, + date: null, + any: [] + }; + + raw_query.split(/\s+/).forEach(term => { + if (term.startsWith('author:')) { + filters.author = term.slice(7); + } else if (term.startsWith('content:')) { + filters.content = term.slice(8); + } else if (term.startsWith('file:')) { + filters.file = term.slice(5); + } else if (term.startsWith('date:')) { + filters.date = term.slice(5); + } else { + filters.any.push(term); + } + }); + + filtered_comments = post.comments.filter(comment => { + const comment_text = (comment.comment || '').toLowerCase(); + const author = (comment.created_by || '').toLowerCase(); + const date_str = new Date(comment.created_at).toLocaleDateString().toLowerCase(); + const file_names = (comment.data || []).map(f => f.filename?.toLowerCase() || '').join(' '); + + const match_author = !filters.author || author.includes(filters.author); + const match_content = !filters.content || comment_text.includes(filters.content); + const match_file = !filters.file || file_names.includes(filters.file); + const match_date = !filters.date || date_str.includes(filters.date); + + const match_any = filters.any.length === 0 || filters.any.some(term => + comment_text.includes(term) || + author.includes(term) || + date_str.includes(term) || + file_names.includes(term) + ); + + return match_author && match_content && match_file && match_date && match_any; + }); + + load_comments(1); + }); + + // print them all out + // we have comment, created_by and created_at, as well as data/x/... + const comments_div = document.createElement('div'); + comments_div.className = 'post-comments-list'; + comments_div.id = 'post-comments-list'; + const comments_per_page = 10; + let current_page = 1; + + function clear_comments() { + while (comments_div.firstChild) { + comments_div.removeChild(comments_div.firstChild); + } + } + + let filtered_comments = []; + let search_query = ''; + + async function load_comments(page) { + clear_comments(); + + const comments_source = search_query ? filtered_comments : post.comments; + const start_index = (page - 1) * comments_per_page; + const comments_to_load = comments_source.slice(start_index, start_index + comments_per_page); + + for (const [index, comment] of comments_to_load.entries()) { + const comment_div = document.createElement('div'); + comment_div.className = 'post-comment'; + comment_div.style.textAlign = "left"; + + const comment_header = document.createElement('div'); + comment_header.style.display = 'flex'; + comment_header.style.alignItems = 'center'; + comment_header.style.gap = '5px'; + comment_header.className = 'post-comment-header'; + + const profile = await get_profile_for_user(comment.created_by); + if (!profile || !profile.profile_key) { + const icon = document.createElement('i'); + icon.className = "fa-solid fa-circle-user"; + icon.style.marginRight = '5px'; + comment_header.appendChild(icon); + } else { + const profile_img = document.createElement('img'); + profile_img.src = `/download/${profile.profile_key}`; + profile_img.className = 'post-comment-profile'; + profile_img.style.marginRight = '5px'; + profile_img.style.maxWidth = '15px'; + profile_img.style.maxHeight = '15px'; + profile_img.style.borderRadius = '50%'; + comment_header.appendChild(profile_img); + } + + const comment_author = document.createElement('p'); + comment_author.style.margin = '0'; + comment_author.style.fontWeight = 'bold'; + comment_author.onclick = () => { + view_profile(comment.created_by); + }; + + const formatted_date = new Date(comment.created_at).toLocaleDateString(); + comment_author.innerHTML = `${profile?.display_name || comment.created_by} on ${formatted_date}`; + comment_header.appendChild(comment_author); + + const comment_content = document.createElement('div'); + comment_content.innerHTML = comment.comment || 'No content'; + comment_content.className = 'post-comment-content'; + + comment_div.appendChild(comment_header); + comment_div.appendChild(comment_content); + + if (comment.data && comment.data.length > 0) { + const files_grid = document.createElement('div'); + files_grid.className = 'post-comment-files-grid'; + + comment.data.forEach(file => { + const file_div = document.createElement('div'); + file_div.className = 'post-comment-file'; + file_div.id = file.download_key; + + const file_name = document.createElement('p'); + file_name.innerHTML = file.filename || 'No filename'; + file_name.className = 'post-comment-file-name'; + file_name.onclick = () => { + play_click(); + document.location.href = `/download/${file.download_key}`; + } + + file_div.appendChild(file_name); + files_grid.appendChild(file_div); + }); + + comment_div.appendChild(document.createElement('br')); + comment_div.appendChild(files_grid); + } + + // add delete button + if ((post.open && post.created_by === get_cookie('username')) || get_cookie('user_type') === "1") { + const delete_button = document.createElement('button'); + delete_button.innerHTML = 'Delete'; + delete_button.className = 'post-comment-delete-button'; + delete_button.onclick = () => { + play_click(); + fetch('/api/delete_comment_post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ post_id: post_id, comment_id: index }) + }).then(response => { + if (response.status === 204) { + show_post(post_id, topic_id); + } else { + console.error('Failed to delete comment'); + } + }); + }; + comment_div.appendChild(document.createElement('br')); + comment_div.appendChild(delete_button); + } + + comments_div.appendChild(comment_div); + } + + current_page = page; + render_pagination_controls(); + } + + function render_pagination_controls() { + const old_pagination = document.getElementById('pagination_controls'); + if (old_pagination) old_pagination.remove(); + + const pagination_div = document.createElement('div'); + pagination_div.id = 'pagination_controls'; + pagination_div.style.marginTop = '10px'; + + const comments_source = search_query ? filtered_comments : post.comments; + const total_pages = Math.ceil(comments_source.length / comments_per_page); + + const prev_btn = document.createElement('button'); + prev_btn.innerText = 'Prev'; + prev_btn.disabled = (current_page === 1); + prev_btn.onclick = () => { + play_click(); + load_comments(current_page - 1); + } + pagination_div.appendChild(prev_btn); + + for (let i = 1; i <= total_pages; i++) { + const page_btn = document.createElement('button'); + page_btn.innerText = i; + page_btn.style.margin = '0 5px'; + page_btn.disabled = (i === current_page); + page_btn.onclick = () => { + play_click(); + load_comments(i); + } + pagination_div.appendChild(page_btn); + } + + const next_btn = document.createElement('button'); + next_btn.innerText = 'Next'; + next_btn.disabled = (current_page === total_pages); + next_btn.onclick = () => { + play_click(); + load_comments(current_page + 1); + } + pagination_div.appendChild(next_btn); + + comments_div.after(pagination_div); + } + + + load_comments(1); + + post_window.appendChild(comments_div); + } else { + const no_comments = document.createElement('p'); + no_comments.innerHTML = 'No replies yet. Be the first to reply!'; + no_comments.className = 'post-no-comments'; + + post_window.appendChild(no_comments); + } + + if (post.open && (post.created_by === get_cookie('username') || get_cookie('user_type') === "1")) { + const close_button = document.createElement('button'); + close_button.innerHTML = 'Close Post'; + close_button.className = 'post-close-button'; + close_button.onclick = () => { + play_click(); + fetch('/api/close_post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ post_id: post_id, open: false }) + }).then(response => { + if (response.httpRequestStatusCode !== 204) { + const json = JSON.parse(text); + if (json.error) { + console.error(json.error); + } else { + show_post(post_id, topic_id); + } + } + show_post(post_id, topic_id); + }); + } + + post_window.appendChild(document.createElement('br')); + post_window.appendChild(close_button); + } else if (!post.open && (post.created_by === get_cookie('username') || get_cookie('user_type') === "1")) { + const reopen_button = document.createElement('button'); + reopen_button.innerHTML = 'Reopen Post'; + reopen_button.className = 'post-reopen-button'; + reopen_button.onclick = () => { + play_click(); + fetch('/api/close_post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ post_id: post_id, open: true }) + }).then(response => { + if (response.httpRequestStatusCode !== 204) { + const json = JSON.parse(text); + if (json.error) { + console.error(json.error); + } else { + show_post(post_id, topic_id); + } + } + show_post(post_id, topic_id); + }); + } + + post_window.appendChild(document.createElement('br')); + post_window.appendChild(reopen_button); + } + + // delete post + if (get_cookie('user_type') === "1" || (post.open && post.created_by === get_cookie('username'))) { + const delete_button = document.createElement('button'); + delete_button.innerHTML = 'Delete Post'; + delete_button.className = 'post-delete-button'; + delete_button.style.marginLeft = "10px"; + delete_button.onclick = () => { + play_click(); + fetch('/api/delete_post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ post_id: post_id }) + }).then(response => { + if (response.status === 204) { + show_topic(topic_id); + } else { + console.error('Failed to delete post'); + } + }); + } + + post_window.appendChild(delete_button); + } + }) +} + +function show_topic(topic_id = '', parent_topic_id = '') { + hide_all_windows(); + set_path('/topic'); + + if (topic_id !== '') { + set_path('/topic/' + topic_id); + } + + const forum = create_window('forum-window-' + (topic_id || 'root') + '-' + (parent_topic_id || 'root'), { close_button: true, classes: ["forum_window"], close_on_escape: true, function_on_close: () => { + hide_all_windows(); + if (parent_topic_id !== '') { + show_topic(parent_topic_id); + } else if (topic_id !== '') { + // dig and get the topic this topic is in + get_topics().then(topics => { + let parent_topic = ''; + topics.forEach(t => { + if (t.topics && t.topics.includes(topic_id)) { + parent_topic = t.identifier; + } + }); + if (parent_topic !== '') { + show_topic(parent_topic); + } else { + show_topic(); + } + }); + } else { + hide_all_windows(); + } + }}); + + const topics_list = document.createElement('div'); + topics_list.className = 'forum-topics-list'; + topics_list.id = 'forum-topics-list'; + + let current_topic; + const topics = get_topics(); + + // iterate over topics and create elements + topics.then(topics => { + topics.forEach(async topic => { + let is_ours = false; + let current_is_subtopic = false; + + if (topic.identifier == topic_id && topic_id !== null && topic_id !== '') { + current_topic = topic; + } + + // check if this topic is part of any other topics' topics list + topics.forEach(t => { + if (t.topics && t.topics.includes(topic.identifier)) { + current_is_subtopic = true; + } + if (t.identifier === topic_id && topic_id !== '' && t.topics && t.topics.includes(topic.identifier)) { + is_ours = true; + } + }); + + if (current_is_subtopic && topic_id === '') { + return; + } + + if (topic.identifier === topic_id && topic_id !== '') { + return; + } + + if (!current_is_subtopic && topic_id !== '') { + return; + } + + if (topic_id === '' && current_is_subtopic) { + return; + } + + if (!is_ours && topic_id !== '') { + return; + } + + const topic_div = document.createElement('div'); + topic_div.className = 'forum-topic'; + topic_div.id = topic.identifier; + topic_div.style.padding = "10px"; + topic_div.style.textAlign = 'left'; + + const title = document.createElement('strong'); + title.innerHTML = topic.title; + title.className = 'forum-topic-title'; + + const description = document.createElement('p'); + description.innerHTML = topic.description; + description.className = 'forum-topic-description'; + if (description.innerHTML.length > 100) { + description.innerHTML = description.innerHTML.substring(0, 100) + '...'; + } + + const topic_header = document.createElement('div'); + topic_header.style.display = 'flex'; + topic_header.style.alignItems = 'center'; + topic_header.style.gap = '5px'; + topic_header.className = 'post-topic-header'; + + const profile = await get_profile_for_user(topic.created_by); + if (!profile || !profile.profile_key) { + const icon = document.createElement('i'); + icon.className = "fa-solid fa-circle-user"; + icon.style.marginRight = '5px'; + topic_header.appendChild(icon); + } else { + const profile_img = document.createElement('img'); + profile_img.src = `/download/${profile.profile_key}`; + profile_img.className = 'post-comment-profile'; + profile_img.style.marginRight = '5px'; + profile_img.style.maxWidth = '15px'; + profile_img.style.maxHeight = '15px'; + profile_img.style.borderRadius = '50%'; + topic_header.appendChild(profile_img); + } + + const topic_author = document.createElement('p'); + topic_author.style.margin = '0'; + topic_author.style.fontWeight = 'bold'; + topic_author.onclick = () => { + view_profile(topic.created_by); + }; + + const formatted_date = new Date(topic.created_at).toLocaleDateString(); + topic_author.innerHTML = `${profile?.display_name || topic.created_by} on ${formatted_date}`; + topic_header.appendChild(topic_author); + + topic_div.appendChild(title); + topic_div.appendChild(topic_header); + topic_div.appendChild(description); + + topic_div.onclick = () => { + play_click(); + show_topic(topic.identifier, topic_id); + }; + + // add delete button + if (get_cookie('user_type') === "1" || (topic.open && topic.created_by === get_cookie('username'))) { + const delete_button = document.createElement('button'); + delete_button.innerHTML = 'Delete Topic'; + delete_button.className = 'forum-topic-delete-button'; + delete_button.onclick = () => { + play_click(); + fetch('/api/delete_topic', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ topic_id: topic.identifier }) + }).then(response => { + if (response.status === 204) { + show_topic(parent_topic_id); + } else { + console.error('Failed to delete topic'); + } + }); + } + + topic_div.appendChild(delete_button); + } + // add close/reopen button + if (get_cookie('user_type') === "1" || (topic.open && topic.created_by === get_cookie('username'))) { + const close_button = document.createElement('button'); + close_button.innerHTML = topic.open ? 'Close Topic' : 'Reopen Topic'; + close_button.className = 'forum-topic-close-button'; + close_button.onclick = () => { + play_click(); + fetch('/api/close_topic', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ topic_id: topic.identifier, open: !topic.open }) + }).then(response => { + if (response.status === 204) { + show_topic(topic_id); + } else { + console.error('Failed to close/reopen topic'); + } + }); + } + + topic_div.appendChild(close_button); + } + + topics_list.appendChild(topic_div); + }); + }); + + const posts_div = document.createElement('div'); + posts_div.className = 'forum-posts-list'; + posts_div.id = 'forum-posts-list'; + + const posts = get_posts(topic_id); + posts.then(posts => { + posts.forEach(async post => { + if (topic_id === '') { + return; + } + + if (post.topic_id !== topic_id) { + return; + } + + const post_div = document.createElement('div'); + post_div.className = 'forum-post'; + post_div.id = post.identifier; + post_div.style.textAlign = 'left'; + post_div.style.padding = "10px"; + post_div.onclick = () => { + play_click(); + + show_post(post.identifier, topic_id); + } + + const post_span = document.createElement('span'); + post_span.className = 'forum-post'; + post_span.id = post.identifier + "_div"; + + const title = document.createElement('p'); + title.innerHTML = post.title || 'No title'; + title.className = 'forum-post-title'; + + const post_header = document.createElement('div'); + post_header.style.display = 'flex'; + post_header.style.alignItems = 'center'; + post_header.style.gap = '5px'; + post_header.className = 'post-post-header'; + + const profile = await get_profile_for_user(post.created_by); + if (!profile || !profile.profile_key) { + const icon = document.createElement('i'); + icon.className = "fa-solid fa-circle-user"; + icon.style.marginRight = '5px'; + post_header.appendChild(icon); + } else { + const profile_img = document.createElement('img'); + profile_img.src = `/download/${profile.profile_key}`; + profile_img.className = 'post-comment-profile'; + profile_img.style.marginRight = '5px'; + profile_img.style.maxWidth = '15px'; + profile_img.style.maxHeight = '15px'; + profile_img.style.borderRadius = '50%'; + post_header.appendChild(profile_img); + } + + const post_author = document.createElement('p'); + post_author.style.margin = '0'; + post_author.style.fontWeight = 'bold'; + post_author.onclick = () => { + view_profile(post.created_by); + }; + + const formatted_date = new Date(post.created_at).toLocaleDateString(); + post_author.innerHTML = `${profile?.display_name || post.created_by} on ${formatted_date}`; + post_header.appendChild(post_author); + + const content = document.createElement('p'); + content.className = 'forum-post-content'; + + if (post.text) { + content.innerHTML = post.text.substring(0, 50).replace(/\n/g, ' '); + if (post.text.length > 50) { + content.innerHTML += '...'; + } + } + + post_span.appendChild(title); + post_span.appendChild(post_header); + post_span.appendChild(content); + + post_div.appendChild(post_span); + posts_div.appendChild(post_div); + }); + }); + + const topics_title = document.createElement('h2'); + topics_title.innerHTML = 'Topics'; + topics_title.className = 'forum-topics-title'; + + forum.appendChild(topics_title); + + if (is_logged_in() && get_cookie('user_type') === '1') { + const create_topic_button = document.createElement('button'); + create_topic_button.innerHTML = 'Create Topic'; + create_topic_button.style.marginRight = '10px'; + create_topic_button.className = 'forum-create-topic-button'; + create_topic_button.onclick = () => { + play_click(); + + const window = create_window('create-topic-window', {classes: ["forum_window"], back_button: null, close_button: true, close_on_click_outside: true, close_on_escape: true}); + + const title = document.createElement('h2'); + title.innerHTML = 'Create Topic'; + title.className = 'forum-create-topic-title'; + + const paragraph = document.createElement('p'); + paragraph.innerHTML = 'Enter a title and description for your fancy topic. These will (obviously) be shown to users.'; + paragraph.className = 'forum-create-topic-paragraph'; + + const title_input = document.createElement('input'); + title_input.type = 'text'; + title_input.name = 'title'; + title_input.placeholder = 'Title'; + + const description_input = document.createElement('textarea'); + description_input.name = 'description'; + description_input.placeholder = 'Description'; + + const closed = document.createElement('input'); + closed.type = 'checkbox'; + closed.name = 'closed'; + closed.id = 'forum-create-topic-closed'; + + const closed_label = document.createElement('label'); + closed_label.for = 'closed'; + closed_label.innerHTML = 'Closed'; + closed_label.className = 'forum-create-topic-closed-label'; + + const button = document.createElement('button'); + button.innerHTML = 'Create Topic'; + button.className = 'forum-create-topic-submit-button'; + button.onclick = () => { + if (title_input.value && description_input.value) { + play_click(); + + const json = { + title: title_input.value, + description: description_input.value, + }; + + if (topic_id !== '' && topic_id !== null) { + json.parent_topics = [topic_id]; + } + if (closed && closed.checked) { + json.closed = true; + } + + fetch('/api/create_topic', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(json) + }) + .then(response => { + hide_all_windows(); + + if (response.status === 204 || response.status === 200) { + show_topic(parent_topic_id); + } else { + console.log("failed to create topic"); + } + }); + } + } + + window.appendChild(title); + window.appendChild(paragraph); + window.appendChild(title_input); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(description_input); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(closed); + window.appendChild(closed_label); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(button); + }; + forum.appendChild(create_topic_button); + } + + const posts_title = document.createElement('h2'); + posts_title.innerHTML = 'Posts'; + posts_title.className = 'forum-posts-title'; + + forum.appendChild(topics_list); + forum.appendChild(posts_title); + + if (is_logged_in() && topic_id !== '' && topic_id !== null && (get_cookie("user_type") === '1' || current_topic.open)) { + const create_post_button = document.createElement('button'); + create_post_button.innerHTML = 'Create Post'; + create_post_button.className = 'forum-create-post-button'; + create_post_button.onclick = () => { + play_click(); + + const window = create_window('create-post-window', {classes: ["forum_window"], back_button: null, close_button: true, close_on_click_outside: true, close_on_escape: true}); + + const title = document.createElement('h2'); + title.innerHTML = 'Create Post'; + title.className = 'forum-create-post-title'; + + const paragraph = document.createElement('p'); + paragraph.innerHTML = 'Enter a title and content for your post. You can also upload files.'; + paragraph.className = 'forum-create-post-paragraph'; + + const title_input = document.createElement('input'); + title_input.type = 'text'; + title_input.name = 'title'; + title_input.placeholder = 'Title'; + title_input.className = 'forum-create-post-title-input'; + + const content_input = document.createElement('textarea'); + content_input.name = 'content'; + content_input.placeholder = 'Content'; + content_input.className = 'forum-create-post-content-input'; + content_input.style.height = '200px'; + content_input.style.width = '80%'; + + const file_upload = document.createElement('input'); + file_upload.type = 'file'; + file_upload.name = 'file'; + file_upload.accept = '*/*'; + file_upload.className = 'forum-create-topic-file-upload'; + file_upload.multiple = true; + + const closed = document.createElement('input'); + closed.type = 'checkbox'; + closed.name = 'closed'; + closed.id = 'forum-create-post-closed'; + + const closed_label = document.createElement('label'); + closed_label.for = 'closed'; + closed_label.innerHTML = 'Closed'; + closed_label.className = 'forum-create-post-closed-label'; + + const button = document.createElement('button'); + button.innerHTML = 'Create Post'; + button.className = 'forum-create-post-submit-button'; + button.onclick = () => { + if (content_input.value) { + const form_data = new FormData(); + + const json = { + topic_id: topic_id, + title: title_input.value, + text: content_input.value, + }; + + if (closed && closed.checked) { + json.open = !closed; + } + + form_data.append('json', new Blob([JSON.stringify(json)], { type: 'application/json' })); + + if (file_upload.files.length > 0) { + for (let i = 0; i < file_upload.files.length; i++) { + if (file_upload.files[i].name === 'json') { + continue; + } + + form_data.append('file_' + i, file_upload.files[i]); + } + } + + fetch('/api/create_post', { + method: 'POST', + body: form_data + }) + .then(data => { + hide_all_windows(); + + if (data.status === 204 || data.status === 200) { + show_topic(topic_id); + } else { + alert('Error creating post: ' + data.error); + } + }) + .catch(error => { + console.error('Error creating post:', error); + alert('Error creating post: ' + error.message); + }); + } + } + + window.appendChild(title); + window.appendChild(paragraph); + window.appendChild(title_input); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(content_input); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(closed); + window.appendChild(closed_label); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(file_upload); + window.appendChild(document.createElement('br')); + window.appendChild(document.createElement('br')); + window.appendChild(button); + }; + + forum.appendChild(create_post_button); + } + + forum.appendChild(posts_div); +} + function show_credits() { set_path('/'); hide_initial(); @@ -5218,6 +6363,9 @@ function show_credits() { credits.style.overflow = "hidden"; credits.style.minWidth = "80%"; credits.style.minHeight = "80%"; + credits.onclick = () => { + hide_all_windows(); + } const roll_credits = (list, interval, restart_index = 1) => { let index = 0; @@ -5441,45 +6589,46 @@ function get_grid(elements) { function get_link_box(p) { const link_box = document.createElement('div'); link_box.className = 'link_box'; + link_box.style.overflow = 'hidden'; + link_box.style.position = 'relative'; + link_box.style.zIndex = '1'; + + if (p.id) { + link_box.id = p.id; + } if (p.location) { - link_box.setAttribute('onclick', `location.href='${p.location}';`); + link_box.onclick = () => location.href = p.location; + link_box.style.cursor = 'pointer'; } else if (p.onclick) { link_box.setAttribute('onclick', p.onclick); } - if (p.id) { - link_box.id = p.id; + if (p.background_color) { + link_box.style.backgroundColor = p.background_color; } - - if (p.background_color || p.color) { - let style = ''; - if (p.background_color) { - style += `background-color: ${p.background_color};`; - } - if (p.color) { - style += `color: ${p.color};`; - } - link_box.setAttribute('style', style); + if (p.color) { + link_box.style.color = p.color; } - - const title = document.createElement('h2'); - title.className = 'link_box_title'; - title.textContent = p.title; - - const description = document.createElement('p'); - description.className = 'link_box_description'; - description.textContent = p.description; - if (p.img) { const icon = document.createElement('img'); icon.src = p.img; + icon.alt = ''; icon.style.width = '24px'; + icon.style.verticalAlign = 'middle'; + icon.style.marginRight = '8px'; link_box.appendChild(icon); } + const title = document.createElement('h2'); + title.className = 'link_box_title'; + title.textContent = p.title; link_box.appendChild(title); + + const description = document.createElement('p'); + description.className = 'link_box_description'; + description.textContent = p.description; link_box.appendChild(description); return link_box; @@ -5492,14 +6641,21 @@ function init_page() { description: "Browse channels uploaded by others.", background_color: "", id: "browse-button", - onclick: "play_click(); show_browse()" + onclick: "hide_all_windows(); play_click(); show_browse()" })); list.push(get_link_box({ title: "Sandbox", description: "Check out files uploaded by users.", background_color: "", id: "sandbox-button", - onclick: "play_click(); show_sandbox()" + onclick: "hide_all_windows(); play_click(); show_sandbox()" + })) + list.push(get_link_box({ + title: "Forum", + description: "Check out the Forwarder Factory forum.", + background_color: "", + id: "forum-button", + onclick: "hide_all_windows(); play_click(); show_topic()", })) if (get_cookie("username") === null) { @@ -5507,13 +6663,13 @@ function init_page() { title: "Log in", description: "Log in to your account.", id: "login-button", - onclick: "play_click(); show_login()" + onclick: "hide_all_windows(); play_click(); show_login()" })); list.push(get_link_box({ title: "Register", description: "Register a new account.", id: "register-button", - onclick: "play_click(); show_register()" + onclick: "hide_all_windows(); play_click(); show_register()" })); } else { if (get_cookie("user_type") === "1") { @@ -5521,20 +6677,20 @@ function init_page() { title: "Admin", description: "Access the admin panel.", id: "admin-button", - onclick: "play_click(); show_admin()" + onclick: "hide_all_windows(); play_click(); show_admin()" })); } list.push(get_link_box({ title: "Upload", description: "Upload a forwarder or channel.", id: "upload-button", - onclick: "play_click(); show_upload()" + onclick: "hide_all_windows(); play_click(); show_upload()" })); list.push(get_link_box({ title: "Log out", description: "Log out of your account.", id: "logout-button", - onclick: "play_click(); show_logout()" + onclick: "hide_all_windows(); play_click(); show_logout()" })); } @@ -5542,25 +6698,96 @@ function init_page() { title: "Discord", description: "Join our awesome Discord server.", id: "discord-button", - onclick: "play_click(); show_discord()", - img: "/img/discord.svg" + onclick: "hide_all_windows(); play_click(); show_discord()", })); list.push(get_link_box({ title: "Announcements", description: "View the latest announcements.", id: "announcements-button", - onclick: "play_click(); get_announcements()", - img: "/img/announcements.svg" + onclick: "hide_all_windows(); play_click(); get_announcements()", })); list.push(get_link_box({ title: "Credits", description: "View the credits for Forwarder Factory.", id: "credits-button", - onclick: "play_click(); show_credits()" + onclick: "hide_all_windows(); play_click(); show_credits()" })); const grid = get_grid(list, 'initial-link-grid'); document.body.appendChild(grid); + + // special case: credits-button should have fancy stars + // just copy from the credits window + const credits_button = document.getElementById('credits-button'); + if (credits_button) { + generate_stars(50, credits_button); + } + + // special case: discord-button should have a grid of sprites + const discord_button = document.getElementById('discord-button'); + if (discord_button) { + generate_sprites(discord_button, '/img/discord.svg'); + } + + const announcements_button = document.getElementById('announcements-button'); + if (announcements_button) { + generate_sprites(announcements_button, '/img/announcements.svg', { invert: true, opacity: 0.5 }); + } + + const browse_button = document.getElementById('browse-button'); + if (browse_button) { + const deg = 1000; + generate_sprites(browse_button, '/img/background-logo-1.png', { opacity: 0.2, hue: deg }); + // on hover, random colors + browse_button.onmouseover = () => { + const sprites = browse_button.querySelectorAll('.sprite'); + sprites.forEach(sprite => { + sprite.style.filter = `hue-rotate(${Math.random() * 360}deg)`; + }); + }; + browse_button.onmouseleave = () => { + // revert + const sprites = browse_button.querySelectorAll('.sprite'); + sprites.forEach(sprite => { + sprite.style.filter = 'hue-rotate(' + deg + 'deg)'; + }); + } + } + + const sandbox_button = document.getElementById('sandbox-button'); + if (sandbox_button) { + generate_sprites(sandbox_button, '/img/shovel.svg', { opacity: 0.1 }); + } + + const forum_button = document.getElementById('forum-button'); + if (forum_button) { + generate_sprites(forum_button, '/img/messages.svg', { opacity: 0.1 }); + } + + const login_button = document.getElementById('login-button'); + if (login_button) { + generate_sprites(login_button, '/img/question-mark-block.svg', { opacity: 0.1 }); + } + + const register_button = document.getElementById('register-button'); + if (register_button) { + generate_sprites(register_button, '/img/coin.svg', { opacity: 0.1 }); + } + + const admin_button = document.getElementById('admin-button'); + if (admin_button) { + generate_sprites(admin_button, '/img/hammer.svg', { opacity: 0.1 }); + } + + const upload_button = document.getElementById('upload-button'); + if (upload_button) { + generate_sprites(upload_button, '/img/retro-star.svg', { opacity: 0.1 }); + } + + const logout_button = document.getElementById('logout-button'); + if (logout_button) { + generate_sprites(logout_button, '/img/wave.svg', { opacity: 0.1 }); + } } async function get_profile_for_user(username) { @@ -5582,7 +6809,7 @@ async function get_profile_for_user(username) { const data = await response.json(); - // Make sure the structure is what you expect + // make sure the structure is what you expect if (data.users && data.users[username]) { return data.users[username]; } else { @@ -5629,6 +6856,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (get_path() === "/admin" && is_logged_in()) show_admin(); if (get_path() === "/admin" && !is_logged_in()) show_login(); if (get_path() === "/logout" && is_logged_in()) show_logout(); + if (get_path() === "/topic") show_topic(); if (get_path().startsWith("/view/")) { const id = get_path().substring(6); @@ -5642,6 +6870,14 @@ document.addEventListener('DOMContentLoaded', async () => { const name = get_path().substring(9); view_profile(name); } + if (get_path().startsWith("/topic/")) { + const topic_id = get_path().substring(7); + show_topic(topic_id); + } + if (get_path().startsWith("/post/")) { + const post_id = get_path().substring(6); + show_post(post_id); + } print_username(username, display_name, profile_key); }); \ No newline at end of file diff --git a/libs/limhamn b/libs/limhamn index 308a9dd..aaa20f1 160000 --- a/libs/limhamn +++ b/libs/limhamn @@ -1 +1 @@ -Subproject commit 308a9dd57d4a92e535dd92cd1db4394dc6644cbb +Subproject commit aaa20f1f86caa208099d6a09b3a1e8bc809b8eb0 diff --git a/sh/setup-environment.sh b/sh/setup-environment.sh index c226af7..59844ef 100755 --- a/sh/setup-environment.sh +++ b/sh/setup-environment.sh @@ -43,13 +43,13 @@ mkdir -p build && cd build || exit 1 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr make && make install || exit 1 cd ..; rm -rf build + cp -r css/ /etc/ff/ cp -r js/ /etc/ff/ cp -r html/ /etc/ff/ - -git clone https://github.com/ForwarderFactory/ff-web-assets .assets/ -cp -r .assets/* /etc/ff/ -rm -rf .assets/ +cp -r img/ /etc/ff/ +cp -r fonts/ /etc/ff/ +cp -r audio/ /etc/ff/ [ ! -f "/etc/ff/config.yaml" ] && ff-web -gc > /etc/ff/config.yaml diff --git a/src/account_manager.cpp b/src/account_manager.cpp index 94bf598..cff7bf2 100755 --- a/src/account_manager.cpp +++ b/src/account_manager.cpp @@ -10,7 +10,8 @@ bool ff::username_is_stored(const limhamn::http::server::request& request) { } bool ff::ensure_admin_account_exists(database& database) { - for (auto& it : database.query("SELECT * FROM users WHERE user_type = ?;", static_cast(UserType::Administrator))) { + for (auto& it: database.query("SELECT * FROM users WHERE user_type = ?;", static_cast(UserType::Administrator))) { + static_cast(it); return true; } @@ -202,7 +203,13 @@ std::pair ff::try_login(database& database, const response.session["username"] = base_username; response.session["key"] = key; - response.cookies.push_back({"username", base_username, .path = "/"}); + response.cookies.push_back({"username", base_username, .path = "/", + .same_site = "Strict", + .http_only = true, +#ifndef FF_DEBUG + .secure = true, +#endif + }); limhamn::http::server::cookie c; @@ -213,7 +220,14 @@ std::pair ff::try_login(database& database, const user_type = 1; } - response.cookies.push_back({"user_type", std::to_string(user_type)}); + response.cookies.push_back({"user_type", std::to_string(user_type), + .path = "/", + .same_site = "Strict", + .http_only = true, +#ifndef FF_DEBUG + .secure = true, +#endif + }); return {ff::LoginStatus::Success, key}; } diff --git a/src/config.cpp b/src/config.cpp index 2185ba7..a20679a 100755 --- a/src/config.cpp +++ b/src/config.cpp @@ -79,6 +79,7 @@ ff::Settings ff::load_settings(const std::string& _config_file) { if (config["upload"]["convert_images_to_webp"]) settings.convert_images_to_webp = config["upload"]["convert_images_to_webp"].as(); if (config["upload"]["convert_videos_to_webm"]) settings.convert_videos_to_webm = config["upload"]["convert_videos_to_webm"].as(); if (config["download"]["preview_files"]) settings.preview_files = config["download"]["preview_files"].as(); + if (config["topic"]["topics_require_admin"]) settings.topics_require_admin = config["topic"]["topics_require_admin"].as(); if (config["smtp"]["server"]) settings.smtp_server = config["smtp"]["server"].as(); if (config["smtp"]["port"]) settings.smtp_port = config["smtp"]["port"].as(); if (config["smtp"]["username"]) settings.email_username = config["smtp"]["username"].as(); @@ -278,6 +279,10 @@ std::string ff::generate_default_config() { ss << " convert_images_to_webp: " << (ff::settings.convert_images_to_webp ? "true" : "false") << "\n"; ss << " convert_videos_to_webm: " << (ff::settings.convert_videos_to_webm ? "true" : "false") << "\n"; ss << "\n"; + ss << "# Topic options:\n"; + ss << "# topics_require_admin: Whether to require admin when creating topics.\n"; + ss << "topic:\n"; + ss << " topics_require_admin: " << (ff::settings.topics_require_admin ? "true" : "false") << "\n"; ss << "# Download options:\n"; ss << "# preview_files: Whether to preview files in the browser when downloading them.\n"; ss << "download:\n"; diff --git a/src/database.cpp b/src/database.cpp index 1d1f5ec..2764252 100755 --- a/src/database.cpp +++ b/src/database.cpp @@ -3,7 +3,7 @@ #include // implement changes made to the database schema -void ff::update_to_latest(database& database) { +void ff::update_to_latest(database&) { // none as of now } @@ -63,6 +63,20 @@ void ff::setup_database(database& database) { throw std::runtime_error{"Error creating the general table."}; } + // id: the topic id + // identifier: the topic identifier + // json: the json of the topic (including title, description, etc.) + if (!database.exec("CREATE TABLE IF NOT EXISTS topics (" + primary + ", identifier TEXT NOT NULL, json TEXT NOT NULL);")) { + throw std::runtime_error{"Error creating the topics table."}; + } + + // id: the post id + // identifier: the post identifier + // json: the json of the post (including content, author, etc.) + if (!database.exec("CREATE TABLE IF NOT EXISTS posts (" + primary + ", identifier TEXT NOT NULL, json TEXT NOT NULL);")) { + throw std::runtime_error{"Error creating the posts table."}; + } + const auto query = database.query("SELECT * FROM general;"); if (query.empty()) { nlohmann::json json; @@ -143,7 +157,7 @@ std::string ff::upload_file(database& db, const ff::FileConstruct& c) { json["path"] = dir; json["size"] = std::filesystem::file_size(dir); - if (std::filesystem::file_size(dir) <= ff::settings.max_file_size_hash) { + if (file_size(dir) <= static_cast(settings.max_file_size_hash)) { json["sha256"] = scrypto::sha256hash_file(dir); } diff --git a/src/ff.cpp b/src/ff.cpp index 151016f..64cc2ef 100755 --- a/src/ff.cpp +++ b/src/ff.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include void ff::print_help(const bool stream) { std::stringstream ss; @@ -216,6 +218,10 @@ void ff::start_server() { settings.psql_password, settings.psql_database, settings.psql_port); + +#ifdef FF_DEBUG + ff::logger.write_to_log(limhamn::logger::type::notice, "PostgreSQL database opened with host: " + settings.psql_host + ", username: " + settings.psql_username + ", password: " + settings.psql_password + ", database: " + settings.psql_database + "\n"); +#endif #endif } else { #ifdef FF_ENABLE_SQLITE @@ -249,6 +255,9 @@ void ff::start_server() { .whitelisted_ips = settings.whitelisted_ips, .default_rate_limit = settings.rate_limit, .trust_x_forwarded_for = settings.trust_x_forwarded_for, +#ifndef FF_DEBUG + .session_is_secure = true, +#endif }, [&](const limhamn::http::server::request& request) -> limhamn::http::server::response { ff::logger.write_to_log(limhamn::logger::type::access, "Request received from " + request.ip_address + " to " + request.endpoint + " received, handling it.\n"); @@ -261,6 +270,9 @@ void ff::start_server() { {"/browse", ff::handle_root_endpoint}, {"/sandbox", ff::handle_root_endpoint}, {"/view", ff::handle_root_endpoint}, + {"/post", ff::handle_root_endpoint}, + {"/forum", ff::handle_root_endpoint}, + {"/topic", ff::handle_root_endpoint}, {"/upload", ff::handle_root_endpoint}, {"/login", ff::handle_root_endpoint}, {"/register", ff::handle_root_endpoint}, @@ -280,16 +292,30 @@ void ff::start_server() { {"/api/comment_file", ff::handle_api_comment_file_endpoint}, {"/api/delete_comment_forwarder", ff::handle_api_delete_comment_forwarder_endpoint}, {"/api/delete_comment_file", ff::handle_api_delete_comment_file_endpoint}, - {"/api/update_profile", ff::handle_api_update_profile}, - {"/api/get_profile", ff::handle_api_get_profile}, - {"/api/create_announcement", ff::handle_api_create_announcement}, - {"/api/get_announcements", ff::handle_api_get_announcements}, + {"/api/update_profile", ff::handle_api_update_profile_endpoint}, + {"/api/get_profile", ff::handle_api_get_profile_endpoint}, + {"/api/create_announcement", ff::handle_api_create_announcement_endpoint}, + {"/api/get_announcements", ff::handle_api_get_announcements_endpoint}, {"/api/delete_announcement", ff::handle_api_delete_announcement}, - {"/api/edit_announcement", ff::handle_api_edit_announcement}, + {"/api/edit_announcement", ff::handle_api_edit_announcement_endpoint}, {"/api/stay_logged_in", ff::handle_api_stay_logged_in}, {"/api/try_logout", ff::handle_api_try_logout_endpoint}, {"/api/delete_forwarder", ff::handle_api_delete_forwarder_endpoint}, {"/api/delete_file", ff::handle_api_delete_file_endpoint}, + + {"/api/create_post", ff::handle_api_create_post_endpoint}, + {"/api/delete_post", ff::handle_api_delete_post_endpoint}, + {"/api/edit_post", ff::handle_api_edit_post_endpoint}, + {"/api/close_post", ff::handle_api_close_post_endpoint}, + {"/api/get_posts", ff::handle_api_get_posts_endpoint}, + {"/api/comment_post", ff::handle_api_comment_post_endpoint}, + {"/api/delete_comment_post", ff::handle_api_delete_comment_post_endpoint}, + {"/api/create_topic", ff::handle_api_create_topic_endpoint}, + {"/api/delete_topic", ff::handle_api_delete_topic_endpoint}, + {"/api/get_topics", ff::handle_api_get_topics_endpoint}, + {"/api/edit_topic", ff::handle_api_edit_topic_endpoint}, + {"/api/close_topic", ff::handle_api_close_topic_endpoint}, + //{"/api/pin_post_to_topic", ff::handle_api_pin_post_to_topic}, }; const std::unordered_map> setup_handlers{ {virtual_favicon_path, ff::handle_virtual_favicon_endpoint}, @@ -377,6 +403,10 @@ void ff::start_server() { return handlers.at("/")(request, *database); } else if (file.find("/profile/") != std::string::npos) { return handlers.at("/")(request, *database); + } else if (file.find("/topic") != std::string::npos) { + return handlers.at("/")(request, *database); + } else if (file.find("/post/") != std::string::npos) { + return handlers.at("/")(request, *database); } // handle activation URLs @@ -429,6 +459,10 @@ void ff::start_server() { ff::fatal = true; } + if (std::string(e.what()).find("Error creating the ") != std::string::npos) { + ff::fatal = true; + } + if (ff::fatal) { ff::logger.write_to_log(limhamn::logger::type::error, "The last error was too severe to recover, and the server will now halt.\n"); std::exit(EXIT_FAILURE); diff --git a/src/main.cpp b/src/main.cpp index 9a543ab..c8beb1c 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,8 +9,8 @@ int main(int argc, char** argv) { limhamn::argument_manager::argument_manager arg{argc, argv}; - arg.push_back("-h|--help|/h|/help|help", [](const limhamn::argument_manager::collection& c) {ff::print_help(); std::exit(EXIT_SUCCESS);}); - arg.push_back("-v|--version|/v|/version|version", [](const limhamn::argument_manager::collection& c) {ff::print_version(); std::exit(EXIT_SUCCESS);}); + arg.push_back("-h|--help|/h|/help|help", [](const limhamn::argument_manager::collection&) {ff::print_help(); std::exit(EXIT_SUCCESS);}); + arg.push_back("-v|--version|/v|/version|version", [](const limhamn::argument_manager::collection&) {ff::print_version(); std::exit(EXIT_SUCCESS);}); arg.push_back("-c|--config-file|/c|/config-file", [&](limhamn::argument_manager::collection& c) { if (c.arguments.size() <= 1) { std::cerr << "The -c/--config flag requires a file to be specified.\n"; @@ -19,7 +19,7 @@ int main(int argc, char** argv) { config_file = c.arguments.at(++c.index); }); - arg.push_back("-gc|--generate-config|/gc|/generate-config", [&](const limhamn::argument_manager::collection& c) {std::cout << ff::generate_default_config(); std::exit(EXIT_SUCCESS);}); + arg.push_back("-gc|--generate-config|/gc|/generate-config", [&](const limhamn::argument_manager::collection&) {std::cout << ff::generate_default_config(); std::exit(EXIT_SUCCESS);}); arg.execute([](const std::string&) {}); ff::settings = ff::load_settings(config_file); @@ -34,8 +34,8 @@ int main(int argc, char** argv) { ff::settings.port = std::stoi(c.arguments.at(++c.index)); }); - arg.push_back("-he|--halt-on-error|/he|/halt-on-error", [&](const limhamn::argument_manager::collection& c) {ff::settings.halt_on_error = true;}); - arg.push_back("-nhe|--no-halt-on-error|/nhe|/no-halt-on-error", [&](const limhamn::argument_manager::collection& c) {ff::settings.halt_on_error = false;}); + arg.push_back("-he|--halt-on-error|/he|/halt-on-error", [&](const limhamn::argument_manager::collection&) {ff::settings.halt_on_error = true;}); + arg.push_back("-nhe|--no-halt-on-error|/nhe|/no-halt-on-error", [&](const limhamn::argument_manager::collection&) {ff::settings.halt_on_error = false;}); arg.push_back("-c|--config-file|/c|/config-file", [&](limhamn::argument_manager::collection& c) { ++c.index; }); diff --git a/src/path_handlers.cpp b/src/path_handlers.cpp index 3ff67da..c13383e 100755 --- a/src/path_handlers.cpp +++ b/src/path_handlers.cpp @@ -4,8 +4,10 @@ #include #include #include +#include +#include -limhamn::http::server::response ff::handle_root_endpoint(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_root_endpoint(const limhamn::http::server::request&, database&) { limhamn::http::server::response response{}; const auto prepare_file = [](const std::string& path) -> std::string { @@ -273,7 +275,7 @@ limhamn::http::server::response ff::handle_try_setup_endpoint(const limhamn::htt } } -limhamn::http::server::response ff::handle_virtual_favicon_endpoint(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_virtual_favicon_endpoint(const limhamn::http::server::request&, database&) { limhamn::http::server::response response{}; response.content_type = "image/svg+xml"; @@ -290,7 +292,7 @@ limhamn::http::server::response ff::handle_virtual_favicon_endpoint(const limham return response; } -limhamn::http::server::response ff::handle_virtual_stylesheet_endpoint(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_virtual_stylesheet_endpoint(const limhamn::http::server::request&, database&) { limhamn::http::server::response response{}; response.content_type = "text/css"; @@ -307,7 +309,7 @@ limhamn::http::server::response ff::handle_virtual_stylesheet_endpoint(const lim return response; } -limhamn::http::server::response ff::handle_virtual_script_endpoint(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_virtual_script_endpoint(const limhamn::http::server::request&, database&) { limhamn::http::server::response response; response.content_type = "text/javascript"; @@ -316,6 +318,7 @@ limhamn::http::server::response ff::handle_virtual_script_endpoint(const limhamn // TODO: Just like the name, this function is UGLY AS FUCK, and does not belong anywhere near // a project like this. But I simply cannot be bothered to write a JS minifier myself, nor // am I aware of any C++ library for doing such a thing, and I am therefore just going to call uglifyjs. +#ifndef FF_DEBUG const auto uglify_file = [](const std::string& path) -> std::string { static const std::string temp_file = settings.temp_directory + "/ff_temp.js"; if (static_exists.is_file(temp_file)) { @@ -330,10 +333,13 @@ limhamn::http::server::response ff::handle_virtual_script_endpoint(const limhamn // run uglifyjs on the file std::string command = "uglifyjs " + temp_file + " -o " + temp_file; - std::system(command.c_str()); + if (std::system(command.c_str()) != 0) { + return path; + } return temp_file; }; +#endif #if FF_DEBUG response.body = ff::cache_manager.open_file(settings.script_file); @@ -1415,7 +1421,7 @@ limhamn::http::server::response ff::handle_api_set_approval_for_uploads_endpoint return response; } -limhamn::http::server::response ff::handle_api_update_profile(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_update_profile_endpoint(const limhamn::http::server::request& request, database& db) { limhamn::http::server::response response{}; if (request.body.empty()) { @@ -1460,7 +1466,7 @@ limhamn::http::server::response ff::handle_api_update_profile(const limhamn::htt return response; } -limhamn::http::server::response ff::handle_api_get_profile(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_get_profile_endpoint(const limhamn::http::server::request& request, database& db) { limhamn::http::server::response response{}; if (request.method != "POST") { @@ -1567,7 +1573,7 @@ limhamn::http::server::response ff::handle_api_get_profile(const limhamn::http:: return response; } -limhamn::http::server::response ff::handle_api_create_announcement(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_create_announcement_endpoint(const limhamn::http::server::request& request, database& db) { limhamn::http::server::response response{}; response.content_type = "application/json"; @@ -1743,7 +1749,7 @@ limhamn::http::server::response ff::handle_api_create_announcement(const limhamn return response; } -limhamn::http::server::response ff::handle_api_get_announcements(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_get_announcements_endpoint(const limhamn::http::server::request&, database& db) { limhamn::http::server::response response{}; const auto query = db.query("SELECT * FROM general WHERE id=1;"); @@ -1976,7 +1982,7 @@ limhamn::http::server::response ff::handle_api_delete_announcement(const limhamn } } -limhamn::http::server::response ff::handle_api_edit_announcement(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_edit_announcement_endpoint(const limhamn::http::server::request& request, database& db) { limhamn::http::server::response response{}; response.content_type = "application/json"; @@ -3171,9 +3177,8 @@ limhamn::http::server::response ff::handle_api_delete_file_endpoint(const limham const std::string& file_identifier = json.at("file_identifier").get(); - nlohmann::json db_json; try { - db_json = nlohmann::json::parse(ff::get_json_from_table(db, "sandbox", "identifier", file_identifier)); + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "sandbox", "identifier", file_identifier)); const auto& uploader = db_json.at("uploader").get(); if (username != uploader && get_user_type(db, username) != ff::UserType::Administrator) { nlohmann::json ret; @@ -3287,9 +3292,8 @@ limhamn::http::server::response ff::handle_api_delete_forwarder_endpoint(const l const std::string& forwarder_identifier = json.at("forwarder_identifier").get(); - nlohmann::json db_json; try { - db_json = nlohmann::json::parse(ff::get_json_from_table(db, "forwarders", "identifier", forwarder_identifier)); + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "forwarders", "identifier", forwarder_identifier)); const auto& uploader = db_json.at("uploader").get(); if (username != uploader && get_user_type(db, username) != ff::UserType::Administrator) { nlohmann::json ret; @@ -3315,7 +3319,7 @@ limhamn::http::server::response ff::handle_api_delete_forwarder_endpoint(const l return response; } -limhamn::http::server::response ff::handle_api_stay_logged_in(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_stay_logged_in(const limhamn::http::server::request& request, database&) { limhamn::http::server::response response{}; if (request.session_id.empty()) { @@ -3339,6 +3343,9 @@ limhamn::http::server::response ff::handle_api_stay_logged_in(const limhamn::htt .path = "/", .same_site = "Strict", .http_only = true, +#ifdef FF_DEBUG + .secure = false, +#endif }); for (const auto& it : request.cookies) { if (it.name == "username" || it.name == "user_type") { @@ -3349,6 +3356,9 @@ limhamn::http::server::response ff::handle_api_stay_logged_in(const limhamn::htt .path = "/", .same_site = "Strict", .http_only = false, +#ifndef FF_DEBUG + .secure = true, +#endif }); } } @@ -3360,7 +3370,7 @@ limhamn::http::server::response ff::handle_api_stay_logged_in(const limhamn::htt return response; } -limhamn::http::server::response ff::handle_api_try_logout_endpoint(const limhamn::http::server::request& request, database& db) { +limhamn::http::server::response ff::handle_api_try_logout_endpoint(const limhamn::http::server::request&, database&) { limhamn::http::server::response response{}; response.content_type = "application/json"; @@ -3372,3 +3382,1468 @@ limhamn::http::server::response ff::handle_api_try_logout_endpoint(const limhamn return response; } + +limhamn::http::server::response ff::handle_api_create_topic_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (get_user_type(db, username) != ff::UserType::Administrator && settings.topics_require_admin) { + nlohmann::json ret; + ret["error_str"] = "You are not allowed to create topics"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + std::string title{}; + std::string description{}; + std::string topic_id = scrypto::generate_random_string(4); + + if (json.contains("title") && json.at("title").is_string()) { + title = json.at("title").get(); + } + + if (json.contains("description") && json.at("description").is_string()) { + description = json.at("description").get(); + } + + if (json.contains("topic_id") && json.at("topic_id").is_string()) { + topic_id = json.at("topic_id").get(); + } + + const auto check_if_topic_exists = [&db, &topic_id]() -> bool { + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + return !db_json.empty(); + } catch (const std::exception&) { + return false; + } + }; + + int i = 4; + while (check_if_topic_exists()) { + topic_id = scrypto::generate_random_string(i); + ++i; + } + + bool open = true; + if (json.contains("open") && json.at("open").is_boolean()) { + open = json.at("open").get(); + } else if (json.contains("closed") && json.at("closed").is_boolean()) { + open = !json.at("closed").get(); + } + + nlohmann::json db_json; + + db_json["title"] = limhamn::http::utils::htmlspecialchars(title); + db_json["description"] = limhamn::http::utils::htmlspecialchars(description); + db_json["created_by"] = username; + db_json["created_at"] = scrypto::return_unix_millis(); + db_json["identifier"] = topic_id; + db_json["topics"] = nlohmann::json::array(); // {identifier, pinned?} + db_json["posts"] = nlohmann::json::array(); // {identifier, pinned?} + db_json["open"] = open; + + // if we find parent_topics in the JSON, that means we are creating a sub-topic + // this identifier should then be added to the "topics" array of each parent topic + if (json.contains("parent_topics") && json.at("parent_topics").is_array()) { + for (const auto& parent_topic : json.at("parent_topics")) { + if (parent_topic.is_string()) { + try { + nlohmann::json parent_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", parent_topic.get())); + if (!parent_json.empty()) { + if (parent_json.find("topics") == parent_json.end() || !parent_json.at("topics").is_array()) { + parent_json["topics"] = nlohmann::json::array(); + } + parent_json["topics"].push_back(topic_id); + ff::set_json_in_table(db, "topics", "identifier", parent_topic.get(), parent_json.dump()); + } + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Parent topic not found: " + parent_topic.get(); + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + } + } + } + + // to check if it's a root topic, simply check if its ID is part of any topic's "topics" array + // yes -> it's not a root topic because it has parent topics + // no -> it's a root topic because it has no parent topics + + try { + db.exec("INSERT INTO topics (identifier, json) VALUES (?, ?)", topic_id, db_json.dump()); + } catch (const std::exception& e) { + nlohmann::json ret; + ret["error_str"] = "Failed to create topic: " + std::string(e.what()); + ret["error"] = "FF_DATABASE_ERROR"; + response.http_status = 500; + response.body = ret.dump(); + return response; + } + + nlohmann::json ret; + ret["topic_id"] = topic_id; + + response.http_status = 200; + response.content_type = "application/json"; + response.body = ret.dump(); + + return response; +} + +limhamn::http::server::response ff::handle_api_delete_topic_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string topic_id{}; + if (json.contains("topic_id") && json.at("topic_id").is_string()) { + topic_id = json.at("topic_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "topic_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (topic_id.empty()) { + nlohmann::json ret; + ret["error_str"] = "topic_id is empty"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + try { + const auto db_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only delete your own topics"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + try { + db.exec("DELETE FROM topics WHERE identifier = ?", topic_id); + + const auto posts = db.query("SELECT * FROM posts;"); + for (const auto& post : posts) { + const auto post_json = nlohmann::json::parse(post.at("json")); + + if (post_json.contains("topic_id") && post_json.at("topic_id").get() == topic_id) { + db.exec("DELETE FROM posts WHERE identifier = ?", post.at("identifier")); + } + } + } catch (const std::exception& e) { + nlohmann::json ret; + ret["error_str"] = "Failed to delete topic: " + std::string(e.what()); + ret["error"] = "FF_DATABASE_ERROR"; + response.http_status = 500; + response.body = ret.dump(); + return response; + } + + response.http_status = 204; + response.body = ""; + return response; +} + +limhamn::http::server::response ff::handle_api_get_topics_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + int start_index{}; + int end_index = -1; + std::vector ids{}; + + try { + nlohmann::json input_json = nlohmann::json::parse(request.body); + + if (input_json.contains("ids") && input_json.at("ids").is_array()) { + for (const auto& it : input_json.at("ids")) { + if (it.is_string()) { + ids.push_back(it.get()); + } + } + } + + if (input_json.contains("start_index") && input_json.at("start_index").is_number_integer()) { + start_index = input_json.at("start_index").get(); + } + + if (input_json.contains("end_index") && input_json.at("end_index").is_number_integer()) { + end_index = input_json.at("end_index").get(); + } + } catch (const std::exception&) {} + + nlohmann::json json; + + json["topics"] = nlohmann::json::array(); + + try { + auto contents = db.query("SELECT * FROM topics"); + + int i{}; + for (const auto& it : contents) { + if (i < start_index) { + ++i; + continue; + } + if (end_index >= 0 && i > end_index) { + break; + } + + const auto db_json = nlohmann::json::parse(it.at("json")); + + if (!db_json.contains("identifier") || !db_json.at("identifier").is_string()) { + continue; + } + + if (!ids.empty() && std::find(ids.begin(), ids.end(), db_json.at("identifier").get()) == ids.end()) { + continue; // skip if the identifier is not in the list of ids + } + + json["topics"].push_back(db_json); + ++i; + } + } catch (const std::exception& e) { + nlohmann::json ret; + ret["error_str"] = "Failed to get topics: " + std::string(e.what()); + ret["error"] = "FF_DATABASE_ERROR"; + response.http_status = 500; + response.body = ret.dump(); + return response; + } + + response.http_status = 200; + response.body = json.dump(); + return response; +} + +limhamn::http::server::response ff::handle_api_close_topic_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string topic_id{}; + if (json.contains("topic_id") && json.at("topic_id").is_string()) { + topic_id = json.at("topic_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "topic_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (topic_id.empty()) { + nlohmann::json ret; + ret["error_str"] = "topic_id is empty"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + bool open = false; + if (json.contains("open") && json.at("open").is_boolean()) { + open = json.at("open").get(); + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only close your own topics"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + db_json["open"] = open; + + ff::set_json_in_table(db, "topics", "identifier", topic_id, db_json.dump()); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + response.http_status = 204; + response.body = ""; + return response; +} + +limhamn::http::server::response ff::handle_api_edit_topic_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string topic_id{}; + if (json.contains("topic_id") && json.at("topic_id").is_string()) { + topic_id = json.at("topic_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "topic_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (topic_id.empty()) { + nlohmann::json ret; + ret["error_str"] = "topic_id is empty"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only edit your own topics"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (json.contains("title") && json.at("title").is_string()) { + db_json["title"] = limhamn::http::utils::htmlspecialchars(json.at("title").get()); + } + if (json.contains("description") && json.at("description").is_string()) { + db_json["description"] = limhamn::http::utils::htmlspecialchars(json.at("description").get()); + } + + ff::set_json_in_table(db, "topics", "identifier", topic_id, db_json.dump()); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + response.http_status = 204; + response.body = ""; + return response; +} + +limhamn::http::server::response ff::handle_api_create_post_endpoint(const limhamn::http::server::request& request, database& db) { + return ff::handle_try_upload_post_endpoint(request, db); +} + +limhamn::http::server::response ff::handle_api_delete_post_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (get_user_type(db, username) != ff::UserType::Administrator && settings.topics_require_admin) { + nlohmann::json ret; + ret["error_str"] = "You are not allowed to create topics"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + std::string post_id = scrypto::generate_random_string(4); + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } + + const auto check_if_post_exists = [&db, &post_id]() -> bool { + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + return !db_json.empty(); + } catch (const std::exception&) { + return false; + } + }; + + if (!check_if_post_exists()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only delete your own posts"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("topic_id") != db_json.end() && db_json.at("topic_id").is_string()) { + const std::string topic_id = db_json.at("topic_id").get(); + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + auto& posts = topic_json["posts"]; + posts.erase(std::remove_if(posts.begin(), posts.end(), + [&post_id](const nlohmann::json& post) { + return post.contains("identifier") && post.at("identifier").get() == post_id; + }), posts.end()); + + ff::set_json_in_table(db, "topics", "identifier", topic_id, topic_json.dump()); + } + + // now delete the post + db.exec("DELETE FROM posts WHERE identifier = ?", post_id); + + response.http_status = 204; + response.body = ""; + + return response; + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } +} + +limhamn::http::server::response ff::handle_api_close_post_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string post_id{}; + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "post_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + if (post_id.empty()) { + nlohmann::json ret; + ret["error_str"] = "post_id is empty"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + bool open = false; + if (json.contains("open") && json.at("open").is_boolean()) { + open = json.at("open").get(); + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only close your own posts"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("topic_id") != db_json.end() && db_json.at("topic_id").is_string()) { + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", db_json.at("topic_id").get())); + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + } + + db_json["open"] = open; + + ff::set_json_in_table(db, "posts", "identifier", post_id, db_json.dump()); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + response.http_status = 204; + response.body = ""; + return response; +} + +limhamn::http::server::response ff::handle_api_get_posts_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + std::vector ids{}; + int start_index{}; + int end_index = -1; + + try { + nlohmann::json input_json = nlohmann::json::parse(request.body); + + if (input_json.contains("ids") && input_json.at("ids").is_array()) { + for (const auto& it : input_json.at("ids")) { + if (it.is_string()) { + ids.push_back(it.get()); + } + } + } + + if (input_json.contains("start_index") && input_json.at("start_index").is_number_integer()) { + start_index = input_json.at("start_index").get(); + } + + if (input_json.contains("end_index") && input_json.at("end_index").is_number_integer()) { + end_index = input_json.at("end_index").get(); + } + } catch (const std::exception&) {} + + nlohmann::json json; + json["posts"] = nlohmann::json::array(); + + try { + auto contents = db.query("SELECT * FROM posts"); + + int i{}; + for (const auto& it : contents) { + if (i < start_index) { + ++i; + continue; + } + if (end_index >= 0 && i > end_index) { + break; + } + + const auto db_json = nlohmann::json::parse(it.at("json")); + + if (!db_json.contains("identifier") || !db_json.at("identifier").is_string()) { + continue; + } + + if (!ids.empty() && std::find(ids.begin(), ids.end(), db_json.at("identifier").get()) == ids.end()) { + continue; // skip if the identifier is not in the list of ids + } + + json["posts"].push_back(db_json); + ++i; + } + } catch (const std::exception& e) { + nlohmann::json ret; + ret["error_str"] = "Failed to get posts: " + std::string(e.what()); + ret["error"] = "FF_DATABASE_ERROR"; + response.http_status = 500; + response.body = ret.dump(); + return response; + } + + response.http_status = 200; + response.body = json.dump(); + return response; +} + +limhamn::http::server::response ff::handle_api_edit_post_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string post_id{}; + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "post_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("created_by") != db_json.end() && db_json.at("created_by").get() != username && + get_user_type(db, username) != ff::UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "You can only edit your own posts"; + ret["error"] = "FF_NOT_AUTHORIZED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("open") != db_json.end() && !db_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Post is closed"; + ret["error"] = "FF_POST_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("topic_id") != db_json.end() && db_json.at("topic_id").is_string()) { + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", db_json.at("topic_id").get())); + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + } + + if (json.contains("title") && json.at("title").is_string()) { + db_json["title"] = limhamn::http::utils::htmlspecialchars(json.at("title").get()); + } + if (json.contains("text") && json.at("text").is_string()) { + db_json["text"] = limhamn::http::utils::htmlspecialchars(json.at("text").get()); + } + + ff::set_json_in_table(db, "posts", "identifier", post_id, db_json.dump()); + + nlohmann::json ret; + ret["post_id"] = post_id; + ret["topic_id"] = db_json.at("topic_id").get(); + response.http_status = 200; + response.body = ret.dump(); + return response; + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } +} + +limhamn::http::server::response ff::handle_api_comment_post_endpoint(const limhamn::http::server::request& request, database& db) { + return ff::handle_try_upload_post_comment_endpoint(request, db); +} + +limhamn::http::server::response ff::handle_api_delete_comment_post_endpoint(const limhamn::http::server::request& request, database& db) { + limhamn::http::server::response response{}; + response.content_type = "application/json"; + + const auto get_username = [&request]() -> std::string { + if (request.session.find("username") != request.session.end()) { + return request.session.at("username"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("username") != json.end() && json.at("username").is_string()) { + return json.at("username").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const auto get_key = [&request]() -> std::string { + if (request.session.find("key") != request.session.end()) { + return request.session.at("key"); + } + + try { + const auto json = nlohmann::json::parse(request.body); + if (json.find("key") != json.end() && json.at("key").is_string()) { + return json.at("key").get(); + } + } catch (const std::exception&) { + // ignore + } + + return ""; + }; + + const std::string username{get_username()}; + const std::string key{get_key()}; + + if (username.empty() || key.empty()) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Username or key is empty.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Username or key is empty."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + if (!ff::verify_key(db, username, key)) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Invalid credentials.\n"); +#endif + nlohmann::json json; + json["error_str"] = "Invalid credentials."; + json["error"] = "FF_INVALID_CREDENTIALS"; + response.http_status = 400; + response.body = json.dump(); + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(request.body); + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Invalid JSON"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string post_id{}; + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "post_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + int comment_id{}; + if (json.contains("comment_id") && json.at("comment_id").is_number_integer()) { + comment_id = json.at("comment_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "comment_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (db_json.find("open") != db_json.end() && !db_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Post is closed"; + ret["error"] = "FF_POST_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("topic_id") != db_json.end() && db_json.at("topic_id").is_string()) { + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", db_json.at("topic_id").get())); + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + } + + if (db_json.find("comments") != db_json.end() && db_json.at("comments").is_array()) { + auto& comments = db_json["comments"]; + bool found = false; + for (size_t i = 0; i < comments.size(); ++i) { + if (i == static_cast(comment_id) && + ((comments[i].contains("created_by") && comments[i].at("created_by").get() == username) + || get_user_type(db, username) == ff::UserType::Administrator)) { + found = true; + comments.erase(i); + + break; + } + } + if (!found) { + nlohmann::json ret; + ret["error_str"] = "Comment not found"; + ret["error"] = "FF_COMMENT_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + } else { + nlohmann::json ret; + ret["error_str"] = "No comments found"; + ret["error"] = "FF_NO_COMMENTS"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + // reinsert + ff::set_json_in_table(db, "posts", "identifier", post_id, db_json.dump()); + + nlohmann::json ret; + response.http_status = 204; + response.body = ""; + return response; + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } +} \ No newline at end of file diff --git a/src/post_handlers.cpp b/src/post_handlers.cpp new file mode 100755 index 0000000..5a4600c --- /dev/null +++ b/src/post_handlers.cpp @@ -0,0 +1,520 @@ +#include +#include +#include +#include +#include + +limhamn::http::server::response ff::handle_try_upload_post_endpoint(const limhamn::http::server::request& req, database& db) { + limhamn::http::server::response response; + response.content_type = "application/json"; + + std::string _json{}; + + struct FilePtr { + std::string file_path{}; + std::string file_name{}; + }; + + std::vector fh{}; + + std::string username{}; + bool auth{false}; + + if (username_is_stored(req)) { // is session cookie + username = req.session.at("username"); + const std::string key = req.session.at("key"); + + if (!verify_key(db, username, key)) { + nlohmann::json json; + json["error"] = "FF_INVALID_CREDENTIALS"; + json["error_str"] = "Invalid credentials provided."; + + response.body = json.dump(); + response.http_status = 401; + + return response; + } + auth = true; + } + +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Attempting to upload a file.\n"); +#endif + + const auto file_handles = limhamn::http::utils::parse_multipart_form_file(req.raw_body, settings.temp_directory + "/%f-%h-%r"); + for (const auto& it : file_handles) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "File name: " + it.filename + ", Name: " + it.name + "\n"); +#endif + if (it.name == "json") { + _json = ff::open_file(it.path); +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Got JSON\n"); +#endif + } else { + logger.write_to_log(limhamn::logger::type::warning, "Got unknown file name: " + it.name + "\n"); + fh.push_back({.file_path = it.path, .file_name = it.filename}); + } + } + + if (_json.empty()) { + nlohmann::json json; + + json["error"] = "FF_INVALID_JSON"; + json["error_str"] = "Invalid JSON provided."; + + response.body = json.dump(); + response.http_status = 400; + + return response; + } + + nlohmann::json json; + nlohmann::json db_json; + try { + json = nlohmann::json::parse(_json); + } catch (const std::exception&) { + nlohmann::json ret_json; + + ret_json["error"] = "FF_INVALID_JSON"; + ret_json["error_str"] = "Invalid JSON provided."; + + response.body = ret_json.dump(); + response.http_status = 400; + + return response; + } + + if (!auth) { + if (json.find("username") == json.end() || !json.at("username").is_string()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + if (json.find("key") == json.end() || !json.at("key").is_string()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + + username = json.at("username").get(); + const std::string key = json.at("key").get(); + + if (!verify_key(db, username, key)) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + } + + if (fh.size() > 10) { + nlohmann::json ret_json; + ret_json["error"] = "FF_TOO_MANY_FILES"; + ret_json["error_str"] = "You have uploaded too many files. The maximum is 10."; + + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + + std::string title{}; + std::string text{}; + std::string post_id = scrypto::generate_random_string(4); + std::string topic_id{}; + + if (json.contains("title") && json.at("title").is_string()) { + title = json.at("title").get(); + } + + if (json.contains("text") && json.at("text").is_string()) { + text = json.at("text").get(); + } + + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } + + if (json.contains("topic_id") && json.at("topic_id").is_string()) { + topic_id = json.at("topic_id").get(); + } else { + nlohmann::json ret_json; + ret_json["error"] = "FF_MISSING_TOPIC_ID"; + ret_json["error_str"] = "Missing topic ID."; + response.body = ret_json.dump(); + response.http_status = 400; + return response; + } + + const auto check_if_topic_exists = [&db, &topic_id]() -> bool { + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + return !db_json.empty(); + } catch (const std::exception&) { + return false; + } + }; + + if (!check_if_topic_exists()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_TOPIC_NOT_FOUND"; + ret_json["error_str"] = "Topic not found."; + response.body = ret_json.dump(); + response.http_status = 404; + return response; + } + + const auto check_if_post_exists = [&db, &post_id]() -> bool { + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + return !db_json.empty(); + } catch (const std::exception&) { + return false; + } + }; + + int i = 4; + while (check_if_post_exists()) { + post_id = scrypto::generate_random_string(i); + ++i; + } + + bool open = true; + if (json.contains("open") && json.at("open").is_boolean()) { + open = json.at("open").get(); + } + + db_json["title"] = limhamn::http::utils::htmlspecialchars(title); + db_json["text"] = limhamn::http::utils::htmlspecialchars(text); + db_json["created_by"] = username; + db_json["created_at"] = scrypto::return_unix_millis(); + db_json["identifier"] = post_id; + db_json["open"] = open; + db_json["comments"] = nlohmann::json::array(); + db_json["topic_id"] = topic_id; + db_json["data"] = nlohmann::json::array(); + + for (const auto& it : fh) { + std::string data_key = ff::upload_file(db, FileConstruct{ + .path = it.file_path, + .name = it.file_name, + .username = username, + .ip_address = req.ip_address, + .user_agent = req.user_agent, + }); + + if (data_key.empty()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_FILE_UPLOAD_FAILED"; + ret_json["error_str"] = "File upload failed."; + response.body = ret_json.dump(); + response.http_status = 500; + return response; + } + + db_json["data"].push_back({ + {"download_key", data_key}, + {"filename", it.file_name} + }); + } + + // insert to db + try { + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", topic_id)); + + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + if ((topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) && get_user_type(db, username) != UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("posts") == topic_json.end() || !topic_json.at("posts").is_array()) { + topic_json["posts"] = nlohmann::json::array(); + } + topic_json["posts"].push_back(post_id); + + ff::logger.write_to_log(limhamn::logger::type::notice, "Inserting post with ID: " + post_id + " into the database.\n"); + + db.exec("INSERT INTO posts (identifier, json) VALUES (?, ?);", post_id, db_json.dump()); + + ff::logger.write_to_log(limhamn::logger::type::notice, "Post with ID: " + post_id + " inserted into the database.\n"); + + ff::set_json_in_table(db, "topics", "identifier", topic_id, topic_json.dump()); + } catch (const std::exception& e) { + nlohmann::json ret; + ret["error_str"] = "Failed to create post: " + std::string(e.what()); + ret["error"] = "FF_DATABASE_ERROR"; + response.http_status = 500; + response.body = ret.dump(); + return response; + } + + nlohmann::json ret; + ret["post_id"] = post_id; + ret["topic_id"] = topic_id; + + response.http_status = 200; + response.content_type = "application/json"; + response.body = ret.dump(); + + return response; +} + +limhamn::http::server::response ff::handle_try_upload_post_comment_endpoint(const limhamn::http::server::request& req, database& db) { + limhamn::http::server::response response; + response.content_type = "application/json"; + + std::string _json{}; + + struct FilePtr { + std::string file_path{}; + std::string file_name{}; + }; + + std::vector fh{}; + + std::string username{}; + bool auth{false}; + + if (username_is_stored(req)) { // is session cookie + username = req.session.at("username"); + const std::string key = req.session.at("key"); + + if (!verify_key(db, username, key)) { + nlohmann::json json; + json["error"] = "FF_INVALID_CREDENTIALS"; + json["error_str"] = "Invalid credentials provided."; + + response.body = json.dump(); + response.http_status = 401; + + return response; + } + auth = true; + } + +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Attempting to upload a file.\n"); +#endif + + const auto file_handles = limhamn::http::utils::parse_multipart_form_file(req.raw_body, settings.temp_directory + "/%f-%h-%r"); + for (const auto& it : file_handles) { +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "File name: " + it.filename + ", Name: " + it.name + "\n"); +#endif + if (it.name == "json") { + _json = ff::open_file(it.path); +#ifdef FF_DEBUG + logger.write_to_log(limhamn::logger::type::notice, "Got JSON\n"); +#endif + } else { + logger.write_to_log(limhamn::logger::type::warning, "Got unknown file name: " + it.name + "\n"); + fh.push_back({.file_path = it.path, .file_name = it.filename}); + } + } + + if (_json.empty()) { + nlohmann::json json; + + json["error"] = "FF_INVALID_JSON"; + json["error_str"] = "Invalid JSON provided."; + + response.body = json.dump(); + response.http_status = 400; + + return response; + } + + nlohmann::json json; + try { + json = nlohmann::json::parse(_json); + } catch (const std::exception&) { + nlohmann::json ret_json; + + ret_json["error"] = "FF_INVALID_JSON"; + ret_json["error_str"] = "Invalid JSON provided."; + + response.body = ret_json.dump(); + response.http_status = 400; + + return response; + } + + if (!auth) { + if (json.find("username") == json.end() || !json.at("username").is_string()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + if (json.find("key") == json.end() || !json.at("key").is_string()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + + username = json.at("username").get(); + const std::string key = json.at("key").get(); + + if (!verify_key(db, username, key)) { + nlohmann::json ret_json; + ret_json["error"] = "FF_INVALID_CREDENTIALS"; + ret_json["error_str"] = "Invalid credentials provided."; + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + } + + if (fh.size() > 10) { + nlohmann::json ret_json; + ret_json["error"] = "FF_TOO_MANY_FILES"; + ret_json["error_str"] = "You have uploaded too many files. The maximum is 10."; + + response.body = ret_json.dump(); + response.http_status = 401; + return response; + } + + std::string post_id{}; + if (json.contains("post_id") && json.at("post_id").is_string()) { + post_id = json.at("post_id").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "post_id is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + std::string comment{}; + if (json.contains("comment") && json.at("comment").is_string()) { + comment = json.at("comment").get(); + } else { + nlohmann::json ret; + ret["error_str"] = "comment is required"; + ret["error"] = "FF_INVALID_JSON"; + response.http_status = 400; + response.body = ret.dump(); + return response; + } + + try { + nlohmann::json db_json = nlohmann::json::parse(ff::get_json_from_table(db, "posts", "identifier", post_id)); + if (db_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if ((db_json.find("open") != db_json.end() && !db_json.at("open").get()) && get_user_type(db, username) != UserType::Administrator) { + nlohmann::json ret; + ret["error_str"] = "Post is closed"; + ret["error"] = "FF_POST_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + + if (db_json.find("topic_id") != db_json.end() && db_json.at("topic_id").is_string()) { + nlohmann::json topic_json = nlohmann::json::parse(ff::get_json_from_table(db, "topics", "identifier", db_json.at("topic_id").get())); + if (topic_json.empty()) { + nlohmann::json ret; + ret["error_str"] = "Topic not found"; + ret["error"] = "FF_TOPIC_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } + + if (topic_json.find("open") != topic_json.end() && !topic_json.at("open").get()) { + nlohmann::json ret; + ret["error_str"] = "Topic is closed"; + ret["error"] = "FF_TOPIC_CLOSED"; + response.http_status = 403; + response.body = ret.dump(); + return response; + } + } + + if (db_json.find("comments") == db_json.end() || !db_json.at("comments").is_array()) { + db_json["comments"] = nlohmann::json::array(); + } + + nlohmann::json comment_json; + + comment_json["comment"] = limhamn::http::utils::htmlspecialchars(comment); + comment_json["created_by"] = username; + comment_json["created_at"] = scrypto::return_unix_millis(); + comment_json["data"] = nlohmann::json::array(); + for (const auto& it : fh) { + std::string data_key = ff::upload_file(db, FileConstruct{ + .path = it.file_path, + .name = it.file_name, + .username = username, + .ip_address = req.ip_address, + .user_agent = req.user_agent, + }); + + if (data_key.empty()) { + nlohmann::json ret_json; + ret_json["error"] = "FF_FILE_UPLOAD_FAILED"; + ret_json["error_str"] = "File upload failed."; + response.body = ret_json.dump(); + response.http_status = 500; + return response; + } + + comment_json["data"].push_back({ + {"download_key", data_key}, + {"filename", it.file_name} + }); + } + + db_json["comments"].push_back(comment_json); + + ff::set_json_in_table(db, "posts", "identifier", post_id, db_json.dump()); + + nlohmann::json ret; + ret["post_id"] = post_id; + ret["topic_id"] = db_json.at("topic_id").get(); + response.http_status = 200; + response.body = ret.dump(); + return response; + } catch (const std::exception&) { + nlohmann::json ret; + ret["error_str"] = "Post not found"; + ret["error"] = "FF_POST_NOT_FOUND"; + response.http_status = 404; + response.body = ret.dump(); + return response; + } +} diff --git a/src/upload_manager.cpp b/src/upload_manager.cpp index c21beca..39f5c8c 100755 --- a/src/upload_manager.cpp +++ b/src/upload_manager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include std::pair ff::try_upload_forwarder(const limhamn::http::server::request& req, database& db) { std::string json{}; diff --git a/src/wadinfo.cpp b/src/wadinfo.cpp index f2428f0..1d230e9 100755 --- a/src/wadinfo.cpp +++ b/src/wadinfo.cpp @@ -1,19 +1,33 @@ -#include +#include +#include +#include +#include #include +#include +#include +#include ff::WADInfo ff::get_info_from_wad(const std::string& wad_path) { - const auto recv = [](const std::string&& cmd) -> std::string { - std::array buffer{}; - std::string result{}; - std::unique_ptr pipe{popen(cmd.c_str(), "r"), pclose}; - if (!pipe) { - throw std::runtime_error{"popen() failed!"}; - } - while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { - result += buffer.data(); - } - return std::move(result); - }; + const auto recv = [](const std::string& cmd) -> std::string { + std::array buffer{}; + std::string result; + + FILE* raw_pipe = popen(cmd.c_str(), "r"); + if (!raw_pipe) { + throw std::runtime_error{"popen() failed"}; + } + + auto deleter = [](FILE* f) { if (f) pclose(f); }; + + std::unique_ptr pipe(raw_pipe, deleter); + + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result.append(buffer.data()); + } + + return result; + }; + const auto extract_value = [](const std::string& output, const std::string& name) -> std::string { std::stringstream ss{output};