Software /
code /
prosody-modules
File
mod_muc_inject_mentions/mod_muc_inject_mentions.lua @ 5119:048e339706ba
mod_rest: Remove manual reference expansion in schema
This hack was originally added to reduce the number of definitions of
common attributes (type, to, from etc) and payloads (e.g. delay). This
predated pointers and references, and until now was needed because
parsing picked out the correct stanza kind from the schema, which broke
internal references.
Removing this hack paves the way for allowing the schema to be
configured or customized more easily.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Tue, 20 Dec 2022 21:48:28 +0100 |
parent | 4312:33a41503b9e3 |
line wrap: on
line source
module:depends("muc"); local jid_resource = require "util.jid".resource; local st = require "util.stanza"; local prefixes = module:get_option_set("muc_inject_mentions_prefixes", {}) local suffixes = module:get_option_set("muc_inject_mentions_suffixes", {}) local enabled_rooms = module:get_option("muc_inject_mentions_enabled_rooms", nil) local disabled_rooms = module:get_option("muc_inject_mentions_disabled_rooms", nil) local mention_delimiters = module:get_option_set("muc_inject_mentions_mention_delimiters", {" ", "", "\n", "\t"}) local append_mentions = module:get_option("muc_inject_mentions_append_mentions", false) local strip_out_prefixes = module:get_option("muc_inject_mentions_strip_out_prefixes", false) local reserved_nicks = module:get_option("muc_inject_mentions_reserved_nicks", false) local use_bare_jid = module:get_option("muc_inject_mentions_use_bare_jid", true) local prefix_mandatory = module:get_option("muc_inject_mentions_prefix_mandatory", false) local reserved_nicknames = {} local reference_xmlns = "urn:xmpp:reference:0" local function update_reserved_nicknames(event) local room, data, jid = event.room.jid, event.data, event.jid load_room_reserved_nicknames(event.room) local nickname = (data or {})["reserved_nickname"] if nickname then reserved_nicknames[room][nickname] = jid else local nickname_to_remove for _nickname, _jid in pairs(reserved_nicknames[room]) do if _jid == jid then nickname_to_remove = _nickname break end end if nickname_to_remove then reserved_nicknames[room][nickname_to_remove] = nil end end end function load_room_reserved_nicknames(room) if not reserved_nicknames[room.jid] then reserved_nicknames[room.jid] = {} for jid, data in pairs(room._affiliations_data or {}) do local reserved_nickname = data["reserved_nickname"] if reserved_nicknames then reserved_nicknames[room.jid][reserved_nickname] = jid end end end end local function get_jid(room, nickname) local bare_jid = reserved_nicknames[room.jid][nickname] if bare_jid and use_bare_jid then return bare_jid end if bare_jid and not use_bare_jid then return room.jid .. "/" .. nickname end end local function get_participants(room) if not reserved_nicks then local occupants = room._occupants local key, occupant = next(occupants) return function () while occupant do -- luacheck: ignore local nick = jid_resource(occupant.nick); local bare_jid = occupant.bare_jid key, occupant = next(occupants, key) return bare_jid, nick end end else local generator = room:each_affiliation() local jid, _, affiliation_data = generator(nil, nil) return function () while jid do local bare_jid, nick = jid, (affiliation_data or {})["reserved_nickname"] jid, _, affiliation_data = generator(nil, bare_jid) if nick then return bare_jid, nick end end end end end local function add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) if strip_out_prefixes then if has_prefix then table.insert(prefix_indices, first-1) end first = first - #prefix_indices last = last - #prefix_indices end mentions[first] = {bare_jid=bare_jid, first=first, last=last} end local function get_client_mentions(stanza) local has_mentions = false local client_mentions = {} for element in stanza:childtags("reference", reference_xmlns) do if element.attr.type == "mention" then local key = tonumber(element.attr.begin) + 1 -- count starts at 0 client_mentions[key] = {bare_jid=element.attr.uri, first=element.attr.begin, last=element.attr["end"]} has_mentions = true end end return has_mentions, client_mentions end local function is_room_eligible(jid) if not enabled_rooms and not disabled_rooms then return true; end if enabled_rooms then for _, _jid in ipairs(enabled_rooms) do if _jid == jid then return true end end return false end if disabled_rooms then for _, _jid in ipairs(disabled_rooms) do if _jid == jid then return false end end return true end end local function has_nick_prefix(body, first) -- There are no configured prefixes if not prefixes or #prefixes < 1 then return false end -- Prefix must have a space before it, -- be the first character of the body -- or be the first character after a new line if not mention_delimiters:contains(body:sub(first - 2, first - 2)) then return false end local prefix = body:sub(first - 1, first - 1) for _, _prefix in ipairs(prefixes) do if prefix == _prefix then return true end end return false end local function has_nick_suffix(body, last) -- There are no configured suffixes if not suffixes or #suffixes < 1 then return false end -- Suffix must have a space after it, -- be the last character of the body -- or be the last character before a new line if not mention_delimiters:contains(body:sub(last + 2, last + 2)) then return false end local suffix = body:sub(last+1, last+1) for _, _suffix in ipairs(suffixes) do if suffix == _suffix then return true end end return false end local function search_mentions(room, body, client_mentions) load_room_reserved_nicknames(room) local mentions, prefix_indices = {}, {} local current_word = "" local current_word_start for i = 1, #body+1 do local char = body:sub(i,i) -- Mention delimiter found, current_word is completed now if mention_delimiters:contains(char) and current_word_start then -- Check for nickname without prefix local jid = get_jid(room, current_word) if jid then if not prefix_mandatory then add_mention(mentions, jid, current_word_start, i - 1, prefix_indices, false) end else -- Check for nickname with affixes local prefix = prefixes:contains(current_word:sub(1,1)) local suffix = suffixes:contains(current_word:sub(-1)) if prefix and suffix then jid = get_jid(room, current_word:sub(2, -2)) if jid then add_mention(mentions, jid, current_word_start + 1, i - 2, prefix_indices, true) end elseif prefix then jid = get_jid(room, current_word:sub(2)) if jid then add_mention(mentions, jid, current_word_start + 1, i - 1, prefix_indices, true) end elseif suffix and not prefix_mandatory then jid = get_jid(room, current_word:sub(1, -2)) if jid then add_mention(mentions, jid, current_word_start, i - 2, prefix_indices, false) end end end current_word = "" current_word_start = nil elseif not mention_delimiters:contains(char) then current_word_start = current_word_start or i current_word = current_word .. char end end return mentions, prefix_indices end local function muc_inject_mentions(event) local room, stanza = event.room, event.stanza; local body = stanza:get_child_text("body") if not body or #body < 1 then return; end -- Inject mentions only if the room is configured for them if not is_room_eligible(room.jid) then return; end -- Only act on messages that do not include mentions -- unless configuration states otherwise. local has_mentions, client_mentions = get_client_mentions(stanza) if has_mentions and not append_mentions then return; end local mentions, prefix_indices = search_mentions(room, body, client_mentions) for _, mention in pairs(mentions) do -- https://xmpp.org/extensions/xep-0372.html#usecase_mention stanza:tag( "reference", { xmlns=reference_xmlns, begin=tostring(mention.first - 1), -- count starts at 0 ["end"]=tostring(mention.last), type="mention", uri="xmpp:" .. mention.bare_jid, } ):up() end if strip_out_prefixes then local body_without_prefixes = "" local from = 0 if #prefix_indices > 0 then for _, prefix_index in ipairs(prefix_indices) do body_without_prefixes = body_without_prefixes .. body:sub(from, prefix_index-1) from = prefix_index + 1 end body_without_prefixes = body_without_prefixes .. body:sub(from, #body) -- Replace original body containing prefixes stanza:maptags( function(tag) if tag.name ~= "body" then return tag end return st.stanza("body"):text(body_without_prefixes) end ) end end end module:hook("muc-occupant-groupchat", muc_inject_mentions) module:hook("muc-set-affiliation", update_reserved_nicknames)