wechaty.ts 11.2 KB
Newer Older
1 2
import { EventEmitter } from 'events'
import * as fs          from 'fs'
3
import * as path        from 'path'
4

5 6
import {
    Config
7
  , HeadName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
8
  , PuppetName
9
  , Sayable
10
  , log
11
}                     from './config'
12

13 14 15 16 17 18 19 20
import { Contact }        from './contact'
import { FriendRequest }  from './friend-request'
import { Message }        from './message'
import { Puppet }         from './puppet'
import { PuppetWeb }      from './puppet-web/'
import { Room }           from './room'
import { StateMonitor }   from './state-monitor'
import { UtilLib }        from './util-lib'
21

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
22
export type PuppetSetting = {
23
  head?:    HeadName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
24
  puppet?:  PuppetName
25
  profile?: string
26
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
27

28 29 30 31 32 33 34 35 36 37 38
export type WechatyEventName = 'error'
                              | 'friend'
                              | 'heartbeat'
                              | 'login'
                              | 'logout'
                              | 'message'
                              | 'room-join'
                              | 'room-leave'
                              | 'room-topic'
                              | 'scan'
                              | 'EVENT_PARAM_ERROR'
39

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
40 41 42 43 44 45 46 47 48 49 50
/**
 *
 * Wechaty: Wechat for ChatBots.
 * Connect ChatBots
 *
 * Class Wechaty
 *
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51
export class Wechaty extends EventEmitter implements Sayable {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
52 53 54
  /**
   * singleton _instance
   */
55
  private static _instance: Wechaty
56

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57 58 59
  /**
   * the puppet
   */
60
  public puppet: Puppet | null
61

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62 63 64
  /**
   * the state
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
65
  private state = new StateMonitor<'standby', 'ready'>('Wechaty', 'standby')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
66 67 68
  /**
   * the npmVersion
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
69
  private npmVersion: string = require('../package.json').version
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
70 71 72
  /**
   * the uuid
   */
73
  public uuid:        string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
75 76 77
  /**
   *
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78
  public static instance(setting?: PuppetSetting) {
79
    if (setting && this._instance) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80
      throw new Error('there has already a instance. no params will be allowed any more')
81 82 83 84 85 86 87
    }
    if (!this._instance) {
      this._instance = new Wechaty(setting)
    }
    return this._instance
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88 89 90
  /**
   *
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
91
  private constructor(private setting: PuppetSetting = {}) {
92
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93 94
    log.verbose('Wechaty', 'contructor()')

95
    setting.head    = setting.head    || Config.head
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96
    setting.puppet  = setting.puppet  || Config.puppet
97 98
    setting.profile = setting.profile || Config.profile

99
    // setting.port    = setting.port    || Config.port
100

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101 102
    if (setting.profile) {
      setting.profile  = /\.wechaty\.json$/i.test(setting.profile)
103 104
                        ? setting.profile
                        : setting.profile + '.wechaty.json'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
105
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106

107
    this.uuid = UtilLib.guid()
108 109
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
110 111 112
  /**
   *
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
113
  public toString() { return 'Class Wechaty(' + this.setting.puppet + ')'}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115 116 117 118 119 120 121 122 123 124 125 126
  /**
   * Return version of Wechaty
   * @param {boolean} [forceNpm=false] if set to true, will only return the version in package.json.
   *                            otherwise will return git commit hash if .git exists.
   * @returns {string} version number
   * @example
   *  console.log(Wechaty.instance().version())
   *  // #git[af39df]
   *  console.log(Wechaty.instance().version(true))
   *  // 0.7.9
   */
  public version(forceNpm = false): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127
    // TODO: use  git rev-parse HEAD  ?
128
    const dotGitPath  = path.join(__dirname, '..', '.git') // only for ts-node, not for dist
129 130
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
131

132 133
    if (!forceNpm) {
      try {
134
        fs.statSync(dotGitPath).isDirectory()
135 136 137

        const ss = require('child_process')
                    .spawnSync(gitLogCmd, gitLogArgs, { cwd:  __dirname })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138

139 140 141 142 143 144 145
        if (ss.status !== 0) {
          throw new Error(ss.error)
        }

        const revision = ss.stdout
                          .toString()
                          .trim()
146
        return `#git[${revision}]`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147

148
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
149 150 151 152
        /**
         *  1. .git not exist
         *  2. git log fail
         */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
        log.silly('Wechaty', 'version() form development environment is not availble: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
      }
155 156 157
    }

    return this.npmVersion
158
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160 161 162
  /**
   * @todo document me
   */
Huan (李卓桓)'s avatar
fix #54  
Huan (李卓桓) 已提交
163 164 165 166 167 168
  public user(): Contact {
    if (!this.puppet || !this.puppet.user) {
      throw new Error('no user')
    }
    return this.puppet.user
  }
169

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170 171 172
  /**
   *
   */
173
  public async reset(reason?: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
174
    log.verbose('Wechaty', 'reset() because %s', reason)
175 176 177
    if (!this.puppet) {
      throw new Error('no puppet')
    }
178 179
    await this.puppet.reset(reason)
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
180
  }
181

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
182 183 184
  /**
   * @todo document me
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
185
  public async init(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
    log.info('Wechaty', 'v%s initializing...' , this.version())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
187
    log.verbose('Wechaty', 'puppet: %s'       , this.setting.puppet)
188 189
    log.verbose('Wechaty', 'head: %s'         , this.setting.head)
    log.verbose('Wechaty', 'profile: %s'      , this.setting.profile)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190
    log.verbose('Wechaty', 'uuid: %s'         , this.uuid)
191

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
192
    if (this.state.current() === 'ready') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
193
      log.error('Wechaty', 'init() already inited. return and do nothing.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
194
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
195 196
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
197
    this.state.target('ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
198 199
    this.state.current('ready', false)

200 201 202
    try {
      await this.initPuppet()
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
203
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204
      throw e
205
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206 207

    this.state.current('ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
208
    return
209
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210

211
  // public on(event: WechatyEventName, listener: Function): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212 213 214 215 216
  /**
   * @param {string} [event='error'] the `error` event
   * @param {Function} listener (error) => void callback function
   * @return {Wechaty} this for chain
   */
217
  public on(event: 'error'      , listener: (this: Wechaty, error: Error) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
218 219 220
  /**
   * @todo document me
   */
221
  public on(event: 'friend'     , listener: (this: Wechaty, friend: Contact, request?: FriendRequest) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222 223 224
  /**
   * @todo document me
   */
225
  public on(event: 'heartbeat'  , listener: (this: Wechaty, data: any) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
226 227 228
  /**
   * @todo document me
   */
229
  public on(event: 'logout'     , listener: (this: Wechaty, user: Contact) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230 231 232
  /**
   * @todo document me
   */
233
  public on(event: 'login'      , listener: (this: Wechaty, user: Contact) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234 235 236
  /**
   * @todo document me
   */
237
  public on(event: 'message'    , listener: (this: Wechaty, message: Message) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238 239 240
  /**
   * @todo document me
   */
241
  public on(event: 'room-join'  , listener: (this: Wechaty, room: Room, inviteeList: Contact[],  inviter: Contact) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
242 243 244
  /**
   * @todo document me
   */
245
  public on(event: 'room-leave' , listener: (this: Wechaty, room: Room, leaverList: Contact[]) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246 247 248
  /**
   * @todo document me
   */
249
  public on(event: 'room-topic' , listener: (this: Wechaty, room: Room, topic: string, oldTopic: string, changer: Contact) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250 251 252
  /**
   * @todo document me
   */
253
  public on(event: 'scan'       , listener: (this: Wechaty, url: string, code: number) => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254 255 256
  /**
   * @todo document me
   */
257
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258 259 260
  /**
   * @todo document me
   */
261

262
  public on(event: WechatyEventName, listener: Function): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
    log.verbose('Wechaty', 'addListener(%s, %s)', event, typeof listener)
264

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265 266 267 268 269 270 271 272 273 274 275 276 277
    // const thisWithSay: Sayable = {
    //   say: (content: string) => {
    //     return Config.puppetInstance()
    //                   .say(content)
    //   }
    // }

    super.on(event, listener) // `this: Wechaty` is Sayable

    // (...args) => {
    //
    //   return listener.apply(this, args)
    // })
278

279
    return this
280 281
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282 283 284
  /**
   * @todo document me
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
  public async initPuppet(): Promise<Puppet> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286
    let puppet: Puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287 288 289 290 291

    if (!this.setting.head) {
      throw new Error('no head')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
292
    switch (this.setting.puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293
      case 'web':
294
        puppet = new PuppetWeb({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295
            head:     this.setting.head
296 297
          , profile:  this.setting.profile
        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
        break
299

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
      default:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
        throw new Error('Puppet unsupport(yet?): ' + this.setting.puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304 305 306 307 308 309 310 311 312 313 314 315 316 317
    const eventList: WechatyEventName[] = [
        'error'
      , 'friend'
      , 'heartbeat'
      , 'login'
      , 'logout'
      , 'message'
      , 'room-join'
      , 'room-leave'
      , 'room-topic'
      , 'scan'
    ]

    eventList.map(e => {
318 319
      // https://strongloop.com/strongblog/an-introduction-to-javascript-es6-arrow-functions/
      // We’ve lost () around the argument list when there’s just one argument (rest arguments are an exception, eg (...args) => ...)
320
      puppet.on(e, (...args: any[]) => {
321 322
        // this.emit(e, data)
        this.emit.apply(this, [e, ...args])
323
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324
    })
325 326 327 328 329 330 331 332
    /**
     * TODO: support more events:
     * 2. send
     * 3. reply
     * 4. quit
     * 5. ...
     */

333
    // set puppet before init, because we need this.puppet if we quit() before init() finish
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334
    this.puppet = <Puppet>puppet // force to use base class Puppet interface for better encapsolation
335 336 337

    // set puppet instance to Wechaty Static variable, for using by Contact/Room/Message/FriendRequest etc.
    Config.puppetInstance(puppet)
338

339
    await puppet.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340
    return puppet
341 342
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343 344 345
  /**
   * @todo document me
   */
346
  public async quit(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
347
    log.verbose('Wechaty', 'quit()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348
    this.state.current('standby', false)
349

350 351
    if (!this.puppet) {
      log.warn('Wechaty', 'quit() without this.puppet')
352
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353 354
    }

355
    const puppetBeforeDie = this.puppet
356 357
    this.puppet     = null
    Config.puppetInstance(null)
358

359 360 361 362 363
    await puppetBeforeDie.quit()
                        .catch(e => {
                          log.error('Wechaty', 'quit() exception: %s', e.message)
                          throw e
                        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364
    this.state.current('standby')
365
    return
366
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
368 369 370
  /**
   * @todo document me
   */
371
  public async logout(): Promise<void>  {
372 373 374
    if (!this.puppet) {
      throw new Error('no puppet')
    }
375 376 377 378 379 380
    await this.puppet.logout()
                    .catch(e => {
                      log.error('Wechaty', 'logout() exception: %s', e.message)
                      throw e
                    })
    return
381
  }
382

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
384
   * get current user
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386
  public self(): Contact {
387
    if (!this.puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
388
      throw new Error('Wechaty.self() no puppet')
389
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
390
    return this.puppet.self()
391
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
392

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
393 394 395
  /**
   * @todo document me
   */
396
  public async send(message: Message): Promise<void> {
397 398 399
    if (!this.puppet) {
      throw new Error('no puppet')
    }
400
    await this.puppet.send(message)
401 402 403 404
                      .catch(e => {
                        log.error('Wechaty', 'send() exception: %s', e.message)
                        throw e
                      })
405
    return
406
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
408 409 410
  /**
   * @todo document me
   */
411
  public async say(content: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
412 413
    log.verbose('Wechaty', 'say(%s)', content)

414 415 416
    if (!this.puppet) {
      throw new Error('no puppet')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
    this.puppet.say(content)
418
    return
419 420
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
421 422 423
  /**
   * @todo document me
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
424 425
  public async sleep(millisecond: number): Promise<void> {
    await new Promise(resolve => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426 427 428 429
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430 431 432
  /**
   * @todo document me
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
433
  public ding() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
434 435 436 437
    if (!this.puppet) {
      return Promise.reject(new Error('wechaty cant ding coz no puppet'))
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
438
    return this.puppet.ding() // should return 'dong'
439 440 441 442
                      .catch(e => {
                        log.error('Wechaty', 'ding() exception: %s', e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
443
  }
444
}