bridge.ts 28.1 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 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 | WebRoomRawPayload> {
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)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
642 643 644 645
                                        wechatyFunc,
                                        args.length === 0
                                          ? ''
                                          : ', ' + args.join(', '),
646
              )
647
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
648 649 650
      const noWechaty = await this.page.evaluate(() => {
        return typeof WechatyBro === 'undefined'
      })
651 652 653 654 655
      if (noWechaty) {
        const e = new Error('there is no WechatyBro in browser(yet)')
        throw e
      }
    } catch (e) {
656
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() noWechaty exception: %s', e)
657 658 659
      throw e
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

777
    // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
778 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
    // 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 []
    //   }
    // }
809

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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