--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_inject_mentions/README.markdown Sun Sep 20 10:31:02 2020 +0200
@@ -0,0 +1,76 @@
+# Introduction
+
+This module intercepts messages sent to a MUC, looks in the message's body if a user was mentioned and injects a mention type reference to that user implementing [XEP-0372](https://xmpp.org/extensions/xep-0372.html#usecase_mention)
+
+## Features
+
+1. Multiple mentions in the same message using affixes, including multiple mentions to the same user.
+ Examples:
+ `Hello nickname`
+ `@nickname hey!`
+ `nickname, hi :)`
+ `Are you sure @nickname?`
+
+2. Mentions are only injected if no mention was found in a message, avoiding this way, injecting mentions in messages sent from clients with mentions support.
+
+3. Configuration settings for customizing affixes and enabling/disabling the module for specific rooms.
+
+
+# Configuring
+
+## Enabling
+
+```{.lua}
+
+Component "rooms.example.net" "muc"
+
+modules_enabled = {
+ "muc_inject_mentions";
+}
+
+```
+
+## Settings
+
+Apart from just writing the nick of an occupant to trigger this module,
+common affixes used when mentioning someone can be configured in Prosody's config file.
+Recommended affixes:
+
+```
+muc_inject_mentions_prefixes = {"@"} -- Example: @bob hello!
+muc_inject_mentions_suffixes = {":", ",", "!", ".", "?"} -- Example: bob! How are you doing?
+```
+
+This module can be enabled/disabled for specific rooms.
+Only one of the following settings must be set.
+
+```
+-- muc_inject_mentions_enabled_rooms = {"room@conferences.server.com"}
+-- muc_inject_mentions_disabled_rooms = {"room@conferences.server.com"}
+```
+
+If none or both are found, all rooms in the muc component will have mentions enabled.
+
+# Example stanzas
+
+Alice sends the following message
+
+```
+<message id="af6ca" to="room@conference.localhost" type="groupchat">
+ <body>@bob hey! Are you there?</body>
+</message>
+```
+
+Then, the module detects `@bob` is a mention to `bob` and injects a mention type reference to him
+
+```
+<message from="room@conference.localhost/alice" id="af6ca" to="alice@localhost/ThinkPad" type="groupchat">
+ <body>@bob hey! Are you there?</body>
+ <reference xmlns="urn:xmpp:reference:0"
+ begin="1"
+ end="3"
+ uri="xmpp:bob@localhost"
+ type="mention"
+ />
+</message>
+```
--- /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