@wazo/sdk 0.43.6 → 1.0.0-alpha.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/dist/wazo-sdk.js +3 -3
- package/esm/__tests__/__voice__/call.test.js +72 -0
- package/esm/__tests__/__voice__/softphone.test.js +66 -0
- package/esm/__tests__/api-client.test.js +1 -20
- package/esm/api/auth.js +2 -13
- package/esm/checker/checks/webrtc.js +2 -1
- package/esm/domain/InvalidState.js +8 -0
- package/esm/domain/InvalidStateTransition.js +10 -0
- package/esm/domain/Phone/WebRTCPhone.js +33 -35
- package/esm/simple/Auth.js +12 -6
- package/esm/simple/Phone.js +2 -1
- package/esm/simple/__tests__/Auth.test.js +64 -8
- package/esm/simple/index.js +8 -2
- package/esm/simple/room/Room.js +2 -1
- package/esm/simple/room/SipRoom.js +2 -1
- package/esm/state-machine/call-state-machine.js +114 -0
- package/esm/state-machine/softphone-state-machine.js +39 -0
- package/esm/state-machine/utils.js +40 -0
- package/esm/types/__tests__/__voice__/call.test.d.ts +2 -0
- package/esm/types/__tests__/__voice__/call.test.d.ts.map +1 -0
- package/esm/types/__tests__/__voice__/softphone.test.d.ts +2 -0
- package/esm/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
- package/esm/types/api/auth.d.ts.map +1 -1
- package/esm/types/checker/checks/webrtc.d.ts.map +1 -1
- package/esm/types/domain/InvalidState.d.ts +6 -0
- package/esm/types/domain/InvalidState.d.ts.map +1 -0
- package/esm/types/domain/InvalidStateTransition.d.ts +7 -0
- package/esm/types/domain/InvalidStateTransition.d.ts.map +1 -0
- package/esm/types/domain/Phone/WebRTCPhone.d.ts +0 -1
- package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/esm/types/domain/types.d.ts +4 -0
- package/esm/types/domain/types.d.ts.map +1 -1
- package/esm/types/simple/Auth.d.ts.map +1 -1
- package/esm/types/simple/Phone.d.ts.map +1 -1
- package/esm/types/simple/index.d.ts +7 -2
- package/esm/types/simple/index.d.ts.map +1 -1
- package/esm/types/simple/room/Room.d.ts.map +1 -1
- package/esm/types/simple/room/SipRoom.d.ts +1 -1
- package/esm/types/simple/room/SipRoom.d.ts.map +1 -1
- package/esm/types/state-machine/call-state-machine.d.ts +43 -0
- package/esm/types/state-machine/call-state-machine.d.ts.map +1 -0
- package/esm/types/state-machine/softphone-state-machine.d.ts +19 -0
- package/esm/types/state-machine/softphone-state-machine.d.ts.map +1 -0
- package/esm/types/state-machine/utils.d.ts +9 -0
- package/esm/types/state-machine/utils.d.ts.map +1 -0
- package/esm/types/utils/api-requester.d.ts.map +1 -1
- package/esm/types/utils/sdp.d.ts +6 -0
- package/esm/types/utils/sdp.d.ts.map +1 -1
- package/esm/types/utils/sip-logger.d.ts +4 -0
- package/esm/types/utils/sip-logger.d.ts.map +1 -0
- package/esm/types/utils/webrtc.d.ts +40 -0
- package/esm/types/utils/webrtc.d.ts.map +1 -1
- package/esm/types/voice/call.d.ts +110 -0
- package/esm/types/voice/call.d.ts.map +1 -0
- package/esm/types/voice/softphone.d.ts +77 -0
- package/esm/types/voice/softphone.d.ts.map +1 -0
- package/esm/types/web-rtc-client.d.ts +1 -36
- package/esm/types/web-rtc-client.d.ts.map +1 -1
- package/esm/utils/__tests__/api-requester.test.js +0 -17
- package/esm/utils/api-requester.js +1 -9
- package/esm/utils/sdp.js +26 -0
- package/esm/utils/sip-logger.js +32 -0
- package/esm/utils/webrtc.js +465 -0
- package/esm/voice/call.js +527 -0
- package/esm/voice/softphone.js +347 -0
- package/esm/web-rtc-client.js +86 -584
- package/lib/__tests__/__voice__/call.test.js +86 -0
- package/lib/__tests__/__voice__/softphone.test.js +80 -0
- package/lib/__tests__/api-client.test.js +1 -20
- package/lib/api/auth.js +2 -13
- package/lib/checker/checks/webrtc.js +2 -1
- package/lib/domain/InvalidState.js +9 -0
- package/lib/domain/InvalidStateTransition.js +10 -0
- package/lib/domain/Phone/WebRTCPhone.js +33 -35
- package/lib/simple/Auth.js +12 -6
- package/lib/simple/Phone.js +2 -1
- package/lib/simple/__tests__/Auth.test.js +64 -11
- package/lib/simple/index.js +6 -0
- package/lib/simple/room/Room.js +4 -3
- package/lib/simple/room/SipRoom.js +2 -1
- package/lib/state-machine/call-state-machine.js +120 -0
- package/lib/state-machine/softphone-state-machine.js +42 -0
- package/lib/state-machine/utils.js +52 -0
- package/lib/types/__tests__/__voice__/call.test.d.ts +2 -0
- package/lib/types/__tests__/__voice__/call.test.d.ts.map +1 -0
- package/lib/types/__tests__/__voice__/softphone.test.d.ts +2 -0
- package/lib/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
- package/lib/types/api/auth.d.ts.map +1 -1
- package/lib/types/checker/checks/webrtc.d.ts.map +1 -1
- package/lib/types/domain/InvalidState.d.ts +6 -0
- package/lib/types/domain/InvalidState.d.ts.map +1 -0
- package/lib/types/domain/InvalidStateTransition.d.ts +7 -0
- package/lib/types/domain/InvalidStateTransition.d.ts.map +1 -0
- package/lib/types/domain/Phone/WebRTCPhone.d.ts +0 -1
- package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/lib/types/domain/types.d.ts +4 -0
- package/lib/types/domain/types.d.ts.map +1 -1
- package/lib/types/simple/Auth.d.ts.map +1 -1
- package/lib/types/simple/Phone.d.ts.map +1 -1
- package/lib/types/simple/index.d.ts +7 -2
- package/lib/types/simple/index.d.ts.map +1 -1
- package/lib/types/simple/room/Room.d.ts.map +1 -1
- package/lib/types/simple/room/SipRoom.d.ts +1 -1
- package/lib/types/simple/room/SipRoom.d.ts.map +1 -1
- package/lib/types/state-machine/call-state-machine.d.ts +43 -0
- package/lib/types/state-machine/call-state-machine.d.ts.map +1 -0
- package/lib/types/state-machine/softphone-state-machine.d.ts +19 -0
- package/lib/types/state-machine/softphone-state-machine.d.ts.map +1 -0
- package/lib/types/state-machine/utils.d.ts +9 -0
- package/lib/types/state-machine/utils.d.ts.map +1 -0
- package/lib/types/utils/api-requester.d.ts.map +1 -1
- package/lib/types/utils/sdp.d.ts +6 -0
- package/lib/types/utils/sdp.d.ts.map +1 -1
- package/lib/types/utils/sip-logger.d.ts +4 -0
- package/lib/types/utils/sip-logger.d.ts.map +1 -0
- package/lib/types/utils/webrtc.d.ts +40 -0
- package/lib/types/utils/webrtc.d.ts.map +1 -1
- package/lib/types/voice/call.d.ts +110 -0
- package/lib/types/voice/call.d.ts.map +1 -0
- package/lib/types/voice/softphone.d.ts +77 -0
- package/lib/types/voice/softphone.d.ts.map +1 -0
- package/lib/types/web-rtc-client.d.ts +1 -36
- package/lib/types/web-rtc-client.d.ts.map +1 -1
- package/lib/utils/__tests__/api-requester.test.js +0 -17
- package/lib/utils/api-requester.js +1 -9
- package/lib/utils/sdp.js +27 -1
- package/lib/utils/sip-logger.js +37 -0
- package/lib/utils/webrtc.js +521 -1
- package/lib/voice/call.js +577 -0
- package/lib/voice/softphone.js +375 -0
- package/lib/web-rtc-client.js +89 -601
- package/package.json +3 -2
- package/src/__tests__/__voice__/call.test.ts +90 -0
- package/src/__tests__/__voice__/softphone.test.ts +82 -0
- package/src/__tests__/api-client.test.ts +1 -22
- package/src/api/auth.ts +3 -25
- package/src/checker/checks/webrtc.ts +2 -1
- package/src/domain/InvalidState.ts +13 -0
- package/src/domain/InvalidStateTransition.ts +17 -0
- package/src/domain/Phone/WebRTCPhone.ts +34 -36
- package/src/domain/types.ts +6 -1
- package/src/simple/Auth.ts +13 -6
- package/src/simple/Phone.ts +2 -1
- package/src/simple/__tests__/Auth.test.ts +87 -9
- package/src/simple/index.ts +9 -2
- package/src/simple/room/Room.ts +2 -1
- package/src/simple/room/SipRoom.ts +2 -1
- package/src/state-machine/call-state-machine.ts +131 -0
- package/src/state-machine/softphone-state-machine.ts +49 -0
- package/src/state-machine/utils.ts +53 -0
- package/src/utils/__tests__/api-requester.test.ts +0 -20
- package/src/utils/api-requester.ts +1 -9
- package/src/utils/sdp.ts +36 -0
- package/src/utils/sip-logger.ts +40 -0
- package/src/utils/webrtc.ts +571 -0
- package/src/voice/call.ts +713 -0
- package/src/voice/softphone.ts +485 -0
- package/src/web-rtc-client.ts +90 -690
package/src/simple/Phone.ts
CHANGED
|
@@ -12,6 +12,7 @@ import Emitter from '../utils/Emitter';
|
|
|
12
12
|
import Wazo from './index';
|
|
13
13
|
import SFUNotAvailableError from '../domain/SFUNotAvailableError';
|
|
14
14
|
import { WazoSession, WebRtcConfig } from '../domain/types';
|
|
15
|
+
import { getSipSessionId } from '../utils/sdp';
|
|
15
16
|
|
|
16
17
|
const logger = IssueReporter.loggerFor('simple-phone');
|
|
17
18
|
const sipLogger = IssueReporter.loggerFor('sip.js');
|
|
@@ -332,7 +333,7 @@ export class Phone extends Emitter {
|
|
|
332
333
|
if (!sipSession || !this.phone) {
|
|
333
334
|
return null;
|
|
334
335
|
}
|
|
335
|
-
return
|
|
336
|
+
return getSipSessionId(sipSession);
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
sendMessage(body: string, sipSession?: WazoSession, contentType = 'text/plain'): void {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import BadResponse from '../../domain/BadResponse';
|
|
2
|
-
import ServerError from '../../domain/ServerError';
|
|
3
1
|
import getApiClient, { setRefreshToken } from '../../service/getApiClient';
|
|
4
2
|
import { Auth as AuthClass } from '../Auth';
|
|
5
3
|
|
|
@@ -126,8 +124,13 @@ describe('simple/Auth', () => {
|
|
|
126
124
|
describe('initiateIdpAuthentication', () => {
|
|
127
125
|
const validResponse = { location: 'idp.com/auth', saml_session_id: 'a1b2C3d4' };
|
|
128
126
|
|
|
129
|
-
it('should return a `location` and `saml_session_id` if the response is
|
|
130
|
-
const initiateIdpAuthentication = jest.fn(() =>
|
|
127
|
+
it('should return a `location` and `saml_session_id` if the response is ok', async () => {
|
|
128
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
129
|
+
Promise.resolve({
|
|
130
|
+
ok: true,
|
|
131
|
+
status: 303,
|
|
132
|
+
json: () => Promise.resolve(validResponse),
|
|
133
|
+
}));
|
|
131
134
|
|
|
132
135
|
(getApiClient as any).mockImplementation(() => ({
|
|
133
136
|
auth: {
|
|
@@ -138,9 +141,12 @@ describe('simple/Auth', () => {
|
|
|
138
141
|
const response = await Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz');
|
|
139
142
|
expect(response).toBe(validResponse);
|
|
140
143
|
});
|
|
141
|
-
|
|
142
144
|
it('should return a `NoSamlRouteError` if the response is a 404', async () => {
|
|
143
|
-
const initiateIdpAuthentication = jest.fn(() =>
|
|
145
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
146
|
+
Promise.resolve({
|
|
147
|
+
ok: false,
|
|
148
|
+
status: 404,
|
|
149
|
+
}));
|
|
144
150
|
|
|
145
151
|
(getApiClient as any).mockImplementation(() => ({
|
|
146
152
|
auth: {
|
|
@@ -149,12 +155,84 @@ describe('simple/Auth', () => {
|
|
|
149
155
|
}));
|
|
150
156
|
|
|
151
157
|
await expect(Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz')).rejects.toThrow(
|
|
152
|
-
'No route found for
|
|
158
|
+
'No route found for saml sso',
|
|
153
159
|
);
|
|
154
160
|
});
|
|
155
161
|
|
|
156
|
-
it('should return
|
|
157
|
-
const initiateIdpAuthentication = jest.fn(() =>
|
|
162
|
+
it('should return an error `message` if the response is NOT ok', async () => {
|
|
163
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
164
|
+
Promise.resolve({
|
|
165
|
+
ok: false,
|
|
166
|
+
json: () => Promise.resolve({ message: 'an error message', reason: 'an error reason' }),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
(getApiClient as any).mockImplementation(() => ({
|
|
170
|
+
auth: {
|
|
171
|
+
initiateIdpAuthentication,
|
|
172
|
+
},
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
await expect(Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz')).rejects.toThrow(
|
|
176
|
+
'an error message',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return an error `reason` if there is no `message` and the response is NOT ok', async () => {
|
|
181
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
182
|
+
Promise.resolve({
|
|
183
|
+
ok: false,
|
|
184
|
+
json: () => Promise.resolve({ reason: 'an error reason' }),
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
(getApiClient as any).mockImplementation(() => ({
|
|
188
|
+
auth: {
|
|
189
|
+
initiateIdpAuthentication,
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
await expect(Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz')).rejects.toThrow(
|
|
194
|
+
'an error reason',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return an error with a null message if there is no `message` and `reason`', async () => {
|
|
199
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
200
|
+
Promise.resolve({
|
|
201
|
+
ok: false,
|
|
202
|
+
json: () => Promise.resolve({}),
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
(getApiClient as any).mockImplementation(() => ({
|
|
206
|
+
auth: {
|
|
207
|
+
initiateIdpAuthentication,
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
await expect(Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz')).rejects.toThrow('');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return a NoSAMLRouteError if the response is a 404', async () => {
|
|
215
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
216
|
+
Promise.resolve({
|
|
217
|
+
ok: false,
|
|
218
|
+
status: 404,
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
(getApiClient as any).mockImplementation(() => ({
|
|
222
|
+
auth: {
|
|
223
|
+
initiateIdpAuthentication,
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
await expect(Auth.initiateIdpAuthentication('example.com', 'https://myapp.xyz')).rejects.toThrow('No route found for saml sso');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should return a SamlConfigError if the response is a 500', async () => {
|
|
231
|
+
const initiateIdpAuthentication = jest.fn(() =>
|
|
232
|
+
Promise.resolve({
|
|
233
|
+
ok: false,
|
|
234
|
+
status: 500,
|
|
235
|
+
}));
|
|
158
236
|
|
|
159
237
|
(getApiClient as any).mockImplementation(() => ({
|
|
160
238
|
auth: {
|
package/src/simple/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { CanceledCallError } from '../web-rtc-client';
|
|
|
17
17
|
import BadResponse from '../domain/BadResponse';
|
|
18
18
|
import ServerError from '../domain/ServerError';
|
|
19
19
|
import SFUNotAvailableError from '../domain/SFUNotAvailableError';
|
|
20
|
-
import
|
|
20
|
+
import ApiCall from '../domain/Call';
|
|
21
21
|
import CallLog from '../domain/CallLog';
|
|
22
22
|
import Recording from '../domain/Recording';
|
|
23
23
|
import ChatMessage from '../domain/ChatMessage';
|
|
@@ -41,6 +41,9 @@ import Features from '../domain/Features';
|
|
|
41
41
|
import Meeting from '../domain/Meeting';
|
|
42
42
|
import Checker from '../checker/Checker';
|
|
43
43
|
|
|
44
|
+
import Softphone from '../voice/softphone';
|
|
45
|
+
import Call from '../voice/call';
|
|
46
|
+
|
|
44
47
|
const Wazo = {
|
|
45
48
|
Auth,
|
|
46
49
|
Phone,
|
|
@@ -60,11 +63,15 @@ const Wazo = {
|
|
|
60
63
|
loggerFor: IssueReporter.loggerFor.bind(IssueReporter),
|
|
61
64
|
Features,
|
|
62
65
|
Checker,
|
|
66
|
+
Voice: {
|
|
67
|
+
Softphone,
|
|
68
|
+
Call,
|
|
69
|
+
},
|
|
63
70
|
// Domain
|
|
64
71
|
domain: {
|
|
65
72
|
BadResponse,
|
|
66
73
|
ServerError,
|
|
67
|
-
Call,
|
|
74
|
+
Call: ApiCall,
|
|
68
75
|
CallLog,
|
|
69
76
|
CTIPhone,
|
|
70
77
|
Recording,
|
package/src/simple/room/Room.ts
CHANGED
|
@@ -10,6 +10,7 @@ import RemoteParticipant from './RemoteParticipant';
|
|
|
10
10
|
import IssueReporter from '../../service/IssueReporter';
|
|
11
11
|
import LocalParticipant from './LocalParticipant';
|
|
12
12
|
import { PeerConnection, WazoSession } from '../../domain/types';
|
|
13
|
+
import { getSipSessionId } from '../../utils/sdp';
|
|
13
14
|
|
|
14
15
|
export const SIGNAL_TYPE_PARTICIPANT_UPDATE = 'signal/PARTICIPANT_UPDATE';
|
|
15
16
|
export const SIGNAL_TYPE_PARTICIPANT_REQUEST = 'signal/PARTICIPANT_REQUEST';
|
|
@@ -497,7 +498,7 @@ class Room extends Emitter {
|
|
|
497
498
|
this.audioStream = stream;
|
|
498
499
|
|
|
499
500
|
if (!this.roomAudioElement) {
|
|
500
|
-
const sessionId =
|
|
501
|
+
const sessionId = getSipSessionId(Wazo.Phone.phone?.currentSipSession as WazoSession);
|
|
501
502
|
this.roomAudioElement = await Wazo.Phone.phone?.createAudioElementFor(sessionId as string) as HTMLAudioElement;
|
|
502
503
|
this.roomAudioElement.srcObject = stream;
|
|
503
504
|
} else {
|
|
@@ -4,6 +4,7 @@ import IssueReporter from '../../service/IssueReporter';
|
|
|
4
4
|
import Wazo from '../index';
|
|
5
5
|
import Room from './Room';
|
|
6
6
|
import { WazoSession } from '../../domain/types';
|
|
7
|
+
import { getSipSessionId } from '../../utils/sdp';
|
|
7
8
|
|
|
8
9
|
const logger = IssueReporter.loggerFor('sdk-sip-room');
|
|
9
10
|
|
|
@@ -156,7 +157,7 @@ class SipRoom extends Room {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
_getCurrentSipCallIs() {
|
|
159
|
-
return
|
|
160
|
+
return getSipSessionId(Wazo.Phone.phone?.currentSipSession as WazoSession);
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { setup, type ActorRefFrom } from 'xstate';
|
|
2
|
+
|
|
3
|
+
import softphone from '../voice/softphone';
|
|
4
|
+
import { States as SoftphoneStates } from './softphone-state-machine';
|
|
5
|
+
|
|
6
|
+
export const States = {
|
|
7
|
+
IDLE: 'idle',
|
|
8
|
+
RINGING: 'ringing',
|
|
9
|
+
IN_PROGRESS: 'progress',
|
|
10
|
+
ESTABLISHED: 'established',
|
|
11
|
+
TERMINATED: 'terminated',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export const EstablishedStates = {
|
|
15
|
+
ONGOING: 'ongoing',
|
|
16
|
+
HELD: 'held',
|
|
17
|
+
MUTED: 'muted',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type StateTypes = typeof States[keyof typeof States];
|
|
21
|
+
|
|
22
|
+
export type EstablishedStateTypes = typeof EstablishedStates[keyof typeof EstablishedStates];
|
|
23
|
+
|
|
24
|
+
export const Actions = {
|
|
25
|
+
INCOMING_CALL: 'incomingCall',
|
|
26
|
+
MAKE_CALL: 'makeCall',
|
|
27
|
+
ACCEPT: 'accept',
|
|
28
|
+
REMOTLY_ACCEPTED: 'remotlyAccepted',
|
|
29
|
+
REJECT: 'reject',
|
|
30
|
+
HANGUP: 'hangup',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export const EstablishedActions = {
|
|
34
|
+
HOLD: 'hold',
|
|
35
|
+
RESUME: 'resume',
|
|
36
|
+
MUTE: 'mute',
|
|
37
|
+
UN_MUTE: 'unMute',
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export type ActionTypes = typeof Actions[keyof typeof Actions];
|
|
41
|
+
|
|
42
|
+
export type EstablishedActionTypes = typeof EstablishedActions[keyof typeof EstablishedActions];
|
|
43
|
+
|
|
44
|
+
const callMachine = setup({
|
|
45
|
+
guards: {
|
|
46
|
+
isRegistered: () => softphone.getState() === SoftphoneStates.REGISTERED,
|
|
47
|
+
},
|
|
48
|
+
}).createMachine({
|
|
49
|
+
id: 'call',
|
|
50
|
+
initial: States.IDLE,
|
|
51
|
+
context: {},
|
|
52
|
+
states: {
|
|
53
|
+
[States.IDLE]: {
|
|
54
|
+
on: {
|
|
55
|
+
[Actions.INCOMING_CALL]: {
|
|
56
|
+
// guard: 'isRegistered',
|
|
57
|
+
target: States.RINGING,
|
|
58
|
+
},
|
|
59
|
+
[Actions.MAKE_CALL]: {
|
|
60
|
+
guard: 'isRegistered',
|
|
61
|
+
target: States.IN_PROGRESS,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
[States.IN_PROGRESS]: {
|
|
66
|
+
on: {
|
|
67
|
+
[Actions.HANGUP]: {
|
|
68
|
+
guard: 'isRegistered',
|
|
69
|
+
target: States.TERMINATED,
|
|
70
|
+
},
|
|
71
|
+
[Actions.REMOTLY_ACCEPTED]: {
|
|
72
|
+
guard: 'isRegistered',
|
|
73
|
+
target: States.ESTABLISHED,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
[States.RINGING]: {
|
|
78
|
+
on: {
|
|
79
|
+
[Actions.ACCEPT]: {
|
|
80
|
+
guard: 'isRegistered',
|
|
81
|
+
target: States.ESTABLISHED,
|
|
82
|
+
},
|
|
83
|
+
[Actions.REJECT]: {
|
|
84
|
+
guard: 'isRegistered',
|
|
85
|
+
target: States.TERMINATED,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
[States.ESTABLISHED]: {
|
|
90
|
+
initial: EstablishedStates.ONGOING,
|
|
91
|
+
on: {
|
|
92
|
+
[Actions.HANGUP]: {
|
|
93
|
+
guard: 'isRegistered',
|
|
94
|
+
target: States.TERMINATED,
|
|
95
|
+
},
|
|
96
|
+
[EstablishedActions.HOLD]: {
|
|
97
|
+
guard: 'isRegistered',
|
|
98
|
+
target: `.${EstablishedStates.HELD}`,
|
|
99
|
+
},
|
|
100
|
+
[EstablishedActions.MUTE]: {
|
|
101
|
+
guard: 'isRegistered',
|
|
102
|
+
target: `.${EstablishedStates.MUTED}`,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
states: {
|
|
106
|
+
[EstablishedStates.ONGOING]: { },
|
|
107
|
+
[EstablishedStates.HELD]: {
|
|
108
|
+
on: {
|
|
109
|
+
[EstablishedActions.RESUME]: {
|
|
110
|
+
guard: 'isRegistered',
|
|
111
|
+
target: EstablishedStates.ONGOING,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
[EstablishedStates.MUTED]: {
|
|
116
|
+
on: {
|
|
117
|
+
[EstablishedActions.UN_MUTE]: {
|
|
118
|
+
guard: 'isRegistered',
|
|
119
|
+
target: EstablishedStates.ONGOING,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
[States.TERMINATED]: { },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export type CallActorRef = ActorRefFrom<typeof callMachine>;
|
|
130
|
+
|
|
131
|
+
export default callMachine;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ActorRefFrom, setup } from 'xstate';
|
|
2
|
+
|
|
3
|
+
export const States = {
|
|
4
|
+
UNREGISTERED: 'unregistered',
|
|
5
|
+
REGISTERING: 'registering',
|
|
6
|
+
REGISTERED: 'registered',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type StateTypes = typeof States[keyof typeof States];
|
|
10
|
+
|
|
11
|
+
export const Actions = {
|
|
12
|
+
REGISTER: 'register',
|
|
13
|
+
REGISTER_DONE: 'registerDone',
|
|
14
|
+
UNREGISTER: 'unregister',
|
|
15
|
+
UNREGISTERED: 'unregistered',
|
|
16
|
+
TRANSPORT_CLOSED: 'transportClosed',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type ActionTypes = typeof Actions[keyof typeof Actions];
|
|
20
|
+
|
|
21
|
+
const softphoneMachine = setup({}).createMachine({
|
|
22
|
+
id: 'softphone',
|
|
23
|
+
initial: States.UNREGISTERED,
|
|
24
|
+
context: {},
|
|
25
|
+
states: {
|
|
26
|
+
[States.UNREGISTERED]: {
|
|
27
|
+
on: {
|
|
28
|
+
[Actions.REGISTER]: States.REGISTERING,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
[States.REGISTERING]: {
|
|
32
|
+
on: {
|
|
33
|
+
[Actions.REGISTER_DONE]: States.REGISTERED,
|
|
34
|
+
[Actions.TRANSPORT_CLOSED]: States.UNREGISTERED,
|
|
35
|
+
[Actions.UNREGISTERED]: States.UNREGISTERED,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
[States.REGISTERED]: {
|
|
39
|
+
on: {
|
|
40
|
+
[Actions.UNREGISTER]: States.UNREGISTERED,
|
|
41
|
+
[Actions.TRANSPORT_CLOSED]: States.UNREGISTERED,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type SoftphoneActorRef = ActorRefFrom<typeof softphoneMachine>;
|
|
48
|
+
|
|
49
|
+
export default softphoneMachine;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type SnapshotFrom } from 'xstate';
|
|
2
|
+
import { type SoftphoneActorRef, type ActionTypes as SoftphoneActions, type StateTypes as SoftphoneStates } from './softphone-state-machine';
|
|
3
|
+
import { type CallActorRef, type ActionTypes as CallActions, type EstablishedActionTypes as EstablishedCallActions, type StateTypes as CallStates, type EstablishedStateTypes } from './call-state-machine';
|
|
4
|
+
|
|
5
|
+
import InvalidStateTransition from '../domain/InvalidStateTransition';
|
|
6
|
+
import InvalidState from '../domain/InvalidState';
|
|
7
|
+
import IssueReporter from '../service/IssueReporter';
|
|
8
|
+
|
|
9
|
+
const logger = IssueReporter.loggerFor('state-machine');
|
|
10
|
+
|
|
11
|
+
export const getState = (actor: SoftphoneActorRef | CallActorRef): SoftphoneStates | CallStates | EstablishedStateTypes => {
|
|
12
|
+
return actor.getSnapshot().value as SoftphoneStates | CallStates | EstablishedStateTypes;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const can = (actor: SoftphoneActorRef | CallActorRef, action: SoftphoneActions | CallActions | EstablishedCallActions): boolean => {
|
|
16
|
+
return actor.getSnapshot().can({ type: action });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const hasState = (actor: SoftphoneActorRef | CallActorRef, state: SoftphoneStates | CallStates): boolean => {
|
|
20
|
+
return getState(actor) === state;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const waitUntilState = (actor: SoftphoneActorRef | CallActorRef, state: SoftphoneStates | CallStates, timeout = 5000): Promise<unknown> => {
|
|
24
|
+
return Promise.race([
|
|
25
|
+
new Promise(resolve => {
|
|
26
|
+
(actor as SoftphoneActorRef).subscribe((snapshot: SnapshotFrom<SoftphoneActorRef | CallActorRef>) => {
|
|
27
|
+
if (snapshot.value === state) {
|
|
28
|
+
resolve(null);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}),
|
|
32
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Waited for state ${state} timeout.`)), timeout)),
|
|
33
|
+
]);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const assertCan = (actor: SoftphoneActorRef | CallActorRef, action: SoftphoneActions | CallActions | EstablishedCallActions): void => {
|
|
37
|
+
if (!can(actor, action)) {
|
|
38
|
+
const currentState = actor.getSnapshot().value;
|
|
39
|
+
const message = `Invalid state transition from ${currentState} with action ${action}`;
|
|
40
|
+
logger.warn(message);
|
|
41
|
+
|
|
42
|
+
throw new InvalidStateTransition(message, action, currentState as string);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const assertState = (actor: SoftphoneActorRef | CallActorRef, state: SoftphoneStates | CallStates): void => {
|
|
47
|
+
if (!hasState(actor, state)) {
|
|
48
|
+
const message = `Invalid state ${state}`;
|
|
49
|
+
logger.warn(message);
|
|
50
|
+
|
|
51
|
+
throw new InvalidState(message, state);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -158,23 +158,3 @@ describe('With a refresh token', () => {
|
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
-
|
|
162
|
-
describe('base64Encode', () => {
|
|
163
|
-
it('should encode a string using base64', () => {
|
|
164
|
-
const input = 'Hello World!';
|
|
165
|
-
const expectedOutput = 'SGVsbG8gV29ybGQh';
|
|
166
|
-
expect(ApiRequester.base64Encode(input)).toBe(expectedOutput);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should handle special characters', () => {
|
|
170
|
-
const input = 'éà$@€';
|
|
171
|
-
const expectedOutput = 'w6nDoCRA4oKs';
|
|
172
|
-
expect(ApiRequester.base64Encode(input)).toBe(expectedOutput);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should handle empty string', () => {
|
|
176
|
-
const input = '';
|
|
177
|
-
const expectedOutput = '';
|
|
178
|
-
expect(ApiRequester.base64Encode(input)).toBe(expectedOutput);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
@@ -64,15 +64,7 @@ export default class ApiRequester {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
static base64Encode(str: string): string {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
return btoa(str);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
// fall back to Base64.encode if btoa fails
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return Base64.encode(str);
|
|
67
|
+
return typeof btoa !== 'undefined' ? btoa(str) : Base64.encode(str);
|
|
76
68
|
}
|
|
77
69
|
|
|
78
70
|
// @see https://github.com/facebook/flow/issues/183#issuecomment-358607052
|
package/src/utils/sdp.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import sdpParser from 'sdp-transform';
|
|
2
|
+
import { UserAgent } from 'sip.js/lib/api/user-agent';
|
|
3
|
+
import { Inviter } from 'sip.js/lib/api';
|
|
4
|
+
import { URI } from 'sip.js/lib/grammar/uri';
|
|
5
|
+
|
|
6
|
+
import type { SipCall } from '../domain/types';
|
|
7
|
+
|
|
8
|
+
export const SIP_ID_LENGTH = 36;
|
|
2
9
|
|
|
3
10
|
export type Candidate = {
|
|
4
11
|
foundation: string;
|
|
@@ -138,3 +145,32 @@ export const addIcesInAllBundles = (sdp: string) => {
|
|
|
138
145
|
}));
|
|
139
146
|
return sdpParser.write(parsedSdp);
|
|
140
147
|
};
|
|
148
|
+
|
|
149
|
+
export const getSipSessionId = (sipSession: SipCall | null | undefined): string => {
|
|
150
|
+
if (!sipSession) {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const message = sipSession.message as Partial<{ callId?: string }>;
|
|
155
|
+
if (message?.callId) {
|
|
156
|
+
return message.callId.substring(0, SIP_ID_LENGTH);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For Inviter
|
|
160
|
+
// @ts-ignore: private
|
|
161
|
+
if (sipSession instanceof Inviter && sipSession.outgoingRequestMessage) {
|
|
162
|
+
// @ts-ignore: private
|
|
163
|
+
return sipSession.outgoingRequestMessage.callId.substring(0, SIP_ID_LENGTH);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// For Invitation
|
|
167
|
+
return (sipSession.id || '').substring(0, SIP_ID_LENGTH);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// We need to replace 0.0.0.0 to 127.0.0.1 in the sdp to avoid MOH during a createOffer.
|
|
171
|
+
export const replaceLocalIpModifier = (description: Record<string, any>) => Promise.resolve({ // description is immutable... so we have to clone it or the `type` attribute won't be returned.
|
|
172
|
+
...JSON.parse(JSON.stringify(description)),
|
|
173
|
+
sdp: description.sdp.replace('c=IN IP4 0.0.0.0', 'c=IN IP4 127.0.0.1'),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export const makeURI = (target: string, host: string): URI | undefined => UserAgent.makeURI(`sip:${target}@${host}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import IssueReporter from '../service/IssueReporter';
|
|
2
|
+
import { WebRtcConfig } from '../domain/types';
|
|
3
|
+
|
|
4
|
+
const sipLogger = IssueReporter.loggerFor('sip.js');
|
|
5
|
+
const protocolLogger = IssueReporter.loggerFor('sip');
|
|
6
|
+
const protocolDebugMessages = ['Received WebSocket text message:', 'Sending WebSocket message:'];
|
|
7
|
+
|
|
8
|
+
const logConnector = (level: any, className: string, label: any, content: string): void => {
|
|
9
|
+
const protocolIndex = content && content.indexOf ? protocolDebugMessages.findIndex(prefix => content.indexOf(prefix) !== -1) : -1;
|
|
10
|
+
|
|
11
|
+
if (className === 'sip.Transport' && protocolIndex !== -1) {
|
|
12
|
+
const direction = protocolIndex === 0 ? 'receiving' : 'sending';
|
|
13
|
+
const message = content.replace(`${protocolDebugMessages[protocolIndex]}\n\n`, '').replace('\r\n', '\n');
|
|
14
|
+
protocolLogger.trace(message, {
|
|
15
|
+
className,
|
|
16
|
+
direction,
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
19
|
+
sipLogger.trace(content, {
|
|
20
|
+
className,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const configureLogger = (rawOptions: Partial<WebRtcConfig> = {}): Partial<WebRtcConfig> => {
|
|
26
|
+
const options = rawOptions;
|
|
27
|
+
|
|
28
|
+
if (options.uaConfigOverrides) {
|
|
29
|
+
options.uaConfigOverrides.traceSip = true;
|
|
30
|
+
}
|
|
31
|
+
options.log = options.log || {};
|
|
32
|
+
options.log.builtinEnabled = false;
|
|
33
|
+
options.log.logLevel = 'debug';
|
|
34
|
+
|
|
35
|
+
options.log.connector = logConnector;
|
|
36
|
+
|
|
37
|
+
return options;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default configureLogger;
|