bridge.ts 28.5 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
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
46
  WebRoomRawPayload,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
47
}                               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
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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
536
    // max = (2*totalTime/backoff) ^ (1/2)
537 538 539
    // 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 (李卓桓) 已提交
540
    const timeout = max * (backoff * max) / 2
541

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

M
Mukaiu 已提交
564
  public async getBaseRequest(): Promise<string> {
565
    log.verbose('PuppetPuppeteerBridge', 'getBaseRequest()')
M
Mukaiu 已提交
566 567 568 569

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

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

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

586
  public async getCheckUploadUrl(): Promise<string> {
587
    log.verbose('PuppetPuppeteerBridge', 'getCheckUploadUrl()')
588 589 590 591

    try {
      return await this.proxyWechaty('getCheckUploadUrl')
    } catch (e) {
592
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getCheckUploadUrl) exception: %s', e.message)
593 594 595 596
      throw e
    }
  }

M
Mukaiu 已提交
597
  public async getUploadMediaUrl(): Promise<string> {
598
    log.verbose('PuppetPuppeteerBridge', 'getUploadMediaUrl()')
M
Mukaiu 已提交
599 600 601 602

    try {
      return await this.proxyWechaty('getUploadMediaUrl')
    } catch (e) {
603
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getUploadMediaUrl) exception: %s', e.message)
M
Mukaiu 已提交
604 605 606 607
      throw e
    }
  }

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

611
    if (!mediaData.ToUserName) {
M
Mukaiu 已提交
612 613
      throw new Error('UserName not found')
    }
614
    if (!mediaData.MediaId) {
M
Mukaiu 已提交
615 616
      throw new Error('cannot say nothing')
    }
617 618 619
    try {
      return await this.proxyWechaty('sendMedia', mediaData)
    } catch (e) {
620
      log.error('PuppetPuppeteerBridge', 'sendMedia() exception: %s', e.message)
621 622
      throw e
    }
M
Mukaiu 已提交
623 624
  }

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

628 629 630 631 632 633
    if (!baseData.ToUserName) {
      throw new Error('UserName not found')
    }
    if (!patchData.MMActualContent && !patchData.MMSendContent && !patchData.Content) {
      throw new Error('cannot say nothing')
    }
634 635 636
    try {
      return await this.proxyWechaty('forward', baseData, patchData)
    } catch (e) {
637
      log.error('PuppetPuppeteerBridge', 'forward() exception: %s', e.message)
638 639
      throw e
    }
640 641
  }

642 643 644
  /**
   * Proxy Call to Wechaty in Bridge
   */
645 646
  public async proxyWechaty(
    wechatyFunc : string,
647
    ...args     : any[]
648
  ): Promise<any> {
649
    log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
650 651 652 653
                                        wechatyFunc,
                                        args.length === 0
                                          ? ''
                                          : ', ' + args.join(', '),
654
              )
655 656 657 658 659

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

660
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
661 662 663
      const noWechaty = await this.page.evaluate(() => {
        return typeof WechatyBro === 'undefined'
      })
664 665 666 667 668
      if (noWechaty) {
        const e = new Error('there is no WechatyBro in browser(yet)')
        throw e
      }
    } catch (e) {
669
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() noWechaty exception: %s', e)
670 671 672
      throw e
    }

673 674
    const argsEncoded = new Buffer(
      encodeURIComponent(
L
lijiarui 已提交
675 676
        JSON.stringify(args),
      ),
677 678
    ).toString('base64')
    // see: http://blog.sqrtthree.com/2015/08/29/utf8-to-b64/
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
679
    const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))`
680

681 682 683 684 685 686 687
    const wechatyScript = `
      WechatyBro
        .${wechatyFunc}
        .apply(
          undefined,
          ${argsDecoded},
        )
