wechaty.ts 28.0 KB
Newer Older
1
/**
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5
 *
6 7 8
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
9
 *
10
 *       http://www.apache.org/licenses/LICENSE-2.0
11
 *
12 13 14 15 16
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
L
lijiarui 已提交
17 18
 *
 *  @ignore
19
 */
20 21
import * as cuid    from 'cuid'
import * as os      from 'os'
22
import * as semver  from 'semver'
23

24
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
25
  // Constructor,
26
  cloneClass,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
27
  // instanceToClass,
28
}                   from 'clone-class'
29 30 31
import {
  callerResolve,
  hotImport,
32 33
}                   from 'hot-import'
import StateSwitch  from 'state-switch'
34

35 36 37
import {
  Accessory,
}                       from './accessory'
38
import {
39
  VERSION,
40
  config,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
41
  log,
42
  Raven,
43
  Sayable,
44 45
}                       from './config'
import Profile          from './profile'
46

M
Mukaiu 已提交
47
import {
48 49 50
  PUPPET_DICT,
  PuppetName,
}                       from './puppet-config'
51 52 53
import {
  Puppet, PuppetOptions,
}                       from './puppet/'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
54

55
import {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
56
  Contact,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57 58
}                       from './contact'
import {
59
  FriendRequest,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60 61
}                       from './friend-request'
import {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
62
  Message,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
63 64
}                       from './message'
import {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
65
  Room,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
66
}                       from './room'
67 68 69 70 71 72 73 74 75 76 77

export const WECHAT_EVENT_DICT = {
  friend      : 'tbw',
  login       : 'tbw',
  logout      : 'tbw',
  message     : 'tbw',
  'room-join' : 'tbw',
  'room-leave': 'tbw',
  'room-topic': 'tbw',
  scan        : 'tbw',
}
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
78

79 80
export const WECHATY_EVENT_DICT = {
  ...WECHAT_EVENT_DICT,
81 82 83 84
  error     : 'tbw',
  heartbeat : 'tbw',
  start     : 'tbw',
  stop      : 'tbw',
85 86 87 88
}

export type WechatEventName   = keyof typeof WECHAT_EVENT_DICT
export type WechatyEventName  = keyof typeof WECHATY_EVENT_DICT
89 90

