; ============================================================== ; Dialplan DID Routing — dashboard-managed (Channel Setup page) ; ; All 5 DIDs (0358700865-869) share ONE handler [did-handle]. Whether a number ; is "incoming" or "outgoing", and the max concurrent inbound calls, are read ; LIVE from Asterisk's internal DB so the dashboard can change them with no edits: ; DB(channelgroup/) = incoming | outgoing ; DB(limits/inbound) = max concurrent inbound (auto = number of incoming DIDs) ; ============================================================== [general] static=yes writeprotect=no [globals] RING_TIMEOUT=60 TRUNKHOST=103.251.203.148 ; ======================== ; INCOMING FROM SIP TRUNK ; ======================== [from-trunk] ; Generic, provider-agnostic: ANY inbound DID routes to the shared handler, ; whose behaviour is driven live by the dashboard (DB channelgroup/advisory per ; DID). Numbers the customer configures work automatically; an unconfigured DID ; still reaches the AI as a normal incoming call. No hardcoded numbers. exten => _X.,1,Goto(did-handle,${EXTEN},1) exten => _+X.,1,Goto(did-handle,${EXTEN:1},1) ; Fallback: provider sends "s" with no DID — extract DID from the To header. exten => s,1,NoOp(=== INCOMING (no DID) from ${CALLERID(num)} ===) same => n,Set(DID=${CUT(PJSIP_HEADER(read,To),@,1)}) same => n,Set(DID=${CUT(DID,:,2)}) same => n,Set(DID=${CUT(DID,>,1)}) same => n,NoOp(=== Extracted DID: ${DID} ===) same => n,GotoIf($["${DID}"=""]?nodid) same => n,Goto(did-handle,${DID},1) ; Truly no DID: hand to the handler as "0" (numeric, unconfigured -> incoming AI). same => n(nodid),Goto(did-handle,0,1) ; ==================================================================== ; Shared DID handler — behaviour driven live by Asterisk DB (dashboard). ; ==================================================================== [did-handle] exten => _X.,1,NoOp(=== DID ${EXTEN} group=${DB(channelgroup/${EXTEN})} advisory=${DB(advisory/${EXTEN})} ===) same => n,GotoIf($["${DB(channelgroup/${EXTEN})}"="outgoing"]?outhandle) same => n,Set(INLIMIT=${IF($["${DB(limits/inbound)}"=""]?5:${DB(limits/inbound)})}) same => n,GotoIf($[${GROUP_COUNT(group-all@company)} >= ${INLIMIT}]?busy) same => n,Set(GROUP(company)=group-all) same => n,Answer() same => n,Wait(1) same => n,Set(DENOISE(rx)=on) same => n,Set(AGC(rx)=4000) same => n,Stasis(ai-reception,incoming,${CALLERID(num)}) same => n,NoOp(=== AI unavailable ===) same => n,Playback(vm-goodbye) same => n,Hangup() same => n(busy),Answer() same => n,Playback(all-circuits-busy-now) same => n,Congestion() ; Outgoing-only number was called by a customer. ; advisory=on -> advise them to call the main support line (default). ; advisory=off -> this outgoing number is not selected for advisory: just ; return an unavailable tone (no call-back message). same => n(outhandle),GotoIf($["${DB(advisory/${EXTEN})}"="off"]?outsilent) same => n(outreject),Answer() same => n,Wait(1) same => n,Playback(/var/spool/asterisk/recording/outreject-advisory) same => n,Hangup() same => n(outsilent),Congestion(5) same => n,Hangup() ; ======================== ; FROM AI AGENT ; ======================== [from-ai-agent] exten => _XXXX,1,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},${RING_TIMEOUT},tTkK) same => n,Hangup() exten => _XXX,1,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},${RING_TIMEOUT},tTkK) same => n,Hangup() exten => _0X.,1,Set(OUTCID=${DB(outcid/${CALLERID(num)})}) same => n,ExecIf($["${OUTCID}" = ""]?Set(OUTCID=${DB(outcid/default)})) same => n,ExecIf($["${OUTCID}" != ""]?Set(CALLERID(num)=${OUTCID})) same => n,Dial(PJSIP/trunk-endpoint/sip:${EXTEN}@${TRUNKHOST},60,tTkK) same => n,Hangup() exten => _+X.,1,Set(OUTCID=${DB(outcid/${CALLERID(num)})}) same => n,ExecIf($["${OUTCID}" = ""]?Set(OUTCID=${DB(outcid/default)})) same => n,ExecIf($["${OUTCID}" != ""]?Set(CALLERID(num)=${OUTCID})) same => n,Dial(PJSIP/trunk-endpoint/sip:${EXTEN}@${TRUNKHOST},60,tTkK) same => n,Hangup() ; ======================== ; INTERNAL (softphone to softphone / transfer from AI) ; ======================== [from-internal] exten => _XXXX,1,GotoIf(${DB_EXISTS(blocked/${EXTEN})}?blocked) same => n,Set(EP_STATE=${DEVICE_STATE(PJSIP/${EXTEN})}) same => n,GotoIf($["${EP_STATE}" = "INVALID"]?notfound) ; M-Phone call forwarding chosen by the user in the app (AstDB fwd//mode|target, ; written by the bridge /api/app/forward). mode: always | noanswer | busy. same => n,Set(FWD_MODE=${DB(fwd/${EXTEN}/mode)}) same => n,Set(FWD_TARGET=${DB(fwd/${EXTEN}/target)}) same => n,GotoIf($["${FWD_MODE}" = "always" & "${FWD_TARGET}" != ""]?fwd) same => n,GotoIf($["${FWD_MODE}" = "busy" & "${FWD_TARGET}" != "" & ("${EP_STATE}" = "UNAVAILABLE" | "${EP_STATE}" = "INUSE" | "${EP_STATE}" = "BUSY" | "${EP_STATE}" = "RINGING" | "${EP_STATE}" = "RINGINUSE" | "${EP_STATE}" = "ONHOLD")]?fwd) same => n,Set(DIAL_STR=${PJSIP_DIAL_CONTACTS(${EXTEN})}) same => n,GotoIf($["${DIAL_STR}" = ""]?direct) same => n,Dial(${DIAL_STR},${RING_TIMEOUT},tTkK) same => n,Goto(after) same => n(direct),Dial(PJSIP/${EXTEN},${RING_TIMEOUT},tTkK) same => n(after),GotoIf($["${FWD_MODE}" = "noanswer" & "${FWD_TARGET}" != "" & "${DIALSTATUS}" = "NOANSWER"]?fwd) same => n,GotoIf($["${FWD_MODE}" = "busy" & "${FWD_TARGET}" != "" & ("${DIALSTATUS}" = "BUSY" | "${DIALSTATUS}" = "CHANUNAVAIL" | "${DIALSTATUS}" = "CONGESTION")]?fwd) same => n,GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Hangup() same => n(fwd),NoOp(=== Forwarding ${EXTEN} (${FWD_MODE}) to ${FWD_TARGET} ===) same => n,Set(FWD_HOPS=$[0${FWD_HOPS} + 1]) same => n,GotoIf($[${FWD_HOPS} > 2]?fwd_stop) same => n,GotoIf($["${FWD_TARGET}" = "ai"]?fwd_ai) same => n,GotoIf($["${FWD_TARGET:0:1}" = "+" | ${LEN(${FWD_TARGET})} > 6]?fwd_out) same => n,Goto(from-internal,${FWD_TARGET},1) same => n(fwd_out),Dial(PJSIP/trunk-endpoint/sip:${FILTER(0-9,${FWD_TARGET})}@${TRUNKHOST},60,tTkK) same => n,Hangup() same => n(fwd_ai),GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Goto(from-internal,2000,1) same => n(fwd_stop),GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Hangup() same => n(noanswer),Stasis(${NOANSWER_APP},noanswer,${NOANSWER_EXT_NAME}) same => n,Hangup() same => n(notfound),Playback(number-not-in-db) same => n,Hangup() same => n(blocked),Playback(pbx-invalid) same => n,Hangup() exten => _XXX,1,GotoIf(${DB_EXISTS(blocked/${EXTEN})}?blocked) same => n,Set(FWD_MODE=${DB(fwd/${EXTEN}/mode)}) same => n,Set(FWD_TARGET=${DB(fwd/${EXTEN}/target)}) same => n,GotoIf($["${FWD_MODE}" = "always" & "${FWD_TARGET}" != ""]?fwd) same => n,Set(DIAL_STR=${PJSIP_DIAL_CONTACTS(${EXTEN})}) same => n,GotoIf($["${DIAL_STR}" = ""]?direct) same => n,Dial(${DIAL_STR},${RING_TIMEOUT},tTkK) same => n,Goto(after) same => n(direct),Dial(PJSIP/${EXTEN},${RING_TIMEOUT},tTkK) same => n(after),GotoIf($["${FWD_MODE}" = "noanswer" & "${FWD_TARGET}" != "" & "${DIALSTATUS}" = "NOANSWER"]?fwd) same => n,GotoIf($["${FWD_MODE}" = "busy" & "${FWD_TARGET}" != "" & ("${DIALSTATUS}" = "BUSY" | "${DIALSTATUS}" = "CHANUNAVAIL" | "${DIALSTATUS}" = "CONGESTION")]?fwd) same => n,GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Hangup() same => n(fwd),Set(FWD_HOPS=$[0${FWD_HOPS} + 1]) same => n,GotoIf($[${FWD_HOPS} > 2]?fwd_stop) same => n,GotoIf($["${FWD_TARGET}" = "ai"]?fwd_ai) same => n,GotoIf($["${FWD_TARGET:0:1}" = "+" | ${LEN(${FWD_TARGET})} > 6]?fwd_out) same => n,Goto(from-internal,${FWD_TARGET},1) same => n(fwd_out),Dial(PJSIP/trunk-endpoint/sip:${FILTER(0-9,${FWD_TARGET})}@${TRUNKHOST},60,tTkK) same => n,Hangup() same => n(fwd_ai),GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Goto(from-internal,2000,1) same => n(fwd_stop),GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Hangup() same => n(noanswer),Stasis(${NOANSWER_APP},noanswer,${NOANSWER_EXT_NAME}) same => n,Hangup() same => n(blocked),Playback(pbx-invalid) same => n,Hangup() ; AI department ring: the bridge sets DIAL_TARGETS (PJSIP/8101&PJSIP/8102&...) ; and continues the caller here. Parallel ring semantics: first answer CANCELs ; the rest; a DECLINE (486/603) on one phone only ends that branch, the others ; keep ringing; when ALL branches fail or the timeout expires -> noanswer ; return to the same AI session. exten => dept_ring,1,NoOp(Department ring: ${DIAL_TARGETS} pickup_mark=${PICKUP_MARK}) same => n,GotoIf($["${DIAL_TARGETS}" = ""]?noanswer) ; Ring Mode "Main phone + pickup": the bridge sets PICKUP_MARK to the main ; extension and PICKUP_KEY to that department's own pickup digit (dashboard ; Ring Mode card). __PICKUPMARK is inherited by the ringing channel so ; colleagues can grab the call with * (or *8) while it rings. ; The AstDB entry pickupkey/ exists ONLY while the phone is ringing: ; cleared on answer (U gosub), on timeout/decline (post-Dial) and on ; caller-abandon-mid-ring (h extension below). same => n,ExecIf($["${PICKUP_MARK}" != ""]?Set(__PICKUPMARK=${PICKUP_MARK})) same => n,ExecIf($["${PICKUP_KEY}" != ""]?Set(DB(pickupkey/${PICKUP_KEY})=${PICKUP_MARK})) same => n,Dial(${DIAL_TARGETS},${RING_TIMEOUT},tTkKU(pk-answered^${PICKUP_KEY})) same => n,ExecIf($["${PICKUP_KEY}" != ""]?Set(PKDEL=${DB_DELETE(pickupkey/${PICKUP_KEY})})) same => n,GotoIf($["${NOANSWER_RETURN}" = "yes"]?noanswer) same => n,Hangup() same => n(noanswer),Stasis(${NOANSWER_APP},noanswer,${NOANSWER_EXT_NAME}) same => n,Hangup() ; ------------------------------------------------------------------ ; Call pickup (Department Ring Mode = "Main phone + pickup"): ; * -> pick up the ringing call of the department that owns this ; key (*1..*9, one per department, set on the dashboard) ; *8 -> pick up the call ringing on that specific extension ; Works from any SIP client (Linphone or physical phone). Pickup only ; succeeds while the target phone is still RINGING; otherwise the ; dialer hears an error beep. (*8 alone is reserved, never a dept key.) ; ------------------------------------------------------------------ exten => _*Z,1,Set(PKMARK=${DB(pickupkey/${EXTEN:1})}) same => n,GotoIf($["${PKMARK}" = ""]?fail) same => n,Pickup(${PKMARK}@PICKUPMARK) ; Pickup failed (nothing ringing): play congestion tone (no sound files in ; this image, so use a generated tone) then hang up. same => n(fail),Answer() same => n,Playtones(congestion) same => n,Wait(3) same => n,Hangup() exten => _*8X.,1,NoOp(Directed pickup attempt for ${EXTEN:2}) same => n,Pickup(${EXTEN:2}@PICKUPMARK) same => n,Answer() same => n,Playtones(congestion) same => n,Wait(3) same => n,Hangup() exten => 2000,1,Dial(PJSIP/ai-agent,${RING_TIMEOUT},tTkK) same => n,Hangup() exten => _0X.,1,Set(OUTCID=${DB(outcid/${CALLERID(num)})}) same => n,ExecIf($["${OUTCID}" = ""]?Set(OUTCID=${DB(outcid/default)})) same => n,ExecIf($["${OUTCID}" != ""]?Set(CALLERID(num)=${OUTCID})) same => n,Dial(PJSIP/trunk-endpoint/sip:${EXTEN}@${TRUNKHOST},60,tTkK) same => n,Hangup() ; Hangup handler reports extension call duration to bridge ; Runs when EITHER party hangs up (caller or extension) ; BRIDGE_CALLID is set by call-handler.js before transfer ; CDR(billsec) = actual talk time on the extension exten => h,1,NoOp(=== Hangup: BRIDGE_CALLID=${BRIDGE_CALLID} billsec=${CDR(billsec)} status=${DIALSTATUS} ===) same => n,ExecIf($["${PICKUP_KEY}" != ""]?Set(PKDEL=${DB_DELETE(pickupkey/${PICKUP_KEY})})) same => n,GotoIf($["${BRIDGE_CALLID}" = ""]?done) same => n,System(curl -s "http://127.0.0.1:3000/api/calls/ext-hangup?callId=${BRIDGE_CALLID}&extDuration=${CDR(billsec)}&status=${DIALSTATUS}" &) same => n(done),NoOp(=== Hangup handler done ===) ; Runs on the phone that answers a department ring (Dial U option): removes ; the pickup key immediately so * can no longer grab this call. [pk-answered] exten => s,1,ExecIf($["${ARG1}" != ""]?Set(PKDEL=${DB_DELETE(pickupkey/${ARG1})})) same => n,Return() ; ======================== ; RECEIVE ONLY ; ======================== [receive-only] exten => _X.,1,Playback(pbx-invalid) same => n,Hangup() [reject-anonymous] exten => _X.,1,Hangup() ; ======================== ; OUTBOUND AMD (answering-machine / voicemail guard) ; ======================== ; Outbound calls are originated into THIS context (not straight into Stasis) so ; AMD() can classify the answer first. Voicemail / answering machine / DND-to- ; voicemail is hung up before the AI or recorder ever engages. Only a real human ; answer is handed to Stasis(,outgoing,,) exactly as before. ; Per requirement "never go to voicemail", anything not clearly HUMAN is hung up; ; that channel then never enters Stasis, so the normal no-answer retry policy ; recalls it (up to the 3-try limit) and finally marks it failed. ; OUTAPP / OUTNUM / OUTPROF are set as channel vars by the ARI originate. [outbound-amd] exten => s,1,NoOp(Outbound AMD to ${OUTNUM} profile=${OUTPROF}) same => n,Answer() ; AMD args inline (NOT amd.conf — that file is image-default and any tuning ; there is lost on rebuild): favor-humans values chosen 2026-07-20 — ; initialSilence 2500, greeting 2500, afterGreetingSilence 800, totalTime 3500, ; minWord 100, betweenWords 50, maxWords 5, silenceThreshold 256, maxWord 5000. ; Default maxWords=2 misjudged a normal talkative human as MACHINE. same => n,AMD(2500,2500,800,3500,100,50,5,256,5000) same => n,NoOp(AMDSTATUS=${AMDSTATUS} AMDCAUSE=${AMDCAUSE}) ; Hang up ONLY when AMD actually HEARD a recorded voicemail greeting playing. ; MACHINE caused by pure INITIALSILENCE is NOT hung up: a real human often picks ; up and waits for the caller (us/AI) to speak first -> that must reach the AI. ; So: talking human, silent/waiting human, and "not sure" all -> AI. Only a ; clear recorded greeting -> hangup. same => n,GotoIf($["${AMDSTATUS}"="MACHINE" & "${AMDCAUSE}"!="INITIALSILENCE"]?machine) same => n(human),Stasis(${OUTAPP},outgoing,${OUTNUM},${OUTPROF}) same => n,Hangup() same => n(machine),Verbose(1,Outbound ${OUTNUM}: voicemail/machine (${AMDCAUSE}) -> mark answered no-retry, no AI) same => n,Stasis(${OUTAPP},machine,${OUTNUM},${OUTPROF}) same => n,Hangup() ; ================================================================ ; Chat messages between extensions (M-Phone "Messages") ; Deliberately EMPTY: every managed extension has ; message_context=mphone-msg, a SIP MESSAGE finds nothing to run here ; and is handed to the bridge (src/msg-store.js), which delivers it or ; keeps it until the receiver signs in. Calls never use this context. ; ================================================================ [mphone-msg]