bridge.ts 27.4 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
import * as path        from 'path'

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

/* tslint:disable:no-var-requires */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35
const retryPromise  = require('retry-promise').default
36

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

41 42 43
import {
  MediaData,
  MsgRawObj,
44 45
}                                 from './schema'
import { PuppeteerContactRawObj } from './puppeteer-contact'
46 47 48 49 50 51 52 53 54

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

export interface BridgeOptions {
  head?   : boolean,
  profile : Profile,
55 56
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57 58
declare const WechatyBro

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

352
  public async roomAddMember(roomId, contactId): Promise<number> {
353
    log.verbose('PuppetPuppeteerBridge', 'roomAddMember(%s, %s)', roomId, contactId)
354

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
355 356 357
    if (!roomId || !contactId) {
      throw new Error('no roomId or contactId')
    }
358 359 360
    try {
      return await this.proxyWechaty('roomAddMember', roomId, contactId)
    } catch (e) {
361
      log.error('PuppetPuppeteerBridge', 'roomAddMember(%s, %s) exception: %s', roomId, contactId, e.message)
362 363
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364 365
  }

366
  public async roomModTopic(roomId, topic): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367 368 369
    if (!roomId) {
      throw new Error('no roomId')
    }
370 371 372 373
    try {
      await this.proxyWechaty('roomModTopic', roomId, topic)
      return topic
    } catch (e) {
374
      log.error('PuppetPuppeteerBridge', 'roomModTopic(%s, %s) exception: %s', roomId, topic, e.message)
375 376
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377 378
  }

379
  public async roomCreate(contactIdList: string[], topic?: string): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
380 381 382 383
    if (!contactIdList || !Array.isArray(contactIdList)) {
      throw new Error('no valid contactIdList')
    }

384 385 386 387 388 389 390 391
    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) {
392
      log.error('PuppetPuppeteerBridge', 'roomCreate(%s) exception: %s', contactIdList, e.message)
393 394
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
395 396
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
397
  public async verifyUserRequest(contactId, hello): Promise<boolean> {
398
    log.verbose('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s)', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399

400 401 402
    if (!contactId) {
      throw new Error('no valid contactId')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403
    try {
404
      return await this.proxyWechaty('verifyUserRequest', contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405
    } catch (e) {
406
      log.error('PuppetPuppeteerBridge', 'verifyUserRequest(%s, %s) exception: %s', contactId, hello, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407 408
      throw e
    }
409 410
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411
  public async verifyUserOk(contactId, ticket): Promise<boolean> {
412
    log.verbose('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s)', contactId, ticket)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
413

414 415 416
    if (!contactId || !ticket) {
      throw new Error('no valid contactId or ticket')
    }
417 418 419
    try {
      return await this.proxyWechaty('verifyUserOk', contactId, ticket)
    } catch (e) {
420
      log.error('PuppetPuppeteerBridge', 'verifyUserOk(%s, %s) exception: %s', contactId, ticket, e.message)
421 422
      throw e
    }
423 424
  }

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
425
  public async send(toUserName: string, content: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426 427 428 429 430 431 432
    if (!toUserName) {
      throw new Error('UserName not found')
    }
    if (!content) {
      throw new Error('cannot say nothing')
    }

433
    try {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
434
      const ret = await this.proxyWechaty('send', toUserName, content)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
435
      if (!ret) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
436 437
        throw new Error('send fail')
      }
438
    } catch (e) {
439
      log.error('PuppetPuppeteerBridge', 'send() exception: %s', e.message)
440 441
      throw e
    }
442
  }
443

444
  public async getMsgImg(id): Promise<string> {
445
    log.verbose('PuppetPuppeteerBridge', 'getMsgImg(%s)', id)
446

447 448 449
    try {
      return await this.proxyWechaty('getMsgImg', id)
    } catch (e) {
450
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgImg, %d) exception: %s', id, e.message)
451 452
      throw e
    }
453 454
  }

455
  public async getMsgEmoticon(id): Promise<string> {
456
    log.verbose('PuppetPuppeteerBridge', 'getMsgEmoticon(%s)', id)
457

458 459 460
    try {
      return await this.proxyWechaty('getMsgEmoticon', id)
    } catch (e) {
461
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgEmoticon, %d) exception: %s', id, e.message)
462 463
      throw e
    }
464 465 466
  }

  public async getMsgVideo(id): Promise<string> {
467
    log.verbose('PuppetPuppeteerBridge', 'getMsgVideo(%s)', id)
468 469 470 471

    try {
      return await this.proxyWechaty('getMsgVideo', id)
    } catch (e) {
472
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVideo, %d) exception: %s', id, e.message)
473
      throw e
474 475 476 477
    }
  }

  public async getMsgVoice(id): Promise<string> {
478
    log.verbose('PuppetPuppeteerBridge', 'getMsgVoice(%s)', id)
479 480 481 482

    try {
      return await this.proxyWechaty('getMsgVoice', id)
    } catch (e) {
483
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getMsgVoice, %d) exception: %s', id, e.message)
484 485
      throw e
    }
486
  }
487

488
  public async getMsgPublicLinkImg(id): Promise<string> {
489
    log.verbose('PuppetPuppeteerBridge', 'getMsgPublicLinkImg(%s)', id)
490 491 492 493

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

499
  public async getContact(id: string): Promise<PuppeteerContactRawObj> {
500
    if (id !== id) { // NaN
501
      const err = new Error('NaN! where does it come from?')
502
      log.error('PuppetPuppeteerBridge', 'getContact(NaN): %s', err)
503
      throw err
504
    }
505 506
    const max = 35
    const backoff = 500
507

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
508
    // max = (2*totalTime/backoff) ^ (1/2)
509 510 511
    // 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 (李卓桓) 已提交
512
    const timeout = max * (backoff * max) / 2
513

514 515
    try {
      return await retryPromise({ max: max, backoff: backoff }, async attempt => {
516
        log.silly('PuppetPuppeteerBridge', 'getContact() retryPromise: attampt %s/%s time for timeout %s',
517 518 519 520 521 522 523 524
                                      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) {
525
          log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getContact, %s) exception: %s', id, e.message)
526
          throw e
527
        }
528
      })
529
    } catch (e) {
530
      log.warn('PuppetPuppeteerBridge', 'retryPromise() getContact() finally FAIL: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531
      throw e
532
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
533
    /////////////////////////////////
534 535
  }

M
Mukaiu 已提交
536
  public async getBaseRequest(): Promise<string> {
537
    log.verbose('PuppetPuppeteerBridge', 'getBaseRequest()')
M
Mukaiu 已提交
538 539 540 541

    try {
      return await this.proxyWechaty('getBaseRequest')
    } catch (e) {
542
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getBaseRequest) exception: %s', e.message)
M
Mukaiu 已提交
543 544 545 546 547
      throw e
    }
  }

  public async getPassticket(): Promise<string> {
548
    log.verbose('PuppetPuppeteerBridge', 'getPassticket()')
M
Mukaiu 已提交
549 550 551 552

    try {
      return await this.proxyWechaty('getPassticket')
    } catch (e) {
553
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getPassticket) exception: %s', e.message)
M
Mukaiu 已提交
554 555 556 557
      throw e
    }
  }

558
  public async getCheckUploadUrl(): Promise<string> {
559
    log.verbose('PuppetPuppeteerBridge', 'getCheckUploadUrl()')
560 561 562 563

    try {
      return await this.proxyWechaty('getCheckUploadUrl')
    } catch (e) {
564
      log.silly('PuppetPuppeteerBridge', 'proxyWechaty(getCheckUploadUrl) exception: %s', e.message)
565 566 567 568
      throw e
    }
  }

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

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

580
  public async sendMedia(mediaData: MediaData): Promise<boolean> {
581
    if (!mediaData.ToUserName) {
M
Mukaiu 已提交
582 583
      throw new Error('UserName not found')
    }
584
    if (!mediaData.MediaId) {
M
Mukaiu 已提交
585 586
      throw new Error('cannot say nothing')
    }
587 588 589
    try {
      return await this.proxyWechaty('sendMedia', mediaData)
    } catch (e) {
590
      log.error('PuppetPuppeteerBridge', 'sendMedia() exception: %s', e.message)
591 592
      throw e
    }
M
Mukaiu 已提交
593 594
  }

595
  public async forward(baseData: MsgRawObj, patchData: MsgRawObj): Promise<boolean> {
596 597 598 599 600 601
    if (!baseData.ToUserName) {
      throw new Error('UserName not found')
    }
    if (!patchData.MMActualContent && !patchData.MMSendContent && !patchData.Content) {
      throw new Error('cannot say nothing')
    }
602 603 604
    try {
      return await this.proxyWechaty('forward', baseData, patchData)
    } catch (e) {
605
      log.error('PuppetPuppeteerBridge', 'forward() exception: %s', e.message)
606 607
      throw e
    }
608 609
  }

610 611 612
  /**
   * Proxy Call to Wechaty in Bridge
   */
613 614 615 616
  public async proxyWechaty(
    wechatyFunc : string,
    ...args     : any[],
  ): Promise<any> {
617
    log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s%s)',
618 619 620 621
                                  wechatyFunc,
                                  args.length
                                  ? ' , ' + args.join(', ')
                                  : '',
622 623
              )

624
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
625 626 627
      const noWechaty = await this.page.evaluate(() => {
        return typeof WechatyBro === 'undefined'
      })
628 629 630 631 632
      if (noWechaty) {
        const e = new Error('there is no WechatyBro in browser(yet)')
        throw e
      }
    } catch (e) {
633
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() noWechaty exception: %s', e)
634 635 636
      throw e
    }

