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 16KB

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