bridge.ts 28.0 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 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 46 47
import {
  PuppeteerRoomRawPayload,
}                               from './puppet-puppeteer'
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 60 61
export class Bridge extends EventEmitter {
  private browser : Browser
  private page    : 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
    this.state.off('pending')

281 282
    try {
      await this.page.close()
283
      log.silly('PuppetPuppeteerBridge', 'quit() page.close()-ed')
284
    } catch (e) {
285
      log.warn('PuppetPuppeteerBridge', 'quit() page.close() exception: %s', e)
286 287 288
    }

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

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

298
  public async getUserName(): Promise<string> {
299
    log.verbose('PuppetPuppeteerBridge', 'getUserName()')
300 301

    try {
302 303
      const userName = await this.proxyWechaty('getUserName')
      return userName
304
    } catch (e) {
305
      log.error('PuppetPuppeteerBridge', 'getUserName() exception: %s', e.message)
306 307
      throw e
    }
308 309
  }

310
  public async contactAlias(contactId: string, alias: string|null): Promise<boolean> {
311
    try {
312
      return await this.proxyWechaty('contactRemark', contactId, alias)
313
    } catch (e) {
314
      log.verbose('PuppetPuppeteerBridge', 'contactRemark() exception: %s', e.message)
315
      // Issue #509 return false instead of throw when contact is not a friend.
316
      // throw e
317
      log.warn('PuppetPuppeteerBridge', 'contactRemark() does not work on contact is not a friend')
318
      return false
319 320 321
    }
  }

322 323 324 325
  public async contactFind(filterFunc: string): Promise<string[]> {
    try {
      return await this.proxyWechaty('contactFind', filterFunc)
    } catch (e) {
326
      log.error('PuppetPuppeteerBridge', 'contactFind() exception: %s', e.message)
327 328
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
329 330
  }

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

340 341 342 343
  public async roomDelMember(
    roomId:     string,
    contactId:  string,
  ): Promise<number> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
344 345 346
    if (!roomId || !contactId) {
      throw new Error('no roomId or contactId')
    }
347 348 349
    try {
      return await this.proxyWechaty('roomDelMember', roomId, contactId)
    } catch (e) {
350
      log.error('PuppetPuppeteerBridge', 'roomDelMember(%s, %s) exception: %s', roomId, contactId, e.message)
351 352
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353 354
  }

355 356 357 358
  public async roomAddMember(
    roomId:     string,
    contactId:  string,
  ): Promise<number> {
359
    log.verbose('PuppetPuppeteerBridge', 'roomAddMember(%s, %s)', roomId, contactId)
360

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361 362 363
    if (!roomId || !contactId) {
      throw new Error('no roomId or contactId')
    }
364 365 366
    try {
      return await this.proxyWechaty('roomAddMember', roomId, contactId)
    } catch (e) {
367
      log.error('PuppetPuppeteerBridge', 'roomAddMember(%s, %s) exception: %s', roomId, contactId, e.message)
368 369
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
370 371
  }

372 373 374 375
  public async roomModTopic(
    roomId: string,
    topic:  string,
  ): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376 377 378
    if (!roomId) {
      throw new Error('no roomId')
    }
379 380 381 382
    try {
      await this.proxyWechaty('roomModTopic', roomId, topic)
      return topic
    } catch (e) {
383
      log.error('PuppetPuppeteerBridge', 'roomModTopic(%s, %s) exception: %s', roomId, topic, e.message)
384 385
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386 387
  }

388
  public async roomCreate(contactIdList: string[], topic?: string): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389 390 391 392
    if (!contactIdList || !Array.isArray(contactIdList)) {
      throw new Error('no valid contactIdList')
    }

393 394 395 396 397 398 399 400
    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) {
401
      log.error('PuppetPuppeteerBridge', 'roomCreate(%s) exception: %s', contactIdList, e.message)
402 403
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
404 405
  }

406 407 408 409
  public async verifyUserRequest(
    contactId:  string,
    hello:      string,
  ): Promise<boolean> {
410
    log.verbose('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s)', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411

412 413 414
    if (!contactId) {
      throw new Error('no valid contactId')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
415
    try {
416
      return await this.proxyWechaty('verifyUserRequest', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
    } catch (e) {
418
      log.error('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s) exception: %s', contactId, hello, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419 420
      throw e
    }
421 422
  }

423 424 425 426
  public async verifyUserOk(
    contactId:  string,
    ticket:     string,
  ): Promise<boolean> {
427
    log.verbose('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s)', contactId, ticket)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
428

429 430 431
    if (!contactId || !ticket) {
      throw new Error('no valid contactId or ticket')
    }
432 433 434
    try {
      return await this.proxyWechaty('verifyUserOk', contactId, ticket)
    } catch (e) {
435
      log.error('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s) exception: %s', contactId, ticket, e.message)
436 437
      throw e
    }
438 439
  }

440 441 442 443
  public async send(
    toUserName: string,
    text:       string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444 445
    log.verbose('PuppetPuppeteerBridge', 'send(%s, %s)', toUserName, text)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446 447 448
    if (!toUserName) {
      throw new Error('UserName not found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449
    if (!text) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
450 451 452
      throw new Error('cannot say nothing')
    }

453
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454
      const ret = await this.proxyWechaty('send', toUserName, text)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
455
      if (!ret) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
456 457
        throw new Error('send fail')
      }
458
    } catch (e) {
459
      log.error('PuppetPuppeteerBridge', 'send() exception: %s', e.message)
460 461
      throw e
    }
462
  }
463

464
  public async getMsgImg(id: string): Promise<string> {
465
    log.verbose('PuppetPuppeteerBridge', 'getMsgImg(%s)', id)
466

467 468 469
    try {
      return await this.proxyWechaty('getMsgImg', id)
    } catch (e) {
470
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgImg, %d) exception: %s', id, e.message)
471 472
      throw e
    }
473 474
  }

475
  public async getMsgEmoticon(id: string): Promise<string> {
476
    log.verbose('PuppetPuppeteerBridge', 'getMsgEmoticon(%s)', id)
477

478 479 480
    try {
      return await this.proxyWechaty('getMsgEmoticon', id)
    } catch (e) {
481
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgEmoticon, %d) exception: %s', id, e.message)
482 483
      throw e
    }
484 485
  }

486
  public async getMsgVideo(id: string): Promise<string> {
487
    log.verbose('PuppetPuppeteerBridge', 'getMsgVideo(%s)', id)
488 489 490 491

    try {
      return await this.proxyWechaty('getMsgVideo', id)
    } catch (e) {
492
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVideo, %d) exception: %s', id, e.message)
493
      throw e
494 495 496
    }
  }

497
  public async getMsgVoice(id: string): Promise<string> {
498
    log.verbose('PuppetPuppeteerBridge', 'getMsgVoice(%s)', id)
499 500 501 502

    try {
      return await this.proxyWechaty('getMsgVoice', id)
    } catch (e) {
503
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVoice, %d) exception: %s', id, e.message)
504 505
      throw e
    }
506
  }
507

508
  public async getMsgPublicLinkImg(id: string): Promise<string> {
509
    log.verbose('PuppetPuppeteerBridge', 'getMsgPublicLinkImg(%s)', id)
510 511 512 513

    try {
      return await this.proxyWechaty('getMsgPublicLinkImg', id)
    } catch (e) {
514
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgPublicLinkImg, %d) exception: %s', id, e.message)
515 516 517 518
      throw e
    }
  }

