wechaty.ts 10.1 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
    }

157 158
    try {
      await this.initPuppet()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159
      this.state.current('ready')
160
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
161
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
      throw e
163
    }
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()')
260

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

266
    const puppetBeforeDie = this.puppet
267 268
    this.puppet     = null
    Config.puppetInstance(null)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269 270
    // this.inited = false
    this.state.current('standby')
271

272 273 274 275 276 277
    await puppetBeforeDie.quit()
                        .catch(e => {
                          log.error('Wechaty', 'quit() exception: %s', e.message)
                          throw e
                        })
    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 293 294 295
  /**
   * @deprecated
   * use Message.self() instead
   */
296
  public self(message: Message): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
    log.warn('Wechaty', 'self() method deprecated. use Message.self() instead')
298 299 300
    if (!this.puppet) {
      throw new Error('no puppet')
    }
301 302
    return this.puppet.self(message)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303

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

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

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

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

336 337 338 339
    if (!this.puppet) {
      throw new Error('no puppet')
    }

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

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

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

360
export default Wechaty