diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d65b55..a0b629db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars). * New translation: Albanian. * Translation updates: Crotian, Japanese. +* Updated mod_muc_moderation to upstream. ## 10.3.3 diff --git a/prosody-modules/mod_muc_moderation/README.markdown b/prosody-modules/mod_muc_moderation/README.markdown index 6c914443..4abb1f66 100644 --- a/prosody-modules/mod_muc_moderation/README.markdown +++ b/prosody-modules/mod_muc_moderation/README.markdown @@ -1,7 +1,9 @@ - +summary: Let moderators remove spam and abuse messages +--- + # Introduction This module implements [XEP-0425: Message Moderation]. @@ -16,7 +18,7 @@ role in the channel / group chat. Example [MUC component][doc:chatrooms] configuration: ``` {.lua} -VirtualHost "channels.example.com" "muc" +Component "channels.example.com" "muc" modules_enabled = { "muc_mam", "muc_moderation", @@ -25,20 +27,19 @@ modules_enabled = { # Compatibility -- Should work with Prosody 0.11.x and later. -- Tested with trunk rev `52c6dfa04dba`. -- Message tombstones requires a compatible storage module implementing - a new message replacement API. +- Basic functionality with Prosody 0.11.x and later +- Full functionality with Prosody 0.12.x and `internal` or `sql` + storage^[Replacing moderated messages with tombstones requires new storage API methods.] +- Works with [mod_storage_xmlarchive] ## Clients -- Tested with [Converse.js](https://conversejs.org/) - [v6.0.1](https://github.com/conversejs/converse.js/releases/tag/v6.0.1) +- [Converse.js](https://conversejs.org/) +- [Gajim](https://dev.gajim.org/gajim/gajim/-/issues/10107) +- [clix](https://code.zash.se/clix/rev/6c1953fbe0fa) ### Feature requests -- [Conv](https://github.com/iNPUTmice/Conversations/issues/3722)[ersa](https://github.com/iNPUTmice/Conversations/issues/3920)[tions](https://github.com/iNPUTmice/Conversations/issues/4227) -- [Dino](https://github.com/dino/dino/issues/1133) -- [Gajim](https://dev.gajim.org/gajim/gajim/-/issues/10107) -- [Poezio](https://lab.louiz.org/poezio/poezio/-/issues/3543) -- [Profanity](https://github.com/profanity-im/profanity/issues/1336) +- [Conversations](https://codeberg.org/iNPUTmice/Conversations/issues/20) +- [Dino](https://github.com/dino/dino/issues/1133) +- [Profanity](https://github.com/profanity-im/profanity/issues/1336) diff --git a/prosody-modules/mod_muc_moderation/mod_muc_moderation.lua b/prosody-modules/mod_muc_moderation/mod_muc_moderation.lua index 318175be..06d30b55 100644 --- a/prosody-modules/mod_muc_moderation/mod_muc_moderation.lua +++ b/prosody-modules/mod_muc_moderation/mod_muc_moderation.lua @@ -27,6 +27,7 @@ end -- Namespaces local xmlns_fasten = "urn:xmpp:fasten:0"; local xmlns_moderate = "urn:xmpp:message-moderate:0"; +local xmlns_occupant_id = "urn:xmpp:occupant-id:0"; local xmlns_retract = "urn:xmpp:message-retract:0"; -- Discovering support @@ -34,36 +35,17 @@ module:hook("muc-disco#info", function (event) event.reply:tag("feature", { var = xmlns_moderate }):up(); end); --- Main handling -module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) - local stanza, origin = event.stanza, event.origin; +-- TODO error registry, requires Prosody 0.12+ - -- Collect info we need - local apply_to = stanza.tags[1]; - local moderate_tag = apply_to:get_child("moderate", xmlns_moderate); - if not moderate_tag then return end -- some other kind of fastening? - - local reason = moderate_tag:get_child_text("reason"); - local retract = moderate_tag:get_child("retract", xmlns_retract); - - local room_jid = stanza.attr.to; +-- moderate : function (string, string, string, boolean, string) : boolean, enum, enum, string +local function moderate(actor, room_jid, stanza_id, retract, reason) local room_node = jid.split(room_jid); local room = mod_muc.get_room_from_jid(room_jid); - local stanza_id = apply_to.attr.id; - - -- Permissions - local actor = stanza.attr.from; + -- Permissions is based on role, which is a property of a current occupant, + -- so check if the actor is an occupant, otherwise if they have a reserved + -- nickname that can be used to retrieve the role. local actor_nick = room:get_occupant_jid(actor); - local affiliation = room:get_affiliation(actor); - -- Retrieve their current role, iff they are in the room, otherwise what they - -- would have based on affiliation. - local role = room:get_role(actor_nick) or room:get_default_role(affiliation); - if valid_roles[role or "none"] < valid_roles.moderator then - origin.send(st.error_reply(stanza, "auth", "forbidden", "You need a role of at least 'moderator'")); - return true; - end - if not actor_nick then local reserved_nickname = room:get_affiliation_data(jid.bare(actor), "reserved_nickname"); if reserved_nickname then @@ -71,6 +53,14 @@ module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) end end + -- Retrieve their current role, iff they are in the room, otherwise what they + -- would have based on affiliation. + local affiliation = room:get_affiliation(actor); + local role = room:get_role(actor_nick) or room:get_default_role(affiliation); + if valid_roles[role or "none"] < valid_roles.moderator then + return false, "auth", "forbidden", "You need a role of at least 'moderator'"; + end + -- Original stanza to base tombstone on local original, err; if muc_log_archive.get then @@ -84,13 +74,13 @@ module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) end end end + if not original then if err == "item-not-found" then - origin.send(st.error_reply(stanza, "modify", "item-not-found")); + return false, "modify", "item-not-found"; else - origin.send(st.error_reply(stanza, "wait", "internal-server-error")); + return false, "wait", "internal-server-error"; end - return true; end @@ -106,19 +96,39 @@ module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) announcement:text_tag("reason", reason); end + local moderated_occupant_id = original:get_child("occupant-id", xmlns_occupant_id); + if room.get_occupant_id and moderated_occupant_id then + announcement:add_direct_child(moderated_occupant_id); + end + + local actor_occupant = room:get_occupant_by_real_jid(actor) or room:new_occupant(jid.bare(actor), actor_nick); + if room.get_occupant_id then + -- This isn't a regular broadcast message going through the events occupant_id.lib hooks so we do this here + announcement:add_direct_child(st.stanza("occupant-id", { xmlns = xmlns_occupant_id; id = room:get_occupant_id(actor_occupant) })) + end + if muc_log_archive.set and retract then local tombstone = st.message({ from = original.attr.from, type = "groupchat", id = original.attr.id }) :tag("moderated", { xmlns = xmlns_moderate, by = actor_nick }) :tag("retracted", { xmlns = xmlns_retract, stamp = dt.datetime() }):up(); + if room.get_occupant_id then + tombstone:add_direct_child(st.stanza("occupant-id", { xmlns = xmlns_occupant_id; id = room:get_occupant_id(actor_occupant) })) + + if moderated_occupant_id then + -- Copy occupant id from moderated message + tombstone:add_child(moderated_occupant_id); + end + end + if reason then tombstone:text_tag("reason", reason); end + tombstone:reset(); local was_replaced = muc_log_archive:set(room_node, stanza_id, tombstone); if not was_replaced then - origin.send(st.error_reply(stanza, "wait", "internal-server-error")); - return true; + return false, "wait", "internal-server-error"; end end @@ -126,6 +136,32 @@ module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) module:log("info", "Message with id '%s' in room %s moderated by %s, reason: %s", stanza_id, room_jid, actor, reason); room:broadcast_message(announcement); + return true; +end + +-- Main handling +module:hook("iq-set/bare/" .. xmlns_fasten .. ":apply-to", function (event) + local stanza, origin = event.stanza, event.origin; + + local actor = stanza.attr.from; + local room_jid = stanza.attr.to; + + -- Collect info we need + local apply_to = stanza.tags[1]; + local moderate_tag = apply_to:get_child("moderate", xmlns_moderate); + if not moderate_tag then return end -- some other kind of fastening? + + local reason = moderate_tag:get_child_text("reason"); + local retract = moderate_tag:get_child("retract", xmlns_retract); + + local stanza_id = apply_to.attr.id; + + local ok, error_type, error_condition, error_text = moderate(actor, room_jid, stanza_id, retract, reason); + if not ok then + origin.send(st.error_reply(stanza, error_type, error_condition, error_text)); + return true; + end + origin.send(st.reply(stanza)); return true; end);