wechaty.ts 27.8 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
}                   from 'hot-import'
33 34 35 36 37 38
import {
  StateSwitch,
}                   from 'state-switch'
import {
  MemoryCard,
}                   from 'memory-card'
39

40 41 42
import {
  Accessory,
}                       from './accessory'
43
import {
44
  VERSION,
45
  config,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
46
  log,
47
  Raven,
48
  Sayable,
49
}                       from './config'
50

M
Mukaiu 已提交
51
import {
52 53 54
  PUPPET_DICT,
  PuppetName,
}                       from './puppet-config'
55
import {
56 57
  Puppet,
  PuppetOptions,
58
}                       from './puppet/'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
59

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

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
import {
  CHAT_EVENT_DICT,
  PUPPET_EVENT_DICT,
  PuppetEventName,
  // ChatEventName,
}                       from './puppet/schemas/puppet'

// 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 (李卓桓) 已提交
90

91
export const WECHATY_EVENT_DICT = {
92
  ...CHAT_EVENT_DICT,
93 94 95 96
  error     : 'tbw',
  heartbeat : 'tbw',
  start     : 'tbw',
  stop      : 'tbw',
97 98 99
}

export type WechatyEventName  = keyof typeof WECHATY_EVENT_DICT
100 101

export interface WechatyOptions {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102 103
  puppet?  : PuppetName | Puppet,
  profile? : null | string,
104
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
105

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
/**
L
lijiarui 已提交
107
 * Main bot class.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
108
 *
L
lijiarui 已提交
109
 * [The World's Shortest ChatBot Code: 6 lines of JavaScript]{@link #wechatyinstance}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
110
 *
L
lijiarui 已提交
111 112 113
 * [Wechaty Starter Project]{@link https://github.com/lijiarui/wechaty-getting-started}
 * @example
 * import { Wechaty } from 'wechaty'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115
 */
116
export class Wechaty extends Accessory implements Sayable {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
  /**
118
   * singleton globalInstance
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
119
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
   */
121
  private static globalInstance: Wechaty
122

123
  private memory: MemoryCard
124

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
125 126
  /**
   * the state
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
127
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
128
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
129
  private state = new StateSwitch('Wechaty', log)
130

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
131
  /**
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
132
   * the cuid
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
133
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134
   */
135
  public readonly id : string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
136

137
  // tslint:disable-next-line:variable-name
138
  public readonly Contact       : typeof Contact
139
  // tslint:disable-next-line:variable-name
140
  public readonly FriendRequest : typeof FriendRequest
141
  // tslint:disable-next-line:variable-name
142
  public readonly Message       : typeof Message
143
  // tslint:disable-next-line:variable-name
144
  public readonly Room          : typeof Room
145

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
146
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
147
   * get the singleton instance of Wechaty
L
lijiarui 已提交
148 149 150 151 152 153 154 155 156
   *
   * @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 (李卓桓) 已提交
157
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158 159 160
  public static instance(
    options?: WechatyOptions,
  ) {
161
    if (options && this.globalInstance) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
      throw new Error('instance can be only inited once by options!')
163
    }
164 165
    if (!this.globalInstance) {
      this.globalInstance = new Wechaty(options)
166
    }
167
    return this.globalInstance
168 169
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
171
   * @public
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173
  constructor(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
174
    private options: WechatyOptions = {},
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
175
  ) {
176
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177 178
    log.verbose('Wechaty', 'contructor()')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179
    options.profile = options.profile === null
180
                      ? null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
181
                      : (options.profile || config.default.DEFAULT_PROFILE)
182

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
183
    this.memory = new MemoryCard(options.profile || undefined)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184

185
    this.id = cuid()
186 187 188 189 190 191 192 193 194 195 196 197 198

    /**
     * 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)
199 200
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
202
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204 205 206 207 208 209
  public toString() {
    if (!this.options) {
      return this.constructor.name
    }

    return [
210 211
      'Wechaty#',
      this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
      `<${this.options && this.options.puppet || ''}>`,
213
      `(${this.memory  && this.memory.name    || ''})`,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
214 215
    ].join('')
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
217
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
218
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
219
   */
220
  public static version(forceNpm = false): string {
221
    if (!forceNpm) {
222
      const revision = config.gitRevision()
223
      if (revision) {
224
        return `#git[${revision}]`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
225
      }
226
    }
227
    return VERSION
228
  }
229

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230
 /**
Huan (李卓桓)'s avatar
linting  
Huan (李卓桓) 已提交
231 232 233 234 235 236 237 238 239
  * 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'
  */
240
  public version(forceNpm = false): string {
241
    return Wechaty.version(forceNpm)
242
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  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)
  }

267
  public on(event: 'error'      , listener: string | ((this: Wechaty, error: Error) => void))                                                 : this
268
  public on(event: 'friend'     , listener: string | ((this: Wechaty, request: FriendRequest) => void))                     : this
269 270 271 272 273 274 275
  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
276
  public on(event: 'scan'       , listener: string | ((this: Wechaty, url: string, code: number) => void))                                   : this
277 278
  public on(event: 'start'      , listener: string | ((this: Wechaty) => void))                                                               : this
  public on(event: 'stop'       , listener: string | ((this: Wechaty) => void))                                                               : this
279

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283
  /**
L
lijiarui 已提交
284 285 286 287 288 289 290 291 292 293 294 295
   * @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 (李卓桓) 已提交
296
   * @property   {string}  scan       - A scan event will be emitted when the bot needs to show you a QR Code for scanning.
L
lijiarui 已提交
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
   */

