diff --git a/prosody-modules/mod_muc_moderation_delay/README.md b/prosody-modules/mod_muc_moderation_delay/README.md new file mode 100644 index 00000000..7765e220 --- /dev/null +++ b/prosody-modules/mod_muc_moderation_delay/README.md @@ -0,0 +1,25 @@ + +# mod_muc_moderation_delay + +With this module, you can apply a delay to groupchat messages delivery, so that room moderators can moderate them before other participants receives them. + +This module is part of peertube-plugin-livechat, and is under the same LICENSE. +This module can work on any Prosody server (version >= 0.12.x). + +## Configuration + +Just enable the module on your MUC component. +The feature will be accessible throught the room configuration form. + +The position in the room config form can be changed be setting the option `moderation_delay_form_position`. +This value will be passed as priority for the "muc-config-form" hook. +By default, the field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom. + +``` lua +VirtualHost "muc.example.com" + modules_enabled = { "muc_moderation_delay" } + moderation_delay_form_position = 96 +``` diff --git a/prosody-modules/mod_muc_moderation_delay/config.lib.lua b/prosody-modules/mod_muc_moderation_delay/config.lib.lua new file mode 100644 index 00000000..7fbb1af2 --- /dev/null +++ b/prosody-modules/mod_muc_moderation_delay/config.lib.lua @@ -0,0 +1,61 @@ +-- SPDX-FileCopyrightText: 2024 John Livingston +-- SPDX-License-Identifier: AGPL-3.0-only + +-- Getter/Setter +local function get_moderation_delay(room) + return room._data.moderation_delay or nil; +end + +local function set_moderation_delay(room, delay) + if delay == 0 then + delay = nil; + end + if delay ~= nil then + delay = assert(tonumber(delay), "Moderation delay is not a valid number"); + if delay < 0 then + delay = nil; + end + end + + if get_moderation_delay(room) == delay then return false; end + + room._data.moderation_delay = delay; + return true; +end + +-- Discovering support +local function add_disco_form(event) + table.insert(event.form, { + name = "muc#roominfo_moderation_delay"; + value = ""; + }); + event.formdata["muc#roominfo_moderation_delay"] = get_moderation_delay(event.room); +end + + +-- Config form declaration +local function add_form_option(event) + table.insert(event.form, { + name = "muc#roomconfig_moderation_delay"; + type = "text-single"; + datatype = "xs:integer"; + range_min = 0; + range_max = 60; -- do not allow too big values, it does not make sense. + label = "Moderation delay (0=disabled, any positive integer= messages will be delayed for X seconds for non-moderator participants.)"; + -- desc = ""; + value = get_moderation_delay(event.room); + }); +end + +local function config_submitted(event) + set_moderation_delay(event.room, event.value); + -- no need to 104 status, this feature is invisible for regular participants. +end + +return { + set_moderation_delay = set_moderation_delay; + get_moderation_delay = get_moderation_delay; + add_disco_form = add_disco_form; + add_form_option = add_form_option; + config_submitted = config_submitted; +} diff --git a/prosody-modules/mod_muc_moderation_delay/delay.lib.lua b/prosody-modules/mod_muc_moderation_delay/delay.lib.lua new file mode 100644 index 00000000..2712849f --- /dev/null +++ b/prosody-modules/mod_muc_moderation_delay/delay.lib.lua @@ -0,0 +1,59 @@ +-- SPDX-FileCopyrightText: 2024 John Livingston +-- SPDX-License-Identifier: AGPL-3.0-only +local async = require "util.async"; +local get_moderation_delay = module:require("config").get_moderation_delay; + +local muc_util = module:require "muc/util"; +local valid_roles = muc_util.valid_roles; + +local function handle_broadcast_message(event) + local room, stanza = event.room, event.stanza; + local delay = get_moderation_delay(room); + if delay == nil then + return; + end + + -- only delay groupchat messages with body. + if stanza.attr.type ~= "groupchat" then + return; + end + if not stanza:get_child("body") then + return; + end + + local id = stanza.attr.id; + if not id then + -- message should alway have an id, but just in case... + module:log("warn", "Message has no id, wont delay it."); + return; + end + + -- TODO: detect message retractation, and stop broadcast for any waiting message. + + -- Message must be delayed, except for: + -- * room moderators + -- * the user that sent the message (if they don't get the echo quickly, their clients could have weird behaviours) + + module:log("debug", "Message must be delayed by %i seconds, sending first broadcast wave.", delay); + local moderator_role_value = valid_roles["moderator"]; + local func = function (nick, occupant) + if valid_roles[occupant.role or "none"] >= moderator_role_value then + return true; + end + if nick == stanza.attr.from then + return true; + end + return false; + end; + room:broadcast(stanza, func); + async.sleep(delay); + module:log("debug", "Message has been delayed, sending to remaining participants."); + room:broadcast(stanza, function (nick, occupant) + return not func(nick, occupant); + end); + return true; -- stop the default process +end + +return { + handle_broadcast_message = handle_broadcast_message; +}; diff --git a/prosody-modules/mod_muc_moderation_delay/mod_muc_moderation_delay.lua b/prosody-modules/mod_muc_moderation_delay/mod_muc_moderation_delay.lua new file mode 100644 index 00000000..61d9abc1 --- /dev/null +++ b/prosody-modules/mod_muc_moderation_delay/mod_muc_moderation_delay.lua @@ -0,0 +1,30 @@ +-- mod_muc_moderation_delay +-- +-- SPDX-FileCopyrightText: 2024 John Livingston +-- SPDX-License-Identifier: AGPL-3.0-only +-- +-- This file is AGPL-v3 licensed. +-- Please see the Peertube livechat plugin copyright information. +-- https://livingston.frama.io/peertube-plugin-livechat/credits/ +-- + +local add_disco_form = module:require("config").add_disco_form; +local config_submitted = module:require("config").config_submitted; +local add_form_option = module:require("config").add_form_option; +local handle_broadcast_message = module:require("delay").handle_broadcast_message; + +-- form_position: the position in the room config form (this value will be passed as priority for the "muc-config-form" hook). +-- By default, field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom +local form_position = module:get_option_number("moderation_delay_form_position") or 80-2; + +-- Plugin dependencies +local mod_muc = module:depends "muc"; + +-- muc-disco and muc-config to configure the feature: +module:hook("muc-disco#info", add_disco_form); +module:hook("muc-config-submitted/muc#roomconfig_moderation_delay", config_submitted); +module:hook("muc-config-form", add_form_option, form_position); + +-- intercept muc-broadcast-message, and broadcast with delay if required. +-- Priority is negative, as we want it to be the last handler. +module:hook("muc-broadcast-message", handle_broadcast_message, -1000); diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index bf5ed4d6..ba093c9f 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -249,6 +249,9 @@ class ProsodyConfigContent { if (chatTerms) { this.muc.set('muc_terms_global', new ConfigEntryValueMultiLineString(chatTerms)) } + + this.muc.add('modules_enabled', 'muc_moderation_delay') + this.muc.add('moderation_delay_form_position', 118) } useAnonymous (autoBanIP: boolean): void {