wechaty-puppet-wechat
Version:
Puppet WeChat for Wechaty
1,108 lines (944 loc) • 34.4 kB
text/typescript
/**
* 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