bridge.ts 28.9 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5
 *
6 7 8
 *   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
9
 *
10 11 12 13 14 15 16
 *       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.
17 18
 *
 */
19
import { EventEmitter } from 'events'
20
import * as fs          from 'fs'
21 22 23 24 25 26 27 28 29
import * as path        from 'path'

import {
  Browser,
  Cookie,
  Dialog,
  launch,
  Page,
}                       from 'puppeteer'
30
import StateSwitch      from 'state-switch'
31 32 33
import { parseString }  from 'xml2js'

/* tslint:disable:no-var-requires */
34
// const retryPromise  = require('retry-promise').default
35

36
import { log }        from '../config'
37
import Profile        from '../profile'
38
import Misc           from '../misc'
39

40
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
41 42 43
  WebMessageMediaPayload,
  WebMessageRawPayload,
  WebContactRawPayload,
44
}                               from '../puppet-puppeteer/web-schemas'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
46
  WebRoomRawPayload,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
47
}                               from './web-schemas'
48 49 50 51 52 53 54 55 56

export interface InjectResult {
  code:    number,
  message: string,
}

export interface BridgeOptions {
  head?   : boolean,
  profile : Profile,
57 58
}

59
export class Bridge extends EventEmitter {
60 61
  private browser : undefined | Browser
  private page    : undefined | Page
62
  private state   : StateSwitch
63 64

  constructor(
65
    public options: BridgeOptions,
66
  ) {
67
    super()
68
    log.verbose('PuppetPuppeteerBridge', 'constructor()')
69

70
    this.state = new StateSwitch('PuppetPuppeteerBridge', log)
71 72
  }

73
  public async init(): Promise<void> {
74
    log.verbose('PuppetPuppeteerBridge', 'init()')
75

76
    this.state.on('pending')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
77
    try {
78
      this.browser = await this.initBrowser()
79
      log.verbose('PuppetPuppeteerBridge', 'init() initBrowser() done')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80

81 82
      this.on('load', this.onLoad.bind(this))

83
      const ready = new Promise(resolve => this.once('ready', resolve))
84
      this.page = await this.initPage(this.browser)
85
      await ready
86

87
      this.state.on(true)
88
      log.verbose('PuppetPuppeteerBridge', 'init() initPage() done')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
    } catch (e) {
90
      log.error('PuppetPuppeteerBridge', 'init() exception: %s', e)
91 92 93 94 95 96 97 98 99 100
      this.state.off(true)

      try {
        if (this.page) {
          await this.page.close()
        }
        if (this.browser) {
          await this.browser.close()
        }
      } catch (e2) {
101
        log.error('PuppetPuppeteerBridge', 'init() exception %s, close page/browser exception %s', e, e2)
102 103 104
      }

      this.emit('error', e)
105
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
    }
107 108
  }

109
  public async initBrowser(): Promise<Browser> {
110
    log.verbose('PuppetPuppeteerBridge', 'initBrowser()')
111

112 113 114 115
    const headless = this.options.head ? false : true
    const browser = await launch({
      headless,
      args: [
116 117 118 119
        '--audio-output-channels=0',
        '--disable-default-apps',
        '--disable-extensions',
        '--disable-translate',
120 121
        '--disable-gpu',
        '--disable-setuid-sandbox',
122 123 124
        '--disable-sync',
        '--hide-scrollbars',
        '--mute-audio',
125 126 127 128 129
        '--no-sandbox',
      ],
    })

    const version = await browser.version()
130
    log.verbose('PuppetPuppeteerBridge', 'initBrowser() version: %s', version)
131 132 133

    return browser
  }
134

135
  public async onDialog(dialog: Dialog) {
136
    log.warn('PuppetPuppeteerBridge', 'init() page.on(dialog) type:%s message:%s',
137 138 139 140 141 142
                                dialog.type, dialog.message())
    try {
      // XXX: Which ONE is better?
      await dialog.accept()
      // await dialog.dismiss()
    } catch (e) {
143
      log.error('PuppetPuppeteerBridge', 'init() dialog.dismiss() reject: %s', e)
144 145 146 147 148
    }
    this.emit('error', new Error(`${dialog.type}(${dialog.message()})`))
  }

