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