event.ts 12.0 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
fix #51  
Huan (李卓桓) 已提交
57 58 59 60 61 62 63
  // because this function is async, so maybe entry more than one times.
  // guard by variable: isBrowserBirthing to prevent the 2nd time entrance.
  if (this.browser.targetState() !== 'open'
      || this.browser.currentState() === 'opening') {
    log.verbose('PuppetWebEvent', 'onBrowserDead() will do nothing because browser.targetState(%s) !== open or browser.currentState = opening'
                                , this.browser.targetState()
              )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
64
    return
65 66
  }

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
67 68 69 70 71 72 73
  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 (李卓桓) 已提交
74
  this.scan = null
75

76
  try {
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
77 78
    await this.browser.quit()
    log.verbose('PuppetWebEvent', 'onBrowserDead() browser quit-ed')
79

80
    if (this.browser.targetState() !== 'open') {
Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
81 82 83
      log.warn('PuppetWebEvent', 'onBrowserDead() will not init browser because browser.targetState(%s) !== open'
                                , this.browser.targetState()
              )
84 85 86
      return
    }

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

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

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

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

Huan (李卓桓)'s avatar
fix #51  
Huan (李卓桓) 已提交
113 114 115 116
  this.emit('watchdog', {
    data: `onBrowserDead() new browser borned`
    , type: 'POISON'
  })
117 118

  return
119 120
}

121
function onServerDing(this: PuppetWeb, data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
122
  log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
123 124
  // this.watchDog(data)
  this.emit('watchdog', { data })
125 126
}

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

130
  this.scan = data
131

132 133 134
  /**
   * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
135 136
  this.browser.saveSession()
      .catch(() => {/* fail safe */})
137

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138 139 140 141 142 143
  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
  }

144
  // feed watchDog a `scan` type of food
145
  // this.watchDog(data, {type: 'scan'})
146 147 148 149 150 151
  const food: WatchdogFood = {
      data
    , type: 'SCAN'
  }
  this.emit('watchdog', food)
  this.emit('scan'    , data.url, data.code)
152 153 154
}

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

158 159
async function onServerDisconnect(this: PuppetWeb, data): Promise<void> {
  log.verbose('PuppetWebEvent', 'onServerDisconnect(%s)', data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160 161 162

  if (this.userId) {
    log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null')
163
    this.emit('logout', this.user || this.userId) // 'onServerDisconnect(' + data + ')')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164 165 166 167
    this.userId = null
    this.user = null
  }

168 169 170 171 172 173
  // 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`')
174 175 176 177 178 179 180 181
    return
  }

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

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

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

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

219
}
220

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

238 239 240 241 242 243
  // 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`')
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
    await this.browser.saveSession()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
310
              .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
311
                log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
312
              })
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 381 382 383 384 385 386
        // 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')
    }

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

406
export default PuppetWebEvent