git » sdk » commit 3f79d2b

Request and process delivery reciepts

author Stephen Paul Weber
2026-04-13 01:19:22 UTC
committer Stephen Paul Weber
2026-04-13 01:19:22 UTC
parent 1b44c7b382a10357b97396526bd91d807d22be32

Request and process delivery reciepts

borogove/ChatMessage.hx +3 -0
borogove/Client.hx +12 -0
test/TestChatMessageBuilder.hx +28 -1
test/TestClient.hx +41 -1

diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx
index e5a15f5..d1d3483 100644
--- a/borogove/ChatMessage.hx
+++ b/borogove/ChatMessage.hx
@@ -599,6 +599,9 @@ class ChatMessage {
 		for (payload in payloads) {
 			stanza.addDirectChild(Element(payload));
 		}
+		if (type == MessageChat) {
+			stanza.tag("request", { xmlns: "urn:xmpp:receipts" }).up();
+		}
 		return stanza;
 	}
 }
diff --git a/borogove/Client.hx b/borogove/Client.hx
index 21b0bfa..c545f52 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -589,6 +589,18 @@ 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);
+			}
+		}
+
 		final pubsubEvent = PubsubEvent.fromStanza(stanza);
 		if (pubsubEvent != null && pubsubEvent.getFrom() != null && pubsubEvent.getNode() == "urn:xmpp:avatar:metadata" && pubsubEvent.getItems().length > 0) {
 			final item = pubsubEvent.getItems()[0];
diff --git a/test/TestChatMessageBuilder.hx b/test/TestChatMessageBuilder.hx
index 0f74653..4dd814f 100644
--- a/test/TestChatMessageBuilder.hx
+++ b/test/TestChatMessageBuilder.hx
@@ -3,8 +3,9 @@ package test;
 import utest.Assert;
 import utest.Async;
 
-import borogove.Html;
 import borogove.ChatMessageBuilder;
+import borogove.Html;
+import borogove.JID;
 
 @:access(borogove)
 class TestChatMessageBuilder extends utest.Test {
@@ -96,4 +97,30 @@ class TestChatMessageBuilder extends utest.Test {
 		Assert.equals("*hello*", msgHtml.text);
 		Assert.equals("<html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\"><b>hello</b></body></html>", msgHtml.payloads[0].toString());
 	}
+
+	public function testReceiptRequest() {
+		final builder = new ChatMessageBuilder();
+		builder.localId = "test-id";
+		builder.from = JID.parse("alice@example.com");
+		builder.to = JID.parse("bob@example.com");
+		builder.senderId = "alice@example.com";
+		builder.type = MessageChat;
+		builder.setBody(Html.text("Hello"));
+		final msg = builder.build();
+		final stanza = msg.asStanza();
+		Assert.notNull(stanza.getChild("request", "urn:xmpp:receipts"));
+	}
+
+	public function testNoReceiptRequestForGroupchat() {
+		final builder = new ChatMessageBuilder();
+		builder.localId = "test-id";
+		builder.from = JID.parse("alice@example.com");
+		builder.to = JID.parse("room@example.com");
+		builder.senderId = "alice@example.com";
+		builder.type = MessageChannel;
+		builder.setBody(Html.text("Hello"));
+		final msg = builder.build();
+		final stanza = msg.asStanza();
+		Assert.isNull(stanza.getChild("request", "urn:xmpp:receipts"));
+	}
 }
diff --git a/test/TestClient.hx b/test/TestClient.hx
index f0cf8f6..3e1ee00 100644
--- a/test/TestClient.hx
+++ b/test/TestClient.hx
@@ -1,11 +1,16 @@
 package test;
 
+import thenshim.Promise;
 import utest.Assert;
 import utest.Async;
 
+import borogove.Chat;
+import borogove.ChatMessage;
+import borogove.ChatMessageBuilder;
 import borogove.Client;
+import borogove.JID;
+import borogove.Message;
 import borogove.Stanza;
-import borogove.Chat;
 import borogove.persistence.Dummy;
 
 using Lambda;
@@ -262,4 +267,39 @@ class TestClient extends utest.Test {
 				.textTag("nick", "Stranger", { xmlns: "http://jabber.org/protocol/nick" })
 		);
 	}
+
+	public function testHandleReceipt(async: Async) {
+		final persistence = new MockPersistence();
+		final client = new Client("test@example.com", persistence);
+
+		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);
+				async.done();
+			}
+			return EventHandled;
+		});
+
+		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();
+
+		client.stream.onStanza(receiptStanza);
+	}
+}
+
+@:access(borogove)
+class MockPersistence extends Dummy {
+	public function new() { super(); }
+
+	override public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, statusText: Null<String>): Promise<ChatMessage> {
+		final builder = new ChatMessageBuilder();
+		builder.localId = localId;
+		builder.status = status;
+		builder.from = JID.parse("bob@example.com");
+		builder.to = JID.parse(accountId);
+		builder.senderId = "bob@example.com";
+		builder.replyTo = [JID.parse("bob@example.com")];
+		return Promise.resolve(builder.build());
+	}
 }