637 638
    const argsEncoded = new Buffer(
      encodeURIComponent(
L
lijiarui 已提交
639 640
        JSON.stringify(args),
      ),
641 642
    ).toString('base64')
    // see: http://blog.sqrtthree.com/2015/08/29/utf8-to-b64/
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
643
    const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))`
644

645 646 647 648 649 650 651
    const wechatyScript = `
      WechatyBro
        .${wechatyFunc}
        .apply(
          undefined,
          ${argsDecoded},
        )
652
    `.replace(/[\n\s]+/, ' ')
653
    // log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
654 655 656
    // console.log('proxyWechaty wechatyFunc args[0]: ')
    // console.log(args[0])

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
657
    try {
658
      const ret = await this.page.evaluate(wechatyScript)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
659 660
      return ret
    } catch (e) {
661 662
      log.verbose('PuppetPuppeteerBridge', 'proxyWechaty(%s, %s) ', wechatyFunc, args.join(', '))
      log.warn('PuppetPuppeteerBridge', 'proxyWechaty() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
663
      throw e
664
    }
665
  }
666

667
  public async ding(data): Promise<any> {
668
    log.verbose('PuppetPuppeteerBridge', 'ding(%s)', data)
669

670 671 672
    try {
      return await this.proxyWechaty('ding', data)
    } catch (e) {
673
      log.error('PuppetPuppeteerBridge', 'ding(%s) exception: %s', data, e.message)
674 675
      throw e
    }
676
  }
677

678
  public preHtmlToXml(text: string): string {
679
    log.verbose('PuppetPuppeteerBridge', 'preHtmlToXml()')
680 681 682 683 684 685 686 687 688

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

689 690 691 692 693 694 695
  public async innerHTML(): Promise<string> {
    const html = await this.evaluate(() => {
      return document.body.innerHTML
    })
    return html
  }

696
  /**
697
   * Throw if there's a blocked message
698
   */
699 700
  public async testBlockedMessage(text?: string): Promise<string | false> {
    if (!text) {
701
      text = await this.innerHTML()
702 703 704 705 706
    }
    if (!text) {
      throw new Error('testBlockedMessage() no text found!')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
707
    const textSnip = text.substr(0, 50).replace(/\n/, '')
708
    log.verbose('PuppetPuppeteerBridge', 'testBlockedMessage(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
709
                                  textSnip)
710

711
    // see unit test for detail
712
    const tryXmlText = this.preHtmlToXml(text)
713

714 715 716 717 718 719 720
    interface BlockedMessage {
      error?: {
        ret     : number,
        message : string,
      }
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
721
    return new Promise<string | false>((resolve, reject) => {
722
      parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
723
        if (err) {  // HTML can not be parsed to JSON
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
724
          return resolve(false)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
725 726 727
        }
        if (!obj) {
          // FIXME: when will this happen?
728
          log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
729
          return resolve(false)
730 731
        }
        if (!obj.error) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
732
          return resolve(false)
733
        }
734
        const ret     = +obj.error.ret
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
735
        const message =  obj.error.message
736

737
        log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() error.ret=%s', ret)
738

739
        if (ret === 1203) {
740 741 742 743
          // <error>
          // <ret>1203</ret>
          // <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message>
          // </error>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
744
          return resolve(message)
745
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
746
        return resolve(message) // other error message
747 748 749 750
      })
    })
  }

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

754 755 756
    // 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

757
    // https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
758
    async function listXpath(thePage: Page, xpath: string): Promise<ElementHandle[]> {
759
      log.verbose('PuppetPuppeteerBridge', 'clickSwitchAccount() listXpath()')
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780

      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())
781
        }
782 783 784
        await Promise.all(releasePromises)
        return elementHandleList
      } catch (e) {
785
        log.verbose('PuppetPuppeteerBridge', 'clickSwitchAccount() listXpath() exception: %s', e)
786
        return []
787 788 789 790
      }
    }

    const XPATH_SELECTOR = `//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']`
