wechaty.ts 9.9 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 18
import {
    Config
  , HeadType
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 29
import Room           from './room'
import UtilLib        from './util-lib'
30

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

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

39 40 41 42 43 44 45 46 47 48 49 50
type WechatyEventName = 'error'
                      | 'friend'
                      | 'heartbeat'
                      | 'login'
                      | 'logout'
                      | 'message'
                      | 'room-join'
                      | 'room-leave'
                      | 'room-topic'
                      | 'scan'
                      | 'EVENT_PARAM_ERROR'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51
export class Wechaty extends EventEmitter implements Sayable {
52
  private static _instance: Wechaty
53

54
  public puppet: Puppet | null
55

56 57
  private inited:     boolean = false
  private npmVersion: string
58 59

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

61 62 63 64 65 66 67 68 69 70
  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 (李卓桓) 已提交
71
  private constructor(private setting: WechatySetting = {}) {
72
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
73 74
    log.verbose('Wechaty', 'contructor()')

75 76 77
    // if (Wechaty._instance instanceof Wechaty) {
    //   throw new Error('Wechaty must be singleton')
    // }
78

79 80 81 82
    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
83

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

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

92
    this.uuid = UtilLib.guid()
93

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

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

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

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

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

        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()
124
        return `#git[${revision}]`
125
      } catch (e) { /* fall safe */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
126 127 128 129
        /**
         *  1. .git not exist
         *  2. git log fail
         */
130
        log.silly('Wechaty', 'version() test %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
131
      }
132 133 134
    }

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
  public async init(): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
    log.info('Wechaty', 'v%s initializing...' , this.version())
155 156 157
    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 (李卓桓) 已提交
158
    log.verbose('Wechaty', 'uuid: %s'         , this.uuid)
159

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160 161
    if (this.inited) {
      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
      this.inited = true
168
    } catch (e) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
169
      log.error('Wechaty', 'init() exception: %s', e && e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
      throw e
171
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172
    return this
173
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
174

175
  public on(event: 'error'      , listener: (this: Sayable, error: Error) => void): this
176
  public on(event: 'friend'     , listener: (this: Sayable, friend: Contact, request?: FriendRequest) => void): this
177 178 179
  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
180
  public on(event: 'message'    , listener: (this: Sayable, message: Message) => void): this
181 182
  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, leaverList: Contact[]) => void): this
183 184
  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
185
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
186

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

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

204
    return this
205 206
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
217
      default:
218
        throw new Error('Puppet unsupport(yet): ' + this.setting.type)
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
251
    this.puppet = puppet
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)
271 272
    this.inited = false

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

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

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

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

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

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

331 332 333 334
    if (!this.puppet) {
      throw new Error('no puppet')
    }

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

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

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

355
export default Wechaty