Skip to content

Commit 7958b17

Browse files
authored
Merge pull request #3 from sipgate-io/feat/webhookServer
merge: webhook server feature into dev
2 parents d12af78 + b3b9ede commit 7958b17

File tree

11 files changed

+850
-256
lines changed

11 files changed

+850
-256
lines changed

README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,247 @@ async function setLog(value: boolean): Promise<void>;
319319
The `setLog` function toggles, the function to display all incoming and outgoing events, which have been sent to your `Incoming` and `Outgoing` Url.
320320
These parameters can be set using these functions: `setIncomingUrl` and `setOutgoingUrl`.
321321

322+
### Webhooks (PUSH-API)
323+
324+
The webhook module provides the following features:
325+
326+
- subscribing to **newCall** events
327+
- replying to **newCall** events with XML
328+
- subscribing to **answer** events
329+
- subscribing to **data** events
330+
- subscribing to **hangup** events
331+
332+
**Please note:** The feature is only available in node.js environments and not available in browser environments
333+
334+
#### Structure
335+
336+
```typescript
337+
interface WebhookModule {
338+
createServer: (port: number) => Promise<WebhookServer>;
339+
}
340+
341+
type HandlerCallback<T, U> = (event: T) => U;
342+
343+
interface WebhookServer {
344+
onNewCall: (fn: HandlerCallback<NewCallEvent, string>) => void;
345+
onAnswer: (fn: HandlerCallback<AnswerEvent, void>) => void;
346+
onHangup: (fn: HandlerCallback<HangupEvent, void>) => void;
347+
onData: (fn: HandlerCallback<DataEvent, void>) => void;
348+
stop: () => void;
349+
}
350+
```
351+
352+
#### Creating the webhook server
353+
354+
By passing a `port` to the `createServer` method, you receive a `Promise<WebhookServer>`.
355+
After the server has been instantiated, you can subscribe to various `Events` (`NewCallEvent`,`AnswerEvent`,`HangupEvent`,`DataEvent`) which are described below.
356+
357+
#### Subscribing to _newCall_ events
358+
359+
After creating the server, you can subscribe to newCall events by passing a callback function to the `.onNewCall` method. This callback function will receive a `NewCallEvent` (described below) when called and expects a valid XML response to be returned.
360+
To receive any further `Events`, you can subscribe to them with the following XML:
361+
**Keep in mind:** you have to replace `https://www.sipgate.de/` with your server URL
362+
363+
```xml
364+
<?xml version="1.0" encoding="UTF-8"?>
365+
<Response onAnswer="https://www.sipgate.de/" onHangup="https://www.sipgate.de/">
366+
</Response>
367+
```
368+
369+
```typescript
370+
enum Direction {
371+
IN = 'in',
372+
OUT = 'out',
373+
}
374+
375+
interface NewCallEvent {
376+
event: EventType;
377+
callId: string;
378+
direction: Direction;
379+
from: string;
380+
to: string;
381+
xcid: string;
382+
event: EventType.NEW_CALL;
383+
originalCallId: string;
384+
user: string[];
385+
userId: string[];
386+
fullUserId: string[];
387+
}
388+
```
389+
390+
#### Replying to _newCall_ events with valid XML
391+
392+
You can return different `XML-Responses` in your callback, which will be passed to the PUSH-API:
393+
394+
##### Redirecting a call:
395+
396+
You can redirect the call to a specific phone number using the following XML:
397+
398+
```xml
399+
<?xml version="1.0" encoding="UTF-8"?>
400+
<Response>
401+
<Dial>
402+
<Number>4915799912345</Number>
403+
</Dial>
404+
</Response>
405+
```
406+
407+
##### Sending a call to the voicemail:
408+
409+
Redirecting a call to the voicemail can be achieved by using the following XML snippet:
410+
411+
```xml
412+
<?xml version="1.0" encoding="UTF-8"?>
413+
<Response>
414+
<Dial>
415+
<Voicemail />
416+
</Dial>
417+
</Response>
418+
```
419+
420+
##### Supressing your phone number and redirecting the call
421+
422+
The snippet mentioned below supresses your phone number and redirects you to a different number:
423+
424+
```xml
425+
<?xml version="1.0" encoding="UTF-8"?>
426+
<Response>
427+
<Dial anonymous="true">
428+
<Number>4915799912345</Number>
429+
</Dial>
430+
</Response>
431+
```
432+
433+
##### Set custom callerId and redirect the call
434+
435+
The custom `callerId` can be set to any validated number in your sipgate account:
436+
437+
```xml
438+
<?xml version="1.0" encoding="UTF-8"?>
439+
<Response>
440+
<Dial callerId="492111234567">
441+
<Number>4915799912345</Number>
442+
</Dial>
443+
</Response>
444+
```
445+
446+
##### Playing a sound file
447+
448+
**Please note:** Currently the sound file needs to be a mono 16bit PCM WAV file with a sampling rate of 8kHz. You can use conversion tools like the open source audio editor Audacity to convert any sound file to the correct format.
449+
450+
```xml
451+
<?xml version="1.0" encoding="UTF-8"?>
452+
<Response>
453+
<Play>
454+
<Url>http://example.com/example.wav</Url>
455+
</Play>
456+
</Response>
457+
```
458+
459+
##### Gathering DTMF sounds
460+
461+
**Please note:** If you want to gather DTMF sounds, no future `onAnswer` and `onHangup` events will be pushed for the specific call.
462+
463+
```xml
464+
<?xml version="1.0" encoding="UTF-8"?>
465+
<Response>
466+
<Gather onData="http://localhost:3000/dtmf" maxDigits="3" timeout="10000">
467+
<Play>
468+
<Url>https://example.com/example.wav</Url>
469+
</Play>
470+
</Gather>
471+
</Response>
472+
```
473+
474+
##### Rejecting a call
475+
476+
```xml
477+
<?xml version="1.0" encoding="UTF-8"?>
478+
<Response>
479+
<Reject />
480+
</Response>
481+
```
482+
483+
##### Rejecting a call like you are busy
484+
485+
```xml
486+
<?xml version="1.0" encoding="UTF-8"?>
487+
<Response>
488+
<Reject reason="busy"/>
489+
</Response>
490+
```
491+
492+
##### Hangup calls
493+
494+
```xml
495+
<?xml version="1.0" encoding="UTF-8"?>
496+
<Response>
497+
<Hangup />
498+
</Response>
499+
```
500+
501+
#### Subscribing to _onAnswer_ events
502+
503+
After creating the server, you can subscribe to onAnswer events by passing a callback function to the `.onAnswer` method. This callback function will receive a `AnswerEvent` (described below) when called and expects nothing to be returned.
504+
To receive this event you have to subscribe to them with the XML mentioned in [Subscribing to **newCall** Events](#subscribing-to-newcall-events)
505+
506+
```typescript
507+
interface AnswerEvent {
508+
callId: string;
509+
direction: Direction;
510+
from: string;
511+
to: string;
512+
xcid: string;
513+
event: EventType.ANSWER;
514+
user: string;
515+
userId: string;
516+
fullUserId: string;
517+
answeringNumber: string;
518+
diversion?: string;
519+
}
520+
```
521+
522+
#### Subscribing to _data_ events
523+
524+
After creating the server, you can subscribe to onData events by passing a callback function to the `.onData` method. This callback function will receive a `DataEvent` (described below) when called and expects nothing to be returned.
525+
To receive this event you have to subscribe to them with the XML mentioned in [Subscribing to **newCall** Events](#subscribing-to-newcall-events)
526+
527+
```typescript
528+
interface DataEvent {
529+
callId: string;
530+
event: EventType.DATA;
531+
dtmf: string;
532+
}
533+
```
534+
535+
#### Subscribing to _hangup_ events
536+
537+
After creating the server, you can subscribe to onHangup events by passing a callback function to the `.onHangup` method. This callback function will receive a `HangupEvent` (described below) when called and expects nothing to be returned.
538+
To receive this event you have to subscribe to them with the XML mentioned in [Subscribing to **newCall** Events](#subscribing-to-newcall-events)
539+
540+
```typescript
541+
enum HangupCause {
542+
NORMAL_CLEARING = 'normalClearing',
543+
BUSY = 'busy',
544+
CANCEL = 'cancel',
545+
NO_ANSWER = 'noAnswer',
546+
CONGESTION = 'congestion',
547+
NOT_FOUND = 'notFound',
548+
FORWARDED = 'forwarded',
549+
}
550+
551+
interface HangupEvent {
552+
callId: string;
553+
direction: Direction;
554+
from: string;
555+
to: string;
556+
xcid: string;
557+
event: EventType.HANGUP;
558+
cause: HangupCause;
559+
answeringNumber: string;
560+
}
561+
```
562+
322563
### Contacts
323564

324565
The contacts module provides the following functions:

lib/contacts/contacts.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-unused-vars */
12
import { ContactsDTO, ContactsModule } from './contacts.module';
23
import { ErrorMessage } from './errors/ErrorMessage';
34
import { HttpClientModule } from '../core/httpClient';
@@ -20,8 +21,8 @@ describe('Contacts Module', () => {
2021
mockClient = {} as HttpClientModule;
2122
mockClient.post = jest
2223
.fn()
23-
.mockImplementation((_, contactsDTO: ContactsDTO) => {
24-
console.log(contactsDTO);
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
.mockImplementation((_, _contactsDTO: ContactsDTO) => {
2526
return Promise.resolve({
2627
status: 204,
2728
});
@@ -319,18 +320,27 @@ describe('Export Contacts as CSV', () => {
319320

320321
beforeEach(() => {
321322
mockClient = {} as HttpClientModule;
323+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
324+
mockClient.get = jest.fn().mockImplementationOnce(_ => {
325+
return Promise.resolve({
326+
data: {
327+
items: [],
328+
},
329+
status: 200,
330+
});
331+
});
322332
mockClient.post = jest
323333
.fn()
324-
.mockImplementation((_, contactsDTO: ContactsDTO) => {
325-
console.log(contactsDTO);
334+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
335+
.mockImplementation((_, _contactsDTO: ContactsDTO) => {
326336
return Promise.resolve({
327337
status: 204,
328338
});
329339
});
330340
contactsModule = createContactsModule(mockClient);
331341
});
332-
it('returns a csv by using scope', () => {
333-
expect(() => contactsModule.exportAsCsv('PRIVATE')).not.toThrowError();
342+
it('returns a csv by using scope', async () => {
343+
await expect(contactsModule.exportAsCsv('PRIVATE')).resolves.not.toThrow();
334344
});
335345

336346
it('throws no error when setting delimiter', () => {

lib/webhook-settings/webhookSettings.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,7 @@ describe('setOutgoingUrl', () => {
116116

117117
await settingsModule.setOutgoingUrl(TEST_OUTGOING_URL);
118118

119-
await expect(mockClient.put).toBeCalledWith(
120-
expect.anything(),
121-
expectedSettings
122-
);
119+
expect(mockClient.put).toBeCalledWith(expect.anything(), expectedSettings);
123120
});
124121

125122
it('should throw an error when supplied with an invalid url', async () => {
@@ -185,9 +182,6 @@ describe('setWhitelist', () => {
185182

186183
await settingsModule.setWhitelist(VALID_WHITELIST);
187184

188-
await expect(mockClient.put).toBeCalledWith(
189-
expect.anything(),
190-
expectedSettings
191-
);
185+
expect(mockClient.put).toBeCalledWith(expect.anything(), expectedSettings);
192186
});
193187
});

lib/webhook/errors/ErrorMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export enum ErrorMessage {}

lib/webhook/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './webhook';
2+
export * from './webhook.module';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { EventType } from '../webhook.module';
2+
3+
enum Direction {
4+
IN = 'in',
5+
OUT = 'out',
6+
}
7+
8+
enum HangupCause {
9+
NORMAL_CLEARING = 'normalClearing',
10+
BUSY = 'busy',
11+
CANCEL = 'cancel',
12+
NO_ANSWER = 'noAnswer',
13+
CONGESTION = 'congestion',
14+
NOT_FOUND = 'notFound',
15+
FORWARDED = 'forwarded',
16+
}
17+
18+
export interface Event {
19+
event: EventType;
20+
callId: string;
21+
}
22+
23+
export interface GenericCallEvent extends Event {
24+
direction: Direction;
25+
from: string;
26+
to: string;
27+
xcid: string;
28+
}
29+
30+
export interface NewCallEvent extends GenericCallEvent {
31+
event: EventType.NEW_CALL;
32+
originalCallId: string;
33+
user: string[];
34+
userId: string[];
35+
fullUserId: string[];
36+
}
37+
38+
export interface AnswerEvent extends GenericCallEvent {
39+
event: EventType.ANSWER;
40+
user: string;
41+
userId: string;
42+
fullUserId: string;
43+
answeringNumber: string;
44+
diversion?: string;
45+
}
46+
47+
export interface DataEvent extends Event {
48+
event: EventType.DATA;
49+
dtmf: string; // Can begin with zero, so it has to be a string
50+
}
51+
52+
export interface HangupEvent extends GenericCallEvent {
53+
event: EventType.HANGUP;
54+
cause: HangupCause;
55+
answeringNumber: string;
56+
}
57+
58+
export type CallEvent = NewCallEvent | AnswerEvent | HangupEvent | DataEvent;

0 commit comments

Comments
 (0)