UNPKG

wechaty-puppet-service

Version:
1,164 lines 57.4 kB
/** * Wechaty Open Source Software - https://github.com/wechaty * * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and * Wechaty Contributors <https://github.com/wechaty>. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import util from 'util'; import * as PUPPET from 'wechaty-puppet'; import { StringValue, puppet as grpcPuppet, } from 'wechaty-grpc'; import { puppet$, Duck as PuppetDuck, } from 'wechaty-redux'; import { Ducks, // Bundle, } from 'ducks'; // import type { Subscription } from 'rxjs' /** * Deprecated. Will be removed after Dec 31, 2022 */ import { packConversationIdFileBoxToPb, unpackFileBoxFromPb, } from '../deprecated/mod.js'; import { serializeFileBox } from '../deprecated/serialize-file-box.js'; import { millisecondsFromTimestamp } from '../pure-functions/timestamp.js'; import { uuidifyFileBoxGrpc, normalizeFileBoxUuid, } from '../file-box-helper/mod.js'; import { envVars, log, VERSION, } from '../config.js'; import { EventTypeRev, } from '../event-type-rev.js'; import { packageJson } from '../package-json.js'; import { GrpcManager } from './grpc-manager.js'; import { PayloadStore } from './payload-store.js'; class PuppetService extends PUPPET.Puppet { options; static VERSION = VERSION; _cleanupCallbackList; _payloadStore; /** * Wechaty Redux */ _ducks; _store; _grpcManager; get grpcManager() { if (!this._grpcManager) { throw new Error('no grpc manager'); } return this._grpcManager; } /** * UUIDify: * We need to clone a FileBox * to set uuid loader/saver with this grpc client */ FileBoxUuid; constructor(options = {}) { super(options); this.options = options; this._payloadStore = new PayloadStore({ token: envVars.WECHATY_PUPPET_SERVICE_TOKEN(this.options.token), }); this.hookPayloadStore(); this._ducks = new Ducks({ puppet: PuppetDuck }); this._store = this._ducks.configureStore(); this._cleanupCallbackList = []; this.FileBoxUuid = uuidifyFileBoxGrpc(() => this.grpcManager.client); } async serializeFileBox(fileBox) { /** * 1. if the fileBox is one of type `Url`, `QRCode`, `Uuid`, etc, * then it can be serialized by `fileBox.toString()` * 2. if the fileBox is one of type `Stream`, `Buffer`, `File`, etc, * then it need to be convert to type `Uuid` * before serialized by `fileBox.toString()` */ const normalizedFileBox = await normalizeFileBoxUuid(this.FileBoxUuid)(fileBox); return JSON.stringify(normalizedFileBox); } name() { return packageJson.name || 'wechaty-puppet-service'; } version() { return packageJson.version || '0.0.0'; } async onStart() { log.verbose('PuppetService', 'onStart()'); if (this._grpcManager) { log.warn('PuppetService', 'onStart() found this.grpc is already existed. dropped.'); this._grpcManager = undefined; } log.verbose('PuppetService', 'start() instanciating GrpcManager ...'); const grpcManager = new GrpcManager(this.options); log.verbose('PuppetService', 'start() instanciating GrpcManager ... done'); /** * Huan(202108): when we started the event stream, * the `this.grpc` need to be available for all listeners. */ this._grpcManager = grpcManager; log.verbose('PuppetService', 'start() setting up bridge grpc event stream ...'); this.bridgeGrpcEventStream(grpcManager); log.verbose('PuppetService', 'start() setting up bridge grpc event stream ... done'); log.verbose('PuppetService', 'start() starting grpc manager...'); await grpcManager.start(); log.verbose('PuppetService', 'start() starting grpc manager... done'); /** * Ducks management */ const subscription = puppet$(this) .subscribe(this._store.dispatch); this._cleanupCallbackList.push(() => subscription.unsubscribe()); log.verbose('PuppetService', 'onStart() ... done'); } async onStop() { log.verbose('PuppetService', 'onStop()'); this._cleanupCallbackList.map(setImmediate); this._cleanupCallbackList.length = 0; if (this._grpcManager) { log.verbose('PuppetService', 'onStop() stopping grpc manager ...'); const grpcManager = this._grpcManager; this._grpcManager = undefined; await grpcManager.stop(); log.verbose('PuppetService', 'onStop() stopping grpc manager ... done'); } log.verbose('PuppetService', 'onStop() ... done'); } hookPayloadStore() { log.verbose('PuppetService', 'hookPayloadStore()'); this.on('login', async ({ contactId }) => { try { log.verbose('PuppetService', 'hookPayloadStore() this.on(login) contactId: "%s"', contactId); await this._payloadStore.start(contactId); } catch (e) { log.verbose('PuppetService', 'hookPayloadStore() this.on(login) rejection "%s"', e.message); } }); this.on('logout', async ({ contactId }) => { log.verbose('PuppetService', 'hookPayloadStore() this.on(logout) contactId: "%s"', contactId); try { await this._payloadStore.stop(); } catch (e) { log.verbose('PuppetService', 'hookPayloadStore() this.on(logout) rejection "%s"', e.message); } }); } bridgeGrpcEventStream(client) { log.verbose('PuppetService', 'bridgeGrpcEventStream(client)'); client .on('data', this.onGrpcStreamEvent.bind(this)) .on('end', () => { log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(end)'); }) .on('error', (e) => { this.emit('error', e); // https://github.com/wechaty/wechaty-puppet-service/issues/16 // log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(error) %s', e) // const reason = 'bridgeGrpcEventStream() eventStream.on(error) ' + e /** * Huan(202110): simple reset puppet when grpc client has error? (or not?) */ // this.wrapAsync(this.reset()) // /** // * The `Puppet` class have a throttleQueue for receiving the `reset` events // * and it's the `Puppet` class's duty for call the `puppet.reset()` to reset the puppet. // */ // if (this.state.on()) { // this.emit('reset', { data: reason }) // } }) .on('cancel', (...args) => { log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(cancel), %s', JSON.stringify(args)); }); } onGrpcStreamEvent(event) { const type = event.getType(); const payload = event.getPayload(); log.verbose('PuppetService', 'onGrpcStreamEvent({type:%s(%s), payload(len:%s)})', EventTypeRev[type], type, payload.length); log.silly('PuppetService', 'onGrpcStreamEvent({type:%s(%s), payload:"%s"})', EventTypeRev[type], type, payload); if (type !== grpcPuppet.EventType.EVENT_TYPE_HEARTBEAT) { this.emit('heartbeat', { data: `onGrpcStreamEvent(${EventTypeRev[type]})`, }); } switch (type) { case grpcPuppet.EventType.EVENT_TYPE_DONG: this.emit('dong', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_ERROR: this.emit('error', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_HEARTBEAT: this.emit('heartbeat', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_FRIENDSHIP: this.emit('friendship', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_LOGIN: { const loginPayload = JSON.parse(payload); (async () => this.login(loginPayload.contactId))().catch(e => log.error('PuppetService', 'onGrpcStreamEvent() this.login() rejection %s', e.message)); } break; case grpcPuppet.EventType.EVENT_TYPE_LOGOUT: { const logoutPayload = JSON.parse(payload); (async () => this.logout(logoutPayload.data))().catch(e => log.error('PuppetService', 'onGrpcStreamEvent() this.logout() rejection %s', e.message)); } break; case grpcPuppet.EventType.EVENT_TYPE_DIRTY: this.emit('dirty', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_MESSAGE: this.emit('message', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_POST: this.emit('post', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_READY: this.emit('ready', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_ROOM_INVITE: this.emit('room-invite', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_ROOM_JOIN: this.emit('room-join', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_ROOM_LEAVE: this.emit('room-leave', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_ROOM_TOPIC: this.emit('room-topic', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_SCAN: this.emit('scan', JSON.parse(payload)); break; case grpcPuppet.EventType.EVENT_TYPE_RESET: log.warn('PuppetService', 'onGrpcStreamEvent() got an EventType.EVENT_TYPE_RESET ?'); // the `reset` event should be dealed not send out break; case grpcPuppet.EventType.EVENT_TYPE_UNSPECIFIED: log.error('PuppetService', 'onGrpcStreamEvent() got an EventType.EVENT_TYPE_UNSPECIFIED ?'); break; default: // Huan(202003): in default, the `type` type should be `never`, please check. throw new Error('eventType ' + type + ' unsupported! (code should not reach here)'); } } async logout(reason) { log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : ''); await super.logout(reason); try { await util.promisify(this.grpcManager.client.logout .bind(this.grpcManager.client))(new grpcPuppet.LogoutRequest()); } catch (e) { log.silly('PuppetService', 'logout() no grpc client'); } } ding(data) { log.silly('PuppetService', 'ding(%s)', data); const request = new grpcPuppet.DingRequest(); request.setData(data || ''); this.grpcManager.client.ding(request, (error, _response) => { if (error) { log.error('PuppetService', 'ding() rejection: %s', error); } }); } /** * * Huan(202111) Issue #158 - Refactoring the 'dirty' event, dirtyPayload(), * and XXXPayloadDirty() methods logic & spec * * @see https://github.com/wechaty/puppet/issues/158 * */ async dirtyPayload(type, id) { log.verbose('PuppetService', 'dirtyPayload(%s, %s)', type, id); const request = new grpcPuppet.DirtyPayloadRequest(); request.setId(id); request.setType(type); try { await util.promisify(this.grpcManager.client.dirtyPayload .bind(this.grpcManager.client))(request); } catch (e) { log.error('PuppetService', 'dirtyPayload() rejection: %s', e && e.message); throw e; } } /** * `onDirty()` is called when the puppet emit `dirty` event. * the event listener will be registered in `start()` from the `PuppetAbstract` class */ onDirty({ payloadType, payloadId, }) { log.verbose('PuppetService', 'onDirty(%s<%s>, %s)', PUPPET.types.Dirty[payloadType], payloadType, payloadId); const dirtyMap = { [PUPPET.types.Dirty.Contact]: async (id) => this._payloadStore.contact?.delete(id), [PUPPET.types.Dirty.Friendship]: async (_) => { }, [PUPPET.types.Dirty.Message]: async (_) => { }, [PUPPET.types.Dirty.Post]: async (_) => { }, [PUPPET.types.Dirty.Room]: async (id) => this._payloadStore.room?.delete(id), [PUPPET.types.Dirty.RoomMember]: async (id) => this._payloadStore.roomMember?.delete(id), [PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); }, }; const dirtyFuncSync = this.wrapAsync(dirtyMap[payloadType]); dirtyFuncSync(payloadId); /** * We need to call `super.onDirty()` to clean the `PuppetAbstract` LRUCache */ super.onDirty({ payloadId, payloadType }); } async contactAlias(contactId, alias) { log.verbose('PuppetService', 'contactAlias(%s, %s)', contactId, alias); /** * Get alias */ if (typeof alias === 'undefined') { const request = new grpcPuppet.ContactAliasRequest(); request.setId(contactId); const response = await util.promisify(this.grpcManager.client.contactAlias .bind(this.grpcManager.client))(request); const result = response.getAlias(); if (result) { return result; } { // DEPRECATED, will be removed after Dec 31, 2022 const aliasWrapper = response.getAliasStringValueDeprecated(); if (!aliasWrapper) { throw new Error('can not get aliasWrapper'); } return aliasWrapper.getValue(); } } /** * Set alias */ const request = new grpcPuppet.ContactAliasRequest(); request.setId(contactId); request.setAlias(alias || ''); // null -> '', in server, we treat '' as null { // DEPRECATED, will be removed after Dec 31, 2022 const aliasWrapper = new StringValue(); aliasWrapper.setValue(alias || ''); // null -> '', in server, we treat '' as null request.setAliasStringValueDeprecated(aliasWrapper); } await util.promisify(this.grpcManager.client.contactAlias .bind(this.grpcManager.client))(request); } async contactPhone(contactId, phoneList) { log.verbose('PuppetService', 'contactPhone(%s, %s)', contactId, phoneList); const request = new grpcPuppet.ContactPhoneRequest(); request.setContactId(contactId); request.setPhonesList(phoneList); await util.promisify(this.grpcManager.client.contactPhone .bind(this.grpcManager.client))(request); } async contactCorporationRemark(contactId, corporationRemark) { log.verbose('PuppetService', 'contactCorporationRemark(%s, %s)', contactId, corporationRemark); const request = new grpcPuppet.ContactCorporationRemarkRequest(); request.setContactId(contactId); if (corporationRemark) { request.setCorporationRemark(corporationRemark); } { // DEPRECATED, will be removed after Dec 31, 2022 const corporationRemarkWrapper = new StringValue(); if (corporationRemark) { corporationRemarkWrapper.setValue(corporationRemark); request.setCorporationRemarkStringValueDeprecated(corporationRemarkWrapper); } } await util.promisify(this.grpcManager.client.contactCorporationRemark .bind(this.grpcManager.client))(request); } async contactDescription(contactId, description) { log.verbose('PuppetService', 'contactDescription(%s, %s)', contactId, description); const request = new grpcPuppet.ContactDescriptionRequest(); request.setContactId(contactId); if (description) { request.setDescription(description); } { // DEPRECATED, will be removed after Dec 31, 2022 const descriptionWrapper = new StringValue(); if (description) { descriptionWrapper.setValue(description); request.setDescriptionStringValueDeprecated(descriptionWrapper); } } await util.promisify(this.grpcManager.client.contactDescription .bind(this.grpcManager.client))(request); } async contactList() { log.verbose('PuppetService', 'contactList()'); const response = await util.promisify(this.grpcManager.client.contactList .bind(this.grpcManager.client))(new grpcPuppet.ContactListRequest()); return response.getIdsList(); } async contactAvatar(contactId, fileBox) { log.verbose('PuppetService', 'contactAvatar(%s)', contactId); /** * 1. set */ if (fileBox) { const request = new grpcPuppet.ContactAvatarRequest(); request.setId(contactId); const serializedFileBox = await this.serializeFileBox(fileBox); request.setFileBox(serializedFileBox); { // DEPRECATED, will be removed after Dec 31, 2022 const fileboxWrapper = new StringValue(); fileboxWrapper.setValue(await serializeFileBox(fileBox)); request.setFileboxStringValueDeprecated(fileboxWrapper); } await util.promisify(this.grpcManager.client.contactAvatar .bind(this.grpcManager.client))(request); return; } /** * 2. get */ const request = new grpcPuppet.ContactAvatarRequest(); request.setId(contactId); const response = await util.promisify(this.grpcManager.client.contactAvatar .bind(this.grpcManager.client))(request); let jsonText; jsonText = response.getFileBox(); { // DEPRECATED, will be removed after Dec 31, 2022 const deprecated = true; void deprecated; if (!jsonText) { const textWrapper = response.getFileboxStringValueDeprecated(); if (!textWrapper) { throw new Error('can not get textWrapper'); } jsonText = textWrapper.getValue(); } } return this.FileBoxUuid.fromJSON(jsonText); } async contactRawPayload(id) { log.verbose('PuppetService', 'contactRawPayload(%s)', id); const cachedPayload = await this._payloadStore.contact?.get(id); if (cachedPayload) { log.silly('PuppetService', 'contactRawPayload(%s) cache HIT', id); return cachedPayload; } const request = new grpcPuppet.ContactPayloadRequest(); request.setId(id); const response = await util.promisify(this.grpcManager.client.contactPayload .bind(this.grpcManager.client))(request); const payload = { address: response.getAddress(), alias: response.getAlias(), avatar: response.getAvatar(), city: response.getCity(), corporation: response.getCorporation(), coworker: response.getCoworker(), description: response.getDescription(), friend: response.getFriend(), gender: response.getGender(), id: response.getId(), name: response.getName(), phone: response.getPhonesList(), province: response.getProvince(), signature: response.getSignature(), star: response.getStar(), title: response.getTitle(), type: response.getType(), weixin: response.getWeixin(), }; await this._payloadStore.contact?.set(id, payload); log.silly('PuppetService', 'contactRawPayload(%s) cache SET', id); return payload; } async contactRawPayloadParser(payload) { // log.silly('PuppetService', 'contactRawPayloadParser({id:%s})', payload.id) // passthrough return payload; } async contactSelfName(name) { log.verbose('PuppetService', 'contactSelfName(%s)', name); const request = new grpcPuppet.ContactSelfNameRequest(); request.setName(name); await util.promisify(this.grpcManager.client.contactSelfName .bind(this.grpcManager.client))(request); } async contactSelfQRCode() { log.verbose('PuppetService', 'contactSelfQRCode()'); const response = await util.promisify(this.grpcManager.client.contactSelfQRCode .bind(this.grpcManager.client))(new grpcPuppet.ContactSelfQRCodeRequest()); return response.getQrcode(); } async contactSelfSignature(signature) { log.verbose('PuppetService', 'contactSelfSignature(%s)', signature); const request = new grpcPuppet.ContactSelfSignatureRequest(); request.setSignature(signature); await util.promisify(this.grpcManager.client.contactSelfSignature .bind(this.grpcManager.client))(request); } /** * * Conversation * */ conversationReadMark(conversationId, hasRead = true) { log.verbose('PuppetService', 'conversationMarkRead(%s, %s)', conversationId, hasRead); return PUPPET.throwUnsupportedError('not implemented. See https://github.com/wechaty/wechaty-puppet/pull/132'); } /** * * Message * */ async messageMiniProgram(messageId) { log.verbose('PuppetService', 'messageMiniProgram(%s)', messageId); const request = new grpcPuppet.MessageMiniProgramRequest(); request.setId(messageId); const response = await util.promisify(this.grpcManager.client.messageMiniProgram .bind(this.grpcManager.client))(request); let miniProgramPayload = response.getMiniProgram()?.toObject(); if (!miniProgramPayload) { /** * Deprecated: will be removed after Dec 22, 2022 */ const jsonText = response.getMiniProgramDeprecated(); miniProgramPayload = JSON.parse(jsonText); } const payload = { ...miniProgramPayload, }; return payload; } async messageLocation(messageId) { log.verbose('PuppetService', 'messageLocation(%s)', messageId); const request = new grpcPuppet.MessageLocationRequest(); request.setId(messageId); const response = await util.promisify(this.grpcManager.client.messageLocation .bind(this.grpcManager.client))(request); const locationPayload = response.getLocation(); const payload = { accuracy: 0, address: 'NOADDRESS', latitude: 0, longitude: 0, name: 'NONAME', ...locationPayload, }; return payload; } async messageImage(messageId, imageType) { log.verbose('PuppetService', 'messageImage(%s, %s[%s])', messageId, imageType, PUPPET.types.Image[imageType]); try { const request = new grpcPuppet.MessageImageRequest(); request.setId(messageId); request.setType(imageType); const response = await util.promisify(this.grpcManager.client.messageImage .bind(this.grpcManager.client))(request); const jsonText = response.getFileBox(); if (jsonText) { return this.FileBoxUuid.fromJSON(jsonText); } } catch (e) { log.verbose('PuppetService', 'messageImage() rejection %s', e.message); } { // Deprecated. Will be removed after Dec 31, 2022 const request = new grpcPuppet.MessageImageStreamRequest(); request.setId(messageId); request.setType(imageType); const pbStream = this.grpcManager.client.messageImageStream(request); const fileBox = await unpackFileBoxFromPb(pbStream); // const fileBoxChunkStream = unpackFileBoxChunk(stream) // return unpackFileBox(fileBoxChunkStream) return fileBox; } } async messageContact(messageId) { log.verbose('PuppetService', 'messageContact(%s)', messageId); const request = new grpcPuppet.MessageContactRequest(); request.setId(messageId); const response = await util.promisify(this.grpcManager.client.messageContact .bind(this.grpcManager.client))(request); const contactId = response.getId(); return contactId; } async messageSendMiniProgram(conversationId, miniProgramPayload) { log.verbose('PuppetService', 'messageSendMiniProgram(%s, "%s")', conversationId, JSON.stringify(miniProgramPayload)); const request = new grpcPuppet.MessageSendMiniProgramRequest(); request.setConversationId(conversationId); const pbMiniProgramPayload = new grpcPuppet.MiniProgramPayload(); if (miniProgramPayload.appid) { pbMiniProgramPayload.setAppid(miniProgramPayload.appid); } if (miniProgramPayload.description) { pbMiniProgramPayload.setDescription(miniProgramPayload.description); } if (miniProgramPayload.iconUrl) { pbMiniProgramPayload.setIconUrl(miniProgramPayload.iconUrl); } if (miniProgramPayload.pagePath) { pbMiniProgramPayload.setPagePath(miniProgramPayload.pagePath); } if (miniProgramPayload.shareId) { pbMiniProgramPayload.setShareId(miniProgramPayload.shareId); } if (miniProgramPayload.thumbKey) { pbMiniProgramPayload.setThumbKey(miniProgramPayload.thumbKey); } if (miniProgramPayload.thumbUrl) { pbMiniProgramPayload.setThumbUrl(miniProgramPayload.thumbUrl); } if (miniProgramPayload.title) { pbMiniProgramPayload.setTitle(miniProgramPayload.title); } if (miniProgramPayload.username) { pbMiniProgramPayload.setUsername(miniProgramPayload.username); } request.setMiniProgram(pbMiniProgramPayload); /** * Deprecated: will be removed after Dec 31, 2022 */ request.setMiniProgramDeprecated(JSON.stringify(miniProgramPayload)); const response = await util.promisify(this.grpcManager.client.messageSendMiniProgram .bind(this.grpcManager.client))(request); const messageId = response.getId(); if (messageId) { return messageId; } { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } } async messageSendLocation(conversationId, locationPayload) { log.verbose('PuppetService', 'messageSendLocation(%s)', conversationId, JSON.stringify(locationPayload)); const request = new grpcPuppet.MessageSendLocationRequest(); request.setConversationId(conversationId); const pbLocationPayload = new grpcPuppet.LocationPayload(); pbLocationPayload.setAccuracy(locationPayload.accuracy); pbLocationPayload.setAddress(locationPayload.address); pbLocationPayload.setLatitude(locationPayload.latitude); pbLocationPayload.setLongitude(locationPayload.longitude); pbLocationPayload.setName(locationPayload.name); request.setLocation(pbLocationPayload); const response = await util.promisify(this.grpcManager.client.messageSendLocation .bind(this.grpcManager.client))(request); const id = response.getId(); if (id) { return id; } } async messageRecall(messageId) { log.verbose('PuppetService', 'messageRecall(%s)', messageId); const request = new grpcPuppet.MessageRecallRequest(); request.setId(messageId); const response = await util.promisify(this.grpcManager.client.messageRecall .bind(this.grpcManager.client))(request); return response.getSuccess(); } async messageFile(id) { log.verbose('PuppetService', 'messageFile(%s)', id); try { const request = new grpcPuppet.MessageFileRequest(); request.setId(id); const response = await util.promisify(this.grpcManager.client.messageFile .bind(this.grpcManager.client))(request); const jsonText = response.getFileBox(); if (jsonText) { return this.FileBoxUuid.fromJSON(jsonText); } } catch (e) { log.warn('PuppetService', 'messageFile() rejection: %s', e.message); log.warn('PuppetService', [ 'This might because you are using Wechaty v1.x with a Puppet Service v0.x', 'Contact your Wechaty Puppet Service provided to report this problem', 'Related issues:', ' - https://github.com/wechaty/puppet-service/issues/179', ' - https://github.com/wechaty/puppet-service/pull/170', ].join('\n')); } { // Deprecated. `MessageFileStream` Will be removed after Dec 31, 2022 const request = new grpcPuppet.MessageFileStreamRequest(); request.setId(id); const pbStream = this.grpcManager.client.messageFileStream(request); // const fileBoxChunkStream = unpackFileBoxChunk(pbStream) // return unpackFileBox(fileBoxChunkStream) const fileBox = await unpackFileBoxFromPb(pbStream); return fileBox; } } async messageForward(conversationId, messageId) { log.verbose('PuppetService', 'messageForward(%s, %s)', conversationId, messageId); const request = new grpcPuppet.MessageForwardRequest(); request.setConversationId(conversationId); request.setMessageId(messageId); const response = await util.promisify(this.grpcManager.client.messageForward .bind(this.grpcManager.client))(request); const forwardedMessageId = response.getId(); if (forwardedMessageId) { return forwardedMessageId; } { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } } async messageRawPayload(id) { log.verbose('PuppetService', 'messageRawPayload(%s)', id); // const cachedPayload = await this.payloadStore.message?.get(id) // if (cachedPayload) { // log.silly('PuppetService', 'messageRawPayload(%s) cache HIT', id) // return cachedPayload // } const request = new grpcPuppet.MessagePayloadRequest(); request.setId(id); const response = await util.promisify(this.grpcManager.client.messagePayload .bind(this.grpcManager.client))(request); let timestamp; const receiveTime = response.getReceiveTime(); if (receiveTime) { timestamp = millisecondsFromTimestamp(receiveTime); } else { // Deprecated: will be removed after Dec 31, 2022 timestamp = response.getTimestampDeprecated(); } const payload = { filename: response.getFilename(), id: response.getId(), listenerId: response.getListenerId(), mentionIdList: response.getMentionIdsList(), roomId: response.getRoomId(), talkerId: response.getTalkerId(), text: response.getText(), timestamp, type: response.getType(), }; // log.silly('PuppetService', 'messageRawPayload(%s) cache SET', id) // await this.payloadStore.message?.set(id, payload) return payload; } async messageRawPayloadParser(payload) { // log.silly('PuppetService', 'messagePayload({id:%s})', payload.id) // passthrough return payload; } async messageSendText(conversationId, text, mentionIdList) { log.verbose('PuppetService', 'messageSend(%s, %s)', conversationId, text); const request = new grpcPuppet.MessageSendTextRequest(); request.setConversationId(conversationId); request.setText(text); if (typeof mentionIdList !== 'undefined') { request.setMentionalIdsList(mentionIdList); } const response = await util.promisify(this.grpcManager.client.messageSendText .bind(this.grpcManager.client))(request); const messageId = response.getId(); if (messageId) { return messageId; } { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } } async messageSendFile(conversationId, fileBox) { log.verbose('PuppetService', 'messageSendFile(%s, %s)', conversationId, fileBox); try { const request = new grpcPuppet.MessageSendFileRequest(); request.setConversationId(conversationId); const serializedFileBox = await this.serializeFileBox(fileBox); request.setFileBox(serializedFileBox); const response = await util.promisify(this.grpcManager.client.messageSendFile .bind(this.grpcManager.client))(request); const messageId = response.getId(); if (messageId) { return messageId; } else { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } return; // void } catch (e) { log.verbose('PuppetService', 'messageSendFile() rejection: %s', e.message); } /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 * The old server will not support `Upload` gRPC method, * which I'm expecting the above code will throw a exception, * then the below code will be executed. */ return this.messageSendFileStream(conversationId, fileBox); } async messageSendContact(conversationId, contactId) { log.verbose('PuppetService', 'messageSend("%s", %s)', conversationId, contactId); const request = new grpcPuppet.MessageSendContactRequest(); request.setConversationId(conversationId); request.setContactId(contactId); const response = await util.promisify(this.grpcManager.client.messageSendContact .bind(this.grpcManager.client))(request); const messageId = response.getId(); if (messageId) { return messageId; } { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } } async messageSendUrl(conversationId, urlLinkPayload) { log.verbose('PuppetService', 'messageSendUrl("%s", %s)', conversationId, JSON.stringify(urlLinkPayload)); const request = new grpcPuppet.MessageSendUrlRequest(); request.setConversationId(conversationId); const pbUrlLinkPayload = new grpcPuppet.UrlLinkPayload(); pbUrlLinkPayload.setUrl(urlLinkPayload.url); pbUrlLinkPayload.setTitle(urlLinkPayload.title); if (urlLinkPayload.description) { pbUrlLinkPayload.setDescription(urlLinkPayload.description); } if (urlLinkPayload.thumbnailUrl) { pbUrlLinkPayload.setThumbnailUrl(urlLinkPayload.thumbnailUrl); } request.setUrlLink(pbUrlLinkPayload); // Deprecated: will be removed after Dec 31, 2022 request.setUrlLinkDeprecated(JSON.stringify(urlLinkPayload)); const response = await util.promisify(this.grpcManager.client.messageSendUrl .bind(this.grpcManager.client))(request); const messageId = response.getId(); if (messageId) { return messageId; } { /** * Huan(202110): Deprecated: will be removed after Dec 31, 2022 */ const messageIdWrapper = response.getIdStringValueDeprecated(); if (messageIdWrapper) { return messageIdWrapper.getValue(); } } } async messageUrl(messageId) { log.verbose('PuppetService', 'messageUrl(%s)', messageId); const request = new grpcPuppet.MessageUrlRequest(); request.setId(messageId); const response = await util.promisify(this.grpcManager.client.messageUrl .bind(this.grpcManager.client))(request); let pbUrlLinkPayload = response.getUrlLink()?.toObject(); if (!pbUrlLinkPayload) { // Deprecated: will be removed after Dec 31, 2022 const jsonText = response.getUrlLinkDeprecated(); pbUrlLinkPayload = JSON.parse(jsonText); } const payload = { title: 'NOTITLE', url: 'NOURL', ...pbUrlLinkPayload, }; return payload; } /** * * Room * */ async roomRawPayload(id) { log.verbose('PuppetService', 'roomRawPayload(%s)', id); const cachedPayload = await this._payloadStore.room?.get(id); if (cachedPayload) { log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id); return cachedPayload; } const request = new grpcPuppet.RoomPayloadRequest(); request.setId(id); const response = await util.promisify(this.grpcManager.client.roomPayload .bind(this.grpcManager.client))(request); const payload = { adminIdList: response.getAdminIdsList(), avatar: response.getAvatar(), id: response.getId(), memberIdList: response.getMemberIdsList(), ownerId: response.getOwnerId(), topic: response.getTopic(), }; await this._payloadStore.room?.set(id, payload); log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id); return payload; } async roomRawPayloadParser(payload) { // log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id) // passthrough return payload; } async roomList() { log.verbose('PuppetService', 'roomList()'); const response = await util.promisify(this.grpcManager.client.roomList .bind(this.grpcManager.client))(new grpcPuppet.RoomListRequest()); return response.getIdsList(); } async roomDel(roomId, contactId) { log.verbose('PuppetService', 'roomDel(%s, %s)', roomId, contactId); const request = new grpcPuppet.RoomDelRequest(); request.setId(roomId); request.setContactId(contactId); await util.promisify(this.grpcManager.client.roomDel .bind(this.grpcManager.client))(request); } async roomAvatar(roomId) { log.verbose('PuppetService', 'roomAvatar(%s)', roomId); const request = new grpcPuppet.RoomAvatarRequest(); request.setId(roomId); const response = await util.promisify(this.grpcManager.client.roomAvatar .bind(this.grpcManager.client))(request); const jsonText = response.getFileBox(); return this.FileBoxUuid.fromJSON(jsonText); } async roomAdd(roomId, contactId, inviteOnly) { log.verbose('PuppetService', 'roomAdd(%s, %s)', roomId, contactId); const request = new grpcPuppet.RoomAddRequest(); request.setId(roomId); request.setContactId(contactId); request.setInviteOnly(inviteOnly); await util.promisify(this.grpcManager.client.roomAdd .bind(this.grpcManager.client))(request); } async roomTopic(roomId, topic) { log.verbose('PuppetService', 'roomTopic(%s, %s)', roomId, topic); /** * Get */ if (typeof topic === 'undefined') { const request = new grpcPuppet.RoomTopicRequest(); request.setId(roomId); const response = await util.promisify(this.grpcManager.client.roomTopic .bind(this.grpcManager.client))(request); const result = response.getTopic(); if (result) { return result; } { // DEPRECATED, will be removed after Dec 31, 2022 const topicWrapper = response.getTopicStringValueDeprecated(); if (topicWrapper) { return topicWrapper.getValue(); } } return ''; } /** * Set */ const request = new grpcPuppet.RoomTopicRequest(); request.setId(roomId); request.setTopic(topic); { // DEPRECATED, will be removed after Dec 31, 2022 const topicWrapper = new StringValue(); topicWrapper.setValue(topic); request.setTopicStringValueDeprecated(topicWrapper); } await util.promisify(this.grpcManager.client.roomTopic .bind(this.grpcManager.client))(request); } async roomCreate(contactIdList, topic) { log.verbose('PuppetService', 'roomCreate(%s, %s)', contactIdList, topic); const request = new grpcPuppet.RoomCreateRequest(); request.setContactIdsList(contactIdList); request.setTopic(topic); const response = await util.promisify(this.grpcManager.client.roomCreate .bind(this.grpcManager.client))(request); return response.getId(); } async roomQuit(roomId) { log.verbose('PuppetService', 'roomQuit(%s)', roomId); const request = new grpcPuppet.RoomQuitRequest(); request.setId(roomId); await util.promisify(this.grpcManager.client.roomQuit .bind(this.grpcManager.client))(request); } async roomQRCode(roomId) { log.verbose('PuppetService', 'roomQRCode(%s)', roomId); const request = new grpcPuppet.RoomQRCodeRequest(); request.setId(roomId); const response = await util.promisify(this.grpcManager.client.roomQRCode .bind(this.grpcManager.client))(request); return response.getQrcode(); } async roomMemberList(roomId) { log.verbose('PuppetService', 'roomMemberList(%s)', roomId); const request = new grpcPuppet.RoomMemberListRequest(); request.setId(roomId); const response = await util.promisify(this.grpcManager.client.roomMemberList .bind(this.grpcManager.client))(request); return response.getMemberIdsList(); } async roomMemberRawPayload(roomId, contactId) { log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId); const cachedPayload = await this._payloadStore.roomMember?.get(roomId); const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId]; if (cachedRoomMemberPayload) { log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId); return cachedRoomMemberPayload; } const request = new grpcPuppet.RoomMemberPayloadRequest(); request.setId(roomId); request.setMemberId(contactId); const response = await util.promisify(this.grpcManager.client.roomMemberPayload .bind(this.grpcManager.client))(request); const payload = { avatar: response.getAvatar(), id: response.getId(), inviterId: response.getInviterId(), name: response.getName(), roomAlias: response.getRoomAlias(), }; await this._payloadStore.roomMember?.set(roomId, { ...cachedPayload, contactId: payload, }); log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId); return payload; } async roomMemberRawPayloadParser(payload) { // log.silly('PuppetService', 'roomMemberRawPayloadParser({id:%s})', payload.id) // passthrough return payload; } async roomAnnounce(roomId, text) { log.verbose('PuppetService', 'roomAnnounce(%s%s)', roomId, typeof text === 'undefined' ? '' : `, ${text}`); /** * Set */ if (text) { const request = new grpcPuppet.RoomAnnounceRequest(); request.setId(roomId); request.setText(text); { // DEPRECATED, will be removed after Dec 31, 2022 const textWrapper = new StringValue(); textWrapper.setValue(text); request.setTextStringValueDeprecated(textWrapper); } await util.promisify(this.grpcManager.client.roomAnnounce .bind(this.grpcManager.client))(request); return; } /** * Get */ const request = new grpcPuppet.RoomAnnounceRequest(); request.setId(roomId); const response = await util.promisify(this.grpcManager.client.roomAnnounce .bind(this.grpcManager.client))(request); const result = response.getText(); if (result) { return result; } { // DEPRECATED, will be removed after Dec 31, 2022 const textWrapper = response.getTextStringValueDeprecated(); if (textWrapper) { return textWrapper.getValue(); } } return ''; } async roomInvitationAccept(roomInvitationId) { log.verbose('PuppetService', 'roomInvitationAccept(%s)', roomInvitationId); const request = new grpcPuppet.RoomInvitationAcceptRequest(); request.setId(roomInvitationId); await util.promisify(this.grpcManager.client.roomInvitationAccept .bind(this.grpcManager.client))(request); } async roomInvitationRawPayload(id) { log.verbose('PuppetService', 'roomInvitationRawPayload(%s)', id); const request = new grpcPuppet.RoomInvitationPayloadRequest(); request.setId(id); const response = await util.promisify(this.grpcManager.client.roomInvitationPayload .bind(this.grpcManager.client))(request); let timestamp; const receiveTime = response.getReceiveTime(); if (receiveTime) { timestamp = millisecondsFromTimestamp(receiveTime); } { // Deprecated: will be removed after Dec 31, 2022 const deprecated = true; void deprecated; if (!receiveTime) { timestamp = response.getTimestampUint64Deprecated(); } } // FIXME: how to set it better? timestamp ??= 0; const payload = { avatar: response.getAvatar(), id: response.getId(), invitation: response.getInvitation(), inviterId: response.getInviterId(), memberCount: response.getMemberCount(), memberIdList: response.getMemberIdsList(), receiverId: response.getReceiverId(), timestamp, topic: response.getTopic(), }; return payload; } async roomInvitationRawPayloadParser(payload) { // log.silly('PuppetService', 'roomInvitationRawPayloadParser({id:%s})', payload.id) // passthrough return payload; } /** * * Friendship * */ async friendshipSearchPhone(phone) { log.verbose('PuppetService', 'friendshipSearchPhone(%s)', phone); const request = new grpcPuppet.Friendsh