  public async onLoad(page: Page): Promise<void> {
149
    log.verbose('PuppetPuppeteerBridge', 'initPage() on(load) %s', page.url())
150 151

    if (this.state.off()) {
152
      log.verbose('PuppetPuppeteerBridge', 'initPage() onLoad() OFF state detected. NOP')
153 154 155 156 157 158 159 160 161 162 163 164 165 166
      return // reject(new Error('onLoad() OFF state detected'))
    }

    try {
      const emitExist = await page.evaluate(() => {
        return typeof window['emit'] === 'function'
      })
      if (!emitExist) {
        await page.exposeFunction('emit', this.emit.bind(this))
      }

      await this.readyAngular(page)
      await this.inject(page)
      await this.clickSwitchAccount(page)
167 168 169

      this.emit('ready')

170
    } catch (e) {
171
      log.error('PuppetPuppeteerBridge', 'init() initPage() onLoad() exception: %s', e)
172
      await page.close()
173 174 175 176
      this.emit('error', e)
    }
  }

177
  public async initPage(browser: Browser): Promise<Page> {
178
    log.verbose('PuppetPuppeteerBridge', 'initPage()')
179

180 181
    // set this in time because the following callbacks
    // might be called before initPage() return.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
182
    const page = this.page =  await browser.newPage()
183

184
    page.on('error',  e => this.emit('error', e))
185

186
    page.on('dialog', this.onDialog.bind(this))
187

188
    const cookieList = (await this.options.profile.get('cookies')) as Cookie[]
189 190
    const url        = this.entryUrl(cookieList)

191
    log.verbose('PuppetPuppeteerBridge', 'initPage() before page.goto(url)')
192
    await page.goto(url) // Does this related to(?) the CI Error: exception: Navigation Timeout Exceeded: 30000ms exceeded
193
    log.verbose('PuppetPuppeteerBridge', 'initPage() after page.goto(url)')
194 195 196

    if (cookieList && cookieList.length) {
      await page.setCookie(...cookieList)
197
      log.silly('PuppetPuppeteerBridge', 'initPage() page.setCookie() %s cookies set back', cookieList.length)
198
    }
199

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
200
    page.on('load', () => this.emit('load', page))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
    await page.reload() // reload page to make effect of the new cookie.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202

203 204 205 206
    return page
  }

  public async readyAngular(page: Page): Promise<void> {
207
    log.verbose('PuppetPuppeteerBridge', 'readyAngular()')
208

209
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210
      await page.waitForFunction(`typeof window.angular !== 'undefined'`)
211
    } catch (e) {
212
      log.verbose('PuppetPuppeteerBridge', 'readyAngular() exception: %s', e)
213

214 215 216 217 218 219 220
      const blockedMessage = await this.testBlockedMessage()
      if (blockedMessage) {  // Wechat Account Blocked
        throw new Error(blockedMessage)
      } else {
        throw e
      }
    }
221 222 223
  }

  public async inject(page: Page): Promise<void> {
224
    log.verbose('PuppetPuppeteerBridge', 'inject()')
225

226 227 228 229 230
    const WECHATY_BRO_JS_FILE = path.join(
      __dirname,
      'wechaty-bro.js',
    )

231
    try {
232 233 234 235
      const sourceCode = fs.readFileSync(WECHATY_BRO_JS_FILE)
                            .toString()

      let retObj = await page.evaluate(sourceCode) as any as InjectResult
236 237 238

      if (retObj && /^(2|3)/.test(retObj.code.toString())) {
        // HTTP Code 2XX & 3XX
239
        log.silly('PuppetPuppeteerBridge', 'inject() eval(Wechaty) return code[%d] message[%s]',
240 241
                                      retObj.code, retObj.message)
      } else {  // HTTP Code 4XX & 5XX
242
        throw new Error('execute injectio error: ' + retObj.code + ', ' + retObj.message)
243 244
      }

245
      retObj = await this.proxyWechaty('init')
246 247
      if (retObj && /^(2|3)/.test(retObj.code.toString())) {
        // HTTP Code 2XX & 3XX
248
        log.silly('PuppetPuppeteerBridge', 'inject() Wechaty.init() return code[%d] message[%s]',
249 250
                                      retObj.code, retObj.message)
      } else {  // HTTP Code 4XX & 5XX
251
        throw new Error('execute proxyWechaty(init) error: ' + retObj.code + ', ' + retObj.message)
252 253
      }

254
      const SUCCESS_CIPHER = 'ding() OK!'
255 256
      const r = await this.ding(SUCCESS_CIPHER)
      if (r !== SUCCESS_CIPHER) {
257 258
        throw new Error('fail to get right return from call ding()')
      }
259
      log.silly('PuppetPuppeteerBridge', 'inject() ding success')
260

261
    } catch (e) {
262
      log.verbose('PuppetPuppeteerBridge', 'inject() exception: %s. stack: %s', e.message, e.stack)
263
      throw e
264
    }
265
  }
