@juzi/wechaty-puppet-service 1.0.128 → 1.0.130

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 (46) 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 +45 -0
  4. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  5. package/dist/cjs/src/client/puppet-service.spec.js +107 -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/event-stream-manager.d.ts.map +1 -1
  9. package/dist/cjs/src/server/event-stream-manager.js +21 -0
  10. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  11. package/dist/cjs/src/server/event-stream-manager.spec.d.ts +3 -0
  12. package/dist/cjs/src/server/event-stream-manager.spec.d.ts.map +1 -0
  13. package/dist/cjs/src/server/event-stream-manager.spec.js +22 -0
  14. package/dist/cjs/src/server/event-stream-manager.spec.js.map +1 -0
  15. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  16. package/dist/cjs/src/server/puppet-implementation.js +44 -0
  17. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  18. package/dist/cjs/src/server/puppet-implementation.spec.js +98 -0
  19. package/dist/cjs/src/server/puppet-implementation.spec.js.map +1 -1
  20. package/dist/esm/src/client/puppet-service.d.ts +2 -0
  21. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  22. package/dist/esm/src/client/puppet-service.js +45 -0
  23. package/dist/esm/src/client/puppet-service.js.map +1 -1
  24. package/dist/esm/src/client/puppet-service.spec.js +107 -0
  25. package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
  26. package/dist/esm/src/package-json.js +4 -4
  27. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  28. package/dist/esm/src/server/event-stream-manager.js +21 -0
  29. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  30. package/dist/esm/src/server/event-stream-manager.spec.d.ts +3 -0
  31. package/dist/esm/src/server/event-stream-manager.spec.d.ts.map +1 -0
  32. package/dist/esm/src/server/event-stream-manager.spec.js +20 -0
  33. package/dist/esm/src/server/event-stream-manager.spec.js.map +1 -0
  34. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  35. package/dist/esm/src/server/puppet-implementation.js +44 -0
  36. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  37. package/dist/esm/src/server/puppet-implementation.spec.js +98 -0
  38. package/dist/esm/src/server/puppet-implementation.spec.js.map +1 -1
  39. package/package.json +4 -4
  40. package/src/client/puppet-service.spec.ts +147 -0
  41. package/src/client/puppet-service.ts +57 -0
  42. package/src/package-json.ts +4 -4
  43. package/src/server/event-stream-manager.spec.ts +30 -0
  44. package/src/server/event-stream-manager.ts +21 -0
  45. package/src/server/puppet-implementation.spec.ts +127 -0
  46. package/src/server/puppet-implementation.ts +50 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juzi/wechaty-puppet-service",
3
- "version": "1.0.128",
3
+ "version": "1.0.130",
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.155",
54
+ "@juzi/wechaty-puppet": "^1.0.157",
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.155"
74
+ "@juzi/wechaty-puppet": "^1.0.157"
75
75
  },
76
76
  "dependencies": {
77
- "@juzi/wechaty-grpc": "^1.0.107",
77
+ "@juzi/wechaty-grpc": "^1.0.109",
78
78
  "clone-class": "^1.1.1",
79
79
  "ducks": "^1.0.2",
80
80
  "file-box": "^1.5.5",
@@ -154,6 +154,153 @@ test('callInviteWithMedia rejects when the server returns an empty call_id', asy
154
154
  )
155
155
  })
156
156
 
