Skip to content

Commit 34d59c3

Browse files
committed
docs: add README documentation
enhance: AMI event handling with dynamic event emission
1 parent f1777e5 commit 34d59c3

File tree

6 files changed

+321
-4
lines changed

6 files changed

+321
-4
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

src/ami.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export declare interface AMI {
2929
listener: (response: Response) => void
3030
): this;
3131
on(event: AmiEvents.RawEvent, listener: (event: Event) => void): this;
32+
on(event: string, listener: (event: Event) => void): this;
3233
}
3334

3435
/**
@@ -133,6 +134,7 @@ export class AMI extends EventEmitter {
133134
}
134135

135136
this.emit(AmiEvents.Event, event);
137+
this.emit(event.name + "Event", event);
136138
}
137139

138140
/**
@@ -300,7 +302,7 @@ export class AMI extends EventEmitter {
300302
});
301303

302304
this.socket.on("close", (hadError: boolean) => {
303-
this.logger.debug("Socket closed");
305+
this.logger.debug("Socket closed, hadError: " + hadError);
304306
this.connected = false;
305307
this.emit(AmiSocketEvents.Closed, hadError);
306308
});

src/event.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Message } from "./message";
88
* @extends Message
99
*/
1010
export class Event extends Message {
11+
/** The name of the event (e.g., "Hangup", "Dial", "Registry") */
12+
name!: string;
1113
/** The type of event (e.g., "Hangup", "Dial", "Registry") */
1214
event!: string;
1315
/** Indicates if this event is part of an event list and its status */
@@ -33,8 +35,7 @@ export class Event extends Message {
3335
*/
3436
completed(): boolean {
3537
return (
36-
new RegExp(`Complete`, "i").test(this.event) ||
37-
this.eventlistCompleted()
38+
new RegExp(`Complete`, "i").test(this.event) || this.eventlistCompleted()
3839
);
3940
}
4041

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ export * from "./event";
1717
export * from "./message";
1818
export * from "./response";
1919
export * as actions from "./actions";
20+
export * from "./enums";
21+
export * from "./interfaces";

src/message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,7 @@ export class Message {
9797
this[keySafe] = valueSafe;
9898
}
9999
}
100+
101+
this.name = this.event;
100102
}
101103
}

test/ami.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,13 @@ describe("AMI", () => {
159159
});
160160

161161
test("onRawEvent should emit Event if not associated with a response", () => {
162-
const event = new Event("Event: TestEvent\r\nSome: Value");
162+
const event = new Event("Event: Test\r\nSome: Value");
163163
const spy = jest.spyOn(ami, "emit");
164164

165165
(ami as any).onRawEvent(event);
166166

167167
expect(spy).toHaveBeenCalledWith(AmiEvents.Event, event);
168+
expect(spy).toHaveBeenCalledWith("TestEvent", event);
168169
});
169170

170171
test("onRawResponse should buffer response if more events will follow", () => {

0 commit comments

Comments
 (0)