UNPKG

wechaty-puppet-official-account

Version:
802 lines (692 loc) 24.1 kB
/** * Wechaty - https://github.com/chatie/wechaty * * @copyright 2016-2018 Huan LI <zixia@zixia.net> * * 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 'wechaty-puppet' import { FileBox, FileBoxInterface, } from 'file-box' import { VERSION, qrCodeForChatie, envOptions, log, } from './config.js' import { OfficialAccountOptions, OfficialAccount, } from './official-account/official-account.js' import type { OAContactPayload, OAMessagePayload, OAMediaType, } from './official-account/schema.js' export type PuppetOAOptions = PUPPET.PuppetOptions & Partial<OfficialAccountOptions> class PuppetOA extends PUPPET.Puppet { override messageLocation (_messageId: string): Promise<PUPPET.payloads.Location> { throw new Error('Method not implemented.') } override messageSendLocation (_conversationId: string, _locationPayload: PUPPET.payloads.Location): Promise<string | void> { throw new Error('Method not implemented.') } override contactPhone (contactId: string, phoneList: string[]): Promise<void> { log.info('contactPhone(%s, %s)', contactId, phoneList) throw new Error('Method not implemented.') } override contactCorporationRemark (contactId: string, corporationRemark: string | null): Promise<void> { log.info('contactCorporationRemark(%s, %s)', contactId, corporationRemark) throw new Error('Method not implemented.') } override contactDescription (contactId: string, description: string | null): Promise<void> { log.info('contactDescription(%s, %s)', contactId, description) throw new Error('Method not implemented.') } static override readonly VERSION = VERSION protected appId : string protected appSecret : string protected port? : number protected token : string protected webhookProxyUrl? : string protected personalMode? : boolean protected accessTokenProxyUrl? : string protected oa? : OfficialAccount private _heartBeatTimer?: ReturnType<typeof setTimeout> constructor ( options: PuppetOAOptions = {}, ) { super() log.verbose('PuppetOA', 'constructor()') options = { ...envOptions(), ...options, } if (options.appId) { this.appId = options.appId } else { throw new Error(` PuppetOA: appId not found. Please either set the WECHATY_PUPPET_OA_APP_ID environment variable, or set 'appId' optoins for PuppetOA. `) } if (options.appSecret) { this.appSecret = options.appSecret } else { throw new Error(` PuppetOA: appSecret not found. Please either set the WECHATY_PUPPET_OA_APP_SECRET environment variable, or set 'appSecret' options for PuppetOA. `) } if (options.token) { this.token = options.token } else { throw new Error(` PuppetOA: token not found. Please either set WECHATY_PUPPET_OA_TOKEN environment variabnle, or set 'token' options for PuppetOA. `) } if (options.personalMode) { this.personalMode = options.personalMode } else { this.personalMode = false } this.port = options.port this.webhookProxyUrl = options.webhookProxyUrl /** * NOTE: if the ip address of server is dynamic, it can't fetch the accessToken from tencent server. * So, the accessTokenProxyUrl configuration is needed to fetch the accessToken from the specific endpoint. * * eg: accessTokenProxyUrl = 'http://your-endpoint/' * puppet-oa will fetch accessToken from: http://your-endpoint/token?grant_type=client_credential&appid=${appId}&secret=${appSecret} */ if (options.accessTokenProxyUrl) { if (options.accessTokenProxyUrl.endsWith('/')) { options.accessTokenProxyUrl = options.accessTokenProxyUrl.substring(0, options.accessTokenProxyUrl.length - 1) } this.accessTokenProxyUrl = options.accessTokenProxyUrl } } override version (): string { return VERSION } override async onStart (): Promise<void> { log.verbose('PuppetOA', 'onStart()') this.oa = new OfficialAccount({ appId : this.appId, appSecret : this.appSecret, personalMode : this.personalMode, port : this.port, token : this.token, webhookProxyUrl : this.webhookProxyUrl, }) this.bridgeEvents(this.oa) await this.oa.start() await this._startPuppetHeart(true) // FIXME: Huan(202008) find a way to get the bot user information // Official Account Info can be customized by user, so It should be // configured by environment variable. // set gh_ prefix to identify the official-account const currentUserId = `gh_${this.appId}` await this.oa.payloadStore.setContactPayload(currentUserId, { openid: currentUserId } as any) this.login(currentUserId) this.emit('ready', { data: 'ready' }) } private async _startPuppetHeart (firstTime: boolean = true) { if (firstTime && this._heartBeatTimer) { return } this.emit('heartbeat', { data: 'heartbeat@office: live' }) // eslint-disable-next-line @typescript-eslint/no-misused-promises this._heartBeatTimer = setTimeout(async (): Promise<void> => { await this._startPuppetHeart(false) return undefined }, 15 * 1000) // 15s } // 停止监听心跳 private _stopPuppetHeart () { if (!this._heartBeatTimer) { return } clearTimeout(this._heartBeatTimer) this._heartBeatTimer = undefined } protected bridgeEvents (oa: OfficialAccount) { oa.on('message', msg => this.emit('message', { messageId: msg.MsgId })) oa.on('login', _ => this.login(this.currentUserId)) oa.on('ready', _ => this.emit('ready', { data: 'ready' })) oa.on('logout', _ => this.wrapAsync(this.logout('oa.on(logout)'))) } override async onStop (): Promise<void> { log.verbose('PuppetOA', 'onStop()') if (this.oa) { this.oa.removeAllListeners() await this.oa.stop() this.oa = undefined await this.logout('oa.on(logout)') } this._stopPuppetHeart() } override ding (data?: string): void { log.silly('PuppetOA', 'ding(%s)', data || '') // FIXME: do the real job setTimeout(() => this.emit('dong', { data: data || '' }), 1000) } /** * * ContactSelf * * */ override async contactSelfQRCode (): Promise<string> { log.verbose('PuppetOA', 'contactSelfQRCode()') return 'qrcode in the future ;^)' } override async contactSelfName (name: string): Promise<void> { log.verbose('PuppetOA', 'contactSelfName(%s)', name) } override async contactSelfSignature (signature: string): Promise<void> { log.verbose('PuppetOA', 'contactSelfSignature(%s)', signature) } /** * * Contact * */ override contactAlias (contactId: string) : Promise<string> override contactAlias (contactId: string, alias: string | null): Promise<void> override async contactAlias (contactId: string, alias?: string | null): Promise<void | string> { log.verbose('PuppetOA', 'contactAlias(%s, %s)', contactId, alias) /** * 1. set */ if (alias) { await this.oa?.updateContactRemark(contactId, alias) return alias } /** * 2. get */ const contactPayload = await this.contactPayload(contactId) if (!contactPayload.alias) { log.warn('Contact<%s> has no alias', contactId) } return contactPayload.alias } override async contactList (): Promise<string[]> { log.verbose('PuppetOA', 'contactList()') const contactIdList = await this.oa?.getContactList() if (!contactIdList) { throw new Error('contactIdList found from oa store') } return contactIdList } override async contactAvatar (contactId: string) : Promise<FileBoxInterface> override async contactAvatar (contactId: string, file: FileBoxInterface) : Promise<void> override async contactAvatar (contactId: string, file?: FileBoxInterface): Promise<void | FileBoxInterface> { log.verbose('PuppetOA', 'contactAvatar(%s)', contactId) /** * 1. set */ if (file) { return } /** * 2. get */ const contactPayload = await this.contactPayload(contactId) const fileBox = contactPayload.avatar ? FileBox.fromUrl(contactPayload.avatar) : undefined if (fileBox) { return fileBox } } override async contactRawPayloadParser (oaPayload: OAContactPayload): Promise<PUPPET.payloads.Contact> { const payload: PUPPET.payloads.Contact = { alias : oaPayload.remark, avatar : oaPayload.headimgurl, city : oaPayload.city, friend : true, gender : oaPayload.sex, id : oaPayload.openid, name : oaPayload.nickname, phone : [], province : oaPayload.province, signature : '', star : false, type : PUPPET.types.Contact.Individual, weixin : oaPayload.unionid, } return payload } override async contactRawPayload (id: string): Promise<OAContactPayload> { log.verbose('PuppetOA', 'contactRawPayload(%s)', id) const contactInfoPayload = await this.oa?.getContactPayload(id) if (!contactInfoPayload) { throw new Error(`can not get PUPPET.payloads.Contact(${id})`) } return contactInfoPayload } /** * * Message * */ override async messageContact ( messageId: string, ): Promise<string> { log.verbose('PuppetOA', 'messageContact(%s)', messageId) // const attachment = this.mocker.MockMessage.loadAttachment(messageId) // if (attachment instanceof ContactMock) { // return attachment.id // } return '' } override async messageImage ( messageId : string, imageType : PUPPET.types.Image, ) : Promise<FileBoxInterface> { log.verbose('PuppetOA', 'messageImage(%s, %s[%s])', messageId, imageType, PUPPET.types.Image[imageType], ) // const attachment = this.mocker.MockMessage.loadAttachment(messageId) // if (attachment instanceof FileBox) { // return attachment // } const payload: PUPPET.payloads.Message = await this.messagePayload(messageId) let fileBox: FileBoxInterface if (payload.type === PUPPET.types.Message.Image) { if (!payload.filename) { throw Error(`image message type must have filename file. <${payload}>`) } fileBox = FileBox.fromUrl(payload.filename) } else { throw Error('can"t get file from the message') } return fileBox } override async messageRecall ( messageId: string, ): Promise<boolean> { log.verbose('PuppetOA', 'messageRecall(%s)', messageId) return false } override async messageFile (id: string): Promise<FileBoxInterface> { log.verbose('PuppetOA', 'messageFile(%s)', id) const payload: PUPPET.payloads.Message = await this.messagePayload(id) switch (payload.type) { case PUPPET.types.Message.Image: if (!payload.filename) { throw Error(`Image message must have filename. <${payload}>`) } return FileBox.fromUrl(payload.filename) case PUPPET.types.Message.Audio: if (!payload.filename) { throw Error(`Audio message must have filename. <${payload}>`) } // payload.filename is an URL to the audio file. The name of the file is not in the URL. // Setting a filename with expected extension is necessary for inference of mime type in // FileBox. return FileBox.fromUrl(payload.filename, 'message.amr') default: throw Error('can"t get file from the message') } } override async messageUrl (messageId: string) : Promise<PUPPET.payloads.UrlLink> { log.verbose('PuppetOA', 'messageUrl(%s)', messageId) // const attachment = this.mocker.MockMessage.loadAttachment(messageId) // if (attachment instanceof UrlLink) { // return attachment.payload // } return { title : 'mock title for ' + messageId, url : 'https://mock.url', } } override async messageMiniProgram (messageId: string): Promise<PUPPET.payloads.MiniProgram> { log.verbose('PuppetOA', 'messageMiniProgram(%s)', messageId) // const attachment = this.mocker.MockMessage.loadAttachment(messageId) // if (attachment instanceof MiniProgram) { // return attachment.payload // } return { title : 'mock title for ' + messageId, } } override async messageRawPayloadParser (rawPayload: OAMessagePayload): Promise<PUPPET.payloads.Message> { const payload: PUPPET.payloads.Message = { id : rawPayload.MsgId, listenerId: rawPayload.ToUserName, talkerId : rawPayload.FromUserName, timestamp : parseInt(rawPayload.CreateTime), type : PUPPET.types.Message.Text, } if (rawPayload.MsgType === 'image') { payload.type = PUPPET.types.Message.Image if (!rawPayload.PicUrl) { throw Error(`Image Payload must has PicUrl field :<${JSON.stringify(rawPayload)}>`) } payload.filename = rawPayload.PicUrl } else if (rawPayload.MsgType === 'video') { payload.type = PUPPET.types.Message.Video } else if (rawPayload.MsgType === 'location') { payload.type = PUPPET.types.Message.Location } else if (rawPayload.MsgType === 'text') { payload.text = rawPayload.Content } else if (rawPayload.MsgType === 'event') { payload.text = rawPayload.EventKey } else if (rawPayload.MsgType === 'voice') { payload.type = PUPPET.types.Message.Audio payload.filename = await this.oa?.getAudioUrl(rawPayload.MediaId!) } return payload } override async messageRawPayload (id: string): Promise<OAMessagePayload> { log.verbose('PuppetOA', 'messageRawPayload(%s)', id) const payload = await this.oa?.payloadStore.getMessagePayload(id) if (!payload) { throw new Error('payload not found from oa store') } return payload } private async _messageSend ( conversationId: string, something: string | FileBox, // | Attachment mediatype: OAMediaType = 'image', ): Promise<string> { log.verbose('PuppetOA', 'messageSend(%s, %s)', conversationId, something) let msgId = null if (typeof something === 'string') { const payload = { content: something, msgtype: 'text' as const, touser: conversationId, } if (this.personalMode) { msgId = await this.oa?.sendCustomMessagePersonal(payload) if (!msgId) { throw new Error('can"t send personal CustomeMessage') } } else { msgId = await this.oa?.sendCustomMessage(payload) } } else if (FileBox.valid(something)) { await something.ready() msgId = await this.oa?.sendFile({ file: something, msgtype: mediatype, touser: conversationId }) } if (!msgId) { throw new Error('PuppetOA messageSend() can"t get msgId response') } return msgId } override async messageSendText ( conversationId: string, text : string, ): Promise<string> { return this._messageSend(conversationId, text) } override async messageSendFile ( conversationId: string, file : FileBox, ): Promise<string> { let msgtype: OAMediaType const mimeType = file.mediaType switch (mimeType) { case 'image/jpeg': case 'image/jpg': case 'image/png': case 'image/gif': msgtype = 'image' break case 'audio/amr': case 'audio/mpeg': msgtype = 'voice' break case 'video/mp4': msgtype = 'video' break default: throw new Error(`unsupported media type: ${file.mimeType}`) } return this._messageSend(conversationId, file, msgtype) } override async messageSendContact ( conversationId: string, contactId : string, ): Promise<void> { log.verbose('PuppetOA', 'messageSendUrl(%s, %s)', conversationId, contactId) // const contact = this.mocker.MockContact.load(contactId) // return this.messageSend(conversationId, contact) } override async messageSendUrl ( conversationId: string, urlLinkPayload : PUPPET.payloads.UrlLink, ) : Promise<string> { log.verbose('PuppetOA', 'messageSendUrl(%s, %s)', conversationId, urlLinkPayload) let msgId = null msgId = await this.oa?.sendCustomLink({ touser: conversationId, urlLinkPayload }) if (!msgId) { throw new Error('PuppetOA messageSendUrl() can"t get msgId response') } return msgId } override async messageSendMiniProgram ( conversationId: string, miniProgramPayload: PUPPET.payloads.MiniProgram, ): Promise<string> { log.verbose('PuppetOA', 'messageSendMiniProgram(%s, %s)', conversationId, JSON.stringify(miniProgramPayload)) let msgId = null msgId = await this.oa?.sendCustomMiniProgram({ miniProgram:miniProgramPayload, touser: conversationId }) if (!msgId) { throw new Error('PuppetOA messageSendMiniProgram() can"t get msgId response') } return msgId } override async messageForward ( conversationId: string, messageId : string, ): Promise<void> { log.verbose('PuppetOA', 'messageForward(%s, %s)', conversationId, messageId, ) } override async conversationReadMark ( conversationId : string, hasRead? : boolean, ): Promise<void | boolean> { log.verbose('PuppetOA', 'conversationReadMark(%s, %s)', conversationId, hasRead, ) } /** * * Room * */ override async roomRawPayloadParser (payload: PUPPET.payloads.Room) { return payload } override async roomRawPayload (id: string): Promise<PUPPET.payloads.Room> { log.verbose('PuppetOA', 'roomRawPayload(%s)', id) return {} as any } override async roomList (): Promise<string[]> { log.verbose('PuppetOA', 'roomList()') return [] } override async roomDel ( roomId : string, contactId : string, ): Promise<void> { log.verbose('PuppetOA', 'roomDel(%s, %s)', roomId, contactId) } override async roomAvatar (roomId: string): Promise<FileBoxInterface> { log.verbose('PuppetOA', 'roomAvatar(%s)', roomId) const payload = await this.roomPayload(roomId) if (payload.avatar) { return FileBox.fromUrl(payload.avatar) } log.warn('PuppetOA', 'roomAvatar() avatar not found, use the chatie default.') return qrCodeForChatie() } override async roomAdd ( roomId : string, contactId : string, ): Promise<void> { log.verbose('PuppetOA', 'roomAdd(%s, %s)', roomId, contactId) } override async roomTopic (roomId: string) : Promise<string> override async roomTopic (roomId: string, topic: string) : Promise<void> override async roomTopic ( roomId: string, topic?: string, ): Promise<void | string> { log.verbose('PuppetOA', 'roomTopic(%s, %s)', roomId, topic) if (typeof topic === 'undefined') { return 'mock room topic' } await this.dirtyPayload(PUPPET.types.Payload.Room, roomId) } override async roomCreate ( contactIdList : string[], topic : string, ): Promise<string> { log.verbose('PuppetOA', 'roomCreate(%s, %s)', contactIdList, topic) return 'mock_room_id' } override async roomQuit (roomId: string): Promise<void> { log.verbose('PuppetOA', 'roomQuit(%s)', roomId) } override async roomQRCode (roomId: string): Promise<string> { log.verbose('PuppetOA', 'roomQRCode(%s)', roomId) return roomId + ' mock qrcode' } override async roomMemberList (roomId: string) : Promise<string[]> { log.verbose('PuppetOA', 'roomMemberList(%s)', roomId) return [] } override async roomMemberRawPayload (roomId: string, contactId: string): Promise<PUPPET.payloads.RoomMember> { log.verbose('PuppetOA', 'roomMemberRawPayload(%s, %s)', roomId, contactId) return { avatar : 'mock-avatar-data', id : 'xx', name : 'mock-name', roomAlias : 'yy', } } override async roomMemberRawPayloadParser (rawPayload: PUPPET.payloads.RoomMember): Promise<PUPPET.payloads.RoomMember> { log.verbose('PuppetOA', 'roomMemberRawPayloadParser(%s)', rawPayload) return rawPayload } override async roomAnnounce (roomId: string) : Promise<string> override async roomAnnounce (roomId: string, text: string) : Promise<void> override async roomAnnounce (roomId: string, text?: string) : Promise<void | string> { if (text) { return } return 'mock announcement for ' + roomId } /** * * Room Invitation * */ override async roomInvitationAccept (roomInvitationId: string): Promise<void> { log.verbose('PuppetOA', 'roomInvitationAccept(%s)', roomInvitationId) } override async roomInvitationRawPayload (roomInvitationId: string): Promise<any> { log.verbose('PuppetOA', 'roomInvitationRawPayload(%s)', roomInvitationId) } override async roomInvitationRawPayloadParser (rawPayload: any): Promise<PUPPET.payloads.RoomInvitation> { log.verbose('PuppetOA', 'roomInvitationRawPayloadParser(%s)', JSON.stringify(rawPayload)) return rawPayload } /** * * Friendship * */ override async friendshipRawPayload (id: string): Promise<any> { return { id } as any } override async friendshipRawPayloadParser (rawPayload: any): Promise<PUPPET.payloads.Friendship> { return rawPayload } override async friendshipSearchPhone ( phone: string, ): Promise<null | string> { log.verbose('PuppetOA', 'friendshipSearchPhone(%s)', phone) return null } override async friendshipSearchWeixin ( weixin: string, ): Promise<null | string> { log.verbose('PuppetOA', 'friendshipSearchWeixin(%s)', weixin) return null } override async friendshipAdd ( contactId : string, hello : string, ): Promise<void> { log.verbose('PuppetOA', 'friendshipAdd(%s, %s)', contactId, hello) } override async friendshipAccept ( friendshipId : string, ): Promise<void> { log.verbose('PuppetOA', 'friendshipAccept(%s)', friendshipId) } /** * * Tag * */ override async tagContactAdd ( tagId: string, contactId: string, ): Promise<void> { log.verbose('PuppetOA', 'tagContactAdd(%s)', tagId, contactId) await this.oa?.addTagToMembers(tagId, [ contactId ]) } override async tagContactRemove ( tagId: string, contactId: string, ): Promise<void> { log.verbose('PuppetOA', 'tagContactRemove(%s)', tagId, contactId) await this.oa?.removeTagFromMembers(tagId, [ contactId ]) } override async tagContactDelete ( tagId: string, ): Promise<void> { log.verbose('PuppetOA', 'tagContactDelete(%s)', tagId) await this.oa?.deleteTag(tagId) } override async tagContactList ( contactId?: string, ): Promise<string[]> { log.verbose('PuppetOA', 'tagContactList(%s)', contactId) if (!this.oa) { throw new Error('can not find oa object') } // 1. get all of the tags if (!contactId) { const tagList = await this.oa.getContactList() return tagList } // 2. get the member tags const tagList = await this.oa.getMemberTags(contactId) return tagList } } export { PuppetOA } export default PuppetOA