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

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

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

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

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

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

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278
    // 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)
    // })
279

280
    return this
281 282
  }

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

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

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

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

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

    eventList.map(e => {
319 320
      // 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) => ...)
321
      puppet.on(e, (...args: any[]) => {
322 323
        // this.emit(e, data)
        this.emit.apply(this, [e, ...args])
324
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
325
    })
326 327 328 329 330 331 332 333
    /**
     * TODO: support more events:
     * 2. send
     * 3. reply
     * 4. quit
     * 5. ...
     */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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