puppet-web.ts 14.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/**
 *
 * wechaty: Wechat for Bot. and for human who talk to bot/robot
 *
 * Class PuppetWeb
 *
 * use to control wechat in web browser.
 *
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */

/**************************************
 *
 * Class PuppetWeb
 *
 ***************************************/
const util  = require('util')
const fs    = require('fs')
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
// const co    = require('co')

import Config         from '../config'
import Contact        from '../contact'
import FriendRequest  from '../friend-request'
import Message        from '../message'
import Puppet         from '../puppet'
import Room           from '../room'
import UtilLib        from '../util-lib'
import log            from '../brolog-env'

import Bridge         from './bridge'
import Browser        from './browser'
import Event          from './event'
import Server         from './server'
import Watchdog       from './watchdog'
37

38 39
const DEFAULT_PUPPET_PORT = 18788 // // W(87) X(88), ascii char code ;-]

40 41
class PuppetWeb extends Puppet {
  constructor({
42
    head = Config.head
43 44 45 46 47 48 49 50
    , profile = null  // if not set profile, then do not store session.
  } = {}) {
    super()
    this.head     = head
    this.profile  = profile

    this.userId = null  // user id
    this.user   = null  // <Contact> of user self
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51 52

    this.on('watchdog', Watchdog.onFeed.bind(this))
53 54 55 56 57 58 59
  }

  toString() { return `Class PuppetWeb({browser:${this.browser},port:${this.port}})` }

  init() {
    log.verbose('PuppetWeb', `init() with head:${this.head}, profile:${this.profile}`)

60 61 62
    // this.readyState('connecting')
    this.targetState('live')
    this.currentState('birthing')
63 64 65

    return co.call(this, function* () {

66
      this.port = yield UtilLib.getPort(DEFAULT_PUPPET_PORT)
67 68
      log.verbose('PuppetWeb', 'init() getPort %d', this.port)

69 70 71
      // @deprecated 20161004
      // yield this.initAttach(this)
      // log.verbose('PuppetWeb', 'initAttach() done')
72

73
      yield this.initServer()
74 75
      log.verbose('PuppetWeb', 'initServer() done')

76
      yield this.initBrowser()
77 78
      log.verbose('PuppetWeb', 'initBrowser() done')

79
      yield this.initBridge()
80 81 82 83 84 85 86 87 88 89 90
      log.verbose('PuppetWeb', 'initBridge() done')

      this.emit('watchdog', { data: 'inited' })
    })
    .catch(e => {   // Reject
      log.error('PuppetWeb', 'init exception: %s', e.message)
      this.quit()
      throw e
    })
    .then(() => {   // Finally
      log.verbose('PuppetWeb', 'init() done')
91 92
      // this.readyState('connected')
      this.currentState('live')
93 94 95 96 97 98
      return this   // for Chaining
    })
  }

  quit() {
    log.verbose('PuppetWeb', 'quit()')
99
    this.targetState('dead')
100

101 102 103 104
    // if (this.readyState() === 'disconnecting') {
    if (this.currentState() === 'killing') {
      // log.warn('PuppetWeb', 'quit() is called but readyState is `disconnecting`?')
      log.warn('PuppetWeb', 'quit() is called but currentState is `killing`?')
105 106 107 108 109 110 111 112 113
      throw new Error('do not call quit again when quiting')
    }

    // POISON must feed before readyState set to "disconnecting"
    this.emit('watchdog', {
      data: 'PuppetWeb.quit()',
      type: 'POISON'
    })

114 115
    // this.readyState('disconnecting')
    this.currentState('killing')
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

    return co.call(this, function* () {

      if (this.bridge)  {
        yield this.bridge.quit()
                        .catch(e => { // fail safe
                          log.warn('PuppetWeb', 'quit() bridge.quit() exception: %s', e.message)
                        })
        log.verbose('PuppetWeb', 'quit() bridge.quit() this.bridge = null')
        this.bridge = null
      } else { log.warn('PuppetWeb', 'quit() without a bridge') }

      if (this.server) {
        yield this.server.quit()
        this.server = null
        log.verbose('PuppetWeb', 'quit() server.quit() this.server = null')
      } else { log.verbose('PuppetWeb', 'quit() without a server') }

      if (this.browser) {
        yield this.browser.quit()
                  .catch(e => { // fail safe
                    log.warn('PuppetWeb', 'quit() browser.quit() exception: %s', e.message)
                  })
        log.verbose('PuppetWeb', 'quit() server.quit() this.browser = null')
        this.browser = null
      } else { log.warn('PuppetWeb', 'quit() without a browser') }

143 144 145
      // @deprecated 20161004
      // log.verbose('PuppetWeb', 'quit() server.quit() this.initAttach(null)')
      // yield this.initAttach(null)
146 147

      this.currentState('dead')
148 149 150
    })
    .catch(e => { // Reject
      log.error('PuppetWeb', 'quit() exception: %s', e.message)
151
      this.currentState('dead')
152 153 154 155
      throw e
    })
    .then(() => { // Finally, Fail Safe
      log.verbose('PuppetWeb', 'quit() done')
156 157
      // this.readyState('disconnected')
      this.currentState('dead')
158 159 160 161
      return this   // for Chaining
    })
  }

162 163 164 165 166 167 168 169
  // @deprecated 20161004
  // initAttach(puppet) {
  //   log.verbose('PuppetWeb', 'initAttach()')
  //   Contact.attach(puppet)
  //   Room.attach(puppet)
  //   Message.attach(puppet)
  //   return Promise.resolve(!!puppet)
  // }
170 171 172 173 174 175 176 177 178 179

