Diff

mod_muc_inject_mentions/mod_muc_inject_mentions.lua @ 4138:e8c1b35bc25b

mod_muc_inject_mentions: Publish module to repository
author Seve Ferrer <seve@delape.net>
date Sun, 20 Sep 2020 10:31:02 +0200
child 4139:c6bb64a12f92
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_inject_mentions/mod_muc_inject_mentions.lua	Sun Sep 20 10:31:02 2020 +0200
@@ -0,0 +1,173 @@
+module:depends("muc");
+
+local jid_split = require "util.jid".split;
+
+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 reference_xmlns = "urn:xmpp:reference:0"
+
+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 is no prefix
+    -- but mention could still be valid
+    if first == 1 then return true end
+
+    -- There are no configured prefixes
+    if not prefixes or #prefixes < 1 then return false end
+
+    -- Preffix must have a space before it
+    -- or be the first character of the body
+    if body:sub(first - 2, first - 2) ~= "" and
+        body:sub(first - 2, first - 2) ~= " "
+    then
+        return false
+    end
+
+    local preffix = body:sub(first - 1, first - 1)
+    for i, _preffix in ipairs(prefixes) do
+        if preffix == _preffix then
+            return true
+        end
+    end
+
+    return false
+end
+
+local function has_nick_suffix(body, last)
+    -- There is no suffix
+    -- but mention could still be valid
+    if last == #body then return true end
+
+    -- There are no configured suffixes
+    if not suffixes or #suffixes < 1 then return false end
+
+    -- Suffix must have a space after it
+    -- or be the last character of the body
+    if body:sub(last + 2, last + 2) ~= "" and
+        body:sub(last + 2, last + 2) ~= " "
+    then
+        return false
+    end
+
+    local suffix = body:sub(last+1, last+1)
+    for i, _suffix in ipairs(suffixes) do
+        if suffix == _suffix then
+            return true
+        end
+    end
+
+    return false
+end
+
+local function search_mentions(room, stanza)
+    local body = stanza:get_child("body"):get_text();
+    local mentions = {}
+
+    for _, occupant in pairs(room._occupants) do
+        local node, host, nick = jid_split(occupant.nick);
+        -- 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, _last = 0, 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 bare_jid = occupant.bare_jid
+            local first, last = match.first, match.last
+
+            -- Body only contains nickname
+            if first == 1 and last == #body then
+                table.insert(mentions, {bare_jid=bare_jid, first=first, last=last})
+
+            -- Nickname between spaces
+            elseif body:sub(first - 1, first - 1) == " " and
+                body:sub(last + 1, last + 1) == " "
+            then
+                table.insert(mentions, {bare_jid=bare_jid, first=first, last=last})
+            else
+                -- Check if occupant is mentioned using affixes
+                local has_preffix = has_nick_prefix(body, first)
+                local has_suffix = has_nick_suffix(body, last)
+
+                -- @nickname: ...
+                if has_preffix and has_suffix then
+                    table.insert(mentions, {bare_jid=bare_jid, first=first, last=last})
+
+                -- @nickname ...
+                elseif has_preffix and not has_suffix then
+                    if body:sub(last + 1, last + 1) == " " then
+                        table.insert(mentions, {bare_jid=bare_jid, first=first, last=last})
+                    end
+
+                -- nickname: ...
+                elseif not has_preffix and has_suffix then
+                    if body:sub(first - 1, first - 1) == " " then
+                        table.insert(mentions, {bare_jid=bare_jid, first=first, last=last})
+                    end
+                end
+            end
+        end
+    end
+
+    return mentions
+end
+
+local function muc_inject_mentions(event)
+    local room, stanza = event.room, event.stanza;
+    -- 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 references.
+    -- If references are found, it is assumed the client has mentions support
+    if stanza:get_child("reference", reference_xmlns) then return; end
+
+    local mentions = search_mentions(room, stanza)
+    for _, mention in ipairs(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
+end
+
+module:hook("muc-occupant-groupchat", muc_inject_mentions)
\ No newline at end of file