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