@juzi/wechaty-puppet-service 1.0.129 → 1.0.131

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 (98) hide show
  1. package/dist/cjs/src/auth/call-cred.d.ts +2 -2
  2. package/dist/cjs/src/auth/call-cred.d.ts.map +1 -1
  3. package/dist/cjs/src/auth/call-cred.js.map +1 -1
  4. package/dist/cjs/src/client/puppet-service.d.ts +27 -2
  5. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  6. package/dist/cjs/src/client/puppet-service.js +431 -124
  7. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  8. package/dist/cjs/src/client/puppet-service.spec.js +107 -0
  9. package/dist/cjs/src/client/puppet-service.spec.js.map +1 -1
  10. package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
  11. package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
  12. package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
  13. package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
  14. package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
  15. package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
  16. package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
  17. package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
  18. package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
  19. package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
  20. package/dist/cjs/src/client/room-member-load.spec.js +324 -0
  21. package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
  22. package/dist/cjs/src/package-json.js +4 -4
  23. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  24. package/dist/cjs/src/server/event-stream-manager.js +30 -0
  25. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  26. package/dist/cjs/src/server/event-stream-manager.spec.d.ts +3 -0
  27. package/dist/cjs/src/server/event-stream-manager.spec.d.ts.map +1 -0
  28. package/dist/cjs/src/server/event-stream-manager.spec.js +22 -0
  29. package/dist/cjs/src/server/event-stream-manager.spec.js.map +1 -0
  30. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  31. package/dist/cjs/src/server/puppet-implementation.js +135 -28
  32. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  33. package/dist/cjs/src/server/puppet-implementation.spec.js +98 -0
  34. package/dist/cjs/src/server/puppet-implementation.spec.js.map +1 -1
  35. package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
  36. package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
  37. package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
  38. package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
  39. package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
  40. package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
  41. package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
  42. package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
  43. package/dist/esm/src/auth/call-cred.d.ts +2 -2
  44. package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
  45. package/dist/esm/src/auth/call-cred.js.map +1 -1
  46. package/dist/esm/src/client/puppet-service.d.ts +27 -2
  47. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  48. package/dist/esm/src/client/puppet-service.js +432 -125
  49. package/dist/esm/src/client/puppet-service.js.map +1 -1
  50. package/dist/esm/src/client/puppet-service.spec.js +107 -0
  51. package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
  52. package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
  53. package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
  54. package/dist/esm/src/client/room-cache-coordinator.js +49 -0
  55. package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
  56. package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
  57. package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
  58. package/dist/esm/src/client/room-member-cache.spec.js +415 -0
  59. package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
  60. package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
  61. package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
  62. package/dist/esm/src/client/room-member-load.spec.js +296 -0
  63. package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
  64. package/dist/esm/src/package-json.js +4 -4
  65. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  66. package/dist/esm/src/server/event-stream-manager.js +30 -0
  67. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  68. package/dist/esm/src/server/event-stream-manager.spec.d.ts +3 -0
  69. package/dist/esm/src/server/event-stream-manager.spec.d.ts.map +1 -0
  70. package/dist/esm/src/server/event-stream-manager.spec.js +20 -0
  71. package/dist/esm/src/server/event-stream-manager.spec.js.map +1 -0
  72. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  73. package/dist/esm/src/server/puppet-implementation.js +136 -29
  74. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  75. package/dist/esm/src/server/puppet-implementation.spec.js +98 -0
  76. package/dist/esm/src/server/puppet-implementation.spec.js.map +1 -1
  77. package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
  78. package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
  79. package/dist/esm/src/utils/pb-payload-helper.js +169 -0
  80. package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
  81. package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
  82. package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
  83. package/dist/esm/src/utils/room-member-load.spec.js +58 -0
  84. package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
  85. package/package.json +4 -4
  86. package/src/auth/call-cred.ts +1 -1
  87. package/src/client/puppet-service.spec.ts +147 -0
  88. package/src/client/puppet-service.ts +378 -138
  89. package/src/client/room-cache-coordinator.ts +50 -0
  90. package/src/client/room-member-cache.spec.ts +448 -0
  91. package/src/client/room-member-load.spec.ts +319 -0
  92. package/src/package-json.ts +4 -4
  93. package/src/server/event-stream-manager.spec.ts +30 -0
  94. package/src/server/event-stream-manager.ts +29 -0
  95. package/src/server/puppet-implementation.spec.ts +127 -0
  96. package/src/server/puppet-implementation.ts +144 -29
  97. package/src/utils/pb-payload-helper.ts +150 -0
  98. package/src/utils/room-member-load.spec.ts +67 -0