791
    try {
792 793 794
      const [button] = await listXpath(page, XPATH_SELECTOR)
      if (button) {
        await button.click()
795
        log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() clicked!')
796
        return true
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
797

798
      } else {
799
        // log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() button not found')
800 801
        return false
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
802

803
    } catch (e) {
804
      log.silly('PuppetPuppeteerBridge', 'clickSwitchAccount() exception: %s', e)
805
      throw e
806
    }
807
  }
808

809
  public async hostname(): Promise<string | null> {
810
    log.verbose('PuppetPuppeteerBridge', 'hostname()')
811
    try {
812
      const hostname = await this.page.evaluate(() => location.hostname) as string
813
      log.silly('PuppetPuppeteerBridge', 'hostname() got %s', hostname)
814 815
      return hostname
    } catch (e) {
816
      log.error('PuppetPuppeteerBridge', 'hostname() exception: %s', e)
817 818 819
      this.emit('error', e)
      return null
    }
820
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
821

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

825 826 827 828 829
  public async cookies(cookieList?: Cookie[]): Promise<void | Cookie[]> {
    if (cookieList) {
      try {
        await this.page.setCookie(...cookieList)
      } catch (e) {
830
        log.error('PuppetPuppeteerBridge', 'cookies(%s) reject: %s', cookieList, e)
831 832 833 834 835 836 837 838 839
        this.emit('error', e)
      }
      return
    } else {
      // FIXME: puppeteer typing bug
      cookieList = await this.page.cookies() as any as Cookie[]
      return cookieList
    }
  }
840

841 842 843
  /**
   * name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
844
  public entryUrl(cookieList?: Cookie[]): string {
845
    log.verbose('PuppetPuppeteerBridge', 'cookieDomain(%s)', cookieList)
846

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

849
    if (!cookieList || cookieList.length === 0) {
850
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no cookie, return default %s', DEFAULT_URL)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
851
      return DEFAULT_URL
852
    }
853

854 855
    const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name))
    if (!wxCookieList.length) {
856
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid cookie, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
857
      return DEFAULT_URL
858 859 860
    }
    let domain = wxCookieList[0].domain
    if (!domain) {
861
      log.silly('PuppetPuppeteerBridge', 'cookieDomain() no valid domain in cookies, return default hostname')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
862
      return DEFAULT_URL
863
    }
864

865 866 867 868
    domain = domain.slice(1)
    if (domain === 'wechat.com') {
      domain = 'web.wechat.com'
    }
869

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
870 871 872 873
    let url
    if (/^http/.test(url)) {
      url = domain
    } else {
874
      // Protocol error (Page.navigate): Cannot navigate to invalid URL undefined
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
875
      url = `https://${domain}`
876
    }
877
    log.silly('PuppetPuppeteerBridge', 'cookieDomain() got %s', url)
878

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
879
    return url
880
  }
881

882
  public async reload(): Promise<void> {
883
    log.verbose('PuppetPuppeteerBridge', 'reload()')
884 885 886
    await this.page.reload()
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
887

888
  public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
889
    log.silly('PuppetPuppeteerBridge', 'evaluate()')
890 891 892
    try {
      return await this.page.evaluate(fn, ...args)
    } catch (e) {
893
      log.error('PuppetPuppeteerBridge', 'evaluate() exception: %s', e)
894 895 896
      this.emit('error', e)
      return null
    }
897 898
  }
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
899

900 901 902
export {
  Cookie,
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
903
export default Bridge