Skip to content

Instantly share code, notes, and snippets.

@bjc
Created April 9, 2010 15:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjc/361285 to your computer and use it in GitHub Desktop.
Save bjc/361285 to your computer and use it in GitHub Desktop.
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