event.ts 11.9 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
import {
    WatchdogFood
  , ScanInfo
21 22 23
  , log
}                     from '../config'
import { Contact }    from '../contact'
24
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
25
    Message
26
  , MediaMessage
27
}                     from '../message'
28

29 30
import { Firer } from './firer'
import { PuppetWeb }      from './puppet-web'
31

32
/* tslint:disable:variable-name */
33
export const Event = {
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

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
53 54
  if (!this.browser || !this.bridge) {
    throw new Error('browser or bridge instance not exist in PuppetWeb instance')
55 56
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
  const browser = this.browser
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
58 59
  // because this function is async, so maybe entry more than one times.
  // guard by variable: isBrowserBirthing to prevent the 2nd time entrance.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60 61 62 63 64 65 66 67
  // if (this.browser.targetState() !== 'open'
  //     || this.browser.currentState() === 'opening') {
  if ((browser.state.current() === 'open' && browser.state.inprocess())
      || browser.state.target() !== 'open'
  ) {
    log.verbose('PuppetWebEvent', 'onBrowserDead() will do nothing because %s, or %s'
                                , 'browser.state.target() !== open'
                                , 'browser.state.current() === open & inprocess'
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
68
              )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
69
    return
70 71
  }

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
72 73 74 75 76 77 78
  const TIMEOUT = 180000 // 180s / 3m
  // 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
  })

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

81
  try {
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
82
    await this.browser.quit()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
83
    log.verbose('PuppetWebEvent', 'onBrowserDead() browser.quit() done')
84

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85 86 87
    if (browser.state.target() !== 'open') {
      log.warn('PuppetWebEvent', 'onBrowserDead() will not init browser because browser.state.target(%s) !== open'
                                , browser.state.target()
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
88
              )
89 90 91
      return
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92
    await this.initBrowser()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
    log.verbose('PuppetWebEvent', 'onBrowserDead() new browser inited')
94

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95
    await this.initBridge()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96
    log.verbose('PuppetWebEvent', 'onBrowserDead() bridge re-inited')
97

98
    const dong = await this.ding()
99
    if (/dong/i.test(dong)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100
      log.verbose('PuppetWebEvent', 'onBrowserDead() ding() works well after reset')
101
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102 103 104
      const err = new Error('ding() got "' + dong + '", should be "dong" ')
      log.warn('PuppetWebEvent', 'onBrowserDead() %s', err.message)
      throw err
105
    }
106 107
  } catch (e) {
    log.error('PuppetWebEvent', 'onBrowserDead() exception: %s', e.message)
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
108 109 110 111 112 113
    try {
      await this.quit()
      await this.init()
    } catch (err) {
      log.warn('PuppetWebEvent', 'onBrowserDead() fail safe for this.quit(): %s', err.message)
    }
114 115
  }

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
116
  log.verbose('PuppetWebEvent', 'onBrowserDead() new browser borned')
117

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
118 119 120 121
  this.emit('watchdog', {
    data: `onBrowserDead() new browser borned`
    , type: 'POISON'
  })
122 123

  return
124 125
}

126
function onServerDing(this: PuppetWeb, data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127
  log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
128 129
  // this.watchDog(data)
  this.emit('watchdog', { data })
130 131
}

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

135
  this.scan = data
136

137 138 139
  /**
   * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
   */
140 141
  await this.browser.saveCookie()
                    .catch(() => {/* fail safe */})
142

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143 144 145 146 147 148
  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
  }

149
  // feed watchDog a `scan` type of food
150
  // this.watchDog(data, {type: 'scan'})
151 152 153 154 155 156
  const food: WatchdogFood = {
      data
    , type: 'SCAN'
  }
  this.emit('watchdog', food)
  this.emit('scan'    , data.url, data.code)
157 158 159
}

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

