@subiz/wsclient 4.0.28 → 4.0.30
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/package.json +2 -2
- package/realtime.js +111 -39
- package/realtime.test.js +276 -83
- package/demo.html +0 -129
- package/demo.js +0 -207
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subiz/wsclient",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.30",
|
|
4
4
|
"main": "realtime.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"description": "",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"tape": "^5.9.0",
|
|
19
|
-
"vite": "^6.
|
|
19
|
+
"vite": "^6.3.3",
|
|
20
20
|
"vite-plugin-commonjs": "^0.10.4"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
package/realtime.js
CHANGED
|
@@ -8,9 +8,9 @@ var {xhrsend, parseJSON, Pubsub} = require('./common.js')
|
|
|
8
8
|
// as fast as possible through onEvents callback.
|
|
9
9
|
//
|
|
10
10
|
// It will try it best to keep the communication with the realtime
|
|
11
|
-
// server. But if the user is offline for too long or
|
|
12
|
-
// keep up with
|
|
13
|
-
//
|
|
11
|
+
// server. But if the user is offline for too long or the network can't
|
|
12
|
+
// keep up with incomming events (rarely happend with today internet speed).
|
|
13
|
+
// The connection will be killed. Dead.
|
|
14
14
|
//
|
|
15
15
|
// Once dead, onDead will be called. After that, the connection is
|
|
16
16
|
// useless, all resources will be released.
|
|
@@ -18,38 +18,44 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
18
18
|
callAPI = callAPI || xhrsend // allow hook
|
|
19
19
|
credential = credential || {}
|
|
20
20
|
// tell if connection is dead
|
|
21
|
-
var dead = false
|
|
22
21
|
|
|
23
22
|
var lastToken = ''
|
|
24
|
-
// kill force the connection to dead state
|
|
25
|
-
this.kill = function () {
|
|
26
|
-
dead = true
|
|
27
|
-
}
|
|
28
23
|
if (!accid) accid = ''
|
|
29
24
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
25
|
+
var status = 'active' // active | polling (trouble while polling) | subbing (trouble while subscribing) | dead
|
|
26
|
+
|
|
27
|
+
// The long polling loop runs sequentially, with each poll starting immediately
|
|
28
|
+
// after the previous one completes. If an error occurs, the loop will pause
|
|
29
|
+
// using an exponential backoff strategy. It will terminate if an unretryable
|
|
30
|
+
// error is encountered. The polling loop begins after the first subscription
|
|
31
|
+
// call completes successfully
|
|
35
32
|
var polling = function (backoff) {
|
|
36
|
-
if (dead) return
|
|
33
|
+
if (status == 'dead') return
|
|
37
34
|
callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account-id=' + accid, undefined, function (body, code) {
|
|
38
|
-
if (dead) return
|
|
39
|
-
|
|
35
|
+
if (status == 'dead') return
|
|
36
|
+
|
|
37
|
+
if (retryable(code)) {
|
|
38
|
+
if (status == 'active') status = 'polling'
|
|
39
|
+
return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
|
|
40
|
+
}
|
|
41
|
+
|
|
40
42
|
if (code !== 200) {
|
|
41
43
|
// unretryable error, should kill
|
|
42
|
-
|
|
44
|
+
status = 'dead'
|
|
43
45
|
return onDead('poll')
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
if (status == 'polling') status = 'active'
|
|
46
49
|
// 200, success
|
|
47
50
|
body = parseJSON(body) || {}
|
|
48
51
|
if (body.host) apiUrl = absUrl(body.host)
|
|
49
52
|
|
|
50
53
|
var seqToken = body.sequential_token
|
|
51
|
-
// the server returns a malform payload. We should
|
|
52
|
-
if (!seqToken)
|
|
54
|
+
// the server returns a malform payload. We should end
|
|
55
|
+
if (!seqToken) {
|
|
56
|
+
status = 'dead'
|
|
57
|
+
return onDead('poll')
|
|
58
|
+
}
|
|
53
59
|
lastToken = seqToken
|
|
54
60
|
onEvents(body.events || [])
|
|
55
61
|
return polling(0)
|
|
@@ -57,44 +63,90 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
var subQueue = flow.batch(50, 100, function (events) {
|
|
60
|
-
if (dead) return
|
|
66
|
+
if (status == 'dead') return repeat('dead', events.length)
|
|
61
67
|
if (events.length <= 0) return []
|
|
62
68
|
|
|
69
|
+
var done = false
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
if (done) return
|
|
72
|
+
if (status == 'dead') return
|
|
73
|
+
if (status == 'active') status = 'subbing'
|
|
74
|
+
}, 4000)
|
|
75
|
+
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
// too long, kill
|
|
78
|
+
if (done) return
|
|
79
|
+
done = true
|
|
80
|
+
if (status == 'dead') return
|
|
81
|
+
status = 'dead' // mark as dead
|
|
82
|
+
onDead('poll')
|
|
83
|
+
out = repeat('dead', events.length)
|
|
84
|
+
resolve(false) // break loop
|
|
85
|
+
}, 60000)
|
|
86
|
+
|
|
87
|
+
var resolve
|
|
88
|
+
var promise = new Promise((rs) => {
|
|
89
|
+
resolve = rs
|
|
90
|
+
})
|
|
63
91
|
var out = []
|
|
64
|
-
|
|
92
|
+
flow
|
|
65
93
|
.loop(function () {
|
|
66
94
|
return new Promise(function (rs) {
|
|
95
|
+
if (status == 'dead') {
|
|
96
|
+
out = repeat('dead', events.length)
|
|
97
|
+
return rs(false) // break loop
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
var query = '?token=' + lastToken
|
|
68
101
|
credential.getAccessToken().then(function (access_token) {
|
|
102
|
+
if (status == 'dead') {
|
|
103
|
+
out = repeat('dead', events.length)
|
|
104
|
+
return rs(false) // break loop
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
70
108
|
else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
71
109
|
else if (access_token) query += '&access-token=' + access_token
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
var fullurl = apiUrl + 'subs' + query + '&account-id=' + encodeURIComponent(accid || credential.account_id)
|
|
74
112
|
callAPI('post', fullurl, JSON.stringify({events: events}), function (body, code) {
|
|
75
|
-
if (
|
|
113
|
+
if (done) return
|
|
114
|
+
if (status == 'subbing') status = 'active'
|
|
115
|
+
|
|
116
|
+
if (status == 'dead') {
|
|
76
117
|
out = repeat('dead', events.length)
|
|
77
118
|
return rs(false) // break loop
|
|
78
119
|
}
|
|
79
120
|
|
|
80
|
-
if (retryable(code))
|
|
121
|
+
if (retryable(code)) {
|
|
122
|
+
status = 'subbing'
|
|
123
|
+
return setTimeout(rs, 3000, true)
|
|
124
|
+
}
|
|
125
|
+
|
|
81
126
|
// unretryable error
|
|
82
127
|
if (code !== 200) {
|
|
83
|
-
// we are unable to start the communication with realtime server.
|
|
128
|
+
// we are unable to start the communication with the realtime server.
|
|
84
129
|
// Must notify the user by killing the connection
|
|
85
|
-
|
|
130
|
+
status = 'dead'
|
|
86
131
|
onDead('subscribe', body, code)
|
|
87
132
|
out = repeat('dead', events.length)
|
|
88
133
|
return rs(false)
|
|
89
134
|
}
|
|
90
135
|
|
|
136
|
+
done = true
|
|
91
137
|
if (lastToken) return rs(false)
|
|
92
138
|
|
|
93
139
|
// first time sub, should grab the initial token
|
|
94
140
|
body = parseJSON(body) || {}
|
|
95
141
|
var initialToken = body.initial_token
|
|
96
|
-
// the server returns a malform payload. We should
|
|
97
|
-
if (!initialToken)
|
|
142
|
+
// the server returns a malform payload. We should end
|
|
143
|
+
if (!initialToken) {
|
|
144
|
+
status = 'dead'
|
|
145
|
+
onDead('subscribe', body, code)
|
|
146
|
+
out = repeat('dead', events.length)
|
|
147
|
+
return rs(false) // break loop
|
|
148
|
+
}
|
|
149
|
+
|
|
98
150
|
if (body.host) apiUrl = absUrl(body.host)
|
|
99
151
|
|
|
100
152
|
lastToken = initialToken
|
|
@@ -105,8 +157,9 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
105
157
|
})
|
|
106
158
|
})
|
|
107
159
|
.then(function () {
|
|
108
|
-
|
|
160
|
+
resolve(out)
|
|
109
161
|
})
|
|
162
|
+
return promise
|
|
110
163
|
})
|
|
111
164
|
|
|
112
165
|
// subscribe call /subs API to tell server that we are listening for those events
|
|
@@ -114,6 +167,16 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
114
167
|
// Note: this function is not design to be thread safed so, do not call this
|
|
115
168
|
// function multiple times at once
|
|
116
169
|
this.subscribe = subQueue.push.bind(subQueue)
|
|
170
|
+
|
|
171
|
+
this.getStatus = function () {
|
|
172
|
+
if (status == 'dead' || status == 'active') return status
|
|
173
|
+
return 'connecting'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// kill force the connection to dead state
|
|
177
|
+
this.kill = function () {
|
|
178
|
+
status = 'dead'
|
|
179
|
+
}
|
|
117
180
|
}
|
|
118
181
|
|
|
119
182
|
// Realtime is just a stronger Conn.
|
|
@@ -122,7 +185,7 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
122
185
|
// additional features compare to conn:
|
|
123
186
|
// + auto recreate and resub if the last conn is dead
|
|
124
187
|
// + don't subscribe already subscribed events
|
|
125
|
-
function Realtime(apiUrls, credential, callAPI, accid) {
|
|
188
|
+
function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
126
189
|
if (typeof apiUrls === 'string' || apiUrls instanceof String) apiUrls = [apiUrls]
|
|
127
190
|
credential = credential || {}
|
|
128
191
|
if (!credential.getAccessToken) {
|
|
@@ -158,6 +221,11 @@ function Realtime(apiUrls, credential, callAPI, accid) {
|
|
|
158
221
|
return pubsub.on('interrupted', cb)
|
|
159
222
|
}
|
|
160
223
|
|
|
224
|
+
this.getStatus = function () {
|
|
225
|
+
if (!conn) return 'dead'
|
|
226
|
+
return conn.getStatus()
|
|
227
|
+
}
|
|
228
|
+
|
|
161
229
|
var conn
|
|
162
230
|
this.subscribe = function (events) {
|
|
163
231
|
if (stop) return Promise.resolve({})
|
|
@@ -169,9 +237,13 @@ function Realtime(apiUrls, credential, callAPI, accid) {
|
|
|
169
237
|
for (var i = 0; i < events.length; i++) {
|
|
170
238
|
var topic = events[i]
|
|
171
239
|
if (!topic) continue
|
|
172
|
-
if (!topics[topic])
|
|
240
|
+
if (!topics[topic]) {
|
|
241
|
+
topics[topic] = true
|
|
242
|
+
all.push(conn.subscribe(topic))
|
|
243
|
+
}
|
|
173
244
|
}
|
|
174
245
|
if (all.length === 0) return Promise.resolve({})
|
|
246
|
+
|
|
175
247
|
return Promise.all(all).then(function (errs) {
|
|
176
248
|
for (var i = 0; i < errs.length; i++) if (errs[i]) return {error: errs[i]}
|
|
177
249
|
for (var j = 0; j < events.length; j++) topics[events[j]] = true
|
|
@@ -181,24 +253,24 @@ function Realtime(apiUrls, credential, callAPI, accid) {
|
|
|
181
253
|
|
|
182
254
|
// reconnect make sure there is alway a Conn running in the background
|
|
183
255
|
// if the Conn is dead, it recreate a new one
|
|
184
|
-
|
|
185
|
-
if (stop) return
|
|
256
|
+
this.reconnect = function () {
|
|
257
|
+
if (stop) return Promise.resolve(false)
|
|
186
258
|
// reset subscribed topic
|
|
187
259
|
var allTopics = Object.keys(topics)
|
|
188
260
|
for (var i = 0; i < allTopics.length; i++) topics[allTopics[i]] = false
|
|
189
|
-
|
|
190
261
|
var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
|
|
262
|
+
if (conn) conn.kill() // kill old connection
|
|
191
263
|
conn = new Conn(
|
|
192
264
|
randomUrl,
|
|
193
265
|
credential,
|
|
194
266
|
function (code, body, status) {
|
|
195
267
|
if (stop) return
|
|
196
268
|
pubsub.emit('interrupted', code, body, status)
|
|
197
|
-
setTimeout(reconnect, 2000) // reconnect and resubscribe after 2 sec
|
|
269
|
+
if (!skipautoreconnect) setTimeout(this.reconnect, 2000) // reconnect and resubscribe after 2 sec
|
|
198
270
|
},
|
|
199
|
-
function (
|
|
271
|
+
function (events) {
|
|
200
272
|
if (stop) return
|
|
201
|
-
for (var i = 0; i <
|
|
273
|
+
for (var i = 0; i < events.length; i++) pubsub.emit('event', events[i])
|
|
202
274
|
},
|
|
203
275
|
callAPI,
|
|
204
276
|
accid,
|
|
@@ -206,9 +278,9 @@ function Realtime(apiUrls, credential, callAPI, accid) {
|
|
|
206
278
|
|
|
207
279
|
// resubscribe all subscribed events
|
|
208
280
|
var topicKeys = Object.keys(topics)
|
|
209
|
-
|
|
281
|
+
return this.subscribe(topicKeys)
|
|
210
282
|
}
|
|
211
|
-
reconnect()
|
|
283
|
+
this.reconnect()
|
|
212
284
|
}
|
|
213
285
|
|
|
214
286
|
// calcNextBackoff returns number of seconds we must wait
|
package/realtime.test.js
CHANGED
|
@@ -1,99 +1,292 @@
|
|
|
1
1
|
var test = require('tape')
|
|
2
2
|
var Realtime = require('./realtime.js')
|
|
3
3
|
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
async function testScript(t, script) {
|
|
5
|
+
let events = []
|
|
6
|
+
let calls = []
|
|
7
|
+
var realtime = new Realtime(
|
|
8
|
+
'https://api5.subiz.com.vn/',
|
|
9
|
+
{user_mask: 'mask123'},
|
|
10
|
+
(method, url, body, cb) => {
|
|
11
|
+
calls.push({method, url, body, cb})
|
|
12
|
+
},
|
|
13
|
+
'acctest',
|
|
14
|
+
true,
|
|
15
|
+
)
|
|
16
|
+
realtime.onEvent((ev) => events.push(ev))
|
|
17
|
+
|
|
18
|
+
for (let stepi in script.steps) {
|
|
19
|
+
let step = script.steps[stepi]
|
|
20
|
+
if (step.type == 'status') {
|
|
21
|
+
t.equal(realtime.getStatus(), step.status, script.name + '[' + stepi + ']')
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (step.type == 'subscribe') {
|
|
26
|
+
realtime.subscribe(step.topics)
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (step.type == 'reconnect') {
|
|
31
|
+
realtime.reconnect()
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (step.type == 'wait') {
|
|
36
|
+
await sleep(step.timeout)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (step.type == 'call') {
|
|
41
|
+
t.true(step.call_id != undefined) // must define call id in the script
|
|
42
|
+
let start = Date.now()
|
|
43
|
+
let timeout = step.timeout || 10_000
|
|
44
|
+
let istimeout
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (Date.now() - start > timeout) {
|
|
47
|
+
istimeout = true
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
// wait util api is called
|
|
51
|
+
await sleep(100)
|
|
52
|
+
let call
|
|
53
|
+
for (let c = 0; c < calls.length; c++) {
|
|
54
|
+
if (!calls[c].call_id) {
|
|
55
|
+
call = calls[c]
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!call) continue
|
|
60
|
+
call.call_id = step.call_id
|
|
61
|
+
const myUrl = new URL(call.url)
|
|
62
|
+
|
|
63
|
+
if (step.path) t.equal(myUrl.pathname, step.path)
|
|
64
|
+
|
|
65
|
+
if (step.method) t.equal(step.method, call.method)
|
|
66
|
+
|
|
67
|
+
if (step.query) {
|
|
68
|
+
for (var key of Object.keys(step.query)) {
|
|
69
|
+
t.equal(step.query[key], myUrl.searchParams.get(key), script.name)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (step.body) {
|
|
74
|
+
let keys = Object.keys(step.body)
|
|
75
|
+
let callbody
|
|
76
|
+
if (call.body) callbody = JSON.parse(call.body)
|
|
77
|
+
for (var key of keys) {
|
|
78
|
+
t.equal(JSON.stringify(step.body[key]), JSON.stringify(callbody[key]))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
t.true(!istimeout, JSON.stringify(step))
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (step.type == 'response') {
|
|
89
|
+
let call
|
|
90
|
+
for (var c = 0; c < calls.length; c++) {
|
|
91
|
+
if (calls[c].call_id == step.call_id) {
|
|
92
|
+
call = calls[c]
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
t.true(call != undefined)
|
|
98
|
+
call.cb(JSON.stringify(step.body), step.code)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
t.true(false, script.name + '[' + stepi + ']=' + step.type)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// check events
|
|
105
|
+
let scriptevents = script.events || []
|
|
106
|
+
t.equal(events.length, scriptevents.length)
|
|
107
|
+
for (let i in events) {
|
|
108
|
+
t.equal(JSON.stringify(events[i]), JSON.stringify(scriptevents[i]))
|
|
9
109
|
}
|
|
110
|
+
realtime.stop()
|
|
10
111
|
}
|
|
11
112
|
|
|
12
|
-
test('realtime', async (t) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
113
|
+
test.only('realtime', async (t) => {
|
|
114
|
+
let scripts = [
|
|
115
|
+
{
|
|
116
|
+
name: 'starting status',
|
|
117
|
+
steps: [
|
|
118
|
+
{type: 'status', status: 'active'},
|
|
119
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
120
|
+
{type: 'status', status: 'active'},
|
|
121
|
+
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
122
|
+
|
|
123
|
+
{type: 'wait', timeout: 5000},
|
|
124
|
+
{type: 'status', status: 'connecting'},
|
|
125
|
+
{type: 'wait', timeout: 60000},
|
|
126
|
+
{type: 'status', status: 'dead'},
|
|
127
|
+
{type: 'response', call_id: '1', body: {initial_token: 'aaaaaa'}, code: 200},
|
|
128
|
+
{type: 'status', status: 'dead'}, // dead must be dead
|
|
129
|
+
],
|
|
20
130
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
131
|
+
{
|
|
132
|
+
name: 'long subscribe',
|
|
133
|
+
steps: [
|
|
134
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
135
|
+
{type: 'status', status: 'active'},
|
|
136
|
+
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
137
|
+
{type: 'wait', timeout: 5000},
|
|
138
|
+
{type: 'status', status: 'connecting'},
|
|
139
|
+
{type: 'wait', timeout: 3000},
|
|
140
|
+
{type: 'status', status: 'connecting'},
|
|
141
|
+
{type: 'response', call_id: '1', body: {initial_token: 'aaaaaa'}, code: 200},
|
|
142
|
+
{type: 'status', status: 'active'},
|
|
143
|
+
],
|
|
144
|
+
events: [],
|
|
27
145
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
146
|
+
{
|
|
147
|
+
name: 'double subscribe -> do nothing',
|
|
148
|
+
steps: [
|
|
149
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
150
|
+
{type: 'status', status: 'active'},
|
|
151
|
+
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
152
|
+
{type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
|
|
153
|
+
{type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
154
|
+
{type: 'subscribe', topics: ['message_sent', 'message_pong']},
|
|
155
|
+
{type: 'call', call_id: '3', path: '/subs', method: 'post', body: {events: ['message_pong']}},
|
|
156
|
+
],
|
|
35
157
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
158
|
+
{
|
|
159
|
+
name: 'polling unstable',
|
|
160
|
+
steps: [
|
|
161
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
162
|
+
{type: 'status', status: 'active'},
|
|
163
|
+
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
164
|
+
{type: 'wait', timeout: 1000},
|
|
165
|
+
{type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
|
|
166
|
+
{type: 'status', status: 'active'},
|
|
167
|
+
{type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
168
|
+
{type: 'wait', timeout: 30000},
|
|
169
|
+
{type: 'response', call_id: '2', body: 'network disconnected', code: 0},
|
|
170
|
+
{type: 'status', status: 'connecting'},
|
|
171
|
+
{type: 'call', call_id: '3', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
172
|
+
{type: 'wait', timeout: 30000},
|
|
173
|
+
{type: 'response', call_id: '3', body: '500 keep internal', code: 500},
|
|
174
|
+
// more than 60 sec without any data => dead
|
|
175
|
+
{type: 'status', status: 'connecting'},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'polling dead',
|
|
180
|
+
steps: [
|
|
181
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
182
|
+
{type: 'status', status: 'active'},
|
|
183
|
+
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
184
|
+
{type: 'wait', timeout: 1000},
|
|
185
|
+
{type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
|
|
186
|
+
{type: 'status', status: 'active'},
|
|
187
|
+
{type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
188
|
+
{type: 'response', call_id: '2', body: '400 dead', code: 400},
|
|
189
|
+
{type: 'status', status: 'dead'},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'basic',
|
|
194
|
+
steps: [
|
|
195
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
196
|
+
{
|
|
197
|
+
type: 'call',
|
|
198
|
+
call_id: '1',
|
|
199
|
+
path: '/subs',
|
|
200
|
+
query: {'user-mask': 'mask123'},
|
|
201
|
+
method: 'post',
|
|
202
|
+
body: {events: ['message_sent']},
|
|
203
|
+
},
|
|
204
|
+
{type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
|
|
205
|
+
{type: 'wait', timeout: 1000},
|
|
206
|
+
{type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
207
|
+
{type: 'status', status: 'active'},
|
|
208
|
+
{type: 'wait', timeout: 3000},
|
|
209
|
+
{type: 'response', call_id: '2', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
|
|
210
|
+
{type: 'status', status: 'active'},
|
|
211
|
+
{type: 'call', call_id: '3', path: '/poll', query: {token: 'token2'}, method: 'get'},
|
|
212
|
+
],
|
|
213
|
+
events: [{text: 'xin chao'}],
|
|
51
214
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
215
|
+
{
|
|
216
|
+
name: 'realtime subs when server down and then backup',
|
|
217
|
+
steps: [
|
|
218
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
219
|
+
{
|
|
220
|
+
type: 'call',
|
|
221
|
+
call_id: '1',
|
|
222
|
+
path: '/subs',
|
|
223
|
+
method: 'post',
|
|
224
|
+
body: {events: ['message_sent']},
|
|
225
|
+
},
|
|
226
|
+
{type: 'wait', timeout: 1000},
|
|
227
|
+
{type: 'response', call_id: '1', body: '502 too busy', code: 502},
|
|
228
|
+
{
|
|
229
|
+
type: 'call',
|
|
230
|
+
call_id: '1.1',
|
|
231
|
+
path: '/subs',
|
|
232
|
+
method: 'post',
|
|
233
|
+
body: {events: ['message_sent']},
|
|
234
|
+
},
|
|
235
|
+
{type: 'status', status: 'connecting'},
|
|
236
|
+
{type: 'wait', timeout: 1000},
|
|
237
|
+
{type: 'response', call_id: '1.1', body: '502 still busy', code: 502},
|
|
238
|
+
{type: 'status', status: 'connecting'},
|
|
239
|
+
{type: 'wait', timeout: 1000},
|
|
240
|
+
{
|
|
241
|
+
type: 'call',
|
|
242
|
+
call_id: '1.2',
|
|
243
|
+
path: '/subs',
|
|
244
|
+
method: 'post',
|
|
245
|
+
body: {events: ['message_sent']},
|
|
246
|
+
},
|
|
247
|
+
{type: 'status', status: 'connecting'},
|
|
248
|
+
{type: 'response', call_id: '1.2', body: {initial_token: 'token1'}, code: 200},
|
|
249
|
+
{type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
|
|
250
|
+
{type: 'status', status: 'active'},
|
|
251
|
+
{type: 'wait', timeout: 3000},
|
|
252
|
+
{type: 'response', call_id: '2', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
|
|
253
|
+
{type: 'status', status: 'active'},
|
|
254
|
+
{type: 'call', call_id: '3', path: '/poll', query: {token: 'token2'}, method: 'get'},
|
|
255
|
+
],
|
|
256
|
+
events: [{text: 'xin chao'}],
|
|
58
257
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
258
|
+
{
|
|
259
|
+
name: 'realtime must stop if there is something wrong with our credential or our code',
|
|
260
|
+
steps: [
|
|
261
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
262
|
+
{type: 'call', call_id: 1, path: '/subs'},
|
|
263
|
+
{type: 'response', call_id: 1, body: '400 invalid mask', code: 400},
|
|
264
|
+
{type: 'status', status: 'dead'},
|
|
265
|
+
],
|
|
65
266
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
267
|
+
{
|
|
268
|
+
name: 'reconect',
|
|
269
|
+
steps: [
|
|
270
|
+
{type: 'subscribe', topics: ['message_sent']},
|
|
271
|
+
{type: 'call', call_id: 1, path: '/subs'},
|
|
272
|
+
{type: 'response', call_id: 1, body: '400 invalid mask', code: 400},
|
|
273
|
+
{type: 'status', status: 'dead'},
|
|
274
|
+
{type: 'reconnect'},
|
|
275
|
+
{type: 'wait', timeout: 1000},
|
|
276
|
+
{type: 'call', call_id: 2, path: '/subs'},
|
|
277
|
+
{type: 'wait', timeout: 1000},
|
|
278
|
+
{type: 'response', call_id: 2, body: {initial_token: 'token1'}, code: 200},
|
|
279
|
+
{type: 'status', status: 'active'},
|
|
280
|
+
],
|
|
72
281
|
},
|
|
73
|
-
]
|
|
282
|
+
]
|
|
74
283
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
284
|
+
let ps = []
|
|
285
|
+
for (const script of scripts) {
|
|
286
|
+
// if (script.name != 'double subscribe -> do nothing') continue
|
|
287
|
+
ps.push(testScript(t, script))
|
|
288
|
+
}
|
|
289
|
+
await Promise.all(ps)
|
|
78
290
|
})
|
|
79
291
|
|
|
80
|
-
|
|
81
|
-
var subcode
|
|
82
|
-
var callapi = makeMockServer([
|
|
83
|
-
async (method, url, bkody, cb) => {
|
|
84
|
-
// must first call sub and must attach user-mask in the query
|
|
85
|
-
t.equal(method, 'post')
|
|
86
|
-
t.true(url.indexOf('subs?') !== -1)
|
|
87
|
-
t.true(url.indexOf('user-mask=mask123') !== -1)
|
|
88
|
-
cb(`400 invalid mask`, 400)
|
|
89
|
-
realtime.stop()
|
|
90
|
-
},
|
|
91
|
-
])
|
|
92
|
-
|
|
93
|
-
var realtime = new Realtime('', {user_mask: 'mask123'}, callapi)
|
|
94
|
-
realtime.onInterrupted((_, _1, code) => {
|
|
95
|
-
subcode = code
|
|
96
|
-
})
|
|
97
|
-
var err = await realtime.subscribe(['message_pong'])
|
|
98
|
-
t.false(Object.keys(err).length === 0)
|
|
99
|
-
})
|
|
292
|
+
let sleep = (ms) => new Promise((rs) => setTimeout(rs, ms))
|
package/demo.html
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
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
|