  /**
   * @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
317
   * @property   {Function} friend          -(this: Wechaty, request?: FriendRequest) => void
L
lijiarui 已提交
318 319 320 321 322 323 324 325
   * @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
326
   * @param   {WechatyEventName}      event      - Emit WechatyEvent
L
lijiarui 已提交
327 328 329
   * @param   {WechatyEventFunction}  listener   - Depends on the WechatyEvent
   * @return  {Wechaty}                          - this for chain
   *
330
   * More Example Gist: [Examples/Friend-Bot]{@link https://github.com/Chatie/wechaty/blob/master/examples/friend-bot.ts}
L
lijiarui 已提交
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
   *
   * @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>
353 354 355
   * 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 已提交
356 357 358 359 360 361
   *     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!`)
   *       }
362
   * 	  } else if (request.type === FriendRequest.Type.CONFIRM) { // 2. confirm friend ship
L
lijiarui 已提交
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
   *       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 (李卓桓) 已提交
383
   */
384
  public on(event: WechatyEventName, listener: string | ((...args: any[]) => any)): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385
    log.verbose('Wechaty', 'on(%s, %s) registered',
386 387 388 389 390 391
                            event,
                            typeof listener === 'string'
                              ? listener
                              : typeof listener,
                )

392
    if (typeof listener === 'function') {
393
      this.addListenerFunction(event, listener)
394
    } else {
395
      this.addListenerModuleFile(event, listener)
396
    }
397
    return this
398 399
  }

400
  private addListenerModuleFile(event: WechatyEventName, modulePath: string): void {
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
    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)
      })
  }

420
  private addListenerFunction(event: WechatyEventName, listener: Function): void {
421 422 423 424 425 426 427 428 429 430 431 432
    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 (李卓桓) 已提交
433 434 435 436 437 438 439 440 441
  private initPuppet(): void {
    log.verbose('Wechaty', 'initPuppet(%s)', this.options.puppet)

    const puppet = this.initPuppetResolver(this.options.puppet)

    this.initPuppetVersionSatisfy(puppet)
    this.initPuppetEventBridge(puppet)
    this.initPuppetAccessory(puppet)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
442

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
443
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
444
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
445
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446 447
  private initPuppetVersionSatisfy(puppet: Puppet): void {
    log.verbose('Wechaty', 'initPuppetVersionSatisfy(%s)', puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449
    if (this.initPuppetSemverSatisfy(
450 451
      puppet.wechatyVersionRange(),
    )) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
      return
453 454
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
455 456 457 458
    throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
      + `requires a version range(${puppet.wechatyVersionRange()}) `
      + `that is not satisfying the Wechaty version: ${this.version()}.`,
    )
459 460 461 462 463
  }

  /**
   * Init the Puppet
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
464
  private initPuppetResolver(puppet?: PuppetName | Puppet): Puppet {
465 466
    log.verbose('Wechaty', 'initPuppetResolver(%s)', puppet)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
467 468 469 470 471
    if (!puppet) {
      log.info('Wechaty', 'initPuppet() using default puppet: %s', config.puppet)
      puppet  = config.puppet
    }

472
    if (typeof puppet === 'string') {
473
      // tslint:disable-next-line:variable-name
474 475 476 477 478
      const MyPuppet = PUPPET_DICT[puppet]
      if (!MyPuppet) {
        throw new Error('no such puppet: ' + puppet)
      }

479
      const options: PuppetOptions = {
480
        memory : this.memory,
481 482
      }

483
      return new MyPuppet(options)
484

485 486
    } else if (puppet instanceof Puppet) {
      return puppet
487 488
    } else {
      throw new Error('unsupported options.puppet!')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
489
    }
490
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
491

492 493 494 495
  /**
   * Plugin Version Range Check
   */
  private initPuppetSemverSatisfy(versionRange: string) {
496
    log.verbose('Wechaty', 'initPuppetSemverSatisfy(%s)', versionRange)
497
    return semver.satisfies(
498
      this.version(true),
499 500 501
      versionRange,
    )
  }
502

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
503
  protected initPuppetEventBridge(puppet: Puppet) {
504
    const eventNameList: PuppetEventName[] = Object.keys(PUPPET_EVENT_DICT) as any
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    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

520
        case 'watchdog':
521
          puppet.removeAllListeners('heartbeat')
522 523 524 525
          puppet.on('watchdog', data => {
            /**
             * Use `watchdog` event from Puppet to `heartbeat` Wechaty.
             */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
526
            // TODO: use a throttle queue to prevent beat too fast.
527 528 529 530 531 532
            this.emit('heartbeat', data)
          })
          break

        case 'start':
        case 'stop':
533 534 535
          // do not emit 'start'/'stop' again for wechaty:
          // because both puppet & wechaty should have their own
          // `start`/`stop` event seprately
536 537
          break

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

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

552 553
        case 'friend':
          puppet.removeAllListeners('friend')
554 555 556
          puppet.on('friend', async requestId => {
            const request = this.FriendRequest.load(requestId)
            await request.ready()
557
            this.emit('friend', request)
558
            request.contact().emit('friend', request)
559 560 561 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
          })
          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)
602
            room.emit('join', inviteeList, inviter)
603 604 605 606 607
          })
          break

        case 'room-leave':
          puppet.removeAllListeners('room-leave')
