git » sdk » commit 7a77cb8

Handle incoming receipts from MAM also

author Stephen Paul Weber
2026-04-18 00:56:20 UTC
committer Stephen Paul Weber
2026-04-18 00:56:20 UTC
parent 3f79d2b8ae7982cf0696a6929f4a1d60990bf704

Handle incoming receipts from MAM also

borogove/Client.hx +17 -11
test/TestClient.hx +48 -0

diff --git a/borogove/Client.hx b/borogove/Client.hx
index c545f52..3dbaaa5 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -589,17 +589,7 @@ class Client extends EventEmitter {
 			}
 		}
 
-		for (receipt in stanza.allTags("received", "urn:xmpp:receipts")) {
-			final id = receipt.attr.get("id");
-			if (id != null) {
-				persistence.updateMessageStatus(
-					this.accountId(),
-					id,
-					MessageDeliveredToDevice,
-					null
-				).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null);
-			}
-		}
+		checkForReceipts(stanza);
 
 		final pubsubEvent = PubsubEvent.fromStanza(stanza);
 		if (pubsubEvent != null && pubsubEvent.getFrom() != null && pubsubEvent.getNode() == "urn:xmpp:avatar:metadata" && pubsubEvent.getItems().length > 0) {
@@ -1759,6 +1749,20 @@ class Client extends EventEmitter {
 		sendQuery(pubsubGet);
 	}
 
+	private function checkForReceipts(stanza: Stanza) {
+		for (receipt in stanza.allTags("received", "urn:xmpp:receipts")) {
+			final id = receipt.attr.get("id");
+			if (id != null) {
+				persistence.updateMessageStatus(
+					this.accountId(),
+					id,
+					MessageDeliveredToDevice,
+					null
+				).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null);
+			}
+		}
+	}
+
 	private function sync(?callback: (Bool)->Void) {
 		if (Std.isOfType(persistence, borogove.persistence.Dummy)) {
 			callback(true); // No reason to sync if we're not storing anyway
@@ -1823,6 +1827,8 @@ class Client extends EventEmitter {
 						).then(m -> [m], _ -> []));
 					case MucInviteStanza(serverId, serverIdBy, reason, password):
 						mucInvite(m.chatId, getChat(m.chatId), m.senderId, m.threadId, serverId, serverIdBy, reason, password);
+					case UnknownMessageStanza(stanza):
+						checkForReceipts(stanza);
 					default:
 						// ignore
 				}
diff --git a/test/TestClient.hx b/test/TestClient.hx
index 3e1ee00..25b65b2 100644
--- a/test/TestClient.hx
+++ b/test/TestClient.hx
@@ -286,13 +286,61 @@ class TestClient extends utest.Test {
 
 		client.stream.onStanza(receiptStanza);
 	}
+
+	public function testHandleReceiptInSync(async: Async) {
+		final persistence = new MockPersistence();
+		final client = new Client("test@example.com", persistence);
+
+		client.stream.on("sendStanza", (stanza: Stanza) -> {
+			final query = stanza.findChild("{urn:xmpp:mam:2}query");
+			if (stanza.name == "iq" && query != null) {
+				final queryId = query.attr.get("queryid");
+
+				final receiptStanza = new Stanza("message", { xmlns: "jabber:client", from: "bob@example.com", to: "test@example.com" })
+					.tag("received", { xmlns: "urn:xmpp:receipts", id: "msg-id" }).up();
+
+				final mamResult = new Stanza("message", { xmlns: "jabber:client", to: "test@example.com", from: "test@example.com" })
+					.tag("result", { xmlns: "urn:xmpp:mam:2", queryid: queryId, id: "mam-id-1" })
+						.tag("forwarded", { xmlns: "urn:xmpp:forward:0" })
+							.tag("delay", { xmlns: "urn:xmpp:delay", stamp: "2023-01-01T00:00:00Z" }).up()
+							.addChild(receiptStanza)
+						.up()
+					.up();
+
+				client.stream.onStanza(mamResult);
+
+				final finishedIq = new Stanza("iq", { xmlns: "jabber:client", type: "result", id: stanza.attr.get("id"), from: "test@example.com" })
+					.tag("fin", { xmlns: "urn:xmpp:mam:2", complete: "true" })
+						.tag("set", { xmlns: "http://jabber.org/protocol/rsm" })
+						.up()
+					.up();
+				client.stream.onStanza(finishedIq);
+			}
+			return EventHandled;
+		});
+
+		client.on("message/new", (data: { message: ChatMessage, event: ChatMessageEvent }) -> {
+			if (data.event == StatusEvent) {
+				Assert.equals("msg-id", data.message.localId);
+				Assert.equals(MessageDeliveredToDevice, data.message.status);
+			}
+			return EventHandled;
+		});
+
+		client.doSync((_) -> {
+			Assert.equals(MessageDeliveredToDevice, persistence.statusUpdates.get("msg-id"));
+			async.done();
+		}, null);
+	}
 }
 
 @:access(borogove)
 class MockPersistence extends Dummy {
+	public var statusUpdates: Map<String, MessageStatus> = [];
 	public function new() { super(); }
 
 	override public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, statusText: Null<String>): Promise<ChatMessage> {
+		statusUpdates.set(localId, status);
 		final builder = new ChatMessageBuilder();
 		builder.localId = localId;
 		builder.status = status;