519 520 521 522 523
  public async getMessage(id: string): Promise<WebMessageRawPayload> {
    const rawPayload = await this.proxyWechaty('getMessage', id)
    return rawPayload
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
524
  public async getContact(id: string): Promise<WebContactRawPayload | PuppeteerRoomRawPayload> {
525 526
    const max = 35
    const backoff = 500
527

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
528
    // max = (2*totalTime/backoff) ^ (1/2)
529 530 531
    // timeout = 11,250 for {max: 15, backoff: 100}
    // timeout = 45,000 for {max: 30, backoff: 100}
    // timeout = 30,6250 for {max: 35, backoff: 500}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
532
    const timeout = max * (backoff * max) / 2
533

534
    try {
535 536
      return await retryPromise({ max: max, backoff: backoff }, async (attempt: number) => {
        log.silly('PuppetPuppeteerBridge', 'getContact() retryPromise: attampt %d/%d time for timeout %d',
537 538 539 540 541 542 543 544
                                      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) {
545
          log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getContact, %s) exception: %s', id, e.message)
546
          throw e
547
        }
548
      })
549
    } catch (e) {
550
      log.warn('PuppetPuppeteerBridge', 'retryPromise() getContact() finally FAIL: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
551
      throw e
552
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
553
    /////////////////////////////////
554 555
  }

M
Mukaiu 已提交
556
  public async getBaseRequest(): Promise<string> {
557
    log.verbose('PuppetPuppeteerBridge', 'getBaseRequest()')
M
Mukaiu 已提交
558 559 560 561

    try {
      return await this.proxyWechaty('getBaseRequest')
    } catch (e) {
562
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getBaseRequest) exception: %s', e.message)
M
Mukaiu 已提交
563 564 565 566 567
      throw e
    }
  }

  public async getPassticket(): Promise<string> {
568
    log.verbose('PuppetPuppeteerBridge', 'getPassticket()')
M
Mukaiu 已提交
569 570 571 572

    try {
      return await this.proxyWechaty('getPassticket')
    } catch (e) {
573
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getPassticket) exception: %s', e.message)
M
Mukaiu 已提交
574 575 576 577
      throw e
    }
  }

