#include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" #include "../../include/ImageUtils.hpp" #include #include #include #include // TODO: Update avatar/display name when its changed in the room/globally. // Send read receipt to server and receive notifications in /sync and show the notifications. // Delete messages. // Edit messages. // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints // TODO: POST /_matrix/client/r0/rooms/{roomId}/read_markers after 5 seconds of receiving a message when the client is focused // to mark messages as read // When reaching top/bottom message, show older/newer messages. // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. namespace QuickMedia { Matrix::Matrix() : Plugin("matrix") { } PluginResult Matrix::get_cached_sync(BodyItems &result_items) { /* Path sync_cache_path = get_cache_dir().join(name).join("sync.json"); Json::Value root; if(!read_file_as_json(sync_cache_path, root)) return PluginResult::ERR; return sync_response_to_body_items(root, result_items); */ (void)result_items; return PluginResult::OK; } PluginResult Matrix::sync() { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token }, { "-m", "35" } }; std::string server_response; // timeout=30000, filter=0. First sync should be without filter and timeout=0, then all other sync should be with timeout=30000 and filter=0. // GET https://glowers.club/_matrix/client/r0/user/%40dec05eba%3Aglowers.club/filter/0 first to check if the filter is available // and if lazy load members is available and get limit to use with https://glowers.club/_matrix/client/r0/rooms/!oSXkiqBKooDcZsmiGO%3Aglowers.club/ // when first launching the client. This call to /rooms/ should be called before /sync/, when accessing a room. But only the first time // (for the session). // Note: the first sync call with always exclude since= (next_batch) because we want to receive the latest messages in a room, // which is important if we for example login to matrix after having not been online for several days and there are many new messages. // We should be shown the latest messages first and if the user wants to see older messages then they should scroll up. // Note: missed mentions are received in /sync and they will remain in new /sync unless we send a read receipt that we have read them. char url[512]; if(next_batch.empty()) snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0&full_state=true", homeserver.c_str()); else snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); 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 sync response parse error: %s\n", json_errors.c_str()); return PluginResult::ERR; } PluginResult result = sync_response_to_body_items(json_root); if(result != PluginResult::OK) return result; const Json::Value &next_batch_json = json_root["next_batch"]; if(next_batch_json.isString()) { next_batch = next_batch_json.asString(); fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); } else { fprintf(stderr, "Matrix: missing next batch\n"); } // TODO: Only create the first time sync is called? /* Path sync_cache_path = get_cache_dir().join(name); if(create_directory_recursive(sync_cache_path) == 0) { sync_cache_path.join("sync.json"); if(!save_json_to_file_atomic(sync_cache_path, json_root)) { fprintf(stderr, "Warning: failed to save sync response to %s\n", sync_cache_path.data.c_str()); } } else { fprintf(stderr, "Warning: failed to create directory: %s\n", sync_cache_path.data.c_str()); } */ return PluginResult::OK; } PluginResult Matrix::get_joined_rooms(BodyItems &result_items) { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/joined_rooms", 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 joined rooms response parse error: %s\n", json_errors.c_str()); return PluginResult::ERR; } if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &joined_rooms_json = json_root["joined_rooms"]; if(!joined_rooms_json.isArray()) return PluginResult::ERR; for(const Json::Value &room_id_json : joined_rooms_json) { if(!room_id_json.isString()) continue; std::string room_id_str = room_id_json.asString(); std::string room_name; std::string avatar_url; auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_name = room_id_str; fprintf(stderr, "Missing room %s from /sync, adding in joined_rooms\n", room_id_str.c_str()); } else { room_name = room_it->second->name; if(room_name.empty()) room_name = room_id_str; avatar_url = room_it->second->avatar_url; } auto body_item = std::make_unique(std::move(room_name)); body_item->url = room_id_str; body_item->thumbnail_url = std::move(avatar_url); result_items.push_back(std::move(body_item)); } return PluginResult::OK; } static void room_messages_to_body_items(RoomData *room_data, Message *messages, size_t num_messages, BodyItems &result_items) { for(size_t i = 0; i < num_messages; ++i) { const UserInfo &user_info = room_data->user_info[messages[i].user_id]; auto body_item = std::make_unique(""); body_item->set_author(user_info.display_name); body_item->set_description(messages[i].body); if(!messages[i].thumbnail_url.empty()) body_item->thumbnail_url = messages[i].thumbnail_url; else if(!messages[i].url.empty() && messages->type == MessageType::IMAGE) body_item->thumbnail_url = messages[i].url; else body_item->thumbnail_url = user_info.avatar_url; // TODO: Show image thumbnail inline instead of url to image and showing it as the thumbnail of the body item body_item->url = messages[i].url; body_item->author_color = user_info.display_name_color; result_items.push_back(std::move(body_item)); } } // TODO: Merge common code with |get_new_room_messages| PluginResult Matrix::get_all_synced_room_messages(const std::string &room_id, BodyItems &result_items) { auto room_it = room_data_by_id.find(room_id); if(room_it == room_data_by_id.end()) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } if(!room_it->second->initial_fetch_finished) { PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); if(result == PluginResult::OK) { room_it->second->initial_fetch_finished = true; } else { fprintf(stderr, "Initial sync failed for room: %s\n", room_id.c_str()); return result; } } room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data(), room_it->second->messages.size(), result_items); room_it->second->last_read_index = room_it->second->messages.size(); return PluginResult::OK; } PluginResult Matrix::get_new_room_messages(const std::string &room_id, BodyItems &result_items) { auto room_it = room_data_by_id.find(room_id); if(room_it == room_data_by_id.end()) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } if(!room_it->second->initial_fetch_finished) { PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); if(result == PluginResult::OK) { room_it->second->initial_fetch_finished = true; } else { fprintf(stderr, "Initial sync failed for room: %s\n", room_id.c_str()); return result; } } size_t num_new_messages = room_it->second->messages.size() - room_it->second->last_read_index; room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data() + room_it->second->last_read_index, num_new_messages, result_items); room_it->second->last_read_index = room_it->second->messages.size(); return PluginResult::OK; } PluginResult Matrix::get_previous_room_messages(const std::string &room_id, BodyItems &result_items) { auto room_it = room_data_by_id.find(room_id); if(room_it == room_data_by_id.end()) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } size_t num_messages_before = room_it->second->messages.size(); PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); if(result != PluginResult::OK) return result; size_t num_messages_after = room_it->second->messages.size(); size_t num_new_messages = num_messages_after - num_messages_before; room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data(), num_new_messages, result_items); return PluginResult::OK; } PluginResult Matrix::sync_response_to_body_items(const Json::Value &root) { if(!root.isObject()) return PluginResult::ERR; const Json::Value &rooms_json = root["rooms"]; if(!rooms_json.isObject()) return PluginResult::OK; const Json::Value &join_json = rooms_json["join"]; if(!join_json.isObject()) return PluginResult::OK; for(Json::Value::const_iterator it = join_json.begin(); it != join_json.end(); ++it) { if(!it->isObject()) continue; Json::Value room_id = it.key(); if(!room_id.isString()) continue; std::string room_id_str = room_id.asString(); auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_it = room_data_by_id.find(room_id_str); // TODO: Get iterator from above insert } const Json::Value &state_json = (*it)["state"]; if(!state_json.isObject()) continue; const Json::Value &events_json = state_json["events"]; events_add_user_info(events_json, room_it->second.get()); events_set_room_name(events_json, room_it->second.get()); } for(Json::Value::const_iterator it = join_json.begin(); it != join_json.end(); ++it) { if(!it->isObject()) continue; Json::Value room_id = it.key(); if(!room_id.isString()) continue; std::string room_id_str = room_id.asString(); auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_it = room_data_by_id.find(room_id_str); // TODO: Get iterator from above insert } const Json::Value &timeline_json = (*it)["timeline"]; if(!timeline_json.isObject()) continue; if(room_it->second->prev_batch.empty()) { // This may be non-existent if this is the first event in the room const Json::Value &prev_batch_json = timeline_json["prev_batch"]; if(prev_batch_json.isString()) room_it->second->prev_batch = prev_batch_json.asString(); } const Json::Value &events_json = timeline_json["events"]; events_add_user_info(events_json, room_it->second.get()); events_add_messages(events_json, room_it->second.get(), MessageDirection::AFTER); events_set_room_name(events_json, room_it->second.get()); } return PluginResult::OK; } static sf::Color user_id_to_color(const std::string &user_id) { uint32_t color = 2166136261; for(unsigned char c : user_id) { color = (color * 16777619) ^ c; } sf::Color result = (sf::Color)color; result.r = 64 + std::max(0, (int)result.r - 64); result.g = 64 + std::max(0, (int)result.g - 64); result.b = 64 + std::max(0, (int)result.b - 64); result.a = 255; return result; } void Matrix::events_add_user_info(const Json::Value &events_json, RoomData *room_data) { if(!events_json.isArray()) return; for(const Json::Value &event_item_json : events_json) { if(!event_item_json.isObject()) continue; const Json::Value &type_json = event_item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "m.room.member") != 0) continue; const Json::Value &sender_json = event_item_json["sender"]; if(!sender_json.isString()) continue; const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; const Json::Value &membership_json = content_json["membership"]; if(!membership_json.isString() || strcmp(membership_json.asCString(), "join") != 0) continue; const Json::Value &avatar_url_json = content_json["avatar_url"]; if(!avatar_url_json.isString()) continue; const Json::Value &display_name_json = content_json["displayname"]; if(!display_name_json.isString()) continue; std::string sender_json_str = sender_json.asString(); auto user_it = room_data->user_info_by_user_id.find(sender_json_str); if(user_it != room_data->user_info_by_user_id.end()) continue; UserInfo user_info; user_info.avatar_url = avatar_url_json.asString(); if(strncmp(user_info.avatar_url.c_str(), "mxc://", 6) == 0) user_info.avatar_url.erase(user_info.avatar_url.begin(), user_info.avatar_url.begin() + 6); // TODO: What if the user hasn't selected an avatar? user_info.avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + user_info.avatar_url + "?width=32&height=32&method=crop"; user_info.display_name = display_name_json.asString(); user_info.display_name_color = user_id_to_color(sender_json_str); room_data->user_info.push_back(std::move(user_info)); room_data->user_info_by_user_id.insert(std::make_pair(sender_json_str, room_data->user_info.size() - 1)); } } static std::string message_content_extract_thumbnail_url(const Json::Value &content_json, const std::string &homeserver) { const Json::Value &info_json = content_json["info"]; if(info_json.isObject()) { const Json::Value &thumbnail_url_json = info_json["thumbnail_url"]; if(thumbnail_url_json.isString()) { std::string thumbnail_str = thumbnail_url_json.asString(); if(strncmp(thumbnail_str.c_str(), "mxc://", 6) == 0) { thumbnail_str.erase(thumbnail_str.begin(), thumbnail_str.begin() + 6); return homeserver + "/_matrix/media/r0/download/" + std::move(thumbnail_str); } } } return ""; } void Matrix::events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir) { if(!events_json.isArray()) return; std::vector new_messages; for(const Json::Value &event_item_json : events_json) { if(!event_item_json.isObject()) continue; const Json::Value &type_json = event_item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "m.room.message") != 0) continue; const Json::Value &sender_json = event_item_json["sender"]; if(!sender_json.isString()) continue; std::string sender_json_str = sender_json.asString(); const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; const Json::Value &content_type = content_json["msgtype"]; if(!content_type.isString()) continue; auto user_it = room_data->user_info_by_user_id.find(sender_json_str); if(user_it == room_data->user_info_by_user_id.end()) { fprintf(stderr, "Warning: skipping unknown user: %s\n", sender_json_str.c_str()); continue; } const Json::Value &body_json = content_json["body"]; if(!body_json.isString()) continue; if(strcmp(content_type.asCString(), "m.text") == 0) { Message message; message.user_id = user_it->second; message.body = body_json.asString(); message.type = MessageType::TEXT; new_messages.push_back(std::move(message)); } else if(strcmp(content_type.asCString(), "m.image") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; Message message; message.user_id = user_it->second; message.body = body_json.asString(); message.url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message.thumbnail_url = message_content_extract_thumbnail_url(content_json, homeserver); message.type = MessageType::IMAGE; new_messages.push_back(std::move(message)); } else if(strcmp(content_type.asCString(), "m.video") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; Message message; message.user_id = user_it->second; message.body = body_json.asString(); message.url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message.thumbnail_url = message_content_extract_thumbnail_url(content_json, homeserver); message.type = MessageType::VIDEO; new_messages.push_back(std::move(message)); } } // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { room_data->messages.insert(room_data->messages.begin(), new_messages.rbegin(), new_messages.rend()); if(room_data->last_read_index != 0) room_data->last_read_index += new_messages.size(); } else if(message_dir == MessageDirection::AFTER) { room_data->messages.insert(room_data->messages.end(), new_messages.begin(), new_messages.end()); } } // Returns empty string on error static std::string extract_user_name_from_user_id(const std::string &user_id) { size_t index = user_id.find(':'); if(index == std::string::npos || index == 0 || user_id.empty() || user_id[0] != '@') return ""; return user_id.substr(1, index - 1); } void Matrix::events_set_room_name(const Json::Value &events_json, RoomData *room_data) { if(!events_json.isArray()) return; for(const Json::Value &event_item_json : events_json) { if(!event_item_json.isObject()) continue; const Json::Value &type_json = event_item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "m.room.name") != 0) continue; const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; const Json::Value &name_json = content_json["name"]; if(!name_json.isString()) continue; room_data->name = name_json.asString(); } for(const Json::Value &event_item_json : events_json) { if(!event_item_json.isObject()) continue; const Json::Value &type_json = event_item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "m.room.create") != 0) continue; const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; const Json::Value &creator_json = content_json["creator"]; if(!creator_json.isString()) continue; if(room_data->name.empty()) room_data->name = extract_user_name_from_user_id(creator_json.asString()); if(room_data->avatar_url.empty()) { auto user_it = room_data->user_info_by_user_id.find(creator_json.asString()); if(user_it != room_data->user_info_by_user_id.end()) room_data->avatar_url = room_data->user_info[user_it->second].avatar_url; } } for(const Json::Value &event_item_json : events_json) { if(!event_item_json.isObject()) continue; const Json::Value &type_json = event_item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "m.room.avatar") != 0) continue; const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; std::string url_json_str = url_json.asCString() + 6; room_data->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + std::move(url_json_str) + "?width=32&height=32&method=crop"; } } PluginResult Matrix::get_previous_room_messages(const std::string &room_id, RoomData *room_data) { std::string from = room_data->prev_batch; if(from.empty()) { fprintf(stderr, "Info: missing previous batch for room: %s, using /sync next batch\n", room_id.c_str()); from = next_batch; if(from.empty()) { fprintf(stderr, "Error: missing next batch!\n"); return PluginResult::OK; } } Json::Value request_data(Json::objectValue); request_data["lazy_load_members"] = true; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; std::string filter = url_param_encode(Json::writeString(builder, std::move(request_data))); char url[512]; snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/messages?from=%s&limit=20&dir=b&filter=%s", homeserver.c_str(), room_id.c_str(), from.c_str(), filter.c_str()); fprintf(stderr, "load initial room data, 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 /rooms//messages/ response parse error: %s\n", json_errors.c_str()); return PluginResult::ERR; } if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &state_json = json_root["state"]; events_add_user_info(state_json, room_data); events_set_room_name(state_json, room_data); const Json::Value &chunk_json = json_root["chunk"]; events_add_messages(chunk_json, room_data, MessageDirection::BEFORE); const Json::Value &end_json = json_root["end"]; if(!end_json.isString()) { fprintf(stderr, "Warning: matrix messages response is missing 'end', this could happen if we received the very first messages in the room\n"); return PluginResult::OK; } room_data->prev_batch = end_json.asString(); return PluginResult::OK; } SearchResult Matrix::search(const std::string&, BodyItems&) { return SearchResult::OK; } static bool generate_random_characters(char *buffer, int buffer_size) { int fd = open("/dev/urandom", O_RDONLY); if(fd == -1) { perror("/dev/urandom"); return false; } if(read(fd, buffer, buffer_size) < buffer_size) { fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size); close(fd); return false; } close(fd); return true; } static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) { std::ostringstream result; result << std::hex; for(int i = 0; i < buffer_size; ++i) result << (int)(unsigned char)buffer[i]; return result.str(); } 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; std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); std::string formatted_body; bool contains_formatted_text = false; 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"] = 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"; builder["indentation"] = ""; std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", Json::writeString(builder, std::move(request_data)) } }; 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(request_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 post message response parse error: %s\n", json_errors.c_str()); return PluginResult::ERR; } if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &event_id_json = json_root["event_id"]; if(!event_id_json.isString()) return PluginResult::ERR; fprintf(stderr, "Matrix post message, response event id: %s\n", event_id_json.asCString()); 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); } PluginResult Matrix::login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg) { Json::Value identifier_json(Json::objectValue); identifier_json["type"] = "m.id.user"; // TODO: What if the server doesn't support this login type? redirect to sso web page etc identifier_json["user"] = username; Json::Value request_data(Json::objectValue); request_data["type"] = "m.login.password"; request_data["identifier"] = std::move(identifier_json); request_data["password"] = password; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; std::vector additional_args = { { "-X", "POST" }, { "-H", "content-type: application/json" }, { "--data-binary", Json::writeString(builder, std::move(request_data)) } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/login", server_response, std::move(additional_args), use_tor, true, false) != DownloadResult::OK) { err_msg = std::move(server_response); 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)) { err_msg = "Matrix login response parse error: " + json_errors; return PluginResult::ERR; } if(!json_root.isObject()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } const Json::Value &error = json_root["error"]; if(error.isString()) { err_msg = error.asString(); return PluginResult::ERR; } const Json::Value &user_id_json = json_root["user_id"]; if(!user_id_json.isString()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } const Json::Value &access_token_json = json_root["access_token"]; if(!access_token_json.isString()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } // Use the user-provided homeserver instead of the one the server tells us about, otherwise this wont work with a proxy // such as pantalaimon json_root["homeserver"] = homeserver; this->user_id = user_id_json.asString(); this->access_token = access_token_json.asString(); this->homeserver = homeserver; // TODO: Handle well_known field. The spec says clients SHOULD handle it if its provided Path session_path = get_storage_dir().join(name); if(create_directory_recursive(session_path) == 0) { session_path.join("session.json"); if(!save_json_to_file_atomic(session_path, json_root)) { fprintf(stderr, "Warning: failed to save login response to %s\n", session_path.data.c_str()); } } else { fprintf(stderr, "Warning: failed to create directory: %s\n", session_path.data.c_str()); } return PluginResult::OK; } PluginResult Matrix::logout() { Path session_path = get_storage_dir().join(name).join("session.json"); remove(session_path.data.c_str()); std::vector additional_args = { { "-X", "POST" }, { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/logout", server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) return PluginResult::NET_ERR; // Make sure all fields are reset here! room_data_by_id.clear(); user_id.clear(); access_token.clear(); homeserver.clear(); next_batch.clear(); return PluginResult::OK; } PluginResult Matrix::load_and_verify_cached_session() { Path session_path = get_storage_dir().join(name).join("session.json"); std::string session_json_content; if(file_get_content(session_path, session_json_content) != 0) { fprintf(stderr, "Info: failed to read matrix session from %s. Either its missing or we failed to read the file\n", session_path.data.c_str()); 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(&session_json_content[0], &session_json_content[session_json_content.size()], &json_root, &json_errors)) { fprintf(stderr, "Matrix cached session parse error: %s\n", json_errors.c_str()); return PluginResult::ERR; } if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &user_id_json = json_root["user_id"]; if(!user_id_json.isString()) { fprintf(stderr, "Failed to parse matrix cached session response\n"); return PluginResult::ERR; } const Json::Value &access_token_json = json_root["access_token"]; if(!access_token_json.isString()) { fprintf(stderr, "Failed to parse matrix cached session response\n"); return PluginResult::ERR; } const Json::Value &homeserver_json = json_root["homeserver"]; if(!homeserver_json.isString()) { fprintf(stderr, "Failed to parse matrix cached session response\n"); return PluginResult::ERR; } std::string user_id = user_id_json.asString(); std::string access_token = access_token_json.asString(); std::string homeserver = homeserver_json.asString(); std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; // We want to make any request to the server that can verify that our token is still valid, doesn't matter which call if(download_to_string(homeserver + "/_matrix/client/r0/account/whoami", server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) { fprintf(stderr, "Matrix whoami response: %s\n", server_response.c_str()); return PluginResult::NET_ERR; } this->user_id = std::move(user_id); this->access_token = std::move(access_token); this->homeserver = std::move(homeserver); return PluginResult::OK; } PluginResult Matrix::on_start_typing(const std::string &room_id) { Json::Value request_data(Json::objectValue); request_data["typing"] = true; request_data["timeout"] = 30000; // 30 sec timeout Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "--data-binary", Json::writeString(builder, std::move(request_data)) }, { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/typing/" + url_param_encode(user_id) , server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) return PluginResult::NET_ERR; return PluginResult::OK; } PluginResult Matrix::on_stop_typing(const std::string &room_id) { Json::Value request_data(Json::objectValue); request_data["typing"] = false; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "--data-binary", Json::writeString(builder, std::move(request_data)) }, { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/typing/" + url_param_encode(user_id), server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) return PluginResult::NET_ERR; return PluginResult::OK; } }