| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-06 20:50:01 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-06 20:50:01 UTC |
| parent | 5f78a92315379fb8430606c5cd6b16901efeddc6 |
| borogove/ChatMessageBuilder.hx | +35 | -0 |
| borogove/Client.hx | +17 | -1 |
| test/TestClient.hx | +49 | -1 |
diff --git a/borogove/ChatMessageBuilder.hx b/borogove/ChatMessageBuilder.hx index c7c1771..b912d44 100644 --- a/borogove/ChatMessageBuilder.hx +++ b/borogove/ChatMessageBuilder.hx @@ -214,6 +214,41 @@ class ChatMessageBuilder { } #end + /** + Create a new ChatMessageBuilder from an existing ChatMessage + **/ + public static function fromMessage(m: ChatMessage): ChatMessageBuilder { + final builder = new ChatMessageBuilder(); + builder.localId = m.localId; + builder.serverId = m.serverId; + builder.serverIdBy = m.serverIdBy; + builder.sortId = m.sortId; + builder.type = m.type; + builder.syncPoint = m.syncPoint; + builder.replyId = m.replyId; + builder.timestamp = m.timestamp; + builder.to = m.to; + builder.from = m.from; + builder.senderId = m.senderId; + builder.recipients = m.recipients.array(); + builder.replyTo = m.replyTo.array(); + builder.replyToMessage = m.replyToMessage; + builder.threadId = m.threadId; + builder.attachments = m.attachments.array(); + builder.reactions = m.reactions; + builder.direction = m.direction; + builder.status = m.status; + builder.statusText = m.statusText; + builder.versions = m.versions.array(); + builder.payloads = m.payloads.array(); + builder.encryption = m.encryption; + builder.linkMetadata = m.linkMetadata.array(); + builder.stanza = m.stanza; + @:privateAccess builder.text = m.text; + builder.lang = m.lang; + return builder; + } + @:allow(borogove) private static function makeModerated(m: ChatMessage, timestamp: String, moderatorId: Null<String>, reason: Null<String>) { final builder = new ChatMessageBuilder(); diff --git a/borogove/Client.hx b/borogove/Client.hx index 292bbfb..6275091 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -1744,7 +1744,23 @@ class Client extends EventEmitter { @:allow(borogove) private function storeMessages(messages: Array<ChatMessage>): Promise<Array<ChatMessage>> { - return persistence.storeMessages(accountId(), messages); + return thenshim.PromiseTools.all(messages.map(m -> { + if (m.encryption?.status == DecryptionFailure && ((!m.isIncoming() && m.localId != null) || m.serverId != null)) { + return persistence.getMessage(accountId(), m.chatId(), m.isIncoming() ? m.serverId : null, m.localId).then(existing -> { + if (existing == null) return m; + + trace("Merging failed decryyption with existing local copy", m, existing); + final b = ChatMessageBuilder.fromMessage(existing); + if (m.serverId != null) b.serverId = m.serverId; + if (m.serverIdBy != null) b.serverIdBy = m.serverIdBy; + b.syncPoint = m.syncPoint; + b.sortId = m.sortId; + b.status = m.status; + return b.build(); + }); + } + return thenshim.Promise.resolve(m); + })).then(updatedMessages -> persistence.storeMessages(accountId(), updatedMessages)); } @:allow(borogove) diff --git a/test/TestClient.hx b/test/TestClient.hx index 0241ca2..4f02a01 100644 --- a/test/TestClient.hx +++ b/test/TestClient.hx @@ -146,6 +146,51 @@ class TestClient extends utest.Test { }); } + public function testDecryptionFailurePreservesLocalMessage(async: Async) { + final persistence = new MessageMockPersistence(); + final client = new Client("test@example.com", persistence); + final chatId = "chat@example.com"; + client.getDirectChat(chatId); + + final localBuilder = new ChatMessageBuilder({ + localId: "local-123", + text: "good original text", + direction: MessageSent, + senderId: "test@example.com", + status: MessagePending + }); + localBuilder.to = JID.parse(chatId); + final localMessage = localBuilder.build(); + + persistence.storeMessages(client.accountId(), [localMessage]).then(_ -> { + final incomingBuilder = new ChatMessageBuilder({ + localId: "local-123", + serverId: "server-456", + serverIdBy: "server.com", + syncPoint: true, + direction: MessageSent, + senderId: "test@example.com", + status: MessageDeliveredToServer, + encryption: new borogove.EncryptionInfo(DecryptionFailure, "omemo"), + text: "failed decryption fallback" + }); + incomingBuilder.sortId = "sort-1"; + incomingBuilder.to = JID.parse(chatId); + + client.storeMessages([incomingBuilder.build()]).then(stored -> { + Assert.equals(1, stored.length); + final m = stored[0]; + Assert.equals("server-456", m.serverId); + Assert.equals("server.com", m.serverIdBy); + Assert.isTrue(m.syncPoint); + Assert.equals("sort-1", m.sortId); + Assert.equals(MessageDeliveredToServer, m.status); + Assert.equals("good original text", m.text); + async.done(); + }); + }); + } + public function testDefaultDisplayName() { final persistence = new Dummy(); final client = new Client("test@example.com", persistence); @@ -714,12 +759,15 @@ class MessageMockPersistence extends Dummy { override public function storeMessages(accountId: String, messages: Array<ChatMessage>): Promise<Array<ChatMessage>> { for (m in messages) { if (m.serverId != null) this.messages.set(m.serverId, m); + if (m.localId != null) this.messages.set(m.localId, m); } return Promise.resolve(messages); } override public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>): Promise<Null<ChatMessage>> { - return Promise.resolve(serverId != null ? messages.get(serverId) : null); + if (serverId != null && messages.exists(serverId)) return Promise.resolve(messages.get(serverId)); + if (localId != null && messages.exists(localId)) return Promise.resolve(messages.get(localId)); + return Promise.resolve(null); } override public function updateMessage(accountId: String, message: ChatMessage) {