puppet-web-event.js 8.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/**
 *
 * 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
 *
 */

/**************************************
 *
 * Class PuppetWeb
 *
 ***************************************/
const util  = require('util')
const fs    = require('fs')
const co    = require('co')

const log = require('./npmlog-env')

const Puppet  = require('./puppet')
const Contact = require('./contact')
const Room    = require('./room')

const Message = require('./message')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
30
const MediaMessage = require('./message-media')
31 32 33 34 35

const Server  = require('./puppet-web-server')
const Browser = require('./puppet-web-browser')
const Bridge  = require('./puppet-web-bridge')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
36
const PuppetWebEvent = {
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
  onBrowserDead: onBrowserDead

  , onServerLogin: onServerLogin
  , onServerLogout: onServerLogout

  , onServerConnection: onServerConnection
  , onServerDisconnect: onServerDisconnect

  , onServerDing: onServerDing
  , onServerScan: onServerScan
  , onServerUnload: onServerUnload
  , onServerLog: onServerLog

  , onServerMessage: onServerMessage
}

function onBrowserDead(e) {
  // because this function is async, so maybe entry more than one times.
  // guard by variable: onBrowserBirthing to prevent the 2nd time entrance.
  if (this.onBrowserBirthing) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
    log.warn('PuppetWebEvent', 'onBrowserDead() Im busy, dont call me again before I return. this time will return and do nothing')
58 59 60 61 62 63 64
    return
  }
  this.onBrowserBirthing = true

  const TIMEOUT = 180000 // 180s / 3m
  this.watchDog(`onBrowserDead() set a timeout of ${Math.floor(TIMEOUT/1000)} seconds to prevent unknown state change`, {timeout: TIMEOUT})

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
65
  log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e.message || e)
66
  if (!this.browser || !this.bridge) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
67
    log.error('PuppetWebEvent', 'onBrowserDead() browser or bridge not found. do nothing')
68 69 70 71
    return
  }

  return co.call(this, function* () {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
72
    log.verbose('PuppetWebEvent', 'onBrowserDead() try to reborn browser')
73 74 75

    yield this.browser.quit()
    .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76
      log.warn('PuppetWebEvent', 'browser.quit() exception: %s', e.message)
77
    })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78
    log.verbose('PuppetWebEvent', 'old browser quited')
79 80

    yield this.initBrowser()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
81
    log.verbose('PuppetWebEvent', 'new browser inited')
82 83

    yield this.bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
84
    log.verbose('PuppetWebEvent', 'bridge re-inited')
85 86 87

    const dong = yield this.ding()
    if (/dong/i.test(dong)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88
      log.verbose('PuppetWebEvent', 'ding() works well after reset')
89
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
90
      log.warn('PuppetWebEvent', 'ding() get error return after reset: ' + dong)
91 92 93
    }
  })
  .catch(e => { // Exception
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
94
    log.error('PuppetWebEvent', 'onBrowserDead() exception: %s', e.message)
95

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96
    log.warn('PuppetWebEvent', 'onBrowserDead() try to re-init PuppetWeb itself')
97 98 99
    return this.quit().then(() => this.init())
  })
  .then(() => { // Finally
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100
    log.verbose('PuppetWebEvent', 'onBrowserDead() new browser borned')
101 102 103 104 105
    this.onBrowserBirthing = false
  })
}

function onServerDing(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
  log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
107 108 109 110
  this.watchDog(data)
}

function onServerScan(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
111
  log.verbose('PuppetWebEvent', 'onServerScan(%d)', data && data.code)
112 113 114 115 116 117 118 119 120 121 122 123

  /**
   * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
   */
  if (this.session) {
     this.browser.saveSession(this.session)
     .catch(() => {/* fail safe */})
   }
  this.emit('scan', data)
}

function onServerConnection(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124
  log.verbose('PuppetWebEvent', 'onServerConnection: %s', typeof data)
125 126 127
}

