@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.
Files changed (72) hide show
  1. package/dist/cjs/src/client/call-raw-payload-parser.spec.d.ts +3 -0
  2. package/dist/cjs/src/client/call-raw-payload-parser.spec.d.ts.map +1 -0
  3. package/dist/cjs/src/client/call-raw-payload-parser.spec.js +128 -0
  4. package/dist/cjs/src/client/call-raw-payload-parser.spec.js.map +1 -0
  5. package/dist/cjs/src/client/puppet-service.d.ts +10 -0
  6. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  7. package/dist/cjs/src/client/puppet-service.js +109 -0
  8. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  9. package/dist/cjs/src/package-json.js +4 -4
  10. package/dist/cjs/src/pure-functions/timestamp.d.ts +15 -1
  11. package/dist/cjs/src/pure-functions/timestamp.d.ts.map +1 -1
  12. package/dist/cjs/src/pure-functions/timestamp.js +10 -1
  13. package/dist/cjs/src/pure-functions/timestamp.js.map +1 -1
  14. package/dist/cjs/src/pure-functions/timestamp.spec.d.ts +3 -0
  15. package/dist/cjs/src/pure-functions/timestamp.spec.d.ts.map +1 -0
  16. package/dist/cjs/src/pure-functions/timestamp.spec.js +48 -0
  17. package/dist/cjs/src/pure-functions/timestamp.spec.js.map +1 -0
  18. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  19. package/dist/cjs/src/server/event-stream-manager.js +7 -0
  20. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  21. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  22. package/dist/cjs/src/server/puppet-implementation.js +148 -0
  23. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  24. package/dist/cjs/src/utils/call-media-mapping.d.ts +17 -0
  25. package/dist/cjs/src/utils/call-media-mapping.d.ts.map +1 -0
  26. package/dist/cjs/src/utils/call-media-mapping.js +60 -0
  27. package/dist/cjs/src/utils/call-media-mapping.js.map +1 -0
  28. package/dist/cjs/src/utils/call-media-mapping.spec.d.ts +3 -0
  29. package/dist/cjs/src/utils/call-media-mapping.spec.d.ts.map +1 -0
  30. package/dist/cjs/src/utils/call-media-mapping.spec.js +65 -0
  31. package/dist/cjs/src/utils/call-media-mapping.spec.js.map +1 -0
  32. package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts +3 -0
  33. package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts.map +1 -0
  34. package/dist/esm/src/client/call-raw-payload-parser.spec.js +103 -0
  35. package/dist/esm/src/client/call-raw-payload-parser.spec.js.map +1 -0
  36. package/dist/esm/src/client/puppet-service.d.ts +10 -0
  37. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  38. package/dist/esm/src/client/puppet-service.js +110 -1
  39. package/dist/esm/src/client/puppet-service.js.map +1 -1
  40. package/dist/esm/src/package-json.js +4 -4
  41. package/dist/esm/src/pure-functions/timestamp.d.ts +15 -1
  42. package/dist/esm/src/pure-functions/timestamp.d.ts.map +1 -1
  43. package/dist/esm/src/pure-functions/timestamp.js +9 -1
  44. package/dist/esm/src/pure-functions/timestamp.js.map +1 -1
  45. package/dist/esm/src/pure-functions/timestamp.spec.d.ts +3 -0
  46. package/dist/esm/src/pure-functions/timestamp.spec.d.ts.map +1 -0
  47. package/dist/esm/src/pure-functions/timestamp.spec.js +46 -0
  48. package/dist/esm/src/pure-functions/timestamp.spec.js.map +1 -0
  49. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  50. package/dist/esm/src/server/event-stream-manager.js +7 -0
  51. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  52. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  53. package/dist/esm/src/server/puppet-implementation.js +148 -0
  54. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  55. package/dist/esm/src/utils/call-media-mapping.d.ts +17 -0
  56. package/dist/esm/src/utils/call-media-mapping.d.ts.map +1 -0
  57. package/dist/esm/src/utils/call-media-mapping.js +32 -0
  58. package/dist/esm/src/utils/call-media-mapping.js.map +1 -0
  59. package/dist/esm/src/utils/call-media-mapping.spec.d.ts +3 -0
  60. package/dist/esm/src/utils/call-media-mapping.spec.d.ts.map +1 -0
  61. package/dist/esm/src/utils/call-media-mapping.spec.js +40 -0
  62. package/dist/esm/src/utils/call-media-mapping.spec.js.map +1 -0
  63. package/package.json +4 -4
  64. package/src/client/call-raw-payload-parser.spec.ts +131 -0
  65. package/src/client/puppet-service.ts +191 -1
  66. package/src/package-json.ts +4 -4
  67. package/src/pure-functions/timestamp.spec.ts +56 -0
  68. package/src/pure-functions/timestamp.ts +19 -0
  69. package/src/server/event-stream-manager.ts +7 -0
  70. package/src/server/puppet-implementation.ts +175 -0
  71. package/src/utils/call-media-mapping.spec.ts +53 -0
  72. 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 { millisecondsFromTimestamp } from '../pure-functions/timestamp.js'
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[]> {
@@ -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.114",
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.137",
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.137"
77
+ "@juzi/wechaty-puppet": "^1.0.138"
78
78
  },
79
79
  "dependencies": {
80
- "@juzi/wechaty-grpc": "^1.0.101",
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