Spec 11 §6.1: sp-sub-dial-user asks /api/internal/caller-name (func_curl, 2 s at most; this machine only, with an APP_SECRET-derived key, not logged) for the ringing user's name for the caller, and sets it on each ringing leg (pre-dial handler, CONNECTEDLINE), never on the call, so forwards and other members don't see someone else's contacts. Ring group/queue/number prefixes kept. Contrack lookups never wait (cache, contactsWarm every 5 min). Proven on real Asterisk with baresip: apps/api/test/asterisk/cnam-test.ts.
265 lines
12 KiB
TypeScript
265 lines
12 KiB
TypeScript
// 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).
|
|
; A call from outside shows the caller's name from this user's contacts (11 §6 C4): set on each
|
|
; ringing leg only (sp-predial-cnam), never on the call itself, so a forward or the next member
|
|
; never sees someone else's contact name.
|
|
[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,GotoIf($["\${SP_DIRECTION}" != "inbound" | "\${SP_CNAM_URL}" = ""]?plain)
|
|
same => n,Gosub(sp-sub-cnam,s,1(\${ARG1}))
|
|
same => n,GotoIf($["\${GOSUB_RETVAL}" = ""]?plain)
|
|
same => n,Set(_SP_CNAM=\${SP_CNAM_PREFIX}\${GOSUB_RETVAL})
|
|
same => n,Dial(\${DS},\${ARG2},txb(sp-predial-cnam^s^1))
|
|
same => n,Set(_SP_CNAM=)
|
|
same => n,Return()
|
|
same => n(plain),Dial(\${DS},\${ARG2},tx)
|
|
same => n,Return()
|
|
same => n(unavail),Set(DIALSTATUS=CHANUNAVAIL)
|
|
same => n,Return()
|
|
|
|
; ─── The caller's name in user ARG1's contacts, or "" (none, or the api didn't answer in 2 s).
|
|
; The api only answers this machine with the key, and only letters, digits and . ' & @ - come back.
|
|
[sp-sub-cnam]
|
|
exten => s,1,Set(CURLOPT(conntimeout)=1)
|
|
same => n,Set(CURLOPT(httptimeout)=2)
|
|
same => n,Return(\${CURL(\${SP_CNAM_URL}?ext=\${FILTER(0-9,\${ARG1})}&number=\${FILTER(0-9,\${CALLERID(num)})}&token=\${SP_CNAM_TOKEN})})
|
|
|
|
; ─── Pre-dial handler on each ringing leg: the name its phone shows.
|
|
[sp-predial-cnam]
|
|
exten => s,1,Set(CONNECTEDLINE(name,i)=\${SP_CNAM})
|
|
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.
|
|
; This runs on the calling side, so with SP_REC_SIDES (call transcripts on) <uniqueid>-in.wav is
|
|
; what the caller said and -out.wav what they heard (the other side).
|
|
[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,GotoIf($["\${GLOBAL(SP_REC_SIDES)}" = "1"]?sides)
|
|
same => n,MixMonitor(\${SP_REC},b)
|
|
same => n,Return()
|
|
same => n(sides),MixMonitor(\${SP_REC},br(\${SP_REC:0:-4}-in.wav)t(\${SP_REC:0:-4}-out.wav))
|
|
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
|
|
`;
|