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<this> {
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 153
    // if (this.inited) {
    if (this.state.current() === 'ready') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
      log.error('Wechaty', 'init() already inited. return and do nothing.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155
      return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
156 157
    }

158 159
    try {
      await this.initPuppet()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160 161
      // this.inited = true
      this.state.current('ready')
162
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
163
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164
      throw e
165
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
166
    return this
167
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168

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

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

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

199
    return this
200 201
  }

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

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

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

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

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

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

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

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

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

260
  public async quit(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261
    log.verbose('Wechaty', 'quit()')
262

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

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

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

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

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

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

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

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

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

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

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

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

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

362
export default Wechaty