266

267
  public async logout(): Promise<any> {
268
    log.verbose('PuppetPuppeteerBridge', 'logout()')
269 270 271
    try {
      return await this.proxyWechaty('logout')
    } catch (e) {
272
      log.error('PuppetPuppeteerBridge', 'logout() exception: %s', e.message)
273
      throw e
274
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
275
  }
276

277
  public async quit(): Promise<void> {
278
    log.verbose('PuppetPuppeteerBridge', 'quit()')
279 280 281 282 283 284 285 286

    if (!this.page) {
      throw new Error('no page')
    }
    if (!this.browser) {
      throw new Error('no browser')
    }

287 288
    this.state.off('pending')

289 290
    try {
      await this.page.close()
291
      log.silly('PuppetPuppeteerBridge', 'quit() page.close()-ed')
292
    } catch (e) {
293
      log.warn('PuppetPuppeteerBridge', 'quit() page.close() exception: %s', e)
294 295 296
    }

    try {
297
      await this.browser.close()
298
      log.silly('PuppetPuppeteerBridge', 'quit() browser.close()-ed')
299
    } catch (e) {
300
      log.warn('PuppetPuppeteerBridge', 'quit() browser.close() exception: %s', e)
301
    }
302 303

    this.state.off(true)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
  }
305

306
  public async getUserName(): Promise<string> {
307
    log.verbose('PuppetPuppeteerBridge', 'getUserName()')
308 309

    try {
310 311
      const userName = await this.proxyWechaty('getUserName')
      return userName
312
    } catch (e) {
313
      log.error('PuppetPuppeteerBridge', 'getUserName() exception: %s', e.message)
314 315
      throw e
    }
316 317
  }

318
  public async contactAlias(contactId: string, alias: string|null): Promise<boolean> {
319
    try {
320
      return await this.proxyWechaty('contactRemark', contactId, alias)
321
    } catch (e) {
322
      log.verbose('PuppetPuppeteerBridge', 'contactRemark() exception: %s', e.message)
323
      // Issue #509 return false instead of throw when contact is not a friend.
324
      // throw e
325
      log.warn('PuppetPuppeteerBridge', 'contactRemark() does not work on contact is not a friend')
326
      return false
327 328 329
    }
  }

330 331 332 333
  public async contactFind(filterFunc: string): Promise<string[]> {
    try {
      return await this.proxyWechaty('contactFind', filterFunc)
    } catch (e) {
334
      log.error('PuppetPuppeteerBridge', 'contactFind() exception: %s', e.message)
335 336
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
337 338
  }

339 340 341 342
  public async roomFind(filterFunc: string): Promise<string[]> {
    try {
      return await this.proxyWechaty('roomFind', filterFunc)
    } catch (e) {
343
      log.error('PuppetPuppeteerBridge', 'roomFind() exception: %s', e.message)
344 345
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
346 347
  }

348 349 350 351
  public async roomDelMember(
    roomId:     string,
    contactId:  string,
  ): Promise<number> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
352 353 354
    if (!roomId || !contactId) {
      throw new Error('no roomId or contactId')
    }
355 356 357
    try {
      return await this.proxyWechaty('roomDelMember', roomId, contactId)
    } catch (e) {
358
      log.error('PuppetPuppeteerBridge', 'roomDelMember(%s, %s) exception: %s', roomId, contactId, e.message)
359 360
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361 362
  }

363 364 365 366
  public async roomAddMember(
    roomId:     string,
    contactId:  string,
  ): Promise<number> {
367
    log.verbose('PuppetPuppeteerBridge', 'roomAddMember(%s, %s)', roomId, contactId)
368

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
369 370 371
    if (!roomId || !contactId) {
      throw new Error('no roomId or contactId')
    }
372 373 374
    try {
      return await this.proxyWechaty('roomAddMember', roomId, contactId)
    } catch (e) {
375
      log.error('PuppetPuppeteerBridge', 'roomAddMember(%s, %s) exception: %s', roomId, contactId, e.message)
376 377
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
378 379
  }

380 381 382 383
  public async roomModTopic(
    roomId: string,
    topic:  string,
  ): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
384 385 386
    if (!roomId) {
      throw new Error('no roomId')
    }
387 388 389 390
    try {
      await this.proxyWechaty('roomModTopic', roomId, topic)
      return topic
    } catch (e) {
391
      log.error('PuppetPuppeteerBridge', 'roomModTopic(%s, %s) exception: %s', roomId, topic, e.message)
392 393
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
394 395
  }

396
  public async roomCreate(contactIdList: string[], topic?: string): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
397 398 399 400
    if (!contactIdList || !Array.isArray(contactIdList)) {
      throw new Error('no valid contactIdList')
    }

401 402 403 404 405 406 407 408
    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) {
409
      log.error('PuppetPuppeteerBridge', 'roomCreate(%s) exception: %s', contactIdList, e.message)
410 411
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
412 413
  }

414 415 416 417
  public async verifyUserRequest(
    contactId:  string,
    hello:      string,
  ): Promise<boolean> {
418
    log.verbose('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s)', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419

420 421 422
    if (!contactId) {
      throw new Error('no valid contactId')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
423
    try {
424
      return await this.proxyWechaty('verifyUserRequest', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
425
    } catch (e) {
426
      log.error('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s) exception: %s', contactId, hello, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427 428
      throw e
    }
429 430
  }

431 432 433 434
  public async verifyUserOk(
    contactId:  string,
    ticket:     string,
  ): Promise<boolean> {
435
    log.verbose('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s)', contactId, ticket)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
436

437 438 439
    if (!contactId || !ticket) {
      throw new Error('no valid contactId or ticket')
    }
440 441 442
    try {
      return await this.proxyWechaty('verifyUserOk', contactId, ticket)
    } catch (e) {
443
      log.error('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s) exception: %s', contactId, ticket, e.message)
444 445
      throw e
    }
446 447
  }

448 449 450 451
  public async send(
    toUserName: string,
    text:       string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452 453
    log.verbose('PuppetPuppeteerBridge', 'send(%s, %s)', toUserName, text)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454 455 456
    if (!toUserName) {
      throw new Error('UserName not found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457
    if (!text) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458 459 460
      throw new Error('cannot say nothing')
    }

461
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
462
      const ret = await this.proxyWechaty('send', toUserName, text)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463
      if (!ret) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
464 465
        throw new Error('send fail')
      }
466
    } catch (e) {
467
      log.error('PuppetPuppeteerBridge', 'send() exception: %s', e.message)
468 469
      throw e
    }
470
  }
471

472
  public async getMsgImg(id: string): Promise<string> {
473
    log.verbose('PuppetPuppeteerBridge', 'getMsgImg(%s)', id)
474

475 476 477
    try {
      return await this.proxyWechaty('getMsgImg', id)
    } catch (e) {
478
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgImg, %d) exception: %s', id, e.message)
479 480
      throw e
    }
481 482
  }

483
  public async getMsgEmoticon(id: string): Promise<string> {
484
    log.verbose('PuppetPuppeteerBridge', 'getMsgEmoticon(%s)', id)
485

486 487 488
    try {
      return await this.proxyWechaty('getMsgEmoticon', id)
    } catch (e) {
489
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgEmoticon, %d) exception: %s', id, e.message)
490 491
      throw e
    }
492 493
  }

494
  public async getMsgVideo(id: string): Promise<string> {
495
    log.verbose('PuppetPuppeteerBridge', 'getMsgVideo(%s)', id)
496 497 498 499

    try {
      return await this.proxyWechaty('getMsgVideo', id)
    } catch (e) {
500
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVideo, %d) exception: %s', id, e.message)
501
      throw e
502 503 504
    }
  }

505
  public async getMsgVoice(id: string): Promise<string> {
506
    log.verbose('PuppetPuppeteerBridge', 'getMsgVoice(%s)', id)
507 508 509 510

    try {
      return await this.proxyWechaty('getMsgVoice', id)
    } catch (e) {
511
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVoice, %d) exception: %s', id, e.message)
512 513
      throw e
    }
514
  }
515

516
  public async getMsgPublicLinkImg(id: string): Promise<string> {
517
    log.verbose('PuppetPuppeteerBridge', 'getMsgPublicLinkImg(%s)', id)
518 519 520 521

    try {
      return await this.proxyWechaty('getMsgPublicLinkImg', id)
    } catch (e) {
522
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgPublicLinkImg, %d) exception: %s', id, e.message)
523 524 525 526
      throw e
    }
  }

527 528 529 530 531
  public async getMessage(id: string): Promise<WebMessageRawPayload> {
    const rawPayload = await this.proxyWechaty('getMessage', id)
    return rawPayload
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
532
  public async getContact(id: string): Promise<WebContactRawPayload | WebRoomRawPayload> {
533 534
    // const max = 35
    // const backoff = 500
535

536
    // const timeout = max * (backoff * max) / 2
537

538
    try {
539 540 541 542
      return await Misc.retry(async (retry, attempt) => {
        log.silly('PuppetPuppeteerBridge', 'getContact() promiseRetry: attempt %d',
                                            attempt,
                  )
543 544 545 546 547 548 549
        try {
          const r = await this.proxyWechaty('getContact', id)
          if (r) {
            return r
          }
          throw new Error('got empty return value at attempt: ' + attempt)
        } catch (e) {
550
          log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getContact, %s) exception: %s', id, e.message)
551
          retry(e)
552
        }
553
      })
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568

      // return await retryPromise({ max: max, backoff: backoff }, async (attempt: number) => {
      //   log.silly('PuppetPuppeteerBridge', 'getContact() retryPromise: attampt %d/%d time for timeout %d',
      //                                 attempt, max, timeout)
      //   try {
      //     const r = await this.proxyWechaty('getContact', id)
      //     if (r) {
      //       return r
      //     }
      //     throw new Error('got empty return value at attempt: ' + attempt)
      //   } catch (e) {
      //     log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getContact, %s) exception: %s', id, e.message)
      //     throw e
      //   }
      // })
569
    } catch (e) {
570
      log.warn('PuppetPuppeteerBridge', 'promiseRetry() getContact() finally FAIL: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
571
      throw e
572
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
573
    /////////////////////////////////
574 575
  }

M
Mukaiu 已提交
576
  public async getBaseRequest(): Promise<string> {
577
    log.verbose('PuppetPuppeteerBridge', 'getBaseRequest()')
M
Mukaiu 已提交
578 579 580 581

    try {
      return await this.proxyWechaty('getBaseRequest')
    } catch (e) {
582
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getBaseRequest) exception: %s', e.message)
M
Mukaiu 已提交
583 584 585 586 587
      throw e
    }
  }

  public async getPassticket(): Promise<string> {
588
    log.verbose('PuppetPuppeteerBridge', 'getPassticket()')
M
Mukaiu 已提交
589 590 591 592

    try {
      return await this.proxyWechaty('getPassticket')
    } catch (e) {
593
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getPassticket) exception: %s', e.message)
M
Mukaiu 已提交
594 595 596 597
      throw e
    }
  }

598
  public async getCheckUploadUrl(): Promise<string> {
599
    log.verbose('PuppetPuppeteerBridge', 'getCheckUploadUrl()')
600 601 602 603

    try {
      return await this.proxyWechaty('getCheckUploadUrl')
    } catch (e) {
604
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getCheckUploadUrl) exception: %s', e.message)
605 606 607 608
      throw e
    }
  }

