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
import {
17 18
    // Config
    ScanInfo
19 20
  , WatchdogFood
}                     from '../config'
21

22
import Contact        from '../contact'
23
// import FriendRequest  from '../friend-request'
24 25 26 27 28 29 30 31 32 33 34
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'
35

36
export type PuppetWebSetting = {
37 38 39
  head?:    string
  profile?: string
}
40 41
const DEFAULT_PUPPET_PORT = 18788 // // W(87) X(88), ascii char code ;-]

42
export class PuppetWeb extends Puppet {
43

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
44 45
  public browser: Browser
  public bridge:  Bridge
46
  public server:  Server
47

48
  public scan: ScanInfo | null
49

50 51
  private port: number

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
52 53 54 55
  public lastScanEventTime: number
  public watchDogLastSaveSession: number

  constructor(public setting: PuppetWebSetting = {}) {
56
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57 58

    this.on('watchdog', Watchdog.onFeed.bind(this))
59 60
  }

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

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

66 67 68
    // this.readyState('connecting')
    this.targetState('live')
    this.currentState('birthing')
69

70 71
    // return co.call(this, function* () {
    try {
72

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

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

80
      await this.initServer()
81 82
      log.verbose('PuppetWeb', 'initServer() done')

83
      await this.initBrowser()
84 85
      log.verbose('PuppetWeb', 'initBrowser() done')

86
      await this.initBridge()
87 88
      log.verbose('PuppetWeb', 'initBridge() done')

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

      // return this
    // }).catch(e => {   // Reject
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
98 99
      log.error('PuppetWeb', 'init() exception: %s', e.stack)
      await this.quit()
100
      throw e
101 102
    }
    // .then(() => {   // Finally
103
      log.verbose('PuppetWeb', 'init() done')
104 105
      // this.readyState('connected')
      this.currentState('live')
106
      return this   // for Chaining
107
    // })
108 109
  }

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

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

127 128
    // this.readyState('disconnecting')
    this.currentState('killing')
129

130 131
    // return co.call(this, function* () {
    try {
132

133
      if (this.bridge)  { // TODO use StateMonitor
134
        await this.bridge.quit()
135 136 137 138
                        .catch(e => { // fail safe
                          log.warn('PuppetWeb', 'quit() bridge.quit() exception: %s', e.message)
                        })
        log.verbose('PuppetWeb', 'quit() bridge.quit() this.bridge = null')
139
        // this.bridge = null
140 141
      } else { log.warn('PuppetWeb', 'quit() without a bridge') }

142
      if (this.server) { // TODO use StateMonitor
143
        await this.server.quit()
144
        // this.server = null
145 146 147
        log.verbose('PuppetWeb', 'quit() server.quit() this.server = null')
      } else { log.verbose('PuppetWeb', 'quit() without a server') }

148
      if (this.browser) { // TODO use StateMonitor
149
        await this.browser.quit()
150 151 152 153
                  .catch(e => { // fail safe
                    log.warn('PuppetWeb', 'quit() browser.quit() exception: %s', e.message)
                  })
        log.verbose('PuppetWeb', 'quit() server.quit() this.browser = null')
154
        // this.browser = null
155 156
      } else { log.warn('PuppetWeb', 'quit() without a browser') }

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

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

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

177
  public async initBrowser(): Promise<Browser> {
178
    log.verbose('PuppetWeb', 'initBrowser()')
179 180 181 182
    const browser = new Browser({
        head:         this.setting.head
      , sessionFile:  this.setting.profile
    })
183 184 185

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

186
    this.browser = browser
187

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

194 195 196 197 198
    // return co.call(this, function* () {
    try {
      await browser.init()
    // }).catch(e => {
    } catch (e) {
199 200
      log.error('PuppetWeb', 'initBrowser() exception: %s', e.message)
      throw e
201
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202
    return browser // follow func name meaning
203 204
  }

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

212 213
    this.bridge = bridge

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

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

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

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

254 255
    this.server = server

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

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

269 270 271 272 273 274 275 276 277
  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')
    }
  }
278

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

290 291 292
  public send(message: Message) {
    const to      = message.to()
    const room    = message.room()
293

294
    let content     = message.content()
295

296
    let destination: Contact|Room = to
297 298
    if (room) {
      destination = room
299
      // TODO use the right @
300 301 302
      // if (to && to!==room) {
      //   content = `@[${to}] ${content}`
      // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303 304

      if (!to) {
305
        message.to(room)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
      }
307 308
    }

309
    log.silly('PuppetWeb', 'send() destination: %s, content: %s)'
310
                          , room ? room.topic() : (to as Contact).name()
311 312 313
                          , content
    )
    return this.bridge.send(destination.id, content)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
314 315 316 317
                      .catch(e => {
                        log.error('PuppetWeb', 'send() exception: %s', e.message)
                        throw e
                      })
318
  }
319

320 321 322 323 324 325 326 327 328 329 330 331 332
  /**
   * Bot say...
   * send to `filehelper` for notice / log
   */
  public say(content: string) {
    const m = new Message()
    m.to('filehelper')
    m.content(content)

    return this.send(m)
  }

  // @deprecated
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333 334
  public reply(message: Message, replyContent: string) {
    log.warn('PuppetWeb', 'reply() @deprecated, please use Message.say()')
335 336 337
    if (this.self(message)) {
      return Promise.reject(new Error('will not to reply message of myself'))
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
338
    return message.say(replyContent)
339 340 341 342 343
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

498 499
// module.exports = PuppetWeb.default = PuppetWeb.PuppetWeb = PuppetWeb
export default PuppetWeb