You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mod_fmuc.lua 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. --- activate under main muc component
  2. --- Add the following config under the main muc component
  3. --- muc_room_default_presence_broadcast = {
  4. --- visitor = false;
  5. --- participant = true;
  6. --- moderator = true;
  7. --- };
  8. --- Enable in global modules: 's2s_bidi'
  9. --- Make sure 's2s' is not in modules_disabled
  10. --- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com), as there
  11. --- are certain configs for whitelisted domains and connections that are domain based
  12. --- TODO: filter presence from main occupants back to main prosody
  13. local jid = require 'util.jid';
  14. local st = require 'util.stanza';
  15. local new_id = require 'util.id'.medium;
  16. local filters = require 'util.filters';
  17. local util = module:require 'util';
  18. local is_vpaas = util.is_vpaas;
  19. local room_jid_match_rewrite = util.room_jid_match_rewrite;
  20. local get_room_from_jid = util.get_room_from_jid;
  21. local get_focus_occupant = util.get_focus_occupant;
  22. local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
  23. local presence_check_status = util.presence_check_status;
  24. -- this is the main virtual host of this vnode
  25. local local_domain = module:get_option_string('muc_mapper_domain_base');
  26. if not local_domain then
  27. module:log('warn', "No 'muc_mapper_domain_base' option set, disabling fmuc plugin");
  28. return;
  29. end
  30. -- this is the main virtual host of the main prosody that this vnode serves
  31. local main_domain = module:get_option_string('main_domain');
  32. if not main_domain then
  33. module:log('warn', "No 'main_domain' option set, disabling fmuc plugin");
  34. return;
  35. end
  36. local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
  37. local local_muc_domain = muc_domain_prefix..'.'..local_domain;
  38. local NICK_NS = 'http://jabber.org/protocol/nick';
  39. -- we send stats for the total number of rooms, total number of participants and total number of visitors
  40. local measure_rooms = module:measure('vnode-rooms', 'amount');
  41. local measure_participants = module:measure('vnode-participants', 'amount');
  42. local measure_visitors = module:measure('vnode-visitors', 'amount');
  43. local sent_iq_cache = require 'util.cache'.new(200);
  44. local sessions = prosody.full_sessions;
  45. local um_is_admin = require 'core.usermanager'.is_admin;
  46. local function is_admin(jid)
  47. return um_is_admin(jid, module.host);
  48. end
  49. -- mark all occupants as visitors
  50. module:hook('muc-occupant-pre-join', function (event)
  51. local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza;
  52. local node, host = jid.split(occupant.bare_jid);
  53. if host == local_domain then
  54. if room._main_room_lobby_enabled then
  55. origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitors not allowed while lobby is on!')
  56. :tag('no-visitors-lobby', { xmlns = 'jitsi:visitors' }));
  57. return true;
  58. else
  59. occupant.role = 'visitor';
  60. end
  61. end
  62. end, 3);
  63. -- if a visitor leaves we want to lower its hand if it was still raised before leaving
  64. -- this is to clear indication for promotion on moderators visitors list
  65. module:hook('muc-occupant-pre-leave', function (event)
  66. local occupant = event.occupant;
  67. ---- we are interested only of visitors presence
  68. if occupant.role ~= 'visitor' then
  69. return;
  70. end
  71. local room = event.room;
  72. -- let's check if the visitor has a raised hand send a lower hand
  73. -- to main prosody
  74. local pr = occupant:get_presence();
  75. local raiseHand = pr:get_child_text('jitsi_participant_raisedHand');
  76. -- a promotion detected let's send it to main prosody
  77. if raiseHand and #raiseHand > 0 then
  78. local iq_id = new_id();
  79. sent_iq_cache:set(iq_id, socket.gettime());
  80. local promotion_request = st.iq({
  81. type = 'set',
  82. to = 'visitors.'..main_domain,
  83. from = local_domain,
  84. id = iq_id })
  85. :tag('visitors', { xmlns = 'jitsi:visitors',
  86. room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
  87. :tag('promotion-request', {
  88. xmlns = 'jitsi:visitors',
  89. jid = occupant.jid,
  90. time = nil;
  91. }):up();
  92. module:send(promotion_request);
  93. end
  94. end, 1); -- rate limit is 0
  95. -- Returns the main participants count and the visitors count
  96. local function get_occupant_counts(room)
  97. local main_count = 0;
  98. local visitors_count = 0;
  99. for _, o in room:each_occupant() do
  100. if o.role == 'visitor' then
  101. visitors_count = visitors_count + 1;
  102. elseif not is_admin(o.bare_jid) then
  103. main_count = main_count + 1;
  104. end
  105. end
  106. return main_count, visitors_count;
  107. end
  108. local function cancel_destroy_timer(room)
  109. if room.visitors_destroy_timer then
  110. room.visitors_destroy_timer:stop();
  111. room.visitors_destroy_timer = nil;
  112. end
  113. end
  114. -- schedules a new destroy timer which will destroy the room if there are no visitors after the timeout
  115. local function schedule_destroy_timer(room)
  116. cancel_destroy_timer(room);
  117. room.visitors_destroy_timer = module:add_timer(15, function()
  118. -- if the room is being destroyed, ignore
  119. if room.destroying then
  120. return;
  121. end
  122. local main_count, visitors_count = get_occupant_counts(room);
  123. if visitors_count == 0 then
  124. module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
  125. room:destroy(nil, 'No visitors.');
  126. end
  127. end);
  128. end
  129. -- when occupant is leaving forward presences to jicofo for visitors
  130. -- do not check occupant.role as it maybe already reset
  131. -- if there are no main occupants or no visitors, destroy the room (give 15 seconds of grace period for reconnections)
  132. module:hook('muc-occupant-left', function (event)
  133. local room, occupant = event.room, event.occupant;
  134. local occupant_domain = jid.host(occupant.bare_jid);
  135. if occupant_domain == local_domain then
  136. local focus_occupant = get_focus_occupant(room);
  137. if not focus_occupant then
  138. module:log('info', 'No focus found for %s', room.jid);
  139. return;
  140. end
  141. -- Let's forward unavailable presence to the special jicofo
  142. room:route_stanza(st.presence({
  143. to = focus_occupant.jid,
  144. from = internal_room_jid_match_rewrite(occupant.nick),
  145. type = 'unavailable' })
  146. :tag('x', { xmlns = 'http://jabber.org/protocol/muc#user' })
  147. :tag('item', {
  148. affiliation = room:get_affiliation(occupant.bare_jid) or 'none';
  149. role = 'none';
  150. nick = event.nick;
  151. jid = occupant.bare_jid }):up():up());
  152. end
  153. -- if the room is being destroyed, ignore
  154. if room.destroying then
  155. return;
  156. end
  157. -- if there are no main participants, the main room will be destroyed and
  158. -- we can destroy and the visitor one as when jicofo leaves all visitors will reload
  159. -- if there are no visitors give them 15 secs to reconnect, if not destroy it
  160. local main_count, visitors_count = get_occupant_counts(room);
  161. if visitors_count == 0 then
  162. schedule_destroy_timer(room);
  163. end
  164. end);
  165. -- forward visitor presences to jicofo
  166. -- detects raise hand in visitors presence, this is request for promotion
  167. module:hook('muc-broadcast-presence', function (event)
  168. local occupant = event.occupant;
  169. ---- we are interested only of visitors presence to send it to jicofo
  170. if occupant.role ~= 'visitor' then
  171. return;
  172. end
  173. local room = event.room;
  174. local focus_occupant = get_focus_occupant(room);
  175. if not focus_occupant then
  176. return;
  177. end
  178. local actor, base_presence, nick, reason, x = event.actor, event.stanza, event.nick, event.reason, event.x;
  179. local actor_nick;
  180. if actor then
  181. actor_nick = jid.resource(room:get_occupant_jid(actor));
  182. end
  183. -- create a presence to send it to jicofo, as jicofo is special :)
  184. local full_x = st.clone(x.full or x);
  185. room:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason);
  186. local full_p = st.clone(base_presence):add_child(full_x);
  187. full_p.attr.to = focus_occupant.jid;
  188. room:route_to_occupant(focus_occupant, full_p);
  189. local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand');
  190. -- a promotion detected let's send it to main prosody
  191. if raiseHand then
  192. local user_id;
  193. local is_moderator;
  194. local session = sessions[occupant.jid];
  195. local identity = session and session.jitsi_meet_context_user;
  196. if is_vpaas(room) and identity then
  197. -- in case of moderator in vpaas meeting we want to do auto-promotion
  198. local is_vpaas_moderator = identity.moderator;
  199. if is_vpaas_moderator == 'true' or is_vpaas_moderator == true then
  200. is_moderator = true;
  201. end
  202. else
  203. -- The case with single moderator in the room, we want to report our id
  204. -- so we can be auto promoted
  205. if identity and identity.id then
  206. user_id = session.jitsi_meet_context_user.id;
  207. -- non-vpass and having a token in correct tenant is considered a moderator
  208. if session.jitsi_meet_str_tenant
  209. and session.jitsi_web_query_prefix == string.lower(session.jitsi_meet_str_tenant) then
  210. is_moderator = true;
  211. end
  212. end
  213. end
  214. local iq_id = new_id();
  215. sent_iq_cache:set(iq_id, socket.gettime());
  216. local promotion_request = st.iq({
  217. type = 'set',
  218. to = 'visitors.'..main_domain,
  219. from = local_domain,
  220. id = iq_id })
  221. :tag('visitors', { xmlns = 'jitsi:visitors',
  222. room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
  223. :tag('promotion-request', {
  224. xmlns = 'jitsi:visitors',
  225. jid = occupant.jid,
  226. time = raiseHand,
  227. userId = user_id,
  228. forcePromote = is_moderator and 'true' or 'false';
  229. }):up();
  230. local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
  231. if nick_element then
  232. promotion_request:add_child(nick_element);
  233. end
  234. module:send(promotion_request);
  235. end
  236. return;
  237. end);
  238. -- listens for responses to the iq sent for requesting promotion and forward it to the visitor
  239. local function stanza_handler(event)
  240. local origin, stanza = event.origin, event.stanza;
  241. if stanza.name ~= 'iq' then
  242. return;
  243. end
  244. if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
  245. sent_iq_cache:set(stanza.attr.id, nil);
  246. return true;
  247. end
  248. if stanza.attr.type ~= 'set' then
  249. return;
  250. end
  251. local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
  252. if not visitors_iq then
  253. return;
  254. end
  255. if stanza.attr.from ~= 'visitors.'..main_domain then
  256. module:log('warn', 'not from visitors component, ignore! %s', stanza);
  257. return true;
  258. end
  259. local room_jid = visitors_iq.attr.room;
  260. local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
  261. if not room then
  262. module:log('warn', 'No room found %s', room_jid);
  263. return;
  264. end
  265. local request_promotion = visitors_iq:get_child('promotion-response');
  266. if not request_promotion then
  267. return;
  268. end
  269. -- respond with successful receiving the iq
  270. origin.send(st.iq({
  271. type = 'result';
  272. from = stanza.attr.to;
  273. to = stanza.attr.from;
  274. id = stanza.attr.id
  275. }));
  276. local req_jid = request_promotion.attr.jid;
  277. -- now let's find the occupant and forward the response
  278. local occupant = room:get_occupant_by_real_jid(req_jid);
  279. if occupant then
  280. stanza.attr.to = occupant.jid;
  281. stanza.attr.from = room.jid;
  282. room:route_to_occupant(occupant, stanza);
  283. return true;
  284. end
  285. end
  286. --process a host module directly if loaded or hooks to wait for its load
  287. function process_host_module(name, callback)
  288. local function process_host(host)
  289. if host == name then
  290. callback(module:context(host), host);
  291. end
  292. end
  293. if prosody.hosts[name] == nil then
  294. module:log('debug', 'No host/component found, will wait for it: %s', name)
  295. -- when a host or component is added
  296. prosody.events.add_handler('host-activated', process_host);
  297. else
  298. process_host(name);
  299. end
  300. end
  301. process_host_module(local_domain, function(host_module, host)
  302. host_module:hook('iq/host', stanza_handler, 10);
  303. end);
  304. -- only live chat is supported for visitors
  305. module:hook('muc-occupant-groupchat', function(event)
  306. local occupant, room, stanza = event.occupant, event.room, event.stanza;
  307. local from = stanza.attr.from;
  308. local occupant_host;
  309. -- if there is no occupant this is a message from main, probably coming from other vnode
  310. if occupant then
  311. occupant_host = jid.host(occupant.bare_jid);
  312. -- we manage nick only for visitors
  313. if occupant_host ~= main_domain then
  314. -- add to message stanza display name for the visitor
  315. -- remove existing nick to avoid forgery
  316. stanza:remove_children('nick', NICK_NS);
  317. local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
  318. if nick_element then
  319. stanza:add_child(nick_element);
  320. else
  321. stanza:tag('nick', { xmlns = NICK_NS })
  322. :text('anonymous'):up();
  323. end
  324. end
  325. stanza.attr.from = occupant.nick;
  326. else
  327. stanza.attr.from = jid.join(jid.node(from), module.host);
  328. end
  329. -- let's send it to main chat and rest of visitors here
  330. for _, o in room:each_occupant() do
  331. -- filter remote occupants
  332. if jid.host(o.bare_jid) == local_domain then
  333. room:route_to_occupant(o, stanza)
  334. end
  335. end
  336. -- send to main participants only messages from local occupants (skip from remote vnodes)
  337. if occupant and occupant_host == local_domain then
  338. local main_message = st.clone(stanza);
  339. main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain);
  340. -- make sure we fix the from to be the real jid
  341. main_message.attr.from = room_jid_match_rewrite(stanza.attr.from);
  342. module:send(main_message);
  343. end
  344. stanza.attr.from = from; -- something prosody does internally
  345. return true;
  346. end, 55); -- prosody check for visitor's chat is prio 50, we want to override it
  347. module:hook('muc-private-message', function(event)
  348. -- private messaging is forbidden
  349. event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden',
  350. 'Private messaging is disabled on visitor nodes'));
  351. return true;
  352. end, 10);
  353. -- we calculate the stats on the configured interval (60 seconds by default)
  354. module:hook_global('stats-update', function ()
  355. local participants_count, rooms_count, visitors_count = 0, 0, 0;
  356. -- iterate over all rooms
  357. for room in prosody.hosts[module.host].modules.muc.each_room() do
  358. rooms_count = rooms_count + 1;
  359. for _, o in room:each_occupant() do
  360. if jid.host(o.bare_jid) == local_domain then
  361. visitors_count = visitors_count + 1;
  362. else
  363. participants_count = participants_count + 1;
  364. end
  365. end
  366. -- do not count jicofo
  367. participants_count = participants_count - 1;
  368. end
  369. measure_rooms(rooms_count);
  370. measure_visitors(visitors_count);
  371. measure_participants(participants_count);
  372. end);
  373. -- we skip it till the main participants are added from the main prosody
  374. module:hook('jicofo-unlock-room', function(e)
  375. -- we do not block events we fired
  376. if e.fmuc_fired then
  377. return;
  378. end
  379. return true;
  380. end);
  381. -- handles incoming iq connect stanzas
  382. local function iq_from_main_handler(event)
  383. local origin, stanza = event.origin, event.stanza;
  384. if stanza.name ~= 'iq' then
  385. return;
  386. end
  387. if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
  388. sent_iq_cache:set(stanza.attr.id, nil);
  389. return true;
  390. end
  391. if stanza.attr.type ~= 'set' then
  392. return;
  393. end
  394. local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
  395. if not visitors_iq then
  396. return;
  397. end
  398. if stanza.attr.from ~= main_domain then
  399. module:log('warn', 'not from main prosody, ignore! %s', stanza);
  400. return true;
  401. end
  402. local room_jid = visitors_iq.attr.room;
  403. local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
  404. if not room then
  405. module:log('warn', 'No room found %s', room_jid);
  406. return;
  407. end
  408. local node = visitors_iq:get_child('connect');
  409. local fire_jicofo_unlock = true;
  410. local process_disconnect = false;
  411. if not node then
  412. node = visitors_iq:get_child('update');
  413. fire_jicofo_unlock = false;
  414. end
  415. if not node then
  416. node = visitors_iq:get_child('disconnect');
  417. process_disconnect = true;
  418. end
  419. if not node then
  420. return;
  421. end
  422. -- respond with successful receiving the iq
  423. origin.send(st.iq({
  424. type = 'result';
  425. from = stanza.attr.to;
  426. to = stanza.attr.from;
  427. id = stanza.attr.id
  428. }));
  429. if process_disconnect then
  430. cancel_destroy_timer(room);
  431. local main_count, visitors_count = get_occupant_counts(room);
  432. module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
  433. room:destroy(nil, 'Conference ended.');
  434. return true;
  435. end
  436. -- if there is password supplied use it
  437. -- if this is update it will either set or remove the password
  438. room:set_password(node.attr.password);
  439. room._data.meetingId = node.attr.meetingId;
  440. local createdTimestamp = node.attr.createdTimestamp;
  441. room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;
  442. if node.attr.lobby == 'true' then
  443. room._main_room_lobby_enabled = true;
  444. elseif node.attr.lobby == 'false' then
  445. room._main_room_lobby_enabled = false;
  446. end
  447. if fire_jicofo_unlock then
  448. -- everything is connected allow participants to join
  449. module:fire_event('jicofo-unlock-room', { room = room; fmuc_fired = true; });
  450. end
  451. return true;
  452. end
  453. module:hook('iq/host', iq_from_main_handler, 10);
  454. -- Filters presences (if detected) that are with destination the main prosody
  455. function filter_stanza(stanza, session)
  456. if (stanza.name == 'presence' or stanza.name == 'message') and session.type ~= 'c2s' then
  457. -- we clone it so we do not affect broadcast using same stanza, sending it to clients
  458. local f_st = st.clone(stanza);
  459. f_st.skipMapping = true;
  460. return f_st;
  461. elseif stanza.name == 'presence' and session.type == 'c2s' and jid.node(stanza.attr.to) == 'focus' then
  462. local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
  463. if presence_check_status(x, '110') then
  464. return stanza; -- no filter
  465. end
  466. -- we want to filter presences to jicofo for the main participants, skipping visitors
  467. -- no point of having them, but if it is the one of the first to be sent
  468. -- when first visitor is joining can produce the 'No hosts[from_host]' error as we
  469. -- rewrite the from, but we need to not do it to be able to filter it later for the s2s
  470. if jid.host(room_jid_match_rewrite(stanza.attr.from)) ~= local_muc_domain then
  471. return nil; -- returning nil filters the stanza
  472. end
  473. end
  474. return stanza; -- no filter
  475. end
  476. function filter_session(session)
  477. -- domain mapper is filtering on default priority 0, and we need it before that
  478. filters.add_filter(session, 'stanzas/out', filter_stanza, 2);
  479. end
  480. filters.add_filter_hook(filter_session);
  481. function route_s2s_stanza(event)
  482. local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
  483. if to_host ~= main_domain then
  484. return; -- continue with hook listeners
  485. end
  486. if stanza.name == 'message' then
  487. if jid.resource(stanza.attr.to) then
  488. -- there is no point of delivering messages to main participants individually
  489. return true; -- drop it
  490. end
  491. return;
  492. end
  493. if stanza.name == 'presence' then
  494. -- we want to leave only unavailable presences to go to main node
  495. -- all other presences from jicofo or the main participants there is no point to go to the main node
  496. -- they are anyway not handled
  497. if stanza.attr.type ~= 'unavailable' then
  498. return true; -- drop it
  499. end
  500. return;
  501. end
  502. end
  503. -- routing to sessions in mod_s2s is -1 and -10, we want to hook before that to make sure to is correct
  504. -- or if we want to filter that stanza
  505. module:hook("route/remote", route_s2s_stanza, 10);