688
    `.replace(/[\n\s]+/, ' ')
689
    // log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
690 691 692
    // console.log('proxyWechaty wechatyFunc args[0]: ')
    // console.log(args[0])

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
693
    try {
694
      const ret = await this.page.evaluate(wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
695 696
      return ret
    } catch (e) {
697 698
      log.verbose('PuppetPuppeteerBridge', 'proxyWechaty(%s, %s) ', wechatyFunc, args.join(', '))
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
699
      throw e
700
    }
701
  }
702

703
  public async ding(data: any): Promise<any> {
704
    log.verbose('PuppetPuppeteerBridge', 'ding(%s)', data)
705

706 707 708
    try {
      return await this.proxyWechaty('ding', data)
    } catch (e) {
709
      log.error('PuppetPuppeteerBridge', 'ding(%s) exception: %s', data, e.message)
710 711
      throw e
    }
712
  }
713

714
  public preHtmlToXml(text: string): string {
715
    log.verbose('PuppetPuppeteerBridge', 'preHtmlToXml()')
716 717 718 719 720 721 722 723 724

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

725 726 727 728 729 730 731
  public async innerHTML(): Promise<string> {
    const html = await this.evaluate(() => {
      return document.body.innerHTML
    })
    return html
  }

732
  /**
733
   * Throw if there's a blocked message
734
   */
735 736
  public async testBlockedMessage(text?: string): Promise<string | false> {
    if (!text) {
737
      text = await this.innerHTML()
738 739 740 741 742
    }
    if (!text) {
      throw new Error('testBlockedMessage() no text found!')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
743
    const textSnip = text.substr(0, 50).replace(/\n/, '')
744
    log.verbose('PuppetPuppeteerBridge', 'testBlockedMessage(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
745
                                  textSnip)
746

747
    // see unit test for detail
748
    const tryXmlText = this.preHtmlToXml(text)
749

750 751 752 753 754 755 756
    interface BlockedMessage {
      error?: {
        ret     : number,
        message : string,
      }
    }

757
    return new Promise<string | false>(resolve => {
758
      parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
759
        if (err) {  // HTML can not be parsed to JSON
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
760
          return resolve(false)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
761 762 763
        }
        if (!obj) {
          // FIXME: when will this happen?
764
          log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
765
          return resolve(false)
766 767
        }
        if (!obj.error) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
768
          return resolve(false)
769
        }
770
        const ret     = +obj.error.ret
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
771
        const message =  obj.error.message
772

773
        log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() error.ret=%s', ret)
774

775
        if (ret === 1203) {
776 777 778 779
          // <error>
          // <ret>1203</ret>
          // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message>
          // </error>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
780
          return resolve(message)
781
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
782
        return resolve(message) // other error message
783 784 785 786
      })
    })
  }

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

790
    // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
    // 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 []
    //   }
    // }
822

823 824
    // 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
825 826

    const XPATH_SELECTOR = `//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']`
827
    try {
828 829
      // const [button] = await listXpath(page, XPATH_SELECTOR)
      const [button] = await page.$x(XPATH_SELECTOR)
830 831
      if (button) {
        await button.click()
832
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() clicked!')
833
        return true
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
834

835
      } else {
836
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() button not found')
837 838
        return false
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
839

840
    } catch (e) {
841
      log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() exception: %s', e)
842
      throw e
843
    }
844
  }
845

846
  public async hostname(): Promise<string | null> {
847
    log.verbose('PuppetPuppeteerBridge', 'hostname()')
848 849 850 851 852

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

853
    try {
854
      const hostname = await this.page.evaluate(() => location.hostname) as string
855
      log.silly('PuppetPuppeteerBridge', 'hostname() got %s', hostname)
856 857
      return hostname
    } catch (e) {
858
      log.error('PuppetPuppeteerBridge', 'hostname() exception: %s', e)
859 860 861
      this.emit('error', e)
      return null
    }
862
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
863

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

867
  public async cookies(cookieList?: Cookie[]): Promise<void | Cookie[]> {
868 869 870 871
    if (!this.page) {
      throw new Error('no page')
    }

872 873 874 875
    if (cookieList) {
      try {
        await this.page.setCookie(...cookieList)
      } catch (e) {
876
        log.error('PuppetPuppeteerBridge', 'cookies(%s) reject: %s', cookieList, e)
877 878 879 880 881 882 883 884 885
        this.emit('error', e)
      }
      return
    } else {
      // FIXME: puppeteer typing bug
      cookieList = await this.page.cookies() as any as Cookie[]
      return cookieList
    }
  }
886

887 888 889
  /**
   * name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
890
  public entryUrl(cookieList?: Cookie[]): string {
891
    log.verbose('PuppetPuppeteerBridge', 'cookieDomain(%s)', cookieList)
892

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

895
    if (!cookieList || cookieList.length === 0) {
896
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no cookie, return default %s', DEFAULT_URL)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
897
      return DEFAULT_URL
898
    }
899

900 901
    const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name))
    if (!wxCookieList.length) {
902
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid cookie, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
903
      return DEFAULT_URL
904 905 906
    }
    let domain = wxCookieList[0].domain
    if (!domain) {
907
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid domain in cookies, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
908
      return DEFAULT_URL
909
    }
910

911 912 913 914
    domain = domain.slice(1)
    if (domain === 'wechat.com') {
      domain = 'web.wechat.com'
    }
915

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
916
    let url
917
    if (/^http/.test(domain)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
918 919
      url = domain
    } else {
920
      // Protocol error (Page.navigate): Cannot navigate to invalid URL undefined
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
921
      url = `https://${domain}`
922
    }
923
    log.silly('PuppetPuppeteerBridge', 'cookieDomain() got %s', url)
924

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
925
    return url
926
  }
927

928
  public async reload(): Promise<void> {
929
    log.verbose('PuppetPuppeteerBridge', 'reload()')
930 931 932 933 934

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

935 936 937
    await this.page.reload()
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
938

939
  public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
940
    log.silly('PuppetPuppeteerBridge', 'evaluate()')
941 942 943 944 945

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

946 947 948
    try {
      return await this.page.evaluate(fn, ...args)
    } catch (e) {
949
      log.error('PuppetPuppeteerBridge', 'evaluate() exception: %s', e)
950 951 952
      this.emit('error', e)
      return null
    }
953 954
  }
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
955

956 957 958
export {
  Cookie,
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
959
export default Bridge