wechaty.ts 9.7 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
  , log
22
}                     from './config'
23

24 25 26 27 28 29 30 31
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'
32

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

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

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

54
  public puppet: Puppet | null
55

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
56
  private state = new StateMonitor<'standby', 'ready'>('Wechaty', 'standby')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
  private npmVersion: string = require('../package.json').version
58
  public uuid:        string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
59

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

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

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

78
    // setting.port    = setting.port    || Config.port
79

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

86
    this.uuid = UtilLib.guid()
87 88
  }

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

91
  public version(forceNpm = false) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92
    // TODO: use  git rev-parse HEAD  ?
93
    const dotGitPath  = path.join(__dirname, '..', '.git') // only for ts-node, not for dist
94 95
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96

97 98
    if (!forceNpm) {
      try {
99
        fs.statSync(dotGitPath).isDirectory()
100 101 102

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

104 105 106 107 108 109 110
        if (ss.status !== 0) {
          throw new Error(ss.error)
        }

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

113
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114 115 116 117
        /**
         *  1. .git not exist
         *  2. git log fail
         */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
        log.silly('Wechaty', 'version() form development environment is not availble: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119
      }
120 121 122
    }

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

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

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
    this.state.target('ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154 155
    this.state.current('ready', false)

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

    this.state.current('ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164
    return
165
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
166

167 168 169 170 171 172 173 174 175 176 177
  // 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
178
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
179

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

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

197
    return this
198 199
  }

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

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

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

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

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

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

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

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

254
    await puppet.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255
    return puppet
256 257
  }

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

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

267
    const puppetBeforeDie = this.puppet
268 269
    this.puppet     = null
    Config.puppetInstance(null)
270

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
292
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293
   * get current user
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295
  public self(): Contact {
296
    if (!this.puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
      throw new Error('Wechaty.self() no puppet')
298
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299
    return this.puppet.self()
300
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301

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

314
  public async say(content: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315 316
    log.verbose('Wechaty', 'say(%s)', content)

317 318 319
    if (!this.puppet) {
      throw new Error('no puppet')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
320
    this.puppet.say(content)
321
    return
322 323
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324 325
  public async sleep(millisecond: number): Promise<void> {
    await new Promise(resolve => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
326 327 328 329
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
330
  public ding() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
331 332 333 334
    if (!this.puppet) {
      return Promise.reject(new Error('wechaty cant ding coz no puppet'))
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335
    return this.puppet.ding() // should return 'dong'
336 337 338 339
                      .catch(e => {
                        log.error('Wechaty', 'ding() exception: %s', e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340
  }
341
}