@subiz/wsclient 4.0.21 → 4.0.22

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/demo.html ADDED
@@ -0,0 +1,129 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link
6
+ rel="stylesheet"
7
+ href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css"
8
+ />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>Subiz WebPhone Demo</title>
11
+ <style>
12
+ #info {
13
+ background: whitesmoke;
14
+ border-radius: 5px;
15
+ }
16
+ label {
17
+ font-weight: 600;
18
+ margin-right: 10px;
19
+ }
20
+ .info_line {
21
+ display: flex;
22
+ align-items: flex-start;
23
+ }
24
+ </style>
25
+ </head>
26
+ <body class="container pt-3">
27
+ <img
28
+ width="80"
29
+ src="https://storage.googleapis.com/sbz-email/images/d009afe944c1948998544e103ab0fef2_logo.png"
30
+ />
31
+ <h1 class="mt-4">Subiz call demo</h1>
32
+ <button
33
+ class="btn btn-primary"
34
+ style="width: 120px"
35
+ id="btnHangup"
36
+ onClick="inputToken()"
37
+ >
38
+ Nhập token
39
+ </button>
40
+ <div id="tokenStatus" class="mt-2 text-danger">Chưa nhập token</div>
41
+
42
+ <div class="mt-3" style="width: 250px">
43
+ <div>
44
+ <label>Đầu số</label>
45
+ <input
46
+ class="form-control"
47
+ id="fromNumber"
48
+ type="text"
49
+ value="842473021368"
50
+ />
51
+ </div>
52
+ <div class="mt-3">
53
+ <label>Số khách</label>
54
+ <input
55
+ class="form-control"
56
+ placeholder="036..."
57
+ id="toNumber"
58
+ type="text"
59
+ />
60
+ </div>
61
+ <div class="mt-3">
62
+ <button
63
+ class="btn btn-success mr-3"
64
+ style="width: 150px"
65
+ id="btnCall"
66
+ disabled
67
+ onClick="makeCall()"
68
+ >
69
+ Gọi
70
+ </button>
71
+ <button
72
+ class="btn btn-outline-danger"
73
+ style="width: 79px"
74
+ id="btnHangup"
75
+ onClick="hangupCall()"
76
+ >
77
+ Ngắt
78
+ </button>
79
+ </div>
80
+ </div>
81
+ <h3 class="mt-4">Thông tin cuộc gọi</h3>
82
+ <div class="p-2" id="info">
83
+ <div class="info_line">
84
+ <label>Call ID:</label>
85
+ <div id="infoCallId"></div>
86
+ </div>
87
+ <div class="info_line">
88
+ <label>Conversation ID:</label>
89
+ <div id="infoConversationId"></div>
90
+ </div>
91
+ <div class="info_line">
92
+ <label>Ghi âm:</label>
93
+ <a href="#" id="infoRecordedUrl" target="_blank"></a>
94
+ </div>
95
+ <div class="info_line">
96
+ <label>Gọi từ số:</label>
97
+ <div id="infoFromNumber"></div>
98
+ </div>
99
+ <div class="info_line">
100
+ <label>Gọi đến số:</label>
101
+ <div id="infoToNumber"></div>
102
+ </div>
103
+ <div class="info_line">
104
+ <label>Trạng thái:</label>
105
+ <div id="infoStatus"></div>
106
+ </div>
107
+ <div class="info_line">
108
+ <label>Gọi lúc:</label>
109
+ <div id="infoCreated"></div>
110
+ </div>
111
+ <div class="info_line">
112
+ <label>Thời lượng (giây):</label>
113
+ <div id="infoDuration"></div>
114
+ </div>
115
+ <div class="info_line">
116
+ <label>Mã ngắt máy:</label>
117
+ <div id="infoHangupCode"></div>
118
+ </div>
119
+ </div>
120
+ <div class="mt-4">
121
+ <h3>Sự kiện realtime</h3>
122
+ <pre
123
+ style="padding: 10px 15px; border-radius: 5px; background: whitesmoke"
124
+ id="infoLog"
125
+ ></pre>
126
+ </div>
127
+ <script type="module" src="./demo.js"></script>
128
+ </body>
129
+ </html>
package/demo.js ADDED
@@ -0,0 +1,207 @@
1
+ const SubizWebPhone = require('./webphone.js')
2
+
3
+ let webphone
4
+ var mycallid = 'x'
5
+ let accid
6
+ let eventLogs = {}
7
+
8
+ // setTokenStatus renders whether token is valid or invalid
9
+ function setTokenStatus(me) {
10
+ let $tokenStatus = document.getElementById('tokenStatus')
11
+ if (me == 'loading') {
12
+ $tokenStatus.innerHTML = 'Đang kiểm tra'
13
+ $tokenStatus.classList.add('text-muted')
14
+ $tokenStatus.classList.remove('text-danger')
15
+ $tokenStatus.classList.remove('text-success')
16
+ return
17
+ }
18
+
19
+ if (!me || !me.account_id) {
20
+ $tokenStatus.innerHTML = 'Token rỗng hoặc không hợp lệ. Vui lòng nhập lại'
21
+ $tokenStatus.classList.remove('text-danger')
22
+ $tokenStatus.classList.remove('text-success')
23
+ $tokenStatus.classList.add('text-muted')
24
+ return
25
+ }
26
+
27
+ $tokenStatus.innerHTML = 'Token hợp lệ. Xin chào ' + me.fullname
28
+ $tokenStatus.classList.add('text-success')
29
+ $tokenStatus.classList.remove('text-danger')
30
+ $tokenStatus.classList.remove('text-muted')
31
+ }
32
+
33
+ function init() {
34
+ setTokenStatus('loading')
35
+ fetch(`https://api.subiz.com.vn/4.0/me?x-access-token=${access_token}`)
36
+ .then((response) => {
37
+ return response.json()
38
+ })
39
+ .then((me) => {
40
+ setTokenStatus(me)
41
+ if (!me || !me.account_id) return null
42
+ return me
43
+ })
44
+ .then((me) => {
45
+ if (!me) return
46
+ // save token
47
+ try {
48
+ webphone = new SubizWebPhone(access_token)
49
+ accid = access_token.split('_')[0]
50
+ } catch (e) {
51
+ alert('ERROR2: ' + e)
52
+ // setTimeout(main)
53
+ return
54
+ }
55
+
56
+ document.getElementById('btnCall').disabled = false
57
+
58
+ webphone.onEvent(function (ev) {
59
+ let call = ev.data.call_info
60
+ let callid = call.call_id
61
+ eventLogs[callid] = eventLogs[callid] || []
62
+ if (callid != mycallid) return
63
+ eventLogs[callid].push(ev)
64
+
65
+ // render log
66
+ let logM = eventLogs[callid]
67
+ if (logM) {
68
+ let log = logM
69
+ .map((ev) => {
70
+ return `${new Date(ev.created).toLocaleString()} | ${ev.type} | ${JSON.stringify(ev.data.call_info)}`
71
+ })
72
+ .join('\n')
73
+ document.getElementById('infoLog').innerHTML = log
74
+ }
75
+
76
+ if (call.status == 'dialing') {
77
+ document.getElementById('infoRecordedUrl').innerHTML = ''
78
+ document.getElementById('infoRecordedUrl').href = undefined
79
+ document.getElementById('info').style['background-color'] = '#fffed3'
80
+ }
81
+
82
+ if (call.status == 'active') {
83
+ document.getElementById('info').style['background-color'] = '#8cff6b'
84
+ }
85
+
86
+ if (call.status == 'ended' || !call.status) {
87
+ document.getElementById('info').style['background-color'] = '#ffc6c6'
88
+ }
89
+
90
+ if (call.status == 'ended' && ev.id) {
91
+ // from server, not fake
92
+ document.getElementById('infoRecordedUrl').innerHTML = 'Đang tải...'
93
+ document.getElementById('infoRecordedUrl').href = undefined
94
+
95
+ fetchRecordedUrl(callid).then((url) => {
96
+ if (url == 'unavailable') {
97
+ document.getElementById('infoRecordedUrl').innerHTML = 'Không khả dụng'
98
+ document.getElementById('infoRecordedUrl').href = undefined
99
+ return
100
+ }
101
+ document.getElementById('infoRecordedUrl').innerHTML = url
102
+ document.getElementById('infoRecordedUrl').href = url
103
+ })
104
+ }
105
+ })
106
+ })
107
+ }
108
+
109
+ // inputToken asks access token from the agent
110
+ function inputToken() {
111
+ access_token = prompt('Vui lòng nhập access token')
112
+ localStorage.setItem('access_token', access_token)
113
+ if (access_token) init()
114
+ }
115
+
116
+ // fetchRecordedUrl attemps to get the recorded audio url
117
+ // since we dont know when the file is uploaded, we must retry multiple times.
118
+ // this code is only for demo purpose
119
+ function fetchRecordedUrl(callid) {
120
+ // try maximum 4 times
121
+ return new Promise((rs) => {
122
+ fetchConversation(callid).then((convo) => {
123
+ if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
124
+ return rs(convo.call.recorded_audio.url)
125
+ }
126
+ setTimeout(() => {
127
+ fetchConversation(callid).then((convo) => {
128
+ if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
129
+ return rs(convo.call.recorded_audio.url)
130
+ }
131
+
132
+ setTimeout(() => {
133
+ fetchConversation(callid).then((convo) => {
134
+ if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
135
+ return rs(convo.call.recorded_audio.url)
136
+ }
137
+
138
+ setTimeout(() => {
139
+ fetchConversation(callid).then((convo) => {
140
+ if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
141
+ return rs(convo.call.recorded_audio.url)
142
+ }
143
+ rs('unavailable')
144
+ })
145
+ }, 4000)
146
+ })
147
+ }, 4000)
148
+ })
149
+ }, 4000)
150
+ })
151
+ })
152
+ }
153
+
154
+ // fetchConversation sends a request API to get the latest conversation
155
+ function fetchConversation(callid) {
156
+ let call = webphone.getCall(callid)
157
+ if (!call) return Promise.resolve(null)
158
+ let convoid = call.conversation_id
159
+ if (!convoid) return Promise.resolve(null)
160
+ return fetch(
161
+ `https://api.subiz.com.vn/4.0/accounts/${accid}/conversations/${convoid}?x-access-token=${access_token}`,
162
+ ).then((response) => {
163
+ return response.json()
164
+ })
165
+ }
166
+
167
+ function makeCall() {
168
+ if (!webphone) return
169
+ let fromNumber = document.getElementById('fromNumber').value
170
+ let toNumber = document.getElementById('toNumber').value
171
+ //webphone.makeCall(toNumber, fromNumber);
172
+
173
+ webphone.makeCall('0364821895', ['935022139843821727', '842473021368'])
174
+ mycallid = webphone.getCurrentCallId()
175
+ }
176
+
177
+ let access_token = localStorage.getItem('access_token')
178
+ if (access_token) init()
179
+
180
+ // update loop
181
+ setInterval(function () {
182
+ if (!webphone) return
183
+ let call = webphone.getCall(mycallid)
184
+ if (!call) return
185
+ document.getElementById('infoCallId').innerHTML = call.call_id
186
+ document.getElementById('infoFromNumber').innerHTML = call.from_number
187
+ document.getElementById('infoConversationId').innerHTML = call.conversation_id
188
+ document.getElementById('infoToNumber').innerHTML = call.to_number
189
+ document.getElementById('infoStatus').innerHTML = call.status
190
+ document.getElementById('infoCreated').innerHTML = new Date(call.started).toLocaleString()
191
+ if (call.status == 'active')
192
+ document.getElementById('infoDuration').innerHTML = Math.floor((Date.now() - call.started) / 1000) || 0
193
+ else {
194
+ document.getElementById('infoDuration').innerHTML = Math.floor((call.ended - call.started) / 1000) || 0
195
+ }
196
+ if (call.hangup_code) document.getElementById('infoHangupCode').innerHTML = call.hangup_code
197
+ }, 500)
198
+
199
+ function hangupCall() {
200
+ if (!webphone) return
201
+ webphone.hangupCall(mycallid)
202
+ }
203
+
204
+ // expose to window so we can call it in HTML
205
+ window.makeCall = makeCall
206
+ window.hangupCall = hangupCall
207
+ window.inputToken = inputToken
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "4.0.21",
3
+ "version": "4.0.22",
4
4
  "main": "realtime.js",
