numbers: any free internal number of 1 to 6 digits; ranges only suggest

Owner wanted the first user on 1. Numbering ranges now choose the Next free suggestion
(Settings → Numbering), the setup wizard takes a number per person, and a one-digit number
can't also be a menu key of a digital receptionist with direct dialing. Dialplan patterns that
take an internal number match one digit (_X!); golden case short-numbers proves it in Asterisk.
This commit is contained in:
iamdoubz committed 2026-10-21 13:17:25 -05:00
1 parent 4143f6d064
commit aa6681da03
42 files changed
+932 -137

No files matched your search

@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -0,0 +1,6 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
[sp_admin_user]
type = user
admin = yes
marked = yes
music_on_hold_when_empty = yes
@@ -0,0 +1,6 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
SP_DEFAULT_CID=
SP_DEFAULT_E911_CID=
SP_REC_ANNOUNCE=
SP_CLASS_1=2
SP_CLASS_12=2
@@ -0,0 +1,321 @@
; 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=)
same => n,Gosub(sp-sub-contacts,s,1(${ARG1}))
same => n,Gosub(sp-sub-contacts,s,1(${ARG1}-web))
same => n,GotoIf($["${DS}" = ""]?unavail)
same => n,Dial(${DS},${ARG2},tx)
same => n,Return()
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
same => n,Return()
; ─── Add AOR ARG1's reachable contacts to DS as PJSIP/<ARG1>/<uri>. Not PJSIP_DIAL_CONTACTS: iOS
; Linphone's push params (pn-prid=…:voip&…:remote) hold '&', which splits Dial's device list.
; pn-* only matter to a push server, so they're dropped; FILTER keeps whatever else a phone puts
; in its Contact from reaching Dial's options (',') or another device ('&').
[sp-sub-contacts]
exten => s,1,Set(LOCAL(CL)=${PJSIP_AOR(${ARG1},contact)})
same => n(next),Set(LOCAL(C)=${POP(CL)})
same => n,GotoIf($["${C}" = ""]?done)
same => n,Set(LOCAL(ST)=${PJSIP_CONTACT(${C},status)})
same => n,GotoIf($["${ST}" = "Unavailable" | "${ST}" = "Unreachable"]?next)
same => n,Set(LOCAL(U)=${PJSIP_CONTACT(${C},uri)})
same => n,Set(LOCAL(D)=${CUT(U,\;,1)})
same => n,Set(LOCAL(I)=1)
same => n(param),Set(I=$[${I} + 1])
same => n,GotoIf($[${I} > ${FIELDQTY(U,\;)}]?add)
same => n,Set(LOCAL(P)=${CUT(U,\;,${I})})
same => n,GotoIf($["${P:0:3}" = "pn-"]?param)
same => n,Set(D=${D}\;${P})
same => n,Goto(param)
same => n(add),Set(DS=${DS}${IF($["${DS}" = ""]?:&)}PJSIP/${ARG1}/${FILTER(0-9a-zA-Z.:@\;=_+%[]\x2d,${D})})
same => n,Goto(next)
same => n(done),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_LIMITEXT=${ARG2}) ; counts toward that extension's call limit
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,Set(CDR(SP_REC)=${SP_REC}) ; cdr_manager maps CDR variables only
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()
; ─── 911 alert ring-out. Originated by the api (04 §5.3): Channel PJSIP/<notify ext>,
; Exten = the extension that dialed 911.
[sp-emergency-alert]
exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
same => n,Wait(1)
same => n,Playback(beep&beep&beep)
same => n,Playback(extension)
same => n,SayDigits(${EXTEN})
same => n,Playback(beep&beep&beep)
same => n,Playback(extension)
same => n,SayDigits(${EXTEN})
same => n,Hangup()
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
[sp-sub-vmlimit]
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
same => n,GotoIf($[${N} >= 10]?no)
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
same => n,Return(ok)
same => n(no),Return()
; ─── Feature codes (03 §6.13).
[sp-featurecodes]
exten => *97,1,Answer() ; my voicemail
same => n,Gosub(sp-sub-vmlimit,s,1)
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
same => n,VoiceMailMain(${SP_EXT}@default)
same => n,Hangup()
same => n(full),Playback(an-error-has-occurred)
same => n,Hangup()
exten => *98,1,Answer() ; any voicemail (asks mailbox)
same => n,Gosub(sp-sub-vmlimit,s,1)
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
same => n,VoiceMailMain(@default)
same => n,Hangup()
same => n(full),Playback(an-error-has-occurred)
same => n,Hangup()
exten => *78,1,Answer() ; DND on
same => n,Set(DB(SP/DND/${SP_EXT})=1)
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
same => n,Playback(do-not-disturb&activated)
same => n,Hangup()
exten => *79,1,Answer() ; DND off
same => n,Set(X=${DB_DELETE(SP/DND/${SP_EXT})})
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
same => n,Playback(call-fwd-unconditional&activated)
same => n,Hangup()
exten => *73,1,Answer() ; cancel forward always
same => n,Set(X=${DB_DELETE(SP/CFU/${SP_EXT})})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
; line covers calls that reach *8 through the dialplan. Uses named_pickup_group.
exten => *8,1,Pickup()
same => n,Hangup()
exten => *43,1,Answer() ; echo test
same => n,Playback(demo-echotest)
same => n,Echo()
same => n,Playback(demo-echodone)
same => n,Hangup()
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
same => n,PauseQueueMember(${Q},Local/${SP_EXT}@sp-queue-member/n)
same => n,GotoIf($["${PQMSTATUS}" != "PAUSED"]?deny) ; not an agent of that queue
same => n,Set(DB(${K})=1)
same => n,UserEvent(SpToggle,Key: QPAUSE,Queue: ${Q},Ext: ${SP_EXT},Value: 1)
same => n,Playback(agent-loggedoff)
same => n,Hangup()
same => n(login),Set(X=${DB_DELETE(${K})})
same => n,UnpauseQueueMember(${Q},Local/${SP_EXT}@sp-queue-member/n)
same => n,GotoIf($["${UPQMSTATUS}" != "UNPAUSED"]?deny)
same => n,UserEvent(SpToggle,Key: QPAUSE,Queue: ${Q},Ext: ${SP_EXT},Value: 0)
same => n,Playback(agent-loginok)
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
same => n,Hangup()
same => n(deny),Answer()
same => n,Playback(ss-noservice)
same => n,Hangup()
; time condition overrides *31N open, *32N closed, *33N auto (admins only)
exten => _*3[123]X,1,Answer()
same => n,GotoIf($["${GLOBAL(SP_TCADMIN_${SP_EXT})}" != "1"]?deny)
same => n,Goto(sp-tc-override,${EXTEN},1)
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
[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 => 1,1,Gosub(sp-sub-user,s,1(1,25,1,never))
same => n,Hangup()
exten => 1,hint,PJSIP/1&PJSIP/1-web
exten => 12,1,Gosub(sp-sub-user,s,1(12,25,1,never))
same => n,Hangup()
exten => 12,hint,PJSIP/12&PJSIP/12-web
; ring groups
exten => 5,1,Goto(sp-rg-22222222-2222-4222-8222-222222222005,s,1)
; queues
exten => 6,1,Goto(sp-queue-44444444-4444-4444-8444-444444444006,s,1)
; digital receptionists
exten => 0,1,Goto(sp-ivr-33333333-3333-4333-8333-333333333000,s,1)
; ─── Ring group 5 "Sales" (ringall)
[sp-rg-22222222-2222-4222-8222-222222222005]
exten => s,1,Set(__SP_TARGET=5)
same => n,Dial(Local/1@sp-rg-member&Local/12@sp-rg-member,25,t)
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?done)
same => n,Goto(sp-vm-direct,1,1)
same => n(done),Hangup()
; ─── Queue 6 "Support"
[sp-queue-44444444-4444-4444-8444-444444444006]
exten => s,1,Set(__SP_TARGET=6)
same => n,Answer()
same => n,Queue(6,t,,,300)
same => n,GotoIf($["${QUEUESTATUS}" = "TIMEOUT" | "${QUEUESTATUS}" = "FULL" | "${QUEUESTATUS}" = "JOINEMPTY" | "${QUEUESTATUS}" = "LEAVEEMPTY" | "${QUEUESTATUS}" = "JOINUNAVAIL" | "${QUEUESTATUS}" = "LEAVEUNAVAIL"]?fail)
same => n,Hangup()
same => n(fail),UserEvent(SpQueueExit,Queue: 6,Status: ${QUEUESTATUS})
same => n,Hangup()
; ─── Digital receptionist 0 "Main menu"
[sp-ivr-33333333-3333-4333-8333-333333333000]
include => sp-ivr-direct
exten => s,1,Answer()
same => n,Set(SP_IVR_TRIES=0)
same => n(menu),Set(TIMEOUT(digit)=3)
same => n,Background(custom/44444444-4444-4444-8444-444444444444)
same => n,WaitExten(5)
exten => 2,1,Goto(sp-rg-22222222-2222-4222-8222-222222222005,s,1)
exten => i,1,Set(SP_IVR_TRIES=$[${SP_IVR_TRIES} + 1])
same => n,GotoIf($[${SP_IVR_TRIES} >= 3]?giveup)
same => n,Playback(pbx-invalid)
same => n,Goto(s,menu)
same => n(giveup),Hangup()
exten => t,1,Set(SP_IVR_TRIES=$[${SP_IVR_TRIES} + 1])
same => n,GotoIf($[${SP_IVR_TRIES} >= 3]?giveup)
same => n,Goto(s,menu)
same => n(giveup),Hangup()
[sp-ivr-direct]
exten => 0,1,Goto(sp-local-numbers,0,1)
exten => 1,1,Goto(sp-local-numbers,1,1)
exten => 5,1,Goto(sp-local-numbers,5,1)
exten => 6,1,Goto(sp-local-numbers,6,1)
exten => 12,1,Goto(sp-local-numbers,12,1)
; ─── Office hours overrides (admins only, see sp-featurecodes)
[sp-tc-override]
exten => _*3[123]X,1,Playback(ss-noservice)
same => n,Hangup()
[sp-inbound]
; anything else
exten => _[0-9a-z].,1,Hangup()
[sp-outbound-routes]
exten => pending,1,Hangup() ; no routes yet
[sp-emergency-dial]
exten => 911,1,Goto(sp-emergency,s,1)
exten => 9911,1,Goto(sp-emergency,s,1) ; people who dial 9 first out of habit
[sp-emergency]
exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E911_CID})})
same => n,UserEvent(SpEmergency,Ext: ${SP_EXT},CallerId: ${CID},Dialed: 911)
same => n,ExecIf($["${CID}" != ""]?Set(CALLERID(num)=${CID}))
same => n,Set(__SP_DIRECTION=outbound)
same => n,Set(CDR(SP_DIRECTION)=outbound)
same => n,Hangup()
@@ -0,0 +1 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
@@ -0,0 +1,173 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
[global]
type = global
user_agent = SelfPhoneNG
endpoint_identifier_order = ip,username,anonymous
keep_alive_interval = 90
max_initial_qualify_time = 4
[system]
type = system
timer_t1 = 500
timer_b = 32000
[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
[transport-tcp]
type = transport
protocol = tcp
bind = 0.0.0.0:5060
[transport-ws]
type = transport
protocol = ws
bind = 0.0.0.0
; ── extension 1 (Dan Doe) ──
[1]
type = endpoint
context = sp-from-ext
disallow = all
allow = g722,ulaw,alaw
auth = 1-auth
aors = 1
direct_media = no
rtp_symmetric = yes
force_rport = yes
rewrite_contact = yes
send_pai = yes
callerid = "Dan Doe" <1>
mailboxes = 1@default
mwi_subscribe_replaces_unsolicited = yes
dtmf_mode = rfc4733
allow_subscribe = yes
language = en
moh_suggest = default
tone_zone = us
set_var = SP_EXT=1
set_var = SP_CLASS=2
set_var = SP_RECMODE=never
set_var = __SP_DIRECTION=internal
[1-auth]
type = auth
auth_type = userpass
username = 1
password = Ab3dEf9hIj2kLm4nOp6qRs8t
[1]
type = aor
max_contacts = 5
remove_existing = yes
qualify_frequency = 30
mailboxes = 1@default
[1-web]
type = endpoint
context = sp-from-ext
webrtc = yes
disallow = all
allow = g722,ulaw
auth = 1-web-auth
aors = 1-web
from_domain = selfphoneng.invalid
callerid = "Dan Doe" <1>
mailboxes = 1@default
mwi_subscribe_replaces_unsolicited = yes
dtmf_mode = rfc4733
allow_subscribe = yes
language = en
moh_suggest = default
tone_zone = us
set_var = SP_EXT=1
set_var = SP_CLASS=2
set_var = SP_RECMODE=never
set_var = __SP_DIRECTION=internal
[1-web-auth]
type = auth
auth_type = userpass
username = 1-web
password = Ab3dEf9hIj2kLm4nOp6qRs8t
[1-web]
type = aor
max_contacts = 3
remove_existing = yes
qualify_frequency = 0
; ── extension 12 (Sue Smith) ──
[12]
type = endpoint
context = sp-from-ext
disallow = all
allow = g722,ulaw,alaw
auth = 12-auth
aors = 12
direct_media = no
rtp_symmetric = yes
force_rport = yes
rewrite_contact = yes
send_pai = yes
callerid = "Sue Smith" <12>
mailboxes = 12@default
mwi_subscribe_replaces_unsolicited = yes
dtmf_mode = rfc4733
allow_subscribe = yes
language = en
moh_suggest = default
tone_zone = us
set_var = SP_EXT=12
set_var = SP_CLASS=2
set_var = SP_RECMODE=never
set_var = __SP_DIRECTION=internal
[12-auth]
type = auth
auth_type = userpass
username = 12
password = Bb3dEf9hIj2kLm4nOp6qRs8t
[12]
type = aor
max_contacts = 5
remove_existing = yes
qualify_frequency = 30
mailboxes = 12@default
[12-web]
type = endpoint
context = sp-from-ext
webrtc = yes
disallow = all
allow = g722,ulaw
auth = 12-web-auth
aors = 12-web
from_domain = selfphoneng.invalid
callerid = "Sue Smith" <12>
mailboxes = 12@default
mwi_subscribe_replaces_unsolicited = yes
dtmf_mode = rfc4733
allow_subscribe = yes
language = en
moh_suggest = default
tone_zone = us
set_var = SP_EXT=12
set_var = SP_CLASS=2
set_var = SP_RECMODE=never
set_var = __SP_DIRECTION=internal
[12-web-auth]
type = auth
auth_type = userpass
username = 12-web
password = Bb3dEf9hIj2kLm4nOp6qRs8t
[12-web]
type = aor
max_contacts = 3
remove_existing = yes
qualify_frequency = 0
@@ -0,0 +1,16 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
; ─── Queue 6 "Support"
[6]
strategy = rrmemory
timeout = 15
retry = 5
wrapuptime = 10
maxlen = 0
musicclass = default
announce-frequency = 60
announce-position = yes
ringinuse = no
setinterfacevar = yes
joinempty = paused,unavailable,invalid,unknown
leavewhenempty = paused,unavailable,invalid,unknown
member => Local/1@sp-queue-member/n,0,Dan Doe,hint:1@sp-local-numbers
@@ -0,0 +1,4 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
rtpstart = 10000
rtpend = 20000
icesupport = yes
@@ -0,0 +1,28 @@
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
[general]
format = wav
attach = no ; the api sends emails itself
maxmsg = 100
maxsecs = 300
minsecs = 1
maxsilence = 10
silencethreshold = 128
maxlogins = 3
minpassword = 4
; PINs changed from the phone pass the api's rules first (asterisk/sp-vm-pincheck); the api
; stores them from VoicemailPasswordChange (services/vmpin.ts).
externpasscheck = /usr/local/bin/sp-vm-pincheck
envelope = yes
saycid = yes
review = no
nextaftercmd = yes
sendvoicemail = no
pollmailboxes = yes
pollfreq = 30
[zonemessages]
local = America/Chicago|'vm-received' Q 'digits/at' IMp
[default]
1 => 4826,Dan Doe,,,tz=local|attach=no
12 => 4826,Sue Smith,,,tz=local|attach=no
@@ -0,0 +1,62 @@
{
"extensions": [
{
"id": "00000000-0000-4000-8000-000000000001",
"number": "1",
"name": "Dan Doe",
"voicemailEnabled": true,
"sipPassword": "Ab3dEf9hIj2kLm4nOp6qRs8t"
},
{
"id": "00000000-0000-4000-8000-000000000012",
"number": "12",
"name": "Sue Smith",
"sipPassword": "Bb3dEf9hIj2kLm4nOp6qRs8t"
}
],
"ringGroups": [
{
"id": "22222222-2222-4222-8222-222222222005",
"number": "5",
"name": "Sales",
"members": ["1", "12"],
"noAnswerDestination": { "type": "voicemail", "id": "00000000-0000-4000-8000-000000000001" }
}
],
"queues": [
{
"id": "44444444-4444-4444-8444-444444444006",
"number": "6",
"name": "Support",
"members": [{ "number": "1", "name": "Dan Doe", "penalty": 0 }]
}
],
"ivrs": [
{
"id": "33333333-3333-4333-8333-333333333000",
"number": "0",
"name": "Main menu",
"greeting": "custom/44444444-4444-4444-8444-444444444444",
"allowDirectDial": true,
"options": [
{
"digit": "2",
"destination": { "type": "ring_group", "id": "22222222-2222-4222-8222-222222222005" }
}
]
}
],
"asteriskMatches": [
["1", "sp-local-numbers"],
["5", "sp-local-numbers"],
["0", "sp-local-numbers"],
["1", "sp-rg-member"],
["1", "sp-queue-member"],
["1", "sp-vm-direct"],
["1", "sp-ivr-direct"],
["**1", "sp-featurecodes"],
["*456", "sp-featurecodes"],
["*721", "sp-featurecodes"],
["*801", "sp-featurecodes"]
]
}
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
@@ -148,7 +148,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(${EXTEN:3}))
same => n,Set(DB(SP/CFU/${SP_EXT})=${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: ${GOSUB_RETVAL})
@@ -159,7 +159,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: ${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=${FILTER(0-9,${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/${T}-&PJSIP/${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -174,7 +174,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=${FILTER(0-9,${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/${Q}/${SP_EXT})
same => n,GotoIf($["${DB(${K})}" = "1"]?login)
@@ -192,7 +192,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=${FILTER(0-9,${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["${T}" != "${EXTEN:3}"]?deny)
same => n,GotoIf($["${PJSIP_ENDPOINT(${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/${T},db(sp-sub-autoanswer^s^1),30)
@@ -208,7 +208,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
@@ -218,12 +218,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
+7 -7
View File
@@ -151,7 +151,7 @@ exten => *79,1,Answer() ; DND off
same => n,UserEvent(SpToggle,Key: DND,Ext: \${SP_EXT},Value: 0)
same => n,Playback(do-not-disturb&de-activated)
same => n,Hangup()
exten => _*72X.,1,Answer() ; *72<number> forward always
exten => _*72X!,1,Answer() ; *72<number> forward always
same => n,Gosub(sp-sub-normalize,s,1(\${EXTEN:3}))
same => n,Set(DB(SP/CFU/\${SP_EXT})=\${GOSUB_RETVAL})
same => n,UserEvent(SpToggle,Key: CFU,Ext: \${SP_EXT},Value: \${GOSUB_RETVAL})
@@ -162,7 +162,7 @@ exten => *73,1,Answer() ; cancel forward alw
same => n,UserEvent(SpToggle,Key: CFU,Ext: \${SP_EXT},Value: )
same => n,Playback(call-fwd-unconditional&de-activated)
same => n,Hangup()
exten => _**X.,1,Set(T=\${FILTER(0-9,\${EXTEN:2})}) ; digits only: "&" or "," would add arguments
exten => _**X!,1,Set(T=\${FILTER(0-9,\${EXTEN:2})}) ; digits only: "&" or "," would add arguments
same => n,PickupChan(PJSIP/\${T}-&PJSIP/\${T}-web-) ; directed pickup **101
same => n,Hangup()
; *8 group pickup: features.conf pickupexten catches it straight from a phone's INVITE; this
@@ -177,7 +177,7 @@ exten => *43,1,Answer() ; echo test
exten => *65,1,Answer() ; say my extension
same => n,SayDigits(\${SP_EXT})
same => n,Hangup()
exten => _*45X.,1,Answer() ; *45<queue> toggle queue login
exten => _*45X!,1,Answer() ; *45<queue> toggle queue login
same => n,Set(Q=\${FILTER(0-9,\${EXTEN:3})})
same => n,Set(K=SP/QPAUSE/\${Q}/\${SP_EXT})
same => n,GotoIf($["\${DB(\${K})}" = "1"]?login)
@@ -195,7 +195,7 @@ exten => _*45X.,1,Answer() ; *45<queue> toggle
same => n,Hangup()
same => n(deny),Playback(ss-noservice)
same => n,Hangup()
exten => _*80X.,1,Set(T=\${FILTER(0-9,\${EXTEN:3})}) ; *80<ext> intercom, both ways
exten => _*80X!,1,Set(T=\${FILTER(0-9,\${EXTEN:3})}) ; *80<ext> intercom, both ways
same => n,GotoIf($["\${T}" != "\${EXTEN:3}"]?deny)
same => n,GotoIf($["\${PJSIP_ENDPOINT(\${T},context)}" != "sp-from-ext"]?deny) ; desk phones only
same => n,Page(PJSIP/\${T},db(sp-sub-autoanswer^s^1),30)
@@ -211,7 +211,7 @@ exten => _*3[123]X,1,Answer()
same => n,Hangup()
[sp-vm-direct]
exten => _X.,1,Answer()
exten => _X!,1,Answer()
same => n,VoiceMail(\${EXTEN}@default,u)
same => n,Hangup()
@@ -221,12 +221,12 @@ exten => _[0-9*#+].,1,Set(SP_CLASS=3) ; admin-configured external desti
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)
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))
exten => _X!,1,Gosub(sp-sub-dial-user,s,1(\${EXTEN},600))
same => n,Hangup()
[sp-from-ext]
+1 -1
View File
@@ -37,7 +37,7 @@ function contactStatus(s: string | undefined): ContactLive['status'] {
/** "101" and "101-web" both belong to extension 101; trunks and others don't match. */
export const extOfEndpoint = (endpoint: string): string | null =>
/^(\d{3,6})(?:-web)?$/.exec(endpoint)?.[1] ?? null;
/^(\d{1,6})(?:-web)?$/.exec(endpoint)?.[1] ?? null;
/** "trunk-<uuid>", "trunk-<uuid>-reg" → the trunk id. */
export const trunkOfObject = (name: string): string | null =>
+27 -1
View File
@@ -21,7 +21,7 @@ import {
describeDestination,
findReferences,
} from '../services/destinations.ts';
import { assertNumberAvailable } from '../services/numbering.ts';
import { assertNumberAvailable, ownerOf } from '../services/numbering.ts';
type Row = typeof ivrs.$inferSelect;
type Tx = Parameters<Parameters<Db['transaction']>[0]>[0];
@@ -99,9 +99,26 @@ export async function ivrRoutes(app: FastifyInstance) {
app.get('/api/ivrs/:id', async (req) => toDto(db, await getIvr(IdParams.parse(req.params).id)));
/** With direct dialing on, a menu key can't also be a one-digit number: pressing it picks the option. */
async function assertMenuKeys(directDial: boolean, keys: string[]) {
if (!directDial) return;
for (const key of keys) {
const owner = /^\d$/.test(key) ? await ownerOf(db, key) : null;
if (owner)
throw conflict(
`Key ${key} is also the number of ${owner}: with "callers can dial numbers" on, pressing ${key} would always choose the menu option. Use another key, or turn that off`,
{ owner },
);
}
}
app.post('/api/ivrs', async (req, reply) => {
const { options = [], ...body } = IvrCreateSchema.parse(req.body);
await assertNumberAvailable(db, body.number, 'ivr');
await assertMenuKeys(
body.allowDirectDial ?? true,
options.map((o) => o.digit),
);
await assertPrompt(db, body.greetingSoundId);
await assertPrompt(db, body.invalidSoundId);
await assertDestinations(db, [
@@ -139,6 +156,15 @@ export async function ivrRoutes(app: FastifyInstance) {
const before = await getIvr(id);
if (body.number && body.number !== before.number)
await assertNumberAvailable(db, body.number, 'ivr', id);
if (options || body.allowDirectDial)
await assertMenuKeys(
body.allowDirectDial ?? before.allowDirectDial,
options
? options.map((o) => o.digit)
: (await db.select().from(ivrOptions).where(eq(ivrOptions.ivrId, id))).map(
(o) => o.digit,
),
);
await assertPrompt(db, body.greetingSoundId);
await assertPrompt(db, body.invalidSoundId);
// Pointing at itself is fine here: the IVR exists.
+1 -1
View File
@@ -20,7 +20,7 @@ export async function checkForward(
const digits = dialedDigits(input);
if (!digits) throw badRequest('Enter a number');
if (digits === ext.number) throw badRequest("You can't forward calls to yourself");
if (/^\d{3,6}$/.test(digits)) {
if (/^\d{1,6}$/.test(digits)) {
if (await ownerOf(db, digits)) return digits;
throw badRequest(`${digits} isn't an extension or group here`);
}
+42 -6
View File
@@ -3,7 +3,15 @@
import { and, eq, ne } from 'drizzle-orm';
import { isReservedNumber, type NumberKind } from '@selfphoneng/shared';
import type { Db } from '../db/client.ts';
import { conferences, extensions, ivrs, pagingGroups, queues, ringGroups } from '../db/schema.ts';
import {
conferences,
extensions,
ivrOptions,
ivrs,
pagingGroups,
queues,
ringGroups,
} from '../db/schema.ts';
import { badRequest, conflict } from '../errors.ts';
import { getSetting } from './settings.ts';
@@ -46,20 +54,48 @@ export async function ownerOf(db: Db, number: string, excludeId?: string): Promi
return null;
}
/**
* A digital receptionist that lets callers dial numbers and has `key` in its menu: callers pressing
* it get the menu option, so a one-digit number `key` can't be dialed from there.
*/
export async function menuKeyOwner(db: Db, key: string, excludeIvrId?: string) {
if (!/^\d$/.test(key)) return null;
const [row] = await db
.select({ name: ivrs.name })
.from(ivrOptions)
.innerJoin(ivrs, eq(ivrs.id, ivrOptions.ivrId))
.where(
and(
eq(ivrOptions.digit, key),
eq(ivrs.allowDirectDial, true),
...(excludeIvrId ? [ne(ivrs.id, excludeIvrId)] : []),
),
)
.limit(1);
return row?.name ?? null;
}
/**
* Any free internal number, for any kind (owner, 2026-10-21): the numbering ranges only choose the
* suggested next number (nextFreeNumber). `kind` is kept for that and for callers' readability.
*/
export async function assertNumberAvailable(
db: Db,
number: string,
kind: NumberKind,
_kind: NumberKind,
excludeId?: string,
): Promise<void> {
if (!/^\d{3,6}$/.test(number)) throw badRequest('Internal numbers have 3 to 6 digits');
if (!/^\d{1,6}$/.test(number)) throw badRequest('Internal numbers have 1 to 6 digits');
if (isReservedNumber(number))
throw badRequest(`${number} is reserved (emergency and service codes can't be used)`);
const [from, to] = await getSetting(db, RANGE_KEY[kind]);
const n = Number(number);
if (n < from || n > to) throw badRequest(`Use a number from ${from} to ${to}`);
const owner = await ownerOf(db, number, excludeId);
if (owner) throw conflict(`${number} is already used by ${owner}`, { owner });
const menu = await menuKeyOwner(db, number);
if (menu)
throw conflict(
`${number} is a menu key of the digital receptionist "${menu}", which also lets callers dial numbers: pressing ${number} there would always choose the menu option`,
{ owner: `digital receptionist "${menu}"` },
);
}
/** Lowest free number in the kind's range, or null when the range is full. */
+8
View File
@@ -141,6 +141,14 @@ async function check(name) {
if (!shownQ.startsWith(`${q} has`) || listed !== members)
problems.push(`queue ${q}: expected ${members} members, got: ${shownQ.trim()}`);
}
// A case can list [number, context] pairs Asterisk must match (short-numbers: one-digit
// numbers, which a pattern like _X. needs two digits for).
const snap = JSON.parse(readFileSync(join(GOLDEN, name, 'snapshot.json'), 'utf8'));
for (const [exten, ctx] of snap.asteriskMatches ?? []) {
const d = cli(`dialplan show ${exten}@${ctx}`).out;
if (!/-= \d+ extensions? \(/.test(d) || /There is no existence/.test(d))
problems.push(`${exten} doesn't match in ${ctx}`);
}
// Asterisk colors its log: strip the escape codes before looking for ERROR[.
// eslint-disable-next-line no-control-regex -- matching ANSI escapes is the point
const log = docker('logs', NAME).out.replace(/\x1b\[[0-9;]*m/g, '');
+1 -1
View File
@@ -62,7 +62,7 @@ describe.skipIf(!hasTestDb)('/api/conferences', () => {
it('checks the number and the PINs', async () => {
const bad = (body: object) =>
req('POST', '/api/conferences', { number: '801', name: 'X', ...body });
expect((await bad({ number: '700' })).statusCode).toBe(400); // parking, not 800–849
expect((await bad({ number: '700' })).statusCode).toBe(409); // the parking lot
expect((await bad({ number: '800' })).statusCode).toBe(409);
expect((await bad({ pin: '12' })).statusCode).toBe(400);
expect((await bad({ pin: '12a4' })).statusCode).toBe(400);
+6 -3
View File
@@ -72,9 +72,12 @@ describe.skipIf(!hasTestDb)('extensions API', () => {
const make = (number: string, extra = {}) =>
req('POST', '/api/extensions', { number, name: 'X', ...extra });
expect((await make('211')).json().error.message).toMatch(/reserved/);
expect((await make('600')).json().error.message).toMatch(/from 100 to 499/);
expect((await make('12')).statusCode).toBe(400);
expect((await make('710')).statusCode).toBe(400); // outside the users range anyway
expect((await make('1234567')).statusCode).toBe(400); // 1 to 6 digits
expect((await make('710')).json().error.message).toMatch(/parking/);
// Any free number, in or out of the users range (owner, 2026-10-21): "1" for the first user.
const one = await make('1');
expect(one.statusCode, one.body).toBe(201);
expect((await req('DELETE', `/api/extensions/${one.json().id}`)).statusCode).toBe(200);
const dup = await make('101');
expect(dup.statusCode).toBe(409);
expect(dup.json().error.message).toMatch(/extension "Dan Doe"/);
+18 -1
View File
@@ -81,7 +81,24 @@ describe.skipIf(!hasTestDb)('/api/ivrs', () => {
const post = (body: object) => req('POST', '/api/ivrs', { ...base(), number: '601', ...body });
const missing = '00000000-0000-4000-8000-000000000999';
expect((await post({ number: '600' })).statusCode).toBe(409);
expect((await post({ number: '500' })).statusCode).toBe(400); // ring-group range
expect((await post({ number: '1234567' })).statusCode).toBe(400);
// A one-digit number and a menu key (direct dialing on) can't be the same, either way round.
const one = await req('POST', '/api/extensions', { number: '1', name: 'X' });
expect(one.statusCode).toBe(409);
expect(one.json().error.message).toMatch(/menu key of the digital receptionist "Main menu"/);
const four = await req('POST', '/api/extensions', { number: '4', name: 'Four' });
expect(four.statusCode, four.body).toBe(201);
const clash = await post({ options: [{ digit: '4', destination: { type: 'hangup' } }] });
expect(clash.statusCode).toBe(409);
expect(clash.json().error.message).toMatch(/Key 4 is also the number of extension "Four"/);
const noDial = await post({
allowDirectDial: false,
number: '602',
options: [{ digit: '4', destination: { type: 'hangup' } }],
});
expect(noDial.statusCode).toBe(201);
await req('DELETE', `/api/ivrs/${noDial.json().id}`);
await req('DELETE', `/api/extensions/${four.json().id}`);
expect((await post({ greetingSoundId: missing })).statusCode).toBe(400);
const twice = await post({
options: [
+1 -1
View File
@@ -53,7 +53,7 @@ describe.skipIf(!hasTestDb)('/api/paging-groups', () => {
it('checks the number and the members', async () => {
const bad = (body: object) =>
req('POST', '/api/paging-groups', { number: '851', name: 'X', ...body });
expect((await bad({ number: '800' })).statusCode).toBe(400); // conference range
expect((await bad({ number: '1234567' })).statusCode).toBe(400); // 1 to 6 digits
expect((await bad({ number: '850' })).statusCode).toBe(409);
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000000'] })).statusCode).toBe(
+1 -1
View File
@@ -99,7 +99,7 @@ describe.skipIf(!hasTestDb)('/api/queues', () => {
timeoutDestination: { type: 'hangup' },
...body,
});
expect((await bad({ number: '500' })).statusCode).toBe(400); // outside 550–599
expect((await bad({ number: '1234567' })).statusCode).toBe(400); // 1 to 6 digits
expect((await bad({ number: '550' })).statusCode).toBe(409);
const twice = { extensionId: ext['101'], penalty: 0 };
expect((await bad({ members: [twice, twice] })).statusCode).toBe(400);
+1 -1
View File
@@ -64,7 +64,7 @@ describe.skipIf(!hasTestDb)('/api/ring-groups', () => {
noAnswerDestination: { type: 'hangup' },
...body,
});
expect((await bad({ number: '101' })).statusCode).toBe(400); // outside 500–549
expect((await bad({ number: '101' })).statusCode).toBe(409); // extension 101 has it
expect((await bad({ number: '500' })).statusCode).toBe(409);
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000999'] })).statusCode).toBe(