157
+ let orgBroadcastTestId = 0
158
+
159
+ function createOrgBroadcastPuppet (
160
+ payloadResponse?: grpcPuppet.OrgBroadcastPayloadResponse,
161
+ ): { puppet: PuppetService, executeRequestList: grpcPuppet.OrgBroadcastExecuteRequest[] } {
162
+ const puppet = new PuppetService({
163
+ token: `org-broadcast-test-${orgBroadcastTestId++}`,
164
+ })
165
+
166
+ const executeRequestList: grpcPuppet.OrgBroadcastExecuteRequest[] = []
167
+
168
+ ;(puppet as any)._grpcManager = {
169
+ client: {
170
+ orgBroadcastExecute: (
171
+ request : grpcPuppet.OrgBroadcastExecuteRequest,
172
+ callback : (error: null | Error, response: grpcPuppet.OrgBroadcastExecuteResponse) => void,
173
+ ) => {
174
+ executeRequestList.push(request)
175
+ callback(null, new grpcPuppet.OrgBroadcastExecuteResponse())
176
+ },
177
+ orgBroadcastPayload: (
178
+ request : grpcPuppet.OrgBroadcastPayloadRequest,
179
+ callback : (error: null | Error, response?: grpcPuppet.OrgBroadcastPayloadResponse) => void,
180
+ ) => {
181
+ void request
182
+ callback(null, payloadResponse)
183
+ },
184
+ },
185
+ }
186
+
187
+ return { puppet, executeRequestList }
188
+ }
189
+
190
+ test('orgBroadcastPayload maps every response field onto OrgBroadcastPayload', async t => {
191
+ const roomTarget = new grpcPuppet.OrgBroadcastTarget()
192
+ roomTarget.setRoomId('R:room-1')
193
+ roomTarget.setStatus(grpcPuppet.OrgBroadcastTargetStatus.ORG_BROADCAST_TARGET_STATUS_UNCONFIRMED)
194
+
195
+ const sentRoomTarget = new grpcPuppet.OrgBroadcastTarget()
196
+ sentRoomTarget.setRoomId('R:room-2')
197
+ sentRoomTarget.setStatus(grpcPuppet.OrgBroadcastTargetStatus.ORG_BROADCAST_TARGET_STATUS_SENT)
198
+
199
+ const response = new grpcPuppet.OrgBroadcastPayloadResponse()
200
+ response.setId('4728994241730369413')
201
+ response.setSendType(2)
202
+ response.setConversationType(1)
203
+ response.setCreatorId('creator-1')
204
+ response.setExecTime(1758600000000)
205
+ response.setStatus(3)
206
+ response.setCanCancel(true)
207
+ response.setAllowSelect(true)
208
+ response.setSent(false)
209
+ response.setTotalCount(7)
210
+ response.setSentCount(5)
211
+ response.setContentListJson('[{"contentType":2}]')
212
+ response.setTargetsList([ roomTarget, sentRoomTarget ])
213
+
214
+ const { puppet } = createOrgBroadcastPuppet(response)
215
+ const payload = await puppet.orgBroadcastPayload('4728994241730369413')
216
+
217
+ t.same(payload, {
218
+ id : '4728994241730369413',
219
+ sendType : 2,
220
+ conversationType : 1,
221
+ creatorId : 'creator-1',
222
+ execTime : 1758600000000,
223
+ status : 3,
224
+ canCancel : true,
225
+ allowSelect : true,
226
+ sent : false,
227
+ totalCount : 7,
228
+ sentCount : 5,
229
+ contentListJson : '[{"contentType":2}]',
230
+ targets : [
231
+ { contactId: undefined, roomId: 'R:room-1', status: 4 },
232
+ { contactId: undefined, roomId: 'R:room-2', status: 1 },
233
+ ],
234
+ }, 'maps every field and drops the empty contact_id of room targets')
235
+ })
236
+
237
+ test('orgBroadcastPayload maps empty creator_id and contact targets', async t => {
238
+ const contactTarget = new grpcPuppet.OrgBroadcastTarget()
239
+ contactTarget.setContactId('contact-1')
240
+
241
+ const response = new grpcPuppet.OrgBroadcastPayloadResponse()
242
+ response.setId('org-broadcast-2')
243
+ response.setTargetsList([ contactTarget ])
244
+
245
+ const { puppet } = createOrgBroadcastPuppet(response)
246
+ const payload = await puppet.orgBroadcastPayload('org-broadcast-2')
247
+
248
+ t.equal(payload.creatorId, undefined, 'maps an empty creator_id to undefined')
249
+ t.same(payload.targets, [ { contactId: 'contact-1', roomId: undefined, status: 0 } ], 'drops the empty room_id of contact targets')
250
+ })
251
+
252
+ test('orgBroadcastExecute sends all targets as an empty target_ids when targetIds is omitted', async t => {
253
+ const { puppet, executeRequestList } = createOrgBroadcastPuppet()
254
+
255
+ await puppet.orgBroadcastExecute('org-broadcast-1')
256
+
257
+ t.equal(executeRequestList.length, 1, 'sends exactly one request')
258
+ t.equal(executeRequestList[0]!.getId(), 'org-broadcast-1', 'maps orgBroadcastId onto id')
259
+ t.same(executeRequestList[0]!.getTargetIdsList(), [], 'leaves target_ids empty')
260
+ })
261
+
262
+ test('orgBroadcastExecute forwards the selected targets', async t => {
263
+ const { puppet, executeRequestList } = createOrgBroadcastPuppet()
264
+
265
+ await puppet.orgBroadcastExecute('org-broadcast-1', [ 'contact-1', 'contact-2' ])
266
+
267
+ t.same(executeRequestList[0]!.getTargetIdsList(), [ 'contact-1', 'contact-2' ], 'maps targetIds onto target_ids')
268
+ })
269
+
270
+ test('orgBroadcastExecute rejects an explicit empty targetIds instead of sending to all targets', async t => {
271
+ const { puppet, executeRequestList } = createOrgBroadcastPuppet()
272
+
273
+ await t.rejects(
274
+ puppet.orgBroadcastExecute('org-broadcast-1', []),
275
+ /targetIds is empty/,
276
+ 'does not turn an empty selection into sending to all targets',
277
+ )
278
+ t.equal(executeRequestList.length, 0, 'never reaches the server')
279
+ })
280
+
281
+ test('org broadcast grpc events are emitted as puppet events', async t => {
282
+ const { puppet } = createOrgBroadcastPuppet()
283
+
284
+ const createdSpy = sinon.spy()
285
+ const sentSpy = sinon.spy()
286
+ puppet.on('org-broadcast-created', createdSpy)
287
+ puppet.on('org-broadcast-sent', sentSpy)
288
+
289
+ const createdEvent = new grpcPuppet.EventResponse()
290
+ createdEvent.setType(grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED)
291
+ createdEvent.setPayload(JSON.stringify({ orgBroadcastId: 'org-broadcast-1', messageId: 'message-1' }))
292
+
293
+ const sentEvent = new grpcPuppet.EventResponse()
294
+ sentEvent.setType(grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT)
295
+ sentEvent.setPayload(JSON.stringify({ orgBroadcastId: 'org-broadcast-1' }))
296
+
297
+ await (puppet as any).onGrpcStreamEvent(createdEvent)
298
+ await (puppet as any).onGrpcStreamEvent(sentEvent)
299
+
300
+ t.same(createdSpy.args[0], [ { orgBroadcastId: 'org-broadcast-1', messageId: 'message-1' } ], 'emits org-broadcast-created')
301
+ t.same(sentSpy.args[0], [ { orgBroadcastId: 'org-broadcast-1' } ], 'emits org-broadcast-sent')
302
+ })
303
+
157
304
  test('version()', async t => {
158
305
  const puppet = new PuppetService({
159
306
  token: 'test',
@@ -429,9 +429,18 @@ class PuppetService extends PUPPET.Puppet {
429
429
  case grpcPuppet.EventType.EVENT_TYPE_VERIFY_SLIDE:
430
430
  this.emit('verify-slide', JSON.parse(payload) as PUPPET.payloads.EventVerifySlide)
431
431
  break
432
+ case grpcPuppet.EventType.EVENT_TYPE_SAME_NET_VERIFY:
433
+ this.emit('same-net-verify', JSON.parse(payload) as PUPPET.payloads.EventSameNetVerify)
434
+ break
432
435
  case grpcPuppet.EventType.EVENT_TYPE_CALL:
433
436
  this.emit('call', JSON.parse(payload) as PUPPET.payloads.EventCall)
434
437
  break
438
+ case grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED:
439
+ this.emit('org-broadcast-created', JSON.parse(payload) as PUPPET.payloads.EventOrgBroadcastCreated)
440
+ break
441
+ case grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT:
442
+ this.emit('org-broadcast-sent', JSON.parse(payload) as PUPPET.payloads.EventOrgBroadcastSent)
443
+ break
435
444
 
436
445
  default:
437
446
  // Huan(202003): in default, the `type` type should be `never`, please check.
@@ -3725,6 +3734,54 @@ class PuppetService extends PUPPET.Puppet {
3725
3734
  return result
3726
3735
  }
3727
3736
 
3737
+ override async orgBroadcastPayload (orgBroadcastId: string): Promise<PUPPET.payloads.OrgBroadcast> {
3738
+ this.log.verbose('PuppetService', 'orgBroadcastPayload(%s)', orgBroadcastId)
3739
+
3740
+ const request = new grpcPuppet.OrgBroadcastPayloadRequest()
3741
+ request.setId(orgBroadcastId)
3742
+
3743
+ const response = await util.promisify(
3744
+ this.grpcManager.client.orgBroadcastPayload.bind(this.grpcManager.client),
3745
+ )(request)
3746
+
3747
+ return {
3748
+ id : response.getId(),
3749
+ sendType : response.getSendType(),
3750
+ conversationType : response.getConversationType() as PUPPET.types.OrgBroadcastConversationType,
3751
+ creatorId : response.getCreatorId() || undefined,
3752
+ execTime : response.getExecTime(),
3753
+ status : response.getStatus(),
3754
+ canCancel : response.getCanCancel(),
3755
+ allowSelect : response.getAllowSelect(),
3756
+ sent : response.getSent(),
3757
+ totalCount : response.getTotalCount(),
3758
+ sentCount : response.getSentCount(),
3759
+ contentListJson : response.getContentListJson(),
3760
+ targets : response.getTargetsList().map(targetPb => ({
3761
+ contactId : targetPb.getContactId() || undefined,
3762
+ roomId : targetPb.getRoomId() || undefined,
3763
+ status : targetPb.getStatus() as number as PUPPET.types.OrgBroadcastTargetStatus,
3764
+ })),
3765
+ }
3766
+ }
3767
+
3768
+ override async orgBroadcastExecute (orgBroadcastId: string, targetIds?: string[]): Promise<void> {
3769
+ this.log.verbose('PuppetService', 'orgBroadcastExecute(%s, %s)', orgBroadcastId, targetIds?.length)
3770
+
3771
+ // on the wire an empty target_ids means all targets, so an explicit empty list must not pass through
3772
+ if (targetIds && targetIds.length === 0) {
3773
+ throw new Error('targetIds is empty, omit it to send to all targets of the org broadcast')
3774
+ }
3775
+
3776
+ const request = new grpcPuppet.OrgBroadcastExecuteRequest()
3777
+ request.setId(orgBroadcastId)
3778
+ request.setTargetIdsList(targetIds ?? [])
3779
+
3780
+ await util.promisify(
3781
+ this.grpcManager.client.orgBroadcastExecute.bind(this.grpcManager.client),
3782
+ )(request)
3783
+ }
3784
+
3728
3785
  healthCheckInterval?: NodeJS.Timeout
3729
3786
  startHealthCheck () {
3730
3787
  this.healthCheckInterval = setInterval(() => {
@@ -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.128",
7
+ "version": "1.0.130",
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.155",
58
+ "@juzi/wechaty-puppet": "^1.0.157",
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.155"
78
+ "@juzi/wechaty-puppet": "^1.0.157"
79
79
  },
80
80
  "dependencies": {
81
- "@juzi/wechaty-grpc": "^1.0.107",
81
+ "@juzi/wechaty-grpc": "^1.0.109",
82
82
  "clone-class": "^1.1.1",
83
83
  "ducks": "^1.0.2",
84
84
  "file-box": "^1.5.5",
@@ -0,0 +1,30 @@
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 { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
9
+ import { PuppetMock } from '@juzi/wechaty-puppet-mock'
10
+
11
+ import { EventStreamManager } from './event-stream-manager.js'
12
+
13
+ test('org broadcast puppet events are forwarded as their own grpc event types', async t => {
14
+ const puppet = new PuppetMock() as any
15
+ const manager = new EventStreamManager(puppet)
16
+
17
+ const write = sinon.spy()
18
+ ;(manager as any).eventStream = { write }
19
+ manager.connectPuppetEventToStreamingCall()
20
+
21
+ puppet.emit('org-broadcast-created', { orgBroadcastId: 'org-broadcast-1', messageId: 'message-1' })
22
+ puppet.emit('org-broadcast-sent', { orgBroadcastId: 'org-broadcast-1' })
23
+
24
+ const [ created, sent ] = write.args.map(args => args[0] as grpcPuppet.EventResponse)
25
+
26
+ t.equal(created?.getType(), grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED, 'forwards org-broadcast-created as EVENT_TYPE_ORG_BROADCAST_CREATED')
27
+ t.same(JSON.parse(created!.getPayload()), { orgBroadcastId: 'org-broadcast-1', messageId: 'message-1' }, 'keeps the created payload')
28
+ t.equal(sent?.getType(), grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT, 'forwards org-broadcast-sent as EVENT_TYPE_ORG_BROADCAST_SENT')
29
+ t.same(JSON.parse(sent!.getPayload()), { orgBroadcastId: 'org-broadcast-1' }, 'keeps the sent payload')
30
+ })
@@ -322,6 +322,13 @@ class EventStreamManager {
322
322
  this.offCallbackList.push(off)
323
323
  break
324
324
  }
325
+ case 'same-net-verify': {
326
+ const listener = (payload: PUPPET.payloads.EventSameNetVerify) => this.grpcEmit(grpcPuppet.EventType.EVENT_TYPE_SAME_NET_VERIFY, payload)
327
+ this.puppet.on('same-net-verify', listener)
328
+ const off = () => this.puppet.off('same-net-verify', listener)
329
+ this.offCallbackList.push(off)
330
+ break
331
+ }
325
332
  case 'call': {
326
333
  const listener = (payload: PUPPET.payloads.EventCall) => this.grpcEmit(grpcPuppet.EventType.EVENT_TYPE_CALL, payload)
327
334
  this.puppet.on('call', listener)
@@ -329,6 +336,20 @@ class EventStreamManager {
329
336
  this.offCallbackList.push(off)
330
337
  break
331
338
  }
339
+ case 'org-broadcast-created': {
340
+ const listener = (payload: PUPPET.payloads.EventOrgBroadcastCreated) => this.grpcEmit(grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED, payload)
341
+ this.puppet.on('org-broadcast-created', listener)
342
+ const off = () => this.puppet.off('org-broadcast-created', listener)
343
+ this.offCallbackList.push(off)
344
+ break
345
+ }
346
+ case 'org-broadcast-sent': {
347
+ const listener = (payload: PUPPET.payloads.EventOrgBroadcastSent) => this.grpcEmit(grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT, payload)
348
+ this.puppet.on('org-broadcast-sent', listener)
349
+ const off = () => this.puppet.off('org-broadcast-sent', listener)
350
+ this.offCallbackList.push(off)
351
+ break
352
+ }
332
353
  case 'reset':
333
354
  // the `reset` event should be dealed internally, should not send out
334
355
  break
@@ -119,3 +119,130 @@ test('callInviteWithMedia rejects an empty callId returned by the puppet', async
119
119
 
120
120
  t.match(error?.message, /empty callId/, 'does not pass an unusable empty callId back on the wire')
121
121
  })
122
+
123
+ function invokeOrgBroadcast<Req, Res> (
124
+ puppet : any,
125
+ method : 'orgBroadcastPayload' | 'orgBroadcastExecute',
126
+ request : Req,
127
+ ): Promise<{ error: null | Error, response: null | Res }> {
128
+ const impl = puppetImplementation(puppet, FileBox)
129
+
130
+ return new Promise(resolve => {
131
+ void (impl[method] as any)(
132
+ { request } as any,
133
+ ((error: any, response: any) => resolve({ error, response })) as any,
134
+ )
135
+ })
136
+ }
137
+
138
+ test('orgBroadcastPayload serializes every payload field onto the response', async t => {
139
+ const puppet = new PuppetMock() as any
140
+ const stub = sinon.stub().resolves({
141
+ id : 'org-broadcast-1',
142
+ sendType : 2,
143
+ conversationType : 1,
144
+ creatorId : 'creator-1',
145
+ execTime : 1758600000000,
146
+ status : 3,
147
+ canCancel : true,
148
+ allowSelect : true,
149
+ sent : false,
150
+ totalCount : 7,
151
+ sentCount : 5,
152
+ contentListJson : '[{"contentType":2}]',
153
+ targets : [ { roomId: 'R:1', status: 4 }, { roomId: 'R:2', status: 1 } ],
154
+ })
155
+ puppet.orgBroadcastPayload = stub
156
+
157
+ const request = new grpcPuppet.OrgBroadcastPayloadRequest()
158
+ request.setId('org-broadcast-1')
159
+
160
+ const { error, response } = await invokeOrgBroadcast<grpcPuppet.OrgBroadcastPayloadRequest, grpcPuppet.OrgBroadcastPayloadResponse>(puppet, 'orgBroadcastPayload', request)
161
+
162
+ t.equal(error, null, 'returns without error')
163
+ t.same(stub.args[0], [ 'org-broadcast-1' ], 'passes the id to the puppet')
164
+ t.same(response?.toObject(), {
165
+ id : 'org-broadcast-1',
166
+ sendType : 2,
167
+ conversationType: 1,
168
+ creatorId : 'creator-1',
169
+ execTime : 1758600000000,
170
+ status : 3,
171
+ canCancel : true,
172
+ allowSelect : true,
173
+ sent : false,
174
+ totalCount : 7,
175
+ sentCount : 5,
176
+ contentListJson : '[{"contentType":2}]',
177
+ targetsList : [
178
+ { contactId: '', roomId: 'R:1', status: 4 },
179
+ { contactId: '', roomId: 'R:2', status: 1 },
180
+ ],
181
+ }, 'serializes every field')
182
+ })
183
+
184
+ test('orgBroadcastPayload serializes a missing creatorId and contact targets as empty strings', async t => {
185
+ const puppet = new PuppetMock() as any
186
+ puppet.orgBroadcastPayload = sinon.stub().resolves({
187
+ id : 'org-broadcast-2',
188
+ sendType : 0,
189
+ conversationType : 0,
190
+ execTime : 0,
191
+ status : 0,
192
+ canCancel : false,
193
+ allowSelect : false,
194
+ sent : false,
195
+ totalCount : 1,
196
+ sentCount : 0,
197
+ contentListJson : '[]',
198
+ targets : [ { contactId: 'contact-1', status: 0 } ],
199
+ })
200
+
201
+ const request = new grpcPuppet.OrgBroadcastPayloadRequest()
202
+ request.setId('org-broadcast-2')
203
+
204
+ const { response } = await invokeOrgBroadcast<grpcPuppet.OrgBroadcastPayloadRequest, grpcPuppet.OrgBroadcastPayloadResponse>(puppet, 'orgBroadcastPayload', request)
205
+
206
+ t.equal(response?.getCreatorId(), '', 'serializes a missing creatorId as empty')
207
+ t.same(response?.getTargetsList()[0]?.toObject(), { contactId: 'contact-1', roomId: '', status: 0 }, 'leaves room_id empty for a contact target')
208
+ })
209
+
210
+ test('orgBroadcastExecute turns an empty target_ids into sending to all targets', async t => {
211
+ const puppet = new PuppetMock() as any
212
+ const stub = sinon.stub().resolves()
213
+ puppet.orgBroadcastExecute = stub
214
+
215
+ const request = new grpcPuppet.OrgBroadcastExecuteRequest()
216
+ request.setId('org-broadcast-1')
217
+
218
+ const { error } = await invokeOrgBroadcast(puppet, 'orgBroadcastExecute', request)
219
+
220
+ t.equal(error, null, 'returns without error')
221
+ t.same(stub.args[0], [ 'org-broadcast-1', undefined ], 'omits targetIds')
222
+ })
223
+
224
+ test('orgBroadcastExecute forwards the selected target_ids', async t => {
225
+ const puppet = new PuppetMock() as any
226
+ const stub = sinon.stub().resolves()
227
+ puppet.orgBroadcastExecute = stub
228
+
229
+ const request = new grpcPuppet.OrgBroadcastExecuteRequest()
230
+ request.setId('org-broadcast-1')
231
+ request.setTargetIdsList([ 'contact-1' ])
232
+
233
+ await invokeOrgBroadcast(puppet, 'orgBroadcastExecute', request)
234
+
235
+ t.same(stub.args[0], [ 'org-broadcast-1', [ 'contact-1' ] ], 'passes targetIds through')
236
+ })
237
+
238
+ test('orgBroadcastExecute reports a puppet failure as a grpc error', async t => {
239
+ const puppet = new PuppetMock() as any
240
+ puppet.orgBroadcastExecute = sinon.stub().rejects(new Error('broadcast not found'))
241
+
242
+ const request = new grpcPuppet.OrgBroadcastExecuteRequest()
243
+ request.setId('org-broadcast-missing')
244
+
245
+ const { error } = await invokeOrgBroadcast(puppet, 'orgBroadcastExecute', request)
246
+
247
+ t.match(error?.message, /broadcast not found/, 'surfaces the puppet error')
248
+ })
@@ -3017,6 +3017,56 @@ function puppetImplementation (
3017
3017
  }
3018
3018
  },
3019
3019
 
3020
+ orgBroadcastPayload: async (call, callback) => {
3021
+ log.verbose('PuppetServiceImpl', 'orgBroadcastPayload()')
3022
+
3023
+ try {
3024
+ const orgBroadcastId = call.request.getId()
3025
+ const payload = await puppet.orgBroadcastPayload(orgBroadcastId)
3026
+
3027
+ const response = new grpcPuppet.OrgBroadcastPayloadResponse()
3028
+ response.setId(payload.id)
3029
+ response.setSendType(payload.sendType)
3030
+ response.setConversationType(payload.conversationType)
3031
+ response.setCreatorId(payload.creatorId ?? '')
3032
+ response.setExecTime(payload.execTime)
3033
+ response.setStatus(payload.status)
3034
+ response.setCanCancel(payload.canCancel)
3035
+ response.setAllowSelect(payload.allowSelect)
3036
+ response.setSent(payload.sent)
3037
+ response.setTotalCount(payload.totalCount)
3038
+ response.setSentCount(payload.sentCount)
3039
+ response.setContentListJson(payload.contentListJson)
3040
+ response.setTargetsList(payload.targets.map(target => {
3041
+ const targetPb = new grpcPuppet.OrgBroadcastTarget()
3042
+ targetPb.setContactId(target.contactId ?? '')
3043
+ targetPb.setRoomId(target.roomId ?? '')
3044
+ targetPb.setStatus(target.status as number as grpcPuppet.OrgBroadcastTargetStatusMap[keyof grpcPuppet.OrgBroadcastTargetStatusMap])
3045
+ return targetPb
3046
+ }))
3047
+
3048
+ return callback(null, response)
3049
+ } catch (e) {
3050
+ return grpcError('orgBroadcastPayload', e, callback)
3051
+ }
3052
+ },
3053
+
3054
+ orgBroadcastExecute: async (call, callback) => {
3055
+ log.verbose('PuppetServiceImpl', 'orgBroadcastExecute()')
3056
+
3057
+ try {
3058
+ const orgBroadcastId = call.request.getId()
3059
+ const targetIds = call.request.getTargetIdsList()
3060
+
3061
+ // an empty target_ids on the wire means all targets of the org broadcast
3062
+ await puppet.orgBroadcastExecute(orgBroadcastId, targetIds.length ? targetIds : undefined)
3063
+
3064
+ return callback(null, new grpcPuppet.OrgBroadcastExecuteResponse())
3065
+ } catch (e) {
3066
+ return grpcError('orgBroadcastExecute', e, callback)
3067
+ }
3068
+ },
3069
+
3020
3070
  download: async (call) => {
3021
3071
  log.verbose('PuppetServiceImpl', 'download()')
3022
3072