您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

mod_muc_poltergeist.lua 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  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. local expired_status = "expired";
  13. -- Options
  14. local poltergeist_component
  15. = module:get_option_string("poltergeist_component", module.host);
  16. -- defaults to 30 seconds
  17. local poltergeist_timeout
  18. = module:get_option_string("poltergeist_leave_timeout", 30);
  19. -- this basically strips the domain from the conference.domain address
  20. local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
  21. if parentHostName == nil then
  22. log("error", "Failed to start - unable to get parent hostname");
  23. return;
  24. end
  25. local parentCtx = module:context(parentHostName);
  26. if parentCtx == nil then
  27. log("error",
  28. "Failed to start - unable to get parent context for host: %s",
  29. tostring(parentHostName));
  30. return;
  31. end
  32. local token_util = module:require "token/util".new(parentCtx);
  33. -- option to enable/disable token verifications
  34. local disableTokenVerification
  35. = module:get_option_boolean("disable_polergeist_token_verification", false);
  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. -- @param session the session to use for storing token specific fields
  102. -- @return true if values are ok or false otherwise
  103. function verify_token(token, room_name, group, session)
  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. 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. -- notify that user connected using the poltergeist
  158. update_poltergeist_occupant_status(
  159. room, nick, "connected");
  160. remove_poltergeist_occupant(room, nick, true);
  161. end
  162. return username;
  163. end
  164. return nil;
  165. end);
  166. -- Removes poltergeist occupant
  167. -- @param room the room instance where to remove the occupant
  168. -- @param nick the nick of the occupant to remove
  169. -- @param ignore to mark the poltergeist unavailble presence to be ignored
  170. function remove_poltergeist_occupant(room, nick, ignore)
  171. log("debug", "remove_poltergeist_occupant %s", nick);
  172. local current_presence = get_presence(room, nick);
  173. if (not current_presence) then
  174. module:log("info", "attempted to remove a poltergeist with no presence")
  175. return;
  176. end
  177. local leave_presence = st.clone(current_presence)
  178. leave_presence.attr.to = room.jid.."/"..nick;
  179. leave_presence.attr.from = poltergeist_component.."/"..nick;
  180. leave_presence.attr.type = "unavailable";
  181. if (ignore) then
  182. poltergeists_pr_ignore[room.jid.."/"..nick] = true;
  183. end
  184. room:handle_normal_presence(
  185. prosody.hosts[poltergeist_component], leave_presence);
  186. remove_username(room, nick);
  187. end
  188. -- Updates poltergeist occupant status
  189. -- @param room the room instance where to remove the occupant
  190. -- @param nick the nick of the occupant to remove
  191. -- @param status the status to update
  192. -- @param call_details is a table of call flow details
  193. function update_poltergeist_occupant_status(room, nick, status, call_details)
  194. local update_presence = get_presence(room, nick);
  195. if (not update_presence) then
  196. -- TODO: determine if we should provide an error and how that would be
  197. -- handled for bosh and http api.
  198. module:log("info", "update issued for a non-existing poltergeist")
  199. return;
  200. end
  201. -- update occupant presence with appropriate to and from
  202. -- so we can send it again
  203. update_presence = st.clone(update_presence);
  204. update_presence.attr.to = room.jid.."/"..nick;
  205. update_presence.attr.from = poltergeist_component.."/"..nick;
  206. update_presence = update_presence_tags(update_presence, status, call_details)
  207. room:handle_normal_presence(
  208. prosody.hosts[poltergeist_component], update_presence);
  209. end
  210. -- Updates the status tags and call flow tags of an existing poltergeist's
  211. -- presence.
  212. -- @param presence_stanza is the actual presence stanza for a poltergeist.
  213. -- @param status is the new status to be updated in the stanza.
  214. -- @param call_details is a table of call flow signal information.
  215. function update_presence_tags(presence_stanza, status, call_details)
  216. local call_cancel = false;
  217. local call_id = nil;
  218. -- Extract optional call flow signal information.
  219. if call_details then
  220. call_id = call_details["id"];
  221. if call_details["cancel"] then
  222. call_cancel = call_details["cancel"];
  223. end
  224. end
  225. presence_stanza:maptags(function (tag)
  226. if tag.name == "status" then
  227. if call_cancel then
  228. -- If call cancel is set then the status should not be changed.
  229. return tag
  230. end
  231. return st.stanza("status"):text(status);
  232. elseif tag.name == "call_id" then
  233. if call_id then
  234. return st.stanza("call_id"):text(call_id);
  235. else
  236. -- If no call id is provided the re-use the existing id.
  237. return tag;
  238. end
  239. elseif tag.name == "call_cancel" then
  240. if call_cancel then
  241. return st.stanza("call_cancel"):text("true");
  242. else
  243. return st.stanza("call_cancel"):text("false");
  244. end
  245. end
  246. return tag;
  247. end);
  248. return presence_stanza
  249. end
  250. -- Checks for existance of a poltergeist occupant
  251. -- @param room the room instance where to check for occupant
  252. -- @param nick the nick of the occupant
  253. -- @return true if occupant is found, false otherwise
  254. function have_poltergeist_occupant(room, nick)
  255. -- Find out if we have a poltergeist occupant in the room for this JID
  256. return not not room:get_occupant_jid(poltergeist_component.."/"..nick);
  257. end
  258. -- Returns the last presence of occupant
  259. -- @param room the room instance where to check for occupant
  260. -- @param nick the nick of the occupant
  261. -- @return presence of the occupant
  262. function get_presence(room, nick)
  263. local occupant_jid
  264. = room:get_occupant_jid(poltergeist_component.."/"..nick);
  265. if (occupant_jid) then
  266. return room:get_occupant_by_nick(occupant_jid):get_presence();
  267. end
  268. return nil;
  269. end
  270. -- Event handlers
  271. --- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
  272. --- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
  273. module:hook("muc-decline", function (event)
  274. remove_poltergeist_occupant(event.room, bare(event.stanza.attr.from), false);
  275. end, -100);
  276. -- before sending the presence for a poltergeist leaving add ignore tag
  277. -- as poltergeist is leaving just before the real user joins and in the client
  278. -- we ignore this presence to avoid leaving/joining experience and the real
  279. -- user will reuse all currently created UI components for the same nick
  280. module:hook("muc-broadcast-presence", function (event)
  281. if (bare(event.occupant.jid) == poltergeist_component) then
  282. if(event.stanza.attr.type == "unavailable"
  283. and poltergeists_pr_ignore[event.occupant.nick]) then
  284. event.stanza:tag(
  285. "ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
  286. poltergeists_pr_ignore[event.occupant.nick] = nil;
  287. end
  288. end
  289. end, -100);
  290. -- cleanup room table after room is destroyed
  291. module:hook("muc-room-destroyed",function(event)
  292. local room_name = jid.node(event.room.jid);
  293. if (poltergeists[room_name]) then
  294. poltergeists[room_name] = nil;
  295. end
  296. end);
  297. --- Handles request for creating/managing poltergeists
  298. -- @param event the http event, holds the request query
  299. -- @return GET response, containing a json with response details
  300. function handle_create_poltergeist (event)
  301. if (not event.request.url.query) then
  302. return 400;
  303. end
  304. local params = parse(event.request.url.query);
  305. local user_id = params["user"];
  306. local room_name = params["room"];
  307. local group = params["group"];
  308. local name = params["name"];
  309. local avatar = params["avatar"];
  310. local status = params["status"];
  311. local session = {};
  312. if not verify_token(params["token"], room_name, group, session) then
  313. return 403;
  314. end
  315. -- If the provided room conference doesn't exist then we
  316. -- can't add a poltergeist to it.
  317. local room = get_room(room_name, group);
  318. if (not room) then
  319. log("error", "no room found %s", room_name);
  320. return 404;
  321. end
  322. -- If the poltergiest is already in the conference then it will
  323. -- be in our username store and another can't be added.
  324. local username = get_username(room, user_id);
  325. if (username ~= nil
  326. and have_poltergeist_occupant(room, string.sub(username, 0, 8))) then
  327. log("warn", "poltergeist for username:%s already in the room:%s",
  328. username, room_name);
  329. return 202;
  330. end
  331. username = generate_uuid();
  332. local context = {
  333. user = {
  334. id = user_id;
  335. };
  336. group = group;
  337. creator_user = session.jitsi_meet_context_user;
  338. creator_group = session.jitsi_meet_context_group;
  339. };
  340. local nick = string.sub(username, 0, 8)
  341. local presence_stanza = original_presence(
  342. poltergeist_component,
  343. room,
  344. nick,
  345. name,
  346. avatar,
  347. username,
  348. context,
  349. status
  350. )
  351. store_username(room, user_id, username);
  352. room:handle_first_presence(
  353. prosody.hosts[poltergeist_component],
  354. presence_stanza
  355. );
  356. -- the timeout before removing so participants can see the status update
  357. local removeTimeout = 5;
  358. local timeout = poltergeist_timeout - removeTimeout;
  359. timer.add_task(timeout,
  360. function ()
  361. update_poltergeist_occupant_status(
  362. room, nick, expired_status);
  363. -- and remove it after some time so participant can see
  364. -- the update
  365. timer.add_task(removeTimeout,
  366. function ()
  367. if (have_poltergeist_occupant(room, nick)) then
  368. remove_poltergeist_occupant(room, nick, false);
  369. end
  370. end);
  371. end);
  372. return 200;
  373. end
  374. -- Generate the original presence for a poltergeist when it is added to a room.
  375. -- @param component is the configured component name for poltergeist.
  376. -- @param room is the room the poltergeist is being added to.
  377. -- @param nick is the nick the poltergeist will use for xmpp.
  378. -- @param avatar is the url of the display avatar for the poltergeist.
  379. -- @param username is the poltergeist unique username.
  380. -- @param context is the context information from the valid auth token.
  381. -- @param status is the status string for the presence.
  382. -- @return a presence stanza
  383. function original_presence(
  384. component, room, nick, name, avatar, username, context, status)
  385. local p = st.presence({
  386. to = room.jid.."/"..nick,
  387. from = component.."/"..nick,
  388. }):tag("x", { xmlns = MUC_NS }):up();
  389. p:tag("bot", { type = "poltergeist" }):up();
  390. p:tag("call_cancel"):text(nil):up();
  391. p:tag("call_id"):text(username):up();
  392. if status then
  393. p:tag("status"):text(status):up();
  394. else
  395. p:tag("status"):text(nil):up();
  396. end
  397. if (name) then
  398. p:tag(
  399. "nick",
  400. { xmlns = "http://jabber.org/protocol/nick" }):text(name):up();
  401. end
  402. if (avatar) then
  403. p:tag("avatar-url"):text(avatar):up();
  404. end
  405. -- If the room has a password set, let the poltergeist enter using it
  406. local room_password = room:get_password();
  407. if room_password then
  408. local join = p:get_child("x", MUC_NS);
  409. join:tag("password", { xmlns = MUC_NS }):text(room_password);
  410. end
  411. update_presence_identity(
  412. p,
  413. context.user,
  414. context.group,
  415. context.creator_user,
  416. context.creator_group
  417. );
  418. return p
  419. end
  420. --- Handles request for updating poltergeists status
  421. -- @param event the http event, holds the request query
  422. -- @return GET response, containing a json with response details
  423. function handle_update_poltergeist (event)
  424. if (not event.request.url.query) then
  425. return 400;
  426. end
  427. local params = parse(event.request.url.query);
  428. local user_id = params["user"];
  429. local room_name = params["room"];
  430. local group = params["group"];
  431. local status = params["status"];
  432. local call_id = params["callid"];
  433. local call_cancel = false
  434. if params["callcancel"] == "true" then
  435. call_cancel = true;
  436. end
  437. if not verify_token(params["token"], room_name, group, {}) then
  438. return 403;
  439. end
  440. local room = get_room(room_name, group);
  441. if (not room) then
  442. log("error", "no room found %s", room_name);
  443. return 404;
  444. end
  445. local username = get_username(room, user_id);
  446. if (not username) then
  447. return 404;
  448. end
  449. local call_details = {
  450. ["cancel"] = call_cancel;
  451. ["id"] = call_id;
  452. };
  453. local nick = string.sub(username, 0, 8);
  454. if (not have_poltergeist_occupant(room, nick)) then
  455. return 404;
  456. end
  457. update_poltergeist_occupant_status(room, nick, status, call_details);
  458. return 200;
  459. end
  460. --- Handles remove poltergeists
  461. -- @param event the http event, holds the request query
  462. -- @return GET response, containing a json with response details
  463. function handle_remove_poltergeist (event)
  464. if (not event.request.url.query) then
  465. return 400;
  466. end
  467. local params = parse(event.request.url.query);
  468. local user_id = params["user"];
  469. local room_name = params["room"];
  470. local group = params["group"];
  471. if not verify_token(params["token"], room_name, group, {}) then
  472. return 403;
  473. end
  474. local room = get_room(room_name, group);
  475. if (not room) then
  476. log("error", "no room found %s", room_name);
  477. return 404;
  478. end
  479. local username = get_username(room, user_id);
  480. if (not username) then
  481. return 404;
  482. end
  483. local nick = string.sub(username, 0, 8);
  484. if (not have_poltergeist_occupant(room, nick)) then
  485. return 404;
  486. end
  487. remove_poltergeist_occupant(room, nick, false);
  488. return 200;
  489. end
  490. log("info", "Loading poltergeist service");
  491. module:depends("http");
  492. module:provides("http", {
  493. default_path = "/";
  494. name = "poltergeist";
  495. route = {
  496. ["GET /poltergeist/create"] = function (event) return wrap_async_run(event,handle_create_poltergeist) end;
  497. ["GET /poltergeist/update"] = function (event) return wrap_async_run(event,handle_update_poltergeist) end;
  498. ["GET /poltergeist/remove"] = function (event) return wrap_async_run(event,handle_remove_poltergeist) end;
  499. };
  500. });