Creating a poltergiest library and using in for mod_muc_poltergeist.

This commit is contained in:
Jacob MacElroy 2018-06-15 19:28:05 +00:00 committed by Дамян Минков
parent 535e5b4f64
commit 6ae5adcb3d
3 changed files with 419 additions and 315 deletions

View File

@ -39,8 +39,7 @@ local function url_from_room_jid(room_jid)
if not(target_node or target_subdomain) then
return "https://"..muc_domain_base.."/"..node
else
return
"https://"..muc_domain_base.."/"..target_subdomain.."/"..target_node
return "https://"..muc_domain_base.."/"..target_subdomain.."/"..target_node
end
end

View File

@ -1,22 +1,15 @@
local bare = require "util.jid".bare;
local generate_uuid = require "util.uuid".generate;
local get_room_from_jid = module:require "util".get_room_from_jid;
local jid = require "util.jid";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local st = require "util.stanza";
local get_room_from_jid = module:require "util".get_room_from_jid;
local poltergeist = module:require "poltergeist";
local wrap_async_run = module:require "util".wrap_async_run;
local update_presence_identity = module:require "util".update_presence_identity;
local timer = require "util.timer";
local MUC_NS = "http://jabber.org/protocol/muc";
local expired_status = "expired";
-- Options
local poltergeist_component
= module:get_option_string("poltergeist_component", module.host);
-- defaults to 30 seconds
local poltergeist_timeout
= module:get_option_string("poltergeist_leave_timeout", 30);
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
@ -37,12 +30,6 @@ local token_util = module:require "token/util".new(parentCtx);
local disableTokenVerification
= module:get_option_boolean("disable_polergeist_token_verification", false);
-- table to store all poltergeists we create
local poltergeists = {};
-- table to mark that outgoing unavailable presences
-- should be marked with ignore
local poltergeists_pr_ignore = {};
-- poltergaist management functions
-- Returns the room if available, work and in multidomain mode
@ -60,54 +47,6 @@ function get_room(room_name, group)
return get_room_from_jid(room_address);
end
-- Stores the username in the table where we store poltergeist usernames
-- based on their room names
-- @param room the room instance
-- @param user_id the user id
-- @param username the username to store
function store_username(room, user_id, username)
local room_name = jid.node(room.jid);
-- we store in poltergeist user ids for room names
if (not poltergeists[room_name]) then
poltergeists[room_name] = {};
end
poltergeists[room_name][user_id] = username;
log("debug", "stored in session: %s", username);
end
-- Retrieve the username for a user
-- @param room the room instance
-- @param user_id the user id
-- @return returns the stored username for user or nil
function get_username(room, user_id)
local room_name = jid.node(room.jid);
if (not poltergeists[room_name]) then
return nil;
end
return poltergeists[room_name][user_id];
end
-- Removes poltergeist values from table
-- @param room the room instance
-- @param nick the user nick
function remove_username(room, nick)
local room_name = jid.node(room.jid);
if (poltergeists[room_name]) then
local user_id_to_remove;
for name,username in pairs(poltergeists[room_name]) do
if (string.sub(username, 0, 8) == nick) then
user_id_to_remove = name;
end
end
if (user_id_to_remove) then
poltergeists[room_name][user_id_to_remove] = nil;
end
end
end
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
@ -151,6 +90,8 @@ function verify_token(token, room_name, group, session)
return true;
end
-- Event handlers
-- if we found that a session for a user with id has a poltergiest already
-- created, retrieve its jid and return it to the authentication
-- so we can reuse it and we that real user will replace the poltergiest
@ -165,8 +106,10 @@ prosody.events.add_handler("pre-jitsi-authentication", function(session)
return nil;
end
local username
= get_username(room, session.jitsi_meet_context_user["id"]);
local username = poltergeist.get_username(
room,
session.jitsi_meet_context_user["id"]
);
if (not username) then
return nil;
@ -178,12 +121,12 @@ prosody.events.add_handler("pre-jitsi-authentication", function(session)
-- lets remove him before the real participant joins
-- when we see the unavailable presence to go out the server
-- we will mark it with ignore tag
local nick = string.sub(username, 0, 8);
if (have_poltergeist_occupant(room, nick)) then
local nick = poltergeist.create_nick(username);
if (poltergeist.occupies(room, nick)) then
module:log("info", "swapping poltergeist for user: %s/%s", room, nick)
-- notify that user connected using the poltergeist
update_poltergeist_occupant_status(
room, nick, "connected");
remove_poltergeist_occupant(room, nick, true);
poltergeist.update(room, nick, "connected");
poltergeist.remove(room, nick, true);
end
return username;
@ -192,134 +135,10 @@ prosody.events.add_handler("pre-jitsi-authentication", function(session)
return nil;
end);
-- Removes poltergeist occupant
-- @param room the room instance where to remove the occupant
-- @param nick the nick of the occupant to remove
-- @param ignore to mark the poltergeist unavailble presence to be ignored
function remove_poltergeist_occupant(room, nick, ignore)
log("debug", "remove_poltergeist_occupant %s", nick);
local current_presence = get_presence(room, nick);
if (not current_presence) then
module:log("info", "attempted to remove a poltergeist with no presence")
return;
end
local leave_presence = st.clone(current_presence)
leave_presence.attr.to = room.jid.."/"..nick;
leave_presence.attr.from = poltergeist_component.."/"..nick;
leave_presence.attr.type = "unavailable";
if (ignore) then
poltergeists_pr_ignore[room.jid.."/"..nick] = true;
end
room:handle_normal_presence(
prosody.hosts[poltergeist_component], leave_presence);
remove_username(room, nick);
end
-- Updates poltergeist occupant status
-- @param room the room instance where to remove the occupant
-- @param nick the nick of the occupant to remove
-- @param status the status to update
-- @param call_details is a table of call flow details
function update_poltergeist_occupant_status(room, nick, status, call_details)
local update_presence = get_presence(room, nick);
if (not update_presence) then
-- TODO: determine if we should provide an error and how that would be
-- handled for bosh and http api.
module:log("info", "update issued for a non-existing poltergeist")
return;
end
-- update occupant presence with appropriate to and from
-- so we can send it again
update_presence = st.clone(update_presence);
update_presence.attr.to = room.jid.."/"..nick;
update_presence.attr.from = poltergeist_component.."/"..nick;
update_presence = update_presence_tags(update_presence, status, call_details)
room:handle_normal_presence(
prosody.hosts[poltergeist_component], update_presence);
end
-- Updates the status tags and call flow tags of an existing poltergeist's
-- presence.
-- @param presence_stanza is the actual presence stanza for a poltergeist.
-- @param status is the new status to be updated in the stanza.
-- @param call_details is a table of call flow signal information.
function update_presence_tags(presence_stanza, status, call_details)
local call_cancel = false;
local call_id = nil;
-- Extract optional call flow signal information.
if call_details then
call_id = call_details["id"];
if call_details["cancel"] then
call_cancel = call_details["cancel"];
end
end
presence_stanza:maptags(function (tag)
if tag.name == "status" then
if call_cancel then
-- If call cancel is set then the status should not be changed.
return tag
end
return st.stanza("status"):text(status);
elseif tag.name == "call_id" then
if call_id then
return st.stanza("call_id"):text(call_id);
else
-- If no call id is provided the re-use the existing id.
return tag;
end
elseif tag.name == "call_cancel" then
if call_cancel then
return st.stanza("call_cancel"):text("true");
else
return st.stanza("call_cancel"):text("false");
end
end
return tag;
end);
return presence_stanza
end
-- Checks for existance of a poltergeist occupant
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return true if occupant is found, false otherwise
function have_poltergeist_occupant(room, nick)
-- Find out if we have a poltergeist occupant in the room for this JID
return not not room:get_occupant_jid(poltergeist_component.."/"..nick);
end
-- Returns the last presence of occupant
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return presence of the occupant
function get_presence(room, nick)
local occupant_jid
= room:get_occupant_jid(poltergeist_component.."/"..nick);
if (occupant_jid) then
return room:get_occupant_by_nick(occupant_jid):get_presence();
end
return nil;
end
-- Event handlers
--- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
--- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
module:hook("muc-decline", function (event)
remove_poltergeist_occupant(event.room, bare(event.stanza.attr.from), false);
poltergeist.remove(event.room, bare(event.stanza.attr.from), false);
end, -100);
-- before sending the presence for a poltergeist leaving add ignore tag
-- as poltergeist is leaving just before the real user joins and in the client
@ -328,21 +147,21 @@ end, -100);
module:hook("muc-broadcast-presence", function (event)
if (bare(event.occupant.jid) == poltergeist_component) then
if(event.stanza.attr.type == "unavailable"
and poltergeists_pr_ignore[event.occupant.nick]) then
and poltergeist.should_ignore(event.occupant.nick)) then
event.stanza:tag(
"ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
poltergeists_pr_ignore[event.occupant.nick] = nil;
poltergeist.reset_ignored(event.occupant.nick);
end
end
end, -100);
-- cleanup room table after room is destroyed
module:hook("muc-room-destroyed",function(event)
local room_name = jid.node(event.room.jid);
if (poltergeists[room_name]) then
poltergeists[room_name] = nil;
module:hook(
"muc-room-destroyed",
function(event)
poltergeist.remove_room(event.room);
end
end);
);
--- Handles request for creating/managing poltergeists
-- @param event the http event, holds the request query
@ -375,14 +194,17 @@ function handle_create_poltergeist (event)
-- If the poltergiest is already in the conference then it will
-- be in our username store and another can't be added.
local username = get_username(room, user_id);
if (username ~= nil
and have_poltergeist_occupant(room, string.sub(username, 0, 8))) then
log("warn", "poltergeist for username:%s already in the room:%s",
username, room_name);
local username = poltergeist.get_username(room, user_id);
if (username ~=nil and
poltergeist.occupies(room, poltergeist.create_nick(username))) then
log("warn",
"poltergeist for username:%s already in the room:%s",
username,
room_name
);
return 202;
end
username = generate_uuid();
local context = {
user = {
id = user_id;
@ -391,99 +213,10 @@ function handle_create_poltergeist (event)
creator_user = session.jitsi_meet_context_user;
creator_group = session.jitsi_meet_context_group;
};
local nick = string.sub(username, 0, 8)
local presence_stanza = original_presence(
poltergeist_component,
room,
nick,
name,
avatar,
username,
context,
status
)
store_username(room, user_id, username);
room:handle_first_presence(
prosody.hosts[poltergeist_component],
presence_stanza
);
-- the timeout before removing so participants can see the status update
local removeTimeout = 5;
local timeout = poltergeist_timeout - removeTimeout;
timer.add_task(timeout,
function ()
update_poltergeist_occupant_status(
room, nick, expired_status);
-- and remove it after some time so participant can see
-- the update
timer.add_task(removeTimeout,
function ()
if (have_poltergeist_occupant(room, nick)) then
remove_poltergeist_occupant(room, nick, false);
end
end);
end);
poltergeist.add_to_muc(room, user_id, name, avatar, context, status)
return 200;
end
-- Generate the original presence for a poltergeist when it is added to a room.
-- @param component is the configured component name for poltergeist.
-- @param room is the room the poltergeist is being added to.
-- @param nick is the nick the poltergeist will use for xmpp.
-- @param avatar is the url of the display avatar for the poltergeist.
-- @param username is the poltergeist unique username.
-- @param context is the context information from the valid auth token.
-- @param status is the status string for the presence.
-- @return a presence stanza
function original_presence(
component, room, nick, name, avatar, username, context, status)
local p = st.presence({
to = room.jid.."/"..nick,
from = component.."/"..nick,
}):tag("x", { xmlns = MUC_NS }):up();
p:tag("bot", { type = "poltergeist" }):up();
p:tag("call_cancel"):text(nil):up();
p:tag("call_id"):text(username):up();
if status then
p:tag("status"):text(status):up();
else
p:tag("status"):text(nil):up();
end
if (name) then
p:tag(
"nick",
{ xmlns = "http://jabber.org/protocol/nick" }):text(name):up();
end
if (avatar) then
p:tag("avatar-url"):text(avatar):up();
end
-- If the room has a password set, let the poltergeist enter using it
local room_password = room:get_password();
if room_password then
local join = p:get_child("x", MUC_NS);
join:tag("password", { xmlns = MUC_NS }):text(room_password);
end
update_presence_identity(
p,
context.user,
context.group,
context.creator_user,
context.creator_group
);
return p
end
--- Handles request for updating poltergeists status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
@ -514,7 +247,7 @@ function handle_update_poltergeist (event)
return 404;
end
local username = get_username(room, user_id);
local username = poltergeist.get_username(room, user_id);
if (not username) then
return 404;
end
@ -524,12 +257,12 @@ function handle_update_poltergeist (event)
["id"] = call_id;
};
local nick = string.sub(username, 0, 8);
if (not have_poltergeist_occupant(room, nick)) then
local nick = poltergeist.create_nick(username);
if (not poltergeist.occupies(room, nick)) then
return 404;
end
update_poltergeist_occupant_status(room, nick, status, call_details);
poltergeist.update(room, nick, status, call_details);
return 200;
end
@ -556,17 +289,17 @@ function handle_remove_poltergeist (event)
return 404;
end
local username = get_username(room, user_id);
local username = poltergeist.get_username(room, user_id);
if (not username) then
return 404;
end
local nick = string.sub(username, 0, 8);
if (not have_poltergeist_occupant(room, nick)) then
local nick = poltergeist.create_nick(username);
if (not poltergeist.occupies(room, nick)) then
return 404;
end
remove_poltergeist_occupant(room, nick, false);
poltergeist.remove(room, nick, false);
return 200;
end

View File

@ -0,0 +1,372 @@
local inspect = require("inspect")
local jid = require("util.jid")
local stanza = require("util.stanza")
local timer = require("util.timer")
local update_presence_identity = module:require("util").update_presence_identity
local uuid = require("util.uuid")
local component = module:get_option_string(
"poltergeist_component",
module.host
)
local expiration_timeout = module:get_option_string(
"poltergeist_leave_timeout",
30 -- defaults to 30 seconds
)
local MUC_NS = "http://jabber.org/protocol/muc"
--------------------------------------------------------------------------------
-- Utility functions for commonly used poltergeist codes.
--------------------------------------------------------------------------------
-- Creates a nick for a poltergeist.
-- @param username is the unique username of the poltergeist
-- @return a nick to use for xmpp
local function create_nick(username)
return string.sub(username, 0,8)
end
-- Returns the last presence of the occupant.
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return presence stanza of the occupant
function get_presence(room, nick)
local occupant_jid = room:get_occupant_jid(component.."/"..nick)
if occupant_jid then
return room:get_occupant_by_nick(occupant_jid):get_presence();
end
return nil;
end
-- Checks for existance of a poltergeist occupant in a room.
-- @param room the room instance where to check for the occupant
-- @param nick the nick of the occupant
-- @return true if occupant is found, false otherwise
function occupies(room, nick)
-- Find out if we have a poltergeist occupant in the room for this JID
return not not room:get_occupant_jid(component.."/"..nick);
end
--------------------------------------------------------------------------------
-- Username storage for poltergeist.
--
-- Every poltergeist will have a username stored in a table underneath
-- the room name that they are currently active in. The username can
-- be retrieved given a room and a user_id. The username is removed from
-- a room by providing the room and the nick.
--
-- A table with a single entry looks like:
-- {
-- ["[hug]hostilewerewolvesthinkslightly"] = {
-- ["655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"] = "ed7757d6-d88d-4e6a-8e24-aca2adc31348",
-- ed7757d6 = "655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"
-- }
-- }
--------------------------------------------------------------------------------
-- username is the table where all poltergeist are stored
local usernames = {}
-- Adds a poltergeist to the store.
-- @param room is the room the poltergeist is being added to
-- @param user_id is the user_id of the user the poltergeist represents
-- @param username is the unique id of the poltergeist itself
local function store_username(room, user_id, username)
local room_name = jid.node(room.jid)
if not usernames[room_name] then
usernames[room_name] = {}
end
usernames[room_name][user_id] = username
usernames[room_name][create_nick(username)] = user_id
end
-- Retrieves a poltergeist username from the store if one exists.
-- @param room is the room to check for the poltergeist in the store
-- @param user_id is the user id of the user the poltergeist represents
local function get_username(room, user_id)
local room_name = jid.node(room.jid)
if not usernames[room_name] then
return nil
end
return usernames[room_name][user_id]
end
local function get_username_from_nick(room_name, nick)
if not usernames[room_name] then
return nil
end
local user_id = usernames[room_name][nick]
return usernames[room_name][user_id]
end
-- Removes the username from the store.
-- @param room is the room the poltergeist is being removed from
-- @param nick is the nick of the muc occupant
local function remove_username(room, nick)
local room_name = jid.node(room.jid);
if not usernames[room_name] then
return
end
local user_id = usernames[room_name][nick]
usernames[room_name][user_id] = nil
usernames[room_name][nick] = nil
end
-- Removes all poltergeists in the store for the provided room.
-- @param room is the room all poltergiest will be removed from
local function remove_room(room)
local room_name = jid.node(room.jid)
if usernames[room_name] then
usernames[room_name] = nil
end
end
--------------------------------------------------------------------------------
-- State for toggling the tagging of presence stanzas with ignored tag.
--
-- A poltergeist with it's full room/nick set to ignore will have a jitsi ignore
-- tag applied to all presence stanza's broadcasted. The following funcitons
-- assisst in managing this state.
--------------------------------------------------------------------------------
local presence_ignored = {}
-- Sets the nick to ignored state.
-- @param room_nick full room/nick jid
local function set_ignored(room_nick)
presence_ignored[room_nick] = true
end
-- Resets the nick out of ignored state.
-- @param room_nick full room/nick jid
local function reset_ignored(room_nick)
presence_ignored[room_nick] = nil
end
-- Determines whether or not the leave presence should be tagged with ignored.
-- @param room_nick full room/nick jid
local function should_ignore(room_nick)
if presence_ignored[room_nick] == nil then
return false
end
return presence_ignored[room_nick]
end
--------------------------------------------------------------------------------
-- Poltergeist control functions for adding, updating and removing poltergeist.
--------------------------------------------------------------------------------
-- Updates the status tags and call flow tags of an existing poltergeist
-- presence.
-- @param presence_stanza is the actual presence stanza for a poltergeist.
-- @param status is the new status to be updated in the stanza.
-- @param call_details is a table of call flow signal information.
function update_presence_tags(presence_stanza, status, call_details)
local call_cancel = false
local call_id = nil
-- Extract optional call flow signal information.
if call_details then
call_id = call_details["id"]
if call_details["cancel"] then
call_cancel = call_details["cancel"]
end
end
presence_stanza:maptags(function (tag)
if tag.name == "status" then
if call_cancel then
-- If call cancel is set then the status should not be changed.
return tag
end
return stanza.stanza("status"):text(status)
elseif tag.name == "call_id" then
if call_id then
return stanza.stanza("call_id"):text(call_id)
else
-- If no call id is provided the re-use the existing id.
return tag
end
elseif tag.name == "call_cancel" then
if call_cancel then
return stanza.stanza("call_cancel"):text("true")
else
return stanza.stanza("call_cancel"):text("false")
end
end
return tag
end)
return presence_stanza
end
-- Updates the presence status of a poltergeist.
-- @param room is the room the poltergeist has occupied
-- @param nick is the xmpp nick of the poltergeist occupant
-- @param status is the status string to set in the presence
-- @param call_details is a table of call flow control details
local function update(room, nick, status, call_details)
local original_presence = get_presence(room, nick)
if not original_presence then
module:log("info", "update issued for a non-existing poltergeist")
return
end
-- update occupant presence with appropriate to and from
-- so we can send it again
update_presence = stanza.clone(original_presence)
update_presence.attr.to = room.jid.."/"..nick
update_presence.attr.from = component.."/"..nick
update_presence = update_presence_tags(update_presence, status, call_details)
module:log("info", "updating poltergeist: %s/%s - %s", room, nick, status)
room:handle_normal_presence(
prosody.hosts[component],
update_presence
)
end
-- Removes the poltergeist from the room.
-- @param room is the room the poltergeist has occupied
-- @param nick is the xmpp nick of the poltergeist occupant
-- @param ignore toggles if the leave subsequent leave presence should be tagged
local function remove(room, nick, ignore)
local original_presence = get_presence(room, nick);
if not original_presence then
module:log("info", "attempted to remove a poltergeist with no presence")
return
end
local leave_presence = stanza.clone(original_presence)
leave_presence.attr.to = room.jid.."/"..nick
leave_presence.attr.from = component.."/"..nick
leave_presence.attr.type = "unavailable"
if (ignore) then
set_ignored(room, nick)
end
remove_username(room, nick)
module:log("info", "removing poltergeist: %s/%s", room, nick)
room:handle_normal_presence(
prosody.hosts[component],
leave_presence
)
end
-- Adds a poltergeist to a muc/room.
-- @param room is the room the poltergeist will occupy
-- @param is the id of the user the poltergeist represents
-- @param display_name is the display name to use for the poltergeist
-- @param avatar is the avatar link used for the poltergeist display
-- @param context is the session context of the user making the request
-- @param status is the presence status string to use
local function add_to_muc(room, user_id, display_name, avatar, context, status)
local username = uuid.generate()
local presence_stanza = original_presence(
room,
username,
display_name,
avatar,
context,
status
)
store_username(room, user_id, username)
module:log("info", "adding poltergeist: %s/%s", room, create_nick(username))
room:handle_first_presence(
prosody.hosts[component],
presence_stanza
)
local remove_delay = 5
local expiration = expiration_timeout - remove_delay;
local nick = create_nick(username)
timer.add_task(
expiration,
function ()
update(room, nick, "expired")
timer.add_task(
remove_delay,
function ()
if occupies(room, nick) then
remove(room, nick, false)
end
end
)
end
)
end
-- Generates an original presence for a new poltergeist
-- @param room is the room the poltergeist will occupy
-- @param username is the unique name for the poltergeist
-- @param display_name is the display name to use for the poltergeist
-- @param avatar is the avatar link used for the poltergeist display
-- @param context is the session context of the user making the request
-- @param status is the presence status string to use
-- @return a presence stanza that can be used to add the poltergeist to the muc
function original_presence(room, username, display_name, avatar, context, status)
local nick = create_nick(username)
local p = stanza.presence({
to = room.jid.."/"..nick,
from = component.."/"..nick,
}):tag("x", { xmlns = MUC_NS }):up();
p:tag("bot", { type = "poltergeist" }):up();
p:tag("call_cancel"):text(nil):up();
p:tag("call_id"):text(username):up();
if status then
p:tag("status"):text(status):up();
else
p:tag("status"):text(nil):up();
end
if display_name then
p:tag(
"nick",
{ xmlns = "http://jabber.org/protocol/nick" }):text(display_name):up();
end
if avatar then
p:tag("avatar-url"):text(avatar):up();
end
-- If the room has a password set, let the poltergeist enter using it
local room_password = room:get_password();
if room_password then
local join = p:get_child("x", MUC_NS);
join:tag("password", { xmlns = MUC_NS }):text(room_password);
end
update_presence_identity(
p,
context.user,
context.group,
context.creator_user,
context.creator_group
)
return p
end
return {
get_username = get_username,
get_username_from_nick = get_username_from_nick,
occupies = occupies,
remove_room = remove_room,
reset_ignored = reset_ignored,
should_ignore = should_ignore,
create_nick = create_nick,
add_to_muc = add_to_muc,
update = update,
remove = remove
}