-- Copyright 2009 Jo-Philipp Wich -- Licensed to the public under the Apache License 2.0. module("luci.asterisk", package.seeall) require("luci.asterisk.cc_idd") local _io = require("io") local uci = require("luci.model.uci").cursor() local sys = require("luci.sys") local util = require("luci.util") AST_BIN = "/usr/sbin/asterisk" AST_FLAGS = "-r -x" --- LuCI Asterisk - Resync uci context function uci_resync() uci = luci.model.uci.cursor() end --- LuCI Asterisk io interface -- Handles low level io. -- @type module io = luci.util.class() --- Execute command and return output -- @param command String containing the command to execute -- @return String containing the command output function io.exec(command) local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" ) assert(fh, "Failed to invoke asterisk") local buffer = fh:read("*a") fh:close() return buffer end --- Execute command and invoke given callback for each read line -- @param command String containing the command to execute -- @param callback Function to call back for each line -- @return Always true function io.execl(command, callback) local ln local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" ) assert(fh, "Failed to invoke asterisk") repeat ln = fh:read("*l") callback(ln) until not ln fh:close() return true end --- Execute command and return an iterator that returns one line per invocation -- @param command String containing the command to execute -- @return Iterator function function io.execi(command) local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" ) assert(fh, "Failed to invoke asterisk") return function() local ln = fh:read("*l") if not ln then fh:close() end return ln end end --- LuCI Asterisk - core status core = luci.util.class() --- Retrieve version string. -- @return String containing the reported asterisk version function core.version(self) local version = io.exec("core show version") return version:gsub(" *\n", "") end --- LuCI Asterisk - SIP information. -- @type module sip = luci.util.class() --- Get a list of known SIP peers -- @return Table containing each SIP peer function sip.peers(self) local head = false local peers = { } for line in io.execi("sip show peers") do if not head then head = true elseif not line:match(" sip peers ") then local online, delay, id, uid local name, host, dyn, nat, acl, port, status = line:match("(.-) +(.-) +([D ]) ([N ]) (.) (%d+) +(.+)") if host == '(Unspecified)' then host = nil end if port == '0' then port = nil else port = tonumber(port) end dyn = ( dyn == 'D' and true or false ) nat = ( nat == 'N' and true or false ) acl = ( acl ~= ' ' and true or false ) online, delay = status:match("(OK) %((%d+) ms%)") if online == 'OK' then online = true delay = tonumber(delay) elseif status ~= 'Unmonitored' then online = false delay = 0 else online = nil delay = 0 end id, uid = name:match("(.+)/(.+)") if not ( id and uid ) then id = name .. "..." uid = nil end peers[#peers+1] = { online = online, delay = delay, name = id, user = uid, dynamic = dyn, nat = nat, acl = acl, host = host, port = port } end end return peers end --- Get information of given SIP peer -- @param peer String containing the name of the SIP peer function sip.peer(peer) local info = { } local keys = { } for line in io.execi("sip show peer " .. peer) do if #line > 0 then local key, val = line:match("(.-) *: +(.*)") if key and val then key = key:gsub("^ +",""):gsub(" +$", "") val = val:gsub("^ +",""):gsub(" +$", "") if key == "* Name" then key = "Name" elseif key == "Addr->IP" then info.address, info.port = val:match("(.+) Port (.+)") info.port = tonumber(info.port) elseif key == "Status" then info.online, info.delay = val:match("(OK) %((%d+) ms%)") if info.online == 'OK' then info.online = true info.delay = tonumber(info.delay) elseif status ~= 'Unmonitored' then info.online = false info.delay = 0 else info.online = nil info.delay = 0 end end if val == 'Yes' or val == 'yes' or val == '' then val = true elseif val == 'No' or val == 'no' then val = false elseif val == '' or val == '(none)' then val = nil end keys[#keys+1] = key info[key] = val end end end return info, keys end --- LuCI Asterisk - Internal helpers -- @type module tools = luci.util.class() --- Convert given value to a list of tokens. Split by white space. -- @param val String or table value -- @return Table containing tokens function tools.parse_list(v) local tokens = { } v = type(v) == "table" and v or { v } for _, v in ipairs(v) do if type(v) == "string" then for v in v:gmatch("(%S+)") do tokens[#tokens+1] = v end end end return tokens end --- Convert given list to a collection of hyperlinks -- @param list Table of tokens -- @param url String pattern or callback function to construct urls (optional) -- @param sep String containing the separator (optional, default is ", ") -- @return String containing the html fragment function tools.hyperlinks(list, url, sep) local html local function mkurl(p, t) if type(p) == "string" then return p:format(t) elseif type(p) == "function" then return p(t) else return '#' end end list = list or { } url = url or "%s" sep = sep or ", " for _, token in ipairs(list) do html = ( html and html .. sep or '' ) .. '%s' %{ mkurl(url, token), token } end return html or '' end --- LuCI Asterisk - International Direct Dialing Prefixes -- @type module idd = luci.util.class() --- Lookup the country name for the given IDD code. -- @param country String containing IDD code -- @return String containing the country name function idd.country(c) for _, v in ipairs(cc_idd.CC_IDD) do if type(v[3]) == "table" then for _, v2 in ipairs(v[3]) do if v2 == tostring(c) then return v[1] end end elseif v[3] == tostring(c) then return v[1] end end end --- Lookup the country code for the given IDD code. -- @param country String containing IDD code -- @return Table containing the country code(s) function idd.cc(c) for _, v in ipairs(cc_idd.CC_IDD) do if type(v[3]) == "table" then for _, v2 in ipairs(v[3]) do if v2 == tostring(c) then return type(v[2]) == "table" and v[2] or { v[2] } end end elseif v[3] == tostring(c) then return type(v[2]) == "table" and v[2] or { v[2] } end end end --- Lookup the IDD code(s) for the given country. -- @param idd String containing the country name -- @return Table containing the IDD code(s) function idd.idd(c) for _, v in ipairs(cc_idd.CC_IDD) do if v[1]:lower():match(c:lower()) then return type(v[3]) == "table" and v[3] or { v[3] } end end end --- Populate given CBI field with IDD codes. -- @param field CBI option object -- @return (nothing) function idd.cbifill(o) for i, v in ipairs(cc_idd.CC_IDD) do o:value("_%i" % i, util.pcdata(v[1])) end o.formvalue = function(...) local val = luci.cbi.Value.formvalue(...) if val:sub(1,1) == "_" then val = tonumber((val:gsub("^_", ""))) if val then return type(cc_idd.CC_IDD[val][3]) == "table" and cc_idd.CC_IDD[val][3] or { cc_idd.CC_IDD[val][3] } end end return val end o.cfgvalue = function(...) local val = luci.cbi.Value.cfgvalue(...) if val then val = tools.parse_list(val) for i, v in ipairs(cc_idd.CC_IDD) do if type(v[3]) == "table" then if v[3][1] == val[1] then return "_%i" % i end else if v[3] == val[1] then return "_%i" % i end end end end return val end end --- LuCI Asterisk - Country Code Prefixes -- @type module cc = luci.util.class() --- Lookup the country name for the given CC code. -- @param country String containing CC code -- @return String containing the country name function cc.country(c) for _, v in ipairs(cc_idd.CC_IDD) do if type(v[2]) == "table" then for _, v2 in ipairs(v[2]) do if v2 == tostring(c) then return v[1] end end elseif v[2] == tostring(c) then return v[1] end end end --- Lookup the international dialing code for the given CC code. -- @param cc String containing CC code -- @return String containing IDD code function cc.idd(c) for _, v in ipairs(cc_idd.CC_IDD) do if type(v[2]) == "table" then for _, v2 in ipairs(v[2]) do if v2 == tostring(c) then return type(v[3]) == "table" and v[3] or { v[3] } end end elseif v[2] == tostring(c) then return type(v[3]) == "table" and v[3] or { v[3] } end end end --- Lookup the CC code(s) for the given country. -- @param country String containing the country name -- @return Table containing the CC code(s) function cc.cc(c) for _, v in ipairs(cc_idd.CC_IDD) do if v[1]:lower():match(c:lower()) then return type(v[2]) == "table" and v[2] or { v[2] } end end end --- Populate given CBI field with CC codes. -- @param field CBI option object -- @return (nothing) function cc.cbifill(o) for i, v in ipairs(cc_idd.CC_IDD) do o:value("_%i" % i, util.pcdata(v[1])) end o.formvalue = function(...) local val = luci.cbi.Value.formvalue(...) if val:sub(1,1) == "_" then val = tonumber((val:gsub("^_", ""))) if val then return type(cc_idd.CC_IDD[val][2]) == "table" and cc_idd.CC_IDD[val][2] or { cc_idd.CC_IDD[val][2] } end end return val end o.cfgvalue = function(...) local val = luci.cbi.Value.cfgvalue(...) if val then val = tools.parse_list(val) for i, v in ipairs(cc_idd.CC_IDD) do if type(v[2]) == "table" then if v[2][1] == val[1] then return "_%i" % i end else if v[2] == val[1] then return "_%i" % i end end end end return val end end --- LuCI Asterisk - Dialzone -- @type module dialzone = luci.util.class() --- Parse a dialzone section -- @param zone Table containing the zone info -- @return Table with parsed information function dialzone.parse(z) if z['.name'] then return { trunks = tools.parse_list(z.uses), name = z['.name'], description = z.description or z['.name'], addprefix = z.addprefix, matches = tools.parse_list(z.match), intlmatches = tools.parse_list(z.international), countrycode = z.countrycode, localzone = z.localzone, localprefix = z.localprefix } end end --- Get a list of known dial zones -- @return Associative table of zones and table of zone names function dialzone.zones() local zones = { } local znames = { } uci:foreach("asterisk", "dialzone", function(z) zones[z['.name']] = dialzone.parse(z) znames[#znames+1] = z['.name'] end) return zones, znames end --- Get a specific dial zone -- @param name Name of the dial zone -- @return Table containing zone information function dialzone.zone(n) local zone uci:foreach("asterisk", "dialzone", function(z) if z['.name'] == n then zone = dialzone.parse(z) end end) return zone end --- Find uci section hash for given zone number -- @param idx Zone number -- @return String containing the uci hash pointing to the section function dialzone.ucisection(i) local hash local index = 1 i = tonumber(i) uci:foreach("asterisk", "dialzone", function(z) if not hash and index == i then hash = z['.name'] end index = index + 1 end) return hash end --- LuCI Asterisk - Voicemailbox -- @type module voicemail = luci.util.class() --- Parse a voicemail section -- @param zone Table containing the mailbox info -- @return Table with parsed information function voicemail.parse(z) if z.number and #z.number > 0 then local v = { id = '%s@%s' %{ z.number, z.context or 'default' }, number = z.number, context = z.context or 'default', name = z.name or z['.name'] or 'OpenWrt', zone = z.zone or 'homeloc', password = z.password or '0000', email = z.email or '', page = z.page or '', dialplans = { } } uci:foreach("asterisk", "dialplanvoice", function(s) if s.dialplan and #s.dialplan > 0 and s.voicebox == v.number then v.dialplans[#v.dialplans+1] = s.dialplan end end) return v end end --- Get a list of known voicemail boxes -- @return Associative table of boxes and table of box numbers function voicemail.boxes() local vboxes = { } local vnames = { } uci:foreach("asterisk", "voicemail", function(z) local v = voicemail.parse(z) if v then local n = '%s@%s' %{ v.number, v.context } vboxes[n] = v vnames[#vnames+1] = n end end) return vboxes, vnames end --- Get a specific voicemailbox -- @param number Number of the voicemailbox -- @return Table containing mailbox information function voicemail.box(n) local box n = n:gsub("@.+$","") uci:foreach("asterisk", "voicemail", function(z) if z.number == tostring(n) then box = voicemail.parse(z) end end) return box end --- Find all voicemailboxes within the given dialplan -- @param plan Dialplan name or table -- @return Associative table containing extensions mapped to mailbox info function voicemail.in_dialplan(p) local plan = type(p) == "string" and p or p.name local boxes = { } uci:foreach("asterisk", "dialplanvoice", function(s) if s.extension and #s.extension > 0 and s.dialplan == plan then local box = voicemail.box(s.voicebox) if box then boxes[s.extension] = box end end end) return boxes end --- Remove voicemailbox and associated extensions from config -- @param box Voicemailbox number or table -- @param ctx UCI context to use (optional) -- @return Boolean indicating success function voicemail.remove(v, ctx) ctx = ctx or uci local box = type(v) == "string" and v or v.number local ok1 = ctx:delete_all("asterisk", "voicemail", {number=box}) local ok2 = ctx:delete_all("asterisk", "dialplanvoice", {voicebox=box}) return ( ok1 or ok2 ) and true or false end --- LuCI Asterisk - MeetMe Conferences -- @type module meetme = luci.util.class() --- Parse a meetme section -- @param room Table containing the room info -- @return Table with parsed information function meetme.parse(r) if r.room and #r.room > 0 then local v = { room = r.room, pin = r.pin or '', adminpin = r.adminpin or '', description = r._description or '', dialplans = { } } uci:foreach("asterisk", "dialplanmeetme", function(s) if s.dialplan and #s.dialplan > 0 and s.room == v.room then v.dialplans[#v.dialplans+1] = s.dialplan end end) return v end end --- Get a list of known meetme rooms -- @return Associative table of rooms and table of room numbers function meetme.rooms() local mrooms = { } local mnames = { } uci:foreach("asterisk", "meetme", function(r) local v = meetme.parse(r) if v then mrooms[v.room] = v mnames[#mnames+1] = v.room end end) return mrooms, mnames end --- Get a specific meetme room -- @param number Number of the room -- @return Table containing room information function meetme.room(n) local room uci:foreach("asterisk", "meetme", function(r) if r.room == tostring(n) then room = meetme.parse(r) end end) return room end --- Find all meetme rooms within the given dialplan -- @param plan Dialplan name or table -- @return Associative table containing extensions mapped to room info function meetme.in_dialplan(p) local plan = type(p) == "string" and p or p.name local rooms = { } uci:foreach("asterisk", "dialplanmeetme", function(s) if s.extension and #s.extension > 0 and s.dialplan == plan then local room = meetme.room(s.room) if room then rooms[s.extension] = room end end end) return rooms end --- Remove meetme room and associated extensions from config -- @param room Voicemailbox number or table -- @param ctx UCI context to use (optional) -- @return Boolean indicating success function meetme.remove(v, ctx) ctx = ctx or uci local room = type(v) == "string" and v or v.number local ok1 = ctx:delete_all("asterisk", "meetme", {room=room}) local ok2 = ctx:delete_all("asterisk", "dialplanmeetme", {room=room}) return ( ok1 or ok2 ) and true or false end --- LuCI Asterisk - Dialplan -- @type module dialplan = luci.util.class() --- Parse a dialplan section -- @param plan Table containing the plan info -- @return Table with parsed information function dialplan.parse(z) if z['.name'] then local plan = { zones = { }, name = z['.name'], description = z.description or z['.name'] } -- dialzones for _, name in ipairs(tools.parse_list(z.include)) do local zone = dialzone.zone(name) if zone then plan.zones[#plan.zones+1] = zone end end -- voicemailboxes plan.voicemailboxes = voicemail.in_dialplan(plan) -- meetme conferences plan.meetmerooms = meetme.in_dialplan(plan) return plan end end --- Get a list of known dial plans -- @return Associative table of plans and table of plan names function dialplan.plans() local plans = { } local pnames = { } uci:foreach("asterisk", "dialplan", function(p) plans[p['.name']] = dialplan.parse(p) pnames[#pnames+1] = p['.name'] end) return plans, pnames end --- Get a specific dial plan -- @param name Name of the dial plan -- @return Table containing plan information function dialplan.plan(n) local plan uci:foreach("asterisk", "dialplan", function(p) if p['.name'] == n then plan = dialplan.parse(p) end end) return plan end