aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-18 23:32:08 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-18 23:32:12 +0100
commitde45f6d8d7d777244006a7998ec971157e51296e (patch)
tree93ab54b196bda4af7d7fe16d74fb25f08c37a931
parentc56cb5d25388d938fce485ff02b067a2fd70e096 (diff)
Readd meme gpg encryption in matrix, this time asynchronous decryption
of only visible items
-rw-r--r--README.md5
-rw-r--r--TODO3
-rw-r--r--example-config.json3
-rw-r--r--include/BodyItem.hpp3
-rw-r--r--include/Config.hpp1
-rw-r--r--include/Program.hpp2
-rw-r--r--include/StringUtils.hpp2
-rw-r--r--plugins/Matrix.hpp40
-rw-r--r--src/Body.cpp2
-rw-r--r--src/Config.cpp1
-rw-r--r--src/DownloadUtils.cpp8
-rw-r--r--src/FileAnalyzer.cpp6
-rw-r--r--src/Program.cpp8
-rw-r--r--src/QuickMedia.cpp73
-rw-r--r--src/ResourceLoader.cpp8
-rw-r--r--src/StringUtils.cpp12
-rw-r--r--src/plugins/Matrix.cpp218
17 files changed, 337 insertions, 58 deletions
diff --git a/README.md b/README.md
index ea1e71f..216994c 100644
--- a/README.md
+++ b/README.md
@@ -180,7 +180,10 @@ Type text and then wait and QuickMedia will automatically search.\
`/leave`: Leave the current room.\
`/me [text]`: Send a message of type "m.emote".\
`/react [text]`: React to the selected message (also works if you are replying to a message).\
-`/id`: Show the room id.
+`/id`: Show the room id.\
+`/encrypt [text]`: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable "matrix.gpg_user_id".
+## Matrix gpg encryption
+Matrix gpg email should use the matrix user id, but in email format. For example: @dec05eba:matrix.org should be created with a gpg with the email dec05eba@matrix.org.
## Config
Config is loaded from `~/.config/quickmedia/config.json` if it exists. See [example-config.json](https://git.dec05eba.com/QuickMedia/plain/example-config.json) for an example config. All fields in the config file are optional. If you have installed quickmedia then you can copy `/usr/share/quickmedia/example-config.json` to `~/.config/quickmedia/config.json` and use that as a base for your personal config file.\
If `use_system_mpv_config` is set to `true` then your systems mpv config in `~/.config/mpv/mpv.conf` and plugins will be used.
diff --git a/TODO b/TODO
index 87df967..95d7a61 100644
--- a/TODO
+++ b/TODO
@@ -245,4 +245,5 @@ Add option to copy somebody else custom emoji in matrix by pressing enter to bri
Detect invidious urls too, even the ones that dont have watch?v=.. this could be done by downloading the webpage (maybe only HEAD?) to check if it's invidious.
Atomic file operations should use a random generated name instead of .tmp, because multiple instances of quickmedia may be running and they may try to write to the same file at the same time. In such cases they can also write to the same temporary file at the same time.
TODO: https://github.com/matrix-org/synapse/issues/14444.
-Use matrix /sync "since" param. Its beneficial even to quickmedia because synapse is written in such a way that using "since" is faster. \ No newline at end of file
+Use matrix /sync "since" param. Its beneficial even to quickmedia because synapse is written in such a way that using "since" is faster.
+/encrypt should support formatted text like greentext, custom emoji, mentions etc. \ No newline at end of file
diff --git a/example-config.json b/example-config.json
index 790ccc1..1a687c5 100644
--- a/example-config.json
+++ b/example-config.json
@@ -50,7 +50,8 @@
"linuxdelta.com",
"tchncs.de",
"jupiterbroadcasting.com"
- ]
+ ],
+ "gpg_user_id": ""
},
"peertube": {
"known_instances": [
diff --git a/include/BodyItem.hpp b/include/BodyItem.hpp
index 647d344..89adc5d 100644
--- a/include/BodyItem.hpp
+++ b/include/BodyItem.hpp
@@ -36,13 +36,16 @@ namespace QuickMedia {
mgl::vec2f size;
};
+ class BodyItem;
struct Widgets {
+ BodyItem *body_item = nullptr;
std::optional<ThumbnailWidget> thumbnail;
};
// TODO: Remove and create an Userdata class instead to replace the void* userdata in BodyItem
class BodyItemExtra {
public:
+ BodyItemExtra() = default;
virtual ~BodyItemExtra() = default;
virtual void draw_overlay(mgl::Window &window, const Widgets &widgets) {
diff --git a/include/Config.hpp b/include/Config.hpp
index 9edc44b..832824f 100644
--- a/include/Config.hpp
+++ b/include/Config.hpp
@@ -49,6 +49,7 @@ namespace QuickMedia {
struct MatrixConfig {
std::vector<std::string> known_homeservers;
+ std::string gpg_user_id;
};
struct PeertubeConfig {
diff --git a/include/Program.hpp b/include/Program.hpp
index e60a6ae..674c834 100644
--- a/include/Program.hpp
+++ b/include/Program.hpp
@@ -9,6 +9,8 @@ struct ReadProgram {
int read_fd = -1;
};
+int accumulate_string(char *data, int size, void *userdata);
+
/* Return 0 if you want to continue reading. @data is null-terminated */
typedef int (*ProgramOutputCallback)(char *data, int size, void *userdata);
diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp
index 2a01f54..91de829 100644
--- a/include/StringUtils.hpp
+++ b/include/StringUtils.hpp
@@ -9,6 +9,8 @@ namespace QuickMedia {
void string_split(const std::string &str, const std::string &delimiter, StringSplitCallback callback_func, bool include_empty = true);
void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func, bool include_empty = true);
+ void string_split_view(const std::string_view str, const std::string &delimiter, StringSplitCallback callback_func, bool include_empty = true);
+ void string_split_view(const std::string_view str, char delimiter, StringSplitCallback callback_func, bool include_empty = true);
// Returns the number of replaced substrings
size_t string_replace_all(std::string &str, char old_char, char new_char);
// Returns the number of replaced substrings
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index e5c86e8..8b9dcc9 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -30,6 +30,38 @@ namespace QuickMedia {
std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text = true, mgl::vec2i image_max_size = mgl::vec2i(0, 0));
std::string pantalaimon_url_to_homeserver_url(Matrix *matrix, const std::string &url);
Message* get_latest_message_in_edit_chain(Message *message);
+ bool matrix_gpg_encrypt_for_each_user_in_room(Matrix *matrix, RoomData *room, const std::string &my_gpg_user_id, const std::string &str, std::string &encrypted_str);
+
+ struct MatrixChatBodyDecryptJob {
+ enum class DecryptState {
+ NOT_DECRYPTED,
+ DECRYPTING,
+ DECRYPTED,
+ FAILED_TO_DECRYPT
+ };
+
+ std::string text;
+ DecryptState decrypt_state = DecryptState::NOT_DECRYPTED;
+ bool cancel = false;
+ };
+
+ class MatrixChatBodyItemData : public BodyItemExtra {
+ public:
+ enum class DecryptState {
+ NOT_DECRYPTED,
+ DECRYPTING,
+ DECRYPTED
+ };
+
+ MatrixChatBodyItemData(Matrix *matrix, std::string text_to_decrypt) : matrix(matrix), text_to_decrypt(std::move(text_to_decrypt)) {}
+ ~MatrixChatBodyItemData();
+ void draw_overlay(mgl::Window&, const Widgets &widgets) override;
+
+ DecryptState decrypt_state = DecryptState::NOT_DECRYPTED;
+ std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job;
+ Matrix *matrix = nullptr;
+ std::string text_to_decrypt;
+ };
struct TimestampedDisplayData {
std::string data;
@@ -178,7 +210,7 @@ namespace QuickMedia {
bool users_fetched = false;
time_t last_read_message_timestamp = 0;
std::shared_ptr<BodyItem> body_item;
- std::string latest_message;
+ int offset_to_latest_message_text = 0;
// These are messages fetched with |Matrix::get_message_by_id|. Needed to show replies, when replying to old message not part of /sync.
// The value is nullptr if the message is fetched and cached but the event if referenced an invalid message.
@@ -707,6 +739,8 @@ namespace QuickMedia {
std::string body_to_formatted_body(RoomData *room, const std::string &body);
void on_exit_room(RoomData *room);
+ void async_decrypt_message(std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job);
+
// Calls the |MatrixDelegate| pending events.
// Should be called from the main (ui) thread
void update();
@@ -761,6 +795,7 @@ namespace QuickMedia {
void load_silenced_invites();
private:
MessageQueue<std::function<void()>> ui_thread_tasks;
+
std::vector<std::unique_ptr<RoomData>> rooms;
std::unordered_map<std::string, size_t> room_data_by_id; // value is an index into |rooms|
std::recursive_mutex room_data_mutex;
@@ -799,5 +834,8 @@ namespace QuickMedia {
std::unordered_map<std::string, CustomEmoji> custom_emoji_by_key;
std::unordered_set<std::string> silenced_invites;
std::unordered_map<std::string, int64_t> qm_read_markers_by_room_cache;
+
+ MessageQueue<std::shared_ptr<MatrixChatBodyDecryptJob>> decrypt_task;
+ std::thread decrypt_thread;
};
} \ No newline at end of file
diff --git a/src/Body.cpp b/src/Body.cpp
index b21012d..fcf932c 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -1553,6 +1553,7 @@ namespace QuickMedia {
if(item->extra) {
Widgets widgets;
+ widgets.body_item = item.get();
if(thumbnail_drawn) {
ThumbnailWidget thumbnail;
thumbnail.position = image.get_position();
@@ -1624,6 +1625,7 @@ namespace QuickMedia {
if(item->extra) {
Widgets widgets;
+ widgets.body_item = item.get();
if(thumbnail_drawn) {
ThumbnailWidget thumbnail;
thumbnail.position = image.get_position();
diff --git a/src/Config.cpp b/src/Config.cpp
index 6a73dcf..570504f 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -226,6 +226,7 @@ namespace QuickMedia {
config->matrix.known_homeservers.push_back(known_homeserver.asString());
}
}
+ get_json_value(matrix_json, "gpg_user_id", config->matrix.gpg_user_id);
}
if(!has_known_matrix_homeservers_config)
diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp
index 3fb8b19..913857b 100644
--- a/src/DownloadUtils.cpp
+++ b/src/DownloadUtils.cpp
@@ -19,14 +19,6 @@ namespace QuickMedia {
int64_t total_downloaded_size = 0;
};
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
- return 1;
- str->append(data, size);
- return 0;
- }
-
static bool http_is_redirect(const char *header, size_t size) {
const void *end_of_first_line_p = memmem(header, size, "\r\n", 2);
if(!end_of_first_line_p)
diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp
index b02d0c2..65c33b5 100644
--- a/src/FileAnalyzer.cpp
+++ b/src/FileAnalyzer.cpp
@@ -139,12 +139,6 @@ namespace QuickMedia {
}
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- str->append(data, size);
- return 0;
- }
-
bool video_get_middle_frame(const FileAnalyzer &file, const char *destination_path, int width, int height) {
Path destination_path_tmp = destination_path;
destination_path_tmp.append(".tmp.jpg"); // TODO: .png, but the below code also needs to be changed for that
diff --git a/src/Program.cpp b/src/Program.cpp
index ee189f7..8b3cc49 100644
--- a/src/Program.cpp
+++ b/src/Program.cpp
@@ -13,6 +13,14 @@
#define READ_END 0
#define WRITE_END 1
+int accumulate_string(char *data, int size, void *userdata) {
+ std::string *str = (std::string*)userdata;
+ if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
+ return 1;
+ str->append(data, size);
+ return 0;
+}
+
struct ReadWriteProgram {
pid_t pid = -1;
int read_fd = -1;
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 7e37214..074f3e9 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -5311,6 +5311,11 @@ namespace QuickMedia {
body_item->embedded_item = BodyItem::create("");
*body_item->embedded_item = *related_body_item;
+ if(related_body_item->extra) {
+ MatrixChatBodyItemData *other_item_data = static_cast<MatrixChatBodyItemData*>(related_body_item->extra.get());
+ if(other_item_data->decrypt_state != MatrixChatBodyItemData::DecryptState::DECRYPTED)
+ body_item->embedded_item->extra = std::make_shared<MatrixChatBodyItemData>(other_item_data->matrix, other_item_data->text_to_decrypt);
+ }
body_item->embedded_item->embedded_item = nullptr;
body_item->embedded_item->reactions.clear();
if(message->user->user_id != my_user_id && ((related_body_item->userdata && static_cast<Message*>(related_body_item->userdata)->user.get() == me) || message_contains_user_mention(body_item, my_display_name, my_user_id)))
@@ -5325,11 +5330,23 @@ namespace QuickMedia {
return load_cached_related_embedded_item(body_item, message, me.get(), current_room->get_user_display_name(me), me->user_id, message_body_items);
}
+ static void matrix_body_set_text_decrypt_if_needed(Matrix *matrix, BodyItem *body_item, std::string text) {
+ // TODO: Check if gpg is installed
+ if(!get_config().matrix.gpg_user_id.empty() && text.find("-----BEGIN PGP MESSAGE-----") != std::string::npos && text.find("-----END PGP MESSAGE-----") != std::string::npos) {
+ body_item->extra = std::make_shared<MatrixChatBodyItemData>(matrix, std::move(text));
+ body_item->set_description("🔒 Decrypting message...");
+ } else {
+ body_item->set_description(std::move(text));
+ }
+ }
+
static std::shared_ptr<BodyItem> message_to_body_item(Matrix *matrix, RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) {
Message *latest_message = get_latest_message_in_edit_chain(message);
+
auto body_item = BodyItem::create("");
body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH));
- body_item->set_description(strip(message_to_qm_text(matrix, latest_message)));
+ //body_item->set_description(matrix_decrypt_gpg_message_if_needed(strip(message_to_qm_text(matrix, latest_message))));
+ matrix_body_set_text_decrypt_if_needed(matrix, body_item.get(), strip(message_to_qm_text(matrix, latest_message)));
body_item->set_timestamp(message->timestamp);
if(!message->thumbnail_url.empty()) {
body_item->thumbnail_url = message->thumbnail_url;
@@ -5631,7 +5648,8 @@ namespace QuickMedia {
if(message->timestamp > edited_message_ref->timestamp) {
std::string qm_formatted_text = message_to_qm_text(matrix, message.get());
- body_item->set_description(std::move(qm_formatted_text));
+ //body_item->set_description(matrix_decrypt_gpg_message_if_needed(std::move(qm_formatted_text)));
+ matrix_body_set_text_decrypt_if_needed(matrix, body_item.get(), std::move(qm_formatted_text));
if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id))
body_item->set_description_color(get_theme().attention_alert_text_color, true);
else
@@ -5674,7 +5692,8 @@ namespace QuickMedia {
if(message->timestamp > edited_message_ref->timestamp) {
std::string qm_formatted_text = formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true);
- body_item->set_description(std::move(qm_formatted_text));
+ //body_item->set_description(matrix_decrypt_gpg_message_if_needed(std::move(qm_formatted_text)));
+ matrix_body_set_text_decrypt_if_needed(matrix, body_item.get(), std::move(qm_formatted_text));
if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id))
body_item->set_description_color(get_theme().attention_alert_text_color, true);
else
@@ -6028,7 +6047,8 @@ namespace QuickMedia {
"/leave: Leave the current room.\n"
"/me [text]: Send a message of type \"m.emote\".\n"
"/react [text]: React to the selected message (also works if you are replying to a message).\n"
- "/id: Show the room id.";
+ "/id: Show the room id.\n"
+ "/encrypt [text]: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable \"matrix.gpg_user_id\".";
message->timestamp = time(nullptr) * 1000; // TODO: What if the user has broken local time?
matrix->append_system_message(current_room, std::move(message));
@@ -6052,7 +6072,7 @@ namespace QuickMedia {
} else if(strncmp(text.c_str(), "/react ", 7) == 0) {
msgtype = "m.reaction";
text.erase(text.begin(), text.begin() + 7);
- } else {
+ } else if(strncmp(text.c_str(), "/encrypt ", 9) != 0) {
show_notification("QuickMedia", "Error: invalid command: " + text + ", type /help to see a list of valid commands.", Urgency::NORMAL);
return false;
}
@@ -6061,7 +6081,27 @@ namespace QuickMedia {
msgtype = "m.reaction";
text.erase(text.begin(), text.begin() + 7);
}
- }
+ }
+
+ if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::EDITING || chat_state == ChatState::REPLYING) && msgtype.empty() && text[0] == '/' && strncmp(text.c_str(), "/encrypt ", 9) == 0) {
+ text.erase(text.begin(), text.begin() + 9);
+ if(!is_program_executable_by_name("gpg")) {
+ show_notification("QuickMedia", "GPG needs to be installed to use the /encrypt command", Urgency::CRITICAL);
+ return false;
+ }
+
+ if(get_config().matrix.gpg_user_id.empty()) {
+ show_notification("QuickMedia", "The config variable matrix.gpg_user_id needs to be set to use the /encrypt command", Urgency::CRITICAL);
+ return false;
+ }
+
+ std::string encrypted_string;
+ if(!matrix_gpg_encrypt_for_each_user_in_room(matrix, current_room, get_config().matrix.gpg_user_id, text, encrypted_string)) {
+ show_notification("QuickMedia", "Failed to encrypt message with gpg. Make sure you used the correct gpg user id in the config variable matrix.gpg_user_id", Urgency::CRITICAL);
+ return false;
+ }
+ text = std::move(encrypted_string);
+ }
auto message = std::make_shared<Message>();
message->body_is_formatted = true;
@@ -6178,7 +6218,8 @@ namespace QuickMedia {
std::string qm_formatted_text = formatted_text_to_qm_text(matrix, formatted_text.c_str(), formatted_text.size(), true);
auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index);
- body_item_shared_ptr->set_description(std::move(qm_formatted_text));
+ //body_item_shared_ptr->set_description(matrix_decrypt_gpg_message_if_needed(std::move(qm_formatted_text)));
+ matrix_body_set_text_decrypt_if_needed(matrix, body_item_shared_ptr.get(), std::move(qm_formatted_text));
body_item_shared_ptr->set_description_color(get_theme().provisional_message_color);
//auto edit_body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
@@ -6272,6 +6313,11 @@ namespace QuickMedia {
auto related_body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), event_data->event_id);
if(related_body_item) {
*body_item = *related_body_item;
+ if(related_body_item->extra) {
+ MatrixChatBodyItemData *other_item_data = static_cast<MatrixChatBodyItemData*>(related_body_item->extra.get());
+ if(other_item_data->decrypt_state != MatrixChatBodyItemData::DecryptState::DECRYPTED)
+ body_item->extra = std::make_shared<MatrixChatBodyItemData>(other_item_data->matrix, other_item_data->text_to_decrypt);
+ }
body_item->reactions.clear();
if(message_contains_user_mention(related_body_item.get(), current_room->get_user_display_name(me), me->user_id))
body_item->set_description_color(get_theme().attention_alert_text_color, true);
@@ -7616,8 +7662,9 @@ namespace QuickMedia {
break;
}
- if(last_timeline_message != -1) {
- current_room->body_item->set_description(current_room->latest_message);
+ if(last_timeline_message != -1 && (!current_room->body_item->extra || static_cast<MatrixChatBodyItemData*>(current_room->body_item->extra.get())->decrypt_state == MatrixChatBodyItemData::DecryptState::DECRYPTED)) {
+ if(current_room->offset_to_latest_message_text < (int)current_room->body_item->get_description().size())
+ current_room->body_item->set_description(current_room->body_item->get_description().substr(current_room->offset_to_latest_message_text));
current_room->body_item->set_description_color(get_theme().faded_text_color);
// TODO: Show a line like nheko instead for unread messages, or something else
current_room->body_item->set_title_color(get_theme().text_color);
@@ -7876,14 +7923,6 @@ namespace QuickMedia {
}
}
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
- return 1;
- str->append(data, size);
- return 0;
- }
-
void Program::download_page(std::string url, std::string download_filename, bool no_dialog) {
window.set_title(("QuickMedia - Select where you want to save " + std::string(url)).c_str());
diff --git a/src/ResourceLoader.cpp b/src/ResourceLoader.cpp
index 86687d2..a120096 100644
--- a/src/ResourceLoader.cpp
+++ b/src/ResourceLoader.cpp
@@ -26,14 +26,6 @@ namespace QuickMedia {
return resource_root.c_str();
}
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
- return 1;
- str->append(data, size);
- return 0;
- }
-
// If absolute, use the path; otherwise use fc-match to find the font
static bool find_font(const std::string &font_name, std::string &font_filepath_result) {
if(get_config().font.latin.find('/') != std::string::npos) {
diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp
index bc59730..564f307 100644
--- a/src/StringUtils.cpp
+++ b/src/StringUtils.cpp
@@ -3,7 +3,7 @@
namespace QuickMedia {
template <typename T>
- static void string_split_t(const std::string &str, const T &delimiter, StringSplitCallback callback_func, bool include_empty) {
+ static void string_split_t(const std::string_view str, const T &delimiter, StringSplitCallback callback_func, bool include_empty) {
size_t index = 0;
while(index < str.size()) {
size_t new_index = str.find(delimiter, index);
@@ -23,10 +23,18 @@ namespace QuickMedia {
}
void string_split(const std::string &str, const std::string &delimiter, StringSplitCallback callback_func, bool include_empty) {
- string_split_t(str, delimiter, callback_func, include_empty);
+ string_split_t(std::string_view(str), delimiter, callback_func, include_empty);
}
void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func, bool include_empty) {
+ string_split_t(std::string_view(str), delimiter, callback_func, include_empty);
+ }
+
+ void string_split_view(const std::string_view str, const std::string &delimiter, StringSplitCallback callback_func, bool include_empty) {
+ string_split_t(str, delimiter, callback_func, include_empty);
+ }
+
+ void string_split_view(const std::string_view str, char delimiter, StringSplitCallback callback_func, bool include_empty) {
string_split_t(str, delimiter, callback_func, include_empty);
}
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 973571e..953543c 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -41,6 +41,145 @@ namespace QuickMedia {
static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\",\"m.room.canonical_alias\",\"m.space.child\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}";
static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\",\"m.room.canonical_alias\",\"m.space.child\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
+ static bool is_gpg_installed = false;
+ static bool gpg_installed_checked = false;
+
+ static std::string matrix_decrypt_gpg_message_if_needed(std::string message, bool &success) {
+ success = false;
+
+ if(!gpg_installed_checked) {
+ gpg_installed_checked = true;
+ is_gpg_installed = is_program_executable_by_name("gpg");
+ }
+
+ std::string result;
+ if(!is_gpg_installed || get_config().matrix.gpg_user_id.empty()) {
+ result = std::move(message);
+ } else {
+ size_t pgp_begin_index;
+ size_t pgp_end_index;
+ if((pgp_begin_index = message.find("-----BEGIN PGP MESSAGE-----")) != std::string::npos && (pgp_end_index = message.find("-----END PGP MESSAGE-----", pgp_begin_index + 27)) != std::string::npos) {
+ std::string decrypted_string;
+ const char *args[] = { "gpg", "-d", nullptr };
+ const std::string_view pgp_message(message.data() + pgp_begin_index, (pgp_end_index + 25) - pgp_begin_index);
+ if(exec_program_write_stdin(args, pgp_message.data(), pgp_message.size(), accumulate_string, &decrypted_string) != 0) {
+ result = "🔒 Failed to decrypt message:\n" + std::move(message);
+ } else {
+ decrypted_string.insert(0, "🔒 ", strlen("🔒 "));
+ result = std::move(message);
+ result.replace(result.begin() + pgp_begin_index, result.begin() + pgp_begin_index + pgp_message.size(), std::move(decrypted_string));
+ success = true;
+ }
+ } else {
+ result = std::move(message);
+ }
+ }
+ return result;
+ }
+
+ enum class GpgLineType {
+ UID,
+ FPR,
+ UNKNOWN
+ };
+
+ static std::string user_id_to_email_format(const std::string_view user_id) {
+ if(user_id.empty() || user_id[0] != '@')
+ return "";
+
+ size_t colon_index = user_id.find(':');
+ if(colon_index == std::string::npos)
+ return "";
+
+ return std::string(user_id.substr(1, colon_index - 1)) + "@" + std::string(user_id.substr(colon_index + 1));
+ }
+
+ static std::string_view gpg_display_name_extract_email(const std::string_view display_name) {
+ if(display_name.empty() || display_name.back() != '>')
+ return std::string_view();
+
+ size_t email_start_index = display_name.rfind('<');
+ if(email_start_index == std::string::npos)
+ return std::string_view();
+
+ email_start_index += 1;
+ return display_name.substr(email_start_index, display_name.size() - 1 - email_start_index);
+ }
+
+ static bool for_each_user_with_matching_gpg_key(const std::vector<std::shared_ptr<UserInfo>> &users, std::function<void(std::string_view pub_key)> callback) {
+ std::string output;
+ const char *args[] = { "gpg", "--list-keys", "--with-colons", nullptr };
+ if(exec_program(args, accumulate_string, &output) != 0)
+ return false;
+
+ std::unordered_set<std::string> users_by_email;
+ for(auto &user : users) {
+ std::string user_email = user_id_to_email_format(user->user_id);
+ if(!user_email.empty())
+ users_by_email.insert(std::move(user_email));
+ }
+
+ std::string_view fpr;
+ string_split(output, '\n', [&](const char *str, size_t size) {
+ GpgLineType line_type = GpgLineType::UNKNOWN;
+ int column = 0;
+
+ string_split_view(std::string_view(str, size), ':', [&](const char *str, size_t size) {
+ std::string_view section(str, size);
+ if(column == 0) {
+ if(section == "uid")
+ line_type = GpgLineType::UID;
+ else if(section == "fpr")
+ line_type = GpgLineType::FPR;
+ } else if(column == 9) {
+ if(line_type == GpgLineType::UID) {
+ // Assumes that each uid is preceeded with fpr
+ const std::string user_email = std::string(gpg_display_name_extract_email(section));
+ if(!user_email.empty() && users_by_email.find(user_email) != users_by_email.end()) {
+ fprintf(stderr, "found public key %.*s for user %s\n", (int)fpr.size(), fpr.data(), user_email.c_str());
+ callback(fpr);
+ }
+ } else if(line_type == GpgLineType::FPR) {
+ fpr = section;
+ }
+ }
+
+ ++column;
+ return true;
+ });
+
+ return true;
+ });
+
+ return true;
+ }
+
+ bool matrix_gpg_encrypt_for_each_user_in_room(Matrix *matrix, RoomData *room, const std::string &my_gpg_user_id, const std::string &str, std::string &encrypted_str) {
+ auto me = matrix->get_me(room);
+ if(!me) {
+ fprintf(stderr, "Error: matrix->get_me failed\n");
+ return false;
+ }
+
+ std::vector<std::string> user_public_keys;
+ const bool found_users = for_each_user_with_matching_gpg_key(room->get_users_excluding_me(me->user_id), [&](std::string_view pub_key) {
+ user_public_keys.emplace_back(pub_key);
+ });
+
+ if(!found_users) {
+ fprintf(stderr, "Error: gpg --list-keys failed\n");
+ return false;
+ }
+
+ std::vector<const char*> args = { "gpg", "-e", "-r", my_gpg_user_id.c_str(), "-u", my_gpg_user_id.c_str(), "--armor", "--trust-model", "always", "--comment", "This message requires you to use QuickMedia to view it and the user that sent it needs to have your key imported in gpg" };
+ for(const std::string &pub_key : user_public_keys) {
+ args.push_back("-r");
+ args.push_back(pub_key.c_str());
+ }
+ args.push_back(nullptr);
+ return exec_program_write_stdin(args.data(), str.c_str(), str.size(), accumulate_string, &encrypted_str) == 0;
+ }
+
static std::string capitalize(const std::string &str) {
if(str.size() >= 1)
return to_upper(str[0]) + str.substr(1);
@@ -163,6 +302,35 @@ namespace QuickMedia {
}
}
+ MatrixChatBodyItemData::~MatrixChatBodyItemData() {
+ if(decrypt_job)
+ decrypt_job->cancel = true;
+ }
+
+ void MatrixChatBodyItemData::draw_overlay(mgl::Window&, const Widgets &widgets) {
+ switch(decrypt_state) {
+ case DecryptState::NOT_DECRYPTED: {
+ decrypt_state = DecryptState::DECRYPTING;
+ decrypt_job = std::make_shared<MatrixChatBodyDecryptJob>();
+ decrypt_job->text = text_to_decrypt;
+ decrypt_job->decrypt_state = MatrixChatBodyDecryptJob::DecryptState::DECRYPTING;
+ matrix->async_decrypt_message(decrypt_job);
+ break;
+ }
+ case DecryptState::DECRYPTING: {
+ if(decrypt_job->decrypt_state == MatrixChatBodyDecryptJob::DecryptState::DECRYPTED || decrypt_job->decrypt_state == MatrixChatBodyDecryptJob::DecryptState::FAILED_TO_DECRYPT) {
+ decrypt_state = DecryptState::DECRYPTED;
+ widgets.body_item->set_description(std::move(decrypt_job->text));
+ decrypt_job.reset();
+ }
+ break;
+ }
+ case DecryptState::DECRYPTED: {
+ break;
+ }
+ }
+ }
+
bool TimestampedDisplayData::set_data_if_newer(std::string new_data, time_t new_timestamp) {
if(new_timestamp == 0) {
data = std::move(new_data);
@@ -307,6 +475,7 @@ namespace QuickMedia {
std::vector<std::shared_ptr<UserInfo>> RoomData::get_users_excluding_me(const std::string &my_user_id) {
std::lock_guard<std::mutex> lock(user_mutex);
std::vector<std::shared_ptr<UserInfo>> users_excluding_me;
+ // TODO: Optimize
for(auto &[user_id, user] : user_info_by_user_id) {
if(user->user_id != my_user_id) {
users_excluding_me.push_back(user);
@@ -687,6 +856,10 @@ namespace QuickMedia {
return body;
}
+ static bool should_message_by_decrypted(const std::string &text) {
+ return !get_config().matrix.gpg_user_id.empty() && text.find("-----BEGIN PGP MESSAGE-----") != std::string::npos && text.find("-----END PGP MESSAGE-----") != std::string::npos;
+ }
+
void MatrixQuickMedia::update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync, bool sync_is_cache) {
time_t read_marker_message_timestamp = 0;
std::shared_ptr<UserInfo> me = matrix->get_me(room);
@@ -756,11 +929,13 @@ namespace QuickMedia {
if(!unread_mentions && set_room_as_unread)
room_desc += "Unread: ";
- room->latest_message = extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message, custom_emoji_max_size);
- room_desc += room->latest_message;
+ room->offset_to_latest_message_text = room_desc.size();
+ room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message, custom_emoji_max_size);
room->body_item->set_description(std::move(room_desc));
room->body_item->set_description_max_lines(3);
+ if(should_message_by_decrypted(room->body_item->get_description()))
+ room->body_item->extra = std::make_shared<MatrixChatBodyItemData>(matrix, room->body_item->get_description());
if(set_room_as_unread)
room->body_item->set_title_color(get_theme().attention_alert_text_color, true);
room->last_message_read = false;
@@ -768,10 +943,12 @@ namespace QuickMedia {
rooms_page->move_room_to_top(room);
room_tags_page->move_room_to_top(room);
} else if(last_new_message) {
+ room->offset_to_latest_message_text = 0;
room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get(), custom_emoji_max_size));
room->body_item->set_description_color(get_theme().faded_text_color);
room->body_item->set_description_max_lines(3);
- room->latest_message = room->body_item->get_description();
+ if(should_message_by_decrypted(room->body_item->get_description()))
+ room->body_item->extra = std::make_shared<MatrixChatBodyItemData>(matrix, room->body_item->get_description());
rooms_page->move_room_to_top(room);
room_tags_page->move_room_to_top(room);
@@ -1763,6 +1940,10 @@ namespace QuickMedia {
custom_emoji_by_key.clear();
silenced_invites.clear();
qm_read_markers_by_room_cache.clear();
+ if(decrypt_thread.joinable()) {
+ decrypt_task.close();
+ decrypt_thread.join();
+ }
}
bool Matrix::is_initial_sync_finished() {
@@ -2674,14 +2855,6 @@ namespace QuickMedia {
mgl::vec2i image_max_size;
};
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
- return 1;
- str->append(data, size);
- return 0;
- }
-
// TODO: Full proper parsing with tag depth
static int formattext_text_parser_callback(HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) {
FormattedTextParseUserdata &parse_userdata = *(FormattedTextParseUserdata*)userdata;
@@ -3882,7 +4055,7 @@ namespace QuickMedia {
if(is_inside_code_block)
formatted_body += '\n';
else
- formatted_body += "<br/>";
+ formatted_body += "<br>";
}
std::string line_str(str, size);
@@ -3924,6 +4097,26 @@ namespace QuickMedia {
}
+ void Matrix::async_decrypt_message(std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job) {
+ if(!decrypt_thread.joinable()) {
+ decrypt_thread = std::thread([this]() {
+ while(decrypt_task.is_running()) {
+ auto decrypt_job = decrypt_task.pop_wait();
+ if(!decrypt_job)
+ return;
+
+ if(decrypt_job.value()->cancel)
+ continue;
+
+ bool success = false;
+ decrypt_job.value()->text = matrix_decrypt_gpg_message_if_needed(std::move(decrypt_job.value()->text), success);
+ decrypt_job.value()->decrypt_state = success ? MatrixChatBodyDecryptJob::DecryptState::DECRYPTED : MatrixChatBodyDecryptJob::DecryptState::FAILED_TO_DECRYPT;
+ }
+ });
+ }
+ decrypt_task.push(std::move(decrypt_job));
+ }
+
PluginResult Matrix::post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype, const std::string &custom_transaction_id) {
std::string transaction_id = custom_transaction_id;
if(transaction_id.empty())
@@ -5855,7 +6048,6 @@ namespace QuickMedia {
}
void Matrix::update() {
- mgl::Clock timer;
std::optional<std::function<void()>> task;
while((task = ui_thread_tasks.pop_if_available()) != std::nullopt) {
task.value()();