Skip to content

Commit 9cf10e0

Browse files
authored
Merge pull request #1 from civilcoder55/feat/main-functionalities
feat: implement Asterisk Manager Interface (AMI) client with core functionality including actions, events, and responses
2 parents 5eb16b9 + 34d59c3 commit 9cf10e0

29 files changed

+2039
-10
lines changed

README.md

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
# TSAMI
2+
3+
[![npm version](https://img.shields.io/npm/v/tsami.svg)](https://www.npmjs.com/package/tsami)
4+
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5+
[![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue)](https://www.typescriptlang.org/)
6+
7+
A TypeScript implementation of the Asterisk Manager Interface (AMI) client for Node.js, inspired by nami.
8+
9+
## Description
10+
11+
TSAMI is a robust, fully typed client library for interacting with the Asterisk PBX through the Asterisk Manager Interface protocol. It provides an elegant event-driven API for performing AMI operations, handling events, and managing connections in modern TypeScript applications.
12+
13+
## Features
14+
15+
- **Fully typed**: Complete TypeScript definitions for all AMI interactions
16+
- **Event-driven architecture**: Based on Node.js EventEmitter
17+
- **Promise support**: Modern async/await API
18+
- **Connection management**: Handles automatic reconnection and error recovery
19+
- **Built-in actions**: Includes common AMI actions like Login, Logoff, Ping, etc.
20+
- **Custom actions support**: Create custom AMI actions with ease
21+
- **Comprehensive event handling**: Process AMI events with typed handlers
22+
- **Robust error handling**: Detailed error reporting and logging
23+
- **Production-ready**: Buffer overflow protection and proper resource cleanup
24+
25+
## Installation
26+
27+
```bash
28+
# Using npm
29+
npm install tsami
30+
31+
# Using yarn
32+
yarn add tsami
33+
```
34+
35+
## Usage
36+
37+
### Basic Connection
38+
39+
```typescript
40+
import { AMI, AmiEvents, AmiSocketEvents } from "tsami";
41+
42+
(async function main() {
43+
// Create AMI client
44+
const ami = new AMI({
45+
host: "localhost",
46+
port: 5038,
47+
username: "admin",
48+
password: "secret",
49+
logLevel: 2, // 0: none, 1: error, 2: warn, 3: info, 4: debug
50+
});
51+
52+
// Set up event handlers
53+
ami.on(AmiSocketEvents.Connected, () => {
54+
console.log("Socket connected");
55+
});
56+
57+
ami.on(AmiSocketEvents.Error, (error) => {
58+
console.error("Socket error:", error);
59+
});
60+
61+
ami.on(AmiEvents.Connected, () => {
62+
console.log("Successfully authenticated with Asterisk");
63+
});
64+
65+
ami.on(AmiEvents.Event, (event) => {
66+
console.log("Received event:", event.name);
67+
});
68+
69+
// Connect to Asterisk server
70+
ami.open();
71+
})();
72+
73+
// Close connection when done
74+
async function shutdown() {
75+
await ami.close();
76+
console.log("Connection closed");
77+
}
78+
```
79+
80+
### Using Built-in Actions
81+
82+
```typescript
83+
import { AMI, AmiEvents, actions } from "tsami";
84+
85+
(async function main() {
86+
const ami = new AMI({
87+
host: "localhost",
88+
port: 5038,
89+
username: "admin",
90+
password: "secret",
91+
});
92+
93+
try {
94+
ami.open();
95+
96+
// Wait for successful login
97+
await new Promise((resolve) => ami.once(AmiEvents.Connected, resolve));
98+
99+
// Get channel information
100+
const response = await ami.asyncSend(new actions.CoreShowChannels());
101+
console.log("Active channels:", response.events.length - 1);
102+
103+
// Loop through channels
104+
response.events.slice(0, response.events.length - 1).forEach((event) => {
105+
console.log(
106+
`Channel: ${event.channel} - State: ${event.channelstatestr}`
107+
);
108+
});
109+
} catch (err) {
110+
console.error("Error:", err);
111+
} finally {
112+
await ami.close();
113+
}
114+
})();
115+
```
116+
117+
### Using Custom Actions: you can send any action to the Asterisk server by creating a custom action instance and pass any parameters you want.
118+
119+
```typescript
120+
import { AMI, AmiEvents actions } from "tsami";
121+
122+
(async function main() {
123+
const ami = new AMI({
124+
host: "localhost",
125+
port: 5038,
126+
username: "admin",
127+
password: "secret",
128+
});
129+
130+
try {
131+
ami.open();
132+
await new Promise((resolve) => ami.once(AmiEvents.Connected, resolve));
133+
134+
// Create a custom action
135+
const dialAction = new actions.CustomAction("Originate", {
136+
Channel: "SIP/1000",
137+
Context: "default",
138+
Exten: "1001",
139+
Priority: 1,
140+
Callerid: "TSAMI <1234>",
141+
Timeout: 30000,
142+
});
143+
144+
const response = await ami.asyncSend(dialAction);
145+
console.log("Originate response:", response.response);
146+
} catch (err) {
147+
console.error("Error:", err);
148+
} finally {
149+
await ami.close();
150+
}
151+
})();
152+
```
153+
154+
## API Reference
155+
156+
### Main Classes
157+
158+
#### AMI
159+
160+
The primary client class for AMI connections.
161+
162+
```typescript
163+
// Constructor
164+
const ami = new AMI(options: AmiClientOptions);
165+
166+
// Methods
167+
ami.open(): void; // Open the connection
168+
ami.close(): Promise<void>; // Close the connection
169+
ami.send(action: Action, callback: Function); // Send an action with callback
170+
ami.asyncSend(action: Action): Promise<Response>; // Send an action, returns Promise
171+
```
172+
173+
#### Events
174+
175+
The AMI client emits various events during operation:
176+
177+
```typescript
178+
// TCP Socket Events
179+
ami.on(AmiSocketEvents.Connected, () => {}); // Socket connected
180+
ami.on(AmiSocketEvents.Error, (error) => {}); // Socket error
181+
ami.on(AmiSocketEvents.Closed, (hadError) => {}); // Socket closed
182+
ami.on(AmiSocketEvents.Timeout, () => {}); // Socket timeout
183+
ami.on(AmiSocketEvents.Ended, () => {}); // Socket ended
184+
185+
// AMI Events
186+
ami.on(AmiEvents.Connected, () => {}); // Authenticated with AMI
187+
ami.on(AmiEvents.Event, (event) => {}); // Received AMI event
188+
ami.on(AmiEvents.LoginIncorrect, () => {}); // Authentication failed
189+
ami.on(AmiEvents.RawMessage, (msg) => {}); // Raw AMI message
190+
ami.on(AmiEvents.RawEvent, (event) => {}); // Raw AMI event before processing
191+
ami.on(AmiEvents.RawResponse, (response) => {}); // Raw AMI response before processing
192+
ami.on(AmiEvents.InvalidPeer, () => {}); // Invalid AMI socket connection
193+
```
194+
195+
### Configuration Options
196+
197+
```typescript
198+
interface AmiClientOptions {
199+
host: string; // Asterisk server hostname/IP
200+
port: number; // AMI port (default: 5038)
201+
username: string; // AMI username
202+
password: string; // AMI password
203+
logLevel?: number; // Log level (0: error, 1: warn, 2: info, 3: debug, -1: none) (default: 0)
204+
logger?: Logger; // Custom logger implementation
205+
}
206+
```
207+
208+
## Examples
209+
210+
### Listening for Specific Events
211+
212+
```typescript
213+
import { AMI } from "tsami";
214+
215+
(async function main() {
216+
const ami = new AMI({
217+
host: "localhost",
218+
port: 5038,
219+
username: "admin",
220+
password: "secret",
221+
});
222+
223+
// Listen for hangup events
224+
ami.on("HangupEvent", (event) => {
225+
console.log(
226+
`Call ended on channel ${event.channel} with cause: ${event.cause}`
227+
);
228+
});
229+
230+
// Listen for new channels
231+
ami.on("NewchannelEvent", (event) => {
232+
console.log(
233+
`New channel ${event.channel} created with state ${event.channelstatestr}`
234+
);
235+
});
236+
237+
ami.open();
238+
})();
239+
```
240+
241+
### Handling Reconnection
242+
243+
```typescript
244+
import { AMI, AmiEvents, AmiSocketEvents } from "tsami";
245+
246+
(async function main() {
247+
const ami = new AMI({
248+
host: "localhost",
249+
port: 5038,
250+
username: "admin",
251+
password: "root",
252+
});
253+
254+
ami.on(AmiSocketEvents.Closed, (hadError) => {
255+
console.log(`Connection closed${hadError ? " with error" : ""}`);
256+
257+
if (hadError) {
258+
ami.open();
259+
}
260+
});
261+
262+
ami.on(AmiEvents.Connected, () => {
263+
console.log("Successfully connected and authenticated");
264+
});
265+
266+
ami.open();
267+
})();
268+
```
269+
270+
## Best Practices
271+
272+
1. **Always close connections** when they're no longer needed using `await ami.close()`
273+
2. **Handle reconnection** gracefully for production applications
274+
3. **Use try/catch blocks** with async/await for proper error handling
275+
4. **Set appropriate log levels** based on your environment (lower in production)
276+
5. **Subscribe to relevant events** rather than processing all AMI events
277+
6. **Implement rate limiting** for actions to avoid overwhelming the Asterisk server
278+
7. **Use typed events** for more maintainable code with TypeScript
279+
280+
## Testing
281+
282+
Run the test suite with:
283+
284+
```bash
285+
npm test
286+
```
287+
288+
## Contributing
289+
290+
Contributions are welcome! Please feel free to submit a Pull Request.
291+
292+
1. Fork the repository
293+
2. Create your feature branch (`git checkout -b feat/amazing-feature`)
294+
3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
295+
4. Push to the branch (`git push origin feat/amazing-feature`)
296+
5. Open a Pull Request
297+
298+
## License
299+
300+
This project is licensed under the ISC License - see the LICENSE file for details.
301+
302+
## Changelog
303+
304+
See the [releases page](https://github.com/civilcoder55/tsami/releases) for a list of version changes.
305+
306+
## Contact/Support
307+
308+
- Create an [issue](https://github.com/civilcoder55/tsami/issues) for bug reports or feature requests
309+
- Reach out to the [author](https://github.com/civilcoder55) for questions or support

jest.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Config } from "@jest/types";
2+
3+
const config: Config.InitialOptions = {
4+
verbose: true,
5+
transform: {
6+
"^.+\\.tsx?$": "ts-jest",
7+
},
8+
};
9+
export default config;

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"dist"
1010
],
1111
"scripts": {
12-
"build": "tsup"
12+
"build": "tsup",
13+
"watch": "tsup --watch",
14+
"test": "jest"
1315
},
1416
"keywords": [
1517
"asterisk",

src/action.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Message } from "./message";
2+
3+
/**
4+
* Action ID generator function.
5+
* Implements a closure to maintain a sequential counter for generating unique Action IDs.
6+
* Ensures each action sent to Asterisk AMI has a unique identifier.
7+
*/
8+
const ActionUniqueId = (function () {
9+
let nextId = 0;
10+
return function () {
11+
return nextId++;
12+
};
13+
})();
14+
15+
/**
16+
* Base action class for all AMI actions.
17+
* Represents a command to be sent to the Asterisk Manager Interface.
18+
* Every action sent to AMI extends this class to implement specific AMI operations.
19+
* Automatically assigns a unique ActionID to each instance for tracking responses.
20+
*
21+
* @extends Message
22+
*/
23+
export class Action extends Message {
24+
/** ActionID field for AMI - string representation of the ID */
25+
ActionID!: string;
26+
/** Internal unique identifier for tracking */
27+
protected id: number;
28+
/** The name of the action to be performed by AMI */
29+
protected Action!: string;
30+
31+
/**
32+
* Creates a new Action with the specified name.
33+
* Automatically assigns a unique Action ID.
34+
*
35+
* @param {String} name - The action name (e.g., "Login", "Status", "Ping")
36+
*/
37+
constructor(name: string) {
38+
super();
39+
this.id = ActionUniqueId();
40+
this.ActionID = String(this.id);
41+
this.Action = name;
42+
}
43+
}

src/actions/CoreShowChannels.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Action } from "../action";
2+
3+
/**
4+
* CoreShowChannels Action for listing all active channels.
5+
* Provides detailed information about all currently active channels in the system.
6+
*
7+
* @extends Action
8+
*/
9+
export class CoreShowChannels extends Action {
10+
/**
11+
* Creates a new CoreShowChannels action.
12+
*/
13+
constructor() {
14+
super("CoreShowChannels");
15+
}
16+
}

0 commit comments

Comments
 (0)