Mercurial > prosody-modules
comparison mod_muc_inject_mentions/mod_muc_inject_mentions.lua @ 4243:aed7038ab2ab
mod_muc_inject_mentions: Make module scalable by iterating through the body instead of participants list as the main loop
author | Seve Ferrer <seve@delape.net> |
---|---|
date | Sat, 14 Nov 2020 18:02:49 +0100 |
parents | a82b0745383b |
children | 32b4901a9d8d |
comparison
equal
deleted
inserted
replaced
4242:6a91d217acc9 | 4243:aed7038ab2ab |
---|---|
1 module:depends("muc"); | 1 module:depends("muc"); |
2 | 2 |
3 local jid_resource = require "util.jid".resource; | 3 local jid_resource = require "util.jid".resource; |
4 local st = require "util.stanza"; | 4 local st = require "util.stanza"; |
5 | 5 |
6 local prefixes = module:get_option("muc_inject_mentions_prefixes", nil) | 6 local prefixes = module:get_option_set("muc_inject_mentions_prefixes", {}) |
7 local suffixes = module:get_option("muc_inject_mentions_suffixes", nil) | 7 local suffixes = module:get_option_set("muc_inject_mentions_suffixes", {}) |
8 local enabled_rooms = module:get_option("muc_inject_mentions_enabled_rooms", nil) | 8 local enabled_rooms = module:get_option("muc_inject_mentions_enabled_rooms", nil) |
9 local disabled_rooms = module:get_option("muc_inject_mentions_disabled_rooms", nil) | 9 local disabled_rooms = module:get_option("muc_inject_mentions_disabled_rooms", nil) |
10 local mention_delimiters = module:get_option_set("muc_inject_mentions_mention_delimiters", {" ", "", "\n"}) | 10 local mention_delimiters = module:get_option_set("muc_inject_mentions_mention_delimiters", {" ", "", "\n", "\t"}) |
11 local append_mentions = module:get_option("muc_inject_mentions_append_mentions", false) | 11 local append_mentions = module:get_option("muc_inject_mentions_append_mentions", false) |
12 local strip_out_prefixes = module:get_option("muc_inject_mentions_strip_out_prefixes", false) | 12 local strip_out_prefixes = module:get_option("muc_inject_mentions_strip_out_prefixes", false) |
13 local reserved_nicks = module:get_option("muc_inject_mentions_reserved_nicks", false) | 13 local reserved_nicks = module:get_option("muc_inject_mentions_reserved_nicks", false) |
14 | 14 local use_real_jid = module:get_option("muc_inject_mentions_use_real_jid", false) |
15 local reserved_nicknames = {} | |
15 | 16 |
16 local reference_xmlns = "urn:xmpp:reference:0" | 17 local reference_xmlns = "urn:xmpp:reference:0" |
18 | |
19 local function update_reserved_nicknames(event) | |
20 local room, data, jid = event.room.jid, event.data, event.jid | |
21 load_room_reserved_nicknames(event.room) | |
22 local nickname = (data or {})["reserved_nickname"] | |
23 | |
24 if nickname then | |
25 reserved_nicknames[room][nickname] = jid | |
26 else | |
27 local nickname_to_remove | |
28 for _nickname, _jid in pairs(reserved_nicknames[room]) do | |
29 if _jid == jid then | |
30 nickname_to_remove = _nickname | |
31 break | |
32 end | |
33 end | |
34 if nickname_to_remove then | |
35 reserved_nicknames[room][nickname_to_remove] = nil | |
36 end | |
37 end | |
38 end | |
39 | |
40 function load_room_reserved_nicknames(room) | |
41 if not reserved_nicknames[room.jid] then | |
42 reserved_nicknames[room.jid] = {} | |
43 for jid, data in pairs(room._affiliations_data or {}) do | |
44 local reserved_nickname = data["reserved_nickname"] | |
45 if reserved_nicknames then | |
46 reserved_nicknames[room.jid][reserved_nickname] = jid | |
47 end | |
48 end | |
49 end | |
50 end | |
51 | |
52 local function get_jid(room, nickname) | |
53 local real_jid = reserved_nicknames[room.jid][nickname] | |
54 if real_jid and use_real_jid then | |
55 return real_jid | |
56 end | |
57 | |
58 if real_jid and not use_real_jid then | |
59 return room.jid .. "/" .. nickname | |
60 end | |
61 end | |
17 | 62 |
18 local function get_participants(room) | 63 local function get_participants(room) |
19 if not reserved_nicks then | 64 if not reserved_nicks then |
20 local occupants = room._occupants | 65 local occupants = room._occupants |
21 local key, occupant = next(occupants) | 66 local key, occupant = next(occupants) |
135 | 180 |
136 return false | 181 return false |
137 end | 182 end |
138 | 183 |
139 local function search_mentions(room, body, client_mentions) | 184 local function search_mentions(room, body, client_mentions) |
185 load_room_reserved_nicknames(room) | |
140 local mentions, prefix_indices = {}, {} | 186 local mentions, prefix_indices = {}, {} |
141 | 187 local current_word = "" |
142 for bare_jid, nick in get_participants(room) do | 188 local current_word_start |
143 -- Check for multiple mentions to the same nickname in a message | 189 for i = 1, #body+1 do |
144 -- Hey @nick remember to... Ah, also @nick please let me know if... | 190 local char = body:sub(i,i) |
145 local matches = {} | 191 -- Mention delimiter found, current_word is completed now |
146 local _first | 192 if mention_delimiters:contains(char) and current_word_start then |
147 local _last = 0 | 193 -- Check for nickname without prefix |
148 while true do | 194 local jid = get_jid(room, current_word) |
149 -- Use plain search as nick could contain | 195 if jid then |
150 -- characters used in Lua patterns | 196 add_mention(mentions, jid, current_word_start, i - 1, prefix_indices, false) |
151 _first, _last = body:find(nick, _last + 1, true) | 197 else |
152 if _first == nil then break end | 198 -- Check for nickname with affixes |
153 table.insert(matches, {first=_first, last=_last}) | 199 local prefix = prefixes:contains(current_word:sub(1,1)) |
154 end | 200 local suffix = suffixes:contains(current_word:sub(-1)) |
155 | 201 if prefix and suffix then |
156 -- Filter out intentional mentions from unintentional ones | 202 jid = get_jid(room, current_word:sub(2, -2)) |
157 for _, match in ipairs(matches) do | 203 if jid then |
158 local first, last = match.first, match.last | 204 add_mention(mentions, jid, current_word_start + 1, i - 2, prefix_indices, true) |
159 -- Only append new mentions in case the client already sent some | 205 end |
160 if not client_mentions[first] then | 206 elseif prefix then |
161 -- Body only contains nickname or is between spaces, new lines or at the end/start of the body | 207 jid = get_jid(room, current_word:sub(2)) |
162 if mention_delimiters:contains(body:sub(first - 1, first - 1)) and | 208 if jid then |
163 mention_delimiters:contains(body:sub(last + 1, last + 1)) | 209 add_mention(mentions, jid, current_word_start + 1, i - 1, prefix_indices, true) |
164 then | 210 end |
165 add_mention(mentions, bare_jid, first, last, prefix_indices, false) | 211 elseif suffix then |
166 else | 212 jid = get_jid(room, current_word:sub(1, -2)) |
167 -- Check if occupant is mentioned using affixes | 213 if jid then |
168 local has_prefix = has_nick_prefix(body, first) | 214 add_mention(mentions, jid, current_word_start, i - 2, prefix_indices, false) |
169 local has_suffix = has_nick_suffix(body, last) | |
170 | |
171 -- @nickname: ... | |
172 if has_prefix and has_suffix then | |
173 add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) | |
174 | |
175 -- @nickname ... | |
176 elseif has_prefix and not has_suffix then | |
177 if mention_delimiters:contains(body:sub(last + 1, last + 1)) then | |
178 add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) | |
179 end | |
180 | |
181 -- nickname: ... | |
182 elseif not has_prefix and has_suffix then | |
183 if mention_delimiters:contains(body:sub(first - 1, first - 1)) then | |
184 add_mention(mentions, bare_jid, first, last, prefix_indices, has_prefix) | |
185 end | |
186 end | 215 end |
187 end | 216 end |
188 end | 217 end |
218 | |
219 current_word = "" | |
220 current_word_start = nil | |
221 elseif not mention_delimiters:contains(char) then | |
222 current_word_start = current_word_start or i | |
223 current_word = current_word .. char | |
189 end | 224 end |
190 end | 225 end |
191 | 226 |
192 return mentions, prefix_indices | 227 return mentions, prefix_indices |
193 end | 228 end |
194 | 229 |
195 local function muc_inject_mentions(event) | 230 local function muc_inject_mentions(event) |
196 local room, stanza = event.room, event.stanza; | 231 local room, stanza = event.room, event.stanza; |
197 local body = stanza:get_child("body") | 232 local body = stanza:get_child_text("body") |
198 | 233 |
199 if not body then return; end | 234 if not body or #body < 1 then return; end |
200 | 235 |
201 -- Inject mentions only if the room is configured for them | 236 -- Inject mentions only if the room is configured for them |
202 if not is_room_eligible(room.jid) then return; end | 237 if not is_room_eligible(room.jid) then return; end |
203 | 238 |
204 -- Only act on messages that do not include mentions | 239 -- Only act on messages that do not include mentions |
205 -- unless configuration states otherwise. | 240 -- unless configuration states otherwise. |
206 local has_mentions, client_mentions = get_client_mentions(stanza) | 241 local has_mentions, client_mentions = get_client_mentions(stanza) |
207 if has_mentions and not append_mentions then return; end | 242 if has_mentions and not append_mentions then return; end |
208 | 243 |
209 local body_text = body:get_text() | 244 local mentions, prefix_indices = search_mentions(room, body, client_mentions) |
210 local mentions, prefix_indices = search_mentions(room, body_text, client_mentions) | |
211 for _, mention in pairs(mentions) do | 245 for _, mention in pairs(mentions) do |
212 -- https://xmpp.org/extensions/xep-0372.html#usecase_mention | 246 -- https://xmpp.org/extensions/xep-0372.html#usecase_mention |
213 stanza:tag( | 247 stanza:tag( |
214 "reference", { | 248 "reference", { |
215 xmlns=reference_xmlns, | 249 xmlns=reference_xmlns, |
224 if strip_out_prefixes then | 258 if strip_out_prefixes then |
225 local body_without_prefixes = "" | 259 local body_without_prefixes = "" |
226 local from = 0 | 260 local from = 0 |
227 if #prefix_indices > 0 then | 261 if #prefix_indices > 0 then |
228 for _, prefix_index in ipairs(prefix_indices) do | 262 for _, prefix_index in ipairs(prefix_indices) do |
229 body_without_prefixes = body_without_prefixes .. body_text:sub(from, prefix_index-1) | 263 body_without_prefixes = body_without_prefixes .. body:sub(from, prefix_index-1) |
230 from = prefix_index + 1 | 264 from = prefix_index + 1 |
231 end | 265 end |
232 body_without_prefixes = body_without_prefixes .. body_text:sub(from, #body_text) | 266 body_without_prefixes = body_without_prefixes .. body:sub(from, #body) |
233 | 267 |
234 -- Replace original body containing prefixes | 268 -- Replace original body containing prefixes |
235 stanza:maptags( | 269 stanza:maptags( |
236 function(tag) | 270 function(tag) |
237 if tag.name ~= "body" then | 271 if tag.name ~= "body" then |
243 end | 277 end |
244 end | 278 end |
245 end | 279 end |
246 | 280 |
247 module:hook("muc-occupant-groupchat", muc_inject_mentions) | 281 module:hook("muc-occupant-groupchat", muc_inject_mentions) |
282 module:hook("muc-set-affiliation", update_reserved_nicknames) |