wechaty.ts 8.2 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 path         from 'path'
13
// const co            = require('co')
14 15
import * as fs          from'fs'

16 17 18
import Config, {
    HeadType
  , PuppetType
19
  , WechatyEventType
20 21
} from './config'

22 23 24 25 26 27 28 29
import Contact        from './contact'
import FriendRequest  from './friend-request'
import Message        from './message'
import Puppet         from './puppet'
import PuppetWeb      from './puppet-web/index'
import Room           from './room'
import UtilLib        from './util-lib'
import EventScope     from './event-scope'
30

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

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

40 41
class Wechaty extends EventEmitter {
  private static _instance: Wechaty
42

43
  public puppet: Puppet
44

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

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

50 51 52 53 54 55 56 57 58 59
  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
  }

60
  private constructor(private setting: WechatySetting) {
61
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62 63
    log.verbose('Wechaty', 'contructor()')

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

68 69 70 71
    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
72

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

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

81
    this.uuid = UtilLib.guid()
82

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

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

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

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

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

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

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

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

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

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

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

144 145 146
    // return co.call(this, function* () {
    try {
      await this.initPuppet()
147

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148
      this.inited = true
149 150 151
      // return this // for chaining
    // }).catch(e => {
    } catch (e) {
152
      log.error('Wechaty', 'init() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
      throw e
154 155
    }
    return this
156
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157

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

  public on(event: WechatyEventType, listener: Function): this {
171
    log.verbose('Wechaty', 'on(%s, %s)', event, typeof listener)
172 173
    typeof FriendRequest
    typeof Room
174

175 176
    const listenerWithScope = EventScope.wrap.call(this, event, listener)
    super.on(event, listenerWithScope)
177

178
    return this
179 180
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
194
    EventScope.list().map(e => {
195 196 197 198 199
      // 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])
200
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
    })
202 203 204 205 206 207 208 209
    /**
     * TODO: support more events:
     * 2. send
     * 3. reply
     * 4. quit
     * 5. ...
     */

210
    // set puppet before init, because we need this.puppet if we quit() before init() finish
211 212 213 214
    this.puppet     = puppet

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
    return puppet.init()
217 218
  }

219
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
220
    log.verbose('Wechaty', 'quit()')
221

222 223
    if (!this.puppet) {
      log.warn('Wechaty', 'quit() without this.puppet')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
224 225 226
      return Promise.resolve()
    }

227
    const puppetBeforeDie = this.puppet
228 229
    this.puppet     = null
    Config.puppetInstance(null)
230 231
    this.inited = false

232
    return puppetBeforeDie.quit()
233 234 235 236 237
    .catch(e => {
      log.error('Wechaty', 'quit() exception: %s', e.message)
      throw e
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238

239
  public logout()  {
240
    return this.puppet.logout()
241 242 243 244
                      .catch(e => {
                        log.error('Wechaty', 'logout() exception: %s', e.message)
                        throw e
                      })
245
  }
246

247
  public self(message?: Message): boolean | Contact {
248 249
    return this.puppet.self(message)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250

251
  public send(message: Message): Promise<any> {
252
    return this.puppet.send(message)
253 254 255 256
                      .catch(e => {
                        log.error('Wechaty', 'send() exception: %s', e.message)
                        throw e
                      })
257
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258

259
  public reply(message: Message, reply: string) {
260 261 262 263 264 265
    return this.puppet.reply(message, reply)
    .catch(e => {
      log.error('Wechaty', 'reply() exception: %s', e.message)
      throw e
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266

267
  public ding(data: string) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268 269 270 271
    if (!this.puppet) {
      return Promise.reject(new Error('wechaty cant ding coz no puppet'))
    }

272
    return this.puppet.ding(data)
273 274 275 276
                      .catch(e => {
                        log.error('Wechaty', 'ding() exception: %s', e.message)
                        throw e
                      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277
  }
278 279
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
280 281 282
/**
 * Expose `Wechaty`.
 */
283 284
// module.exports = Wechaty.default = Wechaty.Wechaty = Wechaty
export default Wechaty