608
          puppet.on('room-leave', async (roomId, leaverIdList, removerId) => {
609 610 611 612 613 614
            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()))

615 616 617 618 619 620 621
            let remover: undefined | Contact = undefined
            if (removerId) {
              remover = this.Contact.load(removerId)
              await remover.ready()
            }

            this.emit('room-leave', room, leaverList, remover)
622
            room.emit('leave', leaverList)
623 624 625 626 627 628 629 630 631 632 633 634 635
          })
          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)
636
            room.emit('topic', topic, oldTopic, changer)
637 638 639 640 641 642 643 644 645 646
          })
          break

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

647 648 649
        case 'watchdog':
          break

650 651 652 653
        default:
          throw new Error('eventName ' + eventName + 'unsupported!')

      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
654
    }
655
  }
656

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
657
  protected initPuppetAccessory(puppet: Puppet) {
658
    log.verbose('Wechaty', 'initAccessory(%s)', puppet)
659

660
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
661
     * 1. Set Wechaty
662
     */
663 664 665 666 667
    this.Contact.wechaty       = this
    this.FriendRequest.wechaty = this
    this.Message.wechaty       = this
    this.Room.wechaty          = this

668
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
669
     * 2. Set Puppet
670
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
671 672 673 674 675 676
    this.Contact.puppet       = puppet
    this.FriendRequest.puppet = puppet
    this.Message.puppet       = puppet
    this.Room.puppet          = puppet

    this.puppet               = puppet
677 678
  }

679 680 681 682 683 684 685 686 687 688 689 690
  /**
   * 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)
691
    log.verbose('Wechaty', 'id: %s'       , this.id)
692 693 694

    if (this.state.on()) {
      log.silly('Wechaty', 'start() on a starting/started instance')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
695
      await this.state.ready('on')
696 697 698 699 700 701 702
      log.silly('Wechaty', 'start() state.ready() resolved')
      return
    }

    this.state.on('pending')

    try {
703
      await this.memory.load()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
704 705

      this.initPuppet()
706 707 708 709

      await this.puppet.start()

    } catch (e) {
710
      // console.log(e)
711 712 713 714 715 716 717 718 719 720 721 722 723
      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 (李卓桓) 已提交
724 725 726 727 728 729 730 731
  /**
   * Stop the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.stop()
   */
  public async stop(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
732
    log.verbose('Wechaty', 'stop()')
733

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
741
    this.state.off('pending')
742
    await this.memory.save()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
743

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

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

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

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

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

796 797 798 799
  /**
   * @deprecated
   */
  public self(): Contact {
800
    log.warn('Wechaty', 'self() DEPRECATED. use userSelf() instead.')
801 802 803
    return this.userSelf()
  }

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
838
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
839
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
840
   */
841 842 843 844 845 846 847 848
  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 (李卓桓) 已提交
849
  }
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864

  /**
   * @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 (李卓桓) 已提交
865 866 867 868 869

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

876 877 878
  public unref(): void {
    log.warn('Wechaty', 'unref() To Be Implemented. See: https://github.com/Chatie/wechaty/issues/1197')
  }
879
}
880 881

export default Wechaty