wechaty.ts 10.2 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
/**
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
3 4
 * Wechaty: Wechat for ChatBots.
 * Connect ChatBots
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
5
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
6
 * Class Wechaty
7
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
8 9 10 11
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */
12 13
import { EventEmitter } from 'events'
import * as fs          from 'fs'
14
import * as path        from 'path'
15

16 17
import {
    Config
18
  , HeadName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
19
  , PuppetName
20
  , Sayable
21
}                     from './config'
22

23 24 25 26
import Contact        from './contact'
import FriendRequest  from './friend-request'
import Message        from './message'
import Puppet         from './puppet'
27
import PuppetWeb      from './puppet-web/'
28
import Room           from './room'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
29
import StateMonitor   from './state-monitor'
30
import UtilLib        from './util-lib'
31

32
import log            from './brolog-env'
33

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
34
export type PuppetSetting = {
35
  head?:    HeadName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
36
  puppet?:  PuppetName
37
  profile?: string
38
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
39

40 41 42 43 44 45 46 47 48 49 50
export type WechatyEventName = 'error'
                              | 'friend'
                              | 'heartbeat'
                              | 'login'
                              | 'logout'
                              | 'message'
                              | 'room-join'
                              | 'room-leave'
                              | 'room-topic'
                              | 'scan'
                              | 'EVENT_PARAM_ERROR'
51

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
52
export class Wechaty extends EventEmitter implements Sayable {
53
  private static _instance: Wechaty
54

55
  public puppet: Puppet | null
56

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
  private state = new StateMonitor<'standby', 'ready'>('Wechaty', 'standby')
58
  private npmVersion: string
59 60

  public uuid:        string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
61

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62
  public static instance(setting?: PuppetSetting) {
63
    if (setting && this._instance) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
64
      throw new Error('there has already a instance. no params will be allowed any more')
65 66 67 68 69 70 71
    }
    if (!this._instance) {
      this._instance = new Wechaty(setting)
    }
    return this._instance
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
72
  private constructor(private setting: PuppetSetting = {}) {
73
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74 75
    log.verbose('Wechaty', 'contructor()')

76
    setting.head    = setting.head    || Config.head
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
77
    setting.puppet  = setting.puppet  || Config.puppet
78 79
    setting.profile = setting.profile || Config.profile

80
    // setting.port    = setting.port    || Config.port
81

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82 83
    if (setting.profile) {
      setting.profile  = /\.wechaty\.json$/i.test(setting.profile)
84 85
                        ? setting.profile
                        : setting.profile + '.wechaty.json'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
87

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88
    this.npmVersion = require('../package.json').version
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89

90
    this.uuid = UtilLib.guid()
91 92
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
  public toString() { return 'Class Wechaty(' + this.setting.puppet + ')'}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
94

95
  public version(forceNpm = false) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96
    // TODO: use  git rev-parse HEAD  ?
97
    const dotGitPath  = path.join(__dirname, '..', '..', '.git') // `/dist/src/../../.git`
98 99
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100

101 102
    if (!forceNpm) {
      try {
103
        fs.statSync(dotGitPath).isDirectory()
104 105 106

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

108 109 110 111 112 113 114
        if (ss.status !== 0) {
          throw new Error(ss.error)
        }

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

117
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118 119 120 121
        /**
         *  1. .git not exist
         *  2. git log fail
         */
122
        log.silly('Wechaty', 'version() test %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123
      }
124 125 126
    }

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

Huan (李卓桓)'s avatar
fix #54  
Huan (李卓桓) 已提交
129 130 131 132 133 134
  public user(): Contact {
    if (!this.puppet || !this.puppet.user) {
      throw new Error('no user')
    }
    return this.puppet.user
  }
135

136
  public async reset(reason?: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
137
    log.verbose('Wechaty', 'reset() because %s', reason)
138 139 140
    if (!this.puppet) {
      throw new Error('no puppet')
    }
141 142
    await this.puppet.reset(reason)
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143
  }
144

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145
  public async init(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
146
    log.info('Wechaty', 'v%s initializing...' , this.version())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147
    log.verbose('Wechaty', 'puppet: %s'       , this.setting.puppet)
148 149
    log.verbose('Wechaty', 'head: %s'         , this.setting.head)
    log.verbose('Wechaty', 'profile: %s'      , this.setting.profile)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
150
    log.verbose('Wechaty', 'uuid: %s'         , this.uuid)
151

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152
    if (this.state.current() === 'ready') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
      log.error('Wechaty', 'init() already inited. return and do nothing.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155 156
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157 158
    this.state.current('ready', false)

159 160 161
    try {
      await this.initPuppet()
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
162
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
163
      throw e
164
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
165 166

    this.state.current('ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167
    return
168
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
169

170 171 172 173 174 175 176 177 178 179 180
  // public on(event: WechatyEventName, listener: Function): this
  public on(event: 'error'      , listener: (this: Wechaty, error: Error) => void): this
  public on(event: 'friend'     , listener: (this: Wechaty, friend: Contact, request?: FriendRequest) => void): this
  public on(event: 'heartbeat'  , listener: (this: Wechaty, data: any) => void): this
  public on(event: 'logout'     , listener: (this: Wechaty, user: Contact) => void): this
  public on(event: 'login'      , listener: (this: Wechaty, user: Contact) => void): this
  public on(event: 'message'    , listener: (this: Wechaty, message: Message) => void): this
  public on(event: 'room-join'  , listener: (this: Wechaty, room: Room, inviteeList: Contact[],  inviter: Contact) => void): this
  public on(event: 'room-leave' , listener: (this: Wechaty, room: Room, leaverList: Contact[]) => void): this
  public on(event: 'room-topic' , listener: (this: Wechaty, room: Room, topic: string, oldTopic: string, changer: Contact) => void): this
  public on(event: 'scan'       , listener: (this: Wechaty, url: string, code: number) => void): this
181
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
182

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186 187 188 189 190 191 192 193 194 195 196 197 198
    // 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)
    // })
199

200
    return this
201 202
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203
  public async initPuppet(): Promise<Puppet> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204
    let puppet: Puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205 206 207 208 209

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210
    switch (this.setting.puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
211
      case 'web':
212
        puppet = new PuppetWeb({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
213
            head:     this.setting.head
214 215
          , profile:  this.setting.profile
        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
        break
217

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
218
      default:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
219
        throw new Error('Puppet unsupport(yet?): ' + this.setting.puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
220
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
221

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222 223 224 225 226 227 228 229 230 231 232 233 234 235
    const eventList: WechatyEventName[] = [
        'error'
      , 'friend'
      , 'heartbeat'
      , 'login'
      , 'logout'
      , 'message'
      , 'room-join'
      , 'room-leave'
      , 'room-topic'
      , 'scan'
    ]

    eventList.map(e => {
236 237
      // 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) => ...)
238
      puppet.on(e, (...args: any[]) => {
239 240
        // this.emit(e, data)
        this.emit.apply(this, [e, ...args])
241
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
242
    })
243 244 245 246 247 248 249 250
    /**
     * TODO: support more events:
     * 2. send
     * 3. reply
     * 4. quit
     * 5. ...
     */

251
    // set puppet before init, because we need this.puppet if we quit() before init() finish
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
    this.puppet = <Puppet>puppet // force to use base class Puppet interface for better encapsolation
253 254 255

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

257
    await puppet.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    return puppet
259 260
  }

261
  public async quit(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
262
    log.verbose('Wechaty', 'quit()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
    this.state.current('standby', false)
264

265 266
    if (!this.puppet) {
      log.warn('Wechaty', 'quit() without this.puppet')
267
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268 269
    }

270
    const puppetBeforeDie = this.puppet
271 272
    this.puppet     = null
    Config.puppetInstance(null)
273

274 275 276 277 278
    await puppetBeforeDie.quit()
                        .catch(e => {
                          log.error('Wechaty', 'quit() exception: %s', e.message)
                          throw e
                        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
    this.state.current('standby')
280
    return
281
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282

283
  public async logout(): Promise<void>  {
284 285 286
    if (!this.puppet) {
      throw new Error('no puppet')
    }
287 288 289 290 291 292
    await this.puppet.logout()
                    .catch(e => {
                      log.error('Wechaty', 'logout() exception: %s', e.message)
                      throw e
                    })
    return
293
  }
294

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295 296 297 298
  /**
   * @deprecated
   * use Message.self() instead
   */
299
  public self(message: Message): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
    log.warn('Wechaty', 'self() method deprecated. use Message.self() instead')
301 302 303
    if (!this.puppet) {
      throw new Error('no puppet')
    }
304 305
    return this.puppet.self(message)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306

307
  public async send(message: Message): Promise<void> {
308 309 310
    if (!this.puppet) {
      throw new Error('no puppet')
    }
311
    await this.puppet.send(message)
312 313 314 315
                      .catch(e => {
                        log.error('Wechaty', 'send() exception: %s', e.message)
                        throw e
                      })
316
    return
317
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
318

319
  public async say(content: string): Promise<void> {
320 321 322
    if (!this.puppet) {
      throw new Error('no puppet')
    }
323 324
    await this.puppet.say(content)
    return
325 326
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327 328 329 330 331 332
  public sleep(millisecond: number): Promise<void> {
    return new Promise(resolve => {
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333 334 335
  /**
   * @deprecated
   */
336
  public reply(message: Message, reply: string) {
337
    log.warn('Wechaty', 'reply() @deprecated, please use Message.say() instead')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
338

339 340 341 342
    if (!this.puppet) {
      throw new Error('no puppet')
    }

343 344 345 346 347 348
    return this.puppet.reply(message, reply)
    .catch(e => {
      log.error('Wechaty', 'reply() exception: %s', e.message)
      throw e
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
349

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
350
  public ding() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
351 352 353 354
    if (!this.puppet) {
      return Promise.reject(new Error('wechaty cant ding coz no puppet'))
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
355
    return this.puppet.ding() // should return 'dong'
356 357 358 359
                      .catch(e => {
                        log.error('Wechaty', 'ding() exception: %s', e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
360
  }
361 362
}

363
export default Wechaty