@juzi/wechaty-puppet-service 1.0.114 → 1.0.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/client/call-raw-payload-parser.spec.d.ts +3 -0
- package/dist/cjs/src/client/call-raw-payload-parser.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/call-raw-payload-parser.spec.js +128 -0
- package/dist/cjs/src/client/call-raw-payload-parser.spec.js.map +1 -0
- package/dist/cjs/src/client/puppet-service.d.ts +10 -0
- package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
- package/dist/cjs/src/client/puppet-service.js +109 -0
- package/dist/cjs/src/client/puppet-service.js.map +1 -1
- package/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/pure-functions/timestamp.d.ts +15 -1
- package/dist/cjs/src/pure-functions/timestamp.d.ts.map +1 -1
- package/dist/cjs/src/pure-functions/timestamp.js +10 -1
- package/dist/cjs/src/pure-functions/timestamp.js.map +1 -1
- package/dist/cjs/src/pure-functions/timestamp.spec.d.ts +3 -0
- package/dist/cjs/src/pure-functions/timestamp.spec.d.ts.map +1 -0
- package/dist/cjs/src/pure-functions/timestamp.spec.js +48 -0
- package/dist/cjs/src/pure-functions/timestamp.spec.js.map +1 -0
- package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/cjs/src/server/event-stream-manager.js +7 -0
- package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.js +148 -0
- package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
- package/dist/cjs/src/utils/call-media-mapping.d.ts +17 -0
- package/dist/cjs/src/utils/call-media-mapping.d.ts.map +1 -0
- package/dist/cjs/src/utils/call-media-mapping.js +60 -0
- package/dist/cjs/src/utils/call-media-mapping.js.map +1 -0
- package/dist/cjs/src/utils/call-media-mapping.spec.d.ts +3 -0
- package/dist/cjs/src/utils/call-media-mapping.spec.d.ts.map +1 -0
- package/dist/cjs/src/utils/call-media-mapping.spec.js +65 -0
- package/dist/cjs/src/utils/call-media-mapping.spec.js.map +1 -0
- package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts +3 -0
- package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts.map +1 -0
- package/dist/esm/src/client/call-raw-payload-parser.spec.js +103 -0
- package/dist/esm/src/client/call-raw-payload-parser.spec.js.map +1 -0
- package/dist/esm/src/client/puppet-service.d.ts +10 -0
- package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
- package/dist/esm/src/client/puppet-service.js +110 -1
- package/dist/esm/src/client/puppet-service.js.map +1 -1
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/pure-functions/timestamp.d.ts +15 -1
- package/dist/esm/src/pure-functions/timestamp.d.ts.map +1 -1
- package/dist/esm/src/pure-functions/timestamp.js +9 -1
- package/dist/esm/src/pure-functions/timestamp.js.map +1 -1
- package/dist/esm/src/pure-functions/timestamp.spec.d.ts +3 -0
- package/dist/esm/src/pure-functions/timestamp.spec.d.ts.map +1 -0
- package/dist/esm/src/pure-functions/timestamp.spec.js +46 -0
- package/dist/esm/src/pure-functions/timestamp.spec.js.map +1 -0
- package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/esm/src/server/event-stream-manager.js +7 -0
- package/dist/esm/src/server/event-stream-manager.js.map +1 -1
- package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/esm/src/server/puppet-implementation.js +148 -0
- package/dist/esm/src/server/puppet-implementation.js.map +1 -1
- package/dist/esm/src/utils/call-media-mapping.d.ts +17 -0
- package/dist/esm/src/utils/call-media-mapping.d.ts.map +1 -0
- package/dist/esm/src/utils/call-media-mapping.js +32 -0
- package/dist/esm/src/utils/call-media-mapping.js.map +1 -0
- package/dist/esm/src/utils/call-media-mapping.spec.d.ts +3 -0
- package/dist/esm/src/utils/call-media-mapping.spec.d.ts.map +1 -0
- package/dist/esm/src/utils/call-media-mapping.spec.js +40 -0
- package/dist/esm/src/utils/call-media-mapping.spec.js.map +1 -0
- package/package.json +4 -4
- package/src/client/call-raw-payload-parser.spec.ts +131 -0
- package/src/client/puppet-service.ts +191 -1
- package/src/package-json.ts +4 -4
- package/src/pure-functions/timestamp.spec.ts +56 -0
- package/src/pure-functions/timestamp.ts +19 -0
- package/src/server/event-stream-manager.ts +7 -0
- package/src/server/puppet-implementation.ts +175 -0
- package/src/utils/call-media-mapping.spec.ts +53 -0
- package/src/utils/call-media-mapping.ts +35 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
|
|
3
|
+
import { test } from 'tstest'
|
|
4
|
+
import * as PUPPET from '@juzi/wechaty-puppet'
|
|
5
|
+
import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
|
|
6
|
+
|
|
7
|
+
import { PuppetService } from './puppet-service.js'
|
|
8
|
+
import { timestampFromMilliseconds } from '../pure-functions/timestamp.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The parser only reads `rawPayload.*` and a module-level `log` — it never
|
|
12
|
+
* touches `this`. Invoke it via `prototype.call({}, raw)` to avoid building a
|
|
13
|
+
* full PuppetService instance for unit-level tests.
|
|
14
|
+
*/
|
|
15
|
+
const callParser = async (raw: grpcPuppet.CallPayloadResponse.AsObject) =>
|
|
16
|
+
PuppetService.prototype.callRawPayloadParser.call({} as PuppetService, raw)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 1. Full payload with starter, participants, media=VOICE, startTime, endTime
|
|
20
|
+
* parses every field, mapping VOICE -> CallMediaType.Audio.
|
|
21
|
+
*/
|
|
22
|
+
test('callRawPayloadParser parses a complete payload', async t => {
|
|
23
|
+
const startMs = 1_700_000_000_000
|
|
24
|
+
const endMs = 1_700_000_030_000
|
|
25
|
+
|
|
26
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
27
|
+
pb.setId('call-1')
|
|
28
|
+
pb.setStarter('alice')
|
|
29
|
+
pb.setParticipantsList([ 'alice', 'bob' ])
|
|
30
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_VOICE)
|
|
31
|
+
pb.setStartTime(timestampFromMilliseconds(startMs))
|
|
32
|
+
pb.setEndTime(timestampFromMilliseconds(endMs))
|
|
33
|
+
|
|
34
|
+
const payload = await callParser(pb.toObject())
|
|
35
|
+
|
|
36
|
+
t.equal(payload.id, 'call-1', 'id propagated')
|
|
37
|
+
t.equal(payload.starter, 'alice', 'starter propagated')
|
|
38
|
+
t.same(payload.participants, [ 'alice', 'bob' ], 'participants propagated')
|
|
39
|
+
t.equal(payload.media, PUPPET.types.CallMediaType.Audio, 'VOICE maps to Audio')
|
|
40
|
+
t.equal(payload.startTime, startMs, 'startTime parsed')
|
|
41
|
+
t.equal(payload.endTime, endMs, 'endTime parsed')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 2. Empty starter string -> parser omits `starter` from the resulting payload.
|
|
46
|
+
* (Mirrors the server, which only `setStarter` when the value is truthy.)
|
|
47
|
+
*/
|
|
48
|
+
test('callRawPayloadParser omits starter when starter is an empty string', async t => {
|
|
49
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
50
|
+
pb.setId('call-2')
|
|
51
|
+
pb.setParticipantsList([ 'alice' ])
|
|
52
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_VIDEO)
|
|
53
|
+
pb.setStartTime(timestampFromMilliseconds(1_700_000_000_000))
|
|
54
|
+
// no setStarter -> default proto3 string '' on the wire
|
|
55
|
+
|
|
56
|
+
const payload = await callParser(pb.toObject())
|
|
57
|
+
|
|
58
|
+
t.equal(payload.starter, undefined, 'starter is undefined when empty on the wire')
|
|
59
|
+
t.equal(payload.media, PUPPET.types.CallMediaType.Video, 'VIDEO maps to Video')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 3. endTime not set (no setEndTime) -> payload.endTime === undefined.
|
|
64
|
+
*/
|
|
65
|
+
test('callRawPayloadParser leaves endTime undefined when endTime is not set', async t => {
|
|
66
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
67
|
+
pb.setId('call-3')
|
|
68
|
+
pb.setStarter('alice')
|
|
69
|
+
pb.setParticipantsList([ 'alice', 'bob' ])
|
|
70
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_VOICE)
|
|
71
|
+
pb.setStartTime(timestampFromMilliseconds(1_700_000_000_000))
|
|
72
|
+
// no setEndTime
|
|
73
|
+
|
|
74
|
+
const payload = await callParser(pb.toObject())
|
|
75
|
+
|
|
76
|
+
t.equal(payload.endTime, undefined, 'endTime is undefined when not set on the wire')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 4. media = CALL_TYPE_UNKNOWN -> throw, error mentions "media".
|
|
81
|
+
*/
|
|
82
|
+
test('callRawPayloadParser throws when media is CALL_TYPE_UNKNOWN', async t => {
|
|
83
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
84
|
+
pb.setId('call-4')
|
|
85
|
+
pb.setParticipantsList([ 'alice' ])
|
|
86
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_UNKNOWN)
|
|
87
|
+
pb.setStartTime(timestampFromMilliseconds(1_700_000_000_000))
|
|
88
|
+
|
|
89
|
+
await t.rejects(
|
|
90
|
+
callParser(pb.toObject()),
|
|
91
|
+
/media/,
|
|
92
|
+
'rejects with a message that mentions media',
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 5. startTime not set -> throw, error mentions "startTime".
|
|
98
|
+
*/
|
|
99
|
+
test('callRawPayloadParser throws when startTime is not set', async t => {
|
|
100
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
101
|
+
pb.setId('call-5')
|
|
102
|
+
pb.setParticipantsList([ 'alice' ])
|
|
103
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_VOICE)
|
|
104
|
+
// no setStartTime
|
|
105
|
+
|
|
106
|
+
await t.rejects(
|
|
107
|
+
callParser(pb.toObject()),
|
|
108
|
+
/startTime/,
|
|
109
|
+
'rejects with a message that mentions startTime',
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 6. endTime explicitly set to epoch 0 -> payload.endTime === 0.
|
|
115
|
+
* proto3 `setEndTime(timestamp(0))` is a legal "ended at epoch-0" value
|
|
116
|
+
* (extreme short session / test stub) and must not be swallowed by a
|
|
117
|
+
* truthy guard. Presence is what matters, not the numeric value.
|
|
118
|
+
*/
|
|
119
|
+
test('callRawPayloadParser preserves endTime when it is set to epoch zero', async t => {
|
|
120
|
+
const pb = new grpcPuppet.CallPayloadResponse()
|
|
121
|
+
pb.setId('call-6')
|
|
122
|
+
pb.setStarter('alice')
|
|
123
|
+
pb.setParticipantsList([ 'alice', 'bob' ])
|
|
124
|
+
pb.setMedia(grpcPuppet.CallType.CALL_TYPE_VOICE)
|
|
125
|
+
pb.setStartTime(timestampFromMilliseconds(1_700_000_000_000))
|
|
126
|
+
pb.setEndTime(timestampFromMilliseconds(0))
|
|
127
|
+
|
|
128
|
+
const payload = await callParser(pb.toObject())
|
|
129
|
+
|
|
130
|
+
t.equal(payload.endTime, 0, 'endTime is 0 when explicitly set to epoch zero on the wire')
|
|
131
|
+
})
|
|
@@ -31,7 +31,10 @@ import {
|
|
|
31
31
|
|
|
32
32
|
// import type { Subscription } from 'rxjs'
|
|
33
33
|
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
millisecondsFromTimestamp,
|
|
36
|
+
millisecondsFromTimestampObject,
|
|
37
|
+
} from '../pure-functions/timestamp.js'
|
|
35
38
|
|
|
36
39
|
import {
|
|
37
40
|
uuidifyFileBoxGrpc,
|
|
@@ -51,6 +54,7 @@ import { packageJson } from '../package-json.js'
|
|
|
51
54
|
import { GrpcManager } from './grpc-manager.js'
|
|
52
55
|
import { PayloadStore } from './payload-store.js'
|
|
53
56
|
import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPbToPayload, channelPayloadToPb, channelPbToPayload, chatHistoryPbToPayload, contactPbToPayload, postPayloadToPb, roomMemberPbToPayload, urlLinkPbToPayload, channelCardPayloadToPb, channelCardPbToPayload, miniProgramPayloadToPb, urlLinkPayloadToPb, locationPayloadToPb } from '../utils/pb-payload-helper.js'
|
|
57
|
+
import { puppetCallMediaTypeToGrpc, grpcCallTypeToPuppetMedia } from '../utils/call-media-mapping.js'
|
|
54
58
|
import type { MessageBroadcastTargets } from '@juzi/wechaty-puppet/dist/esm/src/schemas/message.js'
|
|
55
59
|
import { timeoutPromise } from 'gerror'
|
|
56
60
|
import { BooleanIndicator } from 'state-switch'
|
|
@@ -423,6 +427,9 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
423
427
|
case grpcPuppet.EventType.EVENT_TYPE_VERIFY_SLIDE:
|
|
424
428
|
this.emit('verify-slide', JSON.parse(payload) as PUPPET.payloads.EventVerifySlide)
|
|
425
429
|
break
|
|
430
|
+
case grpcPuppet.EventType.EVENT_TYPE_CALL:
|
|
431
|
+
this.emit('call', JSON.parse(payload) as PUPPET.payloads.EventCall)
|
|
432
|
+
break
|
|
426
433
|
|
|
427
434
|
default:
|
|
428
435
|
// Huan(202003): in default, the `type` type should be `never`, please check.
|
|
@@ -1153,6 +1160,189 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1153
1160
|
return payload
|
|
1154
1161
|
}
|
|
1155
1162
|
|
|
1163
|
+
override async callInvite (
|
|
1164
|
+
contactIds: string[],
|
|
1165
|
+
media: PUPPET.types.CallMediaType,
|
|
1166
|
+
): Promise<string> {
|
|
1167
|
+
log.verbose('PuppetService', 'callInvite(%s, %s)', contactIds, media)
|
|
1168
|
+
|
|
1169
|
+
const request = new grpcPuppet.CallInviteRequest()
|
|
1170
|
+
request.setContactIdsList(contactIds)
|
|
1171
|
+
request.setMedia(puppetCallMediaTypeToGrpc(media))
|
|
1172
|
+
|
|
1173
|
+
const response = await util.promisify(
|
|
1174
|
+
this.grpcManager.client.callInvite
|
|
1175
|
+
.bind(this.grpcManager.client),
|
|
1176
|
+
)(request)
|
|
1177
|
+
|
|
1178
|
+
const callId = response.getCallId()
|
|
1179
|
+
if (!callId) {
|
|
1180
|
+
throw new Error('callInvite: puppet server returned empty call_id')
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return callId
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
override async callAdd (
|
|
1187
|
+
callId: string,
|
|
1188
|
+
contactIds: string[],
|
|
1189
|
+
): Promise<void> {
|
|
1190
|
+
log.verbose('PuppetService', 'callAdd(%s, %s)', callId, contactIds)
|
|
1191
|
+
|
|
1192
|
+
const request = new grpcPuppet.CallAddRequest()
|
|
1193
|
+
request.setCallId(callId)
|
|
1194
|
+
request.setContactIdsList(contactIds)
|
|
1195
|
+
|
|
1196
|
+
await this.grpcUnary(
|
|
1197
|
+
this.grpcManager.client.callAdd,
|
|
1198
|
+
request,
|
|
1199
|
+
)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
override async callAccept (
|
|
1203
|
+
callId: string,
|
|
1204
|
+
): Promise<void> {
|
|
1205
|
+
log.verbose('PuppetService', 'callAccept(%s)', callId)
|
|
1206
|
+
|
|
1207
|
+
const request = new grpcPuppet.CallAcceptRequest()
|
|
1208
|
+
request.setCallId(callId)
|
|
1209
|
+
|
|
1210
|
+
await this.grpcUnary(
|
|
1211
|
+
this.grpcManager.client.callAccept,
|
|
1212
|
+
request,
|
|
1213
|
+
)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
override async callReject (
|
|
1217
|
+
callId: string,
|
|
1218
|
+
reason?: string,
|
|
1219
|
+
): Promise<void> {
|
|
1220
|
+
log.verbose('PuppetService', 'callReject(%s, %s)', callId, reason)
|
|
1221
|
+
|
|
1222
|
+
const request = new grpcPuppet.CallRejectRequest()
|
|
1223
|
+
request.setCallId(callId)
|
|
1224
|
+
if (reason !== undefined && reason !== '') {
|
|
1225
|
+
request.setReason(reason)
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
await this.grpcUnary(
|
|
1229
|
+
this.grpcManager.client.callReject,
|
|
1230
|
+
request,
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
override async callCancel (
|
|
1235
|
+
callId: string,
|
|
1236
|
+
): Promise<void> {
|
|
1237
|
+
log.verbose('PuppetService', 'callCancel(%s)', callId)
|
|
1238
|
+
|
|
1239
|
+
const request = new grpcPuppet.CallCancelRequest()
|
|
1240
|
+
request.setCallId(callId)
|
|
1241
|
+
|
|
1242
|
+
await this.grpcUnary(
|
|
1243
|
+
this.grpcManager.client.callCancel,
|
|
1244
|
+
request,
|
|
1245
|
+
)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
override async callHangup (
|
|
1249
|
+
callId: string,
|
|
1250
|
+
reason?: string,
|
|
1251
|
+
): Promise<void> {
|
|
1252
|
+
log.verbose('PuppetService', 'callHangup(%s, %s)', callId, reason)
|
|
1253
|
+
|
|
1254
|
+
const request = new grpcPuppet.CallHangupRequest()
|
|
1255
|
+
request.setCallId(callId)
|
|
1256
|
+
if (reason !== undefined && reason !== '') {
|
|
1257
|
+
request.setReason(reason)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
await this.grpcUnary(
|
|
1261
|
+
this.grpcManager.client.callHangup,
|
|
1262
|
+
request,
|
|
1263
|
+
)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
override async callMediaEndpoint (
|
|
1267
|
+
callId: string,
|
|
1268
|
+
): Promise<PUPPET.payloads.CallMediaEndpoint> {
|
|
1269
|
+
log.verbose('PuppetService', 'callMediaEndpoint(%s)', callId)
|
|
1270
|
+
|
|
1271
|
+
const request = new grpcPuppet.CallMediaEndpointRequest()
|
|
1272
|
+
request.setCallId(callId)
|
|
1273
|
+
|
|
1274
|
+
const response = await util.promisify(
|
|
1275
|
+
this.grpcManager.client.callMediaEndpoint
|
|
1276
|
+
.bind(this.grpcManager.client),
|
|
1277
|
+
)(request)
|
|
1278
|
+
|
|
1279
|
+
const url = response.getUrl()
|
|
1280
|
+
const token = response.getToken()
|
|
1281
|
+
if (!url || !token) {
|
|
1282
|
+
throw new Error('callMediaEndpoint: puppet server returned empty url or token')
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const payload: PUPPET.payloads.CallMediaEndpoint = {
|
|
1286
|
+
url,
|
|
1287
|
+
token,
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (response.hasExpiresAt()) {
|
|
1291
|
+
payload.expiresAt = millisecondsFromTimestamp(response.getExpiresAt()!)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const protocol = response.getProtocol()
|
|
1295
|
+
if (protocol) {
|
|
1296
|
+
payload.protocol = protocol
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return payload
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
override async callRawPayload (callId: string): Promise<grpcPuppet.CallPayloadResponse.AsObject> {
|
|
1303
|
+
log.verbose('PuppetService', 'callRawPayload(%s)', callId)
|
|
1304
|
+
|
|
1305
|
+
const request = new grpcPuppet.CallPayloadRequest()
|
|
1306
|
+
request.setId(callId)
|
|
1307
|
+
|
|
1308
|
+
const response = await util.promisify(
|
|
1309
|
+
this.grpcManager.client.callPayload
|
|
1310
|
+
.bind(this.grpcManager.client),
|
|
1311
|
+
)(request)
|
|
1312
|
+
|
|
1313
|
+
return response.toObject()
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
override async callRawPayloadParser (rawPayload: grpcPuppet.CallPayloadResponse.AsObject): Promise<PUPPET.payloads.Call> {
|
|
1317
|
+
log.verbose('PuppetService', 'callRawPayloadParser({id:%s})', rawPayload.id)
|
|
1318
|
+
|
|
1319
|
+
const media = grpcCallTypeToPuppetMedia(rawPayload.media)
|
|
1320
|
+
if (media === undefined) {
|
|
1321
|
+
throw new Error('callRawPayloadParser: media is required (got CALL_TYPE_UNKNOWN on the wire)')
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (rawPayload.startTime === undefined) {
|
|
1325
|
+
throw new Error('callRawPayloadParser: startTime is required')
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const payload: PUPPET.payloads.Call = {
|
|
1329
|
+
id : rawPayload.id,
|
|
1330
|
+
participants : rawPayload.participantsList,
|
|
1331
|
+
media,
|
|
1332
|
+
startTime : millisecondsFromTimestampObject(rawPayload.startTime),
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (rawPayload.starter) {
|
|
1336
|
+
payload.starter = rawPayload.starter
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (rawPayload.endTime !== undefined) {
|
|
1340
|
+
payload.endTime = millisecondsFromTimestampObject(rawPayload.endTime)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return payload
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1156
1346
|
override async messageChatHistory (
|
|
1157
1347
|
messageId: string,
|
|
1158
1348
|
): Promise<PUPPET.payloads.ChatHistory[]> {
|
package/src/package-json.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { PackageJson } from 'type-fest'
|
|
5
5
|
export const packageJson: PackageJson = {
|
|
6
6
|
"name": "@juzi/wechaty-puppet-service",
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.115",
|
|
8
8
|
"description": "Puppet Service for Wechaty",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": {
|
|
@@ -55,7 +55,7 @@ export const packageJson: PackageJson = {
|
|
|
55
55
|
"@chatie/eslint-config": "^1.0.4",
|
|
56
56
|
"@chatie/semver": "^0.4.7",
|
|
57
57
|
"@chatie/tsconfig": "^4.6.3",
|
|
58
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
58
|
+
"@juzi/wechaty-puppet": "^1.0.138",
|
|
59
59
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
60
60
|
"@swc/core": "1.3.39",
|
|
61
61
|
"@types/google-protobuf": "^3.15.5",
|
|
@@ -74,10 +74,10 @@ export const packageJson: PackageJson = {
|
|
|
74
74
|
"why-is-node-running": "^2.2.1"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
77
|
+
"@juzi/wechaty-puppet": "^1.0.138"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@juzi/wechaty-grpc": "^1.0.
|
|
80
|
+
"@juzi/wechaty-grpc": "^1.0.102",
|
|
81
81
|
"clone-class": "^1.1.1",
|
|
82
82
|
"ducks": "^1.0.2",
|
|
83
83
|
"file-box": "^1.5.5",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
|
|
3
|
+
import { test } from 'tstest'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
millisecondsFromTimestamp,
|
|
7
|
+
millisecondsFromTimestampObject,
|
|
8
|
+
timestampFromMilliseconds,
|
|
9
|
+
} from './timestamp.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 1. ms -> Timestamp -> ms round-trips for millisecond-aligned values.
|
|
13
|
+
*/
|
|
14
|
+
test('timestampFromMilliseconds <-> millisecondsFromTimestamp round-trip', async t => {
|
|
15
|
+
const cases = [
|
|
16
|
+
0,
|
|
17
|
+
1_000,
|
|
18
|
+
1_700_000_000_000,
|
|
19
|
+
1_700_000_000_123,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
for (const ms of cases) {
|
|
23
|
+
const roundTripped = millisecondsFromTimestamp(timestampFromMilliseconds(ms))
|
|
24
|
+
t.equal(roundTripped, ms, `round-trip identity for ${ms}ms`)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 2. The plain-object variant agrees with the instance variant for the same
|
|
30
|
+
* Timestamp (this is the `toObject()` shape used by callRawPayloadParser).
|
|
31
|
+
*/
|
|
32
|
+
test('millisecondsFromTimestampObject matches millisecondsFromTimestamp', async t => {
|
|
33
|
+
const cases = [
|
|
34
|
+
0,
|
|
35
|
+
1_000,
|
|
36
|
+
1_700_000_000_123,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for (const ms of cases) {
|
|
40
|
+
const timestamp = timestampFromMilliseconds(ms)
|
|
41
|
+
const fromInstance = millisecondsFromTimestamp(timestamp)
|
|
42
|
+
const fromObject = millisecondsFromTimestampObject(timestamp.toObject())
|
|
43
|
+
t.equal(fromObject, fromInstance, `instance and object conversion agree for ${ms}ms`)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 3. Sub-millisecond nanos are rounded to the nearest millisecond.
|
|
49
|
+
*/
|
|
50
|
+
test('millisecondsFromTimestampObject rounds nanos to nearest millisecond', async t => {
|
|
51
|
+
t.equal(millisecondsFromTimestampObject({ seconds: 1, nanos: 0 }), 1000, 'no nanos')
|
|
52
|
+
t.equal(millisecondsFromTimestampObject({ seconds: 1, nanos: 499_999 }), 1000, '0.499ms rounds down')
|
|
53
|
+
t.equal(millisecondsFromTimestampObject({ seconds: 1, nanos: 500_000 }), 1001, '0.5ms rounds up')
|
|
54
|
+
t.equal(millisecondsFromTimestampObject({ seconds: 1, nanos: 999_999 }), 1001, '0.999ms rounds up')
|
|
55
|
+
t.equal(millisecondsFromTimestampObject({ seconds: 0, nanos: 123_000_000 }), 123, '123ms from nanos')
|
|
56
|
+
})
|
|
@@ -22,7 +22,26 @@ function millisecondsFromTimestamp (timestamp: ReturnType<typeof timestampFromMi
|
|
|
22
22
|
return seconds * 1000 + nanos / 1000000
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Plain `{ seconds, nanos }` shape produced by a protobuf `Message.toObject()`
|
|
27
|
+
* for a `google.protobuf.Timestamp` field.
|
|
28
|
+
*/
|
|
29
|
+
type TimestampObject = {
|
|
30
|
+
seconds: number,
|
|
31
|
+
nanos: number,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Same conversion as `millisecondsFromTimestamp`, but for the plain
|
|
36
|
+
* `{ seconds, nanos }` shape produced by `Message.toObject()` rather than a
|
|
37
|
+
* Timestamp class instance. Nanos are rounded to the nearest millisecond.
|
|
38
|
+
*/
|
|
39
|
+
function millisecondsFromTimestampObject (timestamp: TimestampObject): number {
|
|
40
|
+
return timestamp.seconds * 1000 + Math.round(timestamp.nanos / 1000000)
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export {
|
|
26
44
|
millisecondsFromTimestamp,
|
|
45
|
+
millisecondsFromTimestampObject,
|
|
27
46
|
timestampFromMilliseconds,
|
|
28
47
|
}
|
|
@@ -322,6 +322,13 @@ class EventStreamManager {
|
|
|
322
322
|
this.offCallbackList.push(off)
|
|
323
323
|
break
|
|
324
324
|
}
|
|
325
|
+
case 'call': {
|
|
326
|
+
const listener = (payload: PUPPET.payloads.EventCall) => this.grpcEmit(grpcPuppet.EventType.EVENT_TYPE_CALL, payload)
|
|
327
|
+
this.puppet.on('call', listener)
|
|
328
|
+
const off = () => this.puppet.off('call', listener)
|
|
329
|
+
this.offCallbackList.push(off)
|
|
330
|
+
break
|
|
331
|
+
}
|
|
325
332
|
case 'reset':
|
|
326
333
|
// the `reset` event should be dealed internally, should not send out
|
|
327
334
|
break
|
|
@@ -44,6 +44,7 @@ import { log } from '../config.js'
|
|
|
44
44
|
import { grpcError } from './grpc-error.js'
|
|
45
45
|
import { EventStreamManager } from './event-stream-manager.js'
|
|
46
46
|
import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPayloadToPb, channelPayloadToPb, chatHistoryPayloadToPb, postPbToPayload, urlLinkPayloadToPb, channelCardPayloadToPb } from '../utils/pb-payload-helper.js'
|
|
47
|
+
import { grpcCallTypeToPuppetMedia, puppetCallMediaTypeToGrpc } from '../utils/call-media-mapping.js'
|
|
47
48
|
import { TextContentType } from '@juzi/wechaty-puppet/types'
|
|
48
49
|
|
|
49
50
|
type MessageBatchSendResponse = Awaited<ReturnType<PUPPET.impls.PuppetInterface['messageBatchSendText']>>
|
|
@@ -2958,6 +2959,180 @@ function puppetImplementation (
|
|
|
2958
2959
|
|
|
2959
2960
|
},
|
|
2960
2961
|
|
|
2962
|
+
callInvite: async (call, callback) => {
|
|
2963
|
+
log.verbose('PuppetServiceImpl', 'callInvite()')
|
|
2964
|
+
|
|
2965
|
+
try {
|
|
2966
|
+
const contactIds = call.request.getContactIdsList()
|
|
2967
|
+
if (contactIds.length === 0) {
|
|
2968
|
+
throw new Error('callInvite: contact_ids is required')
|
|
2969
|
+
}
|
|
2970
|
+
const media = grpcCallTypeToPuppetMedia(call.request.getMedia())
|
|
2971
|
+
if (media === undefined) {
|
|
2972
|
+
throw new Error('callInvite: media is required (got CALL_TYPE_UNKNOWN on the wire)')
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
const callId = await puppet.callInvite(contactIds, media)
|
|
2976
|
+
if (!callId) {
|
|
2977
|
+
throw new Error('callInvite: puppet returned empty callId')
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
const response = new grpcPuppet.CallInviteResponse()
|
|
2981
|
+
response.setCallId(callId)
|
|
2982
|
+
|
|
2983
|
+
return callback(null, response)
|
|
2984
|
+
} catch (e) {
|
|
2985
|
+
return grpcError('callInvite', e, callback)
|
|
2986
|
+
}
|
|
2987
|
+
},
|
|
2988
|
+
|
|
2989
|
+
callAdd: async (call, callback) => {
|
|
2990
|
+
log.verbose('PuppetServiceImpl', 'callAdd()')
|
|
2991
|
+
|
|
2992
|
+
try {
|
|
2993
|
+
const callId = call.request.getCallId()
|
|
2994
|
+
if (!callId) {
|
|
2995
|
+
throw new Error('callAdd: call_id is required')
|
|
2996
|
+
}
|
|
2997
|
+
const contactIds = call.request.getContactIdsList()
|
|
2998
|
+
if (contactIds.length === 0) {
|
|
2999
|
+
throw new Error('callAdd: contact_ids is required')
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
await puppet.callAdd(callId, contactIds)
|
|
3003
|
+
|
|
3004
|
+
return callback(null, new grpcPuppet.CallAddResponse())
|
|
3005
|
+
} catch (e) {
|
|
3006
|
+
return grpcError('callAdd', e, callback)
|
|
3007
|
+
}
|
|
3008
|
+
},
|
|
3009
|
+
|
|
3010
|
+
callAccept: async (call, callback) => {
|
|
3011
|
+
log.verbose('PuppetServiceImpl', 'callAccept()')
|
|
3012
|
+
|
|
3013
|
+
try {
|
|
3014
|
+
const callId = call.request.getCallId()
|
|
3015
|
+
if (!callId) {
|
|
3016
|
+
throw new Error('callAccept: call_id is required')
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
await puppet.callAccept(callId)
|
|
3020
|
+
|
|
3021
|
+
return callback(null, new grpcPuppet.CallAcceptResponse())
|
|
3022
|
+
} catch (e) {
|
|
3023
|
+
return grpcError('callAccept', e, callback)
|
|
3024
|
+
}
|
|
3025
|
+
},
|
|
3026
|
+
|
|
3027
|
+
callReject: async (call, callback) => {
|
|
3028
|
+
log.verbose('PuppetServiceImpl', 'callReject()')
|
|
3029
|
+
|
|
3030
|
+
try {
|
|
3031
|
+
const callId = call.request.getCallId()
|
|
3032
|
+
if (!callId) {
|
|
3033
|
+
throw new Error('callReject: call_id is required')
|
|
3034
|
+
}
|
|
3035
|
+
const reason = call.request.getReason() || undefined
|
|
3036
|
+
|
|
3037
|
+
await puppet.callReject(callId, reason)
|
|
3038
|
+
|
|
3039
|
+
return callback(null, new grpcPuppet.CallRejectResponse())
|
|
3040
|
+
} catch (e) {
|
|
3041
|
+
return grpcError('callReject', e, callback)
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
3044
|
+
|
|
3045
|
+
callCancel: async (call, callback) => {
|
|
3046
|
+
log.verbose('PuppetServiceImpl', 'callCancel()')
|
|
3047
|
+
|
|
3048
|
+
try {
|
|
3049
|
+
const callId = call.request.getCallId()
|
|
3050
|
+
if (!callId) {
|
|
3051
|
+
throw new Error('callCancel: call_id is required')
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
await puppet.callCancel(callId)
|
|
3055
|
+
|
|
3056
|
+
return callback(null, new grpcPuppet.CallCancelResponse())
|
|
3057
|
+
} catch (e) {
|
|
3058
|
+
return grpcError('callCancel', e, callback)
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
|
|
3062
|
+
callHangup: async (call, callback) => {
|
|
3063
|
+
log.verbose('PuppetServiceImpl', 'callHangup()')
|
|
3064
|
+
|
|
3065
|
+
try {
|
|
3066
|
+
const callId = call.request.getCallId()
|
|
3067
|
+
if (!callId) {
|
|
3068
|
+
throw new Error('callHangup: call_id is required')
|
|
3069
|
+
}
|
|
3070
|
+
const reason = call.request.getReason() || undefined
|
|
3071
|
+
|
|
3072
|
+
await puppet.callHangup(callId, reason)
|
|
3073
|
+
|
|
3074
|
+
return callback(null, new grpcPuppet.CallHangupResponse())
|
|
3075
|
+
} catch (e) {
|
|
3076
|
+
return grpcError('callHangup', e, callback)
|
|
3077
|
+
}
|
|
3078
|
+
},
|
|
3079
|
+
|
|
3080
|
+
callMediaEndpoint: async (call, callback) => {
|
|
3081
|
+
log.verbose('PuppetServiceImpl', 'callMediaEndpoint()')
|
|
3082
|
+
|
|
3083
|
+
try {
|
|
3084
|
+
const callId = call.request.getCallId()
|
|
3085
|
+
if (!callId) {
|
|
3086
|
+
throw new Error('callMediaEndpoint: call_id is required')
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
const payload = await puppet.callMediaEndpoint(callId)
|
|
3090
|
+
|
|
3091
|
+
const response = new grpcPuppet.CallMediaEndpointResponse()
|
|
3092
|
+
response.setUrl(payload.url)
|
|
3093
|
+
response.setToken(payload.token)
|
|
3094
|
+
if (payload.expiresAt !== undefined) {
|
|
3095
|
+
response.setExpiresAt(timestampFromMilliseconds(payload.expiresAt))
|
|
3096
|
+
}
|
|
3097
|
+
if (payload.protocol) {
|
|
3098
|
+
response.setProtocol(payload.protocol)
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
return callback(null, response)
|
|
3102
|
+
} catch (e) {
|
|
3103
|
+
return grpcError('callMediaEndpoint', e, callback)
|
|
3104
|
+
}
|
|
3105
|
+
},
|
|
3106
|
+
|
|
3107
|
+
callPayload: async (call, callback) => {
|
|
3108
|
+
log.verbose('PuppetServiceImpl', 'callPayload()')
|
|
3109
|
+
|
|
3110
|
+
try {
|
|
3111
|
+
const callId = call.request.getId()
|
|
3112
|
+
if (!callId) {
|
|
3113
|
+
throw new Error('callPayload: id is required')
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const payload = await puppet.callPayload(callId)
|
|
3117
|
+
|
|
3118
|
+
const response = new grpcPuppet.CallPayloadResponse()
|
|
3119
|
+
response.setId(payload.id)
|
|
3120
|
+
if (payload.starter) {
|
|
3121
|
+
response.setStarter(payload.starter)
|
|
3122
|
+
}
|
|
3123
|
+
response.setParticipantsList(payload.participants)
|
|
3124
|
+
response.setMedia(puppetCallMediaTypeToGrpc(payload.media))
|
|
3125
|
+
response.setStartTime(timestampFromMilliseconds(payload.startTime))
|
|
3126
|
+
if (payload.endTime !== undefined) {
|
|
3127
|
+
response.setEndTime(timestampFromMilliseconds(payload.endTime))
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
return callback(null, response)
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
return grpcError('callPayload', e, callback)
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
|
|
2961
3136
|
upload: async (call, callback) => {
|
|
2962
3137
|
log.verbose('PuppetServiceImpl', 'upload()')
|
|
2963
3138
|
|