@juzi/wechaty-puppet-service 1.0.113 → 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 (83) 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/payload-store.js +2 -2
  6. package/dist/cjs/src/client/payload-store.js.map +1 -1
  7. package/dist/cjs/src/client/puppet-service.d.ts +21 -0
  8. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  9. package/dist/cjs/src/client/puppet-service.js +230 -1
  10. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  11. package/dist/cjs/src/client/puppet-service.spec.js.map +1 -1
  12. package/dist/cjs/src/package-json.js +4 -4
  13. package/dist/cjs/src/package-json.js.map +1 -1
  14. package/dist/cjs/src/pure-functions/timestamp.d.ts +15 -1
  15. package/dist/cjs/src/pure-functions/timestamp.d.ts.map +1 -1
  16. package/dist/cjs/src/pure-functions/timestamp.js +10 -1
  17. package/dist/cjs/src/pure-functions/timestamp.js.map +1 -1
  18. package/dist/cjs/src/pure-functions/timestamp.spec.d.ts +3 -0
  19. package/dist/cjs/src/pure-functions/timestamp.spec.d.ts.map +1 -0
  20. package/dist/cjs/src/pure-functions/timestamp.spec.js +48 -0
  21. package/dist/cjs/src/pure-functions/timestamp.spec.js.map +1 -0
  22. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  23. package/dist/cjs/src/server/event-stream-manager.js +7 -0
  24. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  25. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  26. package/dist/cjs/src/server/puppet-implementation.js +296 -2
  27. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  28. package/dist/cjs/src/utils/call-media-mapping.d.ts +17 -0
  29. package/dist/cjs/src/utils/call-media-mapping.d.ts.map +1 -0
  30. package/dist/cjs/src/utils/call-media-mapping.js +60 -0
  31. package/dist/cjs/src/utils/call-media-mapping.js.map +1 -0
  32. package/dist/cjs/src/utils/call-media-mapping.spec.d.ts +3 -0
  33. package/dist/cjs/src/utils/call-media-mapping.spec.d.ts.map +1 -0
  34. package/dist/cjs/src/utils/call-media-mapping.spec.js +65 -0
  35. package/dist/cjs/src/utils/call-media-mapping.spec.js.map +1 -0
  36. package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts +3 -0
  37. package/dist/esm/src/client/call-raw-payload-parser.spec.d.ts.map +1 -0
  38. package/dist/esm/src/client/call-raw-payload-parser.spec.js +103 -0
  39. package/dist/esm/src/client/call-raw-payload-parser.spec.js.map +1 -0
  40. package/dist/esm/src/client/payload-store.js +2 -2
  41. package/dist/esm/src/client/payload-store.js.map +1 -1
  42. package/dist/esm/src/client/puppet-service.d.ts +21 -0
  43. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  44. package/dist/esm/src/client/puppet-service.js +232 -3
  45. package/dist/esm/src/client/puppet-service.js.map +1 -1
  46. package/dist/esm/src/client/puppet-service.spec.js +1 -1
  47. package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
  48. package/dist/esm/src/package-json.js +4 -4
  49. package/dist/esm/src/package-json.js.map +1 -1
  50. package/dist/esm/src/pure-functions/timestamp.d.ts +15 -1
  51. package/dist/esm/src/pure-functions/timestamp.d.ts.map +1 -1
  52. package/dist/esm/src/pure-functions/timestamp.js +9 -1
  53. package/dist/esm/src/pure-functions/timestamp.js.map +1 -1
  54. package/dist/esm/src/pure-functions/timestamp.spec.d.ts +3 -0
  55. package/dist/esm/src/pure-functions/timestamp.spec.d.ts.map +1 -0
  56. package/dist/esm/src/pure-functions/timestamp.spec.js +46 -0
  57. package/dist/esm/src/pure-functions/timestamp.spec.js.map +1 -0
  58. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  59. package/dist/esm/src/server/event-stream-manager.js +7 -0
  60. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  61. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  62. package/dist/esm/src/server/puppet-implementation.js +296 -2
  63. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  64. package/dist/esm/src/utils/call-media-mapping.d.ts +17 -0
  65. package/dist/esm/src/utils/call-media-mapping.d.ts.map +1 -0
  66. package/dist/esm/src/utils/call-media-mapping.js +32 -0
  67. package/dist/esm/src/utils/call-media-mapping.js.map +1 -0
  68. package/dist/esm/src/utils/call-media-mapping.spec.d.ts +3 -0
  69. package/dist/esm/src/utils/call-media-mapping.spec.d.ts.map +1 -0
  70. package/dist/esm/src/utils/call-media-mapping.spec.js +40 -0
  71. package/dist/esm/src/utils/call-media-mapping.spec.js.map +1 -0
  72. package/package.json +4 -4
  73. package/src/client/call-raw-payload-parser.spec.ts +131 -0
  74. package/src/client/payload-store.ts +2 -2
  75. package/src/client/puppet-service.spec.ts +3 -1
  76. package/src/client/puppet-service.ts +438 -4
  77. package/src/package-json.ts +4 -4
  78. package/src/pure-functions/timestamp.spec.ts +56 -0
  79. package/src/pure-functions/timestamp.ts +19 -0
  80. package/src/server/event-stream-manager.ts +7 -0
  81. package/src/server/puppet-implementation.ts +384 -3
  82. package/src/utils/call-media-mapping.spec.ts +53 -0
  83. package/src/utils/call-media-mapping.ts +35 -0
