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 21 22
/**
 *
 * 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')
const co    = require('co')

23
const log = require('../brolog-env')
24 25 26 27
const Puppet  = require('../puppet')
const Contact = require('../contact')
const Room    = require('../room')
const Message = require('../message')
28
const FriendRequest = require('../friend-request')
29 30 31 32 33 34 35 36 37 38 39

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

const Event     = require('./event')
const Watchdog  = require('./watchdog')

const UtilLib = require('../util-lib')
const Config  = require('../config')

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

42 43
class PuppetWeb extends Puppet {
  constructor({
44
    head = Config.head
45 46 47 48 49 50 51 52
    , 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 (李卓桓) 已提交
53 54

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

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

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

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

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

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

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

75
      yield this.initServer()
76 77
      log.verbose('PuppetWeb', 'initServer() done')

78
      yield this.initBrowser()
79 80
      log.verbose('PuppetWeb', 'initBrowser() done')

81
      yield this.initBridge()
82 83 84 85 86 87 88 89 90 91 92
      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')
93 94
      // this.readyState('connected')
      this.currentState('live')
95 96 97 98 99 100
      return this   // for Chaining
    })
  }

  quit() {
    log.verbose('PuppetWeb', 'quit()')
101
    this.targetState('dead')
102

103 104 105 106
    // 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`?')
107 108 109 110 111 112 113 114 115
      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'
    })

116 117
    // this.readyState('disconnecting')
    this.currentState('killing')
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 143 144

    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') }

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

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

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

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

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

182
    this.browser = browser
183

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

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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
    })

205 206
    this.bridge = bridge

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

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

  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 (李卓桓) 已提交
236
     *
237
     * when `unload` there should always be a `disconnect` event?
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238
     */
239 240 241 242 243 244 245
    // 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))

246 247
    this.server = server

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

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


  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')
  }
273

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

    let content     = message.get('content')

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

      if (!to) {
        message.set('to', room)
      }
291 292
    }

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

301 302 303 304 305 306 307 308 309 310 311 312 313 314
  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)
315 316 317 318
                .catch(e => {
                  log.error('PuppetWeb', 'reply() exception: %s', e.message)
                  throw e
                })
319 320 321 322 323 324 325
  }

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

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

337
    return this.bridge.getContact(id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
338 339 340 341
                      .catch(e => {
                        log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message)
                        throw e
                      })
342 343 344 345 346 347 348
  }
  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 (李卓桓) 已提交
349 350 351 352
                      .catch(e => {
                        log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message)
                        throw e
                      })
353
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
354

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
355 356 357 358 359 360 361 362 363 364 365
  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 (李卓桓) 已提交
366 367 368 369 370 371 372 373 374 375 376
  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 (李卓桓) 已提交
377
  roomDel(room, contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
378 379 380
    if (!this.bridge) {
      return Promise.reject(new Error('roomDelMember fail: no bridge(yet)!'))
    }
381 382
    const roomId    = room.id
    const contactId = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383 384
    return this.bridge.roomDelMember(roomId, contactId)
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385
                        log.warn('PuppetWeb', 'roomDelMember(%s, %d) rejected: %s', roomId, contactId, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386 387 388 389
                        throw e
                      })
  }

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419
  roomCreate(contactList, topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
420 421 422 423 424 425 426 427
    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 (李卓桓) 已提交
428
    const contactIdList = contactList.map(c => c.id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
429

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

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

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

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

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
475
module.exports = PuppetWeb.default = PuppetWeb.PuppetWeb = PuppetWeb