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

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