Rework LuCI build system
[project/luci.git] / applications / luci-app-asterisk / luasrc / asterisk.lua
1 --[[
2 LuCI - Lua Configuration Interface
3 Asterisk PBX interface library
4
5 Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 $Id$
14
15 ]]--
16
17 module("luci.asterisk", package.seeall)
18 require("luci.asterisk.cc_idd")
19
20 local _io  = require("io")
21 local uci  = require("luci.model.uci").cursor()
22 local sys  = require("luci.sys")
23 local util = require("luci.util")
24
25 AST_BIN   = "/usr/sbin/asterisk"
26 AST_FLAGS = "-r -x"
27
28
29 --- LuCI Asterisk - Resync uci context
30 function uci_resync()
31         uci = luci.model.uci.cursor()
32 end
33
34 --- LuCI Asterisk io interface
35 -- Handles low level io.
36 -- @type        module
37 io = luci.util.class()
38
39 --- Execute command and return output
40 -- @param command       String containing the command to execute
41 -- @return                      String containing the command output
42 function io.exec(command)
43         local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
44         assert(fh, "Failed to invoke asterisk")
45
46         local buffer = fh:read("*a")
47         fh:close()
48         return buffer
49 end
50
51 --- Execute command and invoke given callback for each readed line
52 -- @param command       String containing the command to execute
53 -- @param callback      Function to call back for each line
54 -- @return                      Always true
55 function io.execl(command, callback)
56         local ln
57         local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
58         assert(fh, "Failed to invoke asterisk")
59
60         repeat
61                 ln = fh:read("*l")
62                 callback(ln)
63         until not ln
64
65         fh:close()
66         return true
67 end
68
69 --- Execute command and return an iterator that returns one line per invokation
70 -- @param command       String containing the command to execute
71 -- @return                      Iterator function
72 function io.execi(command)
73         local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
74         assert(fh, "Failed to invoke asterisk")
75
76         return function()
77                 local ln = fh:read("*l")
78                 if not ln then fh:close() end
79                 return ln
80         end
81 end
82
83
84 --- LuCI Asterisk - core status
85 core = luci.util.class()
86
87 --- Retrive version string.
88 -- @return      String containing the reported asterisk version
89 function core.version(self)
90         local version = io.exec("core show version")
91         return version:gsub(" *\n", "")
92 end
93
94
95 --- LuCI Asterisk - SIP information.
96 -- @type module
97 sip = luci.util.class()
98
99 --- Get a list of known SIP peers
100 -- @return              Table containing each SIP peer
101 function sip.peers(self)
102         local head  = false
103         local peers = { }
104
105         for line in io.execi("sip show peers") do
106                 if not head then
107                         head = true
108                 elseif not line:match(" sip peers ") then
109                         local online, delay, id, uid
110                         local name, host, dyn, nat, acl, port, status =
111                                 line:match("(.-) +(.-) +([D ])   ([N ])   (.)  (%d+) +(.+)")
112
113                         if host == '(Unspecified)' then host = nil end
114                         if port == '0' then port = nil else port = tonumber(port) end
115
116                         dyn = ( dyn == 'D' and true or false )
117                         nat = ( nat == 'N' and true or false )
118                         acl = ( acl ~= ' ' and true or false )
119
120                         online, delay = status:match("(OK) %((%d+) ms%)")
121
122                         if online == 'OK' then
123                                 online = true
124                                 delay  = tonumber(delay)
125                         elseif status ~= 'Unmonitored' then
126                                 online = false
127                                 delay  = 0
128                         else
129                                 online = nil
130                                 delay  = 0
131                         end
132
133                         id, uid = name:match("(.+)/(.+)")
134
135                         if not ( id and uid ) then
136                                 id  = name .. "..."
137                                 uid = nil
138                         end
139
140                         peers[#peers+1] = {
141                                 online  = online,
142                                 delay   = delay,
143                                 name    = id,
144                                 user    = uid,
145                                 dynamic = dyn,
146                                 nat     = nat,
147                                 acl     = acl,
148                                 host    = host,
149                                 port    = port
150                         }
151                 end
152         end
153
154         return peers
155 end
156
157 --- Get informations of given SIP peer
158 -- @param peer  String containing the name of the SIP peer
159 function sip.peer(peer)
160         local info = { }
161         local keys = { }
162
163         for line in io.execi("sip show peer " .. peer) do
164                 if #line > 0 then
165                         local key, val = line:match("(.-) *: +(.*)")
166                         if key and val then
167
168                                 key = key:gsub("^ +",""):gsub(" +$", "")
169                                 val = val:gsub("^ +",""):gsub(" +$", "")
170
171                                 if key == "* Name" then
172                                         key = "Name"
173                                 elseif key == "Addr->IP" then
174                                         info.address, info.port = val:match("(.+) Port (.+)")
175                                         info.port = tonumber(info.port)
176                                 elseif key == "Status" then
177                                         info.online, info.delay = val:match("(OK) %((%d+) ms%)")
178                                         if info.online == 'OK' then
179                                                 info.online = true
180                                                 info.delay  = tonumber(info.delay)
181                                         elseif status ~= 'Unmonitored' then
182                                                 info.online = false
183                                                 info.delay  = 0
184                                         else
185                                                 info.online = nil
186                                                 info.delay  = 0
187                                         end
188                                 end
189
190                                 if val == 'Yes' or val == 'yes' or val == '<Set>' then
191                                         val = true
192                                 elseif val == 'No' or val == 'no' then
193                                         val = false
194                                 elseif val == '<Not set>' or val == '(none)' then
195                                         val = nil
196                                 end
197
198                                 keys[#keys+1] = key
199                                 info[key] = val
200                         end
201                 end
202         end
203
204         return info, keys
205 end
206
207
208 --- LuCI Asterisk - Internal helpers
209 -- @type module
210 tools = luci.util.class()
211
212 --- Convert given value to a list of tokens. Split by white space.
213 -- @param val   String or table value
214 -- @return              Table containing tokens
215 function tools.parse_list(v)
216         local tokens = { }
217
218         v = type(v) == "table" and v or { v }
219         for _, v in ipairs(v) do
220                 if type(v) == "string" then
221                         for v in v:gmatch("(%S+)") do
222                                 tokens[#tokens+1] = v
223                         end
224                 end
225         end
226
227         return tokens
228 end
229
230 --- Convert given list to a collection of hyperlinks
231 -- @param list  Table of tokens
232 -- @param url   String pattern or callback function to construct urls (optional)
233 -- @param sep   String containing the seperator (optional, default is ", ")
234 -- @return              String containing the html fragment
235 function tools.hyperlinks(list, url, sep)
236         local html
237
238         local function mkurl(p, t)
239                 if type(p) == "string" then
240                         return p:format(t)
241                 elseif type(p) == "function" then
242                         return p(t)
243                 else
244                         return '#'
245                 end
246         end
247
248         list = list or { }
249         url  = url  or "%s"
250         sep  = sep  or ", "
251
252         for _, token in ipairs(list) do
253                 html = ( html and html .. sep or '' ) ..
254                         '<a href="%s">%s</a>' %{ mkurl(url, token), token }
255         end
256
257         return html or ''
258 end
259
260
261 --- LuCI Asterisk - International Direct Dialing Prefixes
262 -- @type module
263 idd = luci.util.class()
264
265 --- Lookup the country name for the given IDD code.
266 -- @param country       String containing IDD code
267 -- @return                      String containing the country name
268 function idd.country(c)
269         for _, v in ipairs(cc_idd.CC_IDD) do
270                 if type(v[3]) == "table" then
271                         for _, v2 in ipairs(v[3]) do
272                                 if v2 == tostring(c) then
273                                         return v[1]
274                                 end
275                         end
276                 elseif v[3] == tostring(c) then
277                         return v[1]
278                 end
279         end
280 end
281
282 --- Lookup the country code for the given IDD code.
283 -- @param country       String containing IDD code
284 -- @return                      Table containing the country code(s)
285 function idd.cc(c)
286         for _, v in ipairs(cc_idd.CC_IDD) do
287                 if type(v[3]) == "table" then
288                         for _, v2 in ipairs(v[3]) do
289                                 if v2 == tostring(c) then
290                                         return type(v[2]) == "table"
291                                                 and v[2] or { v[2] }
292                                 end
293                         end
294                 elseif v[3] == tostring(c) then
295                         return type(v[2]) == "table"
296                                 and v[2] or { v[2] }
297                 end
298         end
299 end
300
301 --- Lookup the IDD code(s) for the given country.
302 -- @param idd           String containing the country name
303 -- @return                      Table containing the IDD code(s)
304 function idd.idd(c)
305         for _, v in ipairs(cc_idd.CC_IDD) do
306                 if v[1]:lower():match(c:lower()) then
307                         return type(v[3]) == "table"
308                                 and v[3] or { v[3] }
309                 end
310         end
311 end
312
313 --- Populate given CBI field with IDD codes.
314 -- @param field         CBI option object
315 -- @return                      (nothing)
316 function idd.cbifill(o)
317         for i, v in ipairs(cc_idd.CC_IDD) do
318                 o:value("_%i" % i, util.pcdata(v[1]))
319         end
320
321         o.formvalue = function(...)
322                 local val = luci.cbi.Value.formvalue(...)
323                 if val:sub(1,1) == "_" then
324                         val = tonumber((val:gsub("^_", "")))
325                         if val then
326                                 return type(cc_idd.CC_IDD[val][3]) == "table"
327                                         and cc_idd.CC_IDD[val][3] or { cc_idd.CC_IDD[val][3] }
328                         end
329                 end
330                 return val
331         end
332
333         o.cfgvalue = function(...)
334                 local val = luci.cbi.Value.cfgvalue(...)
335                 if val then
336                         val = tools.parse_list(val)
337                         for i, v in ipairs(cc_idd.CC_IDD) do
338                                 if type(v[3]) == "table" then
339                                         if v[3][1] == val[1] then
340                                                 return "_%i" % i
341                                         end
342                                 else
343                                         if v[3] == val[1] then
344                                                 return "_%i" % i
345                                         end
346                                 end
347                         end
348                 end
349                 return val
350         end
351 end
352
353
354 --- LuCI Asterisk - Country Code Prefixes
355 -- @type module
356 cc = luci.util.class()
357
358 --- Lookup the country name for the given CC code.
359 -- @param country       String containing CC code
360 -- @return                      String containing the country name
361 function cc.country(c)
362         for _, v in ipairs(cc_idd.CC_IDD) do
363                 if type(v[2]) == "table" then
364                         for _, v2 in ipairs(v[2]) do
365                                 if v2 == tostring(c) then
366                                         return v[1]
367                                 end
368                         end
369                 elseif v[2] == tostring(c) then
370                         return v[1]
371                 end
372         end
373 end
374
375 --- Lookup the international dialing code for the given CC code.
376 -- @param cc            String containing CC code
377 -- @return                      String containing IDD code
378 function cc.idd(c)
379         for _, v in ipairs(cc_idd.CC_IDD) do
380                 if type(v[2]) == "table" then
381                         for _, v2 in ipairs(v[2]) do
382                                 if v2 == tostring(c) then
383                                         return type(v[3]) == "table"
384                                                 and v[3] or { v[3] }
385                                 end
386                         end
387                 elseif v[2] == tostring(c) then
388                         return type(v[3]) == "table"
389                                 and v[3] or { v[3] }
390                 end
391         end
392 end
393
394 --- Lookup the CC code(s) for the given country.
395 -- @param country       String containing the country name
396 -- @return                      Table containing the CC code(s)
397 function cc.cc(c)
398         for _, v in ipairs(cc_idd.CC_IDD) do
399                 if v[1]:lower():match(c:lower()) then
400                         return type(v[2]) == "table"
401                                 and v[2] or { v[2] }
402                 end
403         end
404 end
405
406 --- Populate given CBI field with CC codes.
407 -- @param field         CBI option object
408 -- @return                      (nothing)
409 function cc.cbifill(o)
410         for i, v in ipairs(cc_idd.CC_IDD) do
411                 o:value("_%i" % i, util.pcdata(v[1]))
412         end
413
414         o.formvalue = function(...)
415                 local val = luci.cbi.Value.formvalue(...)
416                 if val:sub(1,1) == "_" then
417                         val = tonumber((val:gsub("^_", "")))
418                         if val then
419                                 return type(cc_idd.CC_IDD[val][2]) == "table"
420                                         and cc_idd.CC_IDD[val][2] or { cc_idd.CC_IDD[val][2] }
421                         end
422                 end
423                 return val
424         end
425
426         o.cfgvalue = function(...)
427                 local val = luci.cbi.Value.cfgvalue(...)
428                 if val then
429                         val = tools.parse_list(val)
430                         for i, v in ipairs(cc_idd.CC_IDD) do
431                                 if type(v[2]) == "table" then
432                                         if v[2][1] == val[1] then
433                                                 return "_%i" % i
434                                         end
435                                 else
436                                         if v[2] == val[1] then
437                                                 return "_%i" % i
438                                         end
439                                 end
440                         end
441                 end
442                 return val
443         end
444 end
445
446
447 --- LuCI Asterisk - Dialzone
448 -- @type        module
449 dialzone = luci.util.class()
450
451 --- Parse a dialzone section
452 -- @param zone  Table containing the zone info
453 -- @return              Table with parsed information
454 function dialzone.parse(z)
455         if z['.name'] then
456                 return {
457                         trunks          = tools.parse_list(z.uses),
458                         name            = z['.name'],
459                         description     = z.description or z['.name'],
460                         addprefix       = z.addprefix,
461                         matches         = tools.parse_list(z.match),
462                         intlmatches     = tools.parse_list(z.international),
463                         countrycode     = z.countrycode,
464                         localzone       = z.localzone,
465                         localprefix     = z.localprefix
466                 }
467         end
468 end
469
470 --- Get a list of known dial zones
471 -- @return              Associative table of zones and table of zone names
472 function dialzone.zones()
473         local zones  = { }
474         local znames = { }
475         uci:foreach("asterisk", "dialzone",
476                 function(z)
477                         zones[z['.name']] = dialzone.parse(z)
478                         znames[#znames+1] = z['.name']
479                 end)
480         return zones, znames
481 end
482
483 --- Get a specific dial zone
484 -- @param name  Name of the dial zone
485 -- @return              Table containing zone information
486 function dialzone.zone(n)
487         local zone
488         uci:foreach("asterisk", "dialzone",
489                 function(z)
490                         if z['.name'] == n then
491                                 zone = dialzone.parse(z)
492                         end
493                 end)
494         return zone
495 end
496
497 --- Find uci section hash for given zone number
498 -- @param idx   Zone number
499 -- @return              String containing the uci hash pointing to the section
500 function dialzone.ucisection(i)
501         local hash
502         local index = 1
503         i = tonumber(i)
504         uci:foreach("asterisk", "dialzone",
505                 function(z)
506                         if not hash and index == i then
507                                 hash = z['.name']
508                         end
509                         index = index + 1
510                 end)
511         return hash
512 end
513
514
515 --- LuCI Asterisk - Voicemailbox
516 -- @type        module
517 voicemail = luci.util.class()
518
519 --- Parse a voicemail section
520 -- @param zone  Table containing the mailbox info
521 -- @return              Table with parsed information
522 function voicemail.parse(z)
523         if z.number and #z.number > 0 then
524                 local v = {
525                         id                      = '%s@%s' %{ z.number, z.context or 'default' },
526                         number          = z.number,
527                         context         = z.context     or 'default',
528                         name            = z.name                or z['.name'] or 'OpenWrt',
529                         zone            = z.zone                or 'homeloc',
530                         password        = z.password    or '0000',
531                         email           = z.email               or '',
532                         page            = z.page                or '',
533                         dialplans       = { }
534                 }
535
536                 uci:foreach("asterisk", "dialplanvoice",
537                         function(s)
538                                 if s.dialplan and #s.dialplan > 0 and
539                                    s.voicebox == v.number
540                                 then
541                                         v.dialplans[#v.dialplans+1] = s.dialplan
542                                 end
543                         end)
544
545                 return v
546         end
547 end
548
549 --- Get a list of known voicemail boxes
550 -- @return              Associative table of boxes and table of box numbers
551 function voicemail.boxes()
552         local vboxes = { }
553         local vnames = { }
554         uci:foreach("asterisk", "voicemail",
555                 function(z)
556                         local v = voicemail.parse(z)
557                         if v then
558                                 local n = '%s@%s' %{ v.number, v.context }
559                                 vboxes[n]  = v
560                                 vnames[#vnames+1] = n
561                         end
562                 end)
563         return vboxes, vnames
564 end
565
566 --- Get a specific voicemailbox
567 -- @param number        Number of the voicemailbox
568 -- @return                      Table containing mailbox information
569 function voicemail.box(n)
570         local box
571         n = n:gsub("@.+$","")
572         uci:foreach("asterisk", "voicemail",
573                 function(z)
574                         if z.number == tostring(n) then
575                                 box = voicemail.parse(z)
576                         end
577                 end)
578         return box
579 end
580
581 --- Find all voicemailboxes within the given dialplan
582 -- @param plan  Dialplan name or table
583 -- @return              Associative table containing extensions mapped to mailbox info
584 function voicemail.in_dialplan(p)
585         local plan  = type(p) == "string" and p or p.name
586         local boxes = { }
587         uci:foreach("asterisk", "dialplanvoice",
588                 function(s)
589                         if s.extension and #s.extension > 0 and s.dialplan == plan then
590                                 local box = voicemail.box(s.voicebox)
591                                 if box then
592                                         boxes[s.extension] = box
593                                 end
594                         end
595                 end)
596         return boxes
597 end
598
599 --- Remove voicemailbox and associated extensions from config
600 -- @param box   Voicemailbox number or table
601 -- @param ctx   UCI context to use (optional)
602 -- @return              Boolean indicating success
603 function voicemail.remove(v, ctx)
604         ctx = ctx or uci
605         local box = type(v) == "string" and v or v.number
606         local ok1 = ctx:delete_all("asterisk", "voicemail", {number=box})
607         local ok2 = ctx:delete_all("asterisk", "dialplanvoice", {voicebox=box})
608         return ( ok1 or ok2 ) and true or false
609 end
610
611
612 --- LuCI Asterisk - MeetMe Conferences
613 -- @type        module
614 meetme = luci.util.class()
615
616 --- Parse a meetme section
617 -- @param room  Table containing the room info
618 -- @return              Table with parsed information
619 function meetme.parse(r)
620         if r.room and #r.room > 0 then
621                 local v = {
622                         room            = r.room,
623                         pin                     = r.pin                         or '',
624                         adminpin        = r.adminpin            or '',
625                         description = r._description    or '',
626                         dialplans       = { }
627                 }
628
629                 uci:foreach("asterisk", "dialplanmeetme",
630                         function(s)
631                                 if s.dialplan and #s.dialplan > 0 and s.room == v.room then
632                                         v.dialplans[#v.dialplans+1] = s.dialplan
633                                 end
634                         end)
635
636                 return v
637         end
638 end
639
640 --- Get a list of known meetme rooms
641 -- @return              Associative table of rooms and table of room numbers
642 function meetme.rooms()
643         local mrooms = { }
644         local mnames = { }
645         uci:foreach("asterisk", "meetme",
646                 function(r)
647                         local v = meetme.parse(r)
648                         if v then
649                                 mrooms[v.room] = v
650                                 mnames[#mnames+1] = v.room
651                         end
652                 end)
653         return mrooms, mnames
654 end
655
656 --- Get a specific meetme room
657 -- @param number        Number of the room
658 -- @return                      Table containing room information
659 function meetme.room(n)
660         local room
661         uci:foreach("asterisk", "meetme",
662                 function(r)
663                         if r.room == tostring(n) then
664                                 room = meetme.parse(r)
665                         end
666                 end)
667         return room
668 end
669
670 --- Find all meetme rooms within the given dialplan
671 -- @param plan  Dialplan name or table
672 -- @return              Associative table containing extensions mapped to room info
673 function meetme.in_dialplan(p)
674         local plan  = type(p) == "string" and p or p.name
675         local rooms = { }
676         uci:foreach("asterisk", "dialplanmeetme",
677                 function(s)
678                         if s.extension and #s.extension > 0 and s.dialplan == plan then
679                                 local room = meetme.room(s.room)
680                                 if room then
681                                         rooms[s.extension] = room
682                                 end
683                         end
684                 end)
685         return rooms
686 end
687
688 --- Remove meetme room and associated extensions from config
689 -- @param room  Voicemailbox number or table
690 -- @param ctx   UCI context to use (optional)
691 -- @return              Boolean indicating success
692 function meetme.remove(v, ctx)
693         ctx = ctx or uci
694         local room = type(v) == "string" and v or v.number
695         local ok1  = ctx:delete_all("asterisk", "meetme", {room=room})
696         local ok2  = ctx:delete_all("asterisk", "dialplanmeetme", {room=room})
697         return ( ok1 or ok2 ) and true or false
698 end
699
700
701 --- LuCI Asterisk - Dialplan
702 -- @type        module
703 dialplan = luci.util.class()
704
705 --- Parse a dialplan section
706 -- @param plan  Table containing the plan info
707 -- @return              Table with parsed information
708 function dialplan.parse(z)
709         if z['.name'] then
710                 local plan = {
711                         zones           = { },
712                         name            = z['.name'],
713                         description     = z.description or z['.name']
714                 }
715
716                 -- dialzones
717                 for _, name in ipairs(tools.parse_list(z.include)) do
718                         local zone = dialzone.zone(name)
719                         if zone then
720                                 plan.zones[#plan.zones+1] = zone
721                         end
722                 end
723
724                 -- voicemailboxes
725                 plan.voicemailboxes = voicemail.in_dialplan(plan)
726
727                 -- meetme conferences
728                 plan.meetmerooms = meetme.in_dialplan(plan)
729
730                 return plan
731         end
732 end
733
734 --- Get a list of known dial plans
735 -- @return              Associative table of plans and table of plan names
736 function dialplan.plans()
737         local plans  = { }
738         local pnames = { }
739         uci:foreach("asterisk", "dialplan",
740                 function(p)
741                         plans[p['.name']] = dialplan.parse(p)
742                         pnames[#pnames+1] = p['.name']
743                 end)
744         return plans, pnames
745 end
746
747 --- Get a specific dial plan
748 -- @param name  Name of the dial plan
749 -- @return              Table containing plan information
750 function dialplan.plan(n)
751         local plan
752         uci:foreach("asterisk", "dialplan",
753                 function(p)
754                         if p['.name'] == n then
755                                 plan = dialplan.parse(p)
756                         end
757                 end)
758         return plan
759 end