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

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) {
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 259
  // re-init bridge after 1 second XXX: better method to confirm unload/reload finished?
  return 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 268 269 270
  }, 1000)
}

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
    this.user = await Contact.load(this.userId).ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
    log.silly('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`)
302

303
    await this.browser.saveSession()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
              .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305
                log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
              })
307 308 309

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

310 311
  // }).catch(e => {
  } catch (e) {
312 313
    log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e)
    console.log(e.stack)
314
    throw e
315 316 317
  }

  return
318
}
319

320
function onServerLogout(this: PuppetWeb, data) {
321 322 323 324 325
  this.emit('logout', this.user || this.userId)

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

Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
327
  this.userId = null
328
  this.user   = null
Huan (李卓桓)'s avatar
typo  
Huan (李卓桓) 已提交
329

330 331 332 333
  // this.browser.cleanSession()
  // .catch(e => { /* fail safe */
  //   log.verbose('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
  // })
334 335
}

336
async function onServerMessage(this: PuppetWeb, data): Promise<void> {
337
  let m = new Message(data)
338

339 340 341
  // co.call(this, function* () {
  try {
    await m.ready()
342

343 344 345 346 347
    /**
     * Fire Events if match message type & content
     */
    switch (m.type()) { // data.MsgType

348
      case Message.TYPE['VERIFYMSG']:
349 350 351
        Firer.fireFriendRequest.call(this, m)
        break

352
      case Message.TYPE['SYS']:
353
        if (m.room()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
354 355 356
          Firer.fireRoomJoin.call(this  , m)
          Firer.fireRoomLeave.call(this , m)
          Firer.fireRoomTopic.call(this , m)
357 358 359 360 361
        } else {
          Firer.fireFriendConfirm.call(this, m)
        }
        break
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362

363
    /**
364 365
     * Check Type for special Message
     * reload if needed
366
     */
367
    switch (m.type()) {
368
      case Message.TYPE['IMAGE']:
369 370 371 372 373 374 375 376 377 378 379 380
        // 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')
    }

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

400
export default PuppetWebEvent