wechaty.ts 10.3 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
    const dotGitPath  = path.join(__dirname, '..', '.git') // `/src/../.git`
97 98
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
99

100 101
    if (!forceNpm) {
      try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102 103
        /**
         * Synchronous version of fs.access().
104
         * This throws if any accessibility checks fail, and does nothing otherwise.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
105
         */
106
        // fs.accessSync(dotGitPath, fs.F_OK)
107
        fs.statSync(dotGitPath).isDirectory()
108 109 110

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

112 113 114 115 116 117 118
        if (ss.status !== 0) {
          throw new Error(ss.error)
        }

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

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

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

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

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

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

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

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

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

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

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

203
    return this
204 205
  }

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

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

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

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

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

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

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

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

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

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

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

272
    const puppetBeforeDie = this.puppet
273 274
    this.puppet     = null
    Config.puppetInstance(null)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
275 276
    // this.inited = false
    this.state.current('standby')
277

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

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

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

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

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

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

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

342 343 344 345
    if (!this.puppet) {
      throw new Error('no puppet')
    }

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

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

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

366
export default Wechaty