5
5
  "scripts": {
6
+ "dev": "vite",
6
7
  "test": "node realtime.test.js"
7
8
  },
8
9
  "repository": {
@@ -14,10 +15,11 @@
14
15
  "homepage": "https://github.com/subiz/wsclient#readme",
15
16
  "description": "",
16
17
  "devDependencies": {
17
- "tape": "^5.7.2"
18
+ "tape": "^5.7.3",
19
+ "vite": "^5.0.4",
20
+ "vite-plugin-commonjs": "^0.10.1"
18
21
  },
19
22
  "dependencies": {
20
- "@subiz/flow": "^1.1.0",
21
- "request": "^2.88.2"
23
+ "@subiz/flow": "^1.1.0"
22
24
  }
23
25
  }
package/vite.config.js ADDED
@@ -0,0 +1,16 @@
1
+ import {defineConfig} from 'vite'
2
+ import commonjs from 'vite-plugin-commonjs'
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ commonjs({
7
+ filter(id) {
8
+ if (id.includes('node_modules/redux-storage/build-es')) {
9
+ return true
10
+ }
11
+ },
12
+ }),
13
+ // Other plugins
14
+ ],
15
+ // Other configurations
16
+ })
package/webphone.js CHANGED
@@ -56,6 +56,31 @@ function WebPhone(access_token, realtime) {
56
56
  },