M
Mukaiu 已提交
609
  public async getUploadMediaUrl(): Promise<string> {
610
    log.verbose('PuppetPuppeteerBridge', 'getUploadMediaUrl()')
M
Mukaiu 已提交
611 612 613 614

    try {
      return await this.proxyWechaty('getUploadMediaUrl')
    } catch (e) {
615
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getUploadMediaUrl) exception: %s', e.message)
M
Mukaiu 已提交
616 617 618 619
      throw e
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
620
  public async sendMedia(mediaData: WebMessageMediaPayload): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
621 622
    log.verbose('PuppetPuppeteerBridge', 'sendMedia(mediaData)')

623
    if (!mediaData.ToUserName) {
M
Mukaiu 已提交
624 625
      throw new Error('UserName not found')
    }
626
    if (!mediaData.MediaId) {
M
Mukaiu 已提交
627 628
      throw new Error('cannot say nothing')
    }
629 630 631
    try {
      return await this.proxyWechaty('sendMedia', mediaData)
    } catch (e) {
632
      log.error('PuppetPuppeteerBridge', 'sendMedia() exception: %s', e.message)
633 634
      throw e
    }
M
Mukaiu 已提交
635 636
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
637
  public async forward(baseData: WebMessageRawPayload, patchData: WebMessageRawPayload): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
638 639
    log.verbose('PuppetPuppeteerBridge', 'forward()')

640 641 642 643 644 645
    if (!baseData.ToUserName) {
      throw new Error('UserName not found')
    }
    if (!patchData.MMActualContent && !patchData.MMSendContent && !patchData.Content) {
      throw new Error('cannot say nothing')
    }
646 647 648
    try {
      return await this.proxyWechaty('forward', baseData, patchData)
    } catch (e) {
649
      log.error('PuppetPuppeteerBridge', 'forward() exception: %s', e.message)
650 651
      throw e
    }
652 653
  }