578
  public async getCheckUploadUrl(): Promise<string> {
579
    log.verbose('PuppetPuppeteerBridge', 'getCheckUploadUrl()')
580 581 582 583

    try {
      return await this.proxyWechaty('getCheckUploadUrl')
    } catch (e) {
584
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getCheckUploadUrl) exception: %s', e.message)
585 586 587 588
      throw e
    }
  }

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

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

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

603
    if (!mediaData.ToUserName) {
M
Mukaiu 已提交
604 605
      throw new Error('UserName not found')
    }
606
    if (!mediaData.MediaId) {
M
Mukaiu 已提交
607 608
      throw new Error('cannot say nothing')
    }
609 610 611
    try {
      return await this.proxyWechaty('sendMedia', mediaData)
    } catch (e) {
612
      log.error('PuppetPuppeteerBridge', 'sendMedia() exception: %s', e.message)
613 614
      throw e
    }
M
Mukaiu 已提交
615 616
  }

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

620 621 622 623 624 625
    if (!baseData.ToUserName) {
      throw new Error('UserName not found')
    }
    if (!patchData.MMActualContent && !patchData.MMSendContent && !patchData.Content) {
      throw new Error('cannot say nothing')
    }
626 627 628
    try {
      return await this.proxyWechaty('forward', baseData, patchData)
    } catch (e) {
629
      log.error('PuppetPuppeteerBridge', 'forward() exception: %s', e.message)
630 631
      throw e
    }
632 633
  }

634 635 636
  /**
   * Proxy Call to Wechaty in Bridge
   */
637 638
  public async proxyWechaty(
    wechatyFunc : string,
639
    ...args     : any[]
640
  ): Promise<any> {
641
    log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s%s)',
642 643 644 645
                                  wechatyFunc,
                                  args.length
                                  ? ' , ' + args.join(', ')
                                  : '',
646 647
              )

648
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
649 650 651
      const noWechaty = await this.page.evaluate(() => {
        return typeof WechatyBro === 'undefined'
      })
652 653 654 655 656
      if (noWechaty) {
        const e = new Error('there is no WechatyBro in browser(yet)')
        throw e
      }
    } catch (e) {
657
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() noWechaty exception: %s', e)
658 659 660
      throw e
    }

661 662
    const argsEncoded = new Buffer(
      encodeURIComponent(
L
lijiarui 已提交
663 664
        JSON.stringify(args),
      ),
665 666
    ).toString('base64')
    // see: http://blog.sqrtthree.com/2015/08/29/utf8-to-b64/
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
667
    const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))`
668

669 670 671 672 673 674 675
    const wechatyScript = `
      WechatyBro
        .${wechatyFunc}
        .apply(
          undefined,
          ${argsDecoded},
        )
676
    `.replace(/[\n\s]+/, ' ')
677
    // log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
678 679 680
    // console.log('proxyWechaty wechatyFunc args[0]: ')
    // console.log(args[0])

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
681
    try {
682
      const ret = await this.page.evaluate(wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
683 684
      return ret
    } catch (e) {
685 686
      log.verbose('PuppetPuppeteerBridge', 'proxyWechaty(%s, %s) ', wechatyFunc, args.join(', '))
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
687
      throw e
688
    }
689
  }
690

691
  public async ding(data: any): Promise<any> {
692
    log.verbose('PuppetPuppeteerBridge', 'ding(%s)', data)
693

694 695 696
    try {
      return await this.proxyWechaty('ding', data)
    } catch (e) {
697
      log.error('PuppetPuppeteerBridge', 'ding(%s) exception: %s', data, e.message)
698 699
      throw e
    }
700
  }
701

702
  public preHtmlToXml(text: string): string {
703
    log.verbose('PuppetPuppeteerBridge', 'preHtmlToXml()')
704 705 706 707 708 709 710 711 712

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

713 714 715 716 717 718 719
  public async innerHTML(): Promise<string> {
    const html = await this.evaluate(() => {
      return document.body.innerHTML
    })
    return html
  }

720
  /**
721
   * Throw if there's a blocked message
722
   */
723 724
  public async testBlockedMessage(text?: string): Promise<string | false> {
    if (!text) {
725
      text = await this.innerHTML()
726 727 728 729 730
    }
    if (!text) {
      throw new Error('testBlockedMessage() no text found!')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
731
    const textSnip = text.substr(0, 50).replace(/\n/, '')
732
    log.verbose('PuppetPuppeteerBridge', 'testBlockedMessage(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
733
                                  textSnip)
734

735
    // see unit test for detail
736
    const tryXmlText = this.preHtmlToXml(text)
737

738 739 740 741 742 743 744
    interface BlockedMessage {
      error?: {
        ret     : number,
        message : string,
      }
    }

745
    return new Promise<string | false>(resolve => {
746
      parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
747
        if (err) {  // HTML can not be parsed to JSON
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
748
          return resolve(false)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
749 750 751
        }
        if (!obj) {
          // FIXME: when will this happen?
752
          log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
753
          return resolve(false)
754 755
        }
        if (!obj.error) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
756
          return resolve(false)
757
        }
758
        const ret     = +obj.error.ret
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
759
        const message =  obj.error.message
760

761
        log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() error.ret=%s', ret)
762

763
        if (ret === 1203) {
764 765 766 767
          // <error>
          // <ret>1203</ret>
          // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message>
          // </error>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
768
          return resolve(message)
769
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
770
        return resolve(message) // other error message
771 772 773 774
      })
    })
  }

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

778
    // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809
    // 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 []
    //   }
    // }
810

811 812
    // 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
813 814

    const XPATH_SELECTOR = `//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']`
