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_muc_poltergeist.lua 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. local bare = require "util.jid".bare;
  2. local generate_uuid = require "util.uuid".generate;
  3. local jid = require "util.jid";
  4. local neturl = require "net.url";
  5. local parse = neturl.parseQuery;
  6. local st = require "util.stanza";
  7. local get_room_from_jid = module:require "util".get_room_from_jid;
  8. local wrap_async_run = module:require "util".wrap_async_run;
  9. local update_presence_identity = module:require "util".update_presence_identity;
  10. local timer = require "util.timer";
  11. local MUC_NS = "http://jabber.org/protocol/muc";
  12. -- Options
  13. local poltergeist_component
  14. = module:get_option_string("poltergeist_component", module.host);
  15. -- defaults to 3 min
  16. local poltergeist_timeout
  17. = module:get_option_string("poltergeist_leave_timeout", 180);
  18. -- this basically strips the domain from the conference.domain address
  19. local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
  20. if parentHostName == nil then
  21. log("error", "Failed to start - unable to get parent hostname");
  22. return;
  23. end
  24. local parentCtx = module:context(parentHostName);
  25. if parentCtx == nil then
  26. log("error",
  27. "Failed to start - unable to get parent context for host: %s",
  28. tostring(parentHostName));
  29. return;
  30. end
  31. local token_util = module:require "token/util".new(parentCtx);
  32. -- option to enable/disable token verifications
  33. local disableTokenVerification
  34. = module:get_option_boolean("disable_polergeist_token_verification", false);
  35. -- option to expire poltergeist with custom status text
  36. local poltergeistExpiredStatus
  37. = module:get_option_string("poltergeist_expired_status");
  38. -- table to store all poltergeists we create
  39. local poltergeists = {};
  40. -- table to mark that outgoing unavailable presences
  41. -- should be marked with ignore
  42. local poltergeists_pr_ignore = {};
  43. -- poltergaist management functions
  44. -- Returns the room if available, work and in multidomain mode
  45. -- @param room_name the name of the room
  46. -- @param group name of the group (optional)
  47. -- @return returns room if found or nil
  48. function get_room(room_name, group)
  49. local room_address = jid.join(room_name, module:get_host());
  50. -- if there is a group we are in multidomain mode and that group is not
  51. -- our parent host
  52. if group and group ~= "" and group ~= parentHostName then
  53. room_address = "["..group.."]"..room_address;
  54. end
  55. return get_room_from_jid(room_address);
  56. end
  57. -- Stores the username in the table where we store poltergeist usernames
  58. -- based on their room names
  59. -- @param room the room instance
  60. -- @param user_id the user id
  61. -- @param username the username to store
  62. function store_username(room, user_id, username)
  63. local room_name = jid.node(room.jid);
  64. -- we store in poltergeist user ids for room names
  65. if (not poltergeists[room_name]) then
  66. poltergeists[room_name] = {};
  67. end
  68. poltergeists[room_name][user_id] = username;
  69. log("debug", "stored in session: %s", username);
  70. end
  71. -- Retrieve the username for a user
  72. -- @param room the room instance
  73. -- @param user_id the user id
  74. -- @return returns the stored username for user or nil
  75. function get_username(room, user_id)
  76. local room_name = jid.node(room.jid);
  77. if (not poltergeists[room_name]) then
  78. return nil;
  79. end
  80. return poltergeists[room_name][user_id];
  81. end
  82. -- Removes poltergeist values from table
  83. -- @param room the room instance
  84. -- @param nick the user nick
  85. function remove_username(room, nick)
  86. local room_name = jid.node(room.jid);
  87. if (poltergeists[room_name]) then
  88. local user_id_to_remove;
  89. for name,username in pairs(poltergeists[room_name]) do
  90. if (string.sub(username, 0, 8) == nick) then
  91. user_id_to_remove = name;
  92. end
  93. end
  94. if (user_id_to_remove) then
  95. poltergeists[room_name][user_id_to_remove] = nil;
  96. end
  97. end
  98. end
  99. --- Verifies room name, domain name with the values in the token
  100. -- @param token the token we received
  101. -- @param room_name the room name
  102. -- @param group name of the group (optional)
  103. -- @param session the session to use for storing token specific fields
  104. -- @return true if values are ok or false otherwise
  105. function verify_token(token, room_name, group, session)
  106. if disableTokenVerification then
  107. return true;
  108. end
  109. -- if not disableTokenVerification and we do not have token
  110. -- stop here, cause the main virtual host can have guest access enabled
  111. -- (allowEmptyToken = true) and we will allow access to rooms info without
  112. -- a token
  113. if token == nil then
  114. log("warn", "no token provided");
  115. return false;
  116. end
  117. session.auth_token = token;
  118. local verified, reason = token_util:process_and_verify_token(session);
  119. if not verified then
  120. log("warn", "not a valid token %s", tostring(reason));
  121. return false;
  122. end
  123. local room_address = jid.join(room_name, module:get_host());
  124. -- if there is a group we are in multidomain mode and that group is not
  125. -- our parent host
  126. if group and group ~= "" and group ~= parentHostName then
  127. room_address = "["..group.."]"..room_address;
  128. end
  129. if not token_util:verify_room(session, room_address) then
  130. log("warn", "Token %s not allowed to join: %s",
  131. tostring(token), tostring(room_address));
  132. return false;
  133. end
  134. return true;
  135. end
  136. -- if we found that a session for a user with id has a poltergiest already
  137. -- created, retrieve its jid and return it to the authentication
  138. -- so we can reuse it and we that real user will replace the poltergiest
  139. prosody.events.add_handler("pre-jitsi-authentication", function(session)
  140. if (session.jitsi_meet_context_user) then
  141. local room = get_room(
  142. session.jitsi_bosh_query_room,
  143. session.jitsi_meet_domain);
  144. if (not room) then
  145. return nil;
  146. end
  147. local username
  148. = get_username(room, session.jitsi_meet_context_user["id"]);
  149. if (not username) then
  150. return nil;
  151. end
  152. log("debug", "Found predefined username %s", username);
  153. -- let's find the room and if the poltergeist occupant is there
  154. -- lets remove him before the real participant joins
  155. -- when we see the unavailable presence to go out the server
  156. -- we will mark it with ignore tag
  157. local nick = string.sub(username, 0, 8);
  158. if (have_poltergeist_occupant(room, nick)) then
  159. -- notify that user connected using the poltergeist
  160. update_poltergeist_occupant_status(
  161. room, nick, "connected");
  162. remove_poltergeist_occupant(room, nick, true);
  163. end
  164. return username;
  165. end
  166. return nil;
  167. end);
  168. -- Creates poltergeist occupant
  169. -- @param room the room instance where we create the occupant
  170. -- @param nick the nick to use for the new occupant
  171. -- @param name the display name fot the occupant (optional)
  172. -- @param avatar the avatar to use for the new occupant (optional)
  173. -- @param status the initial status to use for the new occupant (optional)
  174. -- @param context the information that we will store for this poltergeist
  175. function create_poltergeist_occupant(room, nick, name, avatar, status, context)
  176. log("debug", "create_poltergeist_occupant %s", nick);
  177. -- Join poltergeist occupant to room, with the invited JID as their nick
  178. local join_presence = st.presence({
  179. to = room.jid.."/"..nick,
  180. from = poltergeist_component.."/"..nick
  181. }):tag("x", { xmlns = MUC_NS }):up();
  182. if (name) then
  183. join_presence:tag(
  184. "nick",
  185. { xmlns = "http://jabber.org/protocol/nick" }):text(name):up();
  186. end
  187. if (avatar) then
  188. join_presence:tag("avatar-url"):text(avatar):up();
  189. end
  190. if (status) then
  191. join_presence:tag("status"):text(status):up();
  192. end
  193. -- If the room has a password set, let the poltergeist enter using it
  194. local room_password = room:get_password();
  195. if room_password then
  196. local join = join_presence:get_child("x", MUC_NS);
  197. join:tag("password", { xmlns = MUC_NS }):text(room_password);
  198. end
  199. update_presence_identity(
  200. join_presence,
  201. context.user,
  202. context.group,
  203. context.creator_user,
  204. context.creator_group
  205. );
  206. room:handle_first_presence(
  207. prosody.hosts[poltergeist_component], join_presence);
  208. local timeout = poltergeist_timeout;
  209. -- the timeout before removing so participants can see the status update
  210. local removeTimeout = 5;
  211. if (poltergeistExpiredStatus) then
  212. timeout = timeout - removeTimeout;
  213. end
  214. timer.add_task(timeout,
  215. function ()
  216. if (poltergeistExpiredStatus) then
  217. update_poltergeist_occupant_status(
  218. room, nick, poltergeistExpiredStatus);
  219. -- and remove it after some time so participant can see
  220. -- the update
  221. timer.add_task(removeTimeout,
  222. function ()
  223. if (have_poltergeist_occupant(room, nick)) then
  224. remove_poltergeist_occupant(room, nick, false);
  225. end
  226. end);
  227. else
  228. if (have_poltergeist_occupant(room, nick)) then
  229. remove_poltergeist_occupant(room, nick, false);
  230. end
  231. end
  232. end);
  233. end
  234. -- Removes poltergeist occupant
  235. -- @param room the room instance where to remove the occupant
  236. -- @param nick the nick of the occupant to remove
  237. -- @param ignore to mark the poltergeist unavailble presence to be ignored
  238. function remove_poltergeist_occupant(room, nick, ignore)
  239. log("debug", "remove_poltergeist_occupant %s", nick);
  240. local leave_presence = st.presence({
  241. to = room.jid.."/"..nick,
  242. from = poltergeist_component.."/"..nick,
  243. type = "unavailable" });
  244. if (ignore) then
  245. poltergeists_pr_ignore[room.jid.."/"..nick] = true;
  246. end
  247. room:handle_normal_presence(
  248. prosody.hosts[poltergeist_component], leave_presence);
  249. remove_username(room, nick);
  250. end
  251. -- Updates poltergeist occupant status
  252. -- @param room the room instance where to remove the occupant
  253. -- @param nick the nick of the occupant to remove
  254. -- @param status the status to update
  255. function update_poltergeist_occupant_status(room, nick, status)
  256. local update_presence = get_presence(room, nick);
  257. if (not update_presence) then
  258. -- no presence found for occupant, create one
  259. update_presence = st.presence({
  260. to = room.jid.."/"..nick,
  261. from = poltergeist_component.."/"..nick
  262. });
  263. else
  264. -- update occupant presence with appropriate to and from
  265. -- so we can send it again
  266. update_presence = st.clone(update_presence);
  267. update_presence.attr.to = room.jid.."/"..nick;
  268. update_presence.attr.from = poltergeist_component.."/"..nick;
  269. end
  270. local once = false;
  271. -- the status tag we will attach
  272. local statusTag = st.stanza("status"):text(status);
  273. -- if there is already a status tag replace it
  274. update_presence:maptags(function (tag)
  275. if tag.name == statusTag.name then
  276. if not once then
  277. once = true;
  278. return statusTag;
  279. else
  280. return nil;
  281. end
  282. end
  283. return tag;
  284. end);
  285. if (not once) then
  286. -- no status tag was repleced, attach it
  287. update_presence:add_child(statusTag);
  288. end
  289. room:handle_normal_presence(
  290. prosody.hosts[poltergeist_component], update_presence);
  291. end
  292. -- Checks for existance of a poltergeist occupant
  293. -- @param room the room instance where to check for occupant
  294. -- @param nick the nick of the occupant
  295. -- @return true if occupant is found, false otherwise
  296. function have_poltergeist_occupant(room, nick)
  297. -- Find out if we have a poltergeist occupant in the room for this JID
  298. return not not room:get_occupant_jid(poltergeist_component.."/"..nick);
  299. end
  300. -- Returns the last presence of occupant
  301. -- @param room the room instance where to check for occupant
  302. -- @param nick the nick of the occupant
  303. -- @return presence of the occupant
  304. function get_presence(room, nick)
  305. local occupant_jid
  306. = room:get_occupant_jid(poltergeist_component.."/"..nick);
  307. if (occupant_jid) then
  308. return room:get_occupant_by_nick(occupant_jid):get_presence();
  309. end
  310. return nil;
  311. end
  312. -- Event handlers
  313. --- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
  314. --- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
  315. module:hook("muc-decline", function (event)
  316. remove_poltergeist_occupant(event.room, bare(event.stanza.attr.from), false);
  317. end, -100);
  318. -- before sending the presence for a poltergeist leaving add ignore tag
  319. -- as poltergeist is leaving just before the real user joins and in the client
  320. -- we ignore this presence to avoid leaving/joining experience and the real
  321. -- user will reuse all currently created UI components for the same nick
  322. module:hook("muc-broadcast-presence", function (event)
  323. if (bare(event.occupant.jid) == poltergeist_component) then
  324. if(event.stanza.attr.type == "unavailable"
  325. and poltergeists_pr_ignore[event.occupant.nick]) then
  326. event.stanza:tag(
  327. "ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
  328. poltergeists_pr_ignore[event.occupant.nick] = nil;
  329. end
  330. end
  331. end, -100);
  332. -- cleanup room table after room is destroyed
  333. module:hook("muc-room-destroyed",function(event)
  334. local room_name = jid.node(event.room.jid);
  335. if (poltergeists[room_name]) then
  336. poltergeists[room_name] = nil;
  337. end
  338. end);
  339. --- Handles request for creating/managing poltergeists
  340. -- @param event the http event, holds the request query
  341. -- @return GET response, containing a json with response details
  342. function handle_create_poltergeist (event)
  343. if (not event.request.url.query) then
  344. return 400;
  345. end
  346. local params = parse(event.request.url.query);
  347. local user_id = params["user"];
  348. local room_name = params["room"];
  349. local group = params["group"];
  350. local name = params["name"];
  351. local avatar = params["avatar"];
  352. local status = params["status"];
  353. local session = {};
  354. if not verify_token(params["token"], room_name, group, session) then
  355. return 403;
  356. end
  357. local room = get_room(room_name, group);
  358. if (not room) then
  359. log("error", "no room found %s", room_name);
  360. return 404;
  361. end
  362. local username = get_username(room, user_id);
  363. if (username ~= nil
  364. and have_poltergeist_occupant(room, string.sub(username, 0, 8))) then
  365. log("warn", "poltergeist for username:%s already in the room:%s",
  366. username, room_name);
  367. return 202;
  368. else
  369. username = generate_uuid();
  370. store_username(room, user_id, username);
  371. local context = {
  372. user = {
  373. id = user_id;
  374. };
  375. group = group;
  376. creator_user = session.jitsi_meet_context_user;
  377. creator_group = session.jitsi_meet_context_group;
  378. };
  379. create_poltergeist_occupant(
  380. room, string.sub(username, 0, 8), name, avatar, status, context);
  381. return 200;
  382. end
  383. end
  384. --- Handles request for updating poltergeists status
  385. -- @param event the http event, holds the request query
  386. -- @return GET response, containing a json with response details
  387. function handle_update_poltergeist (event)
  388. if (not event.request.url.query) then
  389. return 400;
  390. end
  391. local params = parse(event.request.url.query);
  392. local user_id = params["user"];
  393. local room_name = params["room"];
  394. local group = params["group"];
  395. local status = params["status"];
  396. if not verify_token(params["token"], room_name, group, {}) then
  397. return 403;
  398. end
  399. local room = get_room(room_name, group);
  400. if (not room) then
  401. log("error", "no room found %s", room_name);
  402. return 404;
  403. end
  404. local username = get_username(room, user_id);
  405. if (not username) then
  406. return 404;
  407. end
  408. local nick = string.sub(username, 0, 8);
  409. if (have_poltergeist_occupant(room, nick)) then
  410. update_poltergeist_occupant_status(room, nick, status);
  411. return 200;
  412. else
  413. return 404;
  414. end
  415. end
  416. --- Handles remove poltergeists
  417. -- @param event the http event, holds the request query
  418. -- @return GET response, containing a json with response details
  419. function handle_remove_poltergeist (event)
  420. if (not event.request.url.query) then
  421. return 400;
  422. end
  423. local params = parse(event.request.url.query);
  424. local user_id = params["user"];
  425. local room_name = params["room"];
  426. local group = params["group"];
  427. if not verify_token(params["token"], room_name, group, {}) then
  428. return 403;
  429. end
  430. local room = get_room(room_name, group);
  431. if (not room) then
  432. log("error", "no room found %s", room_name);
  433. return 404;
  434. end
  435. local username = get_username(room, user_id);
  436. if (not username) then
  437. return 404;
  438. end
  439. local nick = string.sub(username, 0, 8);
  440. if (have_poltergeist_occupant(room, nick)) then
  441. remove_poltergeist_occupant(room, nick, false);
  442. return 200;
  443. else
  444. return 404;
  445. end
  446. end
  447. log("info", "Loading poltergeist service");
  448. module:depends("http");
  449. module:provides("http", {
  450. default_path = "/";
  451. name = "poltergeist";
  452. route = {
  453. ["GET /poltergeist/create"] = function (event) return wrap_async_run(event,handle_create_poltergeist) end;
  454. ["GET /poltergeist/update"] = function (event) return wrap_async_run(event,handle_update_poltergeist) end;
  455. ["GET /poltergeist/remove"] = function (event) return wrap_async_run(event,handle_remove_poltergeist) end;
  456. };
  457. });