event.ts 12.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
/**
 *
 * wechaty: Wechat for Bot. and for human who talk to bot/robot
 *
 * Class PuppetWeb Events
 *
 * use to control wechat in web browser.
 *
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 *
13
 * Events for Class PuppetWeb
14
 *
15
 * here `this` is a PuppetWeb Instance
16
 *
17
 */
18 19 20 21 22
import {
    WatchdogFood
  , ScanInfo
}                   from '../config'
import Contact      from '../contact'
23
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
24
    Message
25 26
  , MediaMessage
}                   from '../message'
27 28 29 30
import log          from '../brolog-env'

import Firer        from './firer'
import PuppetWeb    from './puppet-web'
31

32
/* tslint:disable:variable-name */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
33
const PuppetWebEvent = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
34
    onBrowserDead
35

36 37
  , onServerLogin
  , onServerLogout
38

39 40
  , onServerConnection
  , onServerDisconnect
41

42 43 44 45
  , onServerDing
  , onServerScan
  , onServerUnload
  , onServerLog
46

47
  , onServerMessage
48 49
}

50
async function onBrowserDead(this: PuppetWeb, e): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51
  log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e && e.message || e)
52
  // because this function is async, so maybe entry more than one times.
53
  // guard by variable: isBrowserBirthing to prevent the 2nd time entrance.
54 55 56 57 58 59
  // if (this.isBrowserBirthing) {
  //   log.warn('PuppetWebEvent', 'onBrowserDead() is busy, this call will return now. stack: %s', (new Error()).stack)
  //   return
  // }

  if (this.browser && this.browser.targetState() !== 'open') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60
    log.verbose('PuppetWebEvent', 'onBrowserDead() will do nothing because browser.targetState(%s) !== open', this.browser.targetState())
61 62 63 64 65
    return
  }

  if (this.browser && this.browser.currentState() === 'opening') {
    log.warn('PuppetWebEvent', 'onBrowserDead() will do nothing because browser.currentState = opening. stack: %s', (new Error()).stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
66
    return
67 68
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
69
  this.scan = null
70

71 72
  // return co.call(this, function* () {
  try {
73 74
    // log.verbose('PuppetWebEvent', 'onBrowserDead() co() set isBrowserBirthing true')
    // this.isBrowserBirthing = true
75

76
    const TIMEOUT = 180000 // 180s / 3m
77 78 79 80 81
    // this.watchDog(`onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`, {timeout: TIMEOUT})
    this.emit('watchdog', {
      data: `onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`
      , timeout: TIMEOUT
    })
82 83

    if (!this.browser || !this.bridge) {
84 85 86
      const err = new Error('no browser or no bridge')
      log.error('PuppetWebEvent', 'onBrowserDead() %s', err.message)
      throw err
87
    }
88

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
    log.verbose('PuppetWebEvent', 'onBrowserDead() try to reborn browser')
90

91 92 93
    await this.browser.restart()
                      .catch((err: Error) => { // fail safe
                        log.warn('PuppetWebEvent', 'browser.quit() exception: %s', err.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
94 95
                      })
    log.verbose('PuppetWebEvent', 'onBrowserDead() old browser quited')
96

97
    if (this.browser.targetState() !== 'open') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
98
      log.warn('PuppetWebEvent', 'onBrowserDead() will not init browser because browser.targetState(%s) !== open', this.browser.targetState())
99 100 101
      return
    }

102
    this.browser = await this.initBrowser()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103
    log.verbose('PuppetWebEvent', 'onBrowserDead() new browser inited')
104

105 106
    // this.bridge = await this.bridge.init()
    this.bridge = await this.initBridge()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107
    log.verbose('PuppetWebEvent', 'onBrowserDead() bridge re-inited')
108

109
    const dong = await this.ding()
110
    if (/dong/i.test(dong)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
111
      log.verbose('PuppetWebEvent', 'onBrowserDead() ding() works well after reset')
112
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
113
      log.warn('PuppetWebEvent', 'onBrowserDead() ding() get error return after reset: ' + dong)
114
    }
115 116 117
  // }).catch(err => { // Exception
  } catch (e) {
    log.error('PuppetWebEvent', 'onBrowserDead() exception: %s', e.message)
118

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119
    log.warn('PuppetWebEvent', 'onBrowserDead() try to re-init PuppetWeb itself')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
    return this.quit()
121
              .catch(error => log.warn('PuppetWebEvent', 'onBrowserDead() fail safe for this.quit(): %s', error.message))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
122
              .then(_ => this.init())
123 124 125
  }

  // .then(() => { // Finally
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
126
    log.verbose('PuppetWebEvent', 'onBrowserDead() new browser borned')
127
    // this.isBrowserBirthing = false
128 129 130 131 132

    this.emit('watchdog', {
      data: `onBrowserDead() new browser borned`
      , type: 'POISON'
    })
133 134 135
  // })

  return
136 137
}

138
function onServerDing(this: PuppetWeb, data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
139
  log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
140 141
  // this.watchDog(data)
  this.emit('watchdog', { data })
142 143
}

144
function onServerScan(this: PuppetWeb, data: ScanInfo) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145
  log.verbose('PuppetWebEvent', 'onServerScan(%d)', data && data.code)
146

147
  this.scan = data
148

149 150 151
  /**
   * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152 153
  this.browser.saveSession()
      .catch(() => {/* fail safe */})
