git » sdk » commit 2875dd7

Add ability to moderate a message

author Stephen Paul Weber
2026-04-21 02:56:53 UTC
committer Stephen Paul Weber
2026-04-21 02:56:53 UTC
parent 40893d4b3ee5ebf2af07094acd3c428be19e2e82

Add ability to moderate a message

borogove/Chat.hx +32 -0
test/TestChat.hx +65 -0

diff --git a/borogove/Chat.hx b/borogove/Chat.hx
index 65f3706..9eee475 100644
--- a/borogove/Chat.hx
+++ b/borogove/Chat.hx
@@ -240,6 +240,22 @@ abstract class Chat {
 	**/
 	abstract public function correctMessage(correct:ChatMessage, message:ChatMessageBuilder):Void;
 
+	/**
+		Moderate a message by replacing it with a tombstone (if permitted)
+
+		@param message the message to moderate
+		@param reason the reason for moderating this message
+	**/
+	public function moderate(message: ChatMessage, reason: String) {
+		if (message.serverId == null || message.serverIdBy != chatId) return;
+
+		final iq = new Stanza("iq", { type: "set", to: chatId })
+			.tag("moderate", { xmlns: "urn:xmpp:message-moderate:1", id: message.serverId })
+			.textTag("retract", "", { xmlns: "urn:xmpp:message-retract:1" })
+			.textTag("reason", reason);
+		stream.sendIq(iq, (response) -> {});
+	}
+
 	/**
 		Add new reaction to a message in this Chat
 
@@ -769,6 +785,13 @@ abstract class Chat {
 		return Caps.withFeature(getCaps(), "urn:xmpp:noreply:0").length < 1;
 	}
 
+	/**
+		Can the user send messages to this chat?
+	**/
+	public function canModerate() {
+		return false;
+	}
+
 	/**
 		Invite another chat's participants to participate in this one
 	**/
@@ -1425,6 +1448,15 @@ class Channel extends Chat {
 		return p.mucUser.role != "visitor";
 	}
 
+	override public function canModerate() {
+		if (_nickInUse == null) return false;
+
+		final p = presence[_nickInUse];
+		if (p == null) return false;
+
+		return disco.features.contains("urn:xmpp:message-moderate:1") && p.mucUser.role == "moderator";
+	}
+
 	@:allow(borogove)
 	override private function getCaps():KeyValueIterator<String, Caps> {
 		return ["" => disco].keyValueIterator();
diff --git a/test/TestChat.hx b/test/TestChat.hx
index b3c316e..4c6323c 100644
--- a/test/TestChat.hx
+++ b/test/TestChat.hx
@@ -211,4 +211,69 @@ class TestChat extends utest.Test {
 
 		chat.getMessagesAfter(message);
 	}
+
+	public function testModerate(async: Async) {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		final chat = new borogove.Chat.Channel(client, client.stream, persistence, "channel@example.com");
+		final builder = new ChatMessageBuilder();
+		builder.serverId = "msg123";
+		builder.serverIdBy = "channel@example.com";
+		builder.to = JID.parse("test@example.com");
+		builder.from = JID.parse("channel@example.com/spammer");
+		builder.senderId = "friend@example.com";
+		final message = builder.build();
+
+		client.stream.on("sendStanza", (stanza: Stanza) -> {
+			if (stanza.name == "iq" && stanza.attr.get("type") == "set") {
+				Assert.notNull(stanza.attr.get("id"));
+				Assert.equals("channel@example.com", stanza.attr.get("to"));
+				final moderate = stanza.getChild("moderate", "urn:xmpp:message-moderate:1");
+				if (moderate != null) {
+					Assert.equals("msg123", moderate.attr.get("id"));
+					Assert.notNull(moderate.getChild("retract", "urn:xmpp:message-retract:1"));
+					Assert.equals("Spam", moderate.getChild("reason").getText());
+					async.done();
+					return EventHandled;
+				}
+			}
+			return EventUnhandled;
+		});
+
+		chat.moderate(message, "Spam");
+	}
+
+	public function testCanModerateDirectChat() {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		final chat = client.getDirectChat("friend@example.com");
+		Assert.isFalse(chat.canModerate());
+	}
+
+	public function testCanModerateChannel() {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		final chat = new borogove.Chat.Channel(client, client.stream, persistence, "channel@example.com");
+
+		// Default
+		Assert.isFalse(chat.canModerate());
+
+		// Feature present but not moderator
+		chat.disco = new borogove.Caps("", [], ["urn:xmpp:message-moderate:1", "http://jabber.org/protocol/muc"], []);
+		Assert.isFalse(chat.canModerate());
+
+		// Nick in use set
+		chat._nickInUse = "mynick";
+		Assert.isFalse(chat.canModerate());
+
+		// Presence set but not moderator
+		final p = new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "participant" }).up(), null);
+		chat.presence.set("mynick", p);
+		Assert.isFalse(chat.canModerate());
+
+		// Is moderator
+		final p2 = new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "moderator" }).up(), null);
+		chat.presence.set("mynick", p2);
+		Assert.isTrue(chat.canModerate());
+	}
 }