UNPKG

wechaty-puppet-wechat

Version:
1,108 lines (944 loc) 34.4 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 { EventEmitter } from 'events' import fs from 'fs' import path from 'path' import puppeteer from 'puppeteer' import puppeteerExtra from 'puppeteer-extra' import stealthPlugin from 'puppeteer-extra-plugin-stealth' import { StateSwitch } from 'state-switch' import { parseString } from 'xml2js' import { wrapAsyncError, GError, } from 'gerror' import type { MemoryCard, } from 'memory-card' import { log, } from 'wechaty-puppet' import { MEMORY_SLOT, } from './config.js' import { codeRoot, } from './cjs.js' import type { WebContactRawPayload, WebMessageMediaPayload, WebMessageRawPayload, WebRoomRawPayload, } from './web-schemas.js' import { unescapeHtml, retryPolicy, } from './pure-function-helpers/mod.js' export interface InjectResult { code: number, message: string, } export interface BridgeOptions { endpoint? : string, head? : boolean, launchOptions? : puppeteer.LaunchOptions, memory : MemoryCard, stealthless? : boolean, uos? : boolean, uosExtSpam? : string } export type Cookie = puppeteer.Protocol.Network.Cookie export class Bridge extends EventEmitter { private browser : undefined | puppeteer.Browser private page : undefined | puppeteer.Page private state : StateSwitch private wrapAsync = wrapAsyncError(e => this.emit('error', e)) constructor ( public options: BridgeOptions, ) { super() log.verbose('PuppetWeChatBridge', 'constructor()') this.state = new StateSwitch('PuppetWeChatBridge', { log }) } public async start (): Promise<void> { log.verbose('PuppetWeChatBridge', 'start()') this.state.active('pending') try { this.browser = await this.initBrowser() log.verbose('PuppetWeChatBridge', 'start() initBrowser() done') this.on('load', this.wrapAsync(this.onLoad.bind(this))) const ready = new Promise(resolve => this.once('ready', resolve)) this.page = await this.initPage(this.browser) await ready this.state.active(true) log.verbose('PuppetWeChatBridge', 'start() initPage() done') } catch (e) { log.error('PuppetWeChatBridge', 'start() exception: %s', e as Error) this.state.inactive(true) try { if (this.page) { await this.page.close() } if (this.browser) { await this.browser.close() } } catch (e2) { log.error('PuppetWeChatBridge', 'start() exception %s, close page/browser exception %s', e, e2) } this.emit('error', e) throw e } } public async initBrowser (): Promise<puppeteer.Browser> { log.verbose('PuppetWeChatBridge', 'initBrowser()') const launchOptions = { ...this.options.launchOptions } as puppeteer.LaunchOptions & puppeteer.BrowserLaunchArgumentOptions const headless = !(this.options.head) const launchOptionsArgs = launchOptions.args || [] if (this.options.endpoint) { launchOptions.executablePath = this.options.endpoint } const options = { ...launchOptions, args: [ '--audio-output-channels=0', '--disable-default-apps', '--disable-translate', '--disable-gpu', '--disable-setuid-sandbox', '--disable-sync', '--hide-scrollbars', '--mute-audio', '--no-sandbox', ...launchOptionsArgs, ], headless, } log.verbose('PuppetWeChatBridge', 'initBrowser() with options=%s', JSON.stringify(options)) let browser if (!this.options.stealthless) { /** * Puppeteer 4.0 * https://github.com/berstend/puppeteer-extra/issues/211#issuecomment-636283110 */ const plugin = stealthPlugin() plugin.onBrowser = () => {} puppeteerExtra.use(plugin) browser = await puppeteerExtra.launch(options) } else { browser = await puppeteer.launch(options) } const version = await browser.version() log.verbose('PuppetWeChatBridge', 'initBrowser() version: %s', version) return browser } public async onDialog (dialog: puppeteer.Dialog) { log.warn('PuppetWeChatBridge', 'onDialog() page.on(dialog) type:%s message:%s', dialog.type, dialog.message(), ) try { // XXX: Which ONE is better? await dialog.accept() // await dialog.dismiss() } catch (e) { log.error('PuppetWeChatBridge', 'onDialog() dialog.dismiss() reject: %s', e as Error) } this.emit('error', GError.from(`${dialog.type}(${dialog.message()})`)) } public async onLoad (page: puppeteer.Page): Promise<void> { log.verbose('PuppetWeChatBridge', 'onLoad() page.url=%s', page.url()) if (this.state.inactive()) { log.verbose('PuppetWeChatBridge', 'onLoad() OFF state detected. NOP') return // reject(new Error('onLoad() OFF state detected')) } try { const emitExist = await page.evaluate(() => { return typeof window['wechatyPuppetBridgeEmit'] === 'function' }) if (!emitExist) { /** * expose window['wechatyPuppetBridgeEmit'] at here. * enable wechaty-bro.js to emit message to bridge */ await page.exposeFunction('wechatyPuppetBridgeEmit', this.emit.bind(this)) } await this.readyAngular(page) await this.inject(page) await this.clickSwitchAccount(page) this.emit('ready') } catch (e) { log.error('PuppetWeChatBridge', 'onLoad() exception: %s', e as Error) await page.close() this.emit('error', e) } } public async initPage (browser: puppeteer.Browser): Promise<puppeteer.Page> { log.verbose('PuppetWeChatBridge', 'initPage()') // set this in time because the following callbacks // might be called before initPage() return. const page = this.page = await browser.newPage() /** * Can we support UOS with puppeteer? #127 * https://github.com/wechaty/wechaty-puppet-wechat/issues/127 */ if (this.options.uos) { await this.uosPatch(page) } page.on('error', e => this.emit('error', e)) page.on('dialog', this.wrapAsync(this.onDialog.bind(this))) const cookieList = ( await this.options.memory.get(MEMORY_SLOT) ) || [] as puppeteer.Protocol.Network.Cookie[] let url = this.entryUrl(cookieList) if (this.options.uos) { url = url + '?lang=zh_CN&target=t' } log.verbose('PuppetWeChatBridge', 'initPage() before page.goto(url)') // set timeout 60000 ms,30000ms always timeout page.setDefaultTimeout(60000) // Does this related to(?) the CI Error: exception: Navigation Timeout Exceeded: 30000ms exceeded await page.goto(url) log.verbose('PuppetWeChatBridge', 'initPage() after page.goto(url)') // await this.uosPatch(page) void this.uosPatch if (cookieList.length) { await page.setCookie(...cookieList) log.silly('PuppetWeChatBridge', 'initPage() page.setCookie() %s cookies set back', cookieList.length) } page.on('load', () => this.emit('load', page)) await page.reload() // reload page to make effect of the new cookie. return page } private async uosPatch (page: puppeteer.Page) { /** * Can we support UOS with puppeteer? #127 * https://github.com/wechaty/wechaty-puppet-wechat/issues/127 * * Credit: @luvletter2333 https://github.com/luvletter2333 */ const UOS_PATCH_CLIENT_VERSION = '2.0.0' const UOS_PATCH_EXTSPAM = this.options.uosExtSpam ?? 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==' const uosHeaders = { 'client-version' : UOS_PATCH_CLIENT_VERSION, extspam : UOS_PATCH_EXTSPAM, } // add RequestInterception await page.setRequestInterception(true) page.on('request', (req) => { const url = new URL(req.url()) if (url.pathname === '/cgi-bin/mmwebwx-bin/webwxnewloginpage') { const override = { headers: { ...req.headers(), ...uosHeaders, }, } this.wrapAsync(req.continue(override)) } else { this.wrapAsync(req.continue()) } }) } public async readyAngular (page: puppeteer.Page): Promise<void> { log.verbose('PuppetWeChatBridge', 'readyAngular()') try { await page.waitForFunction("typeof window.angular !== 'undefined'") } catch (e) { log.verbose('PuppetWeChatBridge', 'readyAngular() exception: %s', e as Error) const blockedMessage = await this.testBlockedMessage() if (blockedMessage) { // Wechat Account Blocked // TODO: advertise for puppet-padchat log.info('PuppetWeChatBridge', ` Please see: Account Login Issue <https://github.com/wechaty/wechaty/issues/872> `) throw new Error(blockedMessage) } else { throw e } } } public async inject (page: puppeteer.Page): Promise<void> { log.verbose('PuppetWeChatBridge', 'inject()') const WECHATY_BRO_JS_FILE = path.join( codeRoot, 'src', 'wechaty-bro.js', ) try { const sourceCode = fs.readFileSync(WECHATY_BRO_JS_FILE) .toString() let retObj = await page.evaluate(sourceCode) as undefined | InjectResult if (retObj && /^(2|3)/.test(retObj.code.toString())) { // HTTP Code 2XX & 3XX log.silly('PuppetWeChatBridge', 'inject() eval(Wechaty) return code[%d] message[%s]', retObj.code, retObj.message) } else { // HTTP Code 4XX & 5XX throw new Error('execute injectio error: ' + retObj?.code + ', ' + retObj?.message) } retObj = await this.proxyWechaty('init') if (retObj && /^(2|3)/.test(retObj.code.toString())) { // HTTP Code 2XX & 3XX log.silly('PuppetWeChatBridge', 'inject() Wechaty.init() return code[%d] message[%s]', retObj.code, retObj.message) } else { // HTTP Code 4XX & 5XX throw new Error('execute proxyWechaty(init) error: ' + retObj?.code + ', ' + retObj?.message) } const SUCCESS_CIPHER = 'ding() OK!' const future = new Promise(resolve => this.once('dong', resolve)) this.ding(SUCCESS_CIPHER) const r = await future if (r !== SUCCESS_CIPHER) { throw new Error('fail to get right return from call ding()') } log.silly('PuppetWeChatBridge', 'inject() ding success') } catch (e) { log.verbose('PuppetWeChatBridge', 'inject() exception: %s. stack: %s', (e as Error).message, (e as Error).stack) throw e } } public async logout (): Promise<any> { log.verbose('PuppetWeChatBridge', 'logout()') try { return await this.proxyWechaty('logout') } catch (e) { log.error('PuppetWeChatBridge', 'logout() exception: %s', (e as Error).message) throw e } } public async stop (): Promise<void> { log.verbose('PuppetWeChatBridge', 'stop()') if (!this.page) { throw new Error('no page') } if (!this.browser) { throw new Error('no browser') } this.state.inactive('pending') try { await this.page.close() log.silly('PuppetWeChatBridge', 'stop() page.close()-ed') } catch (e) { log.warn('PuppetWeChatBridge', 'stop() page.close() exception: %s', e as Error) } try { await this.browser.close() log.silly('PuppetWeChatBridge', 'stop() browser.close()-ed') } catch (e) { log.warn('PuppetWeChatBridge', 'stop() browser.close() exception: %s', e as Error) } this.state.inactive(true) } public async getUserName (): Promise<string> { log.verbose('PuppetWeChatBridge', 'getUserName()') try { const userName = await this.proxyWechaty('getUserName') return userName } catch (e) { log.error('PuppetWeChatBridge', 'getUserName() exception: %s', (e as Error).message) throw e } } public async contactAlias (contactId: string, alias: null | string): Promise<boolean> { try { return await this.proxyWechaty('contactRemark', contactId, alias) } catch (e) { log.verbose('PuppetWeChatBridge', 'contactRemark() exception: %s', (e as Error).message) // Issue #509 return false instead of throw when contact is not a friend. // throw e log.warn('PuppetWeChatBridge', 'contactRemark() does not work on contact is not a friend') return false } } public async contactList (): Promise<string[]> { try { return await this.proxyWechaty('contactList') } catch (e) { log.error('PuppetWeChatBridge', 'contactList() exception: %s', (e as Error).message) throw e } } public async roomList (): Promise<string[]> { try { return await this.proxyWechaty('roomList') } catch (e) { log.error('PuppetWeChatBridge', 'roomList() exception: %s', (e as Error).message) throw e } } public async roomDelMember ( roomId: string, contactId: string, ): Promise<number> { if (!roomId || !contactId) { throw new Error('no roomId or contactId') } try { return await this.proxyWechaty('roomDelMember', roomId, contactId) } catch (e) { log.error('PuppetWeChatBridge', 'roomDelMember(%s, %s) exception: %s', roomId, contactId, (e as Error).message) throw e } } public async roomAddMember ( roomId: string, contactId: string, ): Promise<number> { log.verbose('PuppetWeChatBridge', 'roomAddMember(%s, %s)', roomId, contactId) if (!roomId || !contactId) { throw new Error('no roomId or contactId') } try { return await this.proxyWechaty('roomAddMember', roomId, contactId) } catch (e) { log.error('PuppetWeChatBridge', 'roomAddMember(%s, %s) exception: %s', roomId, contactId, (e as Error).message) throw e } } public async roomModTopic ( roomId: string, topic: string, ): Promise<string> { if (!roomId) { throw new Error('no roomId') } try { await this.proxyWechaty('roomModTopic', roomId, topic) return topic } catch (e) { log.error('PuppetWeChatBridge', 'roomModTopic(%s, %s) exception: %s', roomId, topic, (e as Error).message) throw e } } public async roomCreate (contactIdList: string[], topic?: string): Promise<string> { if (!Array.isArray(contactIdList)) { throw new Error('no valid contactIdList') } try { const roomId = await this.proxyWechaty('roomCreate', contactIdList, topic) if (typeof roomId === 'object') { // It is a Error Object send back by callback in browser(WechatyBro) throw roomId } return roomId } catch (e) { log.error('PuppetWeChatBridge', 'roomCreate(%s) exception: %s', contactIdList, (e as Error).message) throw e } } public async verifyUserRequest ( contactId: string, hello: string, ): Promise<boolean> { log.verbose('PuppetWeChatBridge', 'verifyUserRequest(%s, %s)', contactId, hello) if (!contactId) { throw new Error('no valid contactId') } try { return await this.proxyWechaty('verifyUserRequest', contactId, hello) } catch (e) { log.error('PuppetWeChatBridge', 'verifyUserRequest(%s, %s) exception: %s', contactId, hello, (e as Error).message) throw e } } public async verifyUserOk ( contactId: string, ticket: string, ): Promise<boolean> { log.verbose('PuppetWeChatBridge', 'verifyUserOk(%s, %s)', contactId, ticket) if (!contactId || !ticket) { throw new Error('no valid contactId or ticket') } try { return await this.proxyWechaty('verifyUserOk', contactId, ticket) } catch (e) { log.error('PuppetWeChatBridge', 'verifyUserOk(%s, %s) exception: %s', contactId, ticket, (e as Error).message) throw e } } public async send ( toUserName: string, text: string, ): Promise<void> { log.verbose('PuppetWeChatBridge', 'send(%s, %s)', toUserName, text) if (!toUserName) { throw new Error('UserName not found') } if (!text) { throw new Error('cannot say nothing') } try { const ret = await this.proxyWechaty('send', toUserName, text) if (!ret) { throw new Error('send fail') } } catch (e) { log.error('PuppetWeChatBridge', 'send() exception: %s', (e as Error).message) throw e } } public async getMsgImg (id: string): Promise<string> { log.verbose('PuppetWeChatBridge', 'getMsgImg(%s)', id) try { return await this.proxyWechaty('getMsgImg', id) } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getMsgImg, %d) exception: %s', id, (e as Error).message) throw e } } public async getMsgEmoticon (id: string): Promise<string> { log.verbose('PuppetWeChatBridge', 'getMsgEmoticon(%s)', id) try { return await this.proxyWechaty('getMsgEmoticon', id) } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getMsgEmoticon, %d) exception: %s', id, (e as Error).message) throw e } } public async getMsgVideo (id: string): Promise<string> { log.verbose('PuppetWeChatBridge', 'getMsgVideo(%s)', id) try { return await this.proxyWechaty('getMsgVideo', id) } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getMsgVideo, %d) exception: %s', id, (e as Error).message) throw e } } public async getMsgVoice (id: string): Promise<string> { log.verbose('PuppetWeChatBridge', 'getMsgVoice(%s)', id) try { return await this.proxyWechaty('getMsgVoice', id) } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getMsgVoice, %d) exception: %s', id, (e as Error).message) throw e } } public async getMsgPublicLinkImg (id: string): Promise<string> { log.verbose('PuppetWeChatBridge', 'getMsgPublicLinkImg(%s)', id) try { return await this.proxyWechaty('getMsgPublicLinkImg', id) } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getMsgPublicLinkImg, %d) exception: %s', id, (e as Error).message) throw e } } public async getMessage (id: string): Promise<WebMessageRawPayload> { const doGet = async () => { const rawPayload = await this.proxyWechaty('getMessage', id) if (rawPayload && Object.keys(rawPayload).length > 0) { return rawPayload } throw new Error('doGet fail') } try { const rawPayload = await retryPolicy.execute(doGet) return rawPayload } catch (e) { log.error('PuppetWeChatBridge', 'getMessage() rejection: %s', (e as Error).message) throw e } } public async getContact (id: string): Promise<WebContactRawPayload | WebRoomRawPayload> { const doGet = async () => { const rawPayload = await this.proxyWechaty('getContact', id) if (rawPayload && Object.keys(rawPayload).length > 0) { return rawPayload } throw new Error('doGet fail') } try { const rawPayload = await retryPolicy.execute(doGet) return rawPayload } catch (e) { log.error('PuppetWeChatBridge', 'getContact() rejection: %s', (e as Error).message) throw e } } public async getBaseRequest (): Promise<string> { log.verbose('PuppetWeChatBridge', 'getBaseRequest()') try { return await this.proxyWechaty('getBaseRequest') } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getBaseRequest) exception: %s', (e as Error).message) throw e } } public async getPassticket (): Promise<string> { log.verbose('PuppetWeChatBridge', 'getPassticket()') try { return await this.proxyWechaty('getPassticket') } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getPassticket) exception: %s', (e as Error).message) throw e } } public async getCheckUploadUrl (): Promise<string> { log.verbose('PuppetWeChatBridge', 'getCheckUploadUrl()') try { return await this.proxyWechaty('getCheckUploadUrl') } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getCheckUploadUrl) exception: %s', (e as Error).message) throw e } } public async getUploadMediaUrl (): Promise<string> { log.verbose('PuppetWeChatBridge', 'getUploadMediaUrl()') try { return await this.proxyWechaty('getUploadMediaUrl') } catch (e) { log.silly('PuppetWeChatBridge', 'proxyWechaty(getUploadMediaUrl) exception: %s', (e as Error).message) throw e } } public async sendMedia (mediaData: WebMessageMediaPayload): Promise<boolean> { log.verbose('PuppetWeChatBridge', 'sendMedia(mediaData)') if (!mediaData.ToUserName) { throw new Error('UserName not found') } if (!mediaData.MediaId) { throw new Error('cannot say nothing') } try { return await this.proxyWechaty('sendMedia', mediaData) } catch (e) { log.error('PuppetWeChatBridge', 'sendMedia() exception: %s', (e as Error).message) throw e } } public async forward (baseData: WebMessageRawPayload, patchData: WebMessageRawPayload): Promise<boolean> { log.verbose('PuppetWeChatBridge', 'forward()') if (!baseData.ToUserName) { throw new Error('UserName not found') } if (!patchData.MMActualContent && !patchData.MMSendContent && !patchData.Content) { throw new Error('cannot say nothing') } try { return await this.proxyWechaty('forward', baseData, patchData) } catch (e) { log.error('PuppetWeChatBridge', 'forward() exception: %s', (e as Error).message) throw e } } /** * Proxy Call to Wechaty in Bridge */ public async proxyWechaty ( wechatyFunc : string, ...args : any[] ): Promise<any> { log.silly('PuppetWeChatBridge', 'proxyWechaty(%s%s)', wechatyFunc, args.length === 0 ? '' : ', ' + args.join(', '), ) if (!this.page) { throw new Error('no page') } try { const noWechaty = await this.page.evaluate(() => { return typeof WechatyBro === 'undefined' }) if (noWechaty) { const e = new Error('there is no WechatyBro in browser(yet)') throw e } } catch (e) { log.warn('PuppetWeChatBridge', 'proxyWechaty() noWechaty exception: %s', e as Error) throw e } const argsEncoded = Buffer.from( encodeURIComponent( JSON.stringify(args), ), ).toString('base64') // see: http://blog.sqrtthree.com/2015/08/29/utf8-to-b64/ const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))` const wechatyScript = ` WechatyBro .${wechatyFunc} .apply( undefined, ${argsDecoded}, ) `.replace(/[\n\s]+/, ' ') // log.silly('PuppetWeChatBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript) // console.log('proxyWechaty wechatyFunc args[0]: ') // console.log(args[0]) try { const ret = await this.page.evaluate(wechatyScript) return ret } catch (e) { log.verbose('PuppetWeChatBridge', 'proxyWechaty(%s, %s) ', wechatyFunc, args.join(', ')) log.warn('PuppetWeChatBridge', 'proxyWechaty() exception: %s', (e as Error).message) throw e } } public ding (data: any): void { log.verbose('PuppetWeChatBridge', 'ding(%s)', data || '') this.proxyWechaty('ding', data) .then(dongData => { return this.emit('dong', dongData) }) .catch(e => { log.error('PuppetWeChatBridge', 'ding(%s) exception: %s', data, (e as Error).message) this.emit('error', e) }) } public preHtmlToXml (text: string): string { log.verbose('PuppetWeChatBridge', 'preHtmlToXml()') const preRegex = /^<pre[^>]*>([^<]+)<\/pre>$/i const matches = text.match(preRegex) if (!matches) { return text } return unescapeHtml(matches[1]) } public async innerHTML (): Promise<string> { const html = await this.evaluate(() => { return window.document.body.innerHTML }) return html } /** * Throw if there's a blocked message */ public async testBlockedMessage (text?: string): Promise<string | false> { if (!text) { text = await this.innerHTML() } if (!text) { throw new Error('testBlockedMessage() no text found!') } const textSnip = text.substr(0, 50).replace(/\n/, '') log.verbose('PuppetWeChatBridge', 'testBlockedMessage(%s)', textSnip) interface BlockedMessage { error?: { ret : number, message : string, } } let obj: undefined | BlockedMessage try { // see unit test for detail const tryXmlText = this.preHtmlToXml(text) // obj = JSON.parse(toJson(tryXmlText)) obj = await new Promise((resolve, reject) => { parseString(tryXmlText, { explicitArray: false }, (err: any, result) => { if (err) { return reject(err) } return resolve(result) }) }) } catch (e) { log.warn('PuppetWeChatBridge', 'testBlockedMessage() toJson() exception: %s', e as Error) return false } if (!obj) { // FIXME: when will this happen? log.warn('PuppetWeChatBridge', 'testBlockedMessage() toJson(%s) return empty obj', textSnip) return false } if (!obj.error) { return false } const ret = +obj.error.ret const message = obj.error.message log.warn('PuppetWeChatBridge', 'testBlockedMessage() error.ret=%s', ret) if (ret === 1203) { // <error> // <ret>1203</ret> // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message> // </error> return message } return message // other error message // return new Promise<string | false>(resolve => { // parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => { // if (err) { // HTML can not be parsed to JSON // return resolve(false) // } // if (!obj) { // // FIXME: when will this happen? // log.warn('PuppetWeChatBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip) // return resolve(false) // } // if (!obj.error) { // return resolve(false) // } // const ret = +obj.error.ret // const message = obj.error.message // log.warn('PuppetWeChatBridge', 'testBlockedMessage() error.ret=%s', ret) // if (ret === 1203) { // // <error> // // <ret>1203</ret> // // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message> // // </error> // return resolve(message) // } // return resolve(message) // other error message // }) // }) } public async clickSwitchAccount (page: puppeteer.Page): Promise<boolean> { log.verbose('PuppetWeChatBridge', 'clickSwitchAccount()') // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553 // async function listXpath(thePage: Page, xpath: string): Promise<ElementHandle[]> { // log.verbose('PuppetWeChatBridge', 'clickSwitchAccount() listXpath()') // try { // const nodeHandleList = await (thePage as any).evaluateHandle(xpathInner => { // const nodeList: Node[] = [] // const query = document.evaluate(xpathInner, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null) // for (let i = 0, length = query.snapshotLength; i < length; ++i) { // nodeList.push(query.snapshotItem(i)) // } // return nodeList // }, xpath) // const properties = await nodeHandleList.getProperties() // const elementHandleList: ElementHandle[] = [] // const releasePromises: Promise<void>[] = [] // for (const property of properties.values()) { // const element = property.asElement() // if (element) // elementHandleList.push(element) // else // releasePromises.push(property.dispose()) // } // await Promise.all(releasePromises) // return elementHandleList // } catch (e) { // log.verbose('PuppetWeChatBridge', 'clickSwitchAccount() listXpath() exception: %s', e as Error) // return [] // } // } // TODO: use page.$x() (with puppeteer v1.1 or above) to replace DIY version of listXpath() instead. // See: https://github.com/GoogleChrome/puppeteer/blob/v1.1.0/docs/api.md#pagexexpression const XPATH_SELECTOR = "//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']" try { // const [button] = await listXpath(page, XPATH_SELECTOR) const [button] = await page.$x(XPATH_SELECTOR) if (button) { await button.click() log.silly('PuppetWeChatBridge', 'clickSwitchAccount() clicked!') return true } else { log.silly('PuppetWeChatBridge', 'clickSwitchAccount() button not found') return false } } catch (e) { log.silly('PuppetWeChatBridge', 'clickSwitchAccount() exception: %s', e as Error) throw e } } public async hostname (): Promise<string | null> { log.verbose('PuppetWeChatBridge', 'hostname()') if (!this.page) { throw new Error('no page') } try { const hostname = await this.page.evaluate(() => window.location.hostname) log.silly('PuppetWeChatBridge', 'hostname() got %s', hostname) return hostname } catch (e) { log.error('PuppetWeChatBridge', 'hostname() exception: %s', e as Error) this.emit('error', e) return null } } public async cookies (cookieList: Cookie[]): Promise<void> public async cookies (): Promise<Cookie[]> public async cookies (cookieList?: puppeteer.Protocol.Network.Cookie[]): Promise<void | puppeteer.Protocol.Network.Cookie[]> { if (!this.page) { throw new Error('no page') } if (cookieList) { try { await this.page.setCookie(...cookieList) } catch (e) { log.error('PuppetWeChatBridge', 'cookies(%s) reject: %s', cookieList, e as Error) this.emit('error', e) } // RETURN } else { cookieList = await this.page.cookies() return cookieList } } /** * name */ public entryUrl (cookieList?: puppeteer.Protocol.Network.Cookie[]): string { log.verbose('PuppetWeChatBridge', 'cookieDomain(%s)', cookieList) /** * `?target=t` is from https://github.com/wechaty/wechaty-puppet-wechat/pull/129 */ const DEFAULT_URL = 'https://wx.qq.com' if (!cookieList || cookieList.length === 0) { log.silly('PuppetWeChatBridge', 'cookieDomain() no cookie, return default %s', DEFAULT_URL) return DEFAULT_URL } const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name)) if (!wxCookieList.length) { log.silly('PuppetWeChatBridge', 'cookieDomain() no valid cookie, return default hostname') return DEFAULT_URL } let domain = wxCookieList[0]!.domain if (!domain) { log.silly('PuppetWeChatBridge', 'cookieDomain() no valid domain in cookies, return default hostname') return DEFAULT_URL } domain = domain.slice(1) if (domain === 'wechat.com') { domain = 'web.wechat.com' } let url if (/^http/.test(domain)) { url = domain } else { // Protocol error (Page.navigate): Cannot navigate to invalid URL undefined url = `https://${domain}` } log.silly('PuppetWeChatBridge', 'cookieDomain() got %s', url) return url } public async reload (): Promise<void> { log.verbose('PuppetWeChatBridge', 'reload()') if (!this.page) { throw new Error('no page') } await this.page.reload() } public async evaluate (fn: () => any, ...args: any[]): Promise<any> { log.silly('PuppetWeChatBridge', 'evaluate()') if (!this.page) { throw new Error('no page') } try { return await this.page.evaluate(fn, ...args) } catch (e) { log.error('PuppetWeChatBridge', 'evaluate() exception: %s', e as Error) this.emit('error', e) return null } } } export default Bridge