Created
April 9, 2010 15:36
-
-
Save bjc/361285 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if module:get_host_type() ~= "component" then | |
error("Call presence module must be loaded as a component.", 0) | |
end | |
local serialize = require "util.serialization".serialize | |
local eventmanager = require "core.eventmanager" | |
local st = require "util.stanza" | |
local register_component = require "core.componentmanager".register_component | |
local deregister_component = require "core.componentmanager".deregister_component | |
local component | |
local NS_DISCO_INFO = "http://jabber.org/protocol/disco#info" | |
local NS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items" | |
local NS_COMMANDS = "http://jabber.org/protocol/commands" | |
local NS_X_DATA = "jabber:x:data" | |
local NODE_TOGGLE_ON_CALL = "toggle-on-call" | |
local waiting_for_first_presence = {} | |
local presence_overrides = {} | |
local function make_jid_tuple(jid) | |
local node, domain, resource | |
if jid then | |
local i, j | |
-- Strip resource first, as most specific. | |
i, j = jid:find('/') | |
if i then | |
resource = jid:sub(j + 1, #jid) | |
jid = jid:sub(1, i - 1) | |
end | |
-- Strip node as next most specific. | |
i, j = jid:find('@') | |
if i then | |
node = jid:sub(1, i - 1) | |
jid = jid:sub(j + 1, #jid) | |
end | |
-- Only domain remains. | |
domain = jid | |
end | |
return node, domain, resource | |
end | |
local function tuple_to_bare_jid(node, domain) | |
if node then | |
-- If we have a node then use `node@domain' syntax. | |
return node .. '@' .. domain | |
else | |
-- Otherwise we just return the domain for service JIDs. | |
return domain | |
end | |
end | |
local function tuple_to_full_jid(node, domain, resource) | |
if resource then | |
-- If we have a resource return the full `node@domain/resource' syntax. | |
return tuple_to_bare_jid(node, domain) .. '/' .. resource | |
else | |
-- Otherwise just return a bare JID. | |
return tuple_to_bare_jid(node, domain) | |
end | |
end | |
local function make_stanza(show, status) | |
local stanza = st.presence() | |
if (show) then | |
stanza:tag("show"):text(show):up() | |
end | |
if (status) then | |
stanza:tag("status"):text(status):up() | |
end | |
return stanza | |
end | |
local function send_to_domain(host, stanza) | |
local h = hosts[host] | |
for user, u in pairs(h.sessions) do | |
local jid = user .. "@" .. host | |
stanza.attr.to = jid | |
core_post_stanza(component, stanza) | |
end | |
end | |
local function set_on_call(user, host) | |
local h = hosts[host] | |
local jid = user .. "@" .. host | |
if h and h.type == "local" then | |
local u = h.sessions[user] | |
if u then | |
module:log("error", "DEBUG: jid %s found", jid) | |
local old_presence = {} | |
module:log("debug", "setting %s on call", jid) | |
for resource, session in pairs(u.sessions) do | |
local new_presence = make_stanza("away", "On the Phone") | |
new_presence.attr.from = jid .. "/" .. resource | |
old_presence[resource] = session.presence | |
send_to_domain(host, new_presence) | |
end | |
presence_overrides[jid] = old_presence | |
else | |
-- If the user isn't online, flag them to send new presence | |
-- when they come online. | |
module:log("error", "DEBUG: jid %s not found", jid) | |
waiting_for_first_presence[jid] = true | |
end | |
end | |
end | |
local function set_off_call(user, host) | |
local h = hosts[host] | |
local jid = user .. "@" .. host | |
if h and h.type == "local" then | |
local u = h.sessions[user] | |
if u then | |
module:log("debug", "setting %s off call", jid) | |
for resource, session in pairs(u.sessions) do | |
local old_presence = presence_overrides[jid] | |
if old_presence and old_presence[resource] then | |
send_to_domain(host, old_presence[resource]) | |
end | |
end | |
end | |
-- Clear presence overrides regardless of online status. | |
presence_overrides[jid] = nil | |
waiting_for_first_presence[jid] = nil | |
end | |
end | |
local function disco_info(stanza) | |
local node = stanza.tags[1].attr.node | |
local reply | |
if node == NS_COMMANDS then | |
reply = st.reply(stanza):query(NS_DISCO_INFO) | |
reply:tag("identity", {category = 'automation', | |
type = 'command-list', | |
name = stanza.attr.to}):up() | |
reply:tag("feature", {var = NS_DISCO_INFO}):up() | |
reply:tag("feature", {var = NS_DISCO_ITEMS}):up() | |
reply:tag("feature", {var = NS_COMMANDS}):up() | |
reply:up() | |
elseif node == NODE_TOGGLE_ON_CALL then | |
reply = st.reply(stanza):query(NS_DISCO_INFO) | |
reply:tag("identity", {category = 'automation', | |
type = 'command-node', | |
name = stanza.attr.to}):up() | |
reply:tag("feature", {var = NS_COMMANDS}):up() | |
reply:tag("feature", {var = NS_X_DATA}):up() | |
reply:up() | |
elseif not node then | |
reply = st.reply(stanza):query(NS_DISCO_INFO) | |
reply:tag("feature", {var = NS_DISCO_INFO}):up() | |
reply:tag("feature", {var = NS_DISCO_ITEMS}):up() | |
reply:tag("feature", {var = NS_COMMANDS}):up() | |
reply:up() | |
else | |
reply = st.error_reply(stanza, "cancel", "item-not-found") | |
end | |
return reply | |
end | |
local function disco_items(stanza) | |
local node = stanza.tags[1].attr.node | |
local reply | |
if node == NS_COMMANDS then | |
reply = st.reply(stanza):query(NS_DISCO_ITEMS) | |
reply:tag("item", {jid = stanza.attr.to, | |
node = NODE_TOGGLE_ON_CALL, | |
name = 'Toggle someone on call'}):up() | |
elseif not node then | |
reply = st.reply(stanza):query(NS_DISCO_ITEMS) | |
reply:tag("item", {jid = stanza.attr.to, | |
node = NODE_TOGGLE_ON_CALL, | |
name = 'Toggle someone on call'}):up() | |
reply:up() | |
else | |
reply = st.error_reply(stanza, "cancel", "item-not-found") | |
end | |
return reply | |
end | |
local function xform_toggle_on_call(sessionid) | |
local attr = {xmlns = NS_COMMANDS, node = NODE_TOGGLE_ON_CALL, | |
status = 'executing'} | |
local cmd, xdata | |
if sessionid then | |
attr.sessionid = sessionid | |
end | |
cmd = st.stanza("command", attr) | |
xdata = st.stanza("x", {xmlns = NS_X_DATA}) | |
xdata:tag("title"):text("Toggle on-the-phone status for a JID"):up() | |
xdata:tag("field", {label = "SIP Address", | |
var = "sip-address", | |
type = "jid-single"}):up() | |
xdata:tag("field", {label = "Digital Signature", | |
var = "signature", | |
type = "text-single"}):up() | |
cmd:add_child(xdata) | |
cmd:tag("actions"):tag("complete"):up():up() | |
cmd:tag("note"):text("Enter a JID to put on call and signature."):up() | |
return cmd | |
end | |
local function get_children_with_name(el, name) | |
local children = {} | |
local node | |
for _, node in ipairs(el.tags) do | |
if node.name == name then | |
table.insert(children, node) | |
end | |
end | |
return children | |
end | |
local function parse_x_data(x) | |
local fields = get_children_with_name(x, "field") | |
local form = {} | |
local fieldEl | |
for _, fieldEl in ipairs(fields) do | |
local var = fieldEl.attr.var | |
if var then | |
local valueEls = get_children_with_name(fieldEl, "value") | |
local values = {} | |
local valueEl | |
for _, valueEl in ipairs(valueEls) do | |
table.insert(values, valueEl:get_text()) | |
end | |
form[var] = values | |
end | |
end | |
return form | |
end | |
-- TODO: Check signature instead of always being true. | |
local function valid_signature(sip_address, signature) | |
return true | |
end | |
-- TODO: Return errors when required fields are missing. Cross ref | |
-- with XEP-0050 (Ad-Hoc Commands) for proper error codes. | |
local function complete_toggle_on_call(sessionid, x) | |
local form = parse_x_data(x) | |
local sip_address = form['sip-address'][1] | |
local signature = form['signature'][1] | |
if sip_address and signature and valid_signature(sip_address, signature) then | |
module:log("debug", "Toggle on call for: %s", sip_address) | |
local user, host, _ = make_jid_tuple(sip_address) | |
if user and host then | |
if presence_overrides[sip_address] then | |
set_off_call(user, host) | |
else | |
set_on_call(user, host) | |
end | |
local attr = {xmlns = NS_COMMANDS, node = NODE_TOGGLE_ON_CALL, | |
status = 'completed'} | |
if sessionid then | |
attr.sessionid = sessionid | |
end | |
return st.stanza("command", attr) | |
end | |
else | |
module:log("warn", | |
"Missing either sip address (%s) or signature (%s) when trying to toggle-on-call", | |
tostring(sip_address), tostring(signature)) | |
end | |
end | |
-- TODO: Return specific condition on errors. | |
local function command(stanza) | |
local cmd = stanza:get_child('command', NS_COMMANDS) | |
local node = cmd.attr.node | |
local action = cmd.attr.action or 'execute' | |
local sessionid = cmd.attr.sessionid | |
local x = cmd:get_child('x', NS_X_DATA) | |
local reply | |
module:log("debug", "Got %s command for %s", action, tostring(node)) | |
if action == 'cancel' then | |
local attr = {xmlns = NS_COMMANDS, node = node, status = 'canceled'} | |
if sessionid then | |
attr.sessionid = sessionid | |
end | |
local cmd = st.stanza("command", attr) | |
reply = st.reply(stanza):add_child(cmd) | |
elseif node == NODE_TOGGLE_ON_CALL then | |
if action == 'complete' then | |
if x and x.attr.xmlns == NS_X_DATA then | |
local cmd = complete_toggle_on_call(sessionid, x) | |
reply = st.reply(stanza):add_child(cmd) | |
else | |
reply = st.error_reply(stanza, "modify", "bad-request", | |
"Submission is missing parameters") | |
end | |
elseif action == 'execute' then | |
local cmd = xform_toggle_on_call(sessionid) | |
reply = st.reply(stanza):add_child(cmd) | |
else | |
reply = st.error_reply(stanza, "modify", "bad-request", | |
"Cannot handle action: " .. tostring(action)) | |
end | |
else | |
reply = st.error_reply(stanza, "modify", "bad-request", | |
"Cannot handle node: " .. tostring(node)) | |
end | |
return reply | |
end | |
local function handle_incoming(origin, stanza) | |
local type = stanza.attr.type | |
if type == "error" or type == "result" then | |
return | |
elseif stanza.name == "iq" then | |
local xmlns = stanza.tags[1].attr.xmlns | |
if type == "get" then | |
if xmlns == NS_DISCO_INFO then | |
origin.send(disco_info(stanza)) | |
elseif xmlns == NS_DISCO_ITEMS then | |
origin.send(disco_items(stanza)) | |
else | |
origin.send(st.error_reply(stanza, "cancel", "service-unavailable", | |
"Could not handle request")) | |
end | |
elseif type == "set" then | |
if xmlns == NS_COMMANDS then | |
origin.send(command(stanza)) | |
else | |
origin.send(st.error_reply(stanza, "cancel", "service-unavailable", | |
"Could not handle request")) | |
end | |
else | |
origin.send(st.error_reply(stanza, "cancel", "service-unavailable", | |
"Could not handle request")) | |
end | |
else | |
origin.send(st.error_reply(stanza, "cancel", "service-unavailable", | |
"Could not handle request")) | |
end | |
end | |
local function handle_presence(data) | |
module:log("warn", "Got presence: %s", tostring(data.stanza)) | |
local stanza = data.stanza | |
local from, to = stanza.attr.from, stanza.attr.to | |
local bareFrom, bareTo, node, domain | |
node, domain = make_jid_tuple(from) | |
bareFrom = tuple_to_bare_jid(node, domain) | |
bareTo = tuple_to_bare_jid(make_jid_tuple(to)) | |
-- If this user just logged in and we have an override, send our | |
-- presence. | |
if waiting_for_first_presence[bareFrom] then | |
-- This only happens when we're on a call, so we can just run | |
-- the (mostly-)idempotent set_on_call function. | |
module:log("error", "DEBUG: setting previously offline person on call: %s", bareFrom) | |
set_on_call(node, domain) | |
waiting_for_first_presence[bareFrom] = nil | |
return | |
end | |
-- Clear presence overrides if this is undirected presence. | |
if not to and presence_overrides[bareFrom] then | |
module:log("debug", | |
"Clearing presence override for %s because of new presence.", | |
from) | |
presence_overrides[bareFrom] = stanza | |
end | |
end | |
local function resource_bind(event) | |
-- module:log("error", "DEBUG: setting wait for %s", event.session.full_jid) | |
-- waiting_for_first_presence[event.session.full_jid] = true | |
end | |
local function resource_unbind(event) | |
-- module:log("error", "DEBUG: clearing wait for %s", event.session.full_jid) | |
-- waiting_for_first_presence[event.session.full_jid] = nil | |
end | |
local function host_activated(host) | |
module:log("info", "Activating presence overrides for %s", tostring(host)) | |
local h = hosts[host] | |
h.events.add_handler("resource-bind", resource_bind) | |
h.events.add_handler("resource-unbind", resource_unbind) | |
h.events.add_handler("pre-presence/full", handle_presence) | |
h.events.add_handler("pre-presence/bare", handle_presence) | |
end | |
local function load() | |
local host | |
module:log("info", "Call presence module loaded.") | |
for host, _ in pairs(hosts) do | |
host_activated(host) | |
end | |
eventmanager.add_event_hook("host-activated", host_activated) | |
end | |
local function unload() | |
module:log("info", "Call presence module unloaded.") | |
end | |
local function save() | |
module:log("info", "Call presence module saving.") | |
end | |
local function restore() | |
module:log("info", "Call presence module restoring.") | |
end | |
module.load = load | |
module.unload = unload | |
module.save = save | |
module.restore = restore | |
component = register_component(module:get_host(), handle_incoming) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment