// 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//. 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/.wav under the monitor dir. ; This runs on the calling side, so with SP_REC_SIDES (call transcripts on) -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)=\\;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/, ; 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 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 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 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 `;