bridge.ts 27.9 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5
 *
6 7 8
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
9
 *
10 11 12 13 14 15 16
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
17 18
 *
 */
19
import { EventEmitter } from 'events'
20
import * as fs          from 'fs'
21 22 23 24 25 26 27 28 29
import * as path        from 'path'

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

/* tslint:disable:no-var-requires */
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
  MsgMediaPayload,
  MsgRawPayload,
43 44
}                                 from './schema'
import { PuppeteerContactRawObj } from './puppeteer-contact'
45 46 47 48 49 50 51 52 53

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

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

56 57 58
export class Bridge extends EventEmitter {
  private browser : Browser
  private page    : Page
59
  private state   : StateSwitch
60 61

  constructor(
62
    public options: BridgeOptions,
63
  ) {
64
    super()
65
    log.verbose('PuppetPuppeteerBridge', 'constructor()')
66

67
    this.state = new StateSwitch('PuppetPuppeteerBridge', log)
68 69
  }

70
  public async init(): Promise<void> {
71
    log.verbose('PuppetPuppeteerBridge', 'init()')
72

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

78 79
      this.on('load', this.onLoad.bind(this))

80
      const ready = new Promise(resolve => this.once('ready', resolve))
81
      this.page = await this.initPage(this.browser)
82
      await ready
83

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

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

      this.emit('error', e)
102
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103
    }
104 105
  }

106
  public async initBrowser(): Promise<Browser> {
107
    log.verbose('PuppetPuppeteerBridge', 'initBrowser()')
108

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

    const version = await browser.version()
127
    log.verbose('PuppetPuppeteerBridge', 'initBrowser() version: %s', version)
128 129 130

    return browser
  }
131

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

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

    if (this.state.off()) {
149
      log.verbose('PuppetPuppeteerBridge', 'initPage() onLoad() OFF state detected. NOP')
150 151 152 153 154 155 156 157 158 159 160 161 162 163
      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)
164 165 166

      this.emit('ready')

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

174
  public async initPage(browser: Browser): Promise<Page> {
175
    log.verbose('PuppetPuppeteerBridge', 'initPage()')
176

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

181
    page.on('error',  e => this.emit('error', e))
182

183
    page.on('dialog', this.onDialog.bind(this))
184

185
    const cookieList = (await this.options.profile.get('cookies')) as Cookie[]
186 187
    const url        = this.entryUrl(cookieList)

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

    if (cookieList && cookieList.length) {
      await page.setCookie(...cookieList)
194
      log.silly('PuppetPuppeteerBridge', 'initPage() page.setCookie() %s cookies set back', cookieList.length)
195
    }
196

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

200 201 202 203
    return page
  }

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

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

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

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

223 224 225 226 227
    const WECHATY_BRO_JS_FILE = path.join(
      __dirname,
      'wechaty-bro.js',
    )

228
    try {
229 230 231 232
      const sourceCode = fs.readFileSync(WECHATY_BRO_JS_FILE)
                            .toString()

      let retObj = await page.evaluate(sourceCode) as any as InjectResult
233 234 235

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

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

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

258
    } catch (e) {
259
      log.verbose('PuppetPuppeteerBridge', 'inject() exception: %s. stack: %s', e.message, e.stack)
260
      throw e
261
    }
262
  }
263

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

274
  public async quit(): Promise<void> {
275
    log.verbose('PuppetPuppeteerBridge', 'quit()')
276 277
    this.state.off('pending')

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

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

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

295
  public async getUserName(): Promise<string> {
296
    log.verbose('PuppetPuppeteerBridge', 'getUserName()')
297 298

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

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

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

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

337 338 339 340
  public async roomDelMember(
    roomId:     string,
    contactId:  string,
  ): 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 353 354 355
  public async roomAddMember(
    roomId:     string,
    contactId:  string,
  ): Promise<number> {
356
    log.verbose('PuppetPuppeteerBridge', 'roomAddMember(%s, %s)', roomId, contactId)
357

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

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

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

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

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

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

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

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

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

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

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

461
  public async getMsgImg(id: string): Promise<string> {
462
    log.verbose('PuppetPuppeteerBridge', 'getMsgImg(%s)', id)
463

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

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

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

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

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

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

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

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

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

516
  public async getContact(id: string): Promise<PuppeteerContactRawObj> {
517
    if (id !== id) { // NaN
518
      const err = new Error('NaN! where does it come from?')
519
      log.error('PuppetPuppeteerBridge', 'getContact(NaN): %s', err)
520
      throw err
521
    }
522 523
    const max = 35
    const backoff = 500
524

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
525
    // max = (2*totalTime/backoff) ^ (1/2)
526 527 528
    // 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 (李卓桓) 已提交
529
    const timeout = max * (backoff * max) / 2
530

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

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

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

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

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

575
  public async getCheckUploadUrl(): Promise<string> {
576
    log.verbose('PuppetPuppeteerBridge', 'getCheckUploadUrl()')
577 578 579 580

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
597
  public async sendMedia(mediaData: MsgMediaPayload): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
598 599
    log.verbose('PuppetPuppeteerBridge', 'sendMedia(mediaData)')

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
614
  public async forward(baseData: MsgRawPayload, patchData: MsgRawPayload): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
615 616
    log.verbose('PuppetPuppeteerBridge', 'forward()')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

758
        log.warn('PuppetPuppeteerBridge', 'testBlockedMessage() error.ret=%s', ret)
759

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

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

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

808 809
    // 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
810 811

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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