  initBrowser() {
    log.verbose('PuppetWeb', 'initBrowser()')
    const browser = new Browser({
      head: this.head
      , sessionFile: this.profile
    })

    browser.on('dead', Event.onBrowserDead.bind(this))

180
    this.browser = browser
181

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
182 183 184 185 186
    if (this.targetState() !== 'live') {
      log.warn('PuppetWeb', 'initBrowser() found targetState != live, no init anymore')
      return Promise.resolve('skipped')
    }

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    return co.call(this, function* () {
      yield browser.init()
      return browser // follow func name meaning
    }).catch(e => {
      log.error('PuppetWeb', 'initBrowser() exception: %s', e.message)
      throw e
    })
  }

  initBridge() {
    log.verbose('PuppetWeb', 'initBridge()')
    const bridge = new Bridge({
      puppet:   this // use puppet instead of browser, is because browser might change(die) duaring run time
      , port:   this.port
    })

203 204
    this.bridge = bridge

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205 206 207 208
    if (this.targetState() !== 'live') {
      log.warn('PuppetWeb', 'initBridge() found targetState != live, no init anymore')
      return Promise.resolve('skipped')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
209

210
    return bridge.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
211
                .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212 213 214
                  if (!this.browser){
                    log.warn('PuppetWeb', 'initBridge() without browser?')
                  } else if (this.browser.dead()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215 216 217 218 219 220
                    log.warn('PuppetWeb', 'initBridge() found browser dead, wait it to restore')
                  } else {
                    log.error('PuppetWeb', 'initBridge() exception: %s', e.message)
                    throw e
                  }
                })
221 222 223 224 225 226 227 228 229 230 231 232 233
  }

  initServer() {
    log.verbose('PuppetWeb', 'initServer()')
    const server = new Server({port: this.port})

    server.on('scan'    , Event.onServerScan.bind(this))
    server.on('login'   , Event.onServerLogin.bind(this))
    server.on('logout'  , Event.onServerLogout.bind(this))
    server.on('message' , Event.onServerMessage.bind(this))

    /**
     * @depreciated 20160825 zixia
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234
     *
235
     * when `unload` there should always be a `disconnect` event?
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236
     */
237 238 239 240 241 242 243
    // server.on('unload'  , Event.onServerUnload.bind(this))

    server.on('connection', Event.onServerConnection.bind(this))
    server.on('disconnect', Event.onServerDisconnect.bind(this))
    server.on('log'       , Event.onServerLog.bind(this))
    server.on('ding'      , Event.onServerDing.bind(this))

244 245
    this.server = server

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246 247 248 249 250
    if (this.targetState() !== 'live') {
      log.warn('PuppetWeb', 'initServer() found targetState != live, no init anymore')
      return Promise.resolve('skipped')
    }

251
    return server.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252 253 254 255
                .catch(e => {
                  log.error('PuppetWeb', 'initServer() exception: %s', e.message)
                  throw e
                })
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
  }


  self(message) {
    if (!this.userId) {
      log.verbose('PuppetWeb', 'self() got no this.userId')
      return false
    }
    if (!message || !message.get('from')) {
      log.verbose('PuppetWeb', 'self() got no message')
      return false
    }

    return this.userId == message.get('from')
  }
271

