From 6c7adadf6d5c85d5e280e965d4dee1563bf46821 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 1 Dec 2019 18:05:16 +0100 Subject: Add 4chan posting --- README.md | 6 +- include/DownloadUtils.hpp | 25 ++++ include/GoogleCaptcha.hpp | 21 +++ include/StringUtils.hpp | 2 + plugins/Fourchan.hpp | 1 + plugins/ImageBoard.hpp | 8 ++ plugins/Plugin.hpp | 17 +-- src/Body.cpp | 2 +- src/DownloadUtils.cpp | 53 ++++++++ src/GoogleCaptcha.cpp | 160 +++++++++++++++++++++++ src/QuickMedia.cpp | 242 +++++++++++++++++++++++++++++++++- src/StringUtils.cpp | 33 +++++ src/plugins/Fourchan.cpp | 326 +++++----------------------------------------- src/plugins/Plugin.cpp | 59 --------- 14 files changed, 576 insertions(+), 379 deletions(-) create mode 100644 include/DownloadUtils.hpp create mode 100644 include/GoogleCaptcha.hpp create mode 100644 src/DownloadUtils.cpp create mode 100644 src/GoogleCaptcha.cpp diff --git a/README.md b/README.md index 6c6445a..56b9529 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,13 @@ QuickMedia youtube --tor ``` ## Controls Press `arrow up` and `arrow down` to navigate the menu and also to go to the previous/next image when viewing manga.\ -Press `Return` (aka `Enter`) to select the item.\ +Press `Enter` (aka `Return`) to select the item.\ Press `ESC` to go back to the previous menu.\ Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed and accessible in PATH environment variable.\ -Press `Backspace` to return to the preview item when reading replies in image board threads. +Press `Backspace` to return to the preview item when reading replies in image board threads.\ +Press `Ctrl + R` to post a reply to a thread (TODO: This should be the keybinding for replying to one comment, not the thread).\ +Press `1 to 9` or `Numpad 1 to 9` to select google captcha image when posting a comment on 4chan. ## Video controls Press `space` to pause/unpause video. `Double-click` video to fullscreen or leave fullscreen. # Dependencies diff --git a/include/DownloadUtils.hpp b/include/DownloadUtils.hpp new file mode 100644 index 0000000..78fc859 --- /dev/null +++ b/include/DownloadUtils.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace QuickMedia { + enum class DownloadResult { + OK, + ERR, + NET_ERR + }; + + struct CommandArg { + std::string option; + std::string value; + }; + + struct FormData { + std::string key; + std::string value; + }; + + DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector &additional_args = {}, bool use_tor = false); + std::vector create_command_args_from_form_data(const std::vector &form_data); +} \ No newline at end of file diff --git a/include/GoogleCaptcha.hpp b/include/GoogleCaptcha.hpp new file mode 100644 index 0000000..9c998da --- /dev/null +++ b/include/GoogleCaptcha.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace QuickMedia { + struct GoogleCaptchaChallengeInfo + { + std::string id; + std::string payload_url; + std::string description; + }; + + using RequestChallengeResponse = std::function)>; + std::future google_captcha_request_challenge(const std::string &api_key, const std::string &referer, RequestChallengeResponse challenge_response_callback, bool use_tor = false); + using PostSolutionResponse = std::function, std::optional)>; + std::future google_captcha_post_solution(const std::string &api_key, const std::string &captcha_id, std::array selected_images, PostSolutionResponse solution_response_callback, bool use_tor); +} \ No newline at end of file diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index 72210c9..c6ae244 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -8,4 +8,6 @@ namespace QuickMedia { using StringSplitCallback = std::function; void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func); + void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str); + std::string strip(const std::string &str); } \ No newline at end of file diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index 5011cd9..2f3ae4b 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -11,6 +11,7 @@ namespace QuickMedia { SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; PluginResult get_threads(const std::string &url, BodyItems &result_items) override; PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) override; + PostResult post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) override; bool search_suggestions_has_thumbnails() const override { return false; } bool search_results_has_thumbnails() const override { return false; } int get_search_delay() const override { return 150; } diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index 090f775..e2a43a9 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -3,6 +3,13 @@ #include "Plugin.hpp" namespace QuickMedia { + enum class PostResult { + OK, + TRY_AGAIN, + BANNED, + ERR + }; + class ImageBoard : public Plugin { public: ImageBoard(const std::string &name) : Plugin(name) {} @@ -12,5 +19,6 @@ namespace QuickMedia { virtual PluginResult get_threads(const std::string &url, BodyItems &result_items) = 0; virtual PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) = 0; + virtual PostResult post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) = 0; }; } \ No newline at end of file diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index 47f0864..e19fbcd 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -2,6 +2,8 @@ #include "../include/Page.hpp" #include "../include/Body.hpp" +#include "../include/StringUtils.hpp" +#include "../include/DownloadUtils.hpp" #include #include #include @@ -25,12 +27,6 @@ namespace QuickMedia { NET_ERR }; - enum class DownloadResult { - OK, - ERR, - NET_ERR - }; - enum class ImageResult { OK, END, @@ -38,13 +34,6 @@ namespace QuickMedia { NET_ERR }; - struct CommandArg { - std::string option; - std::string value; - }; - - std::string strip(const std::string &str); - void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str); void html_unescape_sequences(std::string &str); class Plugin { @@ -77,8 +66,6 @@ namespace QuickMedia { virtual bool search_suggestion_is_search() const { return false; } virtual Page get_page_after_search() const = 0; - DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector &additional_args = {}); - const std::string name; bool use_tor = false; protected: diff --git a/src/Body.cpp b/src/Body.cpp index 5ac8466..5147767 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -107,7 +107,7 @@ namespace QuickMedia { loading_thumbnail = true; thumbnail_load_thread = std::thread([this, result, url]() { std::string texture_data; - if(program->get_current_plugin()->download_to_string(url, texture_data) == DownloadResult::OK) { + if(download_to_string(url, texture_data) == DownloadResult::OK) { if(result->loadFromMemory(texture_data.data(), texture_data.size())) { //result->generateMipmap(); result->setSmooth(true); diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp new file mode 100644 index 0000000..a61d0a1 --- /dev/null +++ b/src/DownloadUtils.cpp @@ -0,0 +1,53 @@ +#include "../include/DownloadUtils.hpp" +#include "../include/Program.h" +#include + +static int accumulate_string(char *data, int size, void *userdata) { + std::string *str = (std::string*)userdata; + str->append(data, size); + return 0; +} + +namespace QuickMedia { + // TODO: Add timeout + DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector &additional_args, bool use_tor) { + sf::Clock timer; + std::vector args; + if(use_tor) + args.push_back("torsocks"); + args.insert(args.end(), { "curl", "-f", "-H", "Accept-Language: en-US,en;q=0.5", "--compressed", "-s", "-L" }); + for(const CommandArg &arg : additional_args) { + args.push_back(arg.option.c_str()); + args.push_back(arg.value.c_str()); + } + args.push_back("--"); + args.push_back(url.c_str()); + args.push_back(nullptr); + if(exec_program(args.data(), accumulate_string, &result) != 0) + return DownloadResult::NET_ERR; + fprintf(stderr, "Download duration for %s: %d ms\n", url.c_str(), timer.getElapsedTime().asMilliseconds()); + return DownloadResult::OK; + } + + std::vector create_command_args_from_form_data(const std::vector &form_data) { + // TODO: This boundary value might need to change, depending on the content. What if the form data contains the boundary value? + const std::string boundary = "-----------------------------119561554312148213571335532670"; + std::string form_data_str; + for(const FormData &form_data_item : form_data) { + form_data_str += boundary; + form_data_str += "\r\n"; + // TODO: What if the form key contains " or \r\n ? + form_data_str += "Content-Disposition: form-data; name=\"" + form_data_item.key + "\""; + form_data_str += "\r\n\r\n"; + // TODO: What is the value contains \r\n ? + form_data_str += form_data_item.value; + form_data_str += "\r\n"; + } + // TODO: Verify if this should only be done also if the form data is empty + form_data_str += boundary + "--"; + return { + CommandArg{"-H", "Content-Type: multipart/form-data; boundary=" + boundary}, + CommandArg{"--data-binary", std::move(form_data_str)} + }; + } +} \ No newline at end of file diff --git a/src/GoogleCaptcha.cpp b/src/GoogleCaptcha.cpp new file mode 100644 index 0000000..b5c3e3c --- /dev/null +++ b/src/GoogleCaptcha.cpp @@ -0,0 +1,160 @@ +#include "../include/GoogleCaptcha.hpp" +#include "../include/StringUtils.hpp" +#include "../include/DownloadUtils.hpp" + +namespace QuickMedia { + static bool google_captcha_response_extract_id(const std::string& html, std::string& result) + { + size_t value_index = html.find("value=\""); + if(value_index == std::string::npos) + return false; + + size_t value_begin = value_index + 7; + size_t value_end = html.find('"', value_begin); + if(value_end == std::string::npos) + return false; + + size_t value_length = value_end - value_begin; + // The id is also only valid if it's in base64, but it might be overkill to verify if it's base64 here + if(value_length < 300) + return false; + + result = html.substr(value_begin, value_length); + return true; + } + + static bool google_captcha_response_extract_goal_description(const std::string& html, std::string& result) + { + size_t goal_description_begin = html.find("rc-imageselect-desc-no-canonical"); + if(goal_description_begin == std::string::npos) { + goal_description_begin = html.find("rc-imageselect-desc"); + if(goal_description_begin == std::string::npos) + return false; + } + + goal_description_begin = html.find('>', goal_description_begin); + if(goal_description_begin == std::string::npos) + return false; + + goal_description_begin += 1; + + size_t goal_description_end = html.find("subject". + // TODO: Should the subject be extracted, so bold styling can be applied to it? + result = html.substr(goal_description_begin, goal_description_end - goal_description_begin); + string_replace_all(result, "", ""); + string_replace_all(result, "", ""); + return true; + } + + static std::optional google_captcha_parse_request_challenge_response(const std::string& api_key, const std::string& html_source) + { + GoogleCaptchaChallengeInfo result; + if(!google_captcha_response_extract_id(html_source, result.id)) + return std::nullopt; + result.payload_url = "https://www.google.com/recaptcha/api2/payload?c=" + result.id + "&k=" + api_key; + if(!google_captcha_response_extract_goal_description(html_source, result.description)) + return std::nullopt; + return result; + } + + // Note: This assumes strings (quoted data) in html tags dont contain '<' or '>' + static std::string strip_html_tags(const std::string& text) + { + std::string result; + size_t index = 0; + while(true) + { + size_t tag_start_index = text.find('<', index); + if(tag_start_index == std::string::npos) + { + result.append(text.begin() + index, text.end()); + break; + } + + result.append(text.begin() + index, text.begin() + tag_start_index); + + size_t tag_end_index = text.find('>', tag_start_index + 1); + if(tag_end_index == std::string::npos) + break; + index = tag_end_index + 1; + } + return result; + } + + static std::optional google_captcha_parse_submit_solution_response(const std::string& html_source) + { + size_t start_index = html_source.find("\"fbc-verification-token\">"); + if(start_index == std::string::npos) + return std::nullopt; + + start_index += 25; + size_t end_index = html_source.find(" google_captcha_request_challenge(const std::string &api_key, const std::string &referer, RequestChallengeResponse challenge_response_callback, bool use_tor) { + return std::async(std::launch::async, [challenge_response_callback, api_key, referer, use_tor]() { + std::string captcha_url = "https://www.google.com/recaptcha/api/fallback?k=" + api_key; + std::string response; + std::vector additional_args = { + CommandArg{"-H", "Referer: " + referer} + }; + DownloadResult download_result = download_to_string(captcha_url, response, additional_args, use_tor); + if(download_result == DownloadResult::OK) { + //fprintf(stderr, "Failed to get captcha, response: %s\n", response.c_str()); + challenge_response_callback(google_captcha_parse_request_challenge_response(api_key, response)); + return true; + } else { + fprintf(stderr, "Failed to get captcha, response: %s\n", response.c_str()); + challenge_response_callback(std::nullopt); + return false; + } + }); + } + + static std::string build_post_solution_response(std::array selected_images) { + std::string result; + for(size_t i = 0; i < selected_images.size(); ++i) { + if(selected_images[i]) { + if(!result.empty()) + result += "&"; + result += "response=" + std::to_string(i); + } + } + return result; + } + + std::future google_captcha_post_solution(const std::string &api_key, const std::string &captcha_id, std::array selected_images, PostSolutionResponse solution_response_callback, bool use_tor) { + return std::async(std::launch::async, [solution_response_callback, api_key, captcha_id, selected_images, use_tor]() { + std::string captcha_url = "https://www.google.com/recaptcha/api/fallback?k=" + api_key; + std::string response; + std::vector additional_args = { + CommandArg{"-H", "Referer: " + captcha_url}, + CommandArg{"--data", "c=" + captcha_id + "&" + build_post_solution_response(selected_images)} + }; + DownloadResult post_result = download_to_string(captcha_url, response, additional_args, use_tor); + if(post_result == DownloadResult::OK) { + //fprintf(stderr, "Failed to post captcha solution, response: %s\n", response.c_str()); + std::optional captcha_post_id = google_captcha_parse_submit_solution_response(response); + if(captcha_post_id) { + solution_response_callback(std::move(captcha_post_id), std::nullopt); + } else { + std::optional challenge_info = google_captcha_parse_request_challenge_response(api_key, response); + solution_response_callback(std::nullopt, std::move(challenge_info)); + } + return true; + } else { + fprintf(stderr, "Failed to post captcha solution, response: %s\n", response.c_str()); + solution_response_callback(std::nullopt, std::nullopt); + return false; + } + }); + } +} \ No newline at end of file diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index ea3524c..c1910ad 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -7,6 +7,7 @@ #include "../include/Program.h" #include "../include/VideoPlayer.hpp" #include "../include/StringUtils.hpp" +#include "../include/GoogleCaptcha.hpp" #include #include @@ -278,7 +279,7 @@ namespace QuickMedia { static void show_notification(const std::string &title, const std::string &description, Urgency urgency = Urgency::NORMAL) { const char *args[] = { "notify-send", "-u", urgency_string(urgency), "--", title.c_str(), description.c_str(), nullptr }; - exec_program(args, nullptr, nullptr); + exec_program_async(args, nullptr); printf("Notification: title: %s, description: %s\n", title.c_str(), description.c_str()); } @@ -1031,7 +1032,7 @@ namespace QuickMedia { return true; std::string image_content; - if(current_plugin->download_to_string(url, image_content) != DownloadResult::OK) { + if(download_to_string(url, image_content) != DownloadResult::OK) { show_notification("Manganelo", "Failed to download image: " + url, Urgency::CRITICAL); return false; } @@ -1423,6 +1424,9 @@ namespace QuickMedia { void Program::image_board_thread_page() { assert(current_plugin->is_image_board()); + // TODO: Support image board other than 4chan. To make this work, the captcha code needs to be changed + // to work with other captcha than google captcha + assert(current_plugin->name == "4chan"); ImageBoard *image_board = static_cast(current_plugin); if(image_board->get_thread_comments(image_board_thread_list_url, content_url, body->items) != PluginResult::OK) { show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); @@ -1433,13 +1437,135 @@ namespace QuickMedia { return; } + const std::string &board = image_board_thread_list_url; + const std::string &thread = content_url; + + fprintf(stderr, "boards: %s, thread: %s\n", board.c_str(), thread.c_str()); + + // TODO: Instead of using stage here, use different pages for each stage + enum class NavigationStage { + VIEWING_COMMENTS, + REPLYING, + SOLVING_POST_CAPTCHA, + POSTING_SOLUTION, + POSTING_COMMENT + }; + + NavigationStage navigation_stage = NavigationStage::VIEWING_COMMENTS; + std::future captcha_request_future; + std::future captcha_post_solution_future; + std::future post_comment_future; + sf::Texture captcha_texture; + sf::Sprite captcha_sprite; + std::mutex captcha_image_mutex; + + GoogleCaptchaChallengeInfo challenge_info; + sf::Text challenge_description_text("", font, 24); + challenge_description_text.setFillColor(sf::Color::White); + const size_t captcha_num_columns = 3; + const size_t captcha_num_rows = 3; + std::array selected_captcha_images; + for(size_t i = 0; i < selected_captcha_images.size(); ++i) { + selected_captcha_images[i] = false; + } + sf::RectangleShape captcha_selection_rect; + captcha_selection_rect.setOutlineThickness(5.0f); + captcha_selection_rect.setOutlineColor(sf::Color(0, 85, 119)); + // TODO: Draw only the outline instead of a transparent rectangle + captcha_selection_rect.setFillColor(sf::Color::Transparent); + + // Valid for 2 minutes after solving a captcha + std::string captcha_post_id; + sf::Clock captcha_solved_time; + std::string comment_to_post; + + // TODO: Show a white image with "Loading..." text while the captcha image is downloading + + // TODO: Make this work with other sites than 4chan + auto request_google_captcha_image = [this, &captcha_texture, &captcha_image_mutex, &navigation_stage, &captcha_sprite, &challenge_description_text](GoogleCaptchaChallengeInfo &challenge_info) { + std::string payload_image_data; + DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, current_plugin->use_tor); + if(download_image_result == DownloadResult::OK) { + std::lock_guard lock(captcha_image_mutex); + if(captcha_texture.loadFromMemory(payload_image_data.data(), payload_image_data.size())) { + captcha_texture.setSmooth(true); + captcha_sprite.setTexture(captcha_texture, true); + challenge_description_text.setString(challenge_info.description); + } else { + show_notification("Google captcha", "Failed to load downloaded captcha image", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + } else { + show_notification("Google captcha", "Failed to download captcha image", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + }; + + auto request_new_google_captcha_challenge = [this, &selected_captcha_images, &navigation_stage, &captcha_request_future, &request_google_captcha_image, &challenge_info]() { + fprintf(stderr, "Solving captcha!\n"); + navigation_stage = NavigationStage::SOLVING_POST_CAPTCHA; + for(size_t i = 0; i < selected_captcha_images.size(); ++i) { + selected_captcha_images[i] = false; + } + const std::string fourchan_google_captcha_api_key = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; + const std::string referer = "https://boards.4chan.org/"; + captcha_request_future = google_captcha_request_challenge(fourchan_google_captcha_api_key, referer, + [&navigation_stage, &request_google_captcha_image, &challenge_info](std::optional new_challenge_info) { + if(navigation_stage != NavigationStage::SOLVING_POST_CAPTCHA) + return; + + if(new_challenge_info) { + challenge_info = new_challenge_info.value(); + request_google_captcha_image(challenge_info); + } else { + show_notification("Google captcha", "Failed to get captcha challenge", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + }, current_plugin->use_tor); + }; + + auto post_comment = [this, &navigation_stage, &image_board, &board, &thread, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { + navigation_stage = NavigationStage::POSTING_COMMENT; + PostResult post_result = image_board->post_comment(board, thread, captcha_post_id, comment_to_post); + if(post_result == PostResult::OK) { + show_notification(current_plugin->name, "Comment posted!"); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + // TODO: Append posted comment to the thread so the user can see their posted comment. + // TODO: Asynchronously update the thread periodically to show new comments. + } else if(post_result == PostResult::TRY_AGAIN) { + show_notification(current_plugin->name, "Error while posting, did the captcha expire? Please try again"); + // TODO: Check if the response contains a new captcha instead of requesting a new one manually + request_new_google_captcha_challenge(); + } else if(post_result == PostResult::BANNED) { + show_notification(current_plugin->name, "Failed to post comment because you are banned", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else if(post_result == PostResult::ERR) { + show_notification(current_plugin->name, "Failed to post comment. Is " + current_plugin->name + " down or is your internet down?", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else { + assert(false && "Unhandled post result"); + show_notification(current_plugin->name, "Failed to post comment. Unknown error", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + }; + // Instead of using search bar to searching, use it for commenting. // TODO: Have an option for the search bar to be multi-line. search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment](const std::string &text) -> bool { if(text.empty()) return false; + assert(navigation_stage == NavigationStage::REPLYING); + comment_to_post = text; + if(!captcha_post_id.empty() && captcha_solved_time.getElapsedTime().asSeconds() < 120) { + post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool { + post_comment(); + return true; + }); + } else { + request_new_google_captcha_challenge(); + } return true; }; @@ -1463,7 +1589,7 @@ namespace QuickMedia { if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) redraw = true; - else if(event.type == sf::Event::KeyPressed) { + else if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(event.key.code == sf::Keyboard::Up) { body->select_previous_item(); } else if(event.key.code == sf::Keyboard::Down) { @@ -1502,6 +1628,65 @@ namespace QuickMedia { body->items[reply_index]->visible = true; } } + } else if(event.key.code == sf::Keyboard::R && sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) && selected_item) { + navigation_stage = NavigationStage::REPLYING; + fprintf(stderr, "Replying!\n"); + } + } else if(event.type == sf::Event::TextEntered && navigation_stage == NavigationStage::REPLYING) { + search_bar->onTextEntered(event.text.unicode); + } + + if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::REPLYING) { + if(event.key.code == sf::Keyboard::Escape) { + search_bar->clear(); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + } + + if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA) { + int num = -1; + if(event.key.code >= sf::Keyboard::Num1 && event.key.code <= sf::Keyboard::Num9) { + num = event.key.code - sf::Keyboard::Num1; + } else if(event.key.code >= sf::Keyboard::Numpad1 && event.key.code <= sf::Keyboard::Numpad9) { + num = event.key.code - sf::Keyboard::Numpad1; + } + + constexpr int select_map[9] = { 6, 7, 8, 3, 4, 5, 0, 1, 2 }; + if(num != -1) { + int index = select_map[num]; + selected_captcha_images[index] = !selected_captcha_images[index]; + } + + if(event.key.code == sf::Keyboard::Escape) { + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else if(event.key.code == sf::Keyboard::Enter) { + navigation_stage = NavigationStage::POSTING_SOLUTION; + const std::string fourchan_google_captcha_api_key = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; + captcha_post_solution_future = google_captcha_post_solution(fourchan_google_captcha_api_key, challenge_info.id, selected_captcha_images, + [&navigation_stage, &captcha_post_id, &captcha_solved_time, &selected_captcha_images, &challenge_info, &request_google_captcha_image, &post_comment](std::optional new_captcha_post_id, std::optional new_challenge_info) { + if(navigation_stage != NavigationStage::POSTING_SOLUTION) + return; + + if(new_captcha_post_id) { + captcha_post_id = new_captcha_post_id.value(); + captcha_solved_time.restart(); + post_comment(); + } else if(new_challenge_info) { + show_notification("Google captcha", "Failed to solve captcha, please try again"); + challenge_info = new_challenge_info.value(); + navigation_stage = NavigationStage::SOLVING_POST_CAPTCHA; + for(size_t i = 0; i < selected_captcha_images.size(); ++i) { + selected_captcha_images[i] = false; + } + request_google_captcha_image(challenge_info); + } + }, current_plugin->use_tor); + } + } + + if(event.type == sf::Event::KeyPressed && (navigation_stage == NavigationStage::POSTING_SOLUTION || navigation_stage == NavigationStage::POSTING_COMMENT)) { + if(event.key.code == sf::Keyboard::Escape) { + navigation_stage = NavigationStage::VIEWING_COMMENTS; } } } @@ -1527,9 +1712,54 @@ namespace QuickMedia { //search_bar->update(); window.clear(back_color); - body->draw(window, body_pos, body_size); - search_bar->draw(window); + if(navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA) { + std::lock_guard lock(captcha_image_mutex); + if(captcha_texture.getNativeHandle() != 0) { + const float challenge_description_height = challenge_description_text.getCharacterSize() + 10.0f; + sf::Vector2f content_size = window_size; + content_size.y -= challenge_description_height; + + sf::Vector2u captcha_texture_size = captcha_texture.getSize(); + sf::Vector2f captcha_texture_size_f(captcha_texture_size.x, captcha_texture_size.y); + auto image_scale = get_ratio(captcha_texture_size_f, clamp_to_size(captcha_texture_size_f, content_size)); + captcha_sprite.setScale(image_scale); + + auto image_size = captcha_texture_size_f; + image_size.x *= image_scale.x; + image_size.y *= image_scale.y; + captcha_sprite.setPosition(std::floor(content_size.x * 0.5f - image_size.x * 0.5f), std::floor(challenge_description_height + content_size.y * 0.5f - image_size.y * 0.5f)); + window.draw(captcha_sprite); + + challenge_description_text.setPosition(captcha_sprite.getPosition() + sf::Vector2f(image_size.x * 0.5f, 0.0f) - sf::Vector2f(challenge_description_text.getLocalBounds().width * 0.5f, challenge_description_height)); + window.draw(challenge_description_text); + + for(size_t column = 0; column < captcha_num_columns; ++column) { + for(size_t row = 0; row < captcha_num_rows; ++row) { + if(selected_captcha_images[column + captcha_num_columns * row]) { + captcha_selection_rect.setPosition(captcha_sprite.getPosition() + sf::Vector2f(image_size.x / captcha_num_columns * column, image_size.y / captcha_num_rows * row)); + captcha_selection_rect.setSize(sf::Vector2f(image_size.x / captcha_num_columns, image_size.y / captcha_num_rows)); + window.draw(captcha_selection_rect); + } + } + } + } + } else if(navigation_stage == NavigationStage::POSTING_SOLUTION) { + // TODO: Show "Posting..." when posting solution + } else if(navigation_stage == NavigationStage::POSTING_COMMENT) { + // TODO: Show "Posting..." when posting comment + } else { + body->draw(window, body_pos, body_size); + search_bar->draw(window); + } window.display(); } + + // TODO: Instead of waiting for them, kill them somehow + if(captcha_request_future.valid()) + captcha_request_future.get(); + if(captcha_post_solution_future.valid()) + captcha_post_solution_future.get(); + if(post_comment_future.valid()) + post_comment_future.get(); } } \ No newline at end of file diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index deb4949..16d3b48 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -14,4 +14,37 @@ namespace QuickMedia { index = new_index + 1; } } + + void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str) { + size_t index = 0; + while(true) { + index = str.find(old_str, index); + if(index == std::string::npos) + return; + str.replace(index, old_str.size(), new_str); + } + } + + static bool is_whitespace(char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\v'; + } + + std::string strip(const std::string &str) { + if(str.empty()) + return str; + + int start = 0; + for(; start < (int)str.size(); ++start) { + if(!is_whitespace(str[start])) + break; + } + + int end = str.size() - 1; + for(; end >= start; --end) { + if(!is_whitespace(str[end])) + break; + } + + return str.substr(start, end - start + 1); + } } \ No newline at end of file diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 4df77c4..cd3f7e7 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -10,302 +10,6 @@ static const std::string fourchan_url = "https://a.4cdn.org/"; static const std::string fourchan_image_url = "https://i.4cdn.org/"; -// Legacy recaptcha command: curl 'https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc' -H 'Referer: https://boards.4channel.org/' -H 'Cookie: CONSENT=YES' - -/* -Answering recaptcha: -curl 'https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc' --H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0' --H 'Referer: https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc' --H 'Content-Type: application/x-www-form-urlencoded' ---data 'c=03AOLTBLQ66PjSi9s8S-R1vUS2Jgm-Z_ghEejvvjAaeF3FoR9MiM0zHhCxuertrCo7MAcFUEqcIg4l2WJzVtrJhJVLkncF12OzCaeIvbm46hgDZDZjLD89-LMn1Zs0TP37P-Hd4cuRG8nHuEBXc2ZBD8CVX-6HAs9VBgSmsgQeKF1PWm1tAMBccJhlh4rAOkpjzaEXMMGOe17N0XViwDYZxLGhe4H8IAG2KNB1fb4rz4YKJTPbL30_FvHw7zkdFtojjWiqVW0yCN6N192dhfd9oKz2r9pGRrR6N4AkkX-L0DsBD4yNK3QRsQn3dB1fs3JRZPAh1yqUqTQYhOaqdggyc1EwL8FZHouGRkHTOcCmLQjyv6zuhi6CJbg&response=1&response=4&response=5&response=7' -*/ -/* -Response: - - - - - - reCAPTCHA challenge - - - -
-
-
- -
-
-
Copy this code and paste it in the empty box below
-
- -
This code is valid for 2 minutes
-
-
- -
- - - -*/ - -/* Posting message: -curl 'https://sys.4chan.org/bant/post' --H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0' --H 'Referer: https://boards.4chan.org/' --H 'Content-Type: multipart/form-data; boundary=---------------------------119561554312148213571335532670' --H 'Origin: https://boards.4chan.org' --H 'Cookie: __cfduid=d4bd4932e46bc3272fae4ce7a4e2aac511546800687; 4chan_pass=_SsBuZaATt3dIqfVEWlpemhU5XLQ6i9RC' ---data-binary $'-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="resto"\r\n\r\n8640736\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="com"\r\n\r\n>>8640771\r\nShe looks finnish\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="mode"\r\n\r\nregist\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="pwd"\r\n\r\n_SsBuZaATt3dIqfVEWlpemhU5XLQ6i9RC\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="g-recaptcha-response"\r\n\r\n03AOLTBLS5lshp5aPj5pG6xdVMQ0pHuHxAtJoCEYuPLNKYlsRWNCPQegjB9zgL-vwdGMzjcT-L9iW4bnQ5W3TqUWHOVqtsfnx9GipLUL9o2XbC6r9zy-EEiPde7l6J0WcZbr9nh_MGcUpKl6RGaZoYB3WwXaDq74N5hkmEAbqM_CBtbAVVlQyPmemI2HhO2J6K0yFVKBrBingtIZ6-oXBXZ4jC4rT0PeOuVaH_gf_EBjTpb55ueaPmTbeLGkBxD4-wL1qA8F8h0D8c\r\n-----------------------------119561554312148213571335532670--\r\n' -*/ -/* Response if banned: - - - - - - - - - - - - - - -/g/ - Technology - 4chan - - - - - - -
- -[a / -b / -c / -d / -e / -f / -g / -gif / -h / -hr / -k / -m / -o / -p / -r / -s / -t / -u / -v / -vg / -vr / -w - / wg] - -[i / -ic] - -[r9k / -s4s / -vip / -qa] - -[cm / -hm / -lgbt - / y] - -[3 / -aco / -adv / -an / -asp / -bant / -biz / -cgl / -ck / -co / -diy / -fa / -fit / -gd / -hc / -his / -int / -jp / -lit / -mlp / -mu / -n / -news / -out / -po / -pol / -qst / -sci / -soc / -sp / -tg / -toy / -trv / -tv / -vp / -wsg / -wsr / -x] - - - -[Settings] -[Search] -[Mobile] -[Home] -
- -
-
- Board - -
- -
- - Settings - Mobile - Home -
- -
- - -
-
-
/g/ - Technology
- -
-
Error: You are banned.

[Return]



- - - - -All trademarks and copyrights on this page are owned by their respective parties. Images uploaded are the responsibility of the Poster. Comments are owned by the Poster. - - -
- -
- -*/ -/* Banned page: - - - - - -4chan - Banned - - - - - - - - - - - -
-
-
-4chan -
-
-
-
-
-

You are banned! ;_;

-
-
-Banned - -You have been banned from /g/ for posting >>73505519, a violation of Rule 1:

-Off-topic; all images and discussion should pertain to technology and related topics.

-Your ban was filed on November 9th, 2019 and expires on November 10th, 2019 at 22:10 ET, which is 23 hours and 14 minutes from now. -

-According to our server, your IP is: YOURIP. The name you were posting with was Anonymous.
-
-Because of the short length of your ban, you may not appeal it. Please check back when your ban has expired. -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - - - -*/ - namespace QuickMedia { PluginResult Fourchan::get_front_page(BodyItems &result_items) { std::string server_response; @@ -658,4 +362,34 @@ namespace QuickMedia { return PluginResult::OK; } + + PostResult Fourchan::post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) { + std::string url = "https://sys.4chan.org/" + board + "/post"; + std::vector form_data = { + FormData{"resto", thread}, + FormData{"com", comment}, + FormData{"mode", "regist"}, + FormData{"g-recaptcha-response", captcha_id} + }; + + std::vector additional_args = { + CommandArg{"-H", "Referer: https://boards.4chan.org/"}, + CommandArg{"-H", "Content-Type: multipart/form-data; boundary=---------------------------119561554312148213571335532670"}, + CommandArg{"-H", "Origin: https://boards.4chan.org"} + }; + std::vector form_data_args = create_command_args_from_form_data(form_data); + additional_args.insert(additional_args.end(), form_data_args.begin(), form_data_args.end()); + + std::string response; + if(download_to_string(url, response, additional_args, use_tor) != DownloadResult::OK) + return PostResult::ERR; + + if(response.find("successful") != std::string::npos) + return PostResult::OK; + if(response.find("banned") != std::string::npos) + return PostResult::BANNED; + if(response.find("try again") != std::string::npos || response.find("No valid captcha") != std::string::npos) + return PostResult::TRY_AGAIN; + return PostResult::ERR; + } } \ No newline at end of file diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index 7c31292..a9adf15 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -1,15 +1,8 @@ #include "../../plugins/Plugin.hpp" -#include "../../include/Program.h" #include #include #include -static int accumulate_string(char *data, int size, void *userdata) { - std::string *str = (std::string*)userdata; - str->append(data, size); - return 0; -} - namespace QuickMedia { SearchResult Plugin::search(const std::string &text, BodyItems &result_items) { (void)text; @@ -28,44 +21,11 @@ namespace QuickMedia { return {}; } - static bool is_whitespace(char c) { - return c == ' ' || c == '\n' || c == '\t' || c == '\v'; - } - - std::string strip(const std::string &str) { - if(str.empty()) - return str; - - int start = 0; - for(; start < (int)str.size(); ++start) { - if(!is_whitespace(str[start])) - break; - } - - int end = str.size() - 1; - for(; end >= start; --end) { - if(!is_whitespace(str[end])) - break; - } - - return str.substr(start, end - start + 1); - } - struct HtmlEscapeSequence { std::string escape_sequence; std::string unescaped_str; }; - void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str) { - size_t index = 0; - while(true) { - index = str.find(old_str, index); - if(index == std::string::npos) - return; - str.replace(index, old_str.size(), new_str); - } - } - void html_unescape_sequences(std::string &str) { const std::array escape_sequences = { HtmlEscapeSequence { """, "\"" }, @@ -97,23 +57,4 @@ namespace QuickMedia { return result.str(); } - - DownloadResult Plugin::download_to_string(const std::string &url, std::string &result, const std::vector &additional_args) { - sf::Clock timer; - std::vector args; - if(use_tor) - args.push_back("torsocks"); - args.insert(args.end(), { "curl", "-f", "-H", "Accept-Language: en-US,en;q=0.5", "--compressed", "-s", "-L" }); - for(const CommandArg &arg : additional_args) { - args.push_back(arg.option.c_str()); - args.push_back(arg.value.c_str()); - } - args.push_back("--"); - args.push_back(url.c_str()); - args.push_back(nullptr); - if(exec_program(args.data(), accumulate_string, &result) != 0) - return DownloadResult::NET_ERR; - fprintf(stderr, "Download duration for %s: %d ms\n", url.c_str(), timer.getElapsedTime().asMilliseconds()); - return DownloadResult::OK; - } } \ No newline at end of file -- cgit v1.2.3