@@ -20,6 +20,7 @@
20
20
  /* eslint-disable sort-keys */
21
21
  /* eslint-disable @typescript-eslint/no-misused-promises */
22
22
  import type { Writable } from 'stream'
23
+ import { inspect } from 'util'
23
24
  import {
24
25
  chunkDecoder,
25
26
  chunkEncoder,
@@ -43,12 +44,33 @@ import {
43
44
  import { log } from '../config.js'
44
45
  import { grpcError } from './grpc-error.js'
45
46
  import { EventStreamManager } from './event-stream-manager.js'
46
- import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPayloadToPb, channelPayloadToPb, channelPbToPayload, chatHistoryPayloadToPb, postPbToPayload, urlLinkPayloadToPb, channelCardPayloadToPb } from '../utils/pb-payload-helper.js'
47
+ import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPayloadToPb, channelPayloadToPb, channelPbToPayload, chatHistoryPayloadToPb, postPbToPayload, urlLinkPayloadToPb, channelCardPayloadToPb, roomPayloadToPb, roomMemberLoadStateToPb, roomMemberSnapshotToPb, validateRoomMemberLoadState, validateRoomMemberSnapshot, validateRoomMemberRevision } from '../utils/pb-payload-helper.js'
47
48
  import { grpcCallTypeToPuppetMedia, puppetCallMediaTypeToGrpc } from '../utils/call-media-mapping.js'
48
49
  import { TextContentType } from '@juzi/wechaty-puppet/types'
49
50
 
50
51
  type MessageBatchSendResponse = Awaited<ReturnType<PUPPET.impls.PuppetInterface['messageBatchSendText']>>
51
52
 
53
+ function roomMemberRpcError<Response> (
54
+ method: string, error: unknown, callback: grpc.sendUnaryData<Response>,
55
+ ): void {
56
+ log.error('PuppetServiceImpl', 'roomMemberRpcError() %s() rejection: %s', method, inspect(error))
57
+ if (typeof error === 'object' && error !== null && 'code' in error) {
58
+ const status = error as Record<string, unknown>
59
+ const code = status['code']
60
+ if (typeof code === 'number' && Number.isInteger(code) && code > 0 && code <= 16) {
61
+ const details = typeof status['details'] === 'string' ? status['details']
62
+ : error instanceof Error ? error.message : 'Room member RPC failed'
63
+ callback({ code, details }, null)
64
+ return
65
+ }
66
+ }
67
+ if (error instanceof Error && /unsupported/i.test(error.message)) {
68
+ callback({ code: grpc.status.UNIMPLEMENTED, details: error.message }, null)
69
+ return
70
+ }
71
+ grpcError(method, error, callback)
72
+ }
73
+
52
74
  type GrpcBatchResult = {
53
75
  setConversationId(value: string): void,
54
76
  setError(value: string): void,
@@ -1996,6 +2018,75 @@ function puppetImplementation (
1996
2018
  }
1997
2019
  },
1998
2020
 
