treewide: Fix typos in comments
[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 read 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 invocation
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 --- Retrieve 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 information 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 separator (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