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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return browser
  }
132

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

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

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

      this.emit('ready')

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

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

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

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

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

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

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

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

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

201 202 203 204
    return page
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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