57
57
  })
58
58
 
59
+ var _numbers = []
60
+ var _fetchNumbers = function () {
61
+ xhrsend(
62
+ 'get',
63
+ `https://api.subiz.com.vn/4.0/integrations?x-access-token=${access_token}`,
64
+ undefined,
65
+ function (body, code) {
66
+ setTimeout(_fetchNumbers, 120_000)
67
+ if (code != 200) return
68
+ var integrations = parseJSON(body)
69
+ if (!integrations || !integrations.length) return
70
+ var numbers = []
71
+ for (var i = 0; i < integrations.length; i++) {
72
+ var integration = integrations[i] || {}
73
+ if (integration.connector_type == 'call' && integration.state == 'activated') {
74
+ var number = integration.id.split('.')[1]
75
+ numbers.push(number)
76
+ }
77
+ }
78
+ _numbers = numbers
79
+ },
80
+ )
81
+ }
82
+ _fetchNumbers()
83
+
59
84
  this.onEvent = function (cb) {
60
85
  return pubsub.on('event', cb)
61
86
  }
@@ -129,23 +154,54 @@ function WebPhone(access_token, realtime) {
129
154
  }
130
155
 
131
156
  var base = '0123456789abcdef'
132
- this.makeCall = function (number, fromnumber, streamPm) {
133
- // '11edc52b-2918-4d71-9058-f7285e29d894'
134
- var callid =
135
- randomString(8, base) +
136
- '-' +
137
- randomString(4, base) +
138
- '-' +
139
- randomString(4, base) +
140
- '-' +
141
- randomString(4, base) +
142
- '-' +
143
- randomString(12, base)
157
+ // makeCall(number, [] || undefined || '', streamPm) // fallback all number
158
+ // makeCall(number, ['a','b','c'], streamPm) // fallback specific number
159
+ // makeCall(number, function() {}, streamPm) //
160
+ this.makeCall = function (number, fromnumbers, streamPm, _a, _b, _state) {
161
+ if (!_state) _state = {}
162
+ if (!fromnumbers || (Array.isArray(fromnumbers) && fromnumbers.length == 0)) {
163
+ fromnumbers = _numbers
164
+ }
165
+
166
+ if (typeof fromnumbers === 'string' || fromnumbers instanceof String || typeof value === 'number') {
167
+ fromnumbers = [fromnumbers + '']
168
+ }
144
169
 
170
+ // '11edc52b-2918-4d71-9058-f7285e29d894'
171
+ var callid = `${randomString(8, base)}-${randomString(4, base)}-${randomString(4, base)}`
172
+ callid += `-${randomString(4, base)}-${randomString(12, base)}`
145
173
  current_call_id = callid
146
174
 
147
175
  if (!streamPm) streamPm = getMicroPermissions()
148
- return webrtcconn.makeCall(number, fromnumber, streamPm, callid)
176
+
177
+ let callnumber = chooseCallNumber(fromnumbers, _state.called)
178
+ if (!callnumber)
179
+ return (
180
+ _state.last_result || {
181
+ start: Date.now(),
182
+ ended: Date.now(),
183
+ direction: 'outbound',
184
+ account_id: accid,
185
+ to_number: number,
186
+ error: 'out_of_number',
187
+ id: callid,
188
+ hangup_code: 'failure',
189
+ status: 'ended',
190
+ }
191
+ )
192
+ return webrtcconn.makeCall(number, callnumber, streamPm, callid).then((out) => {
193
+ if (out.error) return out
194
+ if (out.hangup_code == 'cancel') return out
195
+ if (out.answered > 0) return out
196
+
197
+ if (out.hangup_code == 'terminated' || out.hangup_code == 'failure' || out.hangup_code == 'congestion') {
198
+ if (!_state.called) _state.called = {}
199
+ _state.called[callnumber] = true
200
+ _state.last_result = out
201
+ return this.makeCall(number, fromnumbers, streamPm, _a, _b, _state)
202
+ }
203
+ return out
204
+ })
149
205
  }
150
206
 
151
207
  var current_call_id = ''
@@ -183,3 +239,14 @@ function WebPhone(access_token, realtime) {
183
239
 
184
240
  window.SbzWebPhone1 = WebPhone
185
241
  module.exports = WebPhone
242
+
243
+ function chooseCallNumber(numbers, called) {
244
+ if (!called) called = {}
245
+ if (!numbers || !numbers.length) return ''
246
+ for (var i = 0; i < numbers.length; i++) {
247
+ var number = numbers[i]
248
+ if (called[number]) continue
249
+ return number
250
+ }
251
+ return ''
252
+ }
package/webrtc.js CHANGED
@@ -168,6 +168,7 @@ function WebRTCConn(options) {
168
168
  }
169
169
  env.setInterval(cleanEndedCall, 120000)
170
170
 
171
+ let endcallCb = {}
171
172
  let publish = (ev) => {
172
173
  if (ev.type == 'call_ended') {
173
174
  let callid = ev.data && ev.data.call_info && ev.data.call_info.call_id
@@ -181,6 +182,13 @@ function WebRTCConn(options) {
181
182
  // do not clean up this now or some functions cannot work after the call ended
182
183
  // DONOT: delete call2Connection[callid]
183
184
  delete streams[connId]
185
+
186
+ Object.keys(endcallCb).map((callid) => {
187
+ let arr = endcallCb[callid]
188
+ if (!arr || arr.length == 0 || !arr.map) return
189
+ arr.map((cb) => cb())
190
+ delete endcallCb[callid]
191
+ })
184
192
  }
185
193
 
186
194
  let direction = ev.data && ev.data.call_info && ev.data.call_info.direction
@@ -371,7 +379,11 @@ function WebRTCConn(options) {
371
379
  body = parseJSON(body)
372
380
  if (body) serverCalls[callid] = body
373
381
  delete dialingRequest[callid]
374
- rs({body})
382
+
383
+ let call = this.matchCall(callid)
384
+ if (call && call.status == 'ended') return rs(call)
385
+ if (!endcallCb[callid]) endcallCb[callid] = []
386
+ endcallCb[callid].push(() => rs(this.matchCall(callid)))
375
387
  })
376
388
  })
377
389
  })
@@ -389,6 +401,7 @@ function WebRTCConn(options) {
389
401
  serverCalls[callid] = call
390
402
  }
391
403
  call.status = 'ended'
404
+ call.hangup_code = 'cancel'
392
405
  call.ended = env.Date.now()
393
406
  publish({created: env.Date.now(), type: 'call_ended', data: {call_info: {call_id: callid}}})
394
407
  callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&call_id=${callid}`)