@@ -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,8 +44,39 @@ 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
 
50
+ type MessageBatchSendResponse = Awaited<ReturnType<PUPPET.impls.PuppetInterface['messageBatchSendText']>>
51
+
52
+ type GrpcBatchResult = {
53
+ setConversationId(value: string): void,
54
+ setError(value: string): void,
55
+ setId(value: string): void,
56
+ }
57
+
58
+ type GrpcBatchResponse<Result extends GrpcBatchResult> = {
59
+ setResultsList(value: Result[]): void,
60
+ }
61
+
62
+ const setGrpcBatchResults = <Result extends GrpcBatchResult> (
63
+ response : GrpcBatchResponse<Result>,
64
+ ResultClass : new () => Result,
65
+ payload : MessageBatchSendResponse,
66
+ ): void => {
67
+ response.setResultsList(payload.results.map(result => {
68
+ const pbResult = new ResultClass()
69
+ pbResult.setConversationId(result.conversationId)
70
+ if (result.id) {
71
+ pbResult.setId(result.id)
72
+ }
73
+ if (result.error) {
74
+ pbResult.setError(result.error)
75
+ }
76
+ return pbResult
77
+ }))
78
+ }
79
+
49
80
  function puppetImplementation (
50
81
  puppet : PUPPET.impls.PuppetInterface,
51
82
  FileBoxUuid : typeof FileBox,
@@ -1251,6 +1282,180 @@ function puppetImplementation (
1251
1282
  }
1252
1283
  },
1253
1284
 
