#include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" #include "../../include/StringUtils.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. // TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync) static const char* SERVICE_NAME = "matrix"; namespace QuickMedia { std::shared_ptr RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard lock(room_mutex); auto user_it = user_info_by_user_id.find(user_id); if(user_it == user_info_by_user_id.end()) return nullptr; return user_it->second; } void RoomData::add_user(std::shared_ptr user) { std::lock_guard lock(room_mutex); user_info_by_user_id.insert(std::make_pair(user->user_id, user)); } void RoomData::set_user_read_marker(std::shared_ptr &user, const std::string &event_id) { std::lock_guard lock(user_mutex); user->read_marker_event_id = event_id; } std::string RoomData::get_user_read_marker(std::shared_ptr &user) { std::lock_guard lock(user_mutex); return user->read_marker_event_id; } size_t RoomData::prepend_messages_reverse(std::vector> new_messages) { std::lock_guard lock(room_mutex); size_t num_inserted = 0; for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); messages.insert(messages.begin(), std::move(*it)); ++num_inserted; } } return num_inserted; } size_t RoomData::append_messages(std::vector> new_messages) { std::lock_guard lock(room_mutex); size_t num_inserted = 0; for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); messages.push_back(std::move(*it)); ++num_inserted; } } return num_inserted; } std::shared_ptr RoomData::get_message_by_id(const std::string &id) { std::lock_guard lock(room_mutex); auto message_it = message_by_event_id.find(id); if(message_it == message_by_event_id.end()) return nullptr; return message_it->second; } std::vector> RoomData::get_users_excluding_me(const std::string &my_user_id) { std::lock_guard lock(user_mutex); std::vector> users_excluding_me; for(auto &[user_id, user] : user_info_by_user_id) { if(user->user_id != my_user_id) { users_excluding_me.push_back(user); } } return users_excluding_me; } void RoomData::acquire_room_lock() { room_mutex.lock(); } void RoomData::release_room_lock() { room_mutex.unlock(); } const std::vector>& RoomData::get_messages_thread_unsafe() const { return messages; } PluginResult Matrix::sync(RoomSyncMessages &room_messages) { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token }, { "-m", "35" } }; char url[512]; if(next_batch.empty()) snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); else snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); PluginResult result = sync_response_to_body_items(json_root, room_messages); 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"); } return PluginResult::OK; } PluginResult Matrix::get_joined_rooms(BodyItems &result_items) { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; Json::Value json_root; DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/joined_rooms", std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); 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 = get_room_by_id(room_id_str); if(!room) { room = std::make_shared(); room->id = room_id_json.asString(); add_room(std::move(room)); 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->name; if(room_name.empty()) room_name = room_id_str; avatar_url = room->avatar_url; } auto body_item = BodyItem::create(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(const std::shared_ptr *messages, size_t num_messages, BodyItems &result_items) { for(size_t i = 0; i < num_messages; ++i) { auto body_item = BodyItem::create(""); body_item->set_author(messages[i]->user->display_name); body_item->set_description(messages[i]->body); body_item->set_timestamp(messages[i]->timestamp); if(!messages[i]->thumbnail_url.empty()) body_item->thumbnail_url = messages[i]->thumbnail_url; else if(!messages[i]->url.empty() && messages[i]->type == MessageType::IMAGE) body_item->thumbnail_url = messages[i]->url; else body_item->thumbnail_url = messages[i]->user->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 = messages[i]->user->display_name_color; body_item->userdata = (void*)messages[i].get(); // Note: messages[i] has to be valid as long as body_item is used! 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 = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } // TODO: Thread safe? /* if(!room->initial_fetch_finished) { PluginResult result = get_previous_room_messages(room); if(result == PluginResult::OK) { room->initial_fetch_finished = true; } else { fprintf(stderr, "Initial sync failed for room: %s\n", room_id.c_str()); return result; } } */ room->acquire_room_lock(); room_messages_to_body_items(room->get_messages_thread_unsafe().data(), room->get_messages_thread_unsafe().size(), result_items); room->last_read_index = room->get_messages_thread_unsafe().size(); room->release_room_lock(); return PluginResult::OK; } PluginResult Matrix::get_new_room_messages(const std::string &room_id, BodyItems &result_items) { auto room = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } /* if(!room->initial_fetch_finished) { PluginResult result = get_previous_room_messages(room); if(result == PluginResult::OK) { room->initial_fetch_finished = true; } else { fprintf(stderr, "Initial sync failed for room: %s\n", room_id.c_str()); return result; } } */ room->acquire_room_lock(); size_t num_new_messages = room->get_messages_thread_unsafe().size() - room->last_read_index; room_messages_to_body_items(room->get_messages_thread_unsafe().data() + room->last_read_index, num_new_messages, result_items); room->last_read_index = room->get_messages_thread_unsafe().size(); room->release_room_lock(); return PluginResult::OK; } PluginResult Matrix::get_previous_room_messages(const std::string &room_id, BodyItems &result_items) { auto room = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } room->acquire_room_lock(); size_t num_messages_before = room->get_messages_thread_unsafe().size(); room->release_room_lock(); PluginResult result = get_previous_room_messages(room); if(result != PluginResult::OK) return result; room->acquire_room_lock(); size_t num_messages_after = room->get_messages_thread_unsafe().size(); size_t num_new_messages = num_messages_after - num_messages_before; room_messages_to_body_items(room->get_messages_thread_unsafe().data(), num_new_messages, result_items); room->release_room_lock(); return PluginResult::OK; } PluginResult Matrix::sync_response_to_body_items(const Json::Value &root, RoomSyncMessages &room_messages) { 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 = get_room_by_id(room_id_str); if(!room) { room = std::make_shared(); room->id = room_id_str; add_room(room); } const Json::Value &state_json = (*it)["state"]; if(state_json.isObject()) { const Json::Value &events_json = state_json["events"]; events_add_user_info(events_json, room.get()); events_set_room_name(events_json, room.get()); } const Json::Value &timeline_json = (*it)["timeline"]; if(timeline_json.isObject()) { if(room->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->prev_batch = prev_batch_json.asString(); } // TODO: Is there no better way to check for notifications? this is not robust... bool has_unread_notifications = false; const Json::Value &unread_notification_json = (*it)["unread_notifications"]; if(unread_notification_json.isObject()) { const Json::Value &highlight_count_json = unread_notification_json["highlight_count"]; if(highlight_count_json.isNumeric() && highlight_count_json.asInt64() > 0) has_unread_notifications = true; } const Json::Value &events_json = timeline_json["events"]; events_add_user_info(events_json, room.get()); events_add_messages(events_json, room, MessageDirection::AFTER, &room_messages, has_unread_notifications); events_set_room_name(events_json, room.get()); } const Json::Value &ephemeral_json = (*it)["ephemeral"]; if(ephemeral_json.isObject()) { const Json::Value &events_json = ephemeral_json["events"]; events_add_user_read_markers(events_json, room.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; std::string avatar_url_str; const Json::Value &avatar_url_json = content_json["avatar_url"]; if(avatar_url_json.isString()) avatar_url_str = avatar_url_json.asString(); const Json::Value &display_name_json = content_json["displayname"]; std::string sender_json_str = sender_json.asString(); auto user_info = std::make_shared(); user_info->user_id = sender_json_str; user_info->avatar_url = std::move(avatar_url_str); 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); if(!user_info->avatar_url.empty()) 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.isString() ? display_name_json.asString() : sender_json_str; user_info->display_name_color = user_id_to_color(sender_json_str); // Overwrites user data room_data->add_user(std::move(user_info)); } } void Matrix::events_add_user_read_markers(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.receipt") != 0) continue; const Json::Value &content_json = event_item_json["content"]; if(!content_json.isObject()) continue; for(Json::Value::const_iterator it = content_json.begin(); it != content_json.end(); ++it) { if(!it->isObject()) continue; Json::Value event_id_json = it.key(); if(!event_id_json.isString()) continue; const Json::Value &read_json = (*it)["m.read"]; if(!read_json.isObject()) continue; std::string event_id_str = event_id_json.asString(); for(Json::Value::const_iterator user_id_it = read_json.begin(); user_id_it != read_json.end(); ++user_id_it) { if(!user_id_it->isObject()) continue; Json::Value user_id_json = user_id_it.key(); if(!user_id_json.isString()) continue; auto user = room_data->get_user_by_id(user_id_json.asString()); if(!user) { fprintf(stderr, "Receipt read receipt for unknown user: %s, ignoring...\n", user_id_json.asCString()); continue; } room_data->set_user_read_marker(user, event_id_str); } } } } 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 ""; } // TODO: Is this really the proper way to check for username mentions? static bool is_username_seperating_character(char c) { switch(c) { case ' ': case '\n': case '\t': case '\v': case '.': case ',': case '@': case ':': case '?': case '!': case '<': case '>': case '\0': return true; default: return false; } return false; } // TODO: Do not show notification if mention is a reply to somebody else that replies to me? also dont show notification everytime a mention is edited static bool message_contains_user_mention(const std::string &msg, const std::string &username) { if(msg.empty()) return false; size_t index = 0; while(index < msg.size()) { size_t found_index = msg.find(username, index); if(found_index == std::string::npos) return false; char prev_char = ' '; if(found_index > 0) prev_char = msg[found_index - 1]; char next_char = '\0'; if(found_index + username.size() < msg.size() - 1) next_char = msg[found_index + username.size()]; if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char)) return true; index += username.size(); } return false; } void Matrix::events_add_messages(const Json::Value &events_json, std::shared_ptr &room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications) { if(!events_json.isArray()) return; std::vector> *room_sync_messages = nullptr; if(room_messages) room_sync_messages = &(*room_messages)[room_data]; 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 &event_id_json = event_item_json["event_id"]; if(!event_id_json.isString()) continue; std::string event_id_str = event_id_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 = room_data->get_user_by_id(sender_json_str); if(!user) { // Note: this is important because otherwise replying and such is broken 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; time_t timestamp = 0; const Json::Value &origin_server_ts = event_item_json["origin_server_ts"]; if(origin_server_ts.isNumeric()) timestamp = origin_server_ts.asInt64(); std::string replaces_event_id; const Json::Value &relates_to_json = content_json["m.relates_to"]; if(relates_to_json.isObject()) { const Json::Value &replaces_event_id_json = relates_to_json["event_id"]; const Json::Value &rel_type_json = relates_to_json["rel_type"]; if(replaces_event_id_json.isString() && rel_type_json.isString() && strcmp(rel_type_json.asCString(), "m.replace") == 0) replaces_event_id = replaces_event_id_json.asString(); } auto message = std::make_shared(); std::string prefix; // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc if(strcmp(content_type.asCString(), "m.text") == 0) { message->type = MessageType::TEXT; } 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->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; } 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->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; } else if(strcmp(content_type.asCString(), "m.audio") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; message->url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message->type = MessageType::AUDIO; } else if(strcmp(content_type.asCString(), "m.file") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; message->url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message->type = MessageType::FILE; } else if(strcmp(content_type.asCString(), "m.emote") == 0) { // this is a /me message, TODO: show /me messages differently message->type = MessageType::TEXT; prefix = "*" + user->display_name + "* "; } else if(strcmp(content_type.asCString(), "m.notice") == 0) { // TODO: show notices differently message->type = MessageType::TEXT; prefix = "* NOTICE * "; } else if(strcmp(content_type.asCString(), "m.location") == 0) { // TODO: show locations differently const Json::Value &geo_uri_json = content_json["geo_uri"]; if(geo_uri_json.isString()) prefix = geo_uri_json.asString() + " | "; message->type = MessageType::TEXT; message->thumbnail_url = message_content_extract_thumbnail_url(content_json, homeserver); } else { continue; } message->user = user; message->event_id = event_id_str; message->body = prefix + body_json.asString(); message->replaces_event_id = std::move(replaces_event_id); // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) if(has_unread_notifications && !username.empty()) message->mentions_me = message_contains_user_mention(message->body, username) || message_contains_user_mention(message->body, "@room"); message->timestamp = timestamp; new_messages.push_back(message); if(room_sync_messages) room_sync_messages->push_back(message); } // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { size_t num_inserted_messages = room_data->prepend_messages_reverse(std::move(new_messages)); // TODO: Is this thread-safe? if(room_data->last_read_index != 0) room_data->last_read_index += num_inserted_messages; } else if(message_dir == MessageDirection::AFTER) { room_data->append_messages(std::move(new_messages)); } } // 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); } static std::string combine_user_display_names_for_room_name(const std::vector> &user_info, const std::string &fallback_user_id) { std::string result; if(user_info.size() == 0) result = extract_user_name_from_user_id(fallback_user_id); else if(user_info.size() == 1) result = user_info[0]->display_name; else if(user_info.size() == 2) result = user_info[0]->display_name + " and " + user_info[1]->display_name; else if(user_info.size() > 2) result = user_info[0]->display_name + ", " + user_info[1]->display_name + " and " + std::to_string(user_info.size() - 2) + " other(s)"; return result; } 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(); } std::vector> users_excluding_me; if(room_data->name.empty() || room_data->avatar_url.empty()) users_excluding_me = room_data->get_users_excluding_me(user_id); // TODO: What about thread safety with user_id? its reset in /logout 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 = combine_user_display_names_for_room_name(users_excluding_me, creator_json.asString()); if(room_data->avatar_url.empty()) { if(users_excluding_me.empty()) { auto user = room_data->get_user_by_id(creator_json.asString()); if(user) room_data->avatar_url = user->avatar_url; } else { // TODO: If there are multiple users, then we want to use some other type of avatar, not the first users avatar room_data->avatar_url = users_excluding_me.front()->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(std::shared_ptr &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_data->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_data->id.c_str(), from.c_str(), filter.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &state_json = json_root["state"]; events_add_user_info(state_json, room_data.get()); events_set_room_name(state_json, room_data.get()); const Json::Value &chunk_json = json_root["chunk"]; events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); 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; } 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* content_type_to_message_type(ContentType content_type) { if(is_content_type_video(content_type)) return "m.video"; else if(is_content_type_audio(content_type)) return "m.audio"; else if(is_content_type_image(content_type)) return "m.image"; else return "m.file"; } PluginResult Matrix::post_message(const std::string &room_id, const std::string &body, const std::optional &file_info, const std::optional &thumbnail_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(!file_info) { int line = 0; string_split(body, '\n', [&formatted_body, &contains_formatted_text, &line](const char *str, size_t size){ if(line > 0) formatted_body += "
"; 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); } ++line; return true; }); } Json::Value request_data(Json::objectValue); request_data["msgtype"] = (file_info ? content_type_to_message_type(file_info->content_type) : "m.text"); request_data["body"] = body; if(contains_formatted_text) { request_data["format"] = "org.matrix.custom.html"; request_data["formatted_body"] = std::move(formatted_body); } // TODO: Add hashblur? if(file_info) { Json::Value info_json(Json::objectValue); info_json["size"] = file_info->file_size; info_json["mimetype"] = content_type_to_string(file_info->content_type); if(file_info->dimensions) { info_json["w"] = file_info->dimensions->width; info_json["h"] = file_info->dimensions->height; } if(file_info->duration_seconds) { // TODO: Check for overflow? info_json["duration"] = (int)file_info->duration_seconds.value() * 1000; } if(thumbnail_info) { Json::Value thumbnail_info_json(Json::objectValue); thumbnail_info_json["size"] = thumbnail_info->file_size; thumbnail_info_json["mimetype"] = content_type_to_string(thumbnail_info->content_type); if(thumbnail_info->dimensions) { thumbnail_info_json["w"] = thumbnail_info->dimensions->width; thumbnail_info_json["h"] = thumbnail_info->dimensions->height; } info_json["thumbnail_url"] = thumbnail_info->content_uri; info_json["info"] = std::move(thumbnail_info_json); } request_data["info"] = std::move(info_json); request_data["url"] = file_info->content_uri; } 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()); Json::Value json_root; DownloadResult download_result = download_json(json_root, request_url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); 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; } static std::string remove_reply_formatting(const std::string &str) { if(strncmp(str.c_str(), "> <@", 4) == 0) { size_t index = str.find("> ", 4); if(index != std::string::npos) { size_t msg_begin = str.find("\n\n", index + 2); if(msg_begin != std::string::npos) return str.substr(msg_begin + 2); } } return str; } static std::string block_quote(const std::string &str) { std::string result; for(char c : str) { if(c == '>') { result += "\\>"; } else if(c == '\n') { result += "\n> "; } else { result += c; } } return result; } static std::string get_reply_message(const Message *message) { std::string related_to_body; switch(message->type) { case MessageType::TEXT: { if(!message->replaces_event_id.empty() && strncmp(message->body.c_str(), " * ", 3) == 0) related_to_body = remove_reply_formatting(message->body.substr(3)); else related_to_body = remove_reply_formatting(message->body); break; } case MessageType::IMAGE: related_to_body = "sent an image"; break; case MessageType::VIDEO: related_to_body = "sent a video"; break; case MessageType::AUDIO: related_to_body = "sent an audio file"; break; case MessageType::FILE: related_to_body = "sent a file"; break; } return related_to_body; } static std::string create_body_for_message_reply(const Message *message, const std::string &body) { return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(message)) + "\n\n" + body; } static std::string create_formatted_body_for_message_reply(const Message *message, const std::string &body) { std::string formatted_body = body; std::string related_to_body = get_reply_message(message); html_escape_sequences(formatted_body); html_escape_sequences(related_to_body); return "" "
" "In reply to" "user->user_id + "\">" + message->user->user_id + "
" + std::move(related_to_body) + "
" "
" + std::move(formatted_body); } // TODO: Support greentext PluginResult Matrix::post_reply(const std::string &room_id, const std::string &body, void *relates_to) { auto room = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } // TODO: Store shared_ptr instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); std::shared_ptr relates_to_message_original = get_edited_message_original_message(room.get(), relates_to_message_shared); if(!relates_to_message_original) { fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); return PluginResult::ERR; } 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)); Json::Value in_reply_to_json(Json::objectValue); in_reply_to_json["event_id"] = relates_to_message_original->event_id; Json::Value relates_to_json(Json::objectValue); relates_to_json["m.in_reply_to"] = std::move(in_reply_to_json); Json::Value request_data(Json::objectValue); request_data["msgtype"] = "m.text"; // TODO: Allow image reply? element doesn't do that but we could! request_data["body"] = create_body_for_message_reply(relates_to_message_raw, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... request_data["format"] = "org.matrix.custom.html"; request_data["formatted_body"] = create_formatted_body_for_message_reply(relates_to_message_raw, body); request_data["m.relates_to"] = std::move(relates_to_json); 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()); Json::Value json_root; DownloadResult download_result = download_json(json_root, request_url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); 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 reply, response event id: %s\n", event_id_json.asCString()); return PluginResult::OK; } PluginResult Matrix::post_edit(const std::string &room_id, const std::string &body, void *relates_to) { auto room = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return PluginResult::ERR; } Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); std::shared_ptr relates_to_message_original = get_edited_message_original_message(room.get(), relates_to_message_shared); if(!relates_to_message_original) { fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); return PluginResult::ERR; } 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; int line = 0; string_split(body, '\n', [&formatted_body, &contains_formatted_text, &line](const char *str, size_t size){ if(line > 0) formatted_body += "
"; 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); } ++line; return true; }); Json::Value new_content_json(Json::objectValue); new_content_json["msgtype"] = "m.text"; new_content_json["body"] = body; if(contains_formatted_text) { new_content_json["format"] = "org.matrix.custom.html"; new_content_json["formatted_body"] = formatted_body; } Json::Value relates_to_json(Json::objectValue); relates_to_json["event_id"] = relates_to_message_original->event_id; relates_to_json["rel_type"] = "m.replace"; Json::Value request_data(Json::objectValue); request_data["msgtype"] = "m.text"; // TODO: Allow other types of edits request_data["body"] = " * " + body; if(contains_formatted_text) { request_data["format"] = "org.matrix.custom.html"; request_data["formatted_body"] = " * " + formatted_body; } request_data["m.new_content"] = std::move(new_content_json); request_data["m.relates_to"] = std::move(relates_to_json); 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()); Json::Value json_root; DownloadResult download_result = download_json(json_root, request_url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); 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 edit, response event id: %s\n", event_id_json.asCString()); return PluginResult::OK; } // TODO: Right now this recursively calls /rooms//context/ and trusts server to not make it recursive. To make this robust, check iteration count and do not trust server. // TODO: Optimize? std::shared_ptr Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr message) { if(message->replaces_event_id.empty()) return message; auto replaced_message = room_data->get_message_by_id(message->replaces_event_id); if(!replaced_message) { 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/context/%s?limit=0&filter=%s", homeserver.c_str(), room_data->id.c_str(), message->event_id.c_str(), filter.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return nullptr; if(!json_root.isObject()) return nullptr; const Json::Value &event_json = json_root["event"]; if(!event_json.isObject()) return nullptr; const Json::Value &event_id_json = event_json["event_id"]; if(!event_id_json.isString()) return nullptr; const Json::Value &content_json = event_json["content"]; if(!content_json.isObject()) return nullptr; const Json::Value &body_json = content_json["body"]; if(!body_json.isString()) return nullptr; std::string replaces_event_id; const Json::Value &relates_to_json = content_json["m.relates_to"]; if(relates_to_json.isObject()) { const Json::Value &event_id_json = relates_to_json["event_id"]; const Json::Value &rel_type_json = relates_to_json["rel_type"]; if(event_id_json.isString() && rel_type_json.isString() && strcmp(rel_type_json.asCString(), "m.replace") == 0) replaces_event_id = event_id_json.asString(); } const Json::Value &content_type = content_json["msgtype"]; if(!content_type.isString()) return nullptr; auto new_message = std::make_shared(); new_message->event_id = event_id_json.asString(); new_message->replaces_event_id = std::move(replaces_event_id); if(strcmp(content_type.asCString(), "m.text") == 0) { new_message->type = MessageType::TEXT; } else if(strcmp(content_type.asCString(), "m.image") == 0) { new_message->type = MessageType::IMAGE; } else if(strcmp(content_type.asCString(), "m.video") == 0) { new_message->type = MessageType::VIDEO; } else if(strcmp(content_type.asCString(), "m.audio") == 0) { new_message->type = MessageType::AUDIO; } else if(strcmp(content_type.asCString(), "m.file") == 0) { new_message->type = MessageType::FILE; } else { return nullptr; } return get_edited_message_original_message(room_data, std::move(new_message)); } else { return get_edited_message_original_message(room_data, replaced_message); } } // 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; } PluginResult Matrix::post_file(const std::string &room_id, const std::string &filepath, std::string &err_msg) { UploadInfo file_info; UploadInfo thumbnail_info; PluginResult upload_file_result = upload_file(room_id, filepath, file_info, thumbnail_info, err_msg); if(upload_file_result != PluginResult::OK) return upload_file_result; std::optional file_info_opt = std::move(file_info); std::optional thumbnail_info_opt; if(!thumbnail_info.content_uri.empty()) thumbnail_info_opt = std::move(thumbnail_info); const char *filename = file_get_filename(filepath); return post_message(room_id, filename, file_info_opt, thumbnail_info_opt); } PluginResult Matrix::upload_file(const std::string &room_id, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg) { FileAnalyzer file_analyzer; if(!file_analyzer.load_file(filepath.c_str())) { err_msg = "Failed to load " + filepath; return PluginResult::ERR; } file_info.content_type = file_analyzer.get_content_type(); file_info.file_size = file_analyzer.get_file_size(); file_info.dimensions = file_analyzer.get_dimensions(); file_info.duration_seconds = file_analyzer.get_duration_seconds(); int upload_limit; PluginResult config_result = get_config(&upload_limit); if(config_result != PluginResult::OK) { err_msg = "Failed to get file size limit from server"; return config_result; } // Checking for sane file size limit client side, to prevent loading a huge file and crashing if(file_analyzer.get_file_size() > 300 * 1024 * 1024) { // 300mb err_msg = "File is too large! client-side limit is set to 300mb"; return PluginResult::ERR; } if((int)file_analyzer.get_file_size() > upload_limit) { err_msg = "File is too large! max upload size on your homeserver is " + std::to_string(upload_limit) + " bytes, the file you tried to upload is " + std::to_string(file_analyzer.get_file_size()) + " bytes"; return PluginResult::ERR; } if(is_content_type_video(file_analyzer.get_content_type())) { // TODO: Also upload thumbnail for images. Take into consideration below upload_file, we dont want to upload thumbnail of thumbnail char tmp_filename[] = "/tmp/quickmedia_video_frame_XXXXXX"; int tmp_file = mkstemp(tmp_filename); if(tmp_file != -1) { if(video_get_first_frame(filepath.c_str(), tmp_filename)) { UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. PluginResult upload_thumbnail_result = upload_file(room_id, tmp_filename, thumbnail_info, upload_info_ignored, err_msg); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); return upload_thumbnail_result; } } else { fprintf(stderr, "Failed to get first frame of video, ignoring thumbnail...\n"); } close(tmp_file); remove(tmp_filename); } else { fprintf(stderr, "Failed to create temporary file for video thumbnail, ignoring thumbnail...\n"); } } std::vector additional_args = { { "-X", "POST" }, { "-H", std::string("content-type: ") + content_type_to_string(file_analyzer.get_content_type()) }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", "@" + filepath } }; const char *filename = file_get_filename(filepath); std::string filename_escaped = url_param_encode(filename); char url[512]; snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", homeserver.c_str(), filename_escaped.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) { err_msg = "Got corrupt response from server"; return PluginResult::ERR; } const Json::Value &error_json = json_root["error"]; if(error_json.isString()) { err_msg = error_json.asString(); return PluginResult::ERR; } const Json::Value &content_uri_json = json_root["content_uri"]; if(!content_uri_json.isString()) { err_msg = "Missing content_uri is server response"; return PluginResult::ERR; } fprintf(stderr, "Matrix upload, response content uri: %s\n", content_uri_json.asCString()); file_info.content_uri = content_uri_json.asString(); return PluginResult::OK; } 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; request_data["initial_device_display_name"] = "QuickMedia"; // :^) 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)) } }; Json::Value json_root; DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/login", std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } const Json::Value &error_json = json_root["error"]; if(error_json.isString()) { err_msg = error_json.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->username = extract_user_name_from_user_id(this->user_id); 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(SERVICE_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(SERVICE_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(); username.clear(); access_token.clear(); homeserver.clear(); upload_limit.reset(); next_batch.clear(); return PluginResult::OK; } PluginResult Matrix::delete_message(const std::string &room_id, void *message, std::string &err_msg){ 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)); Message *message_typed = (Message*)message; // request_data could contains "reason", maybe it should be added sometime in the future? Json::Value request_data(Json::objectValue); 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 url[512]; snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/redact/%s/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), message_typed->event_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } const Json::Value &error_json = json_root["error"]; if(error_json.isString()) { err_msg = error_json.asString(); return PluginResult::ERR; } const Json::Value &event_id_json = json_root["event_id"]; if(!event_id_json.isString()) return PluginResult::ERR; fprintf(stderr, "Matrix delete message, response event id: %s\n", event_id_json.asCString()); return PluginResult::OK; } PluginResult Matrix::load_and_verify_cached_session() { Path session_path = get_storage_dir().join(SERVICE_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->username = extract_user_name_from_user_id(this->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; } PluginResult Matrix::set_read_marker(const std::string &room_id, const Message *message) { Json::Value request_data(Json::objectValue); request_data["m.fully_read"] = message->event_id; request_data["m.read"] = message->event_id; request_data["m.hidden"] = false; // What is this for? element sends it but its not part of the documentation. Is it for hiding read receipt from other users? in that case, TODO: make it configurable 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)) }, { "-H", "Authorization: Bearer " + access_token } }; std::string server_response; if(download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/read_markers", server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) return PluginResult::NET_ERR; return PluginResult::OK; } bool Matrix::was_message_posted_by_me(void *message) { Message *message_typed = (Message*)message; return user_id == message_typed->user->user_id; } std::string Matrix::message_get_author_displayname(Message *message) const { // TODO: Thread safe? return message->user->display_name; } PluginResult Matrix::get_config(int *upload_size) { // TODO: What if the upload limit changes? is it possible for the upload limit to change while the server is running? if(upload_limit) { *upload_size = upload_limit.value(); return PluginResult::OK; } *upload_size = 0; std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; char url[512]; snprintf(url, sizeof(url), "%s/_matrix/media/r0/config", homeserver.c_str()); Json::Value json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) return PluginResult::ERR; const Json::Value &upload_size_json = json_root["m.upload.size"]; if(!upload_size_json.isNumeric()) return PluginResult::ERR; upload_limit = upload_size_json.asInt(); *upload_size = upload_limit.value(); return PluginResult::OK; } std::shared_ptr Matrix::get_me(const std::string &room_id) { auto room = get_room_by_id(room_id); if(!room) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); return nullptr; } return room->get_user_by_id(user_id); } std::shared_ptr Matrix::get_room_by_id(const std::string &id) { std::lock_guard lock(room_data_mutex); auto room_it = room_data_by_id.find(id); if(room_it == room_data_by_id.end()) return nullptr; return room_it->second; } void Matrix::add_room(std::shared_ptr room) { std::lock_guard lock(room_data_mutex); room_data_by_id.insert(std::make_pair(room->id, room)); } DownloadResult Matrix::download_json(Json::Value &result, const std::string &url, std::vector additional_args, bool use_browser_useragent, std::string *err_msg) const { std::string server_response; if(download_to_string(url, server_response, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { if(err_msg) *err_msg = server_response; return DownloadResult::NET_ERR; } if(server_response.empty()) return DownloadResult::OK; 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()], &result, &json_errors)) { fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); if(err_msg) *err_msg = std::move(json_errors); return DownloadResult::ERR; } return DownloadResult::OK; } }