654 655 656
  /**
   * Proxy Call to Wechaty in Bridge
   */
657 658
  public async proxyWechaty(
    wechatyFunc : string,
659
    ...args     : any[]
660
  ): Promise<any> {
661
    log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
662 663 664 665
                                        wechatyFunc,
                                        args.length === 0
                                          ? ''
                                          : ', ' + args.join(', '),
666
              )
667 668 669 670 671

    if (!this.page) {
      throw new Error('no page')
    }

672
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
673 674 675
      const noWechaty = await this.page.evaluate(() => {
        return typeof WechatyBro === 'undefined'
      })
676 677 678 679 680
      if (noWechaty) {
        const e = new Error('there is no WechatyBro in browser(yet)')
        throw e
      }
    } catch (e) {
681
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() noWechaty exception: %s', e)
682 683 684
      throw e
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
685
    const argsEncoded = Buffer.from(
686
      encodeURIComponent(
L
lijiarui 已提交
687 688
        JSON.stringify(args),
      ),
689 690
    ).toString('base64')
    // see: http://blog.sqrtthree.com/2015/08/29/utf8-to-b64/
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
691
    const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))`
692

693 694 695 696 697 698 699
    const wechatyScript = `
      WechatyBro
        .${wechatyFunc}
        .apply(
          undefined,
          ${argsDecoded},
        )
