wechaty.ts 8.7 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
/**
 *
Huan (李卓桓)'s avatar
comment  
Huan (李卓桓) 已提交
3
 * wechaty: Wechat for ChatBots.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
5
 * Class Wechaty
6
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
7 8 9 10
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */
11 12
import { EventEmitter } from 'events'
import * as fs          from 'fs'
13
import * as path        from 'path'
14

15 16 17
import {
    Config
  , HeadType
18
  , PuppetType
19
  , Sayable
20 21
  , WechatyEventName
}                     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 29
import Room           from './room'
import UtilLib        from './util-lib'
30
// import EventScope     from './event-scope'
31

32
import log            from './brolog-env'
33

34
export type WechatySetting = {
35
  profile?:    string
36 37 38
  head?:       HeadType
  type?:       PuppetType
  // port?:       number
39
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
40

41
export class Wechaty extends EventEmitter {
42
  private static _instance: Wechaty
43

44
  public puppet: Puppet
45

46 47
  private inited:     boolean = false
  private npmVersion: string
48 49

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

51 52 53 54 55 56 57 58 59 60
  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 (李卓桓) 已提交
61
  private constructor(private setting: WechatySetting = {}) {
62
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
63 64
    log.verbose('Wechaty', 'contructor()')

65 66 67
    // if (Wechaty._instance instanceof Wechaty) {
    //   throw new Error('Wechaty must be singleton')
    // }
68

69 70 71 72
    setting.type    = setting.type    || Config.puppet
    setting.head    = setting.head    || Config.head
    // setting.port    = setting.port    || Config.port
    setting.profile = setting.profile || Config.profile  // no profile, no session save/restore
73

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74 75
    if (setting.profile) {
      setting.profile  = /\.wechaty\.json$/i.test(setting.profile)
76 77
                        ? setting.profile
                        : setting.profile + '.wechaty.json'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
79

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

82
    this.uuid = UtilLib.guid()
83

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
84
    this.inited = false
85

86
    // Wechaty._instance = this
87 88 89
  }

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

91
  public version(forceNpm = false) {
92 93 94
    const dotGitPath  = path.join(__dirname, '..', '.git')
    const gitLogCmd   = 'git'
    const gitLogArgs  = ['log', '--oneline', '-1']
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95

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

        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()
113
        return `#git[${revision}]`
114
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115 116 117 118
        /**
         *  1. .git not exist
         *  2. git log fail
         */
119
        log.silly('Wechaty', 'version() test %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
      }
121 122 123
    }

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

126
  public user(): Contact { return this.puppet && this.puppet.user }
127

128
  public reset(reason?: string) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
129
    log.verbose('Wechaty', 'reset() because %s', reason)
130
    return this.puppet.reset(reason)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
131
  }
132

133
  public async init(): Promise<Wechaty> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134
    log.info('Wechaty', 'v%s initializing...' , this.version())
135 136 137
    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 (李卓桓) 已提交
138
    log.verbose('Wechaty', 'uuid: %s'         , this.uuid)
139

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140 141 142 143 144
    if (this.inited) {
      log.error('Wechaty', 'init() already inited. return and do nothing.')
      return Promise.resolve(this)
    }

145 146
    try {
      await this.initPuppet()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147
      this.inited = true
148
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
149
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
150
      throw e
151 152
    }
    return this
153
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154

155 156 157 158 159 160 161 162 163 164 165
  public on(event: 'error'      , listener: (this: Sayable, error: Error) => void): this
  public on(event: 'friend'     , listener: (this: Sayable, friend: Contact, request: FriendRequest) => void): this
  public on(event: 'heartbeat'  , listener: (this: Sayable, data: any) => void): this
  public on(event: 'logout'     , listener: (this: Sayable, user: Contact) => void): this
  public on(event: 'login'      , listener: (this: Sayable, user: Contact) => void): this
  public on(event: 'message'    , listener: (this: Sayable, message: Message, n: number) => void): this
  public on(event: 'room-join'  , listener: (this: Sayable, room: Room, invitee:     Contact, inviter: Contact) => void): this
  public on(event: 'room-join'  , listener: (this: Sayable, room: Room, inviteeList: Contact, inviter: Contact) => void): this
  public on(event: 'room-leave' , listener: (this: Sayable, room: Room, leaver: Contact) => void): this
  public on(event: 'room-topic' , listener: (this: Sayable, room: Room, topic: string, oldTopic: string, changer: Contact) => void): this
  public on(event: 'scan'       , listener: (this: Sayable, url: string, code: number) => void): this
166

167
  public on(event: WechatyEventName, listener: Function): this {
168
    log.verbose('Wechaty', 'on(%s, %s)', event, typeof listener)
169

170 171 172 173 174 175 176 177 178
    const thisWithSay: Sayable = {
      say: (content: string) => {
        return Config.puppetInstance()
                      .say(content)
      }
    }
    super.on(event, function() {
      return listener.apply(thisWithSay, arguments)
    })
179

180
    return this
181 182
  }

183
  public initPuppet() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184
    let puppet
185
    switch (this.setting.type) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
      case 'web':
187 188 189 190
        puppet = new PuppetWeb({
            head:     this.setting.head
          , profile:  this.setting.profile
        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191
        break
192

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
193
      default:
194
        throw new Error('Puppet unsupport(yet): ' + this.setting.type)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
195
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
196

197 198 199 200 201 202 203 204 205 206 207 208
  ; // must have a semicolon here to seperate the last line with `[]`
  [   'error'
    , 'friend'
    , 'heartbeat'
    , 'login'
    , 'logout'
    , 'message'
    , 'room-join'
    , 'room-leave'
    , 'room-topic'
    , 'scan'
  ].map(e => {
209 210 211 212 213
      // 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) => ...)
      puppet.on(e, (...args) => {
        // this.emit(e, data)
        this.emit.apply(this, [e, ...args])
214
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215
    })
216 217 218 219 220 221 222 223
    /**
     * TODO: support more events:
     * 2. send
     * 3. reply
     * 4. quit
     * 5. ...
     */

224
    // set puppet before init, because we need this.puppet if we quit() before init() finish
225
    this.puppet = puppet
226 227 228

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230
    return puppet.init()
231 232
  }

233
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234
    log.verbose('Wechaty', 'quit()')
235

236 237
    if (!this.puppet) {
      log.warn('Wechaty', 'quit() without this.puppet')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238 239 240
      return Promise.resolve()
    }

241
    const puppetBeforeDie = this.puppet
242 243
    this.puppet     = null
    Config.puppetInstance(null)
244 245
    this.inited = false

246
    return puppetBeforeDie.quit()
247 248 249 250 251
    .catch(e => {
      log.error('Wechaty', 'quit() exception: %s', e.message)
      throw e
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252

253
  public logout()  {
254
    return this.puppet.logout()
255 256 257 258
                      .catch(e => {
                        log.error('Wechaty', 'logout() exception: %s', e.message)
                        throw e
                      })
259
  }
260

261
  public self(message?: Message): boolean | Contact {
262 263
    return this.puppet.self(message)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
264

265
  public send(message: Message): Promise<any> {
266
    return this.puppet.send(message)
267 268 269 270
                      .catch(e => {
                        log.error('Wechaty', 'send() exception: %s', e.message)
                        throw e
                      })
271
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
272

273 274 275 276 277
  public say(content: string) {
    return this.puppet.say(content)
  }

  /// @deprecated
278
  public reply(message: Message, reply: string) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279 280
    log.warn('Wechaty', 'reply() @deprecated, please use Message.say()')

281 282 283 284 285 286
    return this.puppet.reply(message, reply)
    .catch(e => {
      log.error('Wechaty', 'reply() exception: %s', e.message)
      throw e
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287

288
  public ding(data: string) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289 290 291 292
    if (!this.puppet) {
      return Promise.reject(new Error('wechaty cant ding coz no puppet'))
    }

293
    return this.puppet.ding(data)
294 295 296 297
                      .catch(e => {
                        log.error('Wechaty', 'ding() exception: %s', e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
  }
299 300
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301 302 303
/**
 * Expose `Wechaty`.
 */
304 305
// module.exports = Wechaty.default = Wechaty.Wechaty = Wechaty
export default Wechaty