From e5fc00e506806f06bde32e9334fdec57dd8cb86d Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 27 Jul 2021 20:24:36 +0200 Subject: Implement new 4chan captcha --- README.md | 2 +- TODO | 3 +- external/cppcodec/base64_rfc4648.hpp | 73 ++++++ include/StringUtils.hpp | 1 + plugins/Fourchan.hpp | 3 +- plugins/ImageBoard.hpp | 12 +- src/GoogleCaptcha.cpp | 160 ------------ src/QuickMedia.cpp | 408 +++++++++++++++--------------- src/StringUtils.cpp | 2 +- src/plugins/Fourchan.cpp | 62 ++++- src/plugins/youtube/YoutubeMediaProxy.cpp | 2 +- 11 files changed, 363 insertions(+), 365 deletions(-) create mode 100644 external/cppcodec/base64_rfc4648.hpp delete mode 100644 src/GoogleCaptcha.cpp diff --git a/README.md b/README.md index be3d3a1..fc49940 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Type text and then wait and QuickMedia will automatically search.\ `U`: Bring up file manager to choose which file should be uploaded, `Esc` to cancel.\ `Ctrl+D`: Remove the selected file from the post.\ `Ctrl+I`: Reverse image search the selected image or select an url to open in the browser.\ -`1 to 9`/`Numpad 1 to 9`: Select/deselect google captcha image when posting a comment on 4chan.\ +`Arrow left/right`: Move captcha slider left/right.\ `Ctrl+S`: Save the image/video attached to the selected post. ### File save controls `Tab`: Switch between navigating the file manager and file name.\ diff --git a/TODO b/TODO index 8b915d8..cfeee31 100644 --- a/TODO +++ b/TODO @@ -182,4 +182,5 @@ Check if user has invite privileges and show error before bringing up invite gui If only users in the same homeserver can join a room then filter out other users in room invite gui. Exclude users that are already in the room from room invite gui. Sort users in matrix users list (efficiently and only once!). -Optimize matrix insert position for room sorting (and user sorting). \ No newline at end of file +Optimize matrix insert position for room sorting (and user sorting). +Cache solved 4chan captcha solution (and challenge id) on disk. ttl is (always?) 120 seconds so restarting quickmedia under that period should use the already solved captcha. \ No newline at end of file diff --git a/external/cppcodec/base64_rfc4648.hpp b/external/cppcodec/base64_rfc4648.hpp new file mode 100644 index 0000000..436bc9d --- /dev/null +++ b/external/cppcodec/base64_rfc4648.hpp @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2015 Topology LP + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef CPPCODEC_BASE64_RFC4648 +#define CPPCODEC_BASE64_RFC4648 + +#include "detail/codec.hpp" +#include "detail/base64.hpp" + +namespace cppcodec { + +namespace detail { + +static constexpr const char base64_rfc4648_alphabet[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' +}; + +class base64_rfc4648 +{ +public: + template using codec_impl = stream_codec; + + static CPPCODEC_ALWAYS_INLINE constexpr size_t alphabet_size() { + static_assert(sizeof(base64_rfc4648_alphabet) == 64, "base64 alphabet must have 64 values"); + return sizeof(base64_rfc4648_alphabet); + } + static CPPCODEC_ALWAYS_INLINE constexpr char symbol(alphabet_index_t idx) + { + return base64_rfc4648_alphabet[idx]; + } + static CPPCODEC_ALWAYS_INLINE constexpr char normalized_symbol(char c) { return c; } + + static CPPCODEC_ALWAYS_INLINE constexpr bool generates_padding() { return true; } + static CPPCODEC_ALWAYS_INLINE constexpr bool requires_padding() { return true; } + static CPPCODEC_ALWAYS_INLINE constexpr char padding_symbol() { return '='; } + static CPPCODEC_ALWAYS_INLINE constexpr bool is_padding_symbol(char c) { return c == '='; } + static CPPCODEC_ALWAYS_INLINE constexpr bool is_eof_symbol(char c) { return c == '\0'; } + + // RFC4648 does not specify any whitespace being allowed in base64 encodings. + static CPPCODEC_ALWAYS_INLINE constexpr bool should_ignore(char) { return false; } +}; + +} // namespace detail + +using base64_rfc4648 = detail::codec>; + +} // namespace cppcodec + +#endif // CPPCODEC_BASE64_RFC4648 \ No newline at end of file diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index f59cbf8..888adbb 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -19,5 +19,6 @@ namespace QuickMedia { bool string_starts_with(const std::string &str, const char *sub); bool string_ends_with(const std::string &str, const std::string &ends_with_str); size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len); + char to_upper(char c); bool strcase_equals(const char *str1, const char *str2); } \ No newline at end of file diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index e2b4671..f09c7bb 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -30,8 +30,9 @@ namespace QuickMedia { FourchanThreadPage(Program *program, std::string board_id, std::string thread_id) : ImageBoardThreadPage(program, std::move(board_id), std::move(thread_id)) {} PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg) override; - PostResult post_comment(const std::string &captcha_id, const std::string &comment, const std::string &filepath = "") override; + PostResult post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath = "") override; const std::string& get_pass_id() override; + PluginResult request_captcha_challenge(ImageBoardCaptchaChallenge &challenge_response) override; private: std::string pass_id; }; diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index 651eabf..835d92a 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -14,6 +14,13 @@ namespace QuickMedia { ERR }; + struct ImageBoardCaptchaChallenge { + std::string challenge_id; + std::string img_data; + std::string bg_data; // optional + int ttl = 0; // optional + }; + class ImageBoardThreadPage : public VideoPage { public: ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : VideoPage(program, ""), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {} @@ -27,9 +34,12 @@ namespace QuickMedia { } virtual PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg); // If |filepath| is empty then no file is uploaded - virtual PostResult post_comment(const std::string &captcha_id, const std::string &comment, const std::string &filepath = "") = 0; + virtual PostResult post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath = "") = 0; virtual const std::string& get_pass_id(); + // |bg_data|, |bg_size| and |ttl| are optional + virtual PluginResult request_captcha_challenge(ImageBoardCaptchaChallenge &challenge_response) = 0; + const std::string board_id; const std::string thread_id; }; 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("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) { - return AsyncTask([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 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 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 google_captcha_post_solution(const std::string &api_key, const std::string &captcha_id, std::array selected_images, PostSolutionResponse solution_response_callback) { - return AsyncTask([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 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 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 f8566fa..5af793f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -54,7 +54,6 @@ #include #include -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(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(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 captcha_request_future; - AsyncTask captcha_post_solution_future; - AsyncTask post_comment_future; - AsyncTask 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::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 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 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 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([&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([&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 comment_navigation_stack; std::deque 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([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(); + 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 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(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 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 #include #include @@ -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 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(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 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) { -- cgit v1.2.3