From 6b347e7310c501b826785e9639d962ba1d448b4b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 23 Sep 2020 00:56:54 +0200 Subject: Add matrix image upload --- include/ImageUtils.hpp | 10 ++- include/QuickMedia.hpp | 2 + include/Storage.hpp | 1 + plugins/Matrix.hpp | 12 +++- src/Body.cpp | 7 +- src/ImageUtils.cpp | 15 +++-- src/QuickMedia.cpp | 90 +++++++++++++++++++------- src/Storage.cpp | 40 ++++++++++-- src/plugins/FileManager.cpp | 35 +++++++--- src/plugins/Matrix.cpp | 152 ++++++++++++++++++++++++++++++++++++++------ 10 files changed, 298 insertions(+), 66 deletions(-) diff --git a/include/ImageUtils.hpp b/include/ImageUtils.hpp index 58eb197..f5f757e 100644 --- a/include/ImageUtils.hpp +++ b/include/ImageUtils.hpp @@ -3,7 +3,13 @@ #include "Path.hpp" namespace QuickMedia { - // Works with jpg, png and gif files - bool image_get_resolution(const Path &path, int *width, int *height); + enum class ImageType { + JPG, + PNG, + GIF + }; + + // Works with jpg, png and gif files. |image_type| can be NULL + bool image_get_resolution(const Path &path, int *width, int *height, ImageType *image_type = nullptr); bool is_image_ext(const char *ext); } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 6afdfce..18f3a6b 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -21,6 +21,7 @@ namespace QuickMedia { class Plugin; + class FileManager; class Manga; enum class ImageViewMode { @@ -122,5 +123,6 @@ namespace QuickMedia { ImageViewMode image_view_mode = ImageViewMode::SINGLE; Body *related_media_body; std::vector selected_files; + FileManager *file_manager = nullptr; }; } \ No newline at end of file diff --git a/include/Storage.hpp b/include/Storage.hpp index cdae7b0..38c083e 100644 --- a/include/Storage.hpp +++ b/include/Storage.hpp @@ -22,6 +22,7 @@ namespace QuickMedia { int create_directory_recursive(const Path &path); FileType get_file_type(const Path &path); int file_get_content(const Path &path, std::string &result); + int file_get_size(const Path &path, size_t *size); int file_overwrite(const Path &path, const std::string &data); void for_files_in_dir(const Path &path, FileIteratorCallback callback); void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback); diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index b2e1711..50f8c6b 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -25,6 +25,13 @@ namespace QuickMedia { MessageType type; }; + struct MessageInfo { + int size = 0; + int w = 0; + int h = 0; + const char* mimetype = nullptr; + }; + struct RoomData { // Each room has its own list of user data, even if multiple rooms has the same user // because users can have different display names and avatars in different rooms. @@ -59,7 +66,10 @@ namespace QuickMedia { PluginResult get_room_messages(const std::string &room_id, size_t start_index, BodyItems &result_items, size_t &num_new_messages); SearchResult search(const std::string &text, BodyItems &result_items) override; - PluginResult post_message(const std::string &room_id, const std::string &text); + // |url| should only be set when uploading media. + // TODO: Make api better. + PluginResult post_message(const std::string &room_id, const std::string &body, const std::string &url = "", MessageType msgtype = MessageType::TEXT, MessageInfo *info = nullptr); + PluginResult post_file(const std::string &room_id, const std::string &filepath); PluginResult login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg); PluginResult load_and_verify_cached_session(); diff --git a/src/Body.cpp b/src/Body.cpp index ae60da2..d650d20 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -285,9 +285,12 @@ namespace QuickMedia { if(new_image_size.x < image->getSize().x || new_image_size.y < image->getSize().y) { sf::Image destination_image; copy_resize(*image, destination_image, new_image_size); - image.reset(); - if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url))) + if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url))) { + image.reset(); result->loadFromImage(destination_image); + } else { + result->loadFromImage(*image); + } loading_thumbnail = false; return; } else { diff --git a/src/ImageUtils.cpp b/src/ImageUtils.cpp index 5f493d1..a0b2c1e 100644 --- a/src/ImageUtils.cpp +++ b/src/ImageUtils.cpp @@ -98,7 +98,7 @@ namespace QuickMedia { // TODO: Also support exif files. Exif files are containers for jpeg, tiff etc so once tiff has been implemented so can exif. // Exif header is similar to jpeg. - bool image_get_resolution(const Path &path, int *width, int *height) { + bool image_get_resolution(const Path &path, int *width, int *height, ImageType *image_type) { FILE *file = fopen(path.data.c_str(), "rb"); if(!file) return false; @@ -107,12 +107,19 @@ namespace QuickMedia { size_t bytes_read = fread(data, 1, sizeof(data), file); fclose(file); - if(png_get_size(data, bytes_read, width, height)) + if(png_get_size(data, bytes_read, width, height)) { + if(image_type) + *image_type = ImageType::PNG; return true; - else if(gif_get_size(data, bytes_read, width, height)) + } else if(gif_get_size(data, bytes_read, width, height)) { + if(image_type) + *image_type = ImageType::GIF; return true; - else if(jpeg_get_size(data, bytes_read, width, height)) + } else if(jpeg_get_size(data, bytes_read, width, height)) { + if(image_type) + *image_type = ImageType::JPG; return true; + } return false; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index e7a310d..a2fd42b 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -216,9 +216,14 @@ namespace QuickMedia { } else { running = false; } - delete related_media_body; - delete body; - delete current_plugin; + if(related_media_body) + delete related_media_body; + if(body) + delete body; + if(file_manager) + delete file_manager; + if(current_plugin) + delete current_plugin; if(disp) XCloseDisplay(disp); } @@ -310,7 +315,7 @@ namespace QuickMedia { plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "matrix") == 0) { current_plugin = new Matrix(); - plugin_logo_path = resources_root + "images/matrix_logo.png"; + //plugin_logo_path = resources_root + "images/matrix_logo.png"; } else if(strcmp(argv[i], "file-manager") == 0) { current_plugin = new FileManager(); } else if(strcmp(argv[i], "dmenu") == 0) { @@ -359,6 +364,7 @@ namespace QuickMedia { if(current_plugin->name == "file-manager") { current_page = Page::FILE_MANAGER; + file_manager = static_cast(current_plugin); } else { if(start_dir) { fprintf(stderr, "Option --dir is only valid with file-manager\n"); @@ -444,7 +450,6 @@ namespace QuickMedia { fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); current_page = Page::CHAT_LOGIN; } - search_placeholder = "Send a message..."; } if(search_placeholder.empty()) @@ -2489,12 +2494,9 @@ namespace QuickMedia { void Program::file_manager_page() { selected_files.clear(); int prev_autosearch_delay = search_bar->text_autosearch_delay; - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); + search_bar->text_autosearch_delay = file_manager->get_search_delay(); Page previous_page = pop_page_stack(); - assert(current_plugin->name == "file-manager"); - FileManager *file_manager = static_cast(current_plugin); - sf::Text current_dir_text(file_manager->get_current_dir().string(), bold_font, 18); // TODO: Make asynchronous. @@ -2514,7 +2516,6 @@ namespace QuickMedia { if(!selected_item) return false; - FileManager *file_manager = static_cast(current_plugin); if(file_manager->set_child_directory(selected_item->get_title())) { std::string current_dir_str = file_manager->get_current_dir().string(); current_dir_text.setString(current_dir_str); @@ -3200,14 +3201,56 @@ namespace QuickMedia { // TODO: Allow empty initial room (if the user hasn't joined any room yet) assert(!current_room_id.empty()); + { + std::string plugin_logo_path = resources_root + "images/matrix_logo.png"; + if(!plugin_logo.loadFromFile(plugin_logo_path)) { + show_notification("QuickMedia", "Failed to load plugin logo, path: " + plugin_logo_path, Urgency::CRITICAL); + exit(1); + } + plugin_logo.generateMipmap(); + plugin_logo.setSmooth(true); + } + + SearchBar chat_input(font, &plugin_logo, "Send a message..."); + // TODO: Filer for rooms and settings - search_bar->onTextUpdateCallback = nullptr; + chat_input.onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = [matrix, &tabs, &selected_tab, &room_message_index, ¤t_room_id](const std::string &text) -> bool { + // TODO: Show post message immediately, instead of waiting for sync. Otherwise it can take a while until we receive the message, + // which happens when uploading an image. + chat_input.onTextSubmitCallback = [this, matrix, &tabs, &selected_tab, &room_message_index, ¤t_room_id](const std::string &text) -> bool { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { if(text.empty()) return false; + if(text[0] == '/') { + std::string command = text; + strip(command); + if(command == "/upload") { + if(!file_manager) + file_manager = new FileManager(); + page_stack.push(Page::CHAT); + current_page = Page::FILE_MANAGER; + file_manager_page(); + if(selected_files.empty()) { + fprintf(stderr, "No files selected!\n"); + return true; + } else { + // TODO: Make asynchronous. + // TODO: Upload multiple files. + if(matrix->post_file(current_room_id, selected_files[0]) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to upload image to room", Urgency::CRITICAL); + return false; + } else { + return true; + } + } + } else { + fprintf(stderr, "Error: invalid command: %s, expected /upload\n", command.c_str()); + return false; + } + } + // TODO: Make asynchronous if(matrix->post_message(current_room_id, text) != PluginResult::OK) { show_notification("QuickMedia", "Failed to post matrix message", Urgency::CRITICAL); @@ -3262,7 +3305,7 @@ namespace QuickMedia { while (current_page == Page::CHAT) { while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false); + base_event_handler(event, Page::EXIT, false, false, false); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { redraw = true; } else if(event.type == sf::Event::KeyPressed) { @@ -3274,17 +3317,16 @@ namespace QuickMedia { current_page = Page::EXIT; body->clear_items(); body->reset_selected(); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Left) { tabs[selected_tab].body->filter_search_fuzzy(""); tabs[selected_tab].body->clamp_selection(); selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); + chat_input.clear(); } else if(event.key.code == sf::Keyboard::Right) { tabs[selected_tab].body->filter_search_fuzzy(""); tabs[selected_tab].body->clamp_selection(); selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); + chat_input.clear(); } if(tabs[selected_tab].type == ChatTabType::MESSAGES && event.key.control && event.key.code == sf::Keyboard::I) { @@ -3302,12 +3344,16 @@ namespace QuickMedia { video_content_page(); } } + + if(event.type == sf::Event::TextEntered) + chat_input.onTextEntered(event.text.unicode); + chat_input.on_event(event); } if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - search_bar->set_vertical_position(window_size.y - search_bar->getBottomWithoutShadow()); + chat_input.onWindowResize(window_size); + chat_input.set_vertical_position(window_size.y - chat_input.getBottomWithoutShadow()); float body_padding_horizontal = 25.0f; float body_padding_vertical = 25.0f; @@ -3318,10 +3364,10 @@ namespace QuickMedia { body_padding_vertical = 10.0f; } - float search_bottom = search_bar->getBottomWithoutShadow(); + float search_bottom = chat_input.getBottomWithoutShadow(); body_pos = sf::Vector2f(body_padding_horizontal, body_padding_vertical + tab_height); body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_height); - //get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); + //get_body_dimensions(window_size, &chat_input, body_pos, body_size, true); } if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) { @@ -3361,7 +3407,7 @@ namespace QuickMedia { sync_running = false; } - search_bar->update(); + chat_input.update(); window.clear(back_color); @@ -3391,7 +3437,7 @@ namespace QuickMedia { tab_drop_shadow.setPosition(0.0f, std::floor(tab_vertical_offset + tab_height)); window.draw(tab_drop_shadow); - search_bar->draw(window, false); + chat_input.draw(window, false); window.display(); } diff --git a/src/Storage.cpp b/src/Storage.cpp index 0c3479a..f82aba3 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -121,6 +121,16 @@ namespace QuickMedia { return 0; } + int file_get_size(const Path &path, size_t *size) { + struct stat file_stat; + if(stat(path.data.c_str(), &file_stat) == 0 && S_ISREG(file_stat.st_mode)) { + *size = file_stat.st_size; + return 0; + } + *size = 0; + return -1; + } + int file_overwrite(const Path &path, const std::string &data) { FILE *file = fopen(path.data.c_str(), "wb"); if(!file) @@ -135,20 +145,38 @@ namespace QuickMedia { } void for_files_in_dir(const Path &path, FileIteratorCallback callback) { - for(auto &p : std::filesystem::directory_iterator(path.data)) { - if(!callback(p.path())) - break; + try { + for(auto &p : std::filesystem::directory_iterator(path.data)) { + if(!callback(p.path())) + break; + } + } catch(const std::filesystem::filesystem_error &err) { + fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what()); + return; + } + } + + static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) { + try { + return path.last_write_time(); + } catch(const std::filesystem::filesystem_error &err) { + return default_value; } } void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback) { std::vector paths; - for(auto &p : std::filesystem::directory_iterator(path.data)) { - paths.push_back(p); + try { + for(auto &p : std::filesystem::directory_iterator(path.data)) { + paths.push_back(p); + } + } catch(const std::filesystem::filesystem_error &err) { + fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what()); + return; } std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) { - return path1.last_write_time() > path2.last_write_time(); + return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min()); }); for(auto &p : paths) { diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp index fc6205c..c68bb94 100644 --- a/src/plugins/FileManager.cpp +++ b/src/plugins/FileManager.cpp @@ -18,23 +18,40 @@ namespace QuickMedia { return ""; } + static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) { + try { + return path.last_write_time(); + } catch(const std::filesystem::filesystem_error &err) { + return default_value; + } + } + PluginResult FileManager::get_files_in_directory(BodyItems &result_items) { + std::vector paths; try { for(auto &p : std::filesystem::directory_iterator(current_dir)) { - auto body_item = std::make_unique(p.path().filename().string()); - if(p.is_regular_file()) { - if(is_image_ext(get_ext(p.path()))) { - body_item->thumbnail_is_local = true; - body_item->thumbnail_url = p.path().string(); - } - } - result_items.push_back(std::move(body_item)); + paths.push_back(p); } - return PluginResult::OK; } catch(const std::filesystem::filesystem_error &err) { fprintf(stderr, "Failed to list files in directory %s, error: %s\n", current_dir.c_str(), err.what()); return PluginResult::ERR; } + + std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) { + return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min()); + }); + + for(auto &p : paths) { + auto body_item = std::make_unique(p.path().filename().string()); + // TODO: Check file magic number instead of extension? + if(p.is_regular_file() && is_image_ext(get_ext(p.path()))) { + body_item->thumbnail_is_local = true; + body_item->thumbnail_url = p.path().string(); + } + result_items.push_back(std::move(body_item)); + } + + return PluginResult::OK; } bool FileManager::set_current_directory(const std::string &path) { diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index ff854c5..e806f39 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1,5 +1,6 @@ #include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" +#include "../../include/ImageUtils.hpp" #include #include #include @@ -557,7 +558,16 @@ namespace QuickMedia { return result.str(); } - PluginResult Matrix::post_message(const std::string &room_id, const std::string &text) { + static const char* message_type_to_request_msg_type_str(MessageType msgtype) { + switch(msgtype) { + case MessageType::TEXT: return "m.text"; + case MessageType::IMAGE: return "m.image"; + case MessageType::VIDEO: return "m.video"; + } + return "m.text"; + } + + PluginResult Matrix::post_message(const std::string &room_id, const std::string &body, const std::string &url, MessageType msgtype, MessageInfo *info) { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) return PluginResult::ERR; @@ -566,27 +576,40 @@ namespace QuickMedia { std::string formatted_body; bool contains_formatted_text = false; - string_split(text, '\n', [&formatted_body, &contains_formatted_text](const char *str, size_t size){ - if(size > 0 && str[0] == '>') { - std::string line(str, size); - html_escape_sequences(line); - formatted_body += ""; - formatted_body += line; - formatted_body += ""; - contains_formatted_text = true; - } else { - formatted_body.append(str, size); - } - return true; - }); + if(msgtype == MessageType::TEXT) { + string_split(body, '\n', [&formatted_body, &contains_formatted_text](const char *str, size_t size){ + if(size > 0 && str[0] == '>') { + std::string line(str, size); + html_escape_sequences(line); + formatted_body += ""; + formatted_body += line; + formatted_body += ""; + contains_formatted_text = true; + } else { + formatted_body.append(str, size); + } + return true; + }); + } Json::Value request_data(Json::objectValue); - request_data["msgtype"] = "m.text"; - request_data["body"] = text; + request_data["msgtype"] = message_type_to_request_msg_type_str(msgtype); + request_data["body"] = body; if(contains_formatted_text) { request_data["format"] = "org.matrix.custom.html"; request_data["formatted_body"] = std::move(formatted_body); } + if(msgtype == MessageType::IMAGE) { + if(info) { + Json::Value info_json(Json::objectValue); + info_json["size"] = info->size; + info_json["w"] = info->w; + info_json["h"] = info->h; + info_json["mimetype"] = info->mimetype; + request_data["info"] = std::move(info_json); + } + request_data["url"] = url; + } Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; @@ -599,12 +622,12 @@ namespace QuickMedia { { "--data-binary", Json::writeString(builder, request_data) } }; - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/send/m.room.message/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str()); - fprintf(stderr, "Post message to |%s|\n", url); + char request_url[512]; + snprintf(request_url, sizeof(request_url), "%s/_matrix/client/r0/rooms/%s/send/m.room.message/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str()); + fprintf(stderr, "Post message to |%s|\n", request_url); std::string server_response; - if(download_to_string(url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + if(download_to_string(request_url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) return PluginResult::NET_ERR; if(server_response.empty()) @@ -630,6 +653,95 @@ namespace QuickMedia { return PluginResult::OK; } + // Returns empty string on error + static const char* file_get_filename(const std::string &filepath) { + size_t index = filepath.rfind('/'); + if(index == std::string::npos) + return ""; + const char *filename = filepath.c_str() + index + 1; + if(filename[0] == '\0') + return ""; + return filename; + } + + static const char* image_type_to_mimetype(ImageType image_type) { + switch(image_type) { + case ImageType::PNG: return "image/png"; + case ImageType::GIF: return "image/gif"; + case ImageType::JPG: return "image/jpeg"; + } + return "application/octet-stream"; + } + + PluginResult Matrix::post_file(const std::string &room_id, const std::string &filepath) { + int image_width, image_height; + ImageType image_type; + if(!image_get_resolution(filepath, &image_width, &image_height, &image_type)) { + fprintf(stderr, "Failed to get resolution of image: %s. Only image uploads are currently supported\n", filepath.c_str()); + return PluginResult::ERR; + } + + const char *mimetype = image_type_to_mimetype(image_type); + + // TODO: What if the file changes after this? is the file size really needed? + size_t file_size; + if(file_get_size(filepath, &file_size) != 0) { + fprintf(stderr, "Failed to get size of image: %s\n", filepath.c_str()); + return PluginResult::ERR; + } + + // TODO: Check server file limit first: GET https://glowers.club/_matrix/media/r0/config + // and also have a sane limit client-side. + if(file_size > 100 * 1024 * 1024) { + fprintf(stderr, "Upload file size it too large!, max size is currently 100mb\n"); + return PluginResult::ERR; + } + + std::vector additional_args = { + { "-X", "POST" }, + { "-H", std::string("content-type: ") + mimetype }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", "@" + filepath } + }; + + const char *filename = file_get_filename(filepath); + + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", homeserver.c_str(), filename); + fprintf(stderr, "Upload url: |%s|\n", url); + + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + return PluginResult::NET_ERR; + + if(server_response.empty()) + return PluginResult::ERR; + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Matrix upload response parse error: %s\n", json_errors.c_str()); + return PluginResult::ERR; + } + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &content_uri_json = json_root["content_uri"]; + if(!content_uri_json.isString()) + return PluginResult::ERR; + + fprintf(stderr, "Matrix upload, response content uri: %s\n", content_uri_json.asCString()); + MessageInfo message_info; + message_info.size = file_size; + message_info.w = image_width; + message_info.h = image_height; + message_info.mimetype = mimetype; + return post_message(room_id, filename, content_uri_json.asString(), MessageType::IMAGE, &message_info); + } + static std::string parse_login_error_response(std::string json_str) { if(json_str.empty()) return "Unknown error"; -- cgit v1.2.3