puppet-web.ts 15.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/**
 *
 * 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
 *
15 16 17
 */
// const util  = require('util')
// const fs    = require('fs')
18 19
// const co    = require('co')

20 21 22 23
import {
  Config
  , WatchdogFood
}                     from '../config'
24
import Contact        from '../contact'
25
// import FriendRequest  from '../friend-request'
26 27 28 29 30 31 32 33 34 35 36
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
class PuppetWeb extends Puppet {
41 42 43

  public browser:  Browser

44
  public bridge:   Bridge
45 46 47 48 49 50 51 52
  private server:   Server

  private port: number

  constructor(
      private head: string    = Config.head
    , private profile: string = null  // if not set profile, then do not store session.
  ) {
53
    super()
54 55
    // this.head     = head
    // this.profile  = profile
56

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

    this.on('watchdog', Watchdog.onFeed.bind(this))
61 62
  }

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

65
  public async init(): Promise<PuppetWeb> {
66 67
    log.verbose('PuppetWeb', `init() with head:${this.head}, profile:${this.profile}`)

68 69 70
    // this.readyState('connecting')
    this.targetState('live')
    this.currentState('birthing')
71

72 73
    // return co.call(this, function* () {
    try {
74

75
      this.port = await UtilLib.getPort(DEFAULT_PUPPET_PORT)
76 77
      log.verbose('PuppetWeb', 'init() getPort %d', this.port)

78 79 80
      // @deprecated 20161004
      // yield this.initAttach(this)
      // log.verbose('PuppetWeb', 'initAttach() done')
81

82
      await this.initServer()
83 84
      log.verbose('PuppetWeb', 'initServer() done')

85
      await this.initBrowser()
86 87
      log.verbose('PuppetWeb', 'initBrowser() done')

88
      await this.initBridge()
89 90
      log.verbose('PuppetWeb', 'initBridge() done')

91 92 93 94 95
      const food: WatchdogFood = {
        data: 'inited'
        , timeout: 120000 // 2 mins for first login
      }
      this.emit('watchdog', food)
96 97 98 99

      // return this
    // }).catch(e => {   // Reject
    } catch (e) {
100 101 102
      log.error('PuppetWeb', 'init exception: %s', e.message)
      this.quit()
      throw e
103 104
    }
    // .then(() => {   // Finally
105
      log.verbose('PuppetWeb', 'init() done')
106 107
      // this.readyState('connected')
      this.currentState('live')
108
      return this   // for Chaining
109
    // })
110 111
  }

112
  public async quit(): Promise<any> {
113
    log.verbose('PuppetWeb', 'quit()')
114
    this.targetState('dead')
115

116 117 118 119
    // 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`?')
120 121 122 123 124 125 126 127 128
      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'
    })

129 130
    // this.readyState('disconnecting')
    this.currentState('killing')
131

132 133
    // return co.call(this, function* () {
    try {
134 135

      if (this.bridge)  {
136
        await this.bridge.quit()
137 138 139 140 141 142 143 144
                        .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) {
145
        await this.server.quit()
146 147 148 149 150
        this.server = null
        log.verbose('PuppetWeb', 'quit() server.quit() this.server = null')
      } else { log.verbose('PuppetWeb', 'quit() without a server') }

      if (this.browser) {
151
        await this.browser.quit()
152 153 154 155 156 157 158
                  .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') }

159 160
      // @deprecated 20161004
      // log.verbose('PuppetWeb', 'quit() server.quit() this.initAttach(null)')
161
      // await this.initAttach(null)
162 163

      this.currentState('dead')
164 165
    // }).catch(e => { // Reject
    } catch (e) {
166
      log.error('PuppetWeb', 'quit() exception: %s', e.message)
167
      this.currentState('dead')
168
      throw e
169 170 171
    }

    // .then(() => { // Finally, Fail Safe
172
      log.verbose('PuppetWeb', 'quit() done')
173 174
      // this.readyState('disconnected')
      this.currentState('dead')
175
      return this   // for Chaining
176
    // })
177 178
  }