815
    try {
816 817
      // const [button] = await listXpath(page, XPATH_SELECTOR)
      const [button] = await page.$x(XPATH_SELECTOR)
818 819
      if (button) {
        await button.click()
820
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() clicked!')
821
        return true
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
822

823
      } else {
824
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() button not found')
825 826
        return false
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
827

828
    } catch (e) {
829
      log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() exception: %s', e)
830
      throw e
831
    }
832
  }
833

834
  public async hostname(): Promise<string | null> {
835
    log.verbose('PuppetPuppeteerBridge', 'hostname()')
836
    try {
837
      const hostname = await this.page.evaluate(() => location.hostname) as string
838
      log.silly('PuppetPuppeteerBridge', 'hostname() got %s', hostname)
839 840
      return hostname
    } catch (e) {
841
      log.error('PuppetPuppeteerBridge', 'hostname() exception: %s', e)
842 843 844
      this.emit('error', e)
      return null
    }
845
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
846

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

850 851 852 853 854
  public async cookies(cookieList?: Cookie[]): Promise<void | Cookie[]> {
    if (cookieList) {
      try {
        await this.page.setCookie(...cookieList)
      } catch (e) {
855
        log.error('PuppetPuppeteerBridge', 'cookies(%s) reject: %s', cookieList, e)
856 857 858 859 860 861 862 863 864
        this.emit('error', e)
      }
      return
    } else {
      // FIXME: puppeteer typing bug
      cookieList = await this.page.cookies() as any as Cookie[]
      return cookieList
    }
  }
865

866 867 868
  /**
   * name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
869
  public entryUrl(cookieList?: Cookie[]): string {
870
    log.verbose('PuppetPuppeteerBridge', 'cookieDomain(%s)', cookieList)
871

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

874
    if (!cookieList || cookieList.length === 0) {
875
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no cookie, return default %s', DEFAULT_URL)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
876
      return DEFAULT_URL
877
    }
878

879 880
    const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name))
    if (!wxCookieList.length) {
881
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid cookie, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
882
      return DEFAULT_URL
883 884 885
    }
    let domain = wxCookieList[0].domain
    if (!domain) {
886
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid domain in cookies, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
887
      return DEFAULT_URL
888
    }
889

890 891 892 893
    domain = domain.slice(1)
    if (domain === 'wechat.com') {
      domain = 'web.wechat.com'
    }
894

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
895
    let url
896
    if (/^http/.test(domain)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
897 898
      url = domain
    } else {
899
      // Protocol error (Page.navigate): Cannot navigate to invalid URL undefined
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
900
      url = `https://${domain}`
901
    }
902
    log.silly('PuppetPuppeteerBridge', 'cookieDomain() got %s', url)
903

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
904
    return url
905
  }
906

907
  public async reload(): Promise<void> {
908
    log.verbose('PuppetPuppeteerBridge', 'reload()')
909 910 911
    await this.page.reload()
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
912

913
  public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
914
    log.silly('PuppetPuppeteerBridge', 'evaluate()')
915 916 917
    try {
      return await this.page.evaluate(fn, ...args)
    } catch (e) {
918
      log.error('PuppetPuppeteerBridge', 'evaluate() exception: %s', e)
919 920 921
      this.emit('error', e)
      return null
    }
922 923
  }
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
924

925 926 927
export {
  Cookie,
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
928
export default Bridge