voicemail: mailbox config, voicemail on no-answer/busy, *97/*98, MWI
Generate voicemail_selfphoneng.conf (03 §7) with one mailbox per extension that has voicemail on, turn on ARG3 in sp-sub-user, and add *97/*98 to sp-featurecodes (which replaces the pending placeholder). Endpoints with a mailbox get mwi_subscribe_replaces_unsolicited so phones that SUBSCRIBE for MWI aren't refused with a 500. A new test:asterisk check leaves a message, sees MWI on a baresip phone, plays it with *97 and sees MWI turn off. Task: M4-T1
This commit is contained in:
1 parent
1e50d12c6e
commit
0d29131d18
31 files changed
+588
-48
No files matched your search
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -132,7 +141,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,ExecIf($["${CID}" != ""]?Set(CALLERID(num)=${CID}))
|
||||
same => n,Set(__SP_DIRECTION=outbound)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -1 +1,23 @@
|
||||
; 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
|
||||
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]
|
||||
|
||||
+12
-7
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -114,13 +123,13 @@ include => sp-outbound-routes
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 103,1,Gosub(sp-sub-user,s,1(103,25,0,never))
|
||||
exten => 103,1,Gosub(sp-sub-user,s,1(103,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 103,hint,PJSIP/103&PJSIP/103-web
|
||||
exten => 104,1,Gosub(sp-sub-user,s,1(104,25,0,never))
|
||||
exten => 104,1,Gosub(sp-sub-user,s,1(104,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 104,hint,PJSIP/104&PJSIP/104-web
|
||||
exten => 105,1,Gosub(sp-sub-user,s,1(105,25,0,never))
|
||||
exten => 105,1,Gosub(sp-sub-user,s,1(105,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 105,hint,PJSIP/105&PJSIP/105-web
|
||||
|
||||
@@ -141,7 +150,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,ExecIf($["${CID}" != ""]?Set(CALLERID(num)=${CID}))
|
||||
same => n,Set(__SP_DIRECTION=outbound)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -41,6 +41,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Dan Systemrm -rf /eviltype = endpoint" <103>
|
||||
mailboxes = 103@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -74,6 +75,7 @@ auth = 103-web-auth
|
||||
aors = 103-web
|
||||
callerid = "Dan Systemrm -rf /eviltype = endpoint" <103>
|
||||
mailboxes = 103@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -111,6 +113,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "104" <104>
|
||||
mailboxes = 104@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -144,6 +147,7 @@ auth = 104-web-auth
|
||||
aors = 104-web
|
||||
callerid = "104" <104>
|
||||
mailboxes = 104@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -181,6 +185,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Zoë Ångström 李 SHELLid evil" <105>
|
||||
mailboxes = 105@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -214,6 +219,7 @@ auth = 105-web-auth
|
||||
aors = 105-web
|
||||
callerid = "Zoë Ångström 李 SHELLid evil" <105>
|
||||
mailboxes = 105@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
|
||||
@@ -1 +1,25 @@
|
||||
; 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
|
||||
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]
|
||||
103 => 4826,Dan Systemrm -rf /eviltype = endpoint,,,tz=local|attach=no
|
||||
104 => 4826,104,,,tz=local|attach=no
|
||||
105 => 4826,Zoë Ångström 李 SHELLid evil,,,tz=local|attach=no
|
||||
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -114,7 +123,7 @@ include => sp-outbound-routes
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 101,1,Gosub(sp-sub-user,s,1(101,25,0,never))
|
||||
exten => 101,1,Gosub(sp-sub-user,s,1(101,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 101,hint,PJSIP/101&PJSIP/101-web
|
||||
|
||||
@@ -141,7 +150,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,ExecIf($["${CID}" != ""]?Set(CALLERID(num)=${CID}))
|
||||
same => n,Set(__SP_DIRECTION=outbound)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -51,6 +51,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
named_call_group = sales
|
||||
@@ -88,6 +89,7 @@ auth = 101-web-auth
|
||||
aors = 101-web
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
named_call_group = sales
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
; 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
|
||||
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]
|
||||
101 => 4826,Dan Doe,,,tz=local|attach=no
|
||||
+9
-4
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -212,7 +221,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,Dial(PJSIP/911@trunk-a1b2c3d4-0000-4000-8000-000000000001)
|
||||
same => n,Dial(PJSIP/911@trunk-a1b2c3d4-0000-4000-8000-000000000002)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -1 +1,23 @@
|
||||
; 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
|
||||
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]
|
||||
|
||||
+10
-5
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -114,7 +123,7 @@ include => sp-outbound-routes
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 101,1,Gosub(sp-sub-user,s,1(101,25,0,never))
|
||||
exten => 101,1,Gosub(sp-sub-user,s,1(101,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 101,hint,PJSIP/101&PJSIP/101-web
|
||||
|
||||
@@ -216,7 +225,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,Set(__SP_DIRECTION=outbound)
|
||||
same => n,Dial(PJSIP/911@trunk-7d3e9a52-1c4b-4f6e-9a0d-2b8c5e1f4a37)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -41,6 +41,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
@@ -74,6 +75,7 @@ auth = 101-web-auth
|
||||
aors = 101-web
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
; 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
|
||||
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]
|
||||
101 => 4826,Dan Doe,,,tz=local|attach=no
|
||||
+10
-5
@@ -85,6 +85,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(${EXTEN}@default,u)
|
||||
@@ -114,7 +123,7 @@ include => sp-outbound-routes
|
||||
include => sp-parkedcalls
|
||||
|
||||
; users (sorted by number)
|
||||
exten => 99,1,Gosub(sp-sub-user,s,1(99,25,0,never))
|
||||
exten => 99,1,Gosub(sp-sub-user,s,1(99,25,1,never))
|
||||
same => n,Hangup()
|
||||
exten => 99,hint,PJSIP/99
|
||||
exten => 102,1,Gosub(sp-sub-user,s,1(102,25,0,all))
|
||||
@@ -138,7 +147,3 @@ exten => s,1,Set(CID=${IF($["${SP_E911CID}" != ""]?${SP_E911CID}:${SP_DEFAULT_E9
|
||||
same => n,ExecIf($["${CID}" != ""]?Set(CALLERID(num)=${CID}))
|
||||
same => n,Set(__SP_DIRECTION=outbound)
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
@@ -36,6 +36,7 @@ rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Lobby phone" <99>
|
||||
mailboxes = 99@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = fr
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
; 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
|
||||
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]
|
||||
99 => 4826,Lobby phone,,,tz=local|attach=no
|
||||
@@ -4,9 +4,6 @@ import { CLASS_RANK } from '@selfphoneng/shared';
|
||||
import { byNumber, type Snapshot } from '../load.ts';
|
||||
import * as s from '../sanitize.ts';
|
||||
|
||||
// TODO(M4): voicemail on. Until then every user gets ARG3 = 0 (09 M2-T4).
|
||||
const VOICEMAIL_READY = false;
|
||||
|
||||
export function renderLocalNumbers(snap: Snapshot): string {
|
||||
const lines = [
|
||||
'[sp-local-numbers]',
|
||||
@@ -17,7 +14,7 @@ export function renderLocalNumbers(snap: Snapshot): string {
|
||||
for (const e of [...snap.extensions].sort(byNumber)) {
|
||||
const num = s.digits(e.number);
|
||||
const ring = s.digits(String(e.ringSeconds));
|
||||
const vm = VOICEMAIL_READY && e.voicemailEnabled ? 1 : 0;
|
||||
const vm = e.voicemailEnabled ? 1 : 0;
|
||||
lines.push(
|
||||
`exten => ${num},1,Gosub(sp-sub-user,s,1(${num},${ring},${vm},${e.recordMode}))`,
|
||||
' same => n,Hangup()',
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// sp-from-ext includes these contexts; until their features exist they hold one undialable
|
||||
// extension, because Asterisk doesn't create an empty context and then warns "tries to include
|
||||
// nonexistent context". Each one moves to its feature's renderer.
|
||||
export const PENDING_CONTEXTS = `; ─── Filled in by later milestones
|
||||
[sp-featurecodes] ; M6-T7 (03 §6.13)
|
||||
exten => pending,1,Hangup()
|
||||
`;
|
||||
@@ -88,6 +88,15 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(\${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Feature codes (03 §6.13). M4: voicemail only; M6-T7 adds the rest.
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,VoiceMailMain(\${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
|
||||
[sp-vm-direct]
|
||||
exten => _X.,1,Answer()
|
||||
same => n,VoiceMail(\${EXTEN}@default,u)
|
||||
|
||||
@@ -72,6 +72,7 @@ export function loadCase(name: string): Snapshot {
|
||||
maxContacts: 5,
|
||||
ringSeconds: 25,
|
||||
voicemailEnabled: true,
|
||||
voicemailPin: '4826',
|
||||
outboundClass: 'national',
|
||||
outboundCallerId: null,
|
||||
emergencyCallerId: null,
|
||||
|
||||
@@ -13,10 +13,10 @@ import { loadSnapshot, type Snapshot } from './load.ts';
|
||||
import { renderPjsip, renderRtp, renderTransports } from './pjsip.ts';
|
||||
import { STATIC_DIALPLAN } from './dialplan/static.ts';
|
||||
import { renderGlobals, renderLocalNumbers } from './dialplan/local.ts';
|
||||
import { PENDING_CONTEXTS } from './dialplan/pending.ts';
|
||||
import { renderDids, renderTrunkEntries } from './dialplan/inbound.ts';
|
||||
import { renderOutbound } from './dialplan/outbound.ts';
|
||||
import { renderEmergency } from './dialplan/emergency.ts';
|
||||
import { renderVoicemail } from './voicemail.ts';
|
||||
|
||||
export { renderTransports };
|
||||
|
||||
@@ -43,6 +43,7 @@ const BODIES: Partial<Record<GeneratedFileName, (snap: Snapshot) => string>> = {
|
||||
'pjsip_selfphoneng.conf': renderPjsip,
|
||||
'rtp_selfphoneng.conf': renderRtp,
|
||||
'extensions_globals_selfphoneng.conf': renderGlobals,
|
||||
'voicemail_selfphoneng.conf': renderVoicemail,
|
||||
'extensions_selfphoneng.conf': (snap) =>
|
||||
[
|
||||
STATIC_DIALPLAN,
|
||||
@@ -51,7 +52,6 @@ const BODIES: Partial<Record<GeneratedFileName, (snap: Snapshot) => string>> = {
|
||||
renderDids(snap),
|
||||
renderOutbound(snap),
|
||||
renderEmergency(snap),
|
||||
PENDING_CONTEXTS,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
|
||||
@@ -17,6 +17,7 @@ export type SnapExtension = {
|
||||
maxContacts: number;
|
||||
ringSeconds: number;
|
||||
voicemailEnabled: boolean;
|
||||
voicemailPin: string; // decrypted
|
||||
outboundClass: 'internal' | 'local' | 'national' | 'international';
|
||||
outboundCallerId: string | null; // DID number
|
||||
emergencyCallerId: string | null; // DID number
|
||||
@@ -147,6 +148,7 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
maxContacts: ext.maxContacts,
|
||||
ringSeconds: ext.ringSeconds,
|
||||
voicemailEnabled: ext.voicemailEnabled,
|
||||
voicemailPin: decrypt(ext.voicemailPinEnc),
|
||||
outboundClass: ext.outboundClass,
|
||||
outboundCallerId: outCid,
|
||||
emergencyCallerId: e911Cid,
|
||||
|
||||
@@ -84,6 +84,8 @@ function extensionSections(e: SnapExtension): string {
|
||||
const common = [
|
||||
`callerid = "${name}" <${num}>`,
|
||||
mailbox,
|
||||
// The AOR also sends unsolicited MWI; without this, a phone's own SUBSCRIBE gets a 500.
|
||||
mailbox && 'mwi_subscribe_replaces_unsolicited = yes',
|
||||
'dtmf_mode = rfc4733',
|
||||
'allow_subscribe = yes',
|
||||
groups && `named_call_group = ${groups}`,
|
||||
|
||||
@@ -32,6 +32,8 @@ export const typedSecret = check(/^[^\s;](?:[^\r\n;]*[^\s;])?$/, 'password', 128
|
||||
export const codec = check(/^(?:ulaw|alaw|g722)$/, 'codec', 4);
|
||||
/** Voicemail / conference PINs. */
|
||||
export const pin = check(/^[0-9]{4,10}$/, 'pin', 10);
|
||||
/** IANA time zone names, e.g. America/Chicago, Etc/GMT+6. */
|
||||
export const timezone = check(/^[A-Za-z][A-Za-z0-9_+/-]*$/, 'timezone', 64);
|
||||
/** Trunk server host: a hostname or an IPv4 address. */
|
||||
export const host = check(
|
||||
/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// voicemail_selfphoneng.conf (03 §7). Asterisk only records; the api indexes and emails (04 §5.6).
|
||||
import { byNumber, type Snapshot } from './load.ts';
|
||||
import * as s from './sanitize.ts';
|
||||
|
||||
export function renderVoicemail(snap: Snapshot): string {
|
||||
const st = snap.settings;
|
||||
const boxes = [...snap.extensions]
|
||||
.filter((e) => e.voicemailEnabled)
|
||||
.sort(byNumber)
|
||||
.map((e) => {
|
||||
const num = s.digits(e.number);
|
||||
return `${num} => ${s.pin(e.voicemailPin)},${s.name(e.name) || num},,,tz=local|attach=no`;
|
||||
});
|
||||
return `[general]
|
||||
format = wav
|
||||
attach = no ; the api sends emails itself
|
||||
maxmsg = ${s.digits(String(st['voicemail.maxMessages']))}
|
||||
maxsecs = ${s.digits(String(st['voicemail.maxMessageSeconds']))}
|
||||
minsecs = 1
|
||||
maxsilence = 10
|
||||
silencethreshold = 128
|
||||
maxlogins = 3
|
||||
envelope = yes
|
||||
saycid = yes
|
||||
review = no
|
||||
nextaftercmd = yes
|
||||
sendvoicemail = no
|
||||
pollmailboxes = yes
|
||||
pollfreq = 30
|
||||
|
||||
[zonemessages]
|
||||
local = ${s.timezone(st['general.timezone'])}|'vm-received' Q 'digits/at' IMp
|
||||
|
||||
[default]
|
||||
${boxes.join('\n')}
|
||||
`;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ function snapshot(): Snapshot {
|
||||
maxContacts: 5,
|
||||
ringSeconds: 25,
|
||||
voicemailEnabled: false,
|
||||
voicemailPin: '4826',
|
||||
outboundClass: 'national' as const,
|
||||
outboundCallerId: null,
|
||||
emergencyCallerId: null,
|
||||
|
||||
@@ -26,7 +26,9 @@ function expectations(dir) {
|
||||
const endpoints = [...pjsip.matchAll(/^\[([^\]]+)\]\ntype = endpoint$/gm)].map((m) => m[1]);
|
||||
const ext = readFileSync(join(dir, 'extensions_selfphoneng.conf'), 'utf8');
|
||||
const contexts = [...ext.matchAll(/^\[([^\]]+)\]$/gm)].map((m) => m[1]);
|
||||
return { endpoints, contexts };
|
||||
const vm = readFileSync(join(dir, 'voicemail_selfphoneng.conf'), 'utf8');
|
||||
const mailboxes = [...vm.matchAll(/^(\d+) =>/gm)].map((m) => m[1]);
|
||||
return { endpoints, contexts, mailboxes };
|
||||
}
|
||||
|
||||
async function check(name) {
|
||||
@@ -55,7 +57,7 @@ async function check(name) {
|
||||
if (!booted) problems.push('Asterisk did not finish booting');
|
||||
await sleep(500);
|
||||
|
||||
const { endpoints, contexts } = expectations(tmp);
|
||||
const { endpoints, contexts, mailboxes } = expectations(tmp);
|
||||
const shown = cli('pjsip show endpoints').out;
|
||||
for (const e of endpoints)
|
||||
if (!new RegExp(`Endpoint:\\s+${e}[\\s/]`).test(shown))
|
||||
@@ -64,6 +66,10 @@ async function check(name) {
|
||||
const d = cli(`dialplan show ${c}`).out;
|
||||
if (!d.includes(`'${c}'`)) problems.push(`context ${c} not loaded`);
|
||||
}
|
||||
const users = cli('voicemail show users').out;
|
||||
for (const m of mailboxes)
|
||||
if (!new RegExp(`^default\\s+${m}\\s`, 'm').test(users))
|
||||
problems.push(`mailbox ${m} not loaded`);
|
||||
// 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, '');
|
||||
|
||||
@@ -88,6 +88,7 @@ function snapshot(): Snapshot {
|
||||
maxContacts: 5,
|
||||
ringSeconds: 25,
|
||||
voicemailEnabled: false,
|
||||
voicemailPin: '4826',
|
||||
outboundClass: 'national',
|
||||
outboundCallerId: null,
|
||||
emergencyCallerId: null,
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// M4-T1 check, automated (09 M4-T1): 102 calls 101, nobody answers, 102 leaves a message, the
|
||||
// message-waiting light on 101's phone turns on (baresip mwi module), and *97 plays it.
|
||||
// Same setup as call-test.ts: private Docker bridge, baresip phones, no image builds.
|
||||
// Usage: node --conditions=source apps/api/test/asterisk/voicemail-test.ts
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { SettingsSchemas, type SettingKey } from '@selfphoneng/shared';
|
||||
import { AmiClient } from '../../src/ami/client.ts';
|
||||
import { GENERATED_FILES, render } from '../../src/generator/index.ts';
|
||||
import type { Snapshot } from '../../src/generator/load.ts';
|
||||
|
||||
const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk';
|
||||
const NET = 'sp-vm';
|
||||
const AST = 'sp-vm-ast';
|
||||
const PHONES = 'sp-vm-phones';
|
||||
const AMI_PORT = 15039;
|
||||
const AMI_SECRET = 'vm-test-secret';
|
||||
const PASS = { '101': 'VmTest101xxxxxxxxxxxxxxx', '102': 'VmTest102xxxxxxxxxxxxxxx' };
|
||||
const PIN = '4826';
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
const docker = (...args: string[]) => {
|
||||
const r = spawnSync('docker', args, { encoding: 'utf8' });
|
||||
return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
||||
};
|
||||
const log = (...a: unknown[]) => console.log(`[voicemail-test]`, ...a);
|
||||
|
||||
function snapshot(): Snapshot {
|
||||
const settings = Object.fromEntries(
|
||||
(Object.keys(SettingsSchemas) as SettingKey[])
|
||||
.filter((k) => k !== 'sms.webhookToken')
|
||||
.map((k) => [k, SettingsSchemas[k].parse(undefined)]),
|
||||
) as Snapshot['settings'];
|
||||
const ext = (number: '101' | '102', name: string) => ({
|
||||
id: `00000000-0000-4000-8000-000000000${number}`,
|
||||
number,
|
||||
name,
|
||||
sipPassword: PASS[number],
|
||||
webrtcEnabled: false,
|
||||
maxContacts: 5,
|
||||
ringSeconds: 5,
|
||||
voicemailEnabled: true,
|
||||
voicemailPin: PIN,
|
||||
outboundClass: 'national' as const,
|
||||
outboundCallerId: null,
|
||||
emergencyCallerId: null,
|
||||
recordMode: 'never' as const,
|
||||
pickupGroup: null,
|
||||
language: 'en',
|
||||
mohClass: 'default',
|
||||
isAdmin: false,
|
||||
});
|
||||
return {
|
||||
today: '2026-10-17',
|
||||
settings,
|
||||
dids: [],
|
||||
trunks: [],
|
||||
routes: [],
|
||||
extensions: [ext('101', 'Alice'), ext('102', 'Bob')],
|
||||
};
|
||||
}
|
||||
|
||||
/** 101 never answers and subscribes to MWI; 102 auto-answers and "talks" (a sine tone). */
|
||||
function phoneConfig(dir: string, ext: '101' | '102', port: number) {
|
||||
const d = join(dir, `phone-${ext}`);
|
||||
spawnSync('mkdir', ['-p', d]);
|
||||
writeFileSync(
|
||||
join(d, 'config'),
|
||||
[
|
||||
'poll_method epoll',
|
||||
`sip_listen 0.0.0.0:${port}`,
|
||||
'module_path /usr/lib/baresip/modules',
|
||||
'module g711.so',
|
||||
'module ausine.so',
|
||||
'module aufile.so',
|
||||
'module menu.so',
|
||||
'module account.so',
|
||||
'module mwi.so',
|
||||
'audio_source ausine,880',
|
||||
'ausrc_srate 48000',
|
||||
'auplay_srate 48000',
|
||||
'audio_player aufile,/dev/null',
|
||||
'audio_alert aufile,/dev/null',
|
||||
'rtp_ports 30000-30100',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const answer = ext === '102' ? 'auto' : 'manual';
|
||||
writeFileSync(
|
||||
join(d, 'accounts'),
|
||||
`<sip:${ext}@${AST};transport=udp>;auth_pass=${PASS[ext]};answermode=${answer};audio_codecs=PCMU;regint=60;mwi=yes\n`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sp-vm-'));
|
||||
const conf = join(dir, 'conf');
|
||||
spawnSync('mkdir', ['-p', conf]);
|
||||
const files = render(snapshot());
|
||||
for (const f of GENERATED_FILES) writeFileSync(join(conf, f), files[f]);
|
||||
writeFileSync(
|
||||
join(dir, 'manager.conf.template'),
|
||||
'[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n',
|
||||
);
|
||||
phoneConfig(dir, '101', 5070);
|
||||
phoneConfig(dir, '102', 5072);
|
||||
|
||||
const problems: string[] = [];
|
||||
const ami = new AmiClient({
|
||||
host: '127.0.0.1',
|
||||
port: AMI_PORT,
|
||||
username: 'selfphoneng',
|
||||
secret: AMI_SECRET,
|
||||
logger: { info() {}, warn() {}, error() {}, debug() {} },
|
||||
backoffMs: [500],
|
||||
});
|
||||
const count = async () => {
|
||||
const r = await ami.action({ Action: 'MailboxCount', Mailbox: '101@default' });
|
||||
return { new: Number(r.NewMessages), old: Number(r.OldMessages) };
|
||||
};
|
||||
const waitIdle = async () => {
|
||||
for (
|
||||
let i = 0;
|
||||
i < 20 && /PJSIP\/|Local\//.test(await ami.command('core show channels concise'));
|
||||
i++
|
||||
)
|
||||
await sleep(500);
|
||||
};
|
||||
try {
|
||||
docker('rm', '-f', AST, PHONES);
|
||||
docker('network', 'create', NET);
|
||||
const run = docker(
|
||||
'run',
|
||||
'-d',
|
||||
'--rm',
|
||||
'--name',
|
||||
AST,
|
||||
'--network',
|
||||
NET,
|
||||
'-p',
|
||||
`127.0.0.1:${AMI_PORT}:5038`,
|
||||
'-e',
|
||||
`AMI_SECRET=${AMI_SECRET}`,
|
||||
'-v',
|
||||
`${conf}:/etc/asterisk/selfphoneng`,
|
||||
'-v',
|
||||
`${join(dir, 'manager.conf.template')}:/etc/asterisk/manager.conf.template:ro`,
|
||||
IMAGE,
|
||||
);
|
||||
if (!run.ok) throw new Error(`asterisk: ${run.out}`);
|
||||
for (
|
||||
let i = 0;
|
||||
i < 60 && !docker('exec', AST, 'asterisk', '-rx', 'core waitfullybooted').ok;
|
||||
i++
|
||||
)
|
||||
await sleep(500);
|
||||
ami.start();
|
||||
await new Promise((r) => ami.once('connected', r));
|
||||
|
||||
log('starting baresip phones…');
|
||||
const phones = docker(
|
||||
'run',
|
||||
'-d',
|
||||
'--rm',
|
||||
'--name',
|
||||
PHONES,
|
||||
'--network',
|
||||
NET,
|
||||
'-v',
|
||||
`${dir}:/phones`,
|
||||
'debian:trixie-slim',
|
||||
'sh',
|
||||
'-c',
|
||||
'apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends baresip-core >/dev/null 2>&1 && ' +
|
||||
'(baresip -f /phones/phone-101 > /phones/phone-101/log 2>&1 &) && ' +
|
||||
'(baresip -f /phones/phone-102 > /phones/phone-102/log 2>&1 &) && sleep 600',
|
||||
);
|
||||
if (!phones.ok) throw new Error(`phones: ${phones.out}`);
|
||||
let contacts = '';
|
||||
for (let i = 0; i < 180; i++) {
|
||||
contacts = await ami.command('pjsip show contacts').catch(() => '');
|
||||
if (/101\/sip:.*Avail/.test(contacts) && /102\/sip:.*Avail/.test(contacts)) break;
|
||||
await sleep(1000);
|
||||
}
|
||||
if (!/101\/sip:.*Avail/.test(contacts) || !/102\/sip:.*Avail/.test(contacts))
|
||||
throw new Error(`phones did not register:\n${contacts}`);
|
||||
log('101 and 102 registered');
|
||||
const before = await count();
|
||||
if (before.new !== 0) problems.push(`mailbox not empty at start: ${JSON.stringify(before)}`);
|
||||
|
||||
// 102 picks up (auto-answer), then calls 101 like a normal internal call; 101 rings 5 s.
|
||||
await ami.action({
|
||||
Action: 'Originate',
|
||||
Channel: 'PJSIP/102',
|
||||
Context: 'sp-from-ext',
|
||||
Exten: '101',
|
||||
Priority: '1',
|
||||
CallerID: '"Bob" <102>',
|
||||
Async: 'true',
|
||||
});
|
||||
let recording = false;
|
||||
for (let i = 0; i < 30 && !recording; i++) {
|
||||
await sleep(1000);
|
||||
recording = /!VoiceMail!/.test(await ami.command('core show channels concise'));
|
||||
}
|
||||
if (!recording) throw new Error('the unanswered call did not reach VoiceMail');
|
||||
log('no answer → voicemail; waiting for the greeting and beep');
|
||||
for (let i = 0; i < 40 && !/Recording the message/.test(docker('logs', AST).out); i++)
|
||||
await sleep(500);
|
||||
await sleep(5000); // five seconds of 880 Hz "speech"
|
||||
await ami.command('channel request hangup all');
|
||||
await waitIdle();
|
||||
await sleep(1000);
|
||||
|
||||
const after = await count();
|
||||
log(`mailbox 101: ${after.new} new, ${after.old} old`);
|
||||
if (after.new !== 1) problems.push(`expected 1 new message, got ${JSON.stringify(after)}`);
|
||||
const inbox = docker('exec', AST, 'ls', '/var/spool/asterisk/voicemail/default/101/INBOX').out;
|
||||
if (!/msg0000\.wav/.test(inbox) || !/msg0000\.txt/.test(inbox))
|
||||
problems.push(`no msg0000.wav/.txt in INBOX: ${inbox}`);
|
||||
const meta = docker(
|
||||
'exec',
|
||||
AST,
|
||||
'cat',
|
||||
'/var/spool/asterisk/voicemail/default/101/INBOX/msg0000.txt',
|
||||
).out;
|
||||
const secs = Number(/^duration=(\d+)/m.exec(meta)?.[1]);
|
||||
if (!/^callerid=.*<102>/m.test(meta) || !(secs >= 2))
|
||||
problems.push(`unexpected message metadata:\n${meta}`);
|
||||
|
||||
// The phone's light: baresip prints the NOTIFY body it gets for its MWI subscription.
|
||||
let mwiOn = false;
|
||||
for (let i = 0; i < 10 && !mwiOn; i++) {
|
||||
mwiOn = /Messages-Waiting:\s*yes/i.test(readFileSync(join(dir, 'phone-101', 'log'), 'utf8'));
|
||||
if (!mwiOn) await sleep(1000);
|
||||
}
|
||||
if (!mwiOn) problems.push('101 never got Messages-Waiting: yes');
|
||||
else log('101 MWI on');
|
||||
|
||||
// *97 from 101: log in with the PIN, press 1 to hear the new message. The trailing pauses
|
||||
// keep the caller side up (the call ends when SendDTMF returns).
|
||||
await ami.action({
|
||||
Action: 'Originate',
|
||||
Channel: 'Local/*97@sp-from-ext',
|
||||
Application: 'SendDTMF',
|
||||
Data: `${'w'.repeat(6)}${PIN}#${'w'.repeat(16)}1${'w'.repeat(60)}`,
|
||||
Variable: '__SP_EXT=101',
|
||||
Async: 'true',
|
||||
});
|
||||
let played = false;
|
||||
for (let i = 0; i < 60 && !played; i++) {
|
||||
await sleep(500);
|
||||
played = /Playing '[^']*voicemail\/default\/101\/INBOX\/msg0000/.test(
|
||||
docker('logs', AST).out,
|
||||
);
|
||||
}
|
||||
if (!played) problems.push('*97 did not play msg0000');
|
||||
else log('*97 is playing the message');
|
||||
await sleep(3000);
|
||||
await ami.command('channel request hangup all');
|
||||
await waitIdle();
|
||||
await sleep(1000);
|
||||
const heard = await count();
|
||||
if (heard.new !== 0 || heard.old !== 1)
|
||||
problems.push(`after *97 expected 0 new / 1 old, got ${JSON.stringify(heard)}`);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const lines = readFileSync(join(dir, 'phone-101', 'log'), 'utf8').match(
|
||||
/Messages-Waiting:\s*\w+/gi,
|
||||
);
|
||||
if (lines?.at(-1)?.toLowerCase().endsWith('no')) break;
|
||||
if (i === 9) problems.push(`101 MWI did not turn off (last: ${lines?.at(-1)})`);
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-control-regex -- strips Asterisk's ANSI colors
|
||||
const plainLog = docker('logs', AST).out.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
problems.push(
|
||||
...plainLog
|
||||
.split('\n')
|
||||
.filter((l) => /ERROR\[/.test(l))
|
||||
.map((l) => `log: ${l.trim()}`),
|
||||
);
|
||||
} catch (e) {
|
||||
problems.push((e as Error).message);
|
||||
} finally {
|
||||
if (problems.length) log(`asterisk log (tail):\n${docker('logs', '--tail', '60', AST).out}`);
|
||||
if (problems.length)
|
||||
for (const e of ['101', '102'])
|
||||
log(`phone ${e} log (tail):
|
||||
${docker('exec', PHONES, 'tail', '-n', '25', `/phones/phone-${e}/log`).out}`);
|
||||
ami.stop();
|
||||
docker('rm', '-f', PHONES, AST);
|
||||
docker('network', 'rm', NET);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
if (problems.length) {
|
||||
console.log('FAIL voicemail');
|
||||
for (const p of problems) console.log(` ${p}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('ok voicemail (leave, MWI on, *97 plays, MWI off)');
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -182,6 +182,9 @@ auth = 101-auth
|
||||
aors = 101
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default ; omit if voicemail disabled
|
||||
mwi_subscribe_replaces_unsolicited = yes ; omit if voicemail disabled. The AOR also sends unsolicited MWI;
|
||||
; without this a phone that SUBSCRIBEs gets 500 "already configured
|
||||
; for unsolicited MWI" (checked on 24.0.0-rc2, M4-T1)
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
@@ -226,6 +229,7 @@ auth = 101-web-auth
|
||||
aors = 101-web
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
mwi_subscribe_replaces_unsolicited = yes
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
named_call_group = sales
|
||||
@@ -898,7 +902,10 @@ local = {general.timezone}|'vm-received' Q 'digits/at' IMp
|
||||
[default]
|
||||
101 => {pin},{name},,,tz=local|attach=no
|
||||
```
|
||||
Only extensions with `voicemail_enabled` get a line.
|
||||
Only extensions with `voicemail_enabled` get a line. Names go through `name()` (no commas or `|`),
|
||||
the time zone through `timezone()`. Until M6-T7, `sp-featurecodes` holds only `*97`/`*98`.
|
||||
Checked live by `apps/api/test/asterisk/voicemail-test.ts` (M4-T1): leave a message, MWI NOTIFY
|
||||
to a subscribed phone, `*97` login and playback, MWI off.
|
||||
|
||||
## 8. `musiconhold_selfphoneng.conf`
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test:asterisk": "node apps/api/test/asterisk/load-golden.mjs && node --conditions=source apps/api/test/asterisk/call-test.ts && node --conditions=source apps/api/test/asterisk/trunk-test.ts"
|
||||
"test:asterisk": "node apps/api/test/asterisk/load-golden.mjs && node --conditions=source apps/api/test/asterisk/call-test.ts && node --conditions=source apps/api/test/asterisk/trunk-test.ts && node --conditions=source apps/api/test/asterisk/voicemail-test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
Reference in new issue
Block a user