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)