event.ts 12.1 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
export 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

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 83
    await this.browser.quit()
    log.verbose('PuppetWebEvent', 'onBrowserDead() browser quit-ed')
84

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85 86 87 88
    // if (this.browser.targetState() !== 'open') {
    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 (李卓桓) 已提交
89
              )
90 91 92
      return
    }

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

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

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

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

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

  return
125 126
}

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

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

136
  this.scan = data
137

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

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

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

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

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

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

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

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

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

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

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

222
}
223

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

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

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

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

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

  return
270 271 272
}

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

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

279
  this.scan = null
280

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

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

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

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

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

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

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

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

  return
324
}
325

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

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

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

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

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

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

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

354
      case Message.TYPE['VERIFYMSG']:
355 356 357
        Firer.fireFriendRequest.call(this, m)
        break

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

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

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

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

404
export default PuppetWebEvent