700
    `.replace(/[\n\s]+/, ' ')
701
    // log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
702 703 704
    // console.log('proxyWechaty wechatyFunc args[0]: ')
    // console.log(args[0])

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
705
    try {
706
      const ret = await this.page.evaluate(wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
707 708
      return ret
    } catch (e) {
709 710
      log.verbose('PuppetPuppeteerBridge', 'proxyWechaty(%s, %s) ', wechatyFunc, args.join(', '))
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
711
      throw e
712
    }
713
  }
714

715
  public async ding(data: any): Promise<any> {
716
    log.verbose('PuppetPuppeteerBridge', 'ding(%s)', data)
717

718 719 720
    try {
      return await this.proxyWechaty('ding', data)
    } catch (e) {
721
      log.error('PuppetPuppeteerBridge', 'ding(%s) exception: %s', data, e.message)
722 723
      throw e
    }
724
  }
725

726
  public preHtmlToXml(text: string): string {
727
    log.verbose('PuppetPuppeteerBridge', 'preHtmlToXml()')
728 729 730 731 732 733 734 735 736

    const preRegex = /^<pre[^>]*>([^<]+)<\/pre>$/i
    const matches = text.match(preRegex)
    if (!matches) {
      return text
    }
    return Misc.unescapeHtml(matches[1])
  }

737 738 739 740 741 742 743
  public async innerHTML(): Promise<string> {
    const html = await this.evaluate(() => {
      return document.body.innerHTML
    })
    return html
  }

744
  /**
745
   * Throw if there's a blocked message
746
   */
747 748
  public async testBlockedMessage(text?: string): Promise<string | false> {
    if (!text) {
749
      text = await this.innerHTML()
750 751 752 753 754
    }
    if (!text) {
      throw new Error('testBlockedMessage() no text found!')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
755
    const textSnip = text.substr(0, 50).replace(/\n/, '')
756
    log.verbose('PuppetPuppeteerBridge', 'testBlockedMessage(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
757
                                  textSnip)
758

759
    // see unit test for detail
760
    const tryXmlText = this.preHtmlToXml(text)
761

762 763 764 765 766 767 768
    interface BlockedMessage {
      error?: {
        ret     : number,
        message : string,
      }
    }

769
    return new Promise<string | false>(resolve => {
770
      parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
771
        if (err) {  // HTML can not be parsed to JSON
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
772
          return resolve(false)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
773 774 775
        }
        if (!obj) {
          // FIXME: when will this happen?
776
          log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
777
          return resolve(false)
778 779
        }
        if (!obj.error) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
780
          return resolve(false)
781
        }
782
        const ret     = +obj.error.ret
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
783
        const message =  obj.error.message
784

785
        log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() error.ret=%s', ret)
786

787
        if (ret === 1203) {
788 789 790 791
          // <error>
          // <ret>1203</ret>
          // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message>
          // </error>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
792
          return resolve(message)
793
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
794
        return resolve(message) // other error message
795 796 797 798
      })
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
799
  public async clickSwitchAccount(page: Page): Promise<boolean> {
800
    log.verbose('PuppetPuppeteerBridge', 'clickSwitchAccount()')
801

802
    // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
    // async function listXpath(thePage: Page, xpath: string): Promise<ElementHandle[]> {
    //   log.verbose('PuppetPuppeteerBridge', '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('PuppetPuppeteerBridge', 'clickSwitchAccount() listXpath() exception: %s', e)
    //     return []
    //   }
    // }
834

835 836
    // 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
837 838

    const XPATH_SELECTOR = `//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']`
