@juzi/wechaty-puppet-service 1.0.126 → 1.0.128

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 (34) hide show
  1. package/dist/cjs/src/client/puppet-service.d.ts +2 -0
  2. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  3. package/dist/cjs/src/client/puppet-service.js +29 -2
  4. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  5. package/dist/cjs/src/client/puppet-service.spec.js +85 -0
  6. package/dist/cjs/src/client/puppet-service.spec.js.map +1 -1
  7. package/dist/cjs/src/package-json.js +4 -4
  8. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  9. package/dist/cjs/src/server/puppet-implementation.js +29 -0
  10. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  11. package/dist/cjs/src/server/puppet-implementation.spec.d.ts +3 -0
  12. package/dist/cjs/src/server/puppet-implementation.spec.d.ts.map +1 -0
  13. package/dist/cjs/src/server/puppet-implementation.spec.js +79 -0
  14. package/dist/cjs/src/server/puppet-implementation.spec.js.map +1 -0
  15. package/dist/esm/src/client/puppet-service.d.ts +2 -0
  16. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  17. package/dist/esm/src/client/puppet-service.js +29 -2
  18. package/dist/esm/src/client/puppet-service.js.map +1 -1
  19. package/dist/esm/src/client/puppet-service.spec.js +86 -1
  20. package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
  21. package/dist/esm/src/package-json.js +4 -4
  22. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  23. package/dist/esm/src/server/puppet-implementation.js +29 -0
  24. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  25. package/dist/esm/src/server/puppet-implementation.spec.d.ts +3 -0
  26. package/dist/esm/src/server/puppet-implementation.spec.d.ts.map +1 -0
  27. package/dist/esm/src/server/puppet-implementation.spec.js +77 -0
  28. package/dist/esm/src/server/puppet-implementation.spec.js.map +1 -0
  29. package/package.json +4 -4
  30. package/src/client/puppet-service.spec.ts +144 -0
  31. package/src/client/puppet-service.ts +44 -2
  32. package/src/package-json.ts +4 -4
  33. package/src/server/puppet-implementation.spec.ts +121 -0
  34. package/src/server/puppet-implementation.ts +35 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"puppet-implementation.spec.js","sourceRoot":"","sources":["../../../../src/server/puppet-implementation.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juzi/wechaty-puppet-service",
3
- "version": "1.0.126",
3
+ "version": "1.0.128",
4
4
  "description": "Puppet Service for Wechaty",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,7 +51,7 @@
51
51
  "@chatie/eslint-config": "^1.0.4",
52
52
  "@chatie/semver": "^0.4.7",
53
53
  "@chatie/tsconfig": "^4.6.3",
54
- "@juzi/wechaty-puppet": "^1.0.151",
54
+ "@juzi/wechaty-puppet": "^1.0.155",
55
55
  "@juzi/wechaty-puppet-mock": "^1.0.1",
56
56
  "@swc/core": "1.3.39",
57
57
  "@types/google-protobuf": "^3.15.5",
@@ -71,10 +71,10 @@
71
71
  "why-is-node-running": "^2.2.1"
72
72
  },
73
73
  "peerDependencies": {
74
- "@juzi/wechaty-puppet": "^1.0.151"
74
+ "@juzi/wechaty-puppet": "^1.0.155"
75
75
  },