function onServerDisconnect(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
128
  log.verbose('PuppetWebEvent', 'onServerDisconnect: %s', data)
129 130 131 132 133 134
  /**
   * conditions:
   * 1. browser crash(i.e.: be killed)
   * 2. quiting
   */
  if (!this.browser) {                // no browser, quiting?
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
135
    log.verbose('PuppetWebEvent', 'onServerDisconnect() no browser. maybe Im quiting, do nothing')
136 137
    return
  } else if (this.browser.dead()) {   // browser is dead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138
    log.verbose('PuppetWebEvent', 'onServerDisconnect() found dead browser. wait it to restore')
139 140
    return
  } else if (!this.bridge) {          // no bridge, quiting???
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141
    log.verbose('PuppetWebEvent', 'onServerDisconnect() no bridge. maybe Im quiting, do nothing')
142 143 144 145 146
    return
  }

  this.browser.readyLive()
  .then(r => {  // browser is alive, and we have a bridge to it
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147
    log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge')
148 149 150 151 152 153
    // must use setTimeout to wait a while.
    // because the browser has just refreshed, need some time to re-init to ready.
    // if the browser is not ready, bridge init will fail,
    // caused browser dead and have to be restarted. 2016/6/12
    setTimeout(() => {
      this.bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154 155
      .then(r  => log.verbose('PuppetWebEvent', 'onServerDisconnect() bridge re-inited: %s', r))
      .catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() exception: [%s]', e))
156 157 158 159
    }, 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 (李卓桓) 已提交
160
    log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead, waiting it recover itself: %s', e.message)
161 162 163
    return
  })
}
164

165 166 167 168 169 170 171 172 173 174
/**
 * `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 (李卓桓) 已提交
175
  log.warn('PuppetWebEvent', 'onServerUnload(%s)', typeof data)
176
  // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser
177 178

  if (!this.browser) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179
    log.warn('PuppetWebEvent', 'onServerUnload() found browser gone, should be quiting now')
180 181
    return
  } else if (!this.bridge) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
182
    log.warn('PuppetWebEvent', 'onServerUnload() found bridge gone, should be quiting now')
183 184
    return
  } else if (this.browser.dead()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
185
    log.error('PuppetWebEvent', 'onServerUnload() found browser dead. wait it to restore itself')
186 187 188 189 190
    return
  }
  // re-init bridge after 1 second XXX: better method to confirm unload/reload finished?
  return setTimeout(() => {
    this.bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191 192
    .then(r  => log.verbose('PuppetWebEvent', 'onServerUnload() bridge.init() done: %s', r))
    .catch(e => log.error('PuppetWebEvent', 'onServerUnload() bridge.init() exceptoin: %s', e.message))
193 194 195 196
  }, 1000)
}

function onServerLog(data) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
197
  log.verbose('PuppetWebEvent', 'onServerLog: %s', data)
198 199 200 201 202 203 204 205
}

function onServerLogin(data) {
  co.call(this, function* () {
    // co.call to make `this` context work inside generator.
    // See also: https://github.com/tj/co/issues/274
    const userName = yield this.bridge.getUserName()
    if (!userName) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206
      log.verbose('PuppetWebEvent', 'onServerLogin: browser not full loaded, retry later.')
207
      setTimeout(onServerLogin.bind(this), 500)
208 209
      return
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210
    log.verbose('PuppetWebEvent', 'bridge.getUserName: %s', userName)
211
    this.user = yield Contact.load(userName).ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
    log.verbose('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`)
213 214 215 216 217
    this.emit('login', this.user)

    if (this.session) {
      yield this.browser.saveSession(this.session)
      .catch(e => { // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
218
        log.warn('PuppetWebEvent', 'browser.saveSession exception: %s', e.message)
219 220 221
      })
    }
  }).catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222
    log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e.message)
223 224
  })
}
225

226 227 228 229
function onServerLogout(data) {
  if (this.user) {
    this.emit('logout', this.user)
    this.user = null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230
  } else { log.verbose('PuppetWebEvent', 'onServerLogout() without this.user initialized') }
231 232 233 234

  if (this.session) {
    this.browser.cleanSession(this.session)
    .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
235
      log.warn('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
236 237 238 239 240 241
    })
  }
}

function onServerMessage(data) {
  let m
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
242
  // log.warn('PuppetWebEvent', 'MsgType: %s', data.MsgType)
243 244
  switch (data.MsgType) {
    case Message.Type.IMAGE:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245
      // log.verbose('PuppetWebEvent', 'onServerMessage() IMAGE message')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246
      m = new MediaMessage(data)
247 248 249 250 251 252 253 254 255 256 257
      break;

    case 'TEXT':
    default:
      m = new Message(data)
      break;
  }

  if (this.user) {
    m.set('self', this.user.id)
  } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    log.warn('PuppetWebEvent', 'onServerMessage() without this.user')
259 260 261 262
  }
  m.ready() // TODO: EventEmitter2 for video/audio/app/sys....
  .then(() => this.emit('message', m))
  .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
    log.error('PuppetWebEvent', 'onServerMessage() message ready exception: %s', e)
264 265 266 267 268 269 270
    /**
     * FIXME: add retry here...
     * setTimeout(onServerMessage.bind(this, data, ++attempt), 1000)
     */
  })
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
271
module.exports = PuppetWebEvent