event.ts 12.6 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
 */
// import * as util  from 'util'
// import * as fs    from 'fs'
20
// const co    = require('co')
21

22 23 24 25
import Contact       from '../contact'
import MediaMessage  from '../message-media'
import Message       from '../message'
import log           from '../brolog-env'
26

27
// import FriendRequest from './friend-request'
28
import Firer         from './firer'
29

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

34 35
  , onServerLogin
  , onServerLogout
36

37 38
  , onServerConnection
  , onServerDisconnect
39

40 41 42 43
  , onServerDing
  , onServerScan
  , onServerUnload
  , onServerLog
44

45
  , onServerMessage
46 47
}

48
async function onBrowserDead(e): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
49
  log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e && e.message || e)
50
  // because this function is async, so maybe entry more than one times.
51
  // guard by variable: isBrowserBirthing to prevent the 2nd time entrance.
52 53 54 55 56 57
  // 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 (李卓桓) 已提交
58
    log.verbose('PuppetWebEvent', 'onBrowserDead() will do nothing because browser.targetState(%s) !== open', this.browser.targetState())
59 60 61 62 63
    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 (李卓桓) 已提交
64
    return
65 66
  }

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

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

74
    const TIMEOUT = 180000 // 180s / 3m
75 76 77 78 79
    // 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
    })
80 81

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

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

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

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

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

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

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

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

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

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

  return
134 135 136
}

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

function onServerScan(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143
  log.verbose('PuppetWebEvent', 'onServerScan(%d)', data && data.code)
144

145
  this.scan = data // ScanInfo
146

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153 154 155 156 157 158
  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
  }

159
  // feed watchDog a `scan` type of food
160 161
  // this.watchDog(data, {type: 'scan'})
  this.emit('watchdog', { data, type: 'SCAN' })
162

163 164 165 166
  this.emit('scan', data)
}

function onServerConnection(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167
  log.verbose('PuppetWebEvent', 'onServerConnection: %s', data)
168 169 170
}

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

  if (this.userId) {
    log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null')
175
    this.emit('logout', this.user || this.userId) // 'onServerDisconnect(' + data + ')')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
176 177 178 179
    this.userId = null
    this.user = null
  }

180 181 182 183 184 185
  // 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`')
186 187 188 189 190 191 192 193
    return
  }

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

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

  this.browser.readyLive()
  .then(r => {  // browser is alive, and we have a bridge to it
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206
    log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge')
207
    // must use setTimeout to wait a while.
208
    // because the browser has just refreshed, need some time to re-init to be ready.
209 210
    // if the browser is not ready, bridge init will fail,
    // caused browser dead and have to be restarted. 2016/6/12
Huan (李卓桓)'s avatar
debug  
Huan (李卓桓) 已提交
211
    setTimeout(_ => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
      if (!this.bridge) {
213 214 215
        // XXX: sometimes this.bridge gone in this timeout. why?
        // what's happend between the last if(!this.bridge) check and the timeout call?
        throw new Error('bridge gone after setTimeout? why???')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
      }
217
      this.bridge.init()
218 219 220
      .then(ret => {
        log.verbose('PuppetWebEvent', 'onServerDisconnect() bridge re-inited: %s', ret)
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
221
      .catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() exception: [%s]', e))
222 223 224 225
    }, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait)
    return
  })
  .catch(e => { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
226
    log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead, waiting it recover itself: %s', e.message)
227 228 229
    return
  })
}
230

231
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
232 233 234
 *
 * @depreciated 20160825 zixia
 * when `unload` there should always be a `disconnect` event?
235
 *
236 237 238 239 240 241 242 243 244
 * `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. ...
 */
function onServerUnload(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245
  log.warn('PuppetWebEvent', 'onServerUnload(%s)', data)
246
  // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser
247

248 249 250 251 252 253
  // 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`')
254
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255 256 257 258 259 260 261
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
  if (this.browser.dead()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
264
    log.error('PuppetWebEvent', 'onServerUnload() found browser dead. wait it to restore itself')
265 266
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267

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

function onServerLog(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
281
  log.silly('PuppetWebEvent', 'onServerLog(%s)', data)
282 283
}

284
async function onServerLogin(data, attempt = 0): Promise<void> {
285 286
  log.verbose('PuppetWebEvent', 'onServerLogin(%s, %d)', data, attempt)

287
  this.scan = null
288

289
  if (this.userId) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
290
    log.verbose('PuppetWebEvent', 'onServerLogin() be called but with userId set?')
291 292
  }

293 294
  // co.call(this, function* () {
  try {
295 296
    // co.call to make `this` context work inside generator.
    // See also: https://github.com/tj/co/issues/274
297 298 299 300

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

303
    if (!this.userId) {
304 305
      log.verbose('PuppetWebEvent', 'onServerLogin: browser not full loaded(%d), retry later', attempt)
      setTimeout(onServerLogin.bind(this, data, ++attempt), 500)
306 307
      return
    }
308

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

313
    await this.browser.saveSession()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
314
              .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315
                log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
316
              })
317 318 319

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

320 321
  // }).catch(e => {
  } catch (e) {
322 323
    log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e)
    console.log(e.stack)
324
    throw e
325 326 327
  }

  return
328
}
329

330
function onServerLogout(data) {
331 332 333 334 335
  this.emit('logout', this.user || this.userId)

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

Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
337
  this.userId = null
338
  this.user   = null
Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
339

340 341 342 343
  // this.browser.cleanSession()
  // .catch(e => { /* fail safe */
  //   log.verbose('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
  // })
344 345
}

346
async function onServerMessage(data): Promise<void> {
347
  let m = new Message(data)
348

349 350 351
  // co.call(this, function* () {
  try {
    await m.ready()
352

353 354 355 356 357
    /**
     * Fire Events if match message type & content
     */
    switch (m.type()) { // data.MsgType

358
      case Message.TYPE['VERIFYMSG']:
359 360 361
        Firer.fireFriendRequest.call(this, m)
        break

362
      case Message.TYPE['SYS']:
363
        if (m.room()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364 365 366
          Firer.fireRoomJoin.call(this  , m)
          Firer.fireRoomLeave.call(this , m)
          Firer.fireRoomTopic.call(this , m)
367 368 369 370 371
        } else {
          Firer.fireFriendConfirm.call(this, m)
        }
        break
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372

373
    /**
374 375
     * Check Type for special Message
     * reload if needed
376
     */
377
    switch (m.type()) {
378
      case Message.TYPE['IMAGE']:
379 380 381 382 383 384 385 386 387 388 389 390
        // 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')
    }

391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    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
408 409
}

410 411
// module.exports = PuppetWebEvent
export default PuppetWebEvent