76
76
  "dependencies": {
77
- "@juzi/wechaty-grpc": "^1.0.106",
77
+ "@juzi/wechaty-grpc": "^1.0.107",
78
78
  "clone-class": "^1.1.1",
79
79
  "ducks": "^1.0.2",
80
80
  "file-box": "^1.5.5",
@@ -1,15 +1,159 @@
1
1
  #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
2
 
3
3
  import {
4
+ sinon,
4
5
  test,
5
6
  } from 'tstest'
6
7
  import getPort from 'get-port'
7
8
 
9
+ import { FileBox } from 'file-box'
10
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
11
+
8
12
  import { PuppetMock } from '@juzi/wechaty-puppet-mock'
9
13
 
10
14
  import { PuppetService } from './puppet-service.js'
11
15
  import { PuppetServer } from '../mod.js'
12
16
 
17
+ let batchRoomFallbackTestId = 0
18
+
19
+ function createBatchRoomFallbackPuppet (
20
+ roomRawPayload: (roomId: string) => Promise<{ id: string }>,
21
+ ): PuppetService {
22
+ const puppet = new PuppetService({
23
+ token: `batch-room-fallback-test-${batchRoomFallbackTestId++}`,
24
+ })
25
+
26
+ ;(puppet as any)._payloadStore.room = {
27
+ get: async () => undefined,
28
+ set: async () => undefined,
29
+ }
30
+ ;(puppet as any)._grpcManager = {
31
+ client: {
32
+ batchRoomPayload: (_request: unknown, callback: (error: Error) => void) => {
33
+ callback(new Error('batch room payload unavailable'))
34
+ },
35
+ },
36
+ }
37
+ sinon.stub(puppet, 'roomRawPayload').callsFake(roomRawPayload as any)
38
+
39
+ return puppet
40
+ }
41
+
42
+ test('batchRoomRawPayload isolates one invalid room during per-room fallback', async t => {
43
+ const puppet = createBatchRoomFallbackPuppet(async roomId => {
44
+ if (roomId === '10006') {
45
+ throw new Error('invalid roomId: 10006')
46
+ }
47
+ return { id: roomId }
48
+ })
49
+
50
+ const result = await puppet.batchRoomRawPayload([
51
+ 'room-valid-before',
52
+ '10006',
53
+ 'room-valid-after',
54
+ ])
55
+
56
+ t.same(
57
+ Array.from(result.keys()),
58
+ [ 'room-valid-before', 'room-valid-after' ],
59
+ 'returns every valid room and omits only the invalid room',
60
+ )
61
+ t.equal(
62
+ (puppet.roomRawPayload as any).callCount,
63
+ 3,
64
+ 'continues the fallback loop after one room fails',
65
+ )
66
+ })
67
+
68
+ test('batchRoomRawPayload still rejects when every per-room fallback fails', async t => {
69
+ const puppet = createBatchRoomFallbackPuppet(async roomId => {
70
+ throw new Error(`invalid roomId: ${roomId}`)
71
+ })
72
+
73
+ await t.rejects(
74
+ puppet.batchRoomRawPayload([ '10006', '10007' ]),
75
+ /batch room payload unavailable/,
76
+ 'does not turn a total upstream failure into an empty successful result',
77
+ )
78
+ })
79
+
80
+ let callInviteWithMediaTestId = 0
81
+
82
+ function createCallInviteWithMediaPuppet (
83
+ callId: string,
84
+ ): { puppet: PuppetService, requestList: grpcPuppet.CallInviteWithMediaRequest[] } {
85
+ const puppet = new PuppetService({
86
+ token: `call-invite-with-media-test-${callInviteWithMediaTestId++}`,
87
+ })
88
+
89
+ const requestList: grpcPuppet.CallInviteWithMediaRequest[] = []
90
+
91
+ ;(puppet as any)._grpcManager = {
92
+ client: {
93
+ callInviteWithMedia: (
94
+ request : grpcPuppet.CallInviteWithMediaRequest,
95
+ callback : (error: null | Error, response: grpcPuppet.CallInviteWithMediaResponse) => void,
96
+ ) => {
97
+ requestList.push(request)
98
+
99
+ const response = new grpcPuppet.CallInviteWithMediaResponse()
100
+ response.setCallId(callId)
101
+ callback(null, response)
102
+ },
103
+ },
104
+ }
105
+
106
+ return { puppet, requestList }
107
+ }
108
+
109
+ test('callInviteWithMedia maps the file and the orchestration options onto the request', async t => {
110
+ const { puppet, requestList } = createCallInviteWithMediaPuppet('call-id-with-file')
111
+
112
+ const file = FileBox.fromUrl('https://example.com/notice.mp3', { name: 'notice.mp3' })
113
+
114
+ const callId = await puppet.callInviteWithMedia(
115
+ [ 'contact-1', 'contact-2' ],
116
+ file,
117
+ { hangupDelayMs: 800, hangupOnFinish: true },
118
+ )
119
+
120
+ t.equal(callId, 'call-id-with-file', 'returns the call_id from the server response')
121
+ t.equal(requestList.length, 1, 'sends exactly one request')
122
+
123
+ const request = requestList[0]!
124
+ t.same(request.getContactIdsList(), [ 'contact-1', 'contact-2' ], 'maps contactIds onto contact_ids')
125
+ t.equal(request.getHangupOnFinish(), true, 'maps hangupOnFinish onto hangup_on_finish')
126
+ t.equal(request.getHangupDelayMs(), 800, 'maps hangupDelayMs onto hangup_delay_ms')
127
+ t.equal(
128
+ JSON.parse(request.getFileBox()).name,
129
+ 'notice.mp3',
130
+ 'serializes the FileBox onto file_box',
131
+ )
132
+ })
133
+
134
+ test('callInviteWithMedia leaves file_box empty and defaults the options when they are omitted', async t => {
135
+ const { puppet, requestList } = createCallInviteWithMediaPuppet('call-id-no-file')
136
+
137
+ const callId = await puppet.callInviteWithMedia([ 'contact-1' ])
138
+
139
+ t.equal(callId, 'call-id-no-file', 'returns the call_id from the server response')
140
+
141
+ const request = requestList[0]!
142
+ t.equal(request.getFileBox(), '', 'leaves file_box empty when no file is given')
143
+ t.equal(request.getHangupOnFinish(), false, 'defaults hangup_on_finish to false')
144
+ t.equal(request.getHangupDelayMs(), 0, 'defaults hangup_delay_ms to 0')
145
+ })
146
+
147
+ test('callInviteWithMedia rejects when the server returns an empty call_id', async t => {
148
+ const { puppet } = createCallInviteWithMediaPuppet('')
149
+
150
+ await t.rejects(
151
+ puppet.callInviteWithMedia([ 'contact-1' ], undefined, { hangupOnFinish: true }),
152
+ /empty call_id/,
153
+ 'does not hand back an unusable empty call_id',
154
+ )
155
+ })
156
+
13
157
  test('version()', async t => {
14
158
  const puppet = new PuppetService({
15
159
  token: 'test',
@@ -56,6 +56,7 @@ import { PayloadStore } from './payload-store.js'
56
56
  import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPbToPayload, channelPayloadToPb, channelPbToPayload, chatHistoryPbToPayload, contactPbToPayload, postPayloadToPb, roomMemberPbToPayload, urlLinkPbToPayload, channelCardPayloadToPb, channelCardPbToPayload, miniProgramPayloadToPb, urlLinkPayloadToPb, locationPayloadToPb } from '../utils/pb-payload-helper.js'
57
57
  import { puppetCallMediaTypeToGrpc, grpcCallTypeToPuppetMedia } from '../utils/call-media-mapping.js'
58
58
  import type { MessageBroadcastTargets } from '@juzi/wechaty-puppet/dist/esm/src/schemas/message.js'
59
+ import type { CallInviteWithMediaOptions } from '@juzi/wechaty-puppet/dist/esm/src/schemas/call.js'
59
60
  import { timeoutPromise } from 'gerror'
60
61
  import { BooleanIndicator } from 'state-switch'
61
62
  import type { Contact } from '@juzi/wechaty-puppet/types'
@@ -1184,6 +1185,38 @@ class PuppetService extends PUPPET.Puppet {
1184
1185
  return callId
1185
1186
  }
1186
1187
 
1188
+ override async callInviteWithMedia (
1189
+ contactIds : string[],
1190
+ file? : FileBoxInterface,
1191
+ options? : CallInviteWithMediaOptions,
1192
+ ): Promise<string> {
1193
+ this.log.verbose('PuppetService', 'callInviteWithMedia(%s, %s, %s)',
1194
+ contactIds,
1195
+ file?.name,
1196
+ JSON.stringify(options),
1197
+ )
1198
+
1199
+ const request = new grpcPuppet.CallInviteWithMediaRequest()
1200
+ request.setContactIdsList(contactIds)
1201
+ if (file) {
1202
+ request.setFileBox(await this.serializeFileBox(file))
1203
+ }
1204
+ request.setHangupOnFinish(options?.hangupOnFinish ?? false)
1205
+ request.setHangupDelayMs(options?.hangupDelayMs ?? 0)
1206
+
1207
+ const response = await util.promisify(
1208
+ this.grpcManager.client.callInviteWithMedia
1209
+ .bind(this.grpcManager.client),
1210
+ )(request)
1211
+
1212
+ const callId = response.getCallId()
1213
+ if (!callId) {
1214
+ throw new Error('callInviteWithMedia: puppet server returned empty call_id')
1215
+ }
1216
+
1217
+ return callId
1218
+ }
1219
+
1187
1220
  override async callAdd (
1188
1221
  callId: string,
1189
1222
  contactIds: string[],
@@ -2294,9 +2327,18 @@ class PuppetService extends PUPPET.Puppet {
2294
2327
  }
2295
2328
  } catch (e) {
2296
2329
  this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e)
2330
+ let fallbackSuccessCount = 0
2297
2331
  for (const roomId of needGetSet) {
2298
- const payload = await this.roomRawPayload(roomId)
2299
- result.set(roomId, payload)
2332
+ try {
2333
+ const payload = await this.roomRawPayload(roomId)
2334
+ result.set(roomId, payload)
2335
+ fallbackSuccessCount++
2336
+ } catch (fallbackError) {
2337
+ this.log.warn('PuppetService', 'roomRawPayload(%s) failed during batch fallback, skip this room: %s', roomId, fallbackError)
2338
+ }
2339
+ }
2340
+ if (fallbackSuccessCount === 0) {
2341
+ throw e
2300
2342
  }
2301
2343
  }
2302
2344
  }
@@ -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.126",
7
+ "version": "1.0.128",
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.151",
58
+ "@juzi/wechaty-puppet": "^1.0.155",
59
59
  "@juzi/wechaty-puppet-mock": "^1.0.1",
60
60
  "@swc/core": "1.3.39",
61
61
  "@types/google-protobuf": "^3.15.5",
@@ -75,10 +75,10 @@ export const packageJson: PackageJson = {
75
75
  "why-is-node-running": "^2.2.1"
76
76
  },
77
77
  "peerDependencies": {
78
- "@juzi/wechaty-puppet": "^1.0.151"
78
+ "@juzi/wechaty-puppet": "^1.0.155"
79
79
  },
80
80
  "dependencies": {
81
- "@juzi/wechaty-grpc": "^1.0.106",
81
+ "@juzi/wechaty-grpc": "^1.0.107",
82
82
  "clone-class": "^1.1.1",
83
83
  "ducks": "^1.0.2",
84
84
  "file-box": "^1.5.5",
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
+
3
+ import {
4
+ sinon,
5
+ test,
6
+ } from 'tstest'
7
+
8
+ import { FileBox } from 'file-box'
9
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
10
+
11
+ import { PuppetMock } from '@juzi/wechaty-puppet-mock'
12
+
13
+ import { puppetImplementation } from './puppet-implementation.js'
14
+
15
+ interface CallInviteWithMediaCase {
16
+ contactIds : string[]
17
+ fileBox? : string
18
+ hangupOnFinish? : boolean
19
+ hangupDelayMs? : number
20
+ }
21
+
22
+ function buildRequest (
23
+ options: CallInviteWithMediaCase,
24
+ ): grpcPuppet.CallInviteWithMediaRequest {
25
+ const request = new grpcPuppet.CallInviteWithMediaRequest()
26
+ request.setContactIdsList(options.contactIds)
27
+ request.setFileBox(options.fileBox ?? '')
28
+ request.setHangupOnFinish(options.hangupOnFinish ?? false)
29
+ request.setHangupDelayMs(options.hangupDelayMs ?? 0)
30
+ return request
31
+ }
32
+
33
+ async function invokeCallInviteWithMedia (
34
+ puppet : any,
35
+ request : grpcPuppet.CallInviteWithMediaRequest,
36
+ ): Promise<{ error: null | Error, response: null | grpcPuppet.CallInviteWithMediaResponse }> {
37
+ const impl = puppetImplementation(puppet, FileBox)
38
+
39
+ return new Promise(resolve => {
40
+ void impl.callInviteWithMedia(
41
+ { request } as any,
42
+ ((error: any, response: any) => resolve({ error, response })) as any,
43
+ )
44
+ })
45
+ }
46
+
47
+ test('callInviteWithMedia rejects an empty contact_ids list', async t => {
48
+ const puppet = new PuppetMock() as any
49
+ const stub = sinon.stub().resolves('call-id')
50
+ puppet.callInviteWithMedia = stub
51
+
52
+ const { error } = await invokeCallInviteWithMedia(puppet, buildRequest({
53
+ contactIds : [],
54
+ hangupOnFinish : true,
55
+ }))
56
+
57
+ t.match(error?.message, /contact_ids is required/, 'reports the missing contact_ids')
58
+ t.equal(stub.callCount, 0, 'never reaches the puppet')
59
+ })
60
+
61
+ test('callInviteWithMedia rejects an empty file_box combined with hangup_on_finish false', async t => {
62
+ const puppet = new PuppetMock() as any
63
+ const stub = sinon.stub().resolves('call-id')
64
+ puppet.callInviteWithMedia = stub
65
+
66
+ const { error } = await invokeCallInviteWithMedia(puppet, buildRequest({
67
+ contactIds : [ 'contact-1' ],
68
+ hangupOnFinish : false,
69
+ }))
70
+
71
+ t.match(error?.message, /use CallInvite directly/, 'points the caller at the equivalent CallInvite')
72
+ t.equal(stub.callCount, 0, 'never reaches the puppet')
73
+ })
74
+
75
+ test('callInviteWithMedia forwards a file-less invite as an undefined file', async t => {
76
+ const puppet = new PuppetMock() as any
77
+ const stub = sinon.stub().resolves('call-id-no-file')
78
+ puppet.callInviteWithMedia = stub
79
+
80
+ const { error, response } = await invokeCallInviteWithMedia(puppet, buildRequest({
81
+ contactIds : [ 'contact-1' ],
82
+ hangupDelayMs : 1500,
83
+ hangupOnFinish : true,
84
+ }))
85
+
86
+ t.equal(error, null, 'accepts the file-less invite')
87
+ t.equal(response?.getCallId(), 'call-id-no-file', 'returns the callId from the puppet')
88
+ t.same(
89
+ stub.args[0],
90
+ [ [ 'contact-1' ], undefined, { hangupDelayMs: 1500, hangupOnFinish: true } ],
91
+ 'forwards contactIds, an undefined file and the orchestration options',
92
+ )
93
+ })
94
+
95
+ test('callInviteWithMedia deserializes a non-empty file_box into a FileBox', async t => {
96
+ const puppet = new PuppetMock() as any
97
+ const stub = sinon.stub().resolves('call-id-with-file')
98
+ puppet.callInviteWithMedia = stub
99
+
100
+ const file = FileBox.fromUrl('https://example.com/notice.mp3', { name: 'notice.mp3' })
101
+
102
+ const { error } = await invokeCallInviteWithMedia(puppet, buildRequest({
103
+ contactIds : [ 'contact-1' ],
104
+ fileBox : JSON.stringify(file),
105
+ }))
106
+
107
+ t.equal(error, null, 'accepts the invite carrying a file')
108
+ t.equal(stub.args[0]?.[1]?.name, 'notice.mp3', 'hands the puppet a FileBox rebuilt from file_box')
109
+ })
110
+
111
+ test('callInviteWithMedia rejects an empty callId returned by the puppet', async t => {
112
+ const puppet = new PuppetMock() as any
113
+ puppet.callInviteWithMedia = sinon.stub().resolves('')
114
+
115
+ const { error } = await invokeCallInviteWithMedia(puppet, buildRequest({
116
+ contactIds : [ 'contact-1' ],
117
+ hangupOnFinish : true,
118
+ }))
119
+
120
+ t.match(error?.message, /empty callId/, 'does not pass an unusable empty callId back on the wire')
121
+ })
@@ -3060,6 +3060,41 @@ function puppetImplementation (
3060
3060
  }
3061
3061
  },
3062
3062
 
3063
+ callInviteWithMedia: async (call, callback) => {
3064
+ log.verbose('PuppetServiceImpl', 'callInviteWithMedia()')
3065
+
3066
+ try {
3067
+ const contactIds = call.request.getContactIdsList()
3068
+ if (contactIds.length === 0) {
3069
+ throw new Error('callInviteWithMedia: contact_ids is required')
3070
+ }
3071
+
3072
+ const jsonText = call.request.getFileBox()
3073
+ const file = jsonText ? FileBoxUuid.fromJSON(jsonText) : undefined
3074
+ const hangupOnFinish = call.request.getHangupOnFinish()
3075
+ const hangupDelayMs = call.request.getHangupDelayMs()
3076
+
3077
+ if (!file && !hangupOnFinish) {
3078
+ throw new Error('callInviteWithMedia: hangup_on_finish must be true when file_box is empty, otherwise it is equivalent to CallInvite — use CallInvite directly')
3079
+ }
3080
+
3081
+ const callId = await puppet.callInviteWithMedia(contactIds, file, {
3082
+ hangupDelayMs,
3083
+ hangupOnFinish,
3084
+ })
3085
+ if (!callId) {
3086
+ throw new Error('callInviteWithMedia: puppet returned empty callId')
3087
+ }
3088
+
3089
+ const response = new grpcPuppet.CallInviteWithMediaResponse()
3090
+ response.setCallId(callId)
3091
+
3092
+ return callback(null, response)
3093
+ } catch (e) {
3094
+ return grpcError('callInviteWithMedia', e, callback)
3095
+ }
3096
+ },
3097
+
3063
3098
  callAdd: async (call, callback) => {
3064
3099
  log.verbose('PuppetServiceImpl', 'callAdd()')
3065
3100