@fonoster/amd 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,91 +1,19 @@
1
1
  amd
2
2
  =================
3
3
 
4
- Standalone Answering Machine Detection sidecar for Fonoster. Runs an ONNX
5
- whisper-tiny classifier over the leading audio of an outbound call and
6
- reports the verdict onto the same `AMDSTATUS`/`AMDCAUSE` Asterisk channel
7
- variables that Asterisk's own native `AMD()` application sets — so nothing
8
- downstream (`apiserver`'s `createCreateVoiceClient.ts` / `mapAsteriskAmd.ts`)
9
- needs to change to consume it.
4
+ Answering-machine detection for Fonoster. An ONNX whisper-tiny classifier reads the first seconds
5
+ of a call's audio and returns a status (HUMAN, MACHINE, VOICEMAIL, IVR or UNKNOWN), a confidence,
6
+ the detector name and the latency.
10
7
 
11
- It runs as its own process/container, decoupled from `apiserver` and
12
- `autopilot`, and can be built, tested, and smoke-tested against a bare
13
- Asterisk box with no other part of the Fonoster stack running.
8
+ AppBridge loads it for the `amd` listener: it attaches a listen-only participant to the room,
9
+ collects the caller's audio (16 kHz L16) and calls `classifyPcm`. The model ships with the package
10
+ in `amd-model/`.
14
11
 
15
- ## How it works
12
+ ```ts
13
+ import { classifyPcm } from "@fonoster/amd";
16
14
 
17
- 1. The dialplan (see `asterisk/config/extensions.conf`'s `AMD_ENGINE` gate)
18
- calls `AGI(agi://amd:4573,${AMD_MODE})`, which blocks the channel.
19
- The argument is the mode feature flag — see below.
20
- 2. Inside that AGI session, this service mints a UUID and runs
21
- `EXEC AudioSocket <uuid>,<host>:<port>`, which blocks until the AudioSocket
22
- side (a second listener in this same process) closes the connection.
23
- 3. The AudioSocket side buffers a few seconds of PCM and classifies it with
24
- the bundled ONNX model — always the classifier's own five-way status
25
- (HUMAN/MACHINE/VOICEMAIL/IVR/UNKNOWN) plus confidence, detector, and
26
- latency; nothing is thrown away yet — then hangs up, which unblocks the
27
- AGI session.
28
- 4. The AGI session maps that raw result to channel variables according to
29
- the requested mode (see below), issues one `SET VARIABLE` per variable
30
- over the same AGI connection, then closes the connection — which is what
31
- tells Asterisk's `AGI()` app to resume the dialplan into
32
- `Stasis(mediacontroller)`, exactly as it does today.
33
-
34
- Every step is fail-open: a missing/corrupt model, a hung inference, a
35
- timeout, or empty audio all resolve to an "unknown" result with a diagnostic
36
- cause (`ML-TIMEOUT`, `ML-ERROR`, `ML-NO-AUDIO`) rather than stalling or
37
- dropping the call.
38
-
39
- ## Modes: compact vs. full
40
-
41
- The mode is a feature flag passed as the AGI call's first dialplan argument
42
- (`agi_arg_1` on the wire) — not an env var, since it's a per-call decision
43
- made in the dialplan, mirroring how `AMD_ENGINE` is already threaded through
44
- as a channel variable.
45
-
46
- - **`compact`** (the default — anything other than the literal `full`,
47
- including no argument at all, falls back to this): collapses the
48
- classifier's five-way status onto the same four-value `AMDSTATUS`
49
- vocabulary Asterisk's native `AMD()` uses (`HUMAN`/`MACHINE`/`NOTSURE`),
50
- with `VOICEMAIL`/`IVR` folded into `MACHINE` and a descriptive `AMDCAUSE`
51
- (e.g. `MACHINE-VOICEMAIL`). Confidence below `AMD_MIN_CONFIDENCE`
52
- is downgraded to `NOTSURE`/`ML-LOW-CONFIDENCE`. Only ever sets
53
- `AMDSTATUS`/`AMDCAUSE` — a consumer built against native AMD() sees no
54
- difference switching engines.
55
- - **`full`**: sets `AMDSTATUS` to the classifier's own status
56
- (`HUMAN`/`MACHINE`/`VOICEMAIL`/`IVR`/`UNKNOWN`, untouched by any
57
- confidence threshold — the consumer applies its own policy), plus
58
- `AMDCONFIDENCE` (0–1), `AMDDETECTOR` (model name), and `AMDLATENCYMS`.
59
-
60
- Set it from the dialplan by giving `AMD_MODE` a value before the `AGI()`
61
- call — apiserver already does this for API-originated calls via
62
- `amd.mode` in `fonoster.yaml` (see below).
63
-
64
- ## Configuration
65
-
66
- The sidecar reads these from its container environment only; it never loads a
67
- `.env` file, in development either. Set them in the container's environment
68
- (Compose `environment:`, a Kubernetes `env:`), or export them before
69
- `npm run start:dev`.
70
-
71
- | env | default | meaning |
72
- |---|---|---|
73
- | `AMD_AGI_PORT` | `4573` | FastAGI listener port |
74
- | `AMD_AUDIOSOCKET_PORT` | `9092` | AudioSocket listener port |
75
- | `AMD_AUDIOSOCKET_BIND_ADDR` | `0.0.0.0` | AudioSocket listener bind address |
76
- | `AMD_AUDIOSOCKET_ADVERTISE_HOST` | `amd` | host Asterisk is told to connect to for the AudioSocket leg — must be reachable *from* Asterisk |
77
- | `AMD_PROBE_MS` | `3000` | leading audio to gather before classifying |
78
- | `AMD_TIMEOUT_MS` | `4000` | hard deadline for the whole AGI session |
79
- | `AMD_MIN_CONFIDENCE` | `0.8` | compact mode only: verdicts below this are reported as `NOTSURE` |
80
- | `AMD_MODEL_PATH` | *(bundled)* | override the model directory |
81
-
82
- ## Enabling it end-to-end
83
-
84
- Set `amd.enabled: true` and `amd.engine: ml` in the API server's
85
- `config/fonoster.yaml`. With the engine unset or set to anything but `ml`,
86
- behavior is unchanged from Asterisk's native `AMD()`. Additionally set
87
- `amd.mode: full` to opt into the richer variable set — omit it (or leave it
88
- `compact`) to stay native-AMD-compatible.
15
+ const verdict = await classifyPcm(pcm);
16
+ ```
89
17
 
90
18
  ## Regression corpus
91
19
 
@@ -106,31 +34,3 @@ few exist on purpose, to track known confusions (e.g. formal/synthesized IVR
106
34
  scripts reading as MACHINE) rather than hide them.
107
35
  Licensing and attribution for every third-party clip is in
108
36
  `test/fixtures/regression/ATTRIBUTION.md`.
109
-
110
- ## Automated verification
111
-
112
- `test/agiAudiosocket.integration.test.ts` starts the real AGI and AudioSocket
113
- listeners in-process, plays both sides of Asterisk's role over raw TCP (the
114
- FastAGI header handshake with `agi_arg_1` set, `EXEC AudioSocket`, streaming
115
- real fixture PCM frame-by-frame, then reading back whatever `SET VARIABLE`
116
- commands the server issues), and asserts the values that arrive over that
117
- wire match calling the classifier directly on the same audio — for both
118
- compact (default) and full mode, plus a case confirming an unrecognized mode
119
- argument falls back to compact. This is the primary proof that the
120
- AGI+AudioSocket mechanism and the mode flag work end-to-end; run it with:
121
-
122
- ```sh
123
- npm run integration
124
- ```
125
-
126
- ## Status after the rebuild
127
-
128
- Asterisk is retired (cleanbreak CB-B6), so the AGI and AudioSocket front end has no caller and its
129
- Asterisk smoke test is gone. The classifier stays: it moves behind a voice-worker listener in
130
- `add-media-node` task 4.3.
131
-
132
- ## Development
133
-
134
- ```sh
135
- npm run start:dev -w @fonoster/amd
136
- ```
package/dist/index.d.ts CHANGED
@@ -16,8 +16,4 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- export * from "./agi/server";
20
- export * from "./audiosocket/server";
21
19
  export * from "./amd/AmdModel";
22
- export * from "./amd/buildAmdVariables";
23
- export * from "./probe/runProbe";
package/dist/index.js CHANGED
@@ -32,8 +32,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
32
  * See the License for the specific language governing permissions and
33
33
  * limitations under the License.
34
34
  */
35
- __exportStar(require("./agi/server"), exports);
36
- __exportStar(require("./audiosocket/server"), exports);
37
35
  __exportStar(require("./amd/AmdModel"), exports);
38
- __exportStar(require("./amd/buildAmdVariables"), exports);
39
- __exportStar(require("./probe/runProbe"), exports);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fonoster/amd",
3
- "version": "0.30.0",
4
- "description": "Standalone answering-machine-detection sidecar for the Fonoster platform",
3
+ "version": "0.31.0",
4
+ "description": "Answering-machine-detection model (ONNX) used by AppBridge",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
7
7
  "license": "MIT",
@@ -14,20 +14,12 @@
14
14
  "scripts": {
15
15
  "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
16
  "build": "tsc -b tsconfig.json",
17
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
- "generate:readme": "node ../../.scripts/gen-readme.cjs",
19
- "start:dev": "nodemon --exec tsx --watch src src/server.ts"
17
+ "generate:readme": "fonoster-readme"
20
18
  },
21
19
  "dependencies": {
22
- "@fonoster/common": "^0.30.0",
23
- "@fonoster/logger": "^0.30.0",
24
- "@fonoster/streams": "^0.30.0",
25
- "@fonoster/types": "^0.30.0",
26
- "onnxruntime-node": "1.21.1",
27
- "uuid": "^11.0.3"
28
- },
29
- "devDependencies": {
30
- "@types/uuid": "^10.0.0"
20
+ "@fonoster/common": "^0.31.0",
21
+ "@fonoster/types": "^0.31.0",
22
+ "onnxruntime-node": "1.30.0"
31
23
  },
32
24
  "files": [
33
25
  "dist",
@@ -43,5 +35,5 @@
43
35
  "bugs": {
44
36
  "url": "https://github.com/fonoster/fonoster/issues"
45
37
  },
46
- "gitHead": "b59b06a3ef325abe3f188c0fb87cc22d50ed5471"
38
+ "gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e"
47
39
  }
@@ -1,77 +0,0 @@
1
- /**
2
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
- * http://github.com/fonoster/fonoster
4
- *
5
- * This file is part of Fonoster
6
- *
7
- * Licensed under the MIT License (the "License");
8
- * you may not use this file except in compliance with
9
- * the License. You may obtain a copy of the License at
10
- *
11
- * https://opensource.org/licenses/MIT
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- import { EventEmitter } from "events";
20
- import * as net from "net";
21
- /**
22
- * Thrown when the channel hangs up (or the socket closes) while a command is
23
- * in flight, or when a command is attempted on an already-hung-up channel.
24
- */
25
- declare class AgiHangupError extends Error {
26
- constructor(message?: string);
27
- }
28
- /**
29
- * One FastAGI session. Minimal, hand-rolled client for the handful of
30
- * commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
31
- * this exists instead of a dependency: the only maintained npm package for
32
- * this protocol (`asteriskagi`) shipped a broken `require()` in its latest
33
- * release at the time this was written.
34
- *
35
- * The AGI protocol is a simple line-based request/response: write one
36
- * command line, read back one `200 result=...` line. Asterisk may also send
37
- * an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
38
- */
39
- declare class AgiChannel extends EventEmitter {
40
- readonly channel: string;
41
- readonly uniqueid: string;
42
- readonly variables: Record<string, string>;
43
- private socket;
44
- private buffer;
45
- private ended;
46
- private pending?;
47
- constructor(props: {
48
- socket: net.Socket;
49
- variables: Record<string, string>;
50
- });
51
- get hungup(): boolean;
52
- /**
53
- * Dialplan arguments passed after the AGI URI, e.g.
54
- * `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
55
- * here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
56
- * local script.
57
- */
58
- get args(): string[];
59
- private onData;
60
- private resolvePending;
61
- private end;
62
- /** Sends one raw FastAGI command line and waits for its response. */
63
- send(command: string): Promise<void>;
64
- /** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
65
- exec(application: string, args?: string): Promise<void>;
66
- setVariable(name: string, value: string): Promise<void>;
67
- /**
68
- * Ends the AGI session cleanly once this side is done issuing commands.
69
- * This — not a special "done" command — is how Asterisk's AGI() app knows
70
- * to return control to the dialplan: it detects the AGI script closing the
71
- * connection. Without this, a real channel would sit in AGI() forever.
72
- * Distinct from `end()`'s hangup semantics: this is *us* finishing, not
73
- * the far end going away, so it doesn't emit "hangup".
74
- */
75
- close(): void;
76
- }
77
- export { AgiChannel, AgiHangupError };
@@ -1,171 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AgiHangupError = exports.AgiChannel = void 0;
4
- /**
5
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
- * http://github.com/fonoster/fonoster
7
- *
8
- * This file is part of Fonoster
9
- *
10
- * Licensed under the MIT License (the "License");
11
- * you may not use this file except in compliance with
12
- * the License. You may obtain a copy of the License at
13
- *
14
- * https://opensource.org/licenses/MIT
15
- *
16
- * Unless required by applicable law or agreed to in writing, software
17
- * distributed under the License is distributed on an "AS IS" BASIS,
18
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
- * See the License for the specific language governing permissions and
20
- * limitations under the License.
21
- */
22
- const events_1 = require("events");
23
- /**
24
- * Thrown when the channel hangs up (or the socket closes) while a command is
25
- * in flight, or when a command is attempted on an already-hung-up channel.
26
- */
27
- class AgiHangupError extends Error {
28
- constructor(message = "AGI channel hung up") {
29
- super(message);
30
- this.name = "AgiHangupError";
31
- }
32
- }
33
- exports.AgiHangupError = AgiHangupError;
34
- /**
35
- * One FastAGI session. Minimal, hand-rolled client for the handful of
36
- * commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
37
- * this exists instead of a dependency: the only maintained npm package for
38
- * this protocol (`asteriskagi`) shipped a broken `require()` in its latest
39
- * release at the time this was written.
40
- *
41
- * The AGI protocol is a simple line-based request/response: write one
42
- * command line, read back one `200 result=...` line. Asterisk may also send
43
- * an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
44
- */
45
- class AgiChannel extends events_1.EventEmitter {
46
- constructor(props) {
47
- var _a, _b;
48
- super();
49
- this.buffer = "";
50
- this.ended = false;
51
- this.onData = (data) => {
52
- if (this.ended)
53
- return;
54
- this.buffer += data.toString();
55
- let newline = this.buffer.indexOf("\n");
56
- while (newline >= 0) {
57
- const line = this.buffer.slice(0, newline).replace(/\r$/, "").trim();
58
- this.buffer = this.buffer.slice(newline + 1);
59
- if (line === "HANGUP") {
60
- this.end();
61
- return;
62
- }
63
- if (line)
64
- this.resolvePending(line);
65
- newline = this.buffer.indexOf("\n");
66
- }
67
- };
68
- this.socket = props.socket;
69
- this.variables = props.variables;
70
- this.channel = (_a = props.variables.channel) !== null && _a !== void 0 ? _a : "";
71
- this.uniqueid = (_b = props.variables.uniqueid) !== null && _b !== void 0 ? _b : "";
72
- this.socket.on("data", this.onData);
73
- this.socket.once("end", () => this.end());
74
- this.socket.once("close", () => this.end());
75
- this.socket.once("error", (err) => this.end(err));
76
- }
77
- get hungup() {
78
- return this.ended;
79
- }
80
- /**
81
- * Dialplan arguments passed after the AGI URI, e.g.
82
- * `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
83
- * here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
84
- * local script.
85
- */
86
- get args() {
87
- const result = [];
88
- for (let i = 1; this.variables[`arg_${i}`] !== undefined; i++) {
89
- result.push(this.variables[`arg_${i}`]);
90
- }
91
- return result;
92
- }
93
- resolvePending(line) {
94
- const pending = this.pending;
95
- if (!pending)
96
- return;
97
- this.pending = undefined;
98
- // "200 result=1 ..." on success; "5xx ..." or a negative result on
99
- // failure — either way this only needs to settle the promise, not parse
100
- // the (unused) payload.
101
- const match = line.match(/^\d+\s+result=(-?\d+)/);
102
- const result = match ? Number(match[1]) : NaN;
103
- if (line.startsWith("2") && !(Number.isFinite(result) && result < 0)) {
104
- pending.resolve();
105
- }
106
- else {
107
- pending.reject(new Error(`AGI command failed: ${line}`));
108
- }
109
- }
110
- end(cause) {
111
- if (this.ended)
112
- return;
113
- this.ended = true;
114
- this.socket.off("data", this.onData);
115
- const pending = this.pending;
116
- this.pending = undefined;
117
- pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError());
118
- if (!this.socket.destroyed)
119
- this.socket.end();
120
- this.emit("hangup", cause);
121
- }
122
- /** Sends one raw FastAGI command line and waits for its response. */
123
- send(command) {
124
- if (this.ended)
125
- return Promise.reject(new AgiHangupError());
126
- if (this.pending) {
127
- return Promise.reject(new Error("An AGI command is already in progress"));
128
- }
129
- return new Promise((resolve, reject) => {
130
- this.pending = { resolve, reject };
131
- if (!this.socket.writable || this.socket.destroyed) {
132
- this.end();
133
- return;
134
- }
135
- this.socket.write(command + "\n", "utf8", (err) => {
136
- if (err)
137
- this.end(err);
138
- });
139
- });
140
- }
141
- /** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
142
- exec(application, args) {
143
- return this.send(`EXEC ${application} ${args !== null && args !== void 0 ? args : ""}`);
144
- }
145
- setVariable(name, value) {
146
- return this.send(`SET VARIABLE ${name} "${value}"`);
147
- }
148
- /**
149
- * Ends the AGI session cleanly once this side is done issuing commands.
150
- * This — not a special "done" command — is how Asterisk's AGI() app knows
151
- * to return control to the dialplan: it detects the AGI script closing the
152
- * connection. Without this, a real channel would sit in AGI() forever.
153
- * Distinct from `end()`'s hangup semantics: this is *us* finishing, not
154
- * the far end going away, so it doesn't emit "hangup".
155
- */
156
- close() {
157
- if (this.ended)
158
- return;
159
- this.ended = true;
160
- this.socket.off("data", this.onData);
161
- // A command can still be in flight (e.g. the caller gave up on it via an
162
- // external race/timeout rather than waiting for a response) — settle it
163
- // so that awaiter doesn't hang forever, same as end()'s hangup path.
164
- const pending = this.pending;
165
- this.pending = undefined;
166
- pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError("AGI session closed locally"));
167
- if (!this.socket.destroyed)
168
- this.socket.end();
169
- }
170
- }
171
- exports.AgiChannel = AgiChannel;
@@ -1,34 +0,0 @@
1
- /**
2
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
- * http://github.com/fonoster/fonoster
4
- *
5
- * This file is part of Fonoster
6
- *
7
- * Licensed under the MIT License (the "License");
8
- * you may not use this file except in compliance with
9
- * the License. You may obtain a copy of the License at
10
- *
11
- * https://opensource.org/licenses/MIT
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- import { EventEmitter } from "events";
20
- /**
21
- * Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
22
- * block of `agi_*` header lines terminated by a blank line, then this emits
23
- * "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
24
- * on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
25
- */
26
- declare class AgiServer extends EventEmitter {
27
- private server;
28
- constructor(props: {
29
- port: number;
30
- });
31
- private handleConnection;
32
- close(): void;
33
- }
34
- export { AgiServer };
@@ -1,109 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AgiServer = void 0;
37
- /**
38
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
39
- * http://github.com/fonoster/fonoster
40
- *
41
- * This file is part of Fonoster
42
- *
43
- * Licensed under the MIT License (the "License");
44
- * you may not use this file except in compliance with
45
- * the License. You may obtain a copy of the License at
46
- *
47
- * https://opensource.org/licenses/MIT
48
- *
49
- * Unless required by applicable law or agreed to in writing, software
50
- * distributed under the License is distributed on an "AS IS" BASIS,
51
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52
- * See the License for the specific language governing permissions and
53
- * limitations under the License.
54
- */
55
- const events_1 = require("events");
56
- const net = __importStar(require("net"));
57
- const AgiChannel_1 = require("./AgiChannel");
58
- /**
59
- * Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
60
- * block of `agi_*` header lines terminated by a blank line, then this emits
61
- * "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
62
- * on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
63
- */
64
- class AgiServer extends events_1.EventEmitter {
65
- constructor(props) {
66
- super();
67
- this.server = net.createServer(this.handleConnection.bind(this));
68
- this.server.on("error", (err) => this.emit("error", err));
69
- this.server.listen(props.port, () => this.emit("ready", props.port));
70
- }
71
- handleConnection(socket) {
72
- let buffer = "";
73
- const onData = (data) => {
74
- buffer += data.toString();
75
- const delimiter = buffer.match(/\r?\n\r?\n/);
76
- if (!delimiter || delimiter.index === undefined)
77
- return;
78
- socket.off("data", onData);
79
- socket.off("error", onError);
80
- const header = buffer.slice(0, delimiter.index);
81
- const remainder = buffer.slice(delimiter.index + delimiter[0].length);
82
- const variables = {};
83
- header.split(/\r?\n/).forEach((line) => {
84
- if (!line.startsWith("agi_"))
85
- return;
86
- const separator = line.indexOf(":");
87
- if (separator < 0)
88
- return;
89
- variables[line.slice(4, separator).trim()] = line
90
- .slice(separator + 1)
91
- .trim();
92
- });
93
- const channel = new AgiChannel_1.AgiChannel({ socket, variables });
94
- this.emit("call", channel);
95
- if (remainder)
96
- socket.unshift(Buffer.from(remainder));
97
- };
98
- const onError = (err) => {
99
- if (this.listenerCount("error"))
100
- this.emit("error", err);
101
- };
102
- socket.once("error", onError);
103
- socket.on("data", onData);
104
- }
105
- close() {
106
- this.server.close();
107
- }
108
- }
109
- exports.AgiServer = AgiServer;
@@ -1,16 +0,0 @@
1
- import { AgiServer } from "./AgiServer";
2
- /**
3
- * Starts the FastAGI listener. Asterisk's `AGI(agi://amd:4573,${AMD_MODE})`
4
- * blocks the channel here. Per session: read the requested mode from the
5
- * dialplan's first AGI argument (`compact`, the default, or `full`), mint a
6
- * UUID, EXEC AudioSocket to pull the leading audio (blocks until the
7
- * AudioSocket side hangs up), await the correlated raw result, map it to
8
- * however many channel variables that mode produces, SET each, then return
9
- * so the dialplan resumes at Stasis(mediacontroller).
10
- *
11
- * Always fail-open: deadlines bound both the wait for a verdict and the wait
12
- * for AudioSocket() to return, and the AGI connection is always closed so the
13
- * dialplan resumes even when no variables could be set.
14
- */
15
- declare function startAgiServer(): AgiServer;
16
- export { startAgiServer };