#include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" #include #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"; static rapidjson::Value nullValue(rapidjson::kNullType); static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char *key) { auto it = obj.FindMember(key); if(it != obj.MemberEnd()) return it->value; return nullValue; } 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; } void RoomData::prepend_messages_reverse(const std::vector> &new_messages) { std::lock_guard lock(room_mutex); 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)); } } } void RoomData::append_messages(const std::vector> &new_messages) { std::lock_guard lock(room_mutex); 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)); } } } 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()); rapidjson::Document 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 rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch"); if(next_batch_json.IsString()) { next_batch = next_batch_json.GetString(); fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); } else { fprintf(stderr, "Matrix: missing next batch\n"); } return PluginResult::OK; } void Matrix::get_room_join_updates(Rooms &new_rooms) { std::lock_guard lock(room_data_mutex); size_t num_new_rooms = rooms.size() - room_list_read_index; size_t new_rooms_prev_size = new_rooms.size(); new_rooms.resize(new_rooms_prev_size + num_new_rooms); for(size_t i = new_rooms_prev_size; i < new_rooms.size(); ++i) { new_rooms[i] = rooms[room_list_read_index + i].get(); } room_list_read_index += num_new_rooms; } PluginResult Matrix::get_all_synced_room_messages(RoomData *room, Messages &messages) { room->acquire_room_lock(); messages = room->get_messages_thread_unsafe(); room->release_room_lock(); return PluginResult::OK; } PluginResult Matrix::get_previous_room_messages(RoomData *room, Messages &messages) { 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; messages.insert(messages.end(), room->get_messages_thread_unsafe().begin(), room->get_messages_thread_unsafe().begin() + num_new_messages); room->release_room_lock(); return PluginResult::OK; } PluginResult Matrix::sync_response_to_body_items(const rapidjson::Document &root, RoomSyncMessages &room_messages) { if(!root.IsObject()) return PluginResult::ERR; const rapidjson::Value &rooms_json = GetMember(root, "rooms"); if(!rooms_json.IsObject()) return PluginResult::OK; const rapidjson::Value &join_json = GetMember(rooms_json, "join"); if(!join_json.IsObject()) return PluginResult::OK; for(auto const &it : join_json.GetObject()) { if(!it.value.IsObject()) continue; const rapidjson::Value &room_id = it.name; if(!room_id.IsString()) continue; std::string room_id_str = room_id.GetString(); RoomData *room = get_room_by_id(room_id_str); if(!room) { auto new_room = std::make_unique(); new_room->id = room_id_str; room = new_room.get(); add_room(std::move(new_room)); } const rapidjson::Value &state_json = GetMember(it.value, "state"); if(state_json.IsObject()) { const rapidjson::Value &events_json = GetMember(state_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); } const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral"); const rapidjson::Value &timeline_json = GetMember(it.value, "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 rapidjson::Value &prev_batch_json = GetMember(timeline_json, "prev_batch"); if(prev_batch_json.IsString()) room->prev_batch = prev_batch_json.GetString(); } // TODO: Use /_matrix/client/r0/notifications ? or remove this and always look for displayname/user_id in messages bool has_unread_notifications = false; const rapidjson::Value &unread_notification_json = GetMember(it.value, "unread_notifications"); if(unread_notification_json.IsObject()) { const rapidjson::Value &highlight_count_json = GetMember(unread_notification_json, "highlight_count"); if(highlight_count_json.IsNumber() && highlight_count_json.GetInt64() > 0) has_unread_notifications = true; } const rapidjson::Value &events_json = GetMember(timeline_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); // We want to do this before adding messages to know if a message that mentions us is a new mention if(ephemeral_json.IsObject()) { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); events_add_user_read_markers(events_json, room); } events_add_messages(events_json, room, MessageDirection::AFTER, &room_messages, has_unread_notifications); } else { if(ephemeral_json.IsObject()) { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); events_add_user_read_markers(events_json, room); } } } 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::Uint8 *col = (sf::Uint8*)&color; sf::Color result(col[0], col[1], col[2]); result.r = std::min(255, 80 + (int)result.r); result.g = std::min(255, 80 + (int)result.g); result.b = std::min(255, 80 + (int)result.b); result.a = 255; return result; } void Matrix::events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.member") != 0) continue; const rapidjson::Value &sender_json = GetMember(event_item_json, "sender"); if(!sender_json.IsString()) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; const rapidjson::Value &membership_json = GetMember(content_json, "membership"); if(!membership_json.IsString() || strcmp(membership_json.GetString(), "join") != 0) continue; std::string avatar_url_str; const rapidjson::Value &avatar_url_json = GetMember(content_json, "avatar_url"); if(avatar_url_json.IsString()) avatar_url_str = avatar_url_json.GetString(); const rapidjson::Value &display_name_json = GetMember(content_json, "displayname"); std::string sender_json_str = sender_json.GetString(); 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.GetString() : 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 rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString() || strcmp(type_json.GetString(), "m.receipt") != 0) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; for(auto const &it2 : content_json.GetObject()) { if(!it2.value.IsObject()) continue; const rapidjson::Value &event_id_json = it2.name; if(!event_id_json.IsString()) continue; const rapidjson::Value &read_json = GetMember(it2.value, "m.read"); if(!read_json.IsObject()) continue; std::string event_id_str = event_id_json.GetString(); for(auto const &it3 : read_json.GetObject()) { if(!it3.value.IsObject()) continue; const rapidjson::Value &user_id_json = it3.name; if(!user_id_json.IsString()) continue; auto user = room_data->get_user_by_id(user_id_json.GetString()); if(!user) { fprintf(stderr, "Receipt read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString()); continue; } room_data->set_user_read_marker(user, event_id_str); } } } } static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver) { const rapidjson::Value &info_json = GetMember(content_json, "info"); if(info_json.IsObject()) { const rapidjson::Value &thumbnail_url_json = GetMember(info_json, "thumbnail_url"); if(thumbnail_url_json.IsString()) { std::string thumbnail_str = thumbnail_url_json.GetString(); 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 ""; } static bool message_content_extract_thumbnail_size(const rapidjson::Value &content_json, sf::Vector2i &thumbnail_size) { const rapidjson::Value &info_json = GetMember(content_json, "info"); if(!info_json.IsObject()) return false; bool found_resolution = false; const rapidjson::Value &w_json = GetMember(info_json, "w"); const rapidjson::Value &h_json = GetMember(info_json, "h"); if(w_json.IsNumber() && h_json.IsNumber()) { thumbnail_size.x = w_json.GetInt(); thumbnail_size.y = h_json.GetInt(); found_resolution = true; } const rapidjson::Value &thumbnail_info_json = GetMember(info_json, "thumbnail_info"); if(thumbnail_info_json.IsObject()) { const rapidjson::Value &w_json = GetMember(thumbnail_info_json, "w"); const rapidjson::Value &h_json = GetMember(thumbnail_info_json, "h"); if(w_json.IsNumber() && h_json.IsNumber()) { thumbnail_size.x = w_json.GetInt(); thumbnail_size.y = h_json.GetInt(); found_resolution = true; } } return found_resolution; } // 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 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 rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications) { if(!events_json.IsArray()) return; std::vector> new_messages; auto me = get_me(room_data); for(const rapidjson::Value &event_item_json : events_json.GetArray()) { std::shared_ptr new_message = parse_message_event(event_item_json, room_data); if(new_message) new_messages.push_back(std::move(new_message)); } if(new_messages.empty()) return; // TODO: Add directly to this instead when set? otherwise add to new_messages if(room_messages) (*room_messages)[room_data] = new_messages; // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { room_data->prepend_messages_reverse(new_messages); } else if(message_dir == MessageDirection::AFTER) { room_data->append_messages(new_messages); } std::shared_ptr read_marker_message; if(me) read_marker_message = room_data->get_message_by_id(room_data->get_user_read_marker(me)); for(auto &message : new_messages) { // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) // TODO: Is comparing against read marker timestamp ok enough? if(has_unread_notifications && me && (!read_marker_message || read_marker_message->timestamp < message->timestamp)) message->mentions_me = message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id) || message_contains_user_mention(message->body, "@room"); } } std::shared_ptr Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { if(!event_item_json.IsObject()) return nullptr; const rapidjson::Value &sender_json = GetMember(event_item_json, "sender"); if(!sender_json.IsString()) return nullptr; std::string sender_json_str = sender_json.GetString(); const rapidjson::Value &event_id_json = GetMember(event_item_json, "event_id"); if(!event_id_json.IsString()) return nullptr; std::string event_id_str = event_id_json.GetString(); const rapidjson::Value *content_json = &GetMember(event_item_json, "content"); if(!content_json->IsObject()) return nullptr; 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()); return nullptr; } time_t timestamp = 0; const rapidjson::Value &origin_server_ts = GetMember(event_item_json, "origin_server_ts"); if(origin_server_ts.IsNumber()) timestamp = origin_server_ts.GetInt64(); const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString()) return nullptr; RelatedEventType related_event_type = RelatedEventType::NONE; std::string related_event_id; const rapidjson::Value &relates_to_json = GetMember(*content_json, "m.relates_to"); if(relates_to_json.IsObject()) { const rapidjson::Value &replaces_event_id_json = GetMember(relates_to_json, "event_id"); const rapidjson::Value &rel_type_json = GetMember(relates_to_json, "rel_type"); if(replaces_event_id_json.IsString() && rel_type_json.IsString() && strcmp(rel_type_json.GetString(), "m.replace") == 0) { related_event_id = replaces_event_id_json.GetString(); related_event_type = RelatedEventType::EDIT; } else { const rapidjson::Value &in_reply_to_json = GetMember(relates_to_json, "m.in_reply_to"); if(in_reply_to_json.IsObject()) { const rapidjson::Value &in_reply_to_event_id = GetMember(in_reply_to_json, "event_id"); if(in_reply_to_event_id.IsString()) { related_event_id = in_reply_to_event_id.GetString(); related_event_type = RelatedEventType::REPLY; } } } } const rapidjson::Value &new_content_json = GetMember(*content_json, "m.new_content"); if(new_content_json.IsObject()) content_json = &new_content_json; const rapidjson::Value &content_type = GetMember(*content_json, "msgtype"); if(!content_type.IsString() || strcmp(type_json.GetString(), "m.room.redaction") == 0) { auto message = std::make_shared(); message->type = MessageType::REDACTION; message->user = user; message->event_id = event_id_str; message->body = "Message deleted"; message->timestamp = timestamp; message->related_event_type = RelatedEventType::REDACTION; const rapidjson::Value &reason_json = GetMember(*content_json, "reason"); if(reason_json.IsString()) { message->body += ", reason: "; message->body += reason_json.GetString(); } const rapidjson::Value &redacts_json = GetMember(event_item_json, "redacts"); if(redacts_json.IsString()) message->related_event_id = redacts_json.GetString(); return message; } if(strcmp(type_json.GetString(), "m.room.message") != 0) return nullptr; const rapidjson::Value &body_json = GetMember(*content_json, "body"); if(!body_json.IsString()) return nullptr; auto message = std::make_shared(); std::string prefix; // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc if(strcmp(content_type.GetString(), "m.text") == 0) { message->type = MessageType::TEXT; } else if(strcmp(content_type.GetString(), "m.image") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); message->type = MessageType::IMAGE; } else if(strcmp(content_type.GetString(), "m.video") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); message->type = MessageType::VIDEO; } else if(strcmp(content_type.GetString(), "m.audio") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); message->type = MessageType::AUDIO; } else if(strcmp(content_type.GetString(), "m.file") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); message->type = MessageType::FILE; } else if(strcmp(content_type.GetString(), "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.GetString(), "m.notice") == 0) { // TODO: show notices differently message->type = MessageType::TEXT; prefix = "* NOTICE * "; } else if(strcmp(content_type.GetString(), "m.location") == 0) { // TODO: show locations differently const rapidjson::Value &geo_uri_json = GetMember(*content_json, "geo_uri"); if(geo_uri_json.IsString()) prefix = geo_uri_json.GetString() + std::string(" | "); message->type = MessageType::TEXT; message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); } else { return nullptr; } message->user = user; message->event_id = event_id_str; message->body = prefix + body_json.GetString(); message->related_event_id = std::move(related_event_id); message->related_event_type = related_event_type; message->timestamp = timestamp; return message; } // 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 rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.name") != 0) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; const rapidjson::Value &name_json = GetMember(content_json, "name"); if(!name_json.IsString()) continue; room_data->name = name_json.GetString(); } 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 rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.create") != 0) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; const rapidjson::Value &creator_json = GetMember(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.GetString()); if(room_data->avatar_url.empty()) { if(users_excluding_me.empty()) { auto user = room_data->get_user_by_id(creator_json.GetString()); 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 rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.avatar") != 0) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; const rapidjson::Value &url_json = GetMember(content_json, "url"); if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) continue; std::string url_json_str = url_json.GetString() + 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(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_data->id.c_str()); from = next_batch; if(from.empty()) { fprintf(stderr, "Error: missing next batch!\n"); return PluginResult::OK; } } rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; std::string filter = url_param_encode(buffer.GetString()); 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()); rapidjson::Document 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 rapidjson::Value &state_json = GetMember(json_root, "state"); events_add_user_info(state_json, room_data); events_set_room_name(state_json, room_data); const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); const rapidjson::Value &end_json = GetMember(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.GetString(); 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(RoomData *room, 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 += "
"; std::string line_str(str, size); html_escape_sequences(line_str); if(size > 0 && str[0] == '>') { formatted_body += ""; formatted_body += line_str; formatted_body += ""; contains_formatted_text = true; } else { formatted_body += line_str; } ++line; return true; }); } rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); request_data.AddMember("body", rapidjson::StringRef(body.c_str()), request_data.GetAllocator()); if(contains_formatted_text) { request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); request_data.AddMember("formatted_body", rapidjson::StringRef(formatted_body.c_str()), request_data.GetAllocator()); } // TODO: Add hashblur? if(file_info) { rapidjson::Value info_json(rapidjson::kObjectType); info_json.AddMember("size", file_info->file_size, request_data.GetAllocator()); info_json.AddMember("mimetype", rapidjson::StringRef(content_type_to_string(file_info->content_type)), request_data.GetAllocator()); if(file_info->dimensions) { info_json.AddMember("w", file_info->dimensions->width, request_data.GetAllocator()); info_json.AddMember("h", file_info->dimensions->height, request_data.GetAllocator()); } if(file_info->duration_seconds) { // TODO: Check for overflow? info_json.AddMember("duration", (int)(file_info->duration_seconds.value() * 1000.0), request_data.GetAllocator()); } if(thumbnail_info) { rapidjson::Value thumbnail_info_json(rapidjson::kObjectType); thumbnail_info_json.AddMember("size", thumbnail_info->file_size, request_data.GetAllocator()); thumbnail_info_json.AddMember("mimetype", rapidjson::StringRef(content_type_to_string(thumbnail_info->content_type)), request_data.GetAllocator()); if(thumbnail_info->dimensions) { thumbnail_info_json.AddMember("w", thumbnail_info->dimensions->width, request_data.GetAllocator()); thumbnail_info_json.AddMember("h", thumbnail_info->dimensions->height, request_data.GetAllocator()); } info_json.AddMember("thumbnail_url", rapidjson::StringRef(thumbnail_info->content_uri.c_str()), request_data.GetAllocator()); info_json.AddMember("info", std::move(thumbnail_info_json), request_data.GetAllocator()); } request_data.AddMember("info", std::move(info_json), request_data.GetAllocator()); request_data.AddMember("url", rapidjson::StringRef(file_info->content_uri.c_str()), request_data.GetAllocator()); } rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", buffer.GetString() } }; 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()); rapidjson::Document 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 rapidjson::Value &event_id_json = GetMember(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.GetString()); 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->related_event_type != RelatedEventType::NONE) related_to_body = remove_reply_formatting(message->body); else related_to_body = 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; case MessageType::REDACTION: related_to_body = message->body; 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 extract_homeserver_from_room_id(const std::string &room_id) { size_t sep_index = room_id.find(':'); if(sep_index != std::string::npos) return room_id.substr(sep_index + 1); return ""; } static std::string create_formatted_body_for_message_reply(RoomData *room, 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); // TODO: Add keybind to navigate to the reply message, which would also depend on this formatting. return "" "
" "id + "/" + message->event_id + "?via=" + extract_homeserver_from_room_id(room->id) + "\">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(RoomData *room, const std::string &body, void *relates_to) { // 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, 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)); rapidjson::Document in_reply_to_json(rapidjson::kObjectType); in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), in_reply_to_json.GetAllocator()); rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); std::string message_reply_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... std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, relates_to_message_raw, body); rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("msgtype", "m.text", request_data.GetAllocator()); // TODO: Allow image reply? element doesn't do that but we could! request_data.AddMember("body", rapidjson::StringRef(message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); request_data.AddMember("formatted_body", rapidjson::StringRef(formatted_message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("m.relates_to", std::move(relates_to_json), request_data.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", buffer.GetString() } }; 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()); rapidjson::Document 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 rapidjson::Value &event_id_json = GetMember(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.GetString()); return PluginResult::OK; } PluginResult Matrix::post_edit(RoomData *room, const std::string &body, void *relates_to) { 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, 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; }); rapidjson::Document new_content_json(rapidjson::kObjectType); new_content_json.AddMember("msgtype", "m.text", new_content_json.GetAllocator()); new_content_json.AddMember("body", rapidjson::StringRef(body.c_str()), new_content_json.GetAllocator()); if(contains_formatted_text) { new_content_json.AddMember("format", "org.matrix.custom.html", new_content_json.GetAllocator()); new_content_json.AddMember("formatted_body", rapidjson::StringRef(formatted_body.c_str()), new_content_json.GetAllocator()); } rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), relates_to_json.GetAllocator()); relates_to_json.AddMember("rel_type", "m.replace", relates_to_json.GetAllocator()); std::string body_edit_str = " * " + body; std::string formatted_body_edit_str; rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("msgtype", "m.text", request_data.GetAllocator()); // TODO: Allow other types of edits request_data.AddMember("body", rapidjson::StringRef(body_edit_str.c_str()), request_data.GetAllocator()); if(contains_formatted_text) { formatted_body_edit_str = " * " + formatted_body; request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); request_data.AddMember("formatted_body", rapidjson::StringRef(formatted_body_edit_str.c_str()), request_data.GetAllocator()); } request_data.AddMember("m.new_content", std::move(new_content_json), request_data.GetAllocator()); request_data.AddMember("m.relates_to", std::move(relates_to_json), request_data.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", buffer.GetString() } }; 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()); rapidjson::Document 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 rapidjson::Value &event_id_json = GetMember(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.GetString()); 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 || message->related_event_type != RelatedEventType::EDIT) return message; return get_edited_message_original_message(room_data, get_message_by_id(room_data, message->related_event_id)); } std::shared_ptr Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { std::shared_ptr existing_room_message = room->get_message_by_id(event_id); if(existing_room_message) return existing_room_message; auto fetched_message_it = room->fetched_messages_by_event_id.find(event_id); if(fetched_message_it != room->fetched_messages_by_event_id.end()) return fetched_message_it->second; rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; std::string filter = url_param_encode(buffer.GetString()); char url[512]; snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); std::string err_msg; rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return nullptr; if(json_root.IsObject()) { const rapidjson::Value &error_json = GetMember(json_root, "error"); if(error_json.IsString()) { fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); return nullptr; } } const rapidjson::Value &event_json = GetMember(json_root, "event"); std::shared_ptr new_message = parse_message_event(event_json, room); room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); return new_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(RoomData *room, const std::string &filepath, std::string &err_msg) { UploadInfo file_info; UploadInfo thumbnail_info; PluginResult upload_file_result = upload_file(room, 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, filename, file_info_opt, thumbnail_info_opt); } PluginResult Matrix::upload_file(RoomData *room, 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, 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()); rapidjson::Document 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 rapidjson::Value &error_json = GetMember(json_root, "error"); if(error_json.IsString()) { err_msg = error_json.GetString(); return PluginResult::ERR; } const rapidjson::Value &content_uri_json = GetMember(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.GetString()); file_info.content_uri = content_uri_json.GetString(); return PluginResult::OK; } PluginResult Matrix::login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg) { rapidjson::Document identifier_json(rapidjson::kObjectType); identifier_json.AddMember("type", "m.id.user", identifier_json.GetAllocator()); // TODO: What if the server doesn't support this login type? redirect to sso web page etc identifier_json.AddMember("user", rapidjson::StringRef(username.c_str()), identifier_json.GetAllocator()); rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("type", "m.login.password", request_data.GetAllocator()); request_data.AddMember("identifier", std::move(identifier_json), request_data.GetAllocator()); request_data.AddMember("password", rapidjson::StringRef(password.c_str()), request_data.GetAllocator()); request_data.AddMember("initial_device_display_name", "QuickMedia", request_data.GetAllocator()); // :^) rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "POST" }, { "-H", "content-type: application/json" }, { "--data-binary", buffer.GetString() } }; rapidjson::Document 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 rapidjson::Value &error_json = GetMember(json_root, "error"); if(error_json.IsString()) { err_msg = error_json.GetString(); return PluginResult::ERR; } const rapidjson::Value &user_id_json = GetMember(json_root, "user_id"); if(!user_id_json.IsString()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; } const rapidjson::Value &access_token_json = GetMember(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.AddMember("homeserver", rapidjson::StringRef(homeserver.c_str()), request_data.GetAllocator()); this->user_id = user_id_json.GetString(); this->username = extract_user_name_from_user_id(this->user_id); this->access_token = access_token_json.GetString(); 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! rooms.clear(); room_list_read_index = 0; 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(RoomData *room, 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? rapidjson::Value request_data(rapidjson::kObjectType); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", buffer.GetString() } }; 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()); rapidjson::Document 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 rapidjson::Value &error_json = GetMember(json_root, "error"); if(error_json.IsString()) { err_msg = error_json.GetString(); return PluginResult::ERR; } const rapidjson::Value &event_id_json = GetMember(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.GetString()); 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; } rapidjson::Document json_root; rapidjson::ParseResult parse_result = json_root.Parse(session_json_content.c_str(), session_json_content.size()); if(parse_result.IsError()) { fprintf(stderr, "Matrix cached session parse error: %d\n", parse_result.Code()); return PluginResult::ERR; } if(!json_root.IsObject()) return PluginResult::ERR; const rapidjson::Value &user_id_json = GetMember(json_root, "user_id"); if(!user_id_json.IsString()) { fprintf(stderr, "Failed to parse matrix cached session response\n"); return PluginResult::ERR; } const rapidjson::Value &access_token_json = GetMember(json_root, "access_token"); if(!access_token_json.IsString()) { fprintf(stderr, "Failed to parse matrix cached session response\n"); return PluginResult::ERR; } const rapidjson::Value &homeserver_json = GetMember(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.GetString(); std::string access_token = access_token_json.GetString(); std::string homeserver = homeserver_json.GetString(); 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(RoomData *room) { rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("typing", true, request_data.GetAllocator()); request_data.AddMember("timeout", 30000, request_data.GetAllocator()); // 30 sec timeout rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "--data-binary", buffer.GetString() }, { "-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(RoomData *room) { rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("typing", false, request_data.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "PUT" }, { "-H", "content-type: application/json" }, { "--data-binary", buffer.GetString() }, { "-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(RoomData *room, const Message *message) { rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("m.fully_read", rapidjson::StringRef(message->event_id.c_str()), request_data.GetAllocator()); request_data.AddMember("m.read", rapidjson::StringRef(message->event_id.c_str()), request_data.GetAllocator()); request_data.AddMember("m.hidden", false, request_data.GetAllocator()); // 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 rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); std::vector additional_args = { { "-X", "POST" }, { "-H", "content-type: application/json" }, { "--data-binary", buffer.GetString() }, { "-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()); rapidjson::Document 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 rapidjson::Value &upload_size_json = GetMember(json_root, "m.upload.size"); if(!upload_size_json.IsNumber()) return PluginResult::ERR; upload_limit = upload_size_json.GetInt(); *upload_size = upload_limit.value(); return PluginResult::OK; } std::shared_ptr Matrix::get_me(RoomData *room) { return room->get_user_by_id(user_id); } RoomData* 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 rooms[room_it->second].get(); } void Matrix::add_room(std::unique_ptr room) { std::lock_guard lock(room_data_mutex); room_data_by_id.insert(std::make_pair(room->id, rooms.size())); rooms.push_back(std::move(room)); } DownloadResult Matrix::download_json(rapidjson::Document &result, const std::string &url, std::vector additional_args, bool use_browser_useragent, std::string *err_msg) const { if(download_to_json(url, result, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { // Cant get error since we parse directory to json. TODO: Make this work somehow? if(err_msg) *err_msg = "Failed to download/parse json"; return DownloadResult::NET_ERR; } return DownloadResult::OK; } }