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
19
  , PuppetType
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

34
export type WechatySetting = {
35 36 37
  head?:    HeadName
  type?:    PuppetType
  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 58
  // private inited:     boolean = false
  private state = new StateMonitor<'standby', 'ready'>('Wechaty', 'standby')
59
  private npmVersion: string
60 61

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

63 64 65 66 67 68 69 70 71 72
  public static instance(setting?: WechatySetting) {
    if (setting && this._instance) {
      throw new Error('there has already a instance. no params allowed any more')
    }
    if (!this._instance) {
      this._instance = new Wechaty(setting)
    }
    return this._instance
  }

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

77
    setting.head    = setting.head    || Config.head
78 79 80
    setting.type    = setting.type    || Config.puppet
    setting.profile = setting.profile || Config.profile

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

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

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

91
    this.uuid = UtilLib.guid()
92

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
    // this.inited = false
94

95
    // Wechaty._instance = this
96 97 98
  }

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

100
  public version(forceNpm = false) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101
    const dotGitPath  = path.join(__dirname, '..', '.git') // `/src/../.git`
102 103
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104

105 106
    if (!forceNpm) {
      try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107 108
        /**
         * Synchronous version of fs.access().
109
         * This throws if any accessibility checks fail, and does nothing otherwise.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
110
         */
111
        // fs.accessSync(dotGitPath, fs.F_OK)
112
        fs.statSync(dotGitPath).isDirectory()
113 114 115 116 117 118 119 120 121 122

        const ss = require('child_process')
                    .spawnSync(gitLogCmd, gitLogArgs, { cwd:  __dirname })
        if (ss.status !== 0) {
          throw new Error(ss.error)
        }

        const revision = ss.stdout
                          .toString()
                          .trim()
123
        return `#git[${revision}]`
124
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
125 126 127 128
        /**
         *  1. .git not exist
         *  2. git log fail
         */
129
        log.silly('Wechaty', 'version() test %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
130
      }
131 132 133
    }

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

Huan (李卓桓)'s avatar
fix #54  
Huan (李卓桓) 已提交
136 137 138 139 140 141
  public user(): Contact {
    if (!this.puppet || !this.puppet.user) {
      throw new Error('no user')
    }
    return this.puppet.user
  }
142

143
  public async reset(reason?: string): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
144
    log.verbose('Wechaty', 'reset() because %s', reason)
145 146 147
    if (!this.puppet) {
      throw new Error('no puppet')
    }
148 149
    await this.puppet.reset(reason)
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
150
  }
151

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152
  public async init(): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
    log.info('Wechaty', 'v%s initializing...' , this.version())
154 155 156
    log.verbose('Wechaty', 'puppet: %s'       , this.setting.type)
    log.verbose('Wechaty', 'head: %s'         , this.setting.head)
    log.verbose('Wechaty', 'profile: %s'      , this.setting.profile)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157
    log.verbose('Wechaty', 'uuid: %s'         , this.uuid)
158

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159 160
    // if (this.inited) {
    if (this.state.current() === 'ready') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
161
      log.error('Wechaty', 'init() already inited. return and do nothing.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
      return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
163 164
    }

165 166
    try {
      await this.initPuppet()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167 168
      // this.inited = true
      this.state.current('ready')
169
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
170
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
171
      throw e
172
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173
    return this
174
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
175

176 177 178 179 180 181 182 183 184 185 186
  // 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
187
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
188

189
  public on(event: WechatyEventName, listener: Function): this {
190
    log.verbose('Wechaty', 'on(%s, %s)', event, typeof listener)
191

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204
    // 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)
    // })
205

206
    return this
207 208
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
209
  public async initPuppet(): Promise<Puppet> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210
    let puppet: Puppet
211
    switch (this.setting.type) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
      case 'web':
213
        puppet = new PuppetWeb({
214
            head:     <HeadName>this.setting.head
215 216
          , profile:  this.setting.profile
        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
217
        break
218

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

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

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

252
    // set puppet before init, because we need this.puppet if we quit() before init() finish
253
    this.puppet = puppet
254 255 256

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

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

262
  public async quit(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
    log.verbose('Wechaty', 'quit()')
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)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
273 274
    // this.inited = false
    this.state.current('standby')
275

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

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

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

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

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

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

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

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

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

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

358
export default Wechaty