generator: base dialplan, globals and sp-local-numbers with hints
Static sp-sub-* helpers and sp-from-ext from 03 §6.2 (with __SP_ variable names), globals (default and E911 caller ID, per-extension class and admin flags), and every user in sp-local-numbers with a PJSIP/<n>[&PJSIP/<n>-web] hint. Voicemail stays off (ARG3=0) until M4. Contexts that later milestones fill (emergency, feature codes, outbound routes) get a placeholder so Asterisk doesn't warn about missing includes. Empty ccss.conf for the next image build. Task: M2-T4
This commit is contained in:
1 parent
06ac99b9f4
commit
a4da81cf8f
16 files changed
+673
-1
No files matched your search
@@ -1 +1,4 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
SP_DEFAULT_CID=
|
||||
SP_DEFAULT_E911_CID=
|
||||
SP_REC_ANNOUNCE=
|
||||
@@ -1 +1,113 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
; ─── Normalize a number. ARG1 = raw. Returns normalized in GOSUB_RETVAL. (matches 00 §8)
|
||||
[sp-sub-normalize]
|
||||
exten => s,1,Set(N=${FILTER(0123456789+,${ARG1})})
|
||||
same => n,ExecIf($["${N:0:2}" = "+1"]?Set(N=${N:1}))
|
||||
same => n,ExecIf($["${N:0:1}" = "+"]?Set(N=011${N:1}))
|
||||
same => n,ExecIf($[${LEN(${N})} = 10]?Set(N=1${N}))
|
||||
same => n,Return(${N})
|
||||
|
||||
; ─── Ring every registered device of a user. ARG1 = ext, ARG2 = seconds.
|
||||
; Sets DIALSTATUS. Rings desk/softphone contacts (PJSIP/101) and browser (PJSIP/101-web).
|
||||
[sp-sub-dial-user]
|
||||
exten => s,1,Set(DS=${PJSIP_DIAL_CONTACTS(${ARG1})})
|
||||
same => n,Set(WS=${PJSIP_DIAL_CONTACTS(${ARG1}-web)})
|
||||
same => n,ExecIf($["${DS}" != "" & "${WS}" != ""]?Set(DS=${DS}&${WS}))
|
||||
same => n,ExecIf($["${DS}" = ""]?Set(DS=${WS}))
|
||||
same => n,GotoIf($["${DS}" = ""]?unavail)
|
||||
same => n,Dial(${DS},${ARG2},tx)
|
||||
same => n,Return()
|
||||
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
||||
same => n,Return()
|
||||
|
||||
; ─── Call a user with all their personal rules.
|
||||
; ARG1 = ext, ARG2 = ring seconds, ARG3 = voicemail enabled (1/0), ARG4 = record mode
|
||||
[sp-sub-user]
|
||||
exten => s,1,Set(__SP_TARGET=${ARG1})
|
||||
same => n,ExecIf($["${SP_DIRECTION}" = "inbound" & ("${ARG4}" = "inbound" | "${ARG4}" = "all")]?Gosub(sp-sub-record,s,1))
|
||||
same => n,GotoIf($["${DB(SP/DND/${ARG1})}" = "1"]?busy)
|
||||
same => n,Set(FWD=${DB(SP/CFU/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${ARG1},${ARG2}))
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?done)
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
|
||||
same => n(noanswer),Set(FWD=${DB(SP/CFNA/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmu)
|
||||
same => n,Return()
|
||||
same => n(vmu),VoiceMail(${ARG1}@default,u)
|
||||
same => n,Return()
|
||||
same => n(busy),Set(FWD=${DB(SP/CFB/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmb)
|
||||
same => n,Busy(5)
|
||||
same => n,Return()
|
||||
same => n(vmb),VoiceMail(${ARG1}@default,b)
|
||||
same => n,Return()
|
||||
same => n(forward),Gosub(sp-sub-forward,s,1(${FWD},${ARG1}))
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Forward a call. ARG1 = target number, ARG2 = ext whose forward rule this is.
|
||||
; Stops after 3 hops so 101→102→101 can't loop forever.
|
||||
[sp-sub-forward]
|
||||
exten => s,1,Set(__SP_FWDHOPS=$[${IF($["${SP_FWDHOPS}" = ""]?0:${SP_FWDHOPS})} + 1])
|
||||
same => n,GotoIf($[${SP_FWDHOPS} > 3]?stop)
|
||||
same => n,GotoIf($[${DIALPLAN_EXISTS(sp-local-numbers,${ARG1},1)}]?internal)
|
||||
same => n,Set(SP_CLASS=${GLOBAL(SP_CLASS_${ARG2})})
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${ARG1},1)
|
||||
same => n(internal),Goto(sp-local-numbers,${ARG1},1)
|
||||
same => n(stop),Return()
|
||||
|
||||
; ─── Start recording this call once. File: YYYY/MM/DD/<uniqueid>.wav under the monitor dir.
|
||||
[sp-sub-record]
|
||||
exten => s,1,GotoIf($["${SP_REC}" != ""]?done)
|
||||
same => n,Set(__SP_REC=${STRFTIME(${EPOCH},,%Y/%m/%d)}/${UNIQUEID}.wav)
|
||||
same => n,MixMonitor(${SP_REC},b)
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Auto-answer headers for paging/intercom (Yealink, Grandstream, Polycom, Snom)
|
||||
[sp-sub-autoanswer]
|
||||
exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=<sip:${SP_EXT}>\;answer-after=0)
|
||||
same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer)
|
||||
same => n,Return()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-ext-forward]
|
||||
exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external destination
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${EXTEN},1)
|
||||
|
||||
[sp-rg-member] ; one member of a ring group (Local channel)
|
||||
exten => _X.,1,GotoIf($["${DB(SP/DND/${EXTEN})}" = "1"]?skip)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n(skip),Hangup()
|
||||
|
||||
[sp-queue-member] ; one queue agent (Local channel)
|
||||
exten => _X.,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-from-ext]
|
||||
include => sp-emergency-dial
|
||||
include => sp-featurecodes
|
||||
include => sp-local-numbers
|
||||
include => sp-outbound-routes
|
||||
|
||||
[sp-local-numbers]
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-emergency-dial] ; M3-T4 (03 §6.14)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-outbound-routes] ; M3-T3 (03 §6.12)
|
||||
exten => pending,1,Hangup()
|
||||
+6
@@ -1 +1,7 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
SP_DEFAULT_CID=
|
||||
SP_DEFAULT_E911_CID=
|
||||
SP_REC_ANNOUNCE=
|
||||
SP_CLASS_103=2
|
||||
SP_CLASS_104=2
|
||||
SP_CLASS_105=2
|
||||
@@ -1 +1,122 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
; ─── Normalize a number. ARG1 = raw. Returns normalized in GOSUB_RETVAL. (matches 00 §8)
|
||||
[sp-sub-normalize]
|
||||
exten => s,1,Set(N=${FILTER(0123456789+,${ARG1})})
|
||||
same => n,ExecIf($["${N:0:2}" = "+1"]?Set(N=${N:1}))
|
||||
same => n,ExecIf($["${N:0:1}" = "+"]?Set(N=011${N:1}))
|
||||
same => n,ExecIf($[${LEN(${N})} = 10]?Set(N=1${N}))
|
||||
same => n,Return(${N})
|
||||
|
||||
; ─── Ring every registered device of a user. ARG1 = ext, ARG2 = seconds.
|
||||
; Sets DIALSTATUS. Rings desk/softphone contacts (PJSIP/101) and browser (PJSIP/101-web).
|
||||
[sp-sub-dial-user]
|
||||
exten => s,1,Set(DS=${PJSIP_DIAL_CONTACTS(${ARG1})})
|
||||
same => n,Set(WS=${PJSIP_DIAL_CONTACTS(${ARG1}-web)})
|
||||
same => n,ExecIf($["${DS}" != "" & "${WS}" != ""]?Set(DS=${DS}&${WS}))
|
||||
same => n,ExecIf($["${DS}" = ""]?Set(DS=${WS}))
|
||||
same => n,GotoIf($["${DS}" = ""]?unavail)
|
||||
same => n,Dial(${DS},${ARG2},tx)
|
||||
same => n,Return()
|
||||
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
||||
same => n,Return()
|
||||
|
||||
; ─── Call a user with all their personal rules.
|
||||
; ARG1 = ext, ARG2 = ring seconds, ARG3 = voicemail enabled (1/0), ARG4 = record mode
|
||||
[sp-sub-user]
|
||||
exten => s,1,Set(__SP_TARGET=${ARG1})
|
||||
same => n,ExecIf($["${SP_DIRECTION}" = "inbound" & ("${ARG4}" = "inbound" | "${ARG4}" = "all")]?Gosub(sp-sub-record,s,1))
|
||||
same => n,GotoIf($["${DB(SP/DND/${ARG1})}" = "1"]?busy)
|
||||
same => n,Set(FWD=${DB(SP/CFU/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${ARG1},${ARG2}))
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?done)
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
|
||||
same => n(noanswer),Set(FWD=${DB(SP/CFNA/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmu)
|
||||
same => n,Return()
|
||||
same => n(vmu),VoiceMail(${ARG1}@default,u)
|
||||
same => n,Return()
|
||||
same => n(busy),Set(FWD=${DB(SP/CFB/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmb)
|
||||
same => n,Busy(5)
|
||||
same => n,Return()
|
||||
same => n(vmb),VoiceMail(${ARG1}@default,b)
|
||||
same => n,Return()
|
||||
same => n(forward),Gosub(sp-sub-forward,s,1(${FWD},${ARG1}))
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Forward a call. ARG1 = target number, ARG2 = ext whose forward rule this is.
|
||||
; Stops after 3 hops so 101→102→101 can't loop forever.
|
||||
[sp-sub-forward]
|
||||
exten => s,1,Set(__SP_FWDHOPS=$[${IF($["${SP_FWDHOPS}" = ""]?0:${SP_FWDHOPS})} + 1])
|
||||
same => n,GotoIf($[${SP_FWDHOPS} > 3]?stop)
|
||||
same => n,GotoIf($[${DIALPLAN_EXISTS(sp-local-numbers,${ARG1},1)}]?internal)
|
||||
same => n,Set(SP_CLASS=${GLOBAL(SP_CLASS_${ARG2})})
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${ARG1},1)
|
||||
same => n(internal),Goto(sp-local-numbers,${ARG1},1)
|
||||
same => n(stop),Return()
|
||||
|
||||
; ─── Start recording this call once. File: YYYY/MM/DD/<uniqueid>.wav under the monitor dir.
|
||||
[sp-sub-record]
|
||||
exten => s,1,GotoIf($["${SP_REC}" != ""]?done)
|
||||
same => n,Set(__SP_REC=${STRFTIME(${EPOCH},,%Y/%m/%d)}/${UNIQUEID}.wav)
|
||||
same => n,MixMonitor(${SP_REC},b)
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Auto-answer headers for paging/intercom (Yealink, Grandstream, Polycom, Snom)
|
||||
[sp-sub-autoanswer]
|
||||
exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=<sip:${SP_EXT}>\;answer-after=0)
|
||||
same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer)
|
||||
same => n,Return()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-ext-forward]
|
||||
exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external destination
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${EXTEN},1)
|
||||
|
||||
[sp-rg-member] ; one member of a ring group (Local channel)
|
||||
exten => _X.,1,GotoIf($["${DB(SP/DND/${EXTEN})}" = "1"]?skip)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n(skip),Hangup()
|
||||
|
||||
[sp-queue-member] ; one queue agent (Local channel)
|
||||
exten => _X.,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-from-ext]
|
||||
include => sp-emergency-dial
|
||||
include => sp-featurecodes
|
||||
include => sp-local-numbers
|
||||
include => sp-outbound-routes
|
||||
|
||||
[sp-local-numbers]
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 103,1,Gosub(sp-sub-user,s,1(103,25,0,never))
|
||||
same => n,Hangup()
|
||||
exten => 103,hint,PJSIP/103&PJSIP/103-web
|
||||
exten => 104,1,Gosub(sp-sub-user,s,1(104,25,0,never))
|
||||
same => n,Hangup()
|
||||
exten => 104,hint,PJSIP/104&PJSIP/104-web
|
||||
exten => 105,1,Gosub(sp-sub-user,s,1(105,25,0,never))
|
||||
same => n,Hangup()
|
||||
exten => 105,hint,PJSIP/105&PJSIP/105-web
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-emergency-dial] ; M3-T4 (03 §6.14)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-outbound-routes] ; M3-T3 (03 §6.12)
|
||||
exten => pending,1,Hangup()
|
||||
+5
@@ -1 +1,6 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
SP_DEFAULT_CID=13125551234
|
||||
SP_DEFAULT_E911_CID=13125551234
|
||||
SP_REC_ANNOUNCE=
|
||||
SP_CLASS_101=2
|
||||
SP_TCADMIN_101=1
|
||||
@@ -1 +1,116 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
; ─── Normalize a number. ARG1 = raw. Returns normalized in GOSUB_RETVAL. (matches 00 §8)
|
||||
[sp-sub-normalize]
|
||||
exten => s,1,Set(N=${FILTER(0123456789+,${ARG1})})
|
||||
same => n,ExecIf($["${N:0:2}" = "+1"]?Set(N=${N:1}))
|
||||
same => n,ExecIf($["${N:0:1}" = "+"]?Set(N=011${N:1}))
|
||||
same => n,ExecIf($[${LEN(${N})} = 10]?Set(N=1${N}))
|
||||
same => n,Return(${N})
|
||||
|
||||
; ─── Ring every registered device of a user. ARG1 = ext, ARG2 = seconds.
|
||||
; Sets DIALSTATUS. Rings desk/softphone contacts (PJSIP/101) and browser (PJSIP/101-web).
|
||||
[sp-sub-dial-user]
|
||||
exten => s,1,Set(DS=${PJSIP_DIAL_CONTACTS(${ARG1})})
|
||||
same => n,Set(WS=${PJSIP_DIAL_CONTACTS(${ARG1}-web)})
|
||||
same => n,ExecIf($["${DS}" != "" & "${WS}" != ""]?Set(DS=${DS}&${WS}))
|
||||
same => n,ExecIf($["${DS}" = ""]?Set(DS=${WS}))
|
||||
same => n,GotoIf($["${DS}" = ""]?unavail)
|
||||
same => n,Dial(${DS},${ARG2},tx)
|
||||
same => n,Return()
|
||||
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
||||
same => n,Return()
|
||||
|
||||
; ─── Call a user with all their personal rules.
|
||||
; ARG1 = ext, ARG2 = ring seconds, ARG3 = voicemail enabled (1/0), ARG4 = record mode
|
||||
[sp-sub-user]
|
||||
exten => s,1,Set(__SP_TARGET=${ARG1})
|
||||
same => n,ExecIf($["${SP_DIRECTION}" = "inbound" & ("${ARG4}" = "inbound" | "${ARG4}" = "all")]?Gosub(sp-sub-record,s,1))
|
||||
same => n,GotoIf($["${DB(SP/DND/${ARG1})}" = "1"]?busy)
|
||||
same => n,Set(FWD=${DB(SP/CFU/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${ARG1},${ARG2}))
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?done)
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
|
||||
same => n(noanswer),Set(FWD=${DB(SP/CFNA/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmu)
|
||||
same => n,Return()
|
||||
same => n(vmu),VoiceMail(${ARG1}@default,u)
|
||||
same => n,Return()
|
||||
same => n(busy),Set(FWD=${DB(SP/CFB/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmb)
|
||||
same => n,Busy(5)
|
||||
same => n,Return()
|
||||
same => n(vmb),VoiceMail(${ARG1}@default,b)
|
||||
same => n,Return()
|
||||
same => n(forward),Gosub(sp-sub-forward,s,1(${FWD},${ARG1}))
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Forward a call. ARG1 = target number, ARG2 = ext whose forward rule this is.
|
||||
; Stops after 3 hops so 101→102→101 can't loop forever.
|
||||
[sp-sub-forward]
|
||||
exten => s,1,Set(__SP_FWDHOPS=$[${IF($["${SP_FWDHOPS}" = ""]?0:${SP_FWDHOPS})} + 1])
|
||||
same => n,GotoIf($[${SP_FWDHOPS} > 3]?stop)
|
||||
same => n,GotoIf($[${DIALPLAN_EXISTS(sp-local-numbers,${ARG1},1)}]?internal)
|
||||
same => n,Set(SP_CLASS=${GLOBAL(SP_CLASS_${ARG2})})
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${ARG1},1)
|
||||
same => n(internal),Goto(sp-local-numbers,${ARG1},1)
|
||||
same => n(stop),Return()
|
||||
|
||||
; ─── Start recording this call once. File: YYYY/MM/DD/<uniqueid>.wav under the monitor dir.
|
||||
[sp-sub-record]
|
||||
exten => s,1,GotoIf($["${SP_REC}" != ""]?done)
|
||||
same => n,Set(__SP_REC=${STRFTIME(${EPOCH},,%Y/%m/%d)}/${UNIQUEID}.wav)
|
||||
same => n,MixMonitor(${SP_REC},b)
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Auto-answer headers for paging/intercom (Yealink, Grandstream, Polycom, Snom)
|
||||
[sp-sub-autoanswer]
|
||||
exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=<sip:${SP_EXT}>\;answer-after=0)
|
||||
same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer)
|
||||
same => n,Return()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-ext-forward]
|
||||
exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external destination
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${EXTEN},1)
|
||||
|
||||
[sp-rg-member] ; one member of a ring group (Local channel)
|
||||
exten => _X.,1,GotoIf($["${DB(SP/DND/${EXTEN})}" = "1"]?skip)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n(skip),Hangup()
|
||||
|
||||
[sp-queue-member] ; one queue agent (Local channel)
|
||||
exten => _X.,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-from-ext]
|
||||
include => sp-emergency-dial
|
||||
include => sp-featurecodes
|
||||
include => sp-local-numbers
|
||||
include => sp-outbound-routes
|
||||
|
||||
[sp-local-numbers]
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 101,1,Gosub(sp-sub-user,s,1(101,25,0,never))
|
||||
same => n,Hangup()
|
||||
exten => 101,hint,PJSIP/101&PJSIP/101-web
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-emergency-dial] ; M3-T4 (03 §6.14)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-outbound-routes] ; M3-T3 (03 §6.12)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -2,7 +2,8 @@
|
||||
"settings": {
|
||||
"network.publicIp": "203.0.113.10",
|
||||
"network.lanIp": "192.168.1.20",
|
||||
"network.stunServer": "stun.example.org:3478"
|
||||
"network.stunServer": "stun.example.org:3478",
|
||||
"routing.defaultCallerIdDidId": "11111111-1111-4111-8111-111111111111"
|
||||
},
|
||||
"extensions": [
|
||||
{
|
||||
@@ -14,5 +15,11 @@
|
||||
"emergencyCallerId": "13125551234",
|
||||
"isAdmin": true
|
||||
}
|
||||
],
|
||||
"dids": [
|
||||
{
|
||||
"id": "11111111-1111-4111-8111-111111111111",
|
||||
"number": "13125551234"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -1 +1,6 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
SP_DEFAULT_CID=
|
||||
SP_DEFAULT_E911_CID=
|
||||
SP_REC_ANNOUNCE=
|
||||
SP_CLASS_99=0
|
||||
SP_CLASS_102=1
|
||||
+118
@@ -1 +1,119 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
; ─── Normalize a number. ARG1 = raw. Returns normalized in GOSUB_RETVAL. (matches 00 §8)
|
||||
[sp-sub-normalize]
|
||||
exten => s,1,Set(N=${FILTER(0123456789+,${ARG1})})
|
||||
same => n,ExecIf($["${N:0:2}" = "+1"]?Set(N=${N:1}))
|
||||
same => n,ExecIf($["${N:0:1}" = "+"]?Set(N=011${N:1}))
|
||||
same => n,ExecIf($[${LEN(${N})} = 10]?Set(N=1${N}))
|
||||
same => n,Return(${N})
|
||||
|
||||
; ─── Ring every registered device of a user. ARG1 = ext, ARG2 = seconds.
|
||||
; Sets DIALSTATUS. Rings desk/softphone contacts (PJSIP/101) and browser (PJSIP/101-web).
|
||||
[sp-sub-dial-user]
|
||||
exten => s,1,Set(DS=${PJSIP_DIAL_CONTACTS(${ARG1})})
|
||||
same => n,Set(WS=${PJSIP_DIAL_CONTACTS(${ARG1}-web)})
|
||||
same => n,ExecIf($["${DS}" != "" & "${WS}" != ""]?Set(DS=${DS}&${WS}))
|
||||
same => n,ExecIf($["${DS}" = ""]?Set(DS=${WS}))
|
||||
same => n,GotoIf($["${DS}" = ""]?unavail)
|
||||
same => n,Dial(${DS},${ARG2},tx)
|
||||
same => n,Return()
|
||||
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
||||
same => n,Return()
|
||||
|
||||
; ─── Call a user with all their personal rules.
|
||||
; ARG1 = ext, ARG2 = ring seconds, ARG3 = voicemail enabled (1/0), ARG4 = record mode
|
||||
[sp-sub-user]
|
||||
exten => s,1,Set(__SP_TARGET=${ARG1})
|
||||
same => n,ExecIf($["${SP_DIRECTION}" = "inbound" & ("${ARG4}" = "inbound" | "${ARG4}" = "all")]?Gosub(sp-sub-record,s,1))
|
||||
same => n,GotoIf($["${DB(SP/DND/${ARG1})}" = "1"]?busy)
|
||||
same => n,Set(FWD=${DB(SP/CFU/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${ARG1},${ARG2}))
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?done)
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
|
||||
same => n(noanswer),Set(FWD=${DB(SP/CFNA/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmu)
|
||||
same => n,Return()
|
||||
same => n(vmu),VoiceMail(${ARG1}@default,u)
|
||||
same => n,Return()
|
||||
same => n(busy),Set(FWD=${DB(SP/CFB/${ARG1})})
|
||||
same => n,GotoIf($["${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["${ARG3}" = "1"]?vmb)
|
||||
same => n,Busy(5)
|
||||
same => n,Return()
|
||||
same => n(vmb),VoiceMail(${ARG1}@default,b)
|
||||
same => n,Return()
|
||||
same => n(forward),Gosub(sp-sub-forward,s,1(${FWD},${ARG1}))
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Forward a call. ARG1 = target number, ARG2 = ext whose forward rule this is.
|
||||
; Stops after 3 hops so 101→102→101 can't loop forever.
|
||||
[sp-sub-forward]
|
||||
exten => s,1,Set(__SP_FWDHOPS=$[${IF($["${SP_FWDHOPS}" = ""]?0:${SP_FWDHOPS})} + 1])
|
||||
same => n,GotoIf($[${SP_FWDHOPS} > 3]?stop)
|
||||
same => n,GotoIf($[${DIALPLAN_EXISTS(sp-local-numbers,${ARG1},1)}]?internal)
|
||||
same => n,Set(SP_CLASS=${GLOBAL(SP_CLASS_${ARG2})})
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${ARG1},1)
|
||||
same => n(internal),Goto(sp-local-numbers,${ARG1},1)
|
||||
same => n(stop),Return()
|
||||
|
||||
; ─── Start recording this call once. File: YYYY/MM/DD/<uniqueid>.wav under the monitor dir.
|
||||
[sp-sub-record]
|
||||
exten => s,1,GotoIf($["${SP_REC}" != ""]?done)
|
||||
same => n,Set(__SP_REC=${STRFTIME(${EPOCH},,%Y/%m/%d)}/${UNIQUEID}.wav)
|
||||
same => n,MixMonitor(${SP_REC},b)
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Auto-answer headers for paging/intercom (Yealink, Grandstream, Polycom, Snom)
|
||||
[sp-sub-autoanswer]
|
||||
exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=<sip:${SP_EXT}>\;answer-after=0)
|
||||
same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer)
|
||||
same => n,Return()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-ext-forward]
|
||||
exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external destination
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,${EXTEN},1)
|
||||
|
||||
[sp-rg-member] ; one member of a ring group (Local channel)
|
||||
exten => _X.,1,GotoIf($["${DB(SP/DND/${EXTEN})}" = "1"]?skip)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n(skip),Hangup()
|
||||
|
||||
[sp-queue-member] ; one queue agent (Local channel)
|
||||
exten => _X.,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-from-ext]
|
||||
include => sp-emergency-dial
|
||||
include => sp-featurecodes
|
||||
include => sp-local-numbers
|
||||
include => sp-outbound-routes
|
||||
|
||||
[sp-local-numbers]
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 99,1,Gosub(sp-sub-user,s,1(99,25,0,never))
|
||||
same => n,Hangup()
|
||||
exten => 99,hint,PJSIP/99
|
||||
exten => 102,1,Gosub(sp-sub-user,s,1(102,25,0,all))
|
||||
same => n,Hangup()
|
||||
exten => 102,hint,PJSIP/102
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-emergency-dial] ; M3-T4 (03 §6.14)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-outbound-routes] ; M3-T3 (03 §6.12)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// sp-local-numbers (03 §6.6) and the globals file (03 §6.3).
|
||||
import { byNumber, type Snapshot } from '../load.ts';
|
||||
import * as s from '../sanitize.ts';
|
||||
|
||||
const CLASS_NUMBER = { internal: 0, local: 1, national: 2, international: 3 } as const;
|
||||
|
||||
// TODO(M4): voicemail on. Until then every user gets ARG3 = 0 (09 M2-T4).
|
||||
const VOICEMAIL_READY = false;
|
||||
|
||||
export function renderLocalNumbers(snap: Snapshot): string {
|
||||
const lines = [
|
||||
'[sp-local-numbers]',
|
||||
'include => sp-parkedcalls',
|
||||
'',
|
||||
'; users (sorted by number)',
|
||||
];
|
||||
for (const e of [...snap.extensions].sort(byNumber)) {
|
||||
const num = s.digits(e.number);
|
||||
const ring = s.digits(String(e.ringSeconds));
|
||||
const vm = VOICEMAIL_READY && e.voicemailEnabled ? 1 : 0;
|
||||
lines.push(
|
||||
`exten => ${num},1,Gosub(sp-sub-user,s,1(${num},${ring},${vm},${e.recordMode}))`,
|
||||
' same => n,Hangup()',
|
||||
`exten => ${num},hint,PJSIP/${num}${e.webrtcEnabled ? `&PJSIP/${num}-web` : ''}`,
|
||||
);
|
||||
}
|
||||
// M6/M8: ring groups, queues, IVRs, conferences and paging groups add one line each here.
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
export function renderGlobals(snap: Snapshot): string {
|
||||
const st = snap.settings;
|
||||
const didNumber = (id: string | null) => {
|
||||
const d = id ? snap.dids.find((x) => x.id === id) : undefined;
|
||||
return d ? s.digits(d.number) : '';
|
||||
};
|
||||
const defaultCid = didNumber(st['routing.defaultCallerIdDidId']);
|
||||
const lines = [
|
||||
`SP_DEFAULT_CID=${defaultCid}`,
|
||||
`SP_DEFAULT_E911_CID=${didNumber(st['emergency.defaultCallerIdDidId']) || defaultCid}`,
|
||||
'SP_REC_ANNOUNCE=', // M7: recordings.announcementSoundId → sound name
|
||||
];
|
||||
const exts = [...snap.extensions].sort(byNumber);
|
||||
for (const e of exts)
|
||||
lines.push(`SP_CLASS_${s.digits(e.number)}=${CLASS_NUMBER[e.outboundClass]}`);
|
||||
for (const e of exts) if (e.isAdmin) lines.push(`SP_TCADMIN_${s.digits(e.number)}=1`);
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// sp-from-ext includes these contexts; until their features exist they hold one undialable
|
||||
// extension, because Asterisk doesn't create an empty context and then warns "tries to include
|
||||
// nonexistent context". Each one moves to its feature's renderer.
|
||||
export const PENDING_CONTEXTS = `; ─── Filled in by later milestones
|
||||
[sp-emergency-dial] ; M3-T4 (03 §6.14)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
|
||||
[sp-outbound-routes] ; M3-T3 (03 §6.12)
|
||||
exten => pending,1,Hangup()
|
||||
`;
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Static dialplan (03 §6.2): no DB data, but kept in code so all dialplan lives in one place.
|
||||
// Variables set with a "__" prefix are inherited by every channel the call creates (03 §9).
|
||||
|
||||
export const STATIC_DIALPLAN = `; ─── Normalize a number. ARG1 = raw. Returns normalized in GOSUB_RETVAL. (matches 00 §8)
|
||||
[sp-sub-normalize]
|
||||
exten => s,1,Set(N=\${FILTER(0123456789+,\${ARG1})})
|
||||
same => n,ExecIf($["\${N:0:2}" = "+1"]?Set(N=\${N:1}))
|
||||
same => n,ExecIf($["\${N:0:1}" = "+"]?Set(N=011\${N:1}))
|
||||
same => n,ExecIf($[\${LEN(\${N})} = 10]?Set(N=1\${N}))
|
||||
same => n,Return(\${N})
|
||||
|
||||
; ─── Ring every registered device of a user. ARG1 = ext, ARG2 = seconds.
|
||||
; Sets DIALSTATUS. Rings desk/softphone contacts (PJSIP/101) and browser (PJSIP/101-web).
|
||||
[sp-sub-dial-user]
|
||||
exten => s,1,Set(DS=\${PJSIP_DIAL_CONTACTS(\${ARG1})})
|
||||
same => n,Set(WS=\${PJSIP_DIAL_CONTACTS(\${ARG1}-web)})
|
||||
same => n,ExecIf($["\${DS}" != "" & "\${WS}" != ""]?Set(DS=\${DS}&\${WS}))
|
||||
same => n,ExecIf($["\${DS}" = ""]?Set(DS=\${WS}))
|
||||
same => n,GotoIf($["\${DS}" = ""]?unavail)
|
||||
same => n,Dial(\${DS},\${ARG2},tx)
|
||||
same => n,Return()
|
||||
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
||||
same => n,Return()
|
||||
|
||||
; ─── Call a user with all their personal rules.
|
||||
; ARG1 = ext, ARG2 = ring seconds, ARG3 = voicemail enabled (1/0), ARG4 = record mode
|
||||
[sp-sub-user]
|
||||
exten => s,1,Set(__SP_TARGET=\${ARG1})
|
||||
same => n,ExecIf($["\${SP_DIRECTION}" = "inbound" & ("\${ARG4}" = "inbound" | "\${ARG4}" = "all")]?Gosub(sp-sub-record,s,1))
|
||||
same => n,GotoIf($["\${DB(SP/DND/\${ARG1})}" = "1"]?busy)
|
||||
same => n,Set(FWD=\${DB(SP/CFU/\${ARG1})})
|
||||
same => n,GotoIf($["\${FWD}" != ""]?forward)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(\${ARG1},\${ARG2}))
|
||||
same => n,GotoIf($["\${DIALSTATUS}" = "ANSWER"]?done)
|
||||
same => n,GotoIf($["\${DIALSTATUS}" = "BUSY"]?busy)
|
||||
same => n(noanswer),Set(FWD=\${DB(SP/CFNA/\${ARG1})})
|
||||
same => n,GotoIf($["\${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["\${ARG3}" = "1"]?vmu)
|
||||
same => n,Return()
|
||||
same => n(vmu),VoiceMail(\${ARG1}@default,u)
|
||||
same => n,Return()
|
||||
same => n(busy),Set(FWD=\${DB(SP/CFB/\${ARG1})})
|
||||
same => n,GotoIf($["\${FWD}" != ""]?forward)
|
||||
same => n,GotoIf($["\${ARG3}" = "1"]?vmb)
|
||||
same => n,Busy(5)
|
||||
same => n,Return()
|
||||
same => n(vmb),VoiceMail(\${ARG1}@default,b)
|
||||
same => n,Return()
|
||||
same => n(forward),Gosub(sp-sub-forward,s,1(\${FWD},\${ARG1}))
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Forward a call. ARG1 = target number, ARG2 = ext whose forward rule this is.
|
||||
; Stops after 3 hops so 101→102→101 can't loop forever.
|
||||
[sp-sub-forward]
|
||||
exten => s,1,Set(__SP_FWDHOPS=$[\${IF($["\${SP_FWDHOPS}" = ""]?0:\${SP_FWDHOPS})} + 1])
|
||||
same => n,GotoIf($[\${SP_FWDHOPS} > 3]?stop)
|
||||
same => n,GotoIf($[\${DIALPLAN_EXISTS(sp-local-numbers,\${ARG1},1)}]?internal)
|
||||
same => n,Set(SP_CLASS=\${GLOBAL(SP_CLASS_\${ARG2})})
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,\${ARG1},1)
|
||||
same => n(internal),Goto(sp-local-numbers,\${ARG1},1)
|
||||
same => n(stop),Return()
|
||||
|
||||
; ─── Start recording this call once. File: YYYY/MM/DD/<uniqueid>.wav under the monitor dir.
|
||||
[sp-sub-record]
|
||||
exten => s,1,GotoIf($["\${SP_REC}" != ""]?done)
|
||||
same => n,Set(__SP_REC=\${STRFTIME(\${EPOCH},,%Y/%m/%d)}/\${UNIQUEID}.wav)
|
||||
same => n,MixMonitor(\${SP_REC},b)
|
||||
same => n(done),Return()
|
||||
|
||||
; ─── Auto-answer headers for paging/intercom (Yealink, Grandstream, Polycom, Snom)
|
||||
[sp-sub-autoanswer]
|
||||
exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=<sip:\${SP_EXT}>\\;answer-after=0)
|
||||
same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer)
|
||||
same => n,Return()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(\${EXTEN}@default,u)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-ext-forward]
|
||||
exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external destination
|
||||
same => n,Set(__SP_FORWARDED=1)
|
||||
same => n,Goto(sp-outbound-routes,\${EXTEN},1)
|
||||
|
||||
[sp-rg-member] ; one member of a ring group (Local channel)
|
||||
exten => _X.,1,GotoIf($["\${DB(SP/DND/\${EXTEN})}" = "1"]?skip)
|
||||
same => n,Gosub(sp-sub-dial-user,s,1(\${EXTEN},600))
|
||||
same => n(skip),Hangup()
|
||||
|
||||
[sp-queue-member] ; one queue agent (Local channel)
|
||||
exten => _X.,1,Gosub(sp-sub-dial-user,s,1(\${EXTEN},600))
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-from-ext]
|
||||
include => sp-emergency-dial
|
||||
include => sp-featurecodes
|
||||
include => sp-local-numbers
|
||||
include => sp-outbound-routes
|
||||
`;
|
||||
@@ -18,6 +18,7 @@ export function loadCase(name: string): Snapshot {
|
||||
today?: string;
|
||||
settings?: Record<string, unknown>;
|
||||
extensions?: Partial<Snapshot['extensions'][number]>[];
|
||||
dids?: Snapshot['dids'];
|
||||
};
|
||||
const settings = Object.fromEntries(
|
||||
(Object.keys(SettingsSchemas) as SettingKey[])
|
||||
@@ -27,6 +28,7 @@ export function loadCase(name: string): Snapshot {
|
||||
return {
|
||||
today: raw.today ?? '2026-10-17',
|
||||
settings,
|
||||
dids: raw.dids ?? [],
|
||||
extensions: (raw.extensions ?? []).map((e, i) => ({
|
||||
id: `00000000-0000-4000-8000-${String(i).padStart(12, '0')}`,
|
||||
number: '101',
|
||||
|
||||
@@ -10,6 +10,9 @@ import { generatorRuns } from '../db/schema.ts';
|
||||
import { getSetting, setSetting } from '../services/settings.ts';
|
||||
import { loadSnapshot, type Snapshot } from './load.ts';
|
||||
import { renderPjsip, renderRtp, renderTransports } from './pjsip.ts';
|
||||
import { STATIC_DIALPLAN } from './dialplan/static.ts';
|
||||
import { renderGlobals, renderLocalNumbers } from './dialplan/local.ts';
|
||||
import { PENDING_CONTEXTS } from './dialplan/pending.ts';
|
||||
|
||||
export { renderTransports };
|
||||
|
||||
@@ -35,6 +38,9 @@ export type RenderedFiles = Record<GeneratedFileName, string>;
|
||||
const BODIES: Partial<Record<GeneratedFileName, (snap: Snapshot) => string>> = {
|
||||
'pjsip_selfphoneng.conf': renderPjsip,
|
||||
'rtp_selfphoneng.conf': renderRtp,
|
||||
'extensions_globals_selfphoneng.conf': renderGlobals,
|
||||
'extensions_selfphoneng.conf': (snap) =>
|
||||
`${STATIC_DIALPLAN}\n${renderLocalNumbers(snap)}\n${PENDING_CONTEXTS}`,
|
||||
};
|
||||
|
||||
/** PURE: no I/O, no clock, no randomness. Same snapshot → byte-identical files. */
|
||||
|
||||
@@ -26,11 +26,14 @@ export type SnapExtension = {
|
||||
isAdmin: boolean; // the extension's user is an admin (time-condition override codes)
|
||||
};
|
||||
|
||||
export type SnapDid = { id: string; number: string };
|
||||
|
||||
export type Snapshot = {
|
||||
/** "YYYY-MM-DD" in general.timezone; render() never reads the clock. */
|
||||
today: string;
|
||||
settings: Awaited<ReturnType<typeof getAllSettings>>;
|
||||
extensions: SnapExtension[];
|
||||
dids: SnapDid[]; // M3 adds routing fields
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -54,9 +57,11 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
.leftJoin(outDid, eq(outDid.id, extensions.outboundCallerIdDidId))
|
||||
.leftJoin(e911Did, eq(e911Did.id, extensions.emergencyCallerIdDidId));
|
||||
|
||||
const didRows = await db.select({ id: dids.id, number: dids.number }).from(dids);
|
||||
return {
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
dids: didRows,
|
||||
extensions: rows.map(({ ext, role, outCid, e911Cid }) => ({
|
||||
id: ext.id,
|
||||
number: ext.number,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
; Call completion: defaults are fine. Present only so Asterisk does not warn at boot.
|
||||
[general]
|
||||
Reference in new issue
Block a user