Mercurial > prosody-modules
changeset 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 |
parents | 5f4bcaad18ee |
children | c6bb64a12f92 |
files | mod_muc_inject_mentions/README.markdown mod_muc_inject_mentions/mod_muc_inject_mentions.lua |
diffstat | 2 files changed, 249 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /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