/api/me/voicemail lists, streams (Range, download) and marks read or deletes through AMI VoicemailMove/VoicemailRemove, then rescans so MWI and the list agree. /api/me/greetings takes the raw recording or file and converts it with ffmpeg to 8 kHz PCM (media/audio.ts). My Phone gets a layout with Home (new voicemails) and Voicemail (play marks read, download, delete, record/upload greetings via MediaRecorder); the list refetches on the live voicemail.new event. e2e m4-t3 checks MWI goes off within 5 s after play and delete, and a browser-recorded greeting is what callers hear. Task: M4-T3
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Startup order (04 §1).
|
|
import { resolve } from 'node:path';
|
|
import { AmiClient } from './ami/client.ts';
|
|
import { buildApp } from './app.ts';
|
|
import { ConfigError, loadConfig } from './config.ts';
|
|
import { setSecretKey } from './crypto/secrets.ts';
|
|
import { createDb, runMigrations } from './db/client.ts';
|
|
import { clearRestartIfRestarted, createGenerator } from './generator/index.ts';
|
|
import { ensureInitialAdmin } from './services/bootstrap.ts';
|
|
import { LiveState } from './live/state.ts';
|
|
import { smtpMailer } from './services/mail.ts';
|
|
import { watchEmergencies } from './services/emergency.ts';
|
|
import { watchVoicemail } from './services/voicemail.ts';
|
|
|
|
let config;
|
|
try {
|
|
config = loadConfig();
|
|
} catch (err) {
|
|
if (!(err instanceof ConfigError)) throw err;
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
}
|
|
setSecretKey(config.APP_SECRET);
|
|
|
|
const { db, pool } = createDb(config.DATABASE_URL);
|
|
const app = buildApp(config, db); // creates the pino logger
|
|
const ami = new AmiClient({
|
|
host: config.AMI_HOST,
|
|
port: config.AMI_PORT,
|
|
username: config.AMI_USERNAME,
|
|
secret: config.AMI_SECRET,
|
|
logger: app.log,
|
|
});
|
|
const live = new LiveState(db, ami);
|
|
const generator = createGenerator({
|
|
db,
|
|
ami,
|
|
dir: resolve(config.ASTERISK_CONFIG_DIR),
|
|
log: app.log,
|
|
onApplied: (r) => {
|
|
live.emit('message', {
|
|
type: 'system.configApplied',
|
|
data: { ok: r.ok, error: r.error, restartRequired: r.restartRequired },
|
|
});
|
|
void live.loadFromDb().catch((err) => app.log.error({ err }, 'live reload failed'));
|
|
},
|
|
});
|
|
app.decorate('generator', generator);
|
|
app.decorate('ami', ami);
|
|
const sendMail = smtpMailer(db);
|
|
app.decorate('sendMail', sendMail);
|
|
watchEmergencies({ db, ami, live, send: sendMail, log: app.log }, ami);
|
|
const voicemail = watchVoicemail(
|
|
{
|
|
db,
|
|
spoolDir: resolve(config.VOICEMAIL_DIR),
|
|
ami,
|
|
live,
|
|
send: sendMail,
|
|
publicHost: config.PUBLIC_HOSTNAME,
|
|
log: app.log,
|
|
},
|
|
ami,
|
|
);
|
|
app.decorate('voicemail', voicemail);
|
|
app.decorate('live', live);
|
|
|
|
await runMigrations(db);
|
|
app.log.info('database migrated');
|
|
const admin = await ensureInitialAdmin(
|
|
db,
|
|
config.INITIAL_ADMIN_EMAIL,
|
|
config.INITIAL_ADMIN_PASSWORD,
|
|
);
|
|
if (admin === 'created')
|
|
app.log.info({ email: config.INITIAL_ADMIN_EMAIL }, 'initial admin created');
|
|
await app.listen({ host: '127.0.0.1', port: config.API_PORT });
|
|
|
|
await live.loadFromDb();
|
|
// Every (re)connect: Asterisk may have restarted, so push the full config again, then read its
|
|
// state (after the apply, so hints for new extensions exist). TODO(M6): sync AstDB first (04 §5.5).
|
|
ami.on('connected', () => {
|
|
void (async () => {
|
|
if (await clearRestartIfRestarted(db, ami))
|
|
app.log.info('asterisk restarted: restart no longer required');
|
|
await generator.apply({ trigger: 'startup', force: true });
|
|
await live.loadFromAsterisk();
|
|
await voicemail.scanAll();
|
|
})().catch((err) => app.log.error({ err }, 'startup sync failed'));
|
|
});
|
|
ami.start();
|
|
|
|
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
|
|
process.once(sig, () => {
|
|
generator.cancelPending();
|
|
ami.stop();
|
|
void app
|
|
.close()
|
|
.then(() => pool.end())
|
|
.then(() => process.exit(0));
|
|
});
|
|
}
|