154

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155 156 157 158 159 160
  if (this.userId) {
    log.verbose('PuppetWebEvent', 'onServerScan() there has userId when got a scan event. emit logout and set userId to null')
    this.emit('logout', this.user || this.userId)
    this.userId = this.user = null
  }

161
  // feed watchDog a `scan` type of food
162
  // this.watchDog(data, {type: 'scan'})
163 164 165 166 167 168
  const food: WatchdogFood = {
      data
    , type: 'SCAN'
  }
  this.emit('watchdog', food)
  this.emit('scan'    , data.url, data.code)
169 170 171
}

function onServerConnection(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172
  log.verbose('PuppetWebEvent', 'onServerConnection: %s', data)
173 174
}

175 176
async function onServerDisconnect(this: PuppetWeb, data): Promise<void> {
  log.verbose('PuppetWebEvent', 'onServerDisconnect(%s)', data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177 178 179

  if (this.userId) {
    log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null')
180
    this.emit('logout', this.user || this.userId) // 'onServerDisconnect(' + data + ')')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
181 182 183 184
    this.userId = null
    this.user = null
  }

185 186 187 188 189 190
  // if (this.readyState() === 'disconnecting') {
  //   log.verbose('PuppetWebEvent', 'onServerDisconnect() be called when readyState is `disconnecting`')
  //   return
  // }
  if (this.currentState() === 'killing') {
    log.verbose('PuppetWebEvent', 'onServerDisconnect() be called when currentState is `killing`')
191 192 193 194 195 196 197 198
    return
  }

  if (!this.browser || !this.bridge) {
    const e = new Error('onServerDisconnect() no browser or bridge')
    log.error('PuppetWebEvent', '%s', e.message)
    throw e
  }
199

200 201 202 203
  /**
   * conditions:
   * 1. browser crash(i.e.: be killed)
   */
204
  if (this.browser.dead()) {   // browser is dead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205
    log.verbose('PuppetWebEvent', 'onServerDisconnect() found dead browser. wait it to restore')
206 207 208
    return
  }

209 210 211 212
  const live = await this.browser.readyLive()

  if (!live) { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself.
    log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead after readyLive() check. waiting it recover itself')
213
    return
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
  }

  // browser is alive, and we have a bridge to it
  log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge')
  // must use setTimeout to wait a while.
  // because the browser has just refreshed, need some time to re-init to be ready.
  // if the browser is not ready, bridge init will fail,
  // caused browser dead and have to be restarted. 2016/6/12
  setTimeout(_ => {
    if (!this.bridge) {
      // XXX: sometimes this.bridge gone in this timeout. why?
      // what's happend between the last if(!this.bridge) check and the timeout call?
      const e = new Error('bridge gone after setTimeout? why???')
      log.warn('PuppetWebEvent', 'onServerDisconnect() setTimeout() %s', e.message)
      throw e
    }
    this.bridge.init()
                .then(ret => log.verbose('PuppetWebEvent', 'onServerDisconnect() setTimeout() bridge re-inited: %s', ret))
                .catch(e  => log.error('PuppetWebEvent', 'onServerDisconnect() setTimeout() exception: [%s]', e))
  }, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait)
  return

236
}
237

238
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
239 240 241
 *
 * @depreciated 20160825 zixia
 * when `unload` there should always be a `disconnect` event?
242
 *
243 244 245 246 247 248 249 250
 * `unload` event is sent from js@browser to webserver via socketio
 * after received `unload`, we should fix bridge by re-inject the Wechaty js code into browser.
 * possible conditions:
 * 1. browser refresh
 * 2. browser navigated to a new url
 * 3. browser quit(crash?)
 * 4. ...
 */
251
function onServerUnload(this: PuppetWeb, data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
  log.warn('PuppetWebEvent', 'onServerUnload(%s)', data)
253
  // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser
254

255 256 257 258 259 260
  // if (this.readyState() === 'disconnecting') {
  //   log.verbose('PuppetWebEvent', 'onServerUnload() will return because readyState is `disconnecting`')
  //   return
  // }
  if (this.currentState() === 'killing') {
    log.verbose('PuppetWebEvent', 'onServerUnload() will return because currentState is `killing`')
261
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
262 263 264 265 266 267 268
  }

  if (!this.browser || !this.bridge) {
    const e = new Error('no bridge or no browser')
    log.warn('PuppetWebEvent', 'onServerUnload() %s', e.message)
    throw e
  }
269

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
270
  if (this.browser.dead()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
271
    log.error('PuppetWebEvent', 'onServerUnload() found browser dead. wait it to restore itself')
272 273
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
274

275 276
  // re-init bridge after 1 second XXX: better method to confirm unload/reload finished?
  return setTimeout(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277 278 279 280
    if (!this.bridge) {
      log.warn('PuppetWebEvent', 'onServerUnload() bridge gone after setTimeout()')
      return
    }
281
    this.bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282 283
              .then(r  => log.verbose('PuppetWebEvent', 'onServerUnload() bridge.init() done: %s', r))
              .catch(e => log.error('PuppetWebEvent', 'onServerUnload() bridge.init() exceptoin: %s', e.message))
284 285 286 287
  }, 1000)
}

function onServerLog(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
288
  log.silly('PuppetWebEvent', 'onServerLog(%s)', data)
289 290
}

291
async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void> {
292 293
  log.verbose('PuppetWebEvent', 'onServerLogin(%s, %d)', data, attempt)

294
  this.scan = null
295

296
  if (this.userId) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
    log.verbose('PuppetWebEvent', 'onServerLogin() be called but with userId set?')
298 299
  }

300 301
  // co.call(this, function* () {
  try {
302 303
    // co.call to make `this` context work inside generator.
    // See also: https://github.com/tj/co/issues/274
304 305 306 307

    /**
     * save login user id to this.userId
     */
308
    this.userId = await this.bridge.getUserName()
309

310
    if (!this.userId) {
311 312
      log.verbose('PuppetWebEvent', 'onServerLogin: browser not full loaded(%d), retry later', attempt)
      setTimeout(onServerLogin.bind(this, data, ++attempt), 500)
313 314
      return
    }
315

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
316
    log.silly('PuppetWebEvent', 'bridge.getUserName: %s', this.userId)
317
    this.user = await Contact.load(this.userId).ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
318
    log.silly('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`)
319

320
    await this.browser.saveSession()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
321
              .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
322
                log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
323
              })
324 325 326

    this.emit('login', this.user)

327 328
  // }).catch(e => {
  } catch (e) {
329 330
    log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e)
    console.log(e.stack)