272 273 274 275 276 277 278 279 280
  send(message) {
    const to      = message.get('to')
    const room    = message.get('room')

    let content     = message.get('content')

    let destination = to
    if (room) {
      destination = room
281
      // TODO use the right @
282 283 284
      // if (to && to!==room) {
      //   content = `@[${to}] ${content}`
      // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285 286 287 288

      if (!to) {
        message.set('to', room)
      }
289 290
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291
    log.silly('PuppetWeb', `send() destination: ${destination}, content: ${content})`)
292
    return this.bridge.send(destination, content)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293 294 295 296
                      .catch(e => {
                        log.error('PuppetWeb', 'send() exception: %s', e.message)
                        throw e
                      })
297
  }
298

299 300 301 302 303 304 305 306 307 308 309 310 311 312
  reply(message, replyContent) {
    if (this.self(message)) {
      return Promise.reject(new Error('will not to reply message of myself'))
    }

    const m = new Message()
    .set('content'  , replyContent)

    .set('from'     , message.obj.to)
    .set('to'       , message.obj.from)
    .set('room'     , message.obj.room)

    // log.verbose('PuppetWeb', 'reply() by message: %s', util.inspect(m))
    return this.send(m)
313 314 315 316
                .catch(e => {
                  log.error('PuppetWeb', 'reply() exception: %s', e.message)
                  throw e
                })
317 318 319 320 321 322 323
  }

  /**
   * logout from browser, then server will emit `logout` event
   */
  logout() {
    return this.bridge.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324 325 326 327
                      .catch(e => {
                        log.error('PuppetWeb', 'logout() exception: %s', e.message)
                        throw e
                      })
328 329 330
  }

  getContact(id) {
331 332 333
    if (!this.bridge) {
      throw new Error('PuppetWeb has no bridge for getContact()')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334

335
    return this.bridge.getContact(id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
336 337 338 339
                      .catch(e => {
                        log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message)
                        throw e
                      })
340 341 342 343 344 345 346
  }
  logined() { return !!(this.user) }
  ding(data) {
    if (!this.bridge) {
      return Promise.reject(new Error('ding fail: no bridge(yet)!'))
    }
    return this.bridge.ding(data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
347 348 349 350
                      .catch(e => {
                        log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message)
                        throw e
                      })
351
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
352

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353 354 355 356 357 358 359 360 361 362 363
  contactFind(filterFunction) {
    if (!this.bridge) {
      return Promise.reject(new Error('contactFind fail: no bridge(yet)!'))
    }
    return this.bridge.contactFind(filterFunction)
                      .catch(e => {
                        log.warn('PuppetWeb', 'contactFind(%s) rejected: %s', filterFunction, e.message)
                        throw e
                      })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364 365 366 367 368 369 370 371 372 373 374
  roomFind(filterFunction) {
    if (!this.bridge) {
      return Promise.reject(new Error('findRoom fail: no bridge(yet)!'))
    }
    return this.bridge.roomFind(filterFunction)
                      .catch(e => {
                        log.warn('PuppetWeb', 'roomFind(%s) rejected: %s', filterFunction, e.message)
                        throw e
                      })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
375
  roomDel(room, contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376 377 378
    if (!this.bridge) {
      return Promise.reject(new Error('roomDelMember fail: no bridge(yet)!'))
    }
379 380
    const roomId    = room.id
    const contactId = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
381 382
    return this.bridge.roomDelMember(roomId, contactId)
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383
                        log.warn('PuppetWeb', 'roomDelMember(%s, %d) rejected: %s', roomId, contactId, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
384 385 386 387
                        throw e
                      })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
388
  roomAdd(room, contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389 390 391
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }
392 393
    const roomId    = room.id
    const contactId = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
394 395 396 397 398
    return this.bridge.roomAddMember(roomId, contactId)
                      .catch(e => {
                        log.warn('PuppetWeb', 'roomAddMember(%s) rejected: %s', contact, e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
400

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
401
  roomTopic(room, topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
402 403
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
404 405
    }
    if (!room || typeof topic === 'undefined') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
406
      return Promise.reject(new Error('room or topic not found'))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
408 409

    const roomId = room.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410 411
    return this.bridge.roomModTopic(roomId, topic)
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
412
                        log.warn('PuppetWeb', 'roomTopic(%s) rejected: %s', topic, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
413 414
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
415 416
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
  roomCreate(contactList, topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
418 419 420 421 422 423 424 425
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

    if (!contactList || ! typeof contactList === 'array') {
      throw new Error('contactList not found')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426
    const contactIdList = contactList.map(c => c.id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427

428 429
// console.log('puppet roomCreate: ')
// console.log(contactIdList)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430
    return this.bridge.roomCreate(contactIdList, topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
431
                      .catch(e => {
432
                        log.warn('PuppetWeb', 'roomCreate(%s, %s) rejected: %s', contactIdList.join(','), topic, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
433 434
                        throw e
                      })
435 436 437 438 439
  }

  /**
   * FriendRequest
   */
440
  friendRequestSend(contact, message) {
441 442 443 444
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

445 446
    if (!contact) {
      throw new Error('contact not found')
447
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448

449
    return this.bridge.verifyUserRequest(contact.id, message)
450
                      .catch(e => {
451
                        log.warn('PuppetWeb', 'bridge.verifyUserRequest(%s, %s) rejected: %s', contact.id, message, e.message)
452 453
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454 455
  }

456
  friendRequestAccept(contact, ticket) {
457 458 459 460
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

461 462
    if (!contact || !ticket) {
      throw new Error('contact or ticket not found')
463 464
    }

465
    return this.bridge.verifyUserOk(contact.id, ticket)
466
                      .catch(e => {
467
                        log.warn('PuppetWeb', 'bridge.verifyUserOk(%s, %s) rejected: %s', contact.id, ticket, e.message)
468 469 470
                        throw e
                      })
  }
471 472
}

473 474
// module.exports = PuppetWeb.default = PuppetWeb.PuppetWeb = PuppetWeb
export default PuppetWeb