163 164
async function onServerDisconnect(this: PuppetWeb, data): Promise<void> {
  log.verbose('PuppetWebEvent', 'onServerDisconnect(%s)', data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
165 166 167

  if (this.userId) {
    log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null')
168
    this.emit('logout', this.user || this.userId) // 'onServerDisconnect(' + data + ')')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
169 170 171 172
    this.userId = null
    this.user = null
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173 174
  // if (this.currentState() === 'killing') {
  if (this.state.current() === 'dead' && this.state.inprocess()) {
Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
175
    log.verbose('PuppetWebEvent', 'onServerDisconnect() be called when state.current() is `dead` and inprocess()')
176 177 178 179 180 181 182 183
    return
  }

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

185 186 187 188
  /**
   * conditions:
   * 1. browser crash(i.e.: be killed)
   */
189
  if (this.browser.dead()) {   // browser is dead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190
    log.verbose('PuppetWebEvent', 'onServerDisconnect() found dead browser. wait it to restore')
191 192 193
    return
  }

194 195 196 197
  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')
198
    return
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
  }

  // 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

221
}
222

223
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
224 225 226
 *
 * @depreciated 20160825 zixia
 * when `unload` there should always be a `disconnect` event?
227
 *
228 229 230 231 232 233 234 235
 * `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. ...
 */
236
function onServerUnload(this: PuppetWeb, data): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
237
  log.warn('PuppetWebEvent', 'onServerUnload(%s)', data)
238
  // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser
239

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
240
  if (this.state.current() === 'dead' && this.state.inprocess()) {
Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
241
    log.verbose('PuppetWebEvent', 'onServerUnload() will return because state.current() is `dead` and inprocess()')
242
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243 244 245 246 247 248 249
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
251
  if (this.browser.dead()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
    log.error('PuppetWebEvent', 'onServerUnload() found browser dead. wait it to restore itself')
253 254
    return
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255

256
  // re-init bridge after 1 second XXX: better method to confirm unload/reload finished?
257
  setTimeout(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258 259 260 261
    if (!this.bridge) {
      log.warn('PuppetWebEvent', 'onServerUnload() bridge gone after setTimeout()')
      return
    }
262
    this.bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263 264
              .then(r  => log.verbose('PuppetWebEvent', 'onServerUnload() bridge.init() done: %s', r))
              .catch(e => log.error('PuppetWebEvent', 'onServerUnload() bridge.init() exceptoin: %s', e.message))
265
  }, 1000)
266 267

  return
268 269 270
}

function onServerLog(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
271
  log.silly('PuppetWebEvent', 'onServerLog(%s)', data)
272 273
}

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

277
  this.scan = null
278

279
  if (this.userId) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
280
    log.verbose('PuppetWebEvent', 'onServerLogin() be called but with userId set?')
281 282
  }

283 284
  // co.call(this, function* () {
  try {
285 286
    // co.call to make `this` context work inside generator.
    // See also: https://github.com/tj/co/issues/274
287 288 289 290

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

293
    if (!this.userId) {
294 295
      log.verbose('PuppetWebEvent', 'onServerLogin: browser not full loaded(%d), retry later', attempt)
      setTimeout(onServerLogin.bind(this, data, ++attempt), 500)
296 297
      return
    }
298

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299
    log.silly('PuppetWebEvent', 'bridge.getUserName: %s', this.userId)
300 301 302 303 304
    this.user = Contact.load(this.userId)
    if (!this.user) {
      throw new Error('no user')
    }
    await this.user.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305
    log.silly('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`)
306

307 308 309 310
    await this.browser.saveCookie()
                      .catch(e => { // fail safe
                        log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
                      })
311 312 313

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

314 315
  // }).catch(e => {
  } catch (e) {
316 317
    log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e)
    console.log(e.stack)
318
    throw e
319 320 321
  }

  return
322
}
323

324
function onServerLogout(this: PuppetWeb, data) {
325 326 327 328 329
  this.emit('logout', this.user || this.userId)

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

Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
331
  this.userId = null
332
  this.user   = null
Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
333

334 335 336 337
  // this.browser.cleanSession()
  // .catch(e => { /* fail safe */
  //   log.verbose('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
  // })
338 339
}

340
async function onServerMessage(this: PuppetWeb, data): Promise<void> {
341
  let m = new Message(data)
342

343 344 345
  // co.call(this, function* () {
  try {
    await m.ready()
346

347 348 349 350 351
    /**
     * Fire Events if match message type & content
     */
    switch (m.type()) { // data.MsgType

352
      case Message.TYPE['VERIFYMSG']:
353
        Firer.checkFriendRequest.call(this, m)
354 355
        break

356
      case Message.TYPE['SYS']:
357
        if (m.room()) {
358 359 360
          Firer.checkRoomJoin.call(this  , m)
          Firer.checkRoomLeave.call(this , m)
          Firer.checkRoomTopic.call(this , m)
361
        } else {
362
          Firer.checkFriendConfirm.call(this, m)
363 364 365
        }
        break
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
366

367
    /**
368 369
     * Check Type for special Message
     * reload if needed
370
     */
371
    switch (m.type()) {
372
      case Message.TYPE['IMAGE']:
373 374 375 376 377 378
        // log.verbose('PuppetWebEvent', 'onServerMessage() IMAGE message')
        m = new MediaMessage(data)
        break
    }

    // To Be Deleted: set self...
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
379
    if (!this.userId) {
380 381 382
      log.warn('PuppetWebEvent', 'onServerMessage() without this.userId')
    }

383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    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
400
}