diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/GoogleCaptcha.cpp | 160 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 408 | ||||
-rw-r--r-- | src/StringUtils.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Fourchan.cpp | 62 | ||||
-rw-r--r-- | src/plugins/youtube/YoutubeMediaProxy.cpp | 2 |
5 files changed, 273 insertions, 361 deletions
diff --git a/src/GoogleCaptcha.cpp b/src/GoogleCaptcha.cpp deleted file mode 100644 index 137eb24..0000000 --- a/src/GoogleCaptcha.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#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("</div", goal_description_begin); - if(goal_description_end == std::string::npos) - return false; - - // The goal description with google captcha right now is "Select all images with <strong>subject</strong>". - // 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, "<strong>", ""); - string_replace_all(result, "</strong>", ""); - return true; - } - - static std::optional<GoogleCaptchaChallengeInfo> 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<std::string> 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("</", start_index); - if(end_index == std::string::npos) - return std::nullopt; - - return strip_html_tags(html_source.substr(start_index, end_index - start_index)); - } - - AsyncTask<bool> google_captcha_request_challenge(const std::string &api_key, const std::string &referer, RequestChallengeResponse challenge_response_callback) { - return AsyncTask<bool>([challenge_response_callback, api_key, referer]() { - std::string captcha_url = "https://www.google.com/recaptcha/api/fallback?k=" + api_key; - std::string response; - std::vector<CommandArg> additional_args = { - CommandArg{"-H", "Referer: " + referer} - }; - DownloadResult download_result = download_to_string(captcha_url, response, additional_args); - 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<bool, 9> 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; - } - - AsyncTask<bool> google_captcha_post_solution(const std::string &api_key, const std::string &captcha_id, std::array<bool, 9> selected_images, PostSolutionResponse solution_response_callback) { - return AsyncTask<bool>([solution_response_callback, api_key, captcha_id, selected_images]() { - std::string captcha_url = "https://www.google.com/recaptcha/api/fallback?k=" + api_key; - std::string response; - std::vector<CommandArg> 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); - if(post_result == DownloadResult::OK) { - //fprintf(stderr, "Failed to post captcha solution, response: %s\n", response.c_str()); - std::optional<std::string> 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<GoogleCaptchaChallengeInfo> 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 f8566fa..5af793f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -54,7 +54,6 @@ #include <X11/extensions/Xrandr.h> #include <GL/glx.h> -static const std::string fourchan_google_captcha_api_key = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; static int FPS_IDLE = 2; static const double IDLE_TIMEOUT_SEC = 2.0; static const sf::Vector2i AVATAR_THUMBNAIL_SIZE(std::floor(32), std::floor(32)); @@ -256,11 +255,11 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p source.a + diff_a * progress); } -static std::string base64_encode(const std::string &data) { - return cppcodec::base64_url::encode(data); +static std::string base64_url_encode(const std::string &data) { + return cppcodec::base64_url::encode<std::string>(data); } -static std::string base64_decode(const std::string &data) { +static std::string base64_url_decode(const std::string &data) { return cppcodec::base64_url::decode<std::string>(data); } @@ -999,7 +998,7 @@ namespace QuickMedia { if(filename.size() > 18) // Ignore new manga ids return true; - std::string id_str = base64_decode(filename); + std::string id_str = base64_url_decode(filename); char *endptr = nullptr; errno = 0; long id = strtol(id_str.c_str(), &endptr, 10); @@ -1024,10 +1023,10 @@ namespace QuickMedia { for(const auto &it : new_manga_ids) { Path old_path = content_storage_dir; - old_path.join(base64_encode(std::to_string(it.first))); + old_path.join(base64_url_encode(std::to_string(it.first))); Path new_path = content_storage_dir; - new_path.join(base64_encode(it.second)); + new_path.join(base64_url_encode(it.second)); if(rename_atomic(old_path.data.c_str(), new_path.data.c_str()) != 0) { show_notification("QuickMedia", "Failed to upgrade legacy mangadex ids", Urgency::CRITICAL); abort(); @@ -1451,19 +1450,19 @@ namespace QuickMedia { body_item->set_description_color(get_current_theme().faded_text_color); if(strcmp(plugin_name, "manganelo") == 0) - body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + body_item->url = "https://manganelo.com/manga/" + base64_url_decode(filename.string()); else if(strcmp(plugin_name, "manganelos") == 0) - body_item->url = "http://manganelos.com/manga/" + base64_decode(filename.string()); + body_item->url = "http://manganelos.com/manga/" + base64_url_decode(filename.string()); else if(strcmp(plugin_name, "mangadex") == 0) - body_item->url = base64_decode(filename.string()); + body_item->url = base64_url_decode(filename.string()); else if(strcmp(plugin_name, "mangatown") == 0) - body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); + body_item->url = "https://mangatown.com/manga/" + base64_url_decode(filename.string()); else if(strcmp(plugin_name, "mangakatana") == 0) - body_item->url = "https://mangakatana.com/manga/" + base64_decode(filename.string()); + body_item->url = "https://mangakatana.com/manga/" + base64_url_decode(filename.string()); else if(strcmp(plugin_name, "onimanga") == 0) - body_item->url = "https://onimanga.com/" + base64_decode(filename.string()); + body_item->url = "https://onimanga.com/" + base64_url_decode(filename.string()); else if(strcmp(plugin_name, "readm") == 0) - body_item->url = "https://readm.org/manga/" + base64_decode(filename.string()); + body_item->url = "https://readm.org/manga/" + base64_url_decode(filename.string()); else fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); @@ -1508,7 +1507,7 @@ namespace QuickMedia { bool Program::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { Path content_storage_dir = get_storage_dir().join(service_name); - manga_id_base64 = base64_encode(manga_id); + manga_id_base64 = base64_url_encode(manga_id); content_storage_file = content_storage_dir.join(manga_id_base64); content_storage_json.clear(); content_storage_file_modified = true; @@ -3377,7 +3376,7 @@ namespace QuickMedia { int page_navigation = 0; image_download_cancel = false; - content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_url_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); current_page = pop_page_stack(); @@ -3585,7 +3584,7 @@ namespace QuickMedia { void Program::image_continuous_page(MangaImagesPage *images_page) { image_download_cancel = false; - content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_url_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); current_page = pop_page_stack(); @@ -3670,8 +3669,8 @@ namespace QuickMedia { enum class NavigationStage { VIEWING_COMMENTS, REPLYING, + REQUESTING_CAPTCHA, SOLVING_POST_CAPTCHA, - POSTING_SOLUTION, POSTING_COMMENT, VIEWING_ATTACHED_IMAGE }; @@ -3679,85 +3678,34 @@ namespace QuickMedia { thread_body->title_mark_urls = true; NavigationStage navigation_stage = NavigationStage::VIEWING_COMMENTS; - AsyncTask<bool> captcha_request_future; - AsyncTask<bool> captcha_post_solution_future; - AsyncTask<bool> post_comment_future; - AsyncTask<std::string> load_image_future; - bool downloading_image = false; sf::Texture captcha_texture; sf::Sprite captcha_sprite; - std::mutex captcha_image_mutex; + sf::Texture captcha_bg_texture; + sf::Sprite captcha_bg_sprite; + bool has_captcha_bg = false; + float captcha_slide = 0.0f; std::string attached_image_url; + ImageBoardCaptchaChallenge captcha_challenge; + + const float captcha_slide_padding_x = std::floor(4.0f * get_ui_scale()); + const float captcha_slide_padding_y = std::floor(4.0f * get_ui_scale()); + sf::Color background_color_darker = get_current_theme().background_color; + background_color_darker.r = std::max(0, (int)background_color_darker.r - 20); + background_color_darker.g = std::max(0, (int)background_color_darker.g - 20); + background_color_darker.b = std::max(0, (int)background_color_darker.b - 20); + RoundedRectangle captcha_slide_bg(sf::Vector2f(1.0f, 1.0f), std::floor(10.0f * get_ui_scale()), background_color_darker, &rounded_rectangle_shader); + RoundedRectangle captcha_slide_fg(sf::Vector2f(1.0f, 1.0f), std::floor(10.0f * get_ui_scale() - captcha_slide_padding_y), get_current_theme().loading_bar_color, &rounded_rectangle_shader); auto attached_image_texture = std::make_unique<sf::Texture>(); sf::Sprite attached_image_sprite; - GoogleCaptchaChallengeInfo challenge_info; - sf::Text challenge_description_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(24 * get_ui_scale())); - challenge_description_text.setFillColor(get_current_theme().text_color); - const size_t captcha_num_columns = 3; - const size_t captcha_num_rows = 3; - std::array<bool, captcha_num_columns * captcha_num_rows> 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::Red); - // 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; + std::string captcha_solution; 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 google captcha images load texture in the main thread, otherwise high cpu usage. I guess its fine right now because of only 1 image? - - // TODO: Make this work with other sites than 4chan - auto request_google_captcha_image = [&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, {}); - if(download_image_result == DownloadResult::OK) { - std::lock_guard<std::mutex> lock(captcha_image_mutex); - sf::Image captcha_image; - if(load_image_from_memory(captcha_image, payload_image_data.data(), payload_image_data.size()) && captcha_texture.loadFromImage(captcha_image)) { - captcha_texture.setSmooth(true); - captcha_sprite.setTexture(captcha_texture, true); - challenge_description_text.setString(challenge_info.description); - } else { - show_notification("QuickMedia", "Failed to load downloaded captcha image", Urgency::CRITICAL); - navigation_stage = NavigationStage::VIEWING_COMMENTS; - } - } else { - show_notification("QuickMedia", "Failed to download captcha image", Urgency::CRITICAL); - navigation_stage = NavigationStage::VIEWING_COMMENTS; - } - }; - - auto request_new_google_captcha_challenge = [&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 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<GoogleCaptchaChallengeInfo> 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("QuickMedia", "Failed to get captcha challenge", Urgency::CRITICAL); - navigation_stage = NavigationStage::VIEWING_COMMENTS; - } - }); - }; + const int captcha_solution_text_height = 18 * get_ui_scale(); + sf::Text captcha_solution_text("", *FontLoader::get_font(FontLoader::FontType::LATIN_BOLD), captcha_solution_text_height); + int solved_captcha_ttl = 120; bool redraw = true; @@ -3767,20 +3715,20 @@ namespace QuickMedia { std::string selected_file_for_upload; - auto post_comment = [&comment_input, &selected_file_for_upload, &navigation_stage, &thread_page, &captcha_post_id, &request_new_google_captcha_challenge](std::string comment_to_post, std::string file_to_upload) { + auto post_comment = [&comment_input, &selected_file_for_upload, &navigation_stage, &thread_page, &captcha_post_id, &captcha_solution](std::string comment_to_post, std::string file_to_upload) { comment_input.set_editable(false); - navigation_stage = NavigationStage::POSTING_COMMENT; - PostResult post_result = thread_page->post_comment(captcha_post_id, comment_to_post, file_to_upload); + PostResult post_result = thread_page->post_comment(captcha_post_id, captcha_solution, comment_to_post, file_to_upload); if(post_result == PostResult::OK) { show_notification("QuickMedia", "Comment posted!"); navigation_stage = NavigationStage::VIEWING_COMMENTS; + comment_input.set_text(""); // 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. selected_file_for_upload.clear(); // TODO: Remove from here, this is async } else if(post_result == PostResult::TRY_AGAIN) { - show_notification("QuickMedia", "Error while posting, did the captcha expire? Please try again"); + show_notification("QuickMedia", "Invalid captcha, try again"); // TODO: Check if the response contains a new captcha instead of requesting a new one manually - request_new_google_captcha_challenge(); + navigation_stage = NavigationStage::SOLVING_POST_CAPTCHA; } else if(post_result == PostResult::BANNED) { show_notification("QuickMedia", "Failed to post comment because you are banned", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; @@ -3808,7 +3756,7 @@ namespace QuickMedia { bool frame_skip_text_entry = false; - comment_input.on_submit_callback = [&frame_skip_text_entry, &comment_input, &post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &selected_file_for_upload, &thread_page](std::string text) -> bool { + comment_input.on_submit_callback = [&frame_skip_text_entry, &comment_input, &navigation_stage, &comment_to_post, &captcha_post_id, &captcha_solved_time, &solved_captcha_ttl, &selected_file_for_upload, &thread_page](std::string text) -> bool { if(text.empty() && selected_file_for_upload.empty()) return false; @@ -3817,20 +3765,12 @@ namespace QuickMedia { assert(navigation_stage == NavigationStage::REPLYING); comment_to_post = std::move(text); - if(!captcha_post_id.empty() && captcha_solved_time.getElapsedTime().asSeconds() < 120) { - post_comment_future = AsyncTask<bool>([&post_comment, comment_to_post, selected_file_for_upload]() -> bool { - post_comment(comment_to_post, selected_file_for_upload); - return true; - }); + if((!captcha_post_id.empty() && captcha_solved_time.getElapsedTime().asSeconds() < solved_captcha_ttl) || !thread_page->get_pass_id().empty()) { + navigation_stage = NavigationStage::POSTING_COMMENT; } else if(thread_page->get_pass_id().empty()) { - request_new_google_captcha_challenge(); - } else if(!thread_page->get_pass_id().empty()) { - post_comment_future = AsyncTask<bool>([&post_comment, comment_to_post, selected_file_for_upload]() -> bool { - post_comment(comment_to_post, selected_file_for_upload); - return true; - }); + navigation_stage = NavigationStage::REQUESTING_CAPTCHA; } - return true; + return false; }; sf::RectangleShape comment_input_shade; @@ -3859,7 +3799,9 @@ namespace QuickMedia { std::deque<int> comment_navigation_stack; std::deque<int> comment_page_scroll_stack; + sf::Clock frame_timer; while (current_page == PageType::IMAGE_BOARD_THREAD && window.isOpen()) { + const float frame_elapsed_time_sec = frame_timer.restart().asSeconds(); while (window.pollEvent(event)) { if(navigation_stage == NavigationStage::REPLYING || navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(thread_body->on_event(window, event, navigation_stage == NavigationStage::VIEWING_COMMENTS)) @@ -3912,18 +3854,32 @@ namespace QuickMedia { } else { BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->url.empty()) { - load_image_future.cancel(); - downloading_image = true; navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; attached_image_url = selected_item->url; - load_image_future = AsyncTask<std::string>([attached_image_url]() { + sf::Image image; + TaskResult task_result = run_task_with_loading_screen([&attached_image_url, &image]{ std::string image_data; if(download_to_string_cache(attached_image_url, image_data, {}) != DownloadResult::OK) { show_notification("QuickMedia", "Failed to download image: " + attached_image_url, Urgency::CRITICAL); - image_data.clear(); + return false; } - return image_data; + + if(!load_image_from_memory(image, image_data.data(), image_data.size())) { + show_notification("QuickMedia", "Failed to load image: " + attached_image_url, Urgency::CRITICAL); + return false; + } + return true; }); + + if(task_result == TaskResult::TRUE) { + attached_image_texture = std::make_unique<sf::Texture>(); + if(attached_image_texture->loadFromImage(image)) { + attached_image_sprite.setTexture(*attached_image_texture, true); + navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; + } else { + show_notification("QuickMedia", "Failed to load image: " + attached_image_url, Urgency::CRITICAL); + } + } } } } @@ -4035,48 +3991,29 @@ namespace QuickMedia { } if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA && !frame_skip_text_entry) { - 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; - 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, comment_to_post, selected_file_for_upload](std::optional<std::string> new_captcha_post_id, std::optional<GoogleCaptchaChallengeInfo> 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(comment_to_post, selected_file_for_upload); - } else if(new_challenge_info) { - show_notification("QuickMedia", "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); - } - }); - } - } - - 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; + navigation_stage = NavigationStage::POSTING_COMMENT; + auto str8 = captcha_solution_text.getString().toUtf8(); + captcha_solution.assign(str8.begin(), str8.end()); + } else if(event.key.code == sf::Keyboard::BackSpace) { + auto str = captcha_solution_text.getString(); + if(!str.isEmpty()) { + str.erase(str.getSize() - 1); + captcha_solution_text.setString(std::move(str)); + } + } else { + const int alpha = (int)event.key.code - (int)sf::Keyboard::A; + const int num = (int)event.key.code - (int)sf::Keyboard::Num0; + const int numpad = (int)event.key.code - (int)sf::Keyboard::Numpad0; + if(alpha >= 0 && alpha <= sf::Keyboard::Z - sf::Keyboard::A) { + captcha_solution_text.setString(captcha_solution_text.getString() + (sf::Uint32)to_upper(alpha + 'a')); + } else if(num >= 0 && num <= sf::Keyboard::Num9 - sf::Keyboard::Num0) { + captcha_solution_text.setString(captcha_solution_text.getString() + (sf::Uint32)(num + '0')); + } else if(numpad >= 0 && numpad <= sf::Keyboard::Numpad9 - sf::Keyboard::Numpad0) { + captcha_solution_text.setString(captcha_solution_text.getString() + (sf::Uint32)(numpad + '0')); + } } } @@ -4099,6 +4036,63 @@ namespace QuickMedia { update_idle_state(); handle_x11_events(); + if(navigation_stage == NavigationStage::REQUESTING_CAPTCHA) { + captcha_challenge = ImageBoardCaptchaChallenge(); + TaskResult task_result = run_task_with_loading_screen([thread_page, &captcha_challenge]{ + return thread_page->request_captcha_challenge(captcha_challenge) == PluginResult::OK; + }); + + if(task_result == TaskResult::TRUE) { + comment_input.set_editable(false); + captcha_post_id = std::move(captcha_challenge.challenge_id); + solved_captcha_ttl = 0;// captcha_challenge.ttl; // TODO: Support ttl to not have to solve captcha again + captcha_solution_text.setString(""); + captcha_slide = 0.0f; + + bool failed = false; + sf::Image image; + if(!load_image_from_memory(image, captcha_challenge.img_data.data(), captcha_challenge.img_data.size()) || !captcha_texture.loadFromImage(image)) { + show_notification("QuickMedia", "Failed to load captcha image", Urgency::CRITICAL); + failed = true; + } + captcha_challenge.img_data = std::string(); + + has_captcha_bg = !failed && !captcha_challenge.bg_data.empty(); + sf::Image bg_Image; + if(has_captcha_bg && (!load_image_from_memory(bg_Image, captcha_challenge.bg_data.data(), captcha_challenge.bg_data.size()) || !captcha_bg_texture.loadFromImage(bg_Image))) { + show_notification("QuickMedia", "Failed to load captcha image", Urgency::CRITICAL); + failed = true; + } + captcha_challenge.bg_data = std::string(); + + if(failed) { + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else { + navigation_stage = NavigationStage::SOLVING_POST_CAPTCHA; + captcha_sprite.setTexture(captcha_texture, true); + if(has_captcha_bg) + captcha_bg_sprite.setTexture(captcha_bg_texture, true); + } + } else if(task_result == TaskResult::CANCEL) { + comment_input.set_editable(false); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else if(task_result == TaskResult::FALSE) { + show_notification("QuickMedia", "Failed to get captcha", Urgency::CRITICAL); + comment_input.set_editable(false); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + } else if(navigation_stage == NavigationStage::POSTING_COMMENT) { + TaskResult task_result = run_task_with_loading_screen([&post_comment, &comment_to_post, &selected_file_for_upload]{ + post_comment(comment_to_post, selected_file_for_upload); + return true; + }); + + if(task_result == TaskResult::CANCEL) { + comment_input.set_editable(false); + navigation_stage = NavigationStage::VIEWING_COMMENTS; + } + } + if(selected_file_for_upload.empty()) { if(file_to_upload_thumbnail_data) { file_to_upload_thumbnail_data.reset(); @@ -4162,61 +4156,81 @@ namespace QuickMedia { //comment_input.update(); window.clear(get_current_theme().background_color); - if(navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA) { - std::lock_guard<std::mutex> lock(captcha_image_mutex); - if(captcha_texture.getNativeHandle() != 0) { - const float challenge_description_height = challenge_description_text.getCharacterSize() + 10.0f; + if(navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA && captcha_texture.getNativeHandle() != 0) { + const float slide_speed = 0.5f; + const bool window_has_focus = window.hasFocus(); + if(window_has_focus && sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { + captcha_slide -= (slide_speed * frame_elapsed_time_sec); + if(captcha_slide < 0.0f) + captcha_slide = 0.0f; + } else if(window_has_focus && sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { + captcha_slide += (slide_speed * frame_elapsed_time_sec); + if(captcha_slide > 1.0f) + captcha_slide = 1.0f; + } + + sf::Vector2f content_size = window_size; + int image_height = 0; + + 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 captcha_image_size = captcha_texture_size_f; + captcha_image_size.x *= image_scale.x; + captcha_image_size.y *= image_scale.y; + captcha_sprite.setPosition(std::floor(content_size.x * 0.5f - captcha_image_size.x * 0.5f), std::floor(content_size.y * 0.5f - captcha_image_size.y * 0.5f)); + image_height = (int)captcha_image_size.y; + + if(has_captcha_bg && captcha_bg_texture.getNativeHandle() != 0) { 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); + sf::Vector2u captcha_bg_texture_size = captcha_bg_texture.getSize(); + sf::Vector2f captcha_bg_texture_size_f(captcha_bg_texture_size.x, captcha_bg_texture_size.y); + auto image_scale = get_ratio(captcha_bg_texture_size_f, clamp_to_size(captcha_bg_texture_size_f, content_size)); + captcha_bg_sprite.setScale(image_scale); - auto image_size = captcha_texture_size_f; + auto image_size = captcha_bg_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(std::floor(image_size.x * 0.5f), 0.0f) - sf::Vector2f(std::floor(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); - } - } - } + const float width_diff = captcha_bg_texture_size_f.x - captcha_texture_size_f.x; + const float left_opening = 0.15f; + const float right_opening = 0.25f; + captcha_bg_sprite.setPosition(std::floor(captcha_sprite.getPosition().x + captcha_image_size.x * left_opening - captcha_slide * (width_diff + right_opening * captcha_image_size.x)), std::floor(captcha_sprite.getPosition().y)); + window.draw(captcha_bg_sprite); + + image_height = std::max(image_height, (int)image_size.y); } - } 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 + + window.draw(captcha_sprite); + + // TODO: Cut off ends with sf::View instead + sf::RectangleShape cut_off_rectangle(captcha_image_size); + cut_off_rectangle.setFillColor(get_current_theme().background_color); + cut_off_rectangle.setPosition(captcha_sprite.getPosition() - sf::Vector2f(cut_off_rectangle.getSize().x, 0.0f)); + window.draw(cut_off_rectangle); + + cut_off_rectangle.setPosition(captcha_sprite.getPosition() + sf::Vector2f(captcha_image_size.x, 0.0f)); + window.draw(cut_off_rectangle); + + const float captcha_slide_bg_height = std::floor(20.0f * get_ui_scale()); + captcha_slide_bg.set_size(sf::Vector2f(captcha_image_size.x, captcha_slide_bg_height)); + captcha_slide_bg.set_position(sf::Vector2f(captcha_sprite.getPosition().x, captcha_sprite.getPosition().y + image_height + 10.0f)); + + const sf::Vector2f captcha_slide_fg_size = captcha_slide_bg.get_size() - sf::Vector2f(captcha_slide_padding_x * 2.0f, captcha_slide_padding_y * 2.0f); + captcha_slide_fg.set_size(sf::Vector2f(std::floor(captcha_slide_fg_size.x * captcha_slide), captcha_slide_fg_size.y)); + captcha_slide_fg.set_position(captcha_slide_bg.get_position() + sf::Vector2f(captcha_slide_padding_x, captcha_slide_padding_y)); + + captcha_slide_bg.draw(window); + captcha_slide_fg.draw(window); + + captcha_solution_text.setPosition( + sf::Vector2f( + std::floor(window_size.x * 0.5f - captcha_solution_text.getLocalBounds().width * 0.5f), + std::floor(captcha_slide_bg.get_position().y + captcha_slide_bg.get_size().y + 10.0f))); + window.draw(captcha_solution_text); } else if(navigation_stage == NavigationStage::VIEWING_ATTACHED_IMAGE) { - // TODO: Use image instead of data with string. texture->loadFromMemory creates a temporary image anyways that parses the string. - if(downloading_image && load_image_future.ready()) { - downloading_image = false; - std::string image_data = load_image_future.get(); - - sf::Image attached_image; - if(load_image_from_memory(attached_image, image_data.data(), image_data.size()) && attached_image_texture->loadFromImage(attached_image)) { - attached_image_texture->setSmooth(true); - //attached_image_texture->generateMipmap(); - attached_image_sprite.setTexture(*attached_image_texture, true); - } else { - BodyItem *selected_item = thread_body->get_selected(); - std::string selected_item_attached_url; - if(selected_item) - selected_item_attached_url = selected_item->url; - show_notification("QuickMedia", "Failed to load image downloaded from url: " + selected_item_attached_url, Urgency::CRITICAL); - } - } - if(attached_image_texture->getNativeHandle() != 0) { auto content_size = window_size; sf::Vector2u texture_size = attached_image_texture->getSize(); diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 041a12d..96fdaeb 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -110,7 +110,7 @@ namespace QuickMedia { return it - str.begin(); } - static char to_upper(char c) { + char to_upper(char c) { if(c >= 'a' && c <= 'z') return c - 32; else diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index d4fb726..84a0675 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -3,6 +3,8 @@ #include "../../include/Storage.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" +#include "../../include/Notification.hpp" +#include "../../external/cppcodec/base64_rfc4648.hpp" #include <HtmlParser.h> #include <json/reader.h> #include <string.h> @@ -500,7 +502,7 @@ namespace QuickMedia { return filepath.c_str() + index + 1; } - PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &comment, const std::string &filepath) { + PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath) { std::string url = "https://sys.4chan.org/" + board_id + "/post"; std::vector<CommandArg> additional_args = { @@ -517,7 +519,9 @@ namespace QuickMedia { } if(pass_id.empty()) { - additional_args.push_back(CommandArg{"--form-string", "g-recaptcha-response=" + captcha_id}); + additional_args.push_back(CommandArg{"--form-string", "t-challenge=" + captcha_id}); + if(!captcha_solution.empty()) + additional_args.push_back(CommandArg{"--form-string", "t-response=" + captcha_solution}); } else { Path cookies_filepath; if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { @@ -558,4 +562,58 @@ namespace QuickMedia { } return pass_id; } + + static bool base64_decode(const Json::Value &json_to_decode, std::string &decoded) { + if(!json_to_decode.isString()) + return false; + + const char *start; + const char *end; + if(!json_to_decode.getString(&start, &end)) + return false; + + try { + decoded = cppcodec::base64_rfc4648::decode<std::string>(start, end - start); + return true; + } catch(std::exception&) { + return false; + } + } + + PluginResult FourchanThreadPage::request_captcha_challenge(ImageBoardCaptchaChallenge &challenge_response) { + Json::Value json_root; + DownloadResult result = download_json(json_root, "https://sys.4chan.org/captcha?board=" + url_param_encode(board_id) + "&thread_id=" + thread_id, {}, true); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &error_json = json_root["error"]; + if(error_json.isString()) { + show_notification("QuickMedia", "Failed to get captcha, error: " + error_json.asString(), Urgency::CRITICAL); + return PluginResult::ERR; + } + + const Json::Value &challenge_json = json_root["challenge"]; + const Json::Value &img_json = json_root["img"]; + const Json::Value &bg_json = json_root["bg"]; + const Json::Value &ttl_json = json_root["ttl"]; + if(!challenge_json.isString() || !img_json.isString()) + return PluginResult::ERR; + + challenge_response.challenge_id = challenge_json.asString(); + + if(!base64_decode(img_json, challenge_response.img_data)) + return PluginResult::ERR; + + if(bg_json.isString() && !base64_decode(bg_json, challenge_response.bg_data)) + return PluginResult::ERR; + + if(ttl_json.isInt()) + challenge_response.ttl = ttl_json.asInt(); + else + challenge_response.ttl = 120; + + return PluginResult::OK; + } }
\ No newline at end of file diff --git a/src/plugins/youtube/YoutubeMediaProxy.cpp b/src/plugins/youtube/YoutubeMediaProxy.cpp index 2296d1a..d7ffb53 100644 --- a/src/plugins/youtube/YoutubeMediaProxy.cpp +++ b/src/plugins/youtube/YoutubeMediaProxy.cpp @@ -38,7 +38,7 @@ namespace QuickMedia { std::string url = media_url + "&rn=" + std::to_string(rn) + "&rbuf=" + std::to_string(rbuf); std::vector<const char*> args = { "curl", //"-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", - //"-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", + "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "-g", "-s", "-L", "-f" }; if(is_livestream) { |