/** * Wechaty - https://github.com/chatie/wechaty * * @copyright 2016-2018 Huan LI * * 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 * as PUPPET from '@juzi/wechaty-puppet' import { log } from '@juzi/wechaty-puppet' import type { MemoryCard } from 'memory-card' import { VERSION, } from './config.js' import Manager from './manager.js' import type { RequestManagerAPIs } from './request/request-manager.js' import type { ClientOptions, WhatsAppClientType } from './schema/whatsapp-type.js' import WAError from './exception/whatsapp-error.js' import { WA_ERROR_TYPE } from './exception/error-type.js' import { EventName } from './schema/event-name.js' import { RequestPool } from './request/request-pool.js' import { contactSelfQRCode, contactSelfName, contactSelfSignature } from './puppet-mixin/contact-self.js' import { contactAlias, contactPhone, contactCorporationRemark, contactDescription, contactList, contactAvatar, contactRawPayloadParser, contactRawPayload } from './puppet-mixin/contact.js' import { conversationReadMark } from './puppet-mixin/conversation.js' import { friendshipRawPayload, friendshipRawPayloadParser, friendshipSearchPhone, friendshipSearchWeixin, friendshipAdd, friendshipAccept, friendshipSearchHandle } from './puppet-mixin/friendship.js' import { messageContact, messageImage, messageRecall, messageFile, messageUrl, messageMiniProgram, messageSendText, messageSendFile, messageSendContact, messageSendUrl, messageSendMiniProgram, messageForward, messageRawPayloadParser, messageRawPayload, messagePost, messageSendChannel, messageChannel, messageSendLocation, messageLocation } from './puppet-mixin/message.js' import { roomRawPayloadParser, roomRawPayload, roomList, roomDel, roomAvatar, roomAdd, roomTopic, roomCreate, roomQuit, roomQRCode, roomMemberList, roomMemberRawPayload, roomMemberRawPayloadParser, roomAnnounce, roomInvitationAccept, roomInvitationRawPayload, roomInvitationRawPayloadParser } from './puppet-mixin/room.js' import { tagContactTagAdd, tagContactTagList, tagContactTagRemove, tagGroupAdd, tagGroupDelete, tagGroupList, tagGroupTagList, tagTagAdd, tagTagContactList, tagTagDelete, tagTagList } from './puppet-mixin/tag.js' // import { Attachment } from './mock/user/types' type ManagerWithRequestManager = Manager & RequestManagerAPIs export type PuppetWhatsAppOptions = PUPPET.PuppetOptions & { memory?: MemoryCard puppeteerOptions?: ClientOptions } const PRE = 'PuppetWhatsapp' class PuppetWhatsapp extends PUPPET.Puppet { static override readonly VERSION = VERSION public manager: ManagerWithRequestManager constructor ( override options: PuppetWhatsAppOptions = {}, ) { super(options) log.verbose(PRE, 'constructor()') this.manager = new Manager(this.options) as ManagerWithRequestManager } override version () { return VERSION } override async onStart (): Promise { log.verbose(PRE, 'onStart()') let whatsapp: WhatsAppClientType try { whatsapp = await this.startManager(this.manager) } catch (err) { log.error(PRE, `Can not start whatsapp, error: ${(err as Error).message}`) throw WAError(WA_ERROR_TYPE.ERR_INIT, `Can not start whatsapp, error: ${(err as Error).message}`) } /** * Huan(202102): Wait for Puppeteer to be inited before resolve start() for robust state management */ const future = new Promise(resolve => { function check () { if (whatsapp.pupBrowser) { resolve() } else { setTimeout(check, 100) } } check() }) return Promise.race([ future, this.state.stable('inactive'), ]) } private async startManager (manager: Manager) { manager.on({ dirty: data => { this.emit('dirty', data) }, error: this.onError.bind(this), friendship: this.onFriendship.bind(this), heartbeat: data => this.emit('heartbeat', { data, }), login: this.onLogin.bind(this), logout: this.onLogout.bind(this), message: this.onMessage.bind(this), ready: this.onReady.bind(this), reset: this.onReset.bind(this), 'room-invite': this.onRoomInvite.bind(this), 'room-join': this.onRoomJoin.bind(this), 'room-leave': this.onRoomLeave.bind(this), 'room-topic': this.onRoomTopic.bind(this), 'room-announce': this.onRoomAnnounce.bind(this), scan: this.onScan.bind(this), }) const memory = this.memory const whatsapp = await this.manager.start(memory) return whatsapp } override async onStop (): Promise { log.verbose(PRE, 'onStop()') try { await this.stopManager() } catch (err) { log.error(PRE, `Can not stop, error: ${(err as Error).message}`) } } private async stopManager () { this.manager.off('*') await this.manager.stop() } /** * Event section: onXXX */ private async onLogin (wxid: string): Promise { log.verbose(PRE, 'onLogin(%s)', wxid) if (this.isLoggedIn) { log.warn(PRE, 'onLogin(%s) already login? NOOP', wxid) return } log.info(PRE, `${EventName.LOGIN}, ${wxid}`) await super.login(wxid) // no need to emit login since super.login will do that // this.emit('login', { contactId: wxid }) } private async onLogout (wxid: string, message: string): Promise { log.verbose(PRE, 'onLogout(%s, %s)', wxid, message) if (!this.isLoggedIn) { log.warn(PRE, 'onLogout(%s) already logged out?', wxid) } log.info(PRE, `${EventName.LOGOUT}, ${wxid}`) const requestPool = RequestPool.Instance requestPool.clearPool() this.emit('logout', { contactId: wxid, data: message }) } private async onMessage (message: PUPPET.payloads.EventMessage): Promise { log.verbose(PRE, 'onMessage(%s)', JSON.stringify(message)) this.emit('message', message) } private async onScan (payload: PUPPET.payloads.EventScan): Promise { log.verbose(PRE, 'onScan(%s)', JSON.stringify(payload)) log.info(PRE, `${EventName.SCAN}`) this.emit('scan', payload) } private async onError (e: string) { log.info(PRE, `${EventName.ERROR}, ${e}`) this.emit('error', { data: e, }) } private async onReset (reason: string) { log.info(PRE, `${EventName.RESET}, ${reason}`) this.emit('reset', { data: reason } as PUPPET.payloads.EventReset) } private async onFriendship (payload: PUPPET.payloads.EventFriendship): Promise { const friendshipPayload = await this.friendshipRawPayload(payload.friendshipId) if (friendshipPayload.type === PUPPET.types.Friendship.Receive) { // 收到好友请求暂不冒泡,直接接受 await this.friendshipAccept(payload.friendshipId) return } this.emit('friendship', payload) } private async onRoomJoin (payload: PUPPET.payloads.EventRoomJoin) { const roomId = payload.roomId await this.dirtyPayload(PUPPET.types.Payload.Room, roomId) this.emit('room-join', payload) } private async onRoomLeave (payload: PUPPET.payloads.EventRoomLeave) { const roomId = payload.roomId await this.dirtyPayload(PUPPET.types.Payload.Room, roomId) this.emit('room-leave', payload) } private async onRoomTopic (payload: PUPPET.payloads.EventRoomTopic) { const roomId = payload.roomId await this.dirtyPayload(PUPPET.types.Payload.Room, roomId) this.emit('room-topic', payload) } private async onRoomInvite (payload: PUPPET.payloads.EventRoomInvite) { this.emit('room-invite', payload) } private async onRoomAnnounce (payload: PUPPET.payloads.EventRoomAnnounce) { const roomId = payload.roomId await this.dirtyPayload(PUPPET.types.Payload.Room, roomId) this.emit('room-announce', payload) } private async onReady () { log.verbose(PRE, 'onReady()') log.info(PRE, `${EventName.READY}`) this.emit('ready', { data: 'ready' }) } /** * Override Methods */ override async logout () { await super.logout() if (!this.isLoggedIn) { log.verbose(PRE, 'logout() do nothing') return } return this.manager.logout() } override ding (data?: string): void { log.silly(PRE, 'ding(%s)', data || '') setTimeout(() => this.emit('dong', { data: data || '' }), 1000) } /** * ContactSelf */ override contactSelfQRCode = contactSelfQRCode override contactSelfName = contactSelfName override contactSelfSignature = contactSelfSignature /** * Contact */ override contactAlias = contactAlias override contactPhone = contactPhone override contactCorporationRemark = contactCorporationRemark override contactDescription = contactDescription override contactList = contactList override contactAvatar = contactAvatar override contactRawPayloadParser = contactRawPayloadParser override contactRawPayload = contactRawPayload /** * Conversation */ override conversationReadMark = conversationReadMark /** * Message */ // FIXME: need to implement in wechaty-puppet messagePost = messagePost override messageContact = messageContact override messageImage = messageImage override messageRecall = messageRecall override messageFile = messageFile override messageUrl = messageUrl override messageMiniProgram = messageMiniProgram override messageChannel = messageChannel override messageSendText = messageSendText override messageSendFile = messageSendFile override messageSendContact = messageSendContact override messageSendUrl = messageSendUrl override messageSendMiniProgram = messageSendMiniProgram override messageSendChannel = messageSendChannel override messageSendLocation = messageSendLocation override messageLocation = messageLocation override messageForward = messageForward override messageRawPayloadParser = messageRawPayloadParser override messageRawPayload = messageRawPayload /** * Room */ override roomRawPayloadParser = roomRawPayloadParser override roomRawPayload = roomRawPayload override roomList = roomList override roomDel = roomDel override roomAvatar = roomAvatar override roomAdd = roomAdd override roomTopic = roomTopic override roomCreate = roomCreate override roomQuit = roomQuit override roomQRCode = roomQRCode override roomMemberList = roomMemberList override roomMemberRawPayload = roomMemberRawPayload override roomMemberRawPayloadParser = roomMemberRawPayloadParser override roomAnnounce = roomAnnounce override roomInvitationAccept = roomInvitationAccept override roomInvitationRawPayload = roomInvitationRawPayload override roomInvitationRawPayloadParser = roomInvitationRawPayloadParser /** * Friendship */ override friendshipRawPayload = friendshipRawPayload override friendshipRawPayloadParser = friendshipRawPayloadParser override friendshipSearchPhone = friendshipSearchPhone override friendshipSearchWeixin = friendshipSearchWeixin override friendshipSearchHandle = friendshipSearchHandle override friendshipAdd = friendshipAdd override friendshipAccept = friendshipAccept /** * Tag */ override tagContactTagAdd = tagContactTagAdd override tagContactTagRemove = tagContactTagRemove override tagContactTagList = tagContactTagList override tagGroupAdd = tagGroupAdd override tagGroupDelete = tagGroupDelete override tagGroupList = tagGroupList override tagGroupTagList = tagGroupTagList override tagTagAdd = tagTagAdd override tagTagDelete = tagTagDelete override tagTagList = tagTagList override tagTagContactList = tagTagContactList } export { PuppetWhatsapp } export default PuppetWhatsapp