839
    try {
840 841
      // const [button] = await listXpath(page, XPATH_SELECTOR)
      const [button] = await page.$x(XPATH_SELECTOR)
842 843
      if (button) {
        await button.click()
844
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() clicked!')
845
        return true
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
846

847
      } else {
848
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() button not found')
849 850
        return false
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
851

852
    } catch (e) {
853
      log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() exception: %s', e)
854
      throw e
855
    }
856
  }
857

858
  public async hostname(): Promise<string | null> {
859
    log.verbose('PuppetPuppeteerBridge', 'hostname()')
860 861 862 863 864

    if (!this.page) {
      throw new Error('no page')
    }

865
    try {
866
      const hostname = await this.page.evaluate(() => location.hostname) as string
867
      log.silly('PuppetPuppeteerBridge', 'hostname() got %s', hostname)
868 869
      return hostname
    } catch (e) {
870
      log.error('PuppetPuppeteerBridge', 'hostname() exception: %s', e)
871 872 873
      this.emit('error', e)
      return null
    }
874
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
875

876 877
  public async cookies(cookieList: Cookie[]): Promise<void>
  public async cookies(): Promise<Cookie[]>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
878

879
  public async cookies(cookieList?: Cookie[]): Promise<void | Cookie[]> {
880 881 882 883
    if (!this.page) {
      throw new Error('no page')
    }

884 885 886 887
    if (cookieList) {
      try {
        await this.page.setCookie(...cookieList)
      } catch (e) {
888
        log.error('PuppetPuppeteerBridge', 'cookies(%s) reject: %s', cookieList, e)
889 890 891 892 893 894 895 896 897
        this.emit('error', e)
      }
      return
    } else {
      // FIXME: puppeteer typing bug
      cookieList = await this.page.cookies() as any as Cookie[]
      return cookieList
    }
  }
898

899 900 901
  /**
   * name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
902
  public entryUrl(cookieList?: Cookie[]): string {
903
    log.verbose('PuppetPuppeteerBridge', 'cookieDomain(%s)', cookieList)
904

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
905
    const DEFAULT_URL = 'https://wx.qq.com'
906

907
    if (!cookieList || cookieList.length === 0) {
908
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no cookie, return default %s', DEFAULT_URL)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
909
      return DEFAULT_URL
910
    }
911

912 913
    const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name))
    if (!wxCookieList.length) {
914
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid cookie, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
915
      return DEFAULT_URL
916 917 918
    }
    let domain = wxCookieList[0].domain
    if (!domain) {
919
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid domain in cookies, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
920
      return DEFAULT_URL
921
    }
922

923 924 925 926
    domain = domain.slice(1)
    if (domain === 'wechat.com') {
      domain = 'web.wechat.com'
    }
927

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
928
    let url
929
    if (/^http/.test(domain)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
930 931
      url = domain
    } else {
932
      // Protocol error (Page.navigate): Cannot navigate to invalid URL undefined
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
933
      url = `https://${domain}`
934
    }
935
    log.silly('PuppetPuppeteerBridge', 'cookieDomain() got %s', url)
936

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
937
    return url
938
  }
939

940
  public async reload(): Promise<void> {
941
    log.verbose('PuppetPuppeteerBridge', 'reload()')
942 943 944 945 946

    if (!this.page) {
      throw new Error('no page')
    }

947 948 949
    await this.page.reload()
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
950

951
  public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
952
    log.silly('PuppetPuppeteerBridge', 'evaluate()')
953 954 955 956 957

    if (!this.page) {
      throw new Error('no page')
    }

958 959 960
    try {
      return await this.page.evaluate(fn, ...args)
    } catch (e) {
961
      log.error('PuppetPuppeteerBridge', 'evaluate() exception: %s', e)
962 963 964
      this.emit('error', e)
      return null
    }
965 966
  }
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
967

968 969 970
export {
  Cookie,
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
971
export default Bridge