diff --git a/src/database.c b/src/database.c index 2f3fcb66af..4991fe1f1f 100644 --- a/src/database.c +++ b/src/database.c @@ -896,3 +896,90 @@ _migrate_to_v3(void) return FALSE; } + +gboolean +log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id) +{ + if (!g_chatlog_database || !stanza_id || !archive_id || !room_or_contact_jid) { + return FALSE; + } + + const char* our_barejid = connection_get_jid()->barejid; + sqlite3_stmt* stmt = NULL; + gboolean success = FALSE; + const char* type_str = _get_message_type_str(type); + + if (!type_str) { + return FALSE; + } + + auto_sqlite char* query = sqlite3_mprintf( + "UPDATE `ChatLogs` " + "SET `archive_id` = %Q " + "WHERE `stanza_id` = %Q " + " AND `from_jid` = %Q " + " AND `to_jid` = %Q " + " AND `type` = %Q " + " AND `archive_id` IS NULL", + archive_id, stanza_id, our_barejid, room_or_contact_jid, type_str); + + if (query) { + if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL)) { + if (sqlite3_step(stmt) == SQLITE_DONE) { + success = TRUE; + } + sqlite3_finalize(stmt); + } + } + + return success; +} + +char* +log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id) +{ + if (!g_chatlog_database || !from_jid || !to_jid) { + return NULL; + } + + sqlite3_stmt* stmt = NULL; + char* decrypted_text = NULL; + auto_sqlite char* query = NULL; + const char* type_str = _get_message_type_str(type); + + if (!type_str) { + return NULL; + } + + if (stanza_id && strlen(stanza_id) > 0) { + query = sqlite3_mprintf( + "SELECT `message` FROM `ChatLogs` " + "WHERE `stanza_id` = %Q " + " AND `type` = %Q " + " AND ((`from_jid` = %Q AND `to_jid` = %Q) OR (`from_jid` = %Q AND `to_jid` = %Q)) " + "LIMIT 1", + stanza_id, type_str, from_jid, to_jid, to_jid, from_jid); + } else if (archive_id && strlen(archive_id) > 0) { + query = sqlite3_mprintf( + "SELECT `message` FROM `ChatLogs` " + "WHERE `archive_id` = %Q " + " AND `type` = %Q " + " AND ((`from_jid` = %Q AND `to_jid` = %Q) OR (`from_jid` = %Q AND `to_jid` = %Q)) " + "LIMIT 1", + archive_id, type_str, from_jid, to_jid, to_jid, from_jid); + } + + if (query) { + if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL)) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* text = (const char*)sqlite3_column_text(stmt, 0); + if (text) { + decrypted_text = strdup(text); + } + } + sqlite3_finalize(stmt); + } + } + + return decrypted_text; +} diff --git a/src/database.h b/src/database.h index 129f25eab6..ae24b52176 100644 --- a/src/database.h +++ b/src/database.h @@ -27,6 +27,8 @@ int log_database_get_chat_count(const gchar* const contact_barejid, const char* int log_database_get_muc_count(const gchar* const room_jid, const char* start_time, const char* end_time); ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last); ProfMessage* log_database_get_limits_info_muc(const gchar* const room_jid, gboolean is_last); +gboolean log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id); +char* log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id); void log_database_close(void); #endif // DATABASE_H diff --git a/src/event/server_events.c b/src/event/server_events.c index 28752dcf44..26becb6cc2 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -330,8 +330,14 @@ sv_ev_room_message(ProfMessage* message) gboolean is_duplicate = FALSE; // only log message not coming from this client (but maybe same account, different client) - // our messages are logged when outgoing - if (!(g_strcmp0(mynick, message->from_jid->resourcepart) == 0 && message_is_sent_by_us(message, TRUE))) { + // our messages are logged when outgoing, and we update them when reflected + gboolean is_self_reflection = (g_strcmp0(mynick, message->from_jid->resourcepart) == 0 && message_is_sent_by_us(message, TRUE)); + + if (is_self_reflection) { + if (message->id && message->stanzaid) { + log_database_update_archive_id(PROF_MSG_TYPE_MUC, message->from_jid->barejid, message->id, message->stanzaid); + } + } else { if (!_log_muc(message)) { is_duplicate = TRUE; } @@ -651,6 +657,16 @@ _sv_ev_incoming_plain(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa void sv_ev_incoming_message(ProfMessage* message) { + gboolean is_self_reflection = (message->is_mam && equals_our_barejid(message->from_jid->barejid)); + + if (is_self_reflection) { + if (message->id && message->stanzaid && message->to_jid) { + log_database_update_archive_id(PROF_MSG_TYPE_CHAT, message->to_jid->barejid, message->id, message->stanzaid); + } + rosterwin_roster(); + return; + } + gboolean new_win = FALSE; ProfChatWin* chatwin; char* looking_for_jid = message->from_jid->barejid; diff --git a/src/xmpp/message.c b/src/xmpp/message.c index a9293d5587..f01d22497f 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -35,6 +35,7 @@ #include "xmpp/xmpp.h" #include "xmpp/form.h" #include "xmpp/iq.h" +#include "database.h" #ifdef HAVE_OMEMO #include "xmpp/omemo.h" @@ -1136,9 +1137,27 @@ _handle_groupchat(xmpp_stanza_t* const stanza, gboolean is_mam, const char* resu #ifdef HAVE_OMEMO message->plain = omemo_receive_message(stanza, &message->trusted, &message->omemo_err); if (message->omemo_err != OMEMO_ERR_NONE) { - message->enc = PROF_MSG_ENC_OMEMO; - if (message->plain == NULL) { - message->plain = omemo_error_to_string(message->omemo_err); + // Get previously decrypted plaintext from database + // Historical replays (MAM and standard delayed MUC history) fail decryption due to OMEMO key deletion. + // Realtime messages skip this. + char* db_decrypted = log_database_get_decrypted_message( + PROF_MSG_TYPE_MUC, + from_jid->barejid, + connection_get_jid()->barejid, + message->id, + message->stanzaid); + if (db_decrypted) { + if (message->plain) { + free(message->plain); + } + message->plain = db_decrypted; + message->omemo_err = OMEMO_ERR_NONE; + message->enc = PROF_MSG_ENC_OMEMO; + } else { + message->enc = PROF_MSG_ENC_OMEMO; + if (message->plain == NULL) { + message->plain = omemo_error_to_string(message->omemo_err); + } } } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; @@ -1487,9 +1506,26 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c // check omemo encryption message->plain = omemo_receive_message(stanza, &message->trusted, &message->omemo_err); if (message->omemo_err != OMEMO_ERR_NONE) { - message->enc = PROF_MSG_ENC_OMEMO; - if (message->plain == NULL) { - message->plain = omemo_error_to_string(message->omemo_err); + // Get previously decrypted plaintext from database + // Historical replays (MAM) fail decryption due to OMEMO key deletion/self-reflections. + char* db_decrypted = log_database_get_decrypted_message( + PROF_MSG_TYPE_CHAT, + message->from_jid->barejid, + message->to_jid ? message->to_jid->barejid : connection_get_jid()->barejid, + message->id, + message->stanzaid); + if (db_decrypted) { + if (message->plain) { + free(message->plain); + } + message->plain = db_decrypted; + message->omemo_err = OMEMO_ERR_NONE; + message->enc = PROF_MSG_ENC_OMEMO; + } else { + message->enc = PROF_MSG_ENC_OMEMO; + if (message->plain == NULL) { + message->plain = omemo_error_to_string(message->omemo_err); + } } } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; diff --git a/tests/unittests/database/stub_database.c b/tests/unittests/database/stub_database.c index cedfc16c84..8de1de65b0 100644 --- a/tests/unittests/database/stub_database.c +++ b/tests/unittests/database/stub_database.c @@ -37,3 +37,15 @@ void log_database_close(void) { } + +gboolean +log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id) +{ + return FALSE; +} + +char* +log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id) +{ + return NULL; +}