179
  private async initBrowser(): Promise<Browser> {
180
    log.verbose('PuppetWeb', 'initBrowser()')
181 182 183 184
    const browser = new Browser(
        this.head
      , this.profile
    )
185 186 187

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

188
    this.browser = browser
189

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190 191
    if (this.targetState() !== 'live') {
      log.warn('PuppetWeb', 'initBrowser() found targetState != live, no init anymore')
192 193
      // return Promise.resolve('skipped')
      return Promise.reject('skipped')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
194 195
    }

196 197 198
    // return co.call(this, function* () {
    try {
      await browser.init()
199
      return browser // follow func name meaning
200 201
    // }).catch(e => {
    } catch (e) {
202 203
      log.error('PuppetWeb', 'initBrowser() exception: %s', e.message)
      throw e
204
    }
205 206
  }

207
  private initBridge(): Promise<Bridge> {
208
    log.verbose('PuppetWeb', 'initBridge()')
209 210 211 212
    const bridge = new Bridge(
        this // use puppet instead of browser, is because browser might change(die) duaring run time
      , this.port
    )
213

214 215
    this.bridge = bridge

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
    if (this.targetState() !== 'live') {
217 218 219
      const errMsg = 'initBridge() found targetState != live, no init anymore'
      log.warn('PuppetWeb', errMsg)
      return Promise.reject(errMsg)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
220
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
221

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

235
  private initServer(): Promise<Server> {
236
    log.verbose('PuppetWeb', 'initServer()')
237
    const server = new Server(this.port)
238 239 240 241 242 243 244 245

    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 (李卓桓) 已提交
246
     *
247
     * when `unload` there should always be a `disconnect` event?
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
248
     */
249 250 251 252 253 254 255
    // 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))

256 257
    this.server = server

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    if (this.targetState() !== 'live') {
259 260 261
      const errMsg = 'initServer() found targetState != live, no init anymore'
      log.warn('PuppetWeb', errMsg)
      return Promise.reject(errMsg)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
262 263
    }

264
    return server.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265 266 267 268
                .catch(e => {
                  log.error('PuppetWeb', 'initServer() exception: %s', e.message)
                  throw e
                })
269 270
  }

271 272 273 274 275 276 277 278 279
  public reset(reason?: string) {
    log.verbose('PuppetWeb', 'reset(%s)', reason)

    if (this.browser) {
      this.browser.dead('restart required by reset()')
    } else {
      log.warn('PuppetWeb', 'reset() without browser')
    }
  }
280

281
  public self(message?: Message): boolean | Contact {
282 283 284 285
    if (!this.userId) {
      log.verbose('PuppetWeb', 'self() got no this.userId')
      return false
    }
286 287
    if (message && message.from()) {
      return this.userId === message.get('from')
288
    }
289
    return this.user
290
  }
291

292
  public send(message) {
293 294 295 296 297 298 299 300
    const to      = message.get('to')
    const room    = message.get('room')

    let content     = message.get('content')

    let destination = to
    if (room) {
      destination = room
301
      // TODO use the right @
302 303 304
      // if (to && to!==room) {
      //   content = `@[${to}] ${content}`
      // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305 306 307 308

      if (!to) {
        message.set('to', room)
      }
309 310
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
311
    log.silly('PuppetWeb', `send() destination: ${destination}, content: ${content})`)
312
    return this.bridge.send(destination, content)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
313 314 315 316
                      .catch(e => {
                        log.error('PuppetWeb', 'send() exception: %s', e.message)
                        throw e
                      })
317
  }
318

319
  public reply(message, replyContent) {
320 321 322 323 324 325 326 327 328 329 330 331 332
    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)
333 334 335 336
                .catch(e => {
                  log.error('PuppetWeb', 'reply() exception: %s', e.message)
                  throw e
                })
337 338 339 340 341
  }

  /**
   * logout from browser, then server will emit `logout` event
   */
342
  public logout() {
343
    return this.bridge.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
344 345 346 347
                      .catch(e => {
                        log.error('PuppetWeb', 'logout() exception: %s', e.message)
                        throw e
                      })
348 349
  }

350
  public getContact(id: string): Promise<any> {
351 352 353
    if (!this.bridge) {
      throw new Error('PuppetWeb has no bridge for getContact()')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
354

355
    return this.bridge.getContact(id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
356 357 358 359
                      .catch(e => {
                        log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message)
                        throw e
                      })
360
  }
361 362
  public logined() { return !!(this.user) }
  public ding(data) {
363 364 365 366
    if (!this.bridge) {
      return Promise.reject(new Error('ding fail: no bridge(yet)!'))
    }
    return this.bridge.ding(data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367 368 369 370
                      .catch(e => {
                        log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message)
                        throw e
                      })
371
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
373
  public contactFind(filterFunc: string): Promise<Contact[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
374 375 376
    if (!this.bridge) {
      return Promise.reject(new Error('contactFind fail: no bridge(yet)!'))
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377
    return this.bridge.contactFind(filterFunc)
378
                      .then(idList => idList.map(id => Contact.load(id)))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
379
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
380
                        log.warn('PuppetWeb', 'contactFind(%s) rejected: %s', filterFunc, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
381 382 383 384
                        throw e
                      })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385
  public roomFind(filterFunc: string): Promise<Room[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386 387 388
    if (!this.bridge) {
      return Promise.reject(new Error('findRoom fail: no bridge(yet)!'))
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389
    return this.bridge.roomFind(filterFunc)
390
                      .then(idList => idList.map(id => Room.load(id)))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
391
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
392
                        log.warn('PuppetWeb', 'roomFind(%s) rejected: %s', filterFunc, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
393 394 395 396
                        throw e
                      })
  }

397
  public roomDel(room: Room, contact: Contact): Promise<number> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
398 399 400
    if (!this.bridge) {
      return Promise.reject(new Error('roomDelMember fail: no bridge(yet)!'))
    }
401 402
    const roomId    = room.id
    const contactId = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403 404
    return this.bridge.roomDelMember(roomId, contactId)
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405
                        log.warn('PuppetWeb', 'roomDelMember(%s, %d) rejected: %s', roomId, contactId, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
406 407 408 409
                        throw e
                      })
  }

410
  public roomAdd(room: Room, contact: Contact): Promise<number> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411 412 413
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }
414 415
    const roomId    = room.id
    const contactId = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
416 417 418 419 420
    return this.bridge.roomAddMember(roomId, contactId)
                      .catch(e => {
                        log.warn('PuppetWeb', 'roomAddMember(%s) rejected: %s', contact, e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
421
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
422

423
  public roomTopic(room: Room, topic: string): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
424 425
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
426 427
    }
    if (!room || typeof topic === 'undefined') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
428
      return Promise.reject(new Error('room or topic not found'))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
429
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430 431

    const roomId = room.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
432 433
    return this.bridge.roomModTopic(roomId, topic)
                      .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
434
                        log.warn('PuppetWeb', 'roomTopic(%s) rejected: %s', topic, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
435 436
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
437 438
  }

439
  public roomCreate(contactList: Contact[], topic: string): Promise<Room> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440 441 442 443
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

444
    if (!contactList || ! contactList.map) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
445 446 447
      throw new Error('contactList not found')
    }

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

450 451
// console.log('puppet roomCreate: ')
// console.log(contactIdList)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
    return this.bridge.roomCreate(contactIdList, topic)
453
                      .then(roomId => Room.load(roomId))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454
                      .catch(e => {
455
                        log.warn('PuppetWeb', 'roomCreate(%s, %s) rejected: %s', contactIdList.join(','), topic, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
456 457
                        throw e
                      })
458 459 460 461 462
  }

  /**
   * FriendRequest
   */
463
  public friendRequestSend(contact: Contact, hello: string): Promise<any> {
464 465 466 467
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

468 469
    if (!contact) {
      throw new Error('contact not found')
470
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471

472
    return this.bridge.verifyUserRequest(contact.id, hello)
473
                      .catch(e => {
474
                        log.warn('PuppetWeb', 'bridge.verifyUserRequest(%s, %s) rejected: %s', contact.id, hello, e.message)
475 476
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
477 478
  }

479
  public friendRequestAccept(contact: Contact, ticket: string): Promise<any> {
480 481 482 483
    if (!this.bridge) {
      return Promise.reject(new Error('fail: no bridge(yet)!'))
    }

484 485
    if (!contact || !ticket) {
      throw new Error('contact or ticket not found')
486 487
    }

488
    return this.bridge.verifyUserOk(contact.id, ticket)
489
                      .catch(e => {
490
                        log.warn('PuppetWeb', 'bridge.verifyUserOk(%s, %s) rejected: %s', contact.id, ticket, e.message)
491 492 493
                        throw e
                      })
  }
494 495
}

496 497
// module.exports = PuppetWeb.default = PuppetWeb.PuppetWeb = PuppetWeb
export default PuppetWeb