Software /
code /
prosody-modules
File
mod_muc_inject_mentions/mod_muc_inject_mentions.lua @ 4203:c4002aae4ad3
mod_s2s_keepalive: Use timestamp as iq @id
RFC 6120 implies that the id attribute must be unique within a stream.
This should fix problems with remote servers that enforce uniqueness and
don't answer duplicated ids.
If it doesn't do that, then at least you can get a guesstimate at
round-trip time from the difference between the result iq stanza and the
timestamp it was logged without having to go look for when it was sent,
or needing to keep state.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Wed, 14 Oct 2020 18:02:10 +0200 |
parent | 4164:a82b0745383b |
child | 4243:aed7038ab2ab |
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("muc_inject_mentions_prefixes", nil) local suffixes = module:get_option("muc_inject_mentions_suffixes", nil) 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"}) 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 reference_xmlns = "urn:xmpp:reference:0" 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 and not disabled_rooms then for _, _jid in ipairs(enabled_rooms) do if _jid == jid then return true end end return false end if disabled_rooms and not enabled_rooms then for _, _jid in ipairs(disabled_rooms) do if _jid == jid then return false end end return true end return true 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) local mentions, prefix_indices = {}, {} for bare_jid, nick in get_participants(room) do -- Check for multiple mentions to the same nickname in a message -- Hey @nick remember to... Ah, also @nick please let me know if... local matches = {} local _first local _last = 0 while true do -- Use plain search as nick could contain -- characters used in Lua patterns _first, _last = body:find(nick, _last + 1, true) if _first == nil then break end table.insert(matches, {first=_first, last=_last}) end -- Filter out intentional mentions from unintentional ones for _, match in ipairs(matches) do local first, last = match.first, match.last -- Only append new mentions in case the client already sent some if not client_mentions[first] then -- Body only contains nickname or is between spaces, new lines or at the end/start of the body if mention_delimiters:contains(body:sub(first - 1, first - 1)) and mention_delimiters:contains(body:sub(last + 1, last + 1)) then add_mention(mentions, bare_jid, first, last, prefix_indices, false) else -- Check if occupant is mentioned using affixes local has_prefix = has_nick_prefix(body, first) local has_suffix = has_nick_suffix(body, last) -- @nickname: ... if has_prefix and has_suffix then add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) -- @nickname ... elseif has_prefix and not has_suffix then if mention_delimiters:contains(body:sub(last + 1, last + 1)) then add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) end -- nickname: ... elseif not has_prefix and has_suffix then if mention_delimiters:contains(body:sub(first - 1, first - 1)) then add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) end end end end 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("body") if not body 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 body_text = body:get_text() local mentions, prefix_indices = search_mentions(room, body_text, 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 - 1), 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_text:sub(from, prefix_index-1) from = prefix_index + 1 end body_without_prefixes = body_without_prefixes .. body_text:sub(from, #body_text) -- 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)