1285
+ messageBatchSendText: async (call, callback) => {
1286
+ log.verbose('PuppetServiceImpl', 'messageBatchSendText()')
1287
+
1288
+ try {
1289
+ const results = await puppet.messageBatchSendText(
1290
+ call.request.getConversationIdsList(),
1291
+ call.request.getText(),
1292
+ call.request.getBatchTaskId(),
1293
+ )
1294
+ const response = new grpcPuppet.MessageBatchSendTextResponse()
1295
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendTextResponse.Result, results)
1296
+ return callback(null, response)
1297
+ } catch (e) {
1298
+ return grpcError('messageBatchSendText', e, callback)
1299
+ }
1300
+ },
1301
+
1302
+ messageBatchSendFile: async (call, callback) => {
1303
+ log.verbose('PuppetServiceImpl', 'messageBatchSendFile()')
1304
+
1305
+ try {
1306
+ const results = await puppet.messageBatchSendFile(
1307
+ call.request.getConversationIdsList(),
1308
+ FileBoxUuid.fromJSON(call.request.getFileBox()),
1309
+ call.request.getBatchTaskId(),
1310
+ )
1311
+ const response = new grpcPuppet.MessageBatchSendFileResponse()
1312
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendFileResponse.Result, results)
1313
+ return callback(null, response)
1314
+ } catch (e) {
1315
+ return grpcError('messageBatchSendFile', e, callback)
1316
+ }
1317
+ },
1318
+
1319
+ messageBatchForward: async (call, callback) => {
1320
+ log.verbose('PuppetServiceImpl', 'messageBatchForward()')
1321
+
1322
+ try {
1323
+ const messageIds = call.request.getMessageIdsList()
1324
+ const results = await puppet.messageBatchForward(
1325
+ call.request.getConversationIdsList(),
1326
+ messageIds.length > 0 ? messageIds : call.request.getMessageId(),
1327
+ call.request.getBatchTaskId(),
1328
+ )
1329
+ const response = new grpcPuppet.MessageBatchForwardResponse()
1330
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchForwardResponse.Result, results)
1331
+ return callback(null, response)
1332
+ } catch (e) {
1333
+ return grpcError('messageBatchForward', e, callback)
1334
+ }
1335
+ },
1336
+
1337
+ messageBatchSendContact: async (call, callback) => {
1338
+ log.verbose('PuppetServiceImpl', 'messageBatchSendContact()')
1339
+
1340
+ try {
1341
+ const results = await puppet.messageBatchSendContact(
1342
+ call.request.getConversationIdsList(),
1343
+ call.request.getContactId(),
1344
+ call.request.getBatchTaskId(),
1345
+ )
1346
+ const response = new grpcPuppet.MessageBatchSendContactResponse()
1347
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendContactResponse.Result, results)
1348
+ return callback(null, response)
1349
+ } catch (e) {
1350
+ return grpcError('messageBatchSendContact', e, callback)
1351
+ }
1352
+ },
1353
+
1354
+ messageBatchSendUrl: async (call, callback) => {
1355
+ log.verbose('PuppetServiceImpl', 'messageBatchSendUrl()')
1356
+
1357
+ try {
1358
+ const payload = call.request.getUrlLink()?.toObject()
1359
+ if (!payload) {
1360
+ throw new Error('no url link payload found')
1361
+ }
1362
+ const results = await puppet.messageBatchSendUrl(
1363
+ call.request.getConversationIdsList(),
1364
+ payload,
1365
+ call.request.getBatchTaskId(),
1366
+ )
1367
+ const response = new grpcPuppet.MessageBatchSendUrlResponse()
1368
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendUrlResponse.Result, results)
1369
+ return callback(null, response)
1370
+ } catch (e) {
1371
+ return grpcError('messageBatchSendUrl', e, callback)
1372
+ }
1373
+ },
1374
+
1375
+ messageBatchSendMiniProgram: async (call, callback) => {
1376
+ log.verbose('PuppetServiceImpl', 'messageBatchSendMiniProgram()')
1377
+
1378
+ try {
1379
+ const payload = call.request.getMiniProgram()?.toObject()
1380
+ if (!payload) {
1381
+ throw new Error('no mini program payload found')
1382
+ }
1383
+ const results = await puppet.messageBatchSendMiniProgram(
1384
+ call.request.getConversationIdsList(),
1385
+ payload,
1386
+ call.request.getBatchTaskId(),
1387
+ )
1388
+ const response = new grpcPuppet.MessageBatchSendMiniProgramResponse()
1389
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendMiniProgramResponse.Result, results)
1390
+ return callback(null, response)
1391
+ } catch (e) {
1392
+ return grpcError('messageBatchSendMiniProgram', e, callback)
1393
+ }
1394
+ },
1395
+
1396
+ messageBatchSendLocation: async (call, callback) => {
1397
+ log.verbose('PuppetServiceImpl', 'messageBatchSendLocation()')
1398
+
1399
+ try {
1400
+ const payload = call.request.getLocation()?.toObject()
1401
+ if (!payload) {
1402
+ throw new Error('no location payload found')
1403
+ }
1404
+ const results = await puppet.messageBatchSendLocation(
1405
+ call.request.getConversationIdsList(),
1406
+ payload,
1407
+ call.request.getBatchTaskId(),
1408
+ )
1409
+ const response = new grpcPuppet.MessageBatchSendLocationResponse()
1410
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendLocationResponse.Result, results)
1411
+ return callback(null, response)
1412
+ } catch (e) {
1413
+ return grpcError('messageBatchSendLocation', e, callback)
1414
+ }
1415
+ },
1416
+
1417
+ messageBatchSendChannel: async (call, callback) => {
1418
+ log.verbose('PuppetServiceImpl', 'messageBatchSendChannel()')
1419
+
1420
+ try {
1421
+ const payload = call.request.getChannel()?.toObject()
1422
+ if (!payload) {
1423
+ throw new Error('no channel payload found')
1424
+ }
1425
+ const results = await puppet.messageBatchSendChannel(
1426
+ call.request.getConversationIdsList(),
1427
+ payload,
1428
+ call.request.getBatchTaskId(),
1429
+ )
1430
+ const response = new grpcPuppet.MessageBatchSendChannelResponse()
1431
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendChannelResponse.Result, results)
1432
+ return callback(null, response)
1433
+ } catch (e) {
1434
+ return grpcError('messageBatchSendChannel', e, callback)
1435
+ }
1436
+ },
1437
+
1438
+ messageBatchSendChannelCard: async (call, callback) => {
1439
+ log.verbose('PuppetServiceImpl', 'messageBatchSendChannelCard()')
1440
+
1441
+ try {
1442
+ const payload = call.request.getChannelCard()?.toObject()
1443
+ if (!payload) {
1444
+ throw new Error('no channel card payload found')
1445
+ }
1446
+ const results = await puppet.messageBatchSendChannelCard(
1447
+ call.request.getConversationIdsList(),
1448
+ payload,
1449
+ call.request.getBatchTaskId(),
1450
+ )
1451
+ const response = new grpcPuppet.MessageBatchSendChannelCardResponse()
1452
+ setGrpcBatchResults(response, grpcPuppet.MessageBatchSendChannelCardResponse.Result, results)
1453
+ return callback(null, response)
1454
+ } catch (e) {
1455
+ return grpcError('messageBatchSendChannelCard', e, callback)
1456
+ }
1457
+ },
1458
+
1254
1459
  messageSendUrl: async (call, callback) => {
1255
1460
  log.verbose('PuppetServiceImpl', 'messageSendUrl()')
1256
1461
 
@@ -1494,9 +1699,11 @@ function puppetImplementation (
1494
1699
  const inviteOnly = call.request.getInviteOnly()
1495
1700
  const quoteIds = call.request.getQuoteIdsList()
1496
1701
 
1497
- const target = contactIds.length > 0 ? contactIds : contactId
1498
-
1499
- await puppet.roomAdd(roomId, target, inviteOnly, quoteIds)
1702
+ if (contactIds.length > 1) {
1703
+ await puppet.roomAddV2(roomId, contactIds, inviteOnly, quoteIds)
1704
+ } else {
1705
+ await puppet.roomAdd(roomId, contactId || contactIds[0] || '', inviteOnly, quoteIds)
1706
+ }
1500
1707
 
1501
1708
  return callback(null, new grpcPuppet.RoomAddResponse())
1502
1709
 
@@ -2752,6 +2959,180 @@ function puppetImplementation (
2752
2959
 
2753
2960
  },
2754
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
+
2755
3136
  upload: async (call, callback) => {
2756
3137
  log.verbose('PuppetServiceImpl', 'upload()')
2757
3138
 
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
+
3
+ import { test } from 'tstest'
4
+
5
+ import * as PUPPET from '@juzi/wechaty-puppet'
6
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
7
+
8
+ import {
9
+ puppetCallMediaTypeToGrpc,
10
+ grpcCallTypeToPuppetMedia,
11
+ } from './call-media-mapping.js'
12
+
13
+ /**
14
+ * 1. CallMediaType Audio and Video round-trip: PUPPET -> gRPC -> PUPPET === original.
15
+ */
16
+ test('CallMediaType: Audio and Video survive a round-trip through gRPC', async t => {
17
+ const mediaCases = [
18
+ PUPPET.types.CallMediaType.Audio,
19
+ PUPPET.types.CallMediaType.Video,
20
+ ] as const
21
+
22
+ for (const media of mediaCases) {
23
+ const grpcValue = puppetCallMediaTypeToGrpc(media)
24
+ const roundTripped = grpcCallTypeToPuppetMedia(grpcValue)
25
+ t.equal(roundTripped, media, `round-trip should be identity for CallMediaType.${media}`)
26
+ }
27
+ })
28
+
29
+ /**
30
+ * 2. Each direction maps to the expected concrete enum member.
31
+ */
32
+ test('puppetCallMediaTypeToGrpc: Audio -> VOICE, Video -> VIDEO', async t => {
33
+ t.equal(puppetCallMediaTypeToGrpc(PUPPET.types.CallMediaType.Audio), grpcPuppet.CallType.CALL_TYPE_VOICE, 'Audio maps to CALL_TYPE_VOICE')
34
+ t.equal(puppetCallMediaTypeToGrpc(PUPPET.types.CallMediaType.Video), grpcPuppet.CallType.CALL_TYPE_VIDEO, 'Video maps to CALL_TYPE_VIDEO')
35
+ })
36
+
37
+ /**
38
+ * 3. CALL_TYPE_UNKNOWN (wire zero value) maps to undefined so callers can reject it explicitly.
39
+ */
40
+ test('grpcCallTypeToPuppetMedia: CALL_TYPE_UNKNOWN returns undefined', async t => {
41
+ const result = grpcCallTypeToPuppetMedia(grpcPuppet.CallType.CALL_TYPE_UNKNOWN)
42
+ t.equal(result, undefined, 'CALL_TYPE_UNKNOWN should map to undefined')
43
+ })
44
+
45
+ /**
46
+ * 4. Unknown numeric values not in the proto enum throw rather than silently returning garbage.
47
+ */
48
+ test('grpcCallTypeToPuppetMedia: unknown value 999 throws', async t => {
49
+ t.throws(
50
+ () => grpcCallTypeToPuppetMedia(999),
51
+ 'should throw for unknown CallType value 999',
52
+ )
53
+ })
@@ -0,0 +1,35 @@
1
+ import * as PUPPET from '@juzi/wechaty-puppet'
2
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
3
+
4
+ type GrpcCallType = grpcPuppet.CallTypeMap[keyof grpcPuppet.CallTypeMap]
5
+
6
+ /**
7
+ * PUPPET CallMediaType (string enum) -> gRPC CallType (number enum).
8
+ * The `never` check in default guarantees exhaustiveness at compile time,
9
+ * while the throw still guards against untyped callers at runtime.
10
+ * media is required per upstream contract: every call invite must carry a media type.
11
+ */
12
+ export function puppetCallMediaTypeToGrpc (media: PUPPET.types.CallMediaType): GrpcCallType {
13
+ switch (media) {
14
+ case PUPPET.types.CallMediaType.Audio: return grpcPuppet.CallType.CALL_TYPE_VOICE
15
+ case PUPPET.types.CallMediaType.Video: return grpcPuppet.CallType.CALL_TYPE_VIDEO
16
+ default: {
17
+ const exhaustive: never = media
18
+ throw new Error(`puppetCallMediaTypeToGrpc: unknown CallMediaType "${String(exhaustive)}"`)
19
+ }
20
+ }
21
+ }
22
+
23
+ /**
24
+ * gRPC CallType (number enum) -> PUPPET CallMediaType (string enum).
25
+ * CALL_TYPE_UNKNOWN (wire zero value) maps to undefined so callers can reject it explicitly.
26
+ */
27
+ export function grpcCallTypeToPuppetMedia (grpcType: number): PUPPET.types.CallMediaType | undefined {
28
+ switch (grpcType) {
29
+ case grpcPuppet.CallType.CALL_TYPE_UNKNOWN: return undefined
30
+ case grpcPuppet.CallType.CALL_TYPE_VOICE: return PUPPET.types.CallMediaType.Audio
31
+ case grpcPuppet.CallType.CALL_TYPE_VIDEO: return PUPPET.types.CallMediaType.Video
32
+ default:
33
+ throw new Error(`grpcCallTypeToPuppetMedia: unknown gRPC CallType value ${grpcType}`)
34
+ }
35
+ }