| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-21 02:56:53 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-21 02:56:53 UTC |
| parent | 40893d4b3ee5ebf2af07094acd3c428be19e2e82 |
| 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()); + } }