/api/paging-groups CRUD (members as memberIds). The generator renders
sp-page-{id} per 03 §6.11 (desk phones only, auto-answer headers via
sp-sub-autoanswer) plus the number in sp-local-numbers. Added the `s`
option so phones already on a call aren't paged, which also keeps a
pager who is a member from being paged themselves. A group with no
members just hangs up.
*80<ext> intercom checks the target first: digits only and an endpoint
in sp-from-ext, else ss-noservice (the spec's one-liner passed
${EXTEN:3} straight into Page()). Both written back into 03.
Web: Paging page (list, drawer form with member checkboxes). Golden
case paging. e2e m8-t4 with baresip phones that honor the headers
(sip_autoanswer): an ordinary call rings, a page is answered by itself
and muted, the busy pager is skipped, *80 is two-way, *80999 refused.
The check on a real Yealink/Grandstream stays open (09).
Task: M8-T4
86 lines
3.0 KiB
JavaScript
86 lines
3.0 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// baresip "desk phones" for the e2e scripts: one container, one baresip per phone.
|
|
// Each phone auto-answers (or not) and "talks" with a sine tone; its log shows MWI and DTMF.
|
|
import { spawnSync } from 'node:child_process';
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const docker = (...a) => {
|
|
const r = spawnSync('docker', a, { encoding: 'utf8' });
|
|
return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
};
|
|
|
|
/**
|
|
* phones: [{ ext, password, answer: 'auto' | 'manual', toneHz = 440, intercom = false }]
|
|
* intercom: a manual phone still answers by itself when the INVITE asks it to (Call-Info
|
|
* answer-after, Alert-Info auto-answer), like a Yealink or Grandstream.
|
|
* Returns { log(ext) → text so far, stop() }.
|
|
*/
|
|
export function startPhones({ name, net, ast, phones }) {
|
|
const dir = mkdtempSync(join(tmpdir(), `${name}-`));
|
|
phones.forEach((p, i) => {
|
|
const d = join(dir, p.ext);
|
|
mkdirSync(d);
|
|
writeFileSync(
|
|
join(d, 'config'),
|
|
[
|
|
`sip_listen 0.0.0.0:${5080 + 2 * i}`,
|
|
'module_path /usr/lib/baresip/modules',
|
|
...['g711', 'ausine', 'aufile', 'menu', 'account', 'mwi'].map((m) => `module ${m}.so`),
|
|
`audio_source ausine,${p.toneHz ?? 440}`,
|
|
'ausrc_srate 48000',
|
|
'auplay_srate 48000',
|
|
'audio_player aufile,/dev/null',
|
|
'audio_alert aufile,/dev/null',
|
|
`rtp_ports ${30000 + 100 * i}-${30099 + 100 * i}`,
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
writeFileSync(
|
|
join(d, 'accounts'),
|
|
`<sip:${p.ext}@${ast};transport=udp>;auth_pass=${p.password};answermode=${p.answer}${p.intercom ? ';sip_autoanswer=yes;sip_autoanswer_beep=off' : ''};audio_codecs=PCMU\n`,
|
|
);
|
|
});
|
|
const run = phones.map((p) => `(baresip -f /phones/${p.ext} > /phones/${p.ext}/log 2>&1 &)`);
|
|
docker('rm', '-f', name);
|
|
const r = docker(
|
|
'run',
|
|
'-d',
|
|
'--rm',
|
|
'--name',
|
|
name,
|
|
'--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 && ${run.join(' && ')} && sleep 3600`,
|
|
);
|
|
if (!r.ok) throw new Error(`phones: ${r.out}`);
|
|
return {
|
|
log: (ext) => {
|
|
const f = join(dir, ext, 'log');
|
|
return existsSync(f) ? readFileSync(f, 'utf8') : '';
|
|
},
|
|
/**
|
|
* baresip unregisters on SIGTERM; killing the container would leave stale "Avail" contacts
|
|
* behind (Asterisk keeps them until they expire) that a later script's phones then share.
|
|
*/
|
|
stop: () => {
|
|
// No procps in the image: find baresip through /proc.
|
|
docker(
|
|
'exec',
|
|
name,
|
|
'sh',
|
|
'-c',
|
|
'for p in /proc/[0-9]*; do [ "$(cat $p/comm 2>/dev/null)" = baresip ] && kill -TERM ${p#/proc/}; done; true',
|
|
);
|
|
spawnSync('sleep', ['2']);
|
|
docker('rm', '-f', name);
|
|
},
|
|
};
|
|
}
|