@subiz/wsclient 4.0.11 → 4.0.13

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/webrtc.js +223 -159
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "4.0.11",
3
+ "version": "4.0.13",
4
4
  "main": "realtime.js",
5
5
  "scripts": {
6
6
  "test": "node realtime.test.js"
package/webrtc.js CHANGED
@@ -8,15 +8,13 @@ const servers = {
8
8
  // env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, setInterval, jsonify, parseJSON}
9
9
  // set env._is_webrtc_local to true for local testing
10
10
  function WebRTCConn(options) {
11
- let {apiUrl, access_token, accid, agid, connId, realtime, callAPI, env, onTrack, onEvent} = options
11
+ let {apiUrl, access_token, accid, agid, realtime, callAPI, env, onTrack, onEvent} = options
12
12
  if (!apiUrl) {
13
13
  apiUrl = 'https://callcenter.subiz.com.vn/wc/'
14
14
  if (env._is_webrtc_local) apiUrl = 'http://localhost:8008/'
15
15
  }
16
16
 
17
17
  callAPI = callAPI || xhrsend // allow hook
18
- let callStream = {} // callid => stream
19
-
20
18
  let parseJSON = (str) => {
21
19
  try {
22
20
  return env.JSON.parse(str)
@@ -27,69 +25,58 @@ function WebRTCConn(options) {
27
25
  return env.JSON.stringify(str)
28
26
  } catch (e) {}
29
27
  }
30
- let currentStream
31
- let pendingStream
32
- let activeCalls = {}
28
+ let streams = {}
29
+ let serverCalls = {}
33
30
 
31
+ let call2Connection = {}
34
32
  let dialingRequest = {}
35
33
  let answerRequest = {}
36
34
  let endRequest = {}
37
35
 
38
- let subscribe = realtime.subscribe([
39
- `webrtc.account.${accid}.agent.${agid}`,
40
- `webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`,
41
- ])
42
-
43
- let peer
44
- // makeSure only returns when stream is added and negotiation is finished
45
- // its safe to call this function multiple times at once
46
- let makeSure = (cb) => {
47
- initConnection()
48
- subscribe.then(cb)
49
- }
50
-
51
- let checkIceConnected = (cb) =>
52
- flow.loop(
53
- () =>
54
- new env.Promise((rs) => {
55
- let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
56
- if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping util connected
57
- cb && cb()
58
- rs(false)
59
- }),
60
- )
61
-
62
- let readyToSendCandidateResolve
63
- let readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
36
+ let peers = {}
64
37
 
65
- let initConnection = () => {
66
- let needConnect =
67
- !peer ||
68
- peer.connectionState === 'disconnected' ||
69
- peer.connectionState === 'closed' ||
70
- peer.connectionState === 'failed'
38
+ // to receive incomming call
39
+ realtime.subscribe([`webrtc.account.${accid}.agent.${agid}`])
71
40
 
72
- if (!needConnect) return
73
- peer = new env.RTCPeerConnection(servers)
74
- let mypeer = peer
75
-
76
- readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
77
- peer.ontrack = (event) => {
78
- if (peer != mypeer) return // outdated
79
- onTrack && onTrack(event)
80
- }
41
+ let initSession = (cb) => {
42
+ let connId = 'webrtc' + randomString(20)
43
+ let peer = new env.RTCPeerConnection(servers)
44
+ peers[connId] = peer
45
+ peer.ontrack = (event) => onTrack && onTrack(event)
46
+ waitForRemoteDescription[connId] = new env.Promise((rs) => (readyToSendCandidateResolve[connId] = rs))
81
47
 
82
48
  peer.onicecandidate = (event) => {
83
49
  if (!event.candidate) return
84
- if (peer != mypeer) return // outdated
85
50
  let url = `${apiUrl}ice-candidates?x-access-token=${access_token}&connection_id=${connId}`
86
51
  callAPI('post', url, jsonify(event.candidate))
87
52
  }
88
53
 
89
54
  peer.onnegotiationneeded = (ev) => console.log('NNNEED RE-NEGOTIATION', ev)
90
- console.log('NEW PERR')
55
+ realtime
56
+ .subscribe([
57
+ `webrtc.account.${accid}.agent.${agid}`,
58
+ `webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`,
59
+ ])
60
+ .then((err) => {
61
+ // create peer connection
62
+
63
+ cb(err, connId)
64
+ })
91
65
  }
92
66
 
67
+ let checkIceConnected = (peer, cb) =>
68
+ flow.loop(
69
+ () =>
70
+ new env.Promise((rs) => {
71
+ let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
72
+ if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping util connected
73
+ cb && cb()
74
+ rs(false)
75
+ }),
76
+ )
77
+
78
+ let readyToSendCandidateResolve = {}
79
+ let waitForRemoteDescription = {}
93
80
  let stopStream = (stream) => {
94
81
  if (!stream) return
95
82
  stream.getTracks().forEach((track) => {
@@ -98,64 +85,109 @@ function WebRTCConn(options) {
98
85
  })
99
86
  }
100
87
 
101
- // auto tear down when byte received is zero
102
- // use to clear peer when there's no byte sent around 100 seconds
103
- let receivedBytes = []
104
- let check = () => {
105
- let url = `${apiUrl}calls?x-access-token=${access_token}&connection_id=${connId}`
88
+ let syncCallWithServer = () => {
89
+ let url = `${apiUrl}calls?x-access-token=${access_token}`
106
90
  callAPI('get', url, undefined, function (body, code) {
107
- if (code == 200) {
108
- body = parseJSON(body) || []
109
- let serverCall = {}
110
- body.map && body.map((call) => (serverCall[call.call_id] = call))
111
-
112
- // not found
113
- Object.keys(activeCalls).map((callid) => {
114
- let call = activeCalls[callid]
115
- if (!serverCall[callid] && call && call.status != 'ended')
116
- publish({type: 'call_ended', data: {call_info: {hanup_code: 'outdated', call_id: callid}}})
117
- })
118
- activeCalls = serverCall
119
- }
91
+ if (code != 200) return
92
+ body = parseJSON(body) || []
93
+ let newCalls = {}
94
+ if (!body.map) return
95
+ body.map((call) => {
96
+ newCalls[call.call_id] = call
97
+ })
120
98
 
121
- if (!peer) return
122
- peer.getStats(null).then((stats) => {
123
- let statsOutput = ''
124
- let receivedByte = 0
125
- stats.forEach((report) => {
126
- if (report.type === 'inbound-rtp' && report.kind === 'audio') {
127
- receivedByte += report.bytesReceived || 0
128
- }
129
- if (report.type === 'inbound-rtp' && report.kind === 'video') {
130
- receivedByte += report.bytesReceived || 0
131
- }
132
- })
133
- receivedBytes.push(receivedByte)
134
- while (receivedBytes.length > 10) receivedBytes.shift()
135
- let diff = receivedBytes.filter((n) => n != receivedByte)
136
- if (diff.length == 0 && receivedBytes.length >= 10 && Object.keys(activeCalls).length == 0) {
137
- // could tear down
138
- peer.close()
139
- peer = undefined
99
+ // end all local calls that not found on the server
100
+ Object.keys(serverCalls).map((i) => {
101
+ let call = serverCalls[i]
102
+ if (!call) return delete serverCalls[i]
103
+ let callid = call.call_id
104
+ if (newCalls[callid]) return
105
+
106
+ // calls that are missing on the server
107
+ newCalls[callid] = call
108
+ if (!call.ended) call.ended = env.Date.now()
109
+ if (!call.hangup_code) call.hangup_code = 'cancel'
110
+ if (call.status != 'ended') {
111
+ call.status = 'ended'
112
+ publish({type: 'call_ended', data: {call_info: {hanup_code: 'outdated', call_id: callid}}})
140
113
  }
141
114
  })
115
+
116
+ serverCalls = {}
117
+ Object.values(newCalls).map((call) => {
118
+ if (!call) return false
119
+ if (call.status == 'ended') {
120
+ // receive ended call
121
+ serverCalls[call.call_id] = call
122
+ return
123
+ }
124
+ // keep all dialing incomming calls
125
+ if (call.status == 'dialing' && (call.direction == 'incoming' || call.direction == 'inbound')) {
126
+ serverCalls[call.call_id] = call
127
+ return
128
+ }
129
+
130
+ let connId = call.device_id
131
+ let peer = peers[connId]
132
+ if (!peer) return false // ignore active call that not in our peer
133
+ call2Connection[call.call_id] = connId
134
+ serverCalls[call.call_id] = call
135
+ return true
136
+ })
137
+ })
138
+ }
139
+ syncCallWithServer()
140
+ env.setInterval(syncCallWithServer, 10000)
141
+
142
+ let cleanEndedCall = () => {
143
+ Object.keys(endRequest).map((callid) => {
144
+ if (env.Date.now() - endRequest[callid] > 60000) delete endRequest[callid]
145
+ })
146
+
147
+ Object.keys(answerRequest).map((callid) => {
148
+ if (env.Date.now() - endRequest[callid] > 60000) delete answerRequest[callid]
149
+ })
150
+
151
+ Object.keys(serverCalls).map((callid) => {
152
+ let call = serverCalls[callid]
153
+ if (!call) return delete serverCalls[callid]
154
+ if (call.status != 'ended') return
155
+ if (env.Date.now() - call.ended > 60000) delete serverCalls[callid]
156
+ })
157
+
158
+ Object.keys(call2Connection).map((callid) => {
159
+ let call = this.matchCall(callid)
160
+ if (!call) return delete call2Connection[callid]
161
+ if (call.status != 'ended') return
162
+ if (env.Date.now() - call.ended > 60000) delete call2Connection[callid]
142
163
  })
143
164
  }
144
- check()
145
- env.setInterval(check, 30000)
165
+ env.setInterval(cleanEndedCall, 120000)
146
166
 
147
167
  let publish = (ev) => {
148
168
  if (ev.type == 'call_ended') {
149
- let callid = lo.get(ev, 'data.call_info.call_id')
150
- stopStream(callStream[callid])
151
- delete callStream[callid]
169
+ let callid = ev.data && ev.data.call_info && ev.data.call_info.call_id
170
+ let connId = call2Connection[callid]
171
+ let peer = peers[connId]
172
+ if (peer) {
173
+ delete peers[connId]
174
+ peer.close()
175
+ }
176
+ stopStream(streams[connId])
177
+ // do not clean up this now or some functions cannot work after the call ended
178
+ // DONOT: delete call2Connection[callid]
179
+ delete streams[connId]
152
180
  }
181
+
182
+ let direction = ev.data && ev.data.call_info && ev.data.call_info.direction
183
+ if (direction == 'incoming') ev.data.call_info.direction = 'inbound'
184
+ if (direction == 'outgoing') ev.data.call_info.direction = 'outbound'
153
185
  onEvent && onEvent(ev)
154
186
  }
155
187
 
156
188
  this.matchCall = (callid) => {
157
189
  if (!callid || callid == '*' || callid == '-') {
158
- let calls = Object.assign({}, activeCalls)
190
+ let calls = Object.assign({}, serverCalls)
159
191
  Object.keys(dialingRequest).map((callid) => {
160
192
  let call = calls[callid]
161
193
  if (!call && dialingRequest[callid]) {
@@ -180,10 +212,16 @@ function WebRTCConn(options) {
180
212
  if (!call) return
181
213
  call.answer_requested = answerRequest[callid]
182
214
  })
215
+
216
+ Object.values(calls).map((call) => {
217
+ let direction = call && call.direction
218
+ if (direction == 'incoming') call.direction = 'inbound'
219
+ if (direction == 'outgoing') call.direction = 'outbound'
220
+ })
183
221
  return calls
184
222
  }
185
223
 
186
- let call = activeCalls[callid]
224
+ let call = serverCalls[callid]
187
225
  if (!call && dialingRequest[callid]) call = dialingRequest[callid]
188
226
  if (endRequest[callid] && call && call.status != 'ended') {
189
227
  call = Object.assign({}, call, {
@@ -194,47 +232,54 @@ function WebRTCConn(options) {
194
232
  })
195
233
  }
196
234
  if (answerRequest[callid]) call = Object.assign({}, call, {answer_requested: answerRequest[callid]})
235
+ let direction = call && call.direction
236
+ if (direction == 'incoming') call.direction = 'inbound'
237
+ if (direction == 'outgoing') call.direction = 'outbound'
238
+
197
239
  return call
198
240
  }
199
241
 
200
242
  realtime.onEvent((ev) => {
201
243
  if (!ev || !ev.type || !ev.data) return
202
- // if (ev.type.startsWith('call')) console.log('EEEEEEE', ev)
203
244
  if (env._is_webrtc_local && ev.user_id != '8') return // local
204
245
  if (!env._is_webrtc_local && ev.user_id != '9') return // prod
205
246
 
206
247
  if (ev.type == 'webrtc_candidate_added') {
207
- let ice_candidate = ev.data.ice_candidate
248
+ let ice_candidate = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.ice_candidate
249
+ let connId = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.connection_id
250
+ if (!connId) return
208
251
  if (!ice_candidate) return
209
- return readyToSendCandidate.then(() => peer && peer.addIceCandidate(parseJSON(ice_candidate)))
252
+ let peer = peers[connId]
253
+
254
+ return waitForRemoteDescription[connId].then(() => peer && peer.addIceCandidate(parseJSON(ice_candidate)))
210
255
  }
211
256
 
212
257
  if (ev.type == 'webrtc_negotiation_offered') {
213
258
  console.log('RT/RECEIVE OFFERED NEGOTIATION', ev)
259
+ let jsonoffer = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.offer
260
+ let connId = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.connection_id
261
+ let peer = peers[connId]
214
262
  if (!peer) return
215
263
  let negid = ev.id
216
- if (!ev.data.ice_candidate) return
217
- let offer = parseJSON(ev.data.ice_candidate)
264
+ // let negid = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.negotiation_id
265
+ let offer = parseJSON(jsonoffer)
266
+ if (!offer) return
218
267
 
219
268
  peer
220
269
  .setRemoteDescription(offer)
221
270
  .then(() => {
222
- readyToSendCandidateResolve()
223
- if (pendingStream && pendingStream != currentStream) {
224
- currentStream = pendingStream
225
- currentStream.getTracks().forEach((track) => {
226
- let replaced = false
227
- peer.getSenders().forEach((sender) => {
228
- if (!sender.track) return
229
- if (sender.track.kind != track.kind) return
230
- sender.replaceTrack(track)
231
- replaced = true
232
- })
233
-
234
- if (!replaced) peer.addTrack(track, currentStream)
271
+ readyToSendCandidateResolve[connId]()
272
+ let stream = streams[connId]
273
+ stream.getTracks().forEach((track) => {
274
+ let replaced = false
275
+ peer.getSenders().forEach((sender) => {
276
+ if (!sender.track) return
277
+ if (sender.track.kind != track.kind) return
278
+ sender.replaceTrack(track)
279
+ replaced = true
235
280
  })
236
- }
237
- pendingStream = undefined
281
+ if (!replaced) peer.addTrack(track, stream)
282
+ })
238
283
  return peer.createAnswer()
239
284
  })
240
285
  .then((answer) =>
@@ -251,29 +296,38 @@ function WebRTCConn(options) {
251
296
  let call_info = ev.data.call_info || {}
252
297
  let callid = call_info.call_id
253
298
  if (!callid) return
299
+ let type = ev.type
300
+ if (type === 'call_invited') {
301
+ if (ev.type === 'call_invited' && agid == call_info.from_number) return // ignore our own invite
302
+ serverCalls[callid] = call_info
303
+ return publish(ev)
304
+ }
305
+
306
+ if (type == 'call_ended') {
307
+ serverCalls[callid] = call_info
308
+ return publish(ev)
309
+ }
254
310
 
255
311
  if (ev.type === 'call_invite_expired') {
256
- let device_id = call_info.device_id
257
- if (device_id == connId) return // ignore since we have accepted the call
312
+ if (Object.keys(peers).find((cid) => cid == call_info.device_id)) return // this call invitation is expired by us => ignore
258
313
  publish(Object.assign({}, ev, {type: 'call_ended'})) // fake ended call
259
- delete activeCalls[callid]
314
+ return
260
315
  }
261
316
 
262
- let type = ev.type
263
- if (type == 'call_ended' || type == 'call_ringing' || type == 'call_joined' || type === 'call_invited') {
264
- if (ev.type === 'call_invited' && agid == call_info.from_number) return // ignore our own invite
265
- activeCalls[callid] = call_info
317
+ let connId = call_info.device_id
318
+ if (!Object.keys(peers).find((cid) => cid == connId)) return
319
+ if (type == 'call_ringing' || type == 'call_joined') {
320
+ serverCalls[callid] = call_info
266
321
  return publish(ev)
267
322
  }
268
323
  })
269
324
 
270
- this.makeCall = (number, fromnumber, stream, callid) => {
325
+ this.makeCall = (number, fromnumber, streamPm, callid) => {
271
326
  if (!callid) callid = 'webcall-' + randomString(30)
272
327
  let now = env.Date.now()
273
328
  let call = {
274
329
  account_id: accid,
275
330
  call_id: callid,
276
- device_id: connId,
277
331
  direction: 'outbound',
278
332
  member_id: agid,
279
333
  started: now,
@@ -283,28 +337,32 @@ function WebRTCConn(options) {
283
337
  dialingRequest[callid] = call
284
338
  publish({type: 'call_ringing', data: {call_info: {call_id: callid}}}) // fake dialing
285
339
  return new env.Promise((rs) => {
286
- this.joinCall(callid, stream, (err) => {
287
- if (err) {
288
- delete dialingRequest[callid]
289
- endRequest[callid] = env.Date.now()
290
- publish({type: 'call_ended', data: {call_info: {call_id: callid}}})
291
- return rs({error: err})
292
- }
293
-
294
- let url = `${apiUrl}call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${number}&from_number=${fromnumber}`
295
- callAPI('post', url, undefined, (body, code) => {
296
- if (code != 200) {
297
- delete dialingRequest[callid]
298
- endRequest[callid] = env.Date.now()
299
- publish({type: 'call_ended', data: {call_info: {call_id: callid}}})
300
- return rs({error: err})
301
- }
302
- body = parseJSON(body)
303
- if (body) activeCalls[callid] = body
304
- delete dialingRequest[callid]
305
- rs({body})
340
+ streamPm
341
+ .then((stream) => {
342
+ this.joinCall(callid, stream, (err, connId) => {
343
+ if (err) {
344
+ delete dialingRequest[callid]
345
+ endRequest[callid] = env.Date.now()
346
+ publish({type: 'call_ended', data: {call_info: {call_id: callid}}})
347
+ return rs({error: err})
348
+ }
349
+
350
+ let url = `${apiUrl}call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${number}&from_number=${fromnumber}`
351
+ callAPI('post', url, undefined, (body, code) => {
352
+ if (code != 200) {
353
+ delete dialingRequest[callid]
354
+ endRequest[callid] = env.Date.now()
355
+ publish({type: 'call_ended', data: {call_info: {call_id: callid}}})
356
+ return rs({error: err})
357
+ }
358
+ body = parseJSON(body)
359
+ if (body) serverCalls[callid] = body
360
+ delete dialingRequest[callid]
361
+ rs({body})
362
+ })
363
+ })
306
364
  })
307
- })
365
+ .catch((err) => rs({error: err}))
308
366
  })
309
367
  }
310
368
 
@@ -312,47 +370,48 @@ function WebRTCConn(options) {
312
370
  if (!callid) return
313
371
  endRequest[callid] = env.Date.now()
314
372
 
315
- let call = activeCalls[callid]
373
+ let call = serverCalls[callid]
316
374
  if (!call) {
317
375
  call = {call_id: callid}
318
- activeCalls[callid] = call
376
+ serverCalls[callid] = call
319
377
  }
320
378
  call.status = 'ended'
321
379
  call.ended = env.Date.now()
322
380
  publish({type: 'call_ended', data: {call_info: {call_id: callid}}})
323
-
324
- callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`)
381
+ callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&call_id=${callid}`)
325
382
  }
326
383
 
327
384
  this.listenCall = (callid, cb) => {
328
- let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
329
- makeSure(() => callAPI('post', url, undefined, (body, code) => (code != 200 ? cb(body) : cb())))
385
+ initSession((err, connId) => {
386
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
387
+ callAPI('post', url, undefined, (body, code) => (code != 200 ? cb(body) : cb()))
388
+ })
330
389
  }
331
390
 
332
391
  this.joinCall = (callid, stream, cb) => {
333
- callStream[callid] = stream
334
392
  console.time('makecall' + callid)
335
- makeSure(() => {
336
- pendingStream = stream
393
+ initSession((err, connId) => {
394
+ call2Connection[callid] = connId
395
+ streams[connId] = stream
337
396
  let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&talk=true`
338
397
  callAPI('post', url, undefined, (body, code) => {
339
- if (code != 200) return cb(body)
398
+ if (code != 200) return cb(body, connId)
340
399
  console.timeEnd('makecall' + callid)
341
- if (code != 200) return cb(body)
342
- checkIceConnected(() => cb())
400
+ if (code != 200) return cb(body, connId)
401
+ checkIceConnected(peers[connId], () => cb(undefined, connId))
343
402
  })
344
403
  })
345
404
  }
346
405
 
347
406
  this.answerCall = (callid, stream) => {
348
- let call = activeCalls[callid]
407
+ let call = serverCalls[callid]
349
408
  if (call && call.status == 'ended') return Promise.resolve({error: 'call_ended'})
350
409
  let now = env.Date.now()
351
410
  if (answerRequest[callid]) return Promise.resolve({body: call})
352
411
  answerRequest[callid] = now
353
412
  publish({type: 'call_ringing', data: {call_info: {call_id: callid}}}) // fake dialing
354
413
  return new env.Promise((rs) => {
355
- this.joinCall(callid, stream, (err) => {
414
+ this.joinCall(callid, stream, (err, connId) => {
356
415
  if (err) {
357
416
  delete answerRequest[callid]
358
417
  endRequest[callid] = now
@@ -377,8 +436,11 @@ function WebRTCConn(options) {
377
436
  this.sendDtmf = (key, callid) =>
378
437
  new env.Promise((rs) => {
379
438
  if (!callid) return rs({body: {}})
439
+ let connId = call2Connection[callid]
440
+ if (!connId) return rs({body: {}})
441
+
380
442
  let url = `${apiUrl}dtml?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&key={key}`
381
- makeSure(() =>
443
+ initSession(() =>
382
444
  callAPI('post', url, undefined, (body, bode) => {
383
445
  body = parseJSON(body)
384
446
  return code != 200 ? rs({error: body}) : rs({body})
@@ -389,8 +451,10 @@ function WebRTCConn(options) {
389
451
  this.transferCall = (tonumber, callid) =>
390
452
  new env.Promise((rs) => {
391
453
  if (!callid) return rs({})
454
+ let connId = call2Connection[callid]
455
+ if (!connId) return rs({body: {}})
392
456
  let url = `${apiUrl}refer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number={tonumber}`
393
- makeSure(() =>
457
+ initSession(() =>
394
458
  callAPI('post', url, undefined, (body, code) => {
395
459
  body = parseJSON(body)
396
460
  return code != 200 ? rs({error: body}) : rs({body})