peertube-plugin-livechat/prosody-modules/mod_muc_moderation/mod_muc_moderation.lua

179 lines
5.7 KiB
Lua
Raw Normal View History

2021-05-11 14:49:49 +00:00
-- mod_muc_moderation
--
2024-05-23 10:18:17 +00:00
-- SPDX-FileCopyrightText: 2015-2021 Kim Alvefur
2021-05-11 14:49:49 +00:00
--
2024-05-23 10:18:17 +00:00
-- SPDX-License-Identifier: MIT
2021-05-11 14:49:49 +00:00
--
-- Implements: XEP-0425: Message Moderation
--
-- Imports
local dt = require "util.datetime";
local id = require "util.id";
local jid = require "util.jid";
local st = require "util.stanza";
-- Plugin dependencies
local mod_muc = module:depends "muc";
local muc_util = module:require "muc/util";
local valid_roles = muc_util.valid_roles;
local muc_log_archive = module:open_store("muc_log", "archive");
if not muc_log_archive.set then
module:log("warn", "Selected archive storage module does not support message replacement, no tombstones will be saved");
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";
2021-05-11 14:49:49 +00:00
local xmlns_retract = "urn:xmpp:message-retract:0";
-- Discovering support
module:hook("muc-disco#info", function (event)
event.reply:tag("feature", { var = xmlns_moderate }):up();
end);
-- TODO error registry, requires Prosody 0.12+
2021-05-11 14:49:49 +00:00
-- moderate : function (string, string, string, boolean, string) : boolean, enum, enum, string
local function moderate(actor, room_jid, stanza_id, retract, reason)
2021-05-11 14:49:49 +00:00
local room_node = jid.split(room_jid);
local room = mod_muc.get_room_from_jid(room_jid);
-- 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.
2021-05-11 14:49:49 +00:00
local actor_nick = room:get_occupant_jid(actor);
if not actor_nick then
local reserved_nickname = room:get_affiliation_data(jid.bare(actor), "reserved_nickname");
if reserved_nickname then
actor_nick = room.jid .. "/" .. reserved_nickname;
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
2021-05-11 14:49:49 +00:00
-- Original stanza to base tombstone on
local original, err;
if muc_log_archive.get then
original, err = muc_log_archive:get(room_node, stanza_id);
else
-- COMPAT missing :get API
err = "item-not-found";
for i, item in muc_log_archive:find(room_node, { key = stanza_id, limit = 1 }) do
if i == stanza_id then
original, err = item, nil;
end
end
end
2021-05-11 14:49:49 +00:00
if not original then
if err == "item-not-found" then
return false, "modify", "item-not-found";
2021-05-11 14:49:49 +00:00
else
return false, "wait", "internal-server-error";
2021-05-11 14:49:49 +00:00
end
end
local actor_occupant = room:get_occupant_by_real_jid(actor) or room:new_occupant(jid.bare(actor), actor_nick);
2021-05-11 14:49:49 +00:00
local announcement = st.message({ from = room_jid, type = "groupchat", id = id.medium(), })
:tag("apply-to", { xmlns = xmlns_fasten, id = stanza_id })
:tag("moderated", { xmlns = xmlns_moderate, by = 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_child(st.stanza("occupant-id", { xmlns = xmlns_occupant_id; id = room:get_occupant_id(actor_occupant) }));
end
if retract then
announcement:tag("retract", { xmlns = xmlns_retract }):up();
end
2021-05-11 14:49:49 +00:00
if reason then
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
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() });
if room.get_occupant_id then
tombstone:add_child(st.stanza("occupant-id", { xmlns = xmlns_occupant_id; id = room:get_occupant_id(actor_occupant) }));
end
tombstone:up();
if room.get_occupant_id then
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();
2021-05-11 14:49:49 +00:00
local was_replaced = muc_log_archive:set(room_node, stanza_id, tombstone);
if not was_replaced then
return false, "wait", "internal-server-error";
2021-05-11 14:49:49 +00:00
end
end
-- Done, tell people about it
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
2021-05-11 14:49:49 +00:00
origin.send(st.reply(stanza));
return true;
end);
module:hook("muc-message-is-historic", function (event)
-- Ensure moderation messages are stored
if event.stanza.attr.from == event.room.jid then
return event.stanza:get_child("apply-to", xmlns_fasten);
end
end, 1);