2021
+ roomMemberSync: async (
2022
+ call: grpc.ServerUnaryCall<grpcPuppet.RoomMemberSyncRequest, grpcPuppet.RoomMemberLoadStateResponse>,
2023
+ callback: grpc.sendUnaryData<grpcPuppet.RoomMemberLoadStateResponse>,
2024
+ ): Promise<void> => {
2025
+ try {
2026
+ const roomId = call.request.getId()
2027
+ if (!roomId) {
2028
+ return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'roomId is required' }, null)
2029
+ }
2030
+ const options = call.request.getForce() ? { force: true } : undefined
2031
+ if (typeof puppet.roomMemberSync !== 'function') { throw new Error('roomMemberSync unsupported by adapter') }
2032
+ const state = await puppet.roomMemberSync(roomId, options)
2033
+ validateRoomMemberLoadState(state, roomId)
2034
+ return callback(null, roomMemberLoadStateToPb(state))
2035
+ } catch (error) {
2036
+ return roomMemberRpcError('roomMemberSync', error, callback)
2037
+ }
2038
+ },
2039
+
2040
+ roomMemberLoadState: async (
2041
+ call: grpc.ServerUnaryCall<grpcPuppet.RoomMemberLoadStateRequest, grpcPuppet.RoomMemberLoadStateResponse>,
2042
+ callback: grpc.sendUnaryData<grpcPuppet.RoomMemberLoadStateResponse>,
2043
+ ): Promise<void> => {
2044
+ try {
2045
+ const roomId = call.request.getId()
2046
+ if (!roomId) {
2047
+ return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'roomId is required' }, null)
2048
+ }
2049
+ if (typeof puppet.roomMemberLoadState !== 'function') { throw new Error('roomMemberLoadState unsupported by adapter') }
2050
+ const state = await puppet.roomMemberLoadState(roomId)
2051
+ validateRoomMemberLoadState(state, roomId)
2052
+ return callback(null, roomMemberLoadStateToPb(state))
2053
+ } catch (error) {
2054
+ return roomMemberRpcError('roomMemberLoadState', error, callback)
2055
+ }
2056
+ },
2057
+
2058
+ roomMemberSnapshot: async (
2059
+ call: grpc.ServerUnaryCall<grpcPuppet.RoomMemberSnapshotRequest, grpcPuppet.RoomMemberSnapshotResponse>,
2060
+ callback: grpc.sendUnaryData<grpcPuppet.RoomMemberSnapshotResponse>,
2061
+ ): Promise<void> => {
2062
+ try {
2063
+ const roomId = call.request.getId()
2064
+ const revision = call.request.getRevision()
2065
+ const pageSize = call.request.getPageSize()
2066
+ try {
2067
+ if (!roomId) { throw new Error('roomId is required') }
2068
+ validateRoomMemberRevision(revision)
2069
+ if (!Number.isInteger(pageSize) || pageSize < 0 || pageSize > 500) {
2070
+ throw new Error('pageSize must be an integer between 0 and 500')
2071
+ }
2072
+ } catch (error) {
2073
+ return callback({ code: grpc.status.INVALID_ARGUMENT, details: (error as Error).message }, null)
2074
+ }
2075
+ if (typeof puppet.roomMemberSnapshot !== 'function') { throw new Error('roomMemberSnapshot unsupported by adapter') }
2076
+ const snapshot = await puppet.roomMemberSnapshot(roomId, revision, {
2077
+ pageToken: call.request.getPageToken(), pageSize,
2078
+ })
2079
+ try {
2080
+ validateRoomMemberSnapshot(snapshot, roomId, revision)
2081
+ } catch (error) {
2082
+ return callback({ code: grpc.status.FAILED_PRECONDITION, details: (error as Error).message }, null)
2083
+ }
2084
+ return callback(null, roomMemberSnapshotToPb(snapshot))
2085
+ } catch (error) {
2086
+ return roomMemberRpcError('roomMemberSnapshot', error, callback)
2087
+ }
2088
+ },
2089
+
1999
2090
  roomMemberList: async (call, callback) => {
2000
2091
  log.verbose('PuppetServiceImpl', 'roomMemberList()')
2001
2092
 
@@ -2085,20 +2176,7 @@ function puppetImplementation (
2085
2176
 
2086
2177
  const payload = await puppet.roomPayload(roomId)
2087
2178
 
2088
- const response = new grpcPuppet.RoomPayloadResponse()
2089
- response.setAdminIdsList(payload.adminIdList)
2090
- response.setAvatar(payload.avatar || '')
2091
- response.setHandle(payload.handle || '')
2092
- response.setId(payload.id)
2093
- response.setMemberIdsList(payload.memberIdList)
2094
- response.setOwnerId(payload.ownerId || '')
2095
- response.setTopic(payload.topic)
2096
- response.setAdditionalInfo(payload.additionalInfo || '')
2097
- response.setRoomRemark(payload.remark || '')
2098
- response.setExternal(!!payload.external)
2099
- if (payload.createTime) {
2100
- response.setCreateTime(timestampFromMilliseconds(payload.createTime))
2101
- }
2179
+ const response = roomPayloadToPb(payload)
2102
2180
 
2103
2181
  return callback(null, response)
2104
2182
 
@@ -2119,20 +2197,7 @@ function puppetImplementation (
2119
2197
 
2120
2198
  const payloads: grpcPuppet.RoomPayloadResponse[] = []
2121
2199
  for (const [ _, payload ] of payloadMap.entries()) {
2122
- const pb = new grpcPuppet.RoomPayloadResponse()
2123
- pb.setAdminIdsList(payload.adminIdList)
2124
- pb.setAvatar(payload.avatar || '')
2125
- pb.setHandle(payload.handle || '')
2126
- pb.setId(payload.id)
2127
- pb.setMemberIdsList(payload.memberIdList)
2128
- pb.setOwnerId(payload.ownerId || '')
2129
- pb.setTopic(payload.topic)
2130
- pb.setAdditionalInfo(payload.additionalInfo || '')
2131
- pb.setRoomRemark(payload.remark || '')
2132
- pb.setExternal(!!payload.external)
2133
- if (payload.createTime) {
2134
- pb.setCreateTime(timestampFromMilliseconds(payload.createTime))
2135
- }
2200
+ const pb = roomPayloadToPb(payload)
2136
2201
  payloads.push(pb)
2137
2202
  }
2138
2203
 
@@ -3017,6 +3082,56 @@ function puppetImplementation (
3017
3082
  }
3018
3083
  },
3019
3084
 
3085
+ orgBroadcastPayload: async (call, callback) => {
3086
+ log.verbose('PuppetServiceImpl', 'orgBroadcastPayload()')
3087
+
3088
+ try {
3089
+ const orgBroadcastId = call.request.getId()
3090
+ const payload = await puppet.orgBroadcastPayload(orgBroadcastId)
3091
+
3092
+ const response = new grpcPuppet.OrgBroadcastPayloadResponse()
3093
+ response.setId(payload.id)
3094
+ response.setSendType(payload.sendType)
3095
+ response.setConversationType(payload.conversationType)
3096
+ response.setCreatorId(payload.creatorId ?? '')
3097
+ response.setExecTime(payload.execTime)
3098
+ response.setStatus(payload.status)
3099
+ response.setCanCancel(payload.canCancel)
3100
+ response.setAllowSelect(payload.allowSelect)
3101
+ response.setSent(payload.sent)
3102
+ response.setTotalCount(payload.totalCount)
3103
+ response.setSentCount(payload.sentCount)
3104
+ response.setContentListJson(payload.contentListJson)
3105
+ response.setTargetsList(payload.targets.map(target => {
3106
+ const targetPb = new grpcPuppet.OrgBroadcastTarget()
3107
+ targetPb.setContactId(target.contactId ?? '')
3108
+ targetPb.setRoomId(target.roomId ?? '')
3109
+ targetPb.setStatus(target.status as number as grpcPuppet.OrgBroadcastTargetStatusMap[keyof grpcPuppet.OrgBroadcastTargetStatusMap])
3110
+ return targetPb
3111
+ }))
3112
+
3113
+ return callback(null, response)
3114
+ } catch (e) {
3115
+ return grpcError('orgBroadcastPayload', e, callback)
3116
+ }
3117
+ },
3118
+
3119
+ orgBroadcastExecute: async (call, callback) => {
3120
+ log.verbose('PuppetServiceImpl', 'orgBroadcastExecute()')
3121
+
3122
+ try {
3123
+ const orgBroadcastId = call.request.getId()
3124
+ const targetIds = call.request.getTargetIdsList()
3125
+
3126
+ // an empty target_ids on the wire means all targets of the org broadcast
3127
+ await puppet.orgBroadcastExecute(orgBroadcastId, targetIds.length ? targetIds : undefined)
3128
+
3129
+ return callback(null, new grpcPuppet.OrgBroadcastExecuteResponse())
3130
+ } catch (e) {
3131
+ return grpcError('orgBroadcastExecute', e, callback)
3132
+ }
3133
+ },
3134
+
3020
3135
  download: async (call) => {
3021
3136
  log.verbose('PuppetServiceImpl', 'download()')
3022
3137
 
@@ -6,8 +6,91 @@ import {
6
6
  import * as PUPPET from '@juzi/wechaty-puppet'
7
7
  import type { FileBox, FileBoxInterface } from 'file-box'
8
8
 
9
+ import { millisecondsFromTimestamp, timestampFromMilliseconds } from '../pure-functions/timestamp.js'
10
+
9
11
  type grpcPuppet = typeof puppet
10
12
 
13
+ export function roomPbToPayload (pb: puppet.RoomPayloadResponse): PUPPET.payloads.Room {
14
+ const createTime = pb.getCreateTime()
15
+ const loadedAt = pb.getMemberLoadedAt()
16
+ return {
17
+ adminIdList: pb.getAdminIdsList(),
18
+ avatar: pb.getAvatar(),
19
+ handle: pb.getHandle(),
20
+ id: pb.getId(),
21
+ memberIdList: pb.getMemberIdsList(),
22
+ ownerId: pb.getOwnerId(),
23
+ topic: pb.getTopic(),
24
+ additionalInfo: pb.getAdditionalInfo(),
25
+ remark: pb.getRoomRemark(),
26
+ external: pb.getExternal(),
27
+ ...(createTime ? { createTime: millisecondsFromTimestamp(createTime) } : {}),
28
+ memberCount: pb.getMemberCount(),
29
+ memberLoadMode: pb.getMemberLoadMode(),
30
+ memberLoadStatus: pb.getMemberLoadStatus(),
31
+ memberLoadRevision: pb.getMemberLoadRevision(),
32
+ ...(loadedAt ? { memberLoadedAt: millisecondsFromTimestamp(loadedAt) } : {}),
33
+ }
34
+ }
35
+
36
+ export function roomPayloadToPb (payload: PUPPET.payloads.Room): puppet.RoomPayloadResponse {
37
+ const pb = new puppet.RoomPayloadResponse()
38
+ pb.setAdminIdsList(payload.adminIdList)
39
+ pb.setAvatar(payload.avatar || '')
40
+ pb.setHandle(payload.handle || '')
41
+ pb.setId(payload.id)
42
+ pb.setMemberIdsList(payload.memberIdList)
43
+ pb.setOwnerId(payload.ownerId || '')
44
+ pb.setTopic(payload.topic)
45
+ pb.setAdditionalInfo(payload.additionalInfo || '')
46
+ pb.setRoomRemark(payload.remark || '')
47
+ pb.setExternal(!!payload.external)
48
+ if (payload.createTime !== undefined) { pb.setCreateTime(timestampFromMilliseconds(payload.createTime)) }
49
+ if (payload.memberCount !== undefined) { pb.setMemberCount(payload.memberCount) }
50
+ if (payload.memberLoadMode !== undefined) { pb.setMemberLoadMode(payload.memberLoadMode) }
51
+ if (payload.memberLoadStatus !== undefined) { pb.setMemberLoadStatus(payload.memberLoadStatus) }
52
+ if (payload.memberLoadRevision !== undefined) { pb.setMemberLoadRevision(payload.memberLoadRevision) }
53
+ if (payload.memberLoadedAt !== undefined) { pb.setMemberLoadedAt(timestampFromMilliseconds(payload.memberLoadedAt)) }
54
+ return pb
55
+ }
56
+
57
+ export function validateRoomMemberRevision (revision: string): void {
58
+ if (typeof revision !== 'string' || !/^(0|[1-9][0-9]*)$/.test(revision)) {
59
+ throw new Error('Invalid room member revision')
60
+ }
61
+ }
62
+
63
+ export function validateRoomMemberLoadState (state: unknown, roomId?: string): asserts state is PUPPET.payloads.RoomMemberLoadState {
64
+ if (typeof state !== 'object' || state === null) { throw new Error('Invalid room member load state') }
65
+ const value = state as Record<string, unknown>
66
+ if (typeof value['roomId'] !== 'string' || !value['roomId'].trim()
67
+ || (roomId !== undefined && value['roomId'] !== roomId)) { throw new Error('Invalid room member roomId') }
68
+ if (typeof value['taskId'] !== 'string') { throw new Error('Invalid room member taskId') }
69
+ if (typeof value['revision'] !== 'string') { throw new Error('Invalid room member revision') }
70
+ validateRoomMemberRevision(value['revision'])
71
+ const status = value['status']
72
+ if (typeof status !== 'number' || !Number.isInteger(status) || status < 0 || status > 4) {
73
+ throw new Error('Invalid room member load status')
74
+ }
75
+ if (value['loadedAt'] !== undefined && (typeof value['loadedAt'] !== 'number' || !Number.isFinite(value['loadedAt']))) {
76
+ throw new Error('Invalid room member loadedAt')
77
+ }
78
+ if (value['error'] !== undefined && typeof value['error'] !== 'string') { throw new Error('Invalid room member error') }
79
+ }
80
+
81
+ export function validateRoomMemberSnapshot (
82
+ snapshot: PUPPET.payloads.RoomMemberSnapshot, roomId: string, revision: string,
83
+ ): void {
84
+ if (!roomId || snapshot.roomId !== roomId) { throw new Error('Room member snapshot roomId mismatch') }
85
+ validateRoomMemberRevision(snapshot.revision)
86
+ validateRoomMemberRevision(revision)
87
+ if (snapshot.revision !== revision) { throw new Error('Room member snapshot revision mismatch') }
88
+ if (!Array.isArray(snapshot.members)) { throw new Error('Invalid room member snapshot members') }
89
+ if (snapshot.loadedAt !== undefined && !Number.isFinite(snapshot.loadedAt)) {
90
+ throw new Error('Invalid room member snapshot loadedAt')
91
+ }
92
+ }
93
+
11
94
  export const miniProgramPayloadToPb = (grpcPuppet: grpcPuppet, miniProgramPayload: PUPPET.payloads.MiniProgram) => {
12
95
  const pbMiniProgramPayload = new grpcPuppet.MiniProgramPayload()
13
96
  if (miniProgramPayload.appid) { pbMiniProgramPayload.setAppid(miniProgramPayload.appid) }
@@ -440,6 +523,73 @@ export const roomMemberPbToPayload = (pb: puppet.RoomMemberPayloadResponse) => {
440
523
  return payload
441
524
  }
442
525
 
526
+ export function roomMemberPayloadToPb (payload: PUPPET.payloads.RoomMember): puppet.RoomMemberPayloadResponse {
527
+ const pb = new puppet.RoomMemberPayloadResponse()
528
+ pb.setAvatar(payload.avatar)
529
+ pb.setId(payload.id)
530
+ pb.setInviterId(payload.inviterId || '')
531
+ pb.setName(payload.name)
532
+ pb.setRoomAlias(payload.roomAlias || '')
533
+ pb.setAdditionalInfo(payload.additionalInfo || '')
534
+ pb.setJoinScene(payload.joinScene ?? PUPPET.types.RoomMemberJoinScene.Unknown)
535
+ pb.setJoinTime(payload.joinTime ?? 0)
536
+ return pb
537
+ }
538
+
539
+ export function roomMemberLoadStateToPb (state: PUPPET.payloads.RoomMemberLoadState): puppet.RoomMemberLoadStateResponse {
540
+ validateRoomMemberLoadState(state, state.roomId)
541
+ const pb = new puppet.RoomMemberLoadStateResponse()
542
+ pb.setId(state.roomId)
543
+ pb.setTaskId(state.taskId)
544
+ pb.setStatus(state.status)
545
+ pb.setRevision(state.revision)
546
+ if (state.loadedAt !== undefined) { pb.setLoadedAt(timestampFromMilliseconds(state.loadedAt)) }
547
+ if (state.error !== undefined) { pb.setError(state.error) }
548
+ return pb
549
+ }
550
+
551
+ export function roomMemberLoadStatePbToPayload (
552
+ pb: puppet.RoomMemberLoadStateResponse, roomId: string,
553
+ ): PUPPET.payloads.RoomMemberLoadState {
554
+ const loadedAt = pb.getLoadedAt()
555
+ const state: PUPPET.payloads.RoomMemberLoadState = {
556
+ roomId: pb.getId(),
557
+ taskId: pb.getTaskId(),
558
+ status: pb.getStatus(),
559
+ revision: pb.getRevision(),
560
+ ...(loadedAt ? { loadedAt: millisecondsFromTimestamp(loadedAt) } : {}),
561
+ ...(pb.getError() ? { error: pb.getError() } : {}),
562
+ }
563
+ validateRoomMemberLoadState(state, roomId)
564
+ return state
565
+ }
566
+
567
+ export function roomMemberSnapshotToPb (snapshot: PUPPET.payloads.RoomMemberSnapshot): puppet.RoomMemberSnapshotResponse {
568
+ validateRoomMemberSnapshot(snapshot, snapshot.roomId, snapshot.revision)
569
+ const pb = new puppet.RoomMemberSnapshotResponse()
570
+ pb.setId(snapshot.roomId)
571
+ pb.setRevision(snapshot.revision)
572
+ pb.setMembersList(snapshot.members.map(roomMemberPayloadToPb))
573
+ if (snapshot.loadedAt !== undefined) { pb.setLoadedAt(timestampFromMilliseconds(snapshot.loadedAt)) }
574
+ if (snapshot.nextPageToken !== undefined) { pb.setNextPageToken(snapshot.nextPageToken) }
575
+ return pb
576
+ }
577
+
578
+ export function roomMemberSnapshotPbToPayload (
579
+ pb: puppet.RoomMemberSnapshotResponse, roomId: string, revision: string,
580
+ ): PUPPET.payloads.RoomMemberSnapshot {
581
+ const loadedAt = pb.getLoadedAt()
582
+ const snapshot: PUPPET.payloads.RoomMemberSnapshot = {
583
+ roomId: pb.getId(),
584
+ revision: pb.getRevision(),
585
+ members: pb.getMembersList().map(roomMemberPbToPayload),
586
+ ...(loadedAt ? { loadedAt: millisecondsFromTimestamp(loadedAt) } : {}),
587
+ ...(pb.getNextPageToken() ? { nextPageToken: pb.getNextPageToken() } : {}),
588
+ }
589
+ validateRoomMemberSnapshot(snapshot, roomId, revision)
590
+ return snapshot
591
+ }
592
+
443
593
  export const contactPbToPayload = (pb: puppet.ContactPayloadResponse) => {
444
594
  const payload: PUPPET.payloads.Contact = {
445
595
  address : pb.getAddress(),
@@ -0,0 +1,67 @@
1
+ import { test } from 'tstest'
2
+ import * as PUPPET from '@juzi/wechaty-puppet'
3
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
4
+
5
+ import {
6
+ roomPayloadToPb,
7
+ roomPbToPayload,
8
+ roomMemberLoadStateToPb,
9
+ roomMemberLoadStatePbToPayload,
10
+ validateRoomMemberSnapshot,
11
+ } from './pb-payload-helper.js'
12
+
13
+ test('room payload roundtrip retains lazy fields and timestamp zero', async t => {
14
+ const payload: PUPPET.payloads.Room = {
15
+ id: 'room',
16
+ topic: 'topic',
17
+ adminIdList: [],
18
+ memberIdList: [],
19
+ memberCount: 0,
20
+ memberLoadMode: PUPPET.types.RoomMemberLoadMode.Lazy,
21
+ memberLoadStatus: PUPPET.types.RoomMemberLoadStatus.Loaded,
22
+ memberLoadRevision: '9007199254740993',
23
+ memberLoadedAt: 0,
24
+ }
25
+ const result = roomPbToPayload(roomPayloadToPb(payload))
26
+ t.equal(result.memberCount, 0)
27
+ t.equal(result.memberLoadMode, payload.memberLoadMode)
28
+ t.equal(result.memberLoadStatus, payload.memberLoadStatus)
29
+ t.equal(result.memberLoadRevision, payload.memberLoadRevision)
30
+ t.equal(result.memberLoadedAt, 0)
31
+ const legacy = roomPbToPayload(new grpcPuppet.RoomPayloadResponse())
32
+ t.equal(legacy.memberLoadMode, PUPPET.types.RoomMemberLoadMode.Unspecified)
33
+ t.equal(legacy.memberLoadedAt, undefined)
34
+ })
35
+
36
+ test('load state preserves exact revision and rejects invalid responses', async t => {
37
+ const state: PUPPET.payloads.RoomMemberLoadState = {
38
+ roomId: 'room',
39
+ taskId: 'task',
40
+ status: PUPPET.types.RoomMemberLoadStatus.Pending,
41
+ revision: '9007199254740993',
42
+ loadedAt: 0,
43
+ }
44
+ t.same(roomMemberLoadStatePbToPayload(roomMemberLoadStateToPb(state), 'room'), state)
45
+ t.throws(() => roomMemberLoadStatePbToPayload(roomMemberLoadStateToPb(state), 'other'), /roomId/)
46
+ t.throws(() => roomMemberLoadStateToPb({ ...state, revision: '01' }), /revision/)
47
+ t.throws(() => roomMemberLoadStateToPb({ ...state, status: 99 }), /status/)
48
+ const response = roomMemberLoadStateToPb(state)
49
+ response.setStatus(99 as Parameters<typeof response.setStatus>[0])
50
+ t.throws(() => roomMemberLoadStatePbToPayload(response, 'room'), /status/)
51
+ response.setStatus(state.status)
52
+ response.setRevision('01')
53
+ t.throws(() => roomMemberLoadStatePbToPayload(response, 'room'), /revision/)
54
+ const failed: PUPPET.payloads.RoomMemberLoadState = {
55
+ ...state, status: PUPPET.types.RoomMemberLoadStatus.Failed, error: 'load failed',
56
+ }
57
+ t.same(roomMemberLoadStatePbToPayload(roomMemberLoadStateToPb(failed), 'room'), failed)
58
+ })
59
+
60
+ test('snapshot must match both room and requested revision', async t => {
61
+ const snapshot: PUPPET.payloads.RoomMemberSnapshot = {
62
+ roomId: 'room', revision: '2', members: [], nextPageToken: 'next',
63
+ }
64
+ t.doesNotThrow(() => validateRoomMemberSnapshot(snapshot, 'room', '2'))
65
+ t.throws(() => validateRoomMemberSnapshot(snapshot, 'room', '1'), /revision/)
66
+ t.throws(() => validateRoomMemberSnapshot(snapshot, 'other', '2'), /roomId/)
67
+ })