331
    throw e
332 333 334
  }

  return
335
}
336

337
function onServerLogout(this: PuppetWeb, data) {
338 339 340 341 342
  this.emit('logout', this.user || this.userId)

  if (!this.user && !this.userId) {
    log.warn('PuppetWebEvent', 'onServerLogout() without this.user or userId initialized')
  }
343

Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
344
  this.userId = null
345
  this.user   = null
Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
346

347 348 349 350
  // this.browser.cleanSession()
  // .catch(e => { /* fail safe */
  //   log.verbose('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
  // })
351 352
}

353
async function onServerMessage(this: PuppetWeb, data): Promise<void> {
354
  let m = new Message(data)
355

356 357 358
  // co.call(this, function* () {
  try {
    await m.ready()
359

360 361 362 363 364
    /**
     * Fire Events if match message type & content
     */
    switch (m.type()) { // data.MsgType

365
      case Message.TYPE['VERIFYMSG']:
366 367 368
        Firer.fireFriendRequest.call(this, m)
        break

369
      case Message.TYPE['SYS']:
370
        if (m.room()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371 372 373
          Firer.fireRoomJoin.call(this  , m)
          Firer.fireRoomLeave.call(this , m)
          Firer.fireRoomTopic.call(this , m)
374 375 376 377 378
        } else {
          Firer.fireFriendConfirm.call(this, m)
        }
        break
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
379

380
    /**
381 382
     * Check Type for special Message
     * reload if needed
383
     */
384
    switch (m.type()) {
385
      case Message.TYPE['IMAGE']:
386 387 388 389 390 391 392 393 394 395 396 397
        // log.verbose('PuppetWebEvent', 'onServerMessage() IMAGE message')
        m = new MediaMessage(data)
        break
    }

    // To Be Deleted: set self...
    if (this.userId) {
      m.set('self', this.userId)
    } else {
      log.warn('PuppetWebEvent', 'onServerMessage() without this.userId')
    }

398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
    await m.ready() // TODO: EventEmitter2 for video/audio/app/sys....
    this.emit('message', m)

    // .catch(e => {
    //   log.error('PuppetWebEvent', 'onServerMessage() message ready exception: %s', e.stack)
    //   // console.log(e)
    //   /**
    //    * FIXME: add retry here...
    //    * setTimeout(onServerMessage.bind(this, data, ++attempt), 1000)
    //    */
    // })
  } catch (e) {
    log.error('PuppetWebEvent', 'onServerMessage() exception: %s', e.stack)
    throw e
  }

  return
415 416
}

417
export default PuppetWebEvent