export interface WechatyOptions {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
91 92
  puppet?  : PuppetName | Puppet,
  profile? : null | string,
93
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
94

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95
/**
L
lijiarui 已提交
96
 * Main bot class.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
97
 *
L
lijiarui 已提交
98
 * [The World's Shortest ChatBot Code: 6 lines of JavaScript]{@link #wechatyinstance}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
99
 *
L
lijiarui 已提交
100 101 102
 * [Wechaty Starter Project]{@link https://github.com/lijiarui/wechaty-getting-started}
 * @example
 * import { Wechaty } from 'wechaty'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104
 */
105
export class Wechaty extends Accessory implements Sayable {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
  /**
107
   * singleton globalInstance
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
108
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109
   */
110
  private static globalInstance: Wechaty
111

112 113
  private profile: Profile

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114 115
  /**
   * the state
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
116
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
  private state = new StateSwitch('Wechaty', log)
119

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
  /**
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
121
   * the cuid
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
122
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123
   */
124
  public readonly id : string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
125

126
  // tslint:disable-next-line:variable-name
127
  public readonly Contact       : typeof Contact
128
  // tslint:disable-next-line:variable-name
129
  public readonly FriendRequest : typeof FriendRequest
130
  // tslint:disable-next-line:variable-name
131
  public readonly Message       : typeof Message
132
  // tslint:disable-next-line:variable-name
133
  public readonly Room          : typeof Room
134

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
135
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
136
   * get the singleton instance of Wechaty
L
lijiarui 已提交
137 138 139 140 141 142 143 144 145
   *
   * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
   * const { Wechaty } = require('wechaty')
   *
   * Wechaty.instance() // Singleton
   * .on('scan', (url, code) => console.log(`Scan QR Code to login: ${code}\n${url}`))
   * .on('login',       user => console.log(`User ${user} logined`))
   * .on('message',  message => console.log(`Message: ${message}`))
   * .init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
146
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147 148 149
  public static instance(
    options?: WechatyOptions,
  ) {
150
    if (options && this.globalInstance) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151
      throw new Error('instance can be only set once!')
152
    }
153 154
    if (!this.globalInstance) {
      this.globalInstance = new Wechaty(options)
155
    }
156
    return this.globalInstance
157 158
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
   * @public
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
161
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
  constructor(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
163
    private options: WechatyOptions = {},
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164
  ) {
165
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
166 167
    log.verbose('Wechaty', 'contructor()')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168
    options.profile = options.profile === null
169
                      ? null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
                      : (options.profile || config.default.DEFAULT_PROFILE)
171

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172
    this.profile = new Profile(options.profile)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173

174
    this.id = cuid()
175 176 177 178 179 180 181 182 183 184 185 186 187

    /**
     * Clone Classes for this bot and attach the `puppet` to the Class
     *
     *   https://stackoverflow.com/questions/36886082/abstract-constructor-type-in-typescript
     *   https://github.com/Microsoft/TypeScript/issues/5843#issuecomment-290972055
     *   https://github.com/Microsoft/TypeScript/issues/19197
     */
    // TODO: make Message & Room constructor private???
    this.Contact        = cloneClass(Contact)
    this.FriendRequest  = cloneClass(FriendRequest)
    this.Message        = cloneClass(Message)
    this.Room           = cloneClass(Room)
188 189
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
191
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
192
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
193 194 195 196 197 198
  public toString() {
    if (!this.options) {
      return this.constructor.name
    }

    return [
199 200
      'Wechaty#',
      this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201 202 203 204
      `<${this.options && this.options.puppet || ''}>`,
      `(${this.profile && this.profile.name   || ''})`,
    ].join('')
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
208
   */
209
  public static version(forceNpm = false): string {
210
    if (!forceNpm) {
211
      const revision = config.gitRevision()
212
      if (revision) {
213
        return `#git[${revision}]`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
214
      }
215
    }
216
    return VERSION
217
  }
218

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
219
 /**
Huan (李卓桓)'s avatar
linting  
Huan (李卓桓) 已提交
220 221 222 223 224 225 226 227 228
  * Return version of Wechaty
  *
  * @param {boolean} [forceNpm=false]  - if set to true, will only return the version in package.json.
  *                                      otherwise will return git commit hash if .git exists.
  * @returns {string}                  - the version number
  * @example
  * console.log(Wechaty.instance().version())       // return '#git[af39df]'
  * console.log(Wechaty.instance().version(true))   // return '0.7.9'
  */
229
  public version(forceNpm = false): string {
230
    return Wechaty.version(forceNpm)
231
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
232

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
  public emit(event: 'error'      , error: Error)                                                  : boolean
  public emit(event: 'friend'     , request: FriendRequest)                                        : boolean
  public emit(event: 'heartbeat'  , data: any)                                                     : boolean
  public emit(event: 'logout'     , user: Contact)                                                 : boolean
  public emit(event: 'login'      , user: Contact)                                                 : boolean
  public emit(event: 'message'    , message: Message)                                              : boolean
  public emit(event: 'room-join'  , room: Room, inviteeList : Contact[], inviter  : Contact)       : boolean
  public emit(event: 'room-leave' , room: Room, leaverList  : Contact[], remover? : Contact)       : boolean
  public emit(event: 'room-topic' , room: Room, topic: string, oldTopic: string, changer: Contact) : boolean
  public emit(event: 'scan'       , qrCode: string, code: number, data?: string)                   : boolean
  public emit(event: 'start')                                                                      : boolean
  public emit(event: 'stop')                                                                       : boolean

  // guard for the above event: make sure it includes all the possible values
  public emit(event: never, listener: never): never

  public emit(
    event:   WechatyEventName,
    ...args: any[]
  ): boolean {
    return super.emit(event, ...args)
  }

256
  public on(event: 'error'      , listener: string | ((this: Wechaty, error: Error) => void))                                                 : this
257
  public on(event: 'friend'     , listener: string | ((this: Wechaty, request: FriendRequest) => void))                     : this
258 259 260 261 262 263 264
  public on(event: 'heartbeat'  , listener: string | ((this: Wechaty, data: any) => void))                                                    : this
  public on(event: 'logout'     , listener: string | ((this: Wechaty, user: Contact) => void))                                                : this
  public on(event: 'login'      , listener: string | ((this: Wechaty, user: Contact) => void))                                                : this
  public on(event: 'message'    , listener: string | ((this: Wechaty, message: Message) => void))                                             : this
  public on(event: 'room-join'  , listener: string | ((this: Wechaty, room: Room, inviteeList: Contact[],  inviter: Contact) => void))        : this
  public on(event: 'room-leave' , listener: string | ((this: Wechaty, room: Room, leaverList: Contact[], remover?: Contact) => void))         : this
  public on(event: 'room-topic' , listener: string | ((this: Wechaty, room: Room, topic: string, oldTopic: string, changer: Contact) => void)): this
265
  public on(event: 'scan'       , listener: string | ((this: Wechaty, url: string, code: number) => void))                                   : this
266 267
  public on(event: 'start'      , listener: string | ((this: Wechaty) => void))                                                               : this
  public on(event: 'stop'       , listener: string | ((this: Wechaty) => void))                                                               : this
268

269
  // guard for the above event: make sure it includes all the possible values
270
  public on(event: never, listener: never): never
L
lijiarui 已提交
271

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
272
  /**
L
lijiarui 已提交
273 274 275 276 277 278 279 280 281 282 283 284
   * @desc       Wechaty Class Event Type
   * @typedef    WechatyEventName
   * @property   {string}  error      - When the bot get error, there will be a Wechaty error event fired.
   * @property   {string}  login      - After the bot login full successful, the event login will be emitted, with a Contact of current logined user.
   * @property   {string}  logout     - Logout will be emitted when bot detected log out, with a Contact of the current login user.
   * @property   {string}  heartbeat  - Get bot's heartbeat.
   * @property   {string}  friend     - When someone sends you a friend request, there will be a Wechaty friend event fired.
   * @property   {string}  message    - Emit when there's a new message.
   * @property   {string}  room-join  - Emit when anyone join any room.
   * @property   {string}  room-topic - Get topic event, emitted when someone change room topic.
   * @property   {string}  room-leave - Emit when anyone leave the room.<br>
   *                                    If someone leaves the room by themselves, wechat will not notice other people in the room, so the bot will never get the "leave" event.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
   * @property   {string}  scan       - A scan event will be emitted when the bot needs to show you a QR Code for scanning.
L
lijiarui 已提交
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
   */

  /**
   * @desc       Wechaty Class Event Function
   * @typedef    WechatyEventFunction
   * @property   {Function} error           -(this: Wechaty, error: Error) => void callback function
   * @property   {Function} login           -(this: Wechaty, user: Contact)=> void
   * @property   {Function} logout          -(this: Wechaty, user: Contact) => void
   * @property   {Function} scan            -(this: Wechaty, url: string, code: number) => void <br>
   * <ol>
   * <li>URL: {String} the QR code image URL</li>
   * <li>code: {Number} the scan status code. some known status of the code list here is:</li>
   * </ol>
   * <ul>
   * <li>0 initial_</li>
   * <li>200 login confirmed</li>
   * <li>201 scaned, wait for confirm</li>
   * <li>408 waits for scan</li>
   * </ul>
   * @property   {Function} heartbeat       -(this: Wechaty, data: any) => void
306
   * @property   {Function} friend          -(this: Wechaty, request?: FriendRequest) => void
L
lijiarui 已提交
307 308 309 310 311 312 313 314
   * @property   {Function} message         -(this: Wechaty, message: Message) => void
   * @property   {Function} room-join       -(this: Wechaty, room: Room, inviteeList: Contact[],  inviter: Contact) => void
   * @property   {Function} room-topic      -(this: Wechaty, room: Room, topic: string, oldTopic: string, changer: Contact) => void
   * @property   {Function} room-leave      -(this: Wechaty, room: Room, leaverList: Contact[]) => void
   */

  /**
   * @listens Wechaty
315
   * @param   {WechatyEventName}      event      - Emit WechatyEvent
L
lijiarui 已提交
316 317 318
   * @param   {WechatyEventFunction}  listener   - Depends on the WechatyEvent
   * @return  {Wechaty}                          - this for chain
   *
319
   * More Example Gist: [Examples/Friend-Bot]{@link https://github.com/Chatie/wechaty/blob/master/examples/friend-bot.ts}
L
lijiarui 已提交
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
   *
   * @example <caption>Event:scan </caption>
   * wechaty.on('scan', (url: string, code: number) => {
   *   console.log(`[${code}] Scan ${url} to login.` )
   * })
   *
   * @example <caption>Event:login </caption>
   * bot.on('login', (user: Contact) => {
   *   console.log(`user ${user} login`)
   * })
   *
   * @example <caption>Event:logout </caption>
   * bot.on('logout', (user: Contact) => {
   *   console.log(`user ${user} logout`)
   * })
   *
   * @example <caption>Event:message </caption>
   * wechaty.on('message', (message: Message) => {
   *   console.log(`message ${message} received`)
   * })
   *
   * @example <caption>Event:friend </caption>
342 343 344
   * bot.on('friend', (request: FriendRequest) => {
   *   if(request.type === FriendRequest.Type.RECEIVE){ // 1. receive new friend request from new contact
   *     const contact = request.contact()
L
lijiarui 已提交
345 346 347 348 349 350
   *     let result = await request.accept()
   *       if(result){
   *         console.log(`Request from ${contact.name()} is accept succesfully!`)
   *       } else{
   *         console.log(`Request from ${contact.name()} failed to accept!`)
   *       }
351
   * 	  } else if (request.type === FriendRequest.Type.CONFIRM) { // 2. confirm friend ship
L
lijiarui 已提交
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
   *       console.log(`new friendship confirmed with ${contact.name()}`)
   *    }
   *  })
   *
   * @example <caption>Event:room-join </caption>
   * bot.on('room-join', (room: Room, inviteeList: Contact[], inviter: Contact) => {
   *   const nameList = inviteeList.map(c => c.name()).join(',')
   *   console.log(`Room ${room.topic()} got new member ${nameList}, invited by ${inviter}`)
   * })
   *
   * @example <caption>Event:room-leave </caption>
   * bot.on('room-leave', (room: Room, leaverList: Contact[]) => {
   *   const nameList = leaverList.map(c => c.name()).join(',')
   *   console.log(`Room ${room.topic()} lost member ${nameList}`)
   * })
   *
   * @example <caption>Event:room-topic </caption>
   * bot.on('room-topic', (room: Room, topic: string, oldTopic: string, changer: Contact) => {
   *   console.log(`Room ${room.topic()} topic changed from ${oldTopic} to ${topic} by ${changer.name()}`)
   * })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372
   */
373
  public on(event: WechatyEventName, listener: string | ((...args: any[]) => any)): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
374
    log.verbose('Wechaty', 'on(%s, %s) registered',
375 376 377 378 379 380
                            event,
                            typeof listener === 'string'
                              ? listener
                              : typeof listener,
                )

381
    if (typeof listener === 'function') {
382
      this.addListenerFunction(event, listener)
383
    } else {
384
      this.addListenerModuleFile(event, listener)
385
    }
386
    return this
387 388
  }

389
  private addListenerModuleFile(event: WechatyEventName, modulePath: string): void {
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
    const absoluteFilename = callerResolve(modulePath, __filename)
    log.verbose('Wechaty', 'onModulePath() hotImpor(%s)', absoluteFilename)
    hotImport(absoluteFilename)
      .then((func: Function) => super.on(event, (...args: any[]) => {
        try {
          func.apply(this, args)
        } catch (e) {
          log.error('Wechaty', 'onModulePath(%s, %s) listener exception: %s',
                                event, modulePath, e)
          this.emit('error', e)
        }
      }))
      .catch(e => {
        log.error('Wechaty', 'onModulePath(%s, %s) hotImport() exception: %s',
                              event, modulePath, e)
        this.emit('error', e)
      })
  }

409
  private addListenerFunction(event: WechatyEventName, listener: Function): void {
410 411 412 413 414 415 416 417 418 419 420 421
    log.verbose('Wechaty', 'onFunction(%s)', event)

    super.on(event, (...args: any[]) => {
      try {
        listener.apply(this, args)
      } catch (e) {
        log.error('Wechaty', 'onFunction(%s) listener exception: %s', event, e)
        this.emit('error', e)
      }
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
422
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
423 424 425 426 427 428
   * set Wechaty attach to the puppet,
   * this function should be called before `start()`
   *
   * Will be called from the Puppet in constructor:
   *  When we declare a wechaty without a puppet instance,
   *  the wechaty need to attach to puppet at here.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
429
   */
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
  // public attach(puppet: Puppet) {
  //   log.verbose('Wechaty', 'attach(%s) this.options.puppet="%s"',
  //                           puppet,
  //                           this.options.puppet && this.options.puppet.toString() || '',
  //               )

  //   if (this.options.puppet instanceof Puppet) {
  //     if (this.options.puppet === puppet) {
  //       log.silly('Wechaty', 'attach(%s) called again', puppet)
  //       return
  //     } else {
  //       throw new Error('puppet can only be attached once!')
  //     }
  //   }

  //   this.options.puppet = puppet
  // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
447

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
449
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
450
   */
451
  private initPuppet(): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
    log.verbose('Wechaty', 'initPuppet()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454
    if (!this.options.puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
455
      log.info('Wechaty', 'initPuppet() using default puppet: %s', config.puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
456
      this.options.puppet  = config.puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457 458
    }

459 460 461 462 463 464 465 466 467 468 469 470
    const puppet = this.initPuppetResolver(this.options.puppet)

    if (!this.initPuppetSemverSatisfy(
      puppet.wechatyVersionRange(),
    )) {
      throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
          + `requires a version range(${puppet.wechatyVersionRange()}) `
          + `that is not satisfying the Wechaty version: ${this.version()}.`,
        )
    }

    this.initPuppetEventBridge(puppet)
471
    this.initAccessory(puppet)
472 473 474 475 476 477 478 479 480
  }

  /**
   * Init the Puppet
   */
  private initPuppetResolver(puppet: PuppetName | Puppet): Puppet {
    log.verbose('Wechaty', 'initPuppetResolver(%s)', puppet)

    if (typeof puppet === 'string') {
481
      // tslint:disable-next-line:variable-name
482 483 484 485 486
      const MyPuppet = PUPPET_DICT[puppet]
      if (!MyPuppet) {
        throw new Error('no such puppet: ' + puppet)
      }

487 488 489
      const options: PuppetOptions = {
        profile : this.profile,
        // wechaty:  this,
490 491
      }

492
      return new MyPuppet(options)
493

494 495
    } else if (puppet instanceof Puppet) {
      return puppet
496 497
    } else {
      throw new Error('unsupported options.puppet!')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
498
    }
499
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
500

501 502 503 504
  /**
   * Plugin Version Range Check
   */
  private initPuppetSemverSatisfy(versionRange: string) {
505
    log.verbose('Wechaty', 'initPuppetSemverSatisfy(%s)', versionRange)
506
    return semver.satisfies(
507
      this.version(true),
508 509 510
      versionRange,
    )
  }
511

512
  private initPuppetEventBridge(puppet: Puppet) {
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
    const eventNameList: WechatyEventName[] = Object.keys(WECHATY_EVENT_DICT) as any
    for (const eventName of eventNameList) {
      log.verbose('Wechaty', 'initPuppetEventBridge() puppet.on(%s) registered', eventName)
      // /// e as any ??? Maybe this is a bug of TypeScript v2.5.3
      // puppet.on(event as any, (...args: any[]) => {
      //   this.emit(event, ...args)
      // })

      switch (eventName) {
        case 'error':
          puppet.removeAllListeners('error')
          puppet.on('error', error => {
            this.emit('error', new Error(error))
          })
          break

        case 'heartbeat':
          puppet.removeAllListeners('heartbeat')
          puppet.on('heartbeat', data => {
            this.emit('heartbeat', data)
          })
          break

        case 'start':
        case 'stop':
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
538
          // do not emit 'start'/'stop' again for wechaty
539 540
          break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
541 542 543 544 545 546 547 548 549 550 551 552 553 554
        // case 'start':
        //   puppet.removeAllListeners('start')
        //   puppet.on('start', () => {
        //     this.emit('start')
        //   } )
        //   break

        // case 'stop':
        //   puppet.removeAllListeners('stop')
        //   puppet.on('stop', () => {
        //     this.emit('stop')
        //   } )
        //   break

555 556
        case 'friend':
          puppet.removeAllListeners('friend')
557 558 559
          puppet.on('friend', async requestId => {
            const request = this.FriendRequest.load(requestId)
            await request.ready()
560
            this.emit('friend', request)
561
            request.contact().emit('friend', request)
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
          })
          break

        case 'login':
          puppet.removeAllListeners('login')
          puppet.on('login', async contactId => {
            const contact = this.Contact.load(contactId)
            await contact.ready()
            this.emit('login', contact)
          })
          break

        case 'logout':
          puppet.removeAllListeners('logout')
          puppet.on('logout', async contactId => {
            const contact = this.Contact.load(contactId)
            await contact.ready()
            this.emit('logout', contact)
          })
          break

        case 'message':
          puppet.removeAllListeners('message')
          puppet.on('message', async messageId => {
            const msg = this.Message.create(messageId)
            await msg.ready()
            this.emit('message', msg)
          })
          break

        case 'room-join':
          puppet.removeAllListeners('room-join')
          puppet.on('room-join', async (roomId, inviteeIdList, inviterId) => {
            const room = this.Room.load(roomId)
            await room.ready()

            const inviteeList = inviteeIdList.map(id => this.Contact.load(id))
            await Promise.all(inviteeList.map(c => c.ready()))

            const inviter = this.Contact.load(inviterId)
            await inviter.ready()

            this.emit('room-join', room, inviteeList, inviter)
605
            room.emit('join', inviteeList, inviter)
606 607 608 609 610 611 612 613 614 615 616 617 618
          })
          break

        case 'room-leave':
          puppet.removeAllListeners('room-leave')
          puppet.on('room-leave', async (roomId, leaverIdList) => {
            const room = this.Room.load(roomId)
            await room.ready()

            const leaverList = leaverIdList.map(id => this.Contact.load(id))
            await Promise.all(leaverList.map(c => c.ready()))

            this.emit('room-leave', room, leaverList)
619
            room.emit('leave', leaverList)
620 621 622 623 624 625 626 627 628 629 630 631 632
          })
          break

        case 'room-topic':
          puppet.removeAllListeners('room-topic')
          puppet.on('room-topic', async (roomId, topic, oldTopic, changerId) => {
            const room = this.Room.load(roomId)
            await room.ready()

            const changer = this.Contact.load(changerId)
            await changer.ready()

            this.emit('room-topic', room, topic, oldTopic, changer)
633
            room.emit('topic', topic, oldTopic, changer)
634 635 636 637 638 639 640 641 642 643 644 645 646 647
          })
          break

        case 'scan':
          puppet.removeAllListeners('scan')
          puppet.on('scan', async (qrCode, code, data) => {
            this.emit('scan', qrCode, code, data)
          })
          break

        default:
          throw new Error('eventName ' + eventName + 'unsupported!')

      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
648
    }
649
  }
650

651 652
  private initAccessory(puppet: Puppet) {
    log.verbose('Wechaty', 'initAccessory(%s)', puppet)
653

654 655 656
    /**
     * 1. Set Puppet
     */
657 658 659 660 661
    this.Contact.puppet       = puppet
    this.FriendRequest.puppet = puppet
    this.Message.puppet       = puppet
    this.Room.puppet          = puppet

662 663 664
    /**
     * 2. Set Wechaty
     */
665 666 667 668 669
    this.Contact.wechaty       = this
    this.FriendRequest.wechaty = this
    this.Message.wechaty       = this
    this.Room.wechaty          = this

670 671 672
    /**
     * 3. Set Puppet/Wechaty for Wechaty-self
     */
673 674
    this.puppet  = puppet
    this.wechaty = this
675 676
  }

677 678 679 680 681 682 683 684 685 686 687 688
  /**
   * Start the bot, return Promise.
   *
   * @returns {Promise<void>}
   * @example
   * await bot.start()
   * // do other stuff with bot here
   */
  public async start(): Promise<void> {
    log.info('Wechaty', 'v%s starting...' , this.version())
    log.verbose('Wechaty', 'puppet: %s'   , this.options.puppet)
    log.verbose('Wechaty', 'profile: %s'  , this.options.profile)
689
    log.verbose('Wechaty', 'id: %s'       , this.id)
690 691 692 693 694 695 696 697 698 699 700

    if (this.state.on()) {
      log.silly('Wechaty', 'start() on a starting/started instance')
      await this.state.ready()
      log.silly('Wechaty', 'start() state.ready() resolved')
      return
    }

    this.state.on('pending')

    try {
701 702
      await this.profile.load()
      await this.initPuppet()
703 704 705 706

      await this.puppet.start()

    } catch (e) {
707
      // console.log(e)
708 709 710 711 712 713 714 715 716 717 718 719 720
      log.error('Wechaty', 'start() exception: %s', e && e.message)
      Raven.captureException(e)
      throw e
    }

    this.on('heartbeat', () => this.memoryCheck())

    this.state.on(true)
    this.emit('start')

    return
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
721 722 723 724 725 726 727 728
  /**
   * Stop the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.stop()
   */
  public async stop(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
729
    log.verbose('Wechaty', 'stop()')
730

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
731
    if (this.state.off()) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
732 733 734
      log.silly('Wechaty', 'stop() on an stopping/stopped instance')
      await this.state.ready('off')
      log.silly('Wechaty', 'stop() state.ready(off) resolved')
735
      return
736
    }
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
737

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
738
    this.state.off('pending')
739

740 741 742 743
    let puppet: Puppet
    try {
      puppet = this.puppet
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
744
      log.warn('Wechaty', 'stop() without this.puppet')
745
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
746 747
    }

748
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
749
      await puppet.stop()
750
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
751
      log.error('Wechaty', 'stop() exception: %s', e.message)
752 753
      Raven.captureException(e)
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
754
    } finally {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
755
      this.state.off(true)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
756
      this.emit('stop')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
757

758 759
      // MUST use setImmediate at here(the end of this function),
      // because we need to run the micro task registered by the `emit` method
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
760
      setImmediate(() => puppet.removeAllListeners())
761
    }
762
    return
763
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
764

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
765
  /**
L
lijiarui 已提交
766 767 768 769 770
   * Logout the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
771
   */
772
  public async logout(): Promise<void>  {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
773 774
    log.verbose('Wechaty', 'logout()')

775 776 777 778 779 780 781
    try {
      await this.puppet.logout()
    } catch (e) {
      log.error('Wechaty', 'logout() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
782
    return
783
  }
784

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
785 786 787 788 789
  /**
   * Get the logon / logoff state
   *
   * @returns {boolean}
   * @example
790
   * if (bot.logonoff()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
791 792 793 794 795
   *   console.log('Bot logined')
   * } else {
   *   console.log('Bot not logined')
   * }
   */
796
  public logonoff(): Boolean {
797
    return this.puppet.logonoff()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
798 799
  }

800 801 802 803
  /**
   * @deprecated
   */
  public self(): Contact {
804
    log.warn('Wechaty', 'self() DEPRECATED. use userSelf() instead.')
805 806 807
    return this.userSelf()
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
808
  /**
L
lijiarui 已提交
809 810 811 812
   * Get current user
   *
   * @returns {Contact}
   * @example
813
   * const contact = bot.userSelf()
L
lijiarui 已提交
814
   * console.log(`Bot is ${contact.name()}`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
815
   */
816
  public userSelf(): Contact {
817 818 819
    const userId = this.puppet.selfId()
    const user = this.Contact.load(userId)
    return user
820
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
821

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
822
  /**
L
lijiarui 已提交
823 824
   * Send message to filehelper
   *
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
825
   * @param {string} text
L
lijiarui 已提交
826
   * @returns {Promise<boolean>}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
827
   */
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
828 829 830
  public async say(text: string): Promise<void> {
    log.verbose('Wechaty', 'say(%s)', text)
    await this.puppet.say(text)
831 832
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
833
  /**
L
lijiarui 已提交
834
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
835
   */
836
  public static async sleep(millisecond: number): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
837
    await new Promise(resolve => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
838 839 840 841
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
842
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
843
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
844
   */
845 846 847 848 849 850 851 852
  public async ding(): Promise<string> {
    try {
      return await this.puppet.ding() // should return 'dong'
    } catch (e) {
      log.error('Wechaty', 'ding() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
853
  }
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868

  /**
   * @private
   */
  private memoryCheck(minMegabyte = 4): void {
    const freeMegabyte = Math.floor(os.freemem() / 1024 / 1024)
    log.silly('Wechaty', 'memoryCheck() free: %d MB, require: %d MB',
                          freeMegabyte, minMegabyte)

    if (freeMegabyte < minMegabyte) {
      const e = new Error(`memory not enough: free ${freeMegabyte} < require ${minMegabyte} MB`)
      log.warn('Wechaty', 'memoryCheck() %s', e.message)
      this.emit('error', e)
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
869 870 871 872 873

  /**
   * @private
   */
  public async reset(reason?: string): Promise<void> {
874
    log.verbose('Wechaty', 'reset() because %s', reason || 'no reason')
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
875 876
    await this.puppet.stop()
    await this.puppet.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
877 878 879
    return
  }

880 881 882
  public unref(): void {
    log.warn('Wechaty', 'unref() To Be Implemented. See: https://github.com/Chatie/wechaty/issues/1197')
  }
883
}
884 885

export default Wechaty