wechaty.ts 37.4 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 22
import cuid    from 'cuid'
import os      from 'os'
import semver  from 'semver'
23

24
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
25
  // Constructor,
26
  cloneClass,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
27
  // instanceToClass,
28
}                   from 'clone-class'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
29 30 31
import {
  FileBox,
}                   from 'file-box'
32 33 34
import {
  callerResolve,
  hotImport,
35
}                   from 'hot-import'
36 37 38
import {
  MemoryCard,
}                   from 'memory-card'
39 40 41
import {
  StateSwitch,
}                   from 'state-switch'
42

43
import {
44
  CHAT_EVENT_DICT,
45 46 47 48
  Puppet,

  PUPPET_EVENT_DICT,
  PuppetEventName,
49
  PuppetOptions,
50 51
}                       from 'wechaty-puppet'

52 53 54
import {
  Accessory,
}                       from './accessory'
55
import {
56
  config,
57
  isProduction,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
58
  log,
59
  Raven,
60
  VERSION,
61
}                       from './config'
62

63 64 65 66 67
import {
  AnyFunction,
  Sayable,
}                       from './types'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
68 69 70
import {
  Io,
}                       from './io'
M
Mukaiu 已提交
71
import {
72
  PuppetName,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
73
  puppetResolver,
74
}                       from './puppet-config'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
75

76
import {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
77
  Contact,
78
  ContactSelf,
79
  Friendship,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
80 81
  Message,
  Room,
82
}                       from './user/'
83 84

export const WECHATY_EVENT_DICT = {
85
  ...CHAT_EVENT_DICT,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86
  dong      : 'tbw',
87 88 89 90
  error     : 'tbw',
  heartbeat : 'tbw',
  start     : 'tbw',
  stop      : 'tbw',
91 92 93
}

export type WechatyEventName  = keyof typeof WECHATY_EVENT_DICT
94 95

export interface WechatyOptions {
96 97 98 99
  profile?       : null | string,           // Wechaty Name
  puppet?        : PuppetName | Puppet,     // Puppet name or instance
  puppetOptions? : PuppetOptions,           // Puppet TOKEN
  ioToken?       : string,                  // Io TOKEN
100
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102
/**
L
lijiarui 已提交
103
 * Main bot class.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104
 *
L
lijiarui 已提交
105 106 107 108 109
 * A `Bot` is a wechat client depends on which puppet you use.
 * It may equals
 * - web-wechat, when you use: [puppet-puppeteer](https://github.com/chatie/wechaty-puppet-puppeteer)/[puppet-wechat4u](https://github.com/chatie/wechaty-puppet-wechat4u)
 * - ipad-wechat, when you use: [puppet-padchat](https://github.com/lijiarui/wechaty-puppet-padchat)
 * - ios-wechat, when you use: puppet-ioscat
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
110
 *
L
lijiarui 已提交
111 112
 * See more:
 * - [What is a Puppet in Wechaty](https://github.com/Chatie/wechaty-getting-started/wiki/FAQ-EN#31-what-is-a-puppet-in-wechaty)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
113
 *
L
lijiarui 已提交
114 115 116 117 118 119 120 121 122 123
 * > If you want to know how to send message, see [Message](#Message) <br>
 * > If you want to know how to get contact, see [Contact](#Contact)
 *
 * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
 * const { Wechaty } = require('wechaty')
 * const bot = new Wechaty()
 * bot.on('scan',    (qrcode, status) => console.log(['https://api.qrserver.com/v1/create-qr-code/?data=',encodeURIComponent(qrcode),'&size=220x220&margin=20',].join('')))
 * bot.on('login',   user => console.log(`User ${user} logined`))
 * bot.on('message', message => console.log(`Message: ${message}`))
 * bot.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124
 */
125
export class Wechaty extends Accessory implements Sayable {
126

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127 128
  public readonly VERSION = VERSION

129 130
  public readonly state  : StateSwitch

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
131
  /**
132
   * singleton globalInstance
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
133
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134
   */
135
  private static globalInstance: Wechaty
136

137
  private readonly memory : MemoryCard
138

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
139 140
  private lifeTimer? : NodeJS.Timer
  private io?        : Io
141

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
142
  /**
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
143
   * the cuid
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
144
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145
   */
146
  public readonly id : string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148 149 150
  /**
   * @private
   */
151
  // tslint:disable-next-line:variable-name
152
  public readonly Contact       : typeof Contact
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153 154 155 156

  /**
   * @private
   */
157
  // tslint:disable-next-line:variable-name
158
  public readonly ContactSelf   : typeof ContactSelf
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159 160 161 162

  /**
   * @private
   */
163
  // tslint:disable-next-line:variable-name
164
  public readonly Friendship    : typeof Friendship
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
165 166 167 168

  /**
   * @private
   */
169
  // tslint:disable-next-line:variable-name
170
  public readonly Message       : typeof Message
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
171 172 173 174

  /**
   * @private
   */
175
  // tslint:disable-next-line:variable-name
176
  public readonly Room          : typeof Room
177

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179
   * Get the global instance of Wechaty
L
lijiarui 已提交
180
   *
L
lijiarui 已提交
181 182
   * @param {WechatyOptions} [options={}]
   *
L
lijiarui 已提交
183 184 185
   * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
   * const { Wechaty } = require('wechaty')
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
   * Wechaty.instance() // Global instance
L
lijiarui 已提交
187 188 189
   * .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}`))
L
lijiarui 已提交
190
   * .start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191
   */
192
  public static instance (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
193 194
    options?: WechatyOptions,
  ) {
195
    if (options && this.globalInstance) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
196
      throw new Error('instance can be only inited once by options!')
197
    }
198 199
    if (!this.globalInstance) {
      this.globalInstance = new Wechaty(options)
200
    }
201
    return this.globalInstance
202 203
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204
  /**
L
lijiarui 已提交
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
   * The term [Puppet](https://github.com/Chatie/wechaty/wiki/Puppet) in Wechaty is an Abstract Class for implementing protocol plugins.
   * The plugins are the component that helps Wechaty to control the Wechat(that's the reason we call it puppet).
   * The plugins are named XXXPuppet, for example:
   * - [PuppetPuppeteer](https://github.com/Chatie/wechaty-puppet-puppeteer):
   * - [PuppetPadchat](https://github.com/lijiarui/wechaty-puppet-padchat)
   *
   * @typedef    PuppetName
   * @property   {string}  wechat4u
   * The default puppet, using the [wechat4u](https://github.com/nodeWechat/wechat4u) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
   * @property   {string}  padchat
   * - Using the WebSocket protocol to connect with a Protocol Server for controlling the iPad Wechat program.
   * @property   {string}  puppeteer
   * - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
   * @property   {string}  mock
   * - Using the mock data to mock wechat operation, just for test.
   */

  /**
   * The option parameter to create a wechaty instance
   *
   * @typedef    WechatyOptions
   * @property   {string}                 profile            -Wechaty Name. </br>
   *          When you set this: </br>
   *          `new Wechaty({profile: 'wechatyName'}) ` </br>
   *          it will generate a file called `wechatyName.memory-card.json`. </br>
   *          This file stores the bot's login information. </br>
   *          If the file is valid, the bot can auto login so you don't need to scan the qrcode to login again. </br>
   *          Also, you can set the environment variable for `WECHATY_PROFILE` to set this value when you start. </br>
   *          eg:  `WECHATY_PROFILE="your-cute-bot-name" node bot.js`
   * @property   {PuppetName | Puppet}    puppet             -Puppet name or instance
   * @property   {Partial<PuppetOptions>} puppetOptions      -Puppet TOKEN
   * @property   {string}                 ioToken            -Io TOKEN
   */

  /**
   * Creates an instance of Wechaty.
   * @param {WechatyOptions} [options={}]
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243
   */
244
  constructor (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245
    private options: WechatyOptions = {},
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246
  ) {
247
    super()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
248 249
    log.verbose('Wechaty', 'contructor()')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250
    options.profile = options.profile === null
251
                      ? null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
                      : (options.profile || config.default.DEFAULT_PROFILE)
253

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254
    this.memory = new MemoryCard(options.profile || undefined)
255 256
    this.state  = new StateSwitch('Wechaty', log)
    this.id     = cuid()
257 258

    /**
L
lijiarui 已提交
259
     * @ignore
260 261 262 263 264 265 266
     * 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???
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267 268
    this.Contact     = cloneClass(Contact)
    this.ContactSelf = cloneClass(ContactSelf)
269
    this.Friendship  = cloneClass(Friendship)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
270 271
    this.Message     = cloneClass(Message)
    this.Room        = cloneClass(Room)
272 273
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
274
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
275
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
276
   */
277
  public toString () {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
278 279 280 281 282
    if (!this.options) {
      return this.constructor.name
    }

    return [
283 284
      'Wechaty#',
      this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
      `<${this.options && this.options.puppet || ''}>`,
286
      `(${this.memory  && this.memory.name    || ''})`,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287 288
    ].join('')
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289

290 291 292 293 294 295 296 297 298 299 300
  public emit (event: 'dong'       , data?: string)                                                    : boolean
  public emit (event: 'error'      , error: Error)                                                     : boolean
  public emit (event: 'friendship' , friendship: Friendship)                                           : boolean
  public emit (event: 'heartbeat'  , data: any)                                                        : boolean
  public emit (event: 'login' | 'logout', user: ContactSelf)                                           : 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, newTopic: string, oldTopic: string, changer: Contact) : boolean
  public emit (event: 'scan'       , qrcode: string, status: number, data?: string)                    : boolean
  public emit (event: 'start' | 'stop')                                                                         : boolean
301 302

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

305
  public emit (
306 307 308 309 310 311
    event:   WechatyEventName,
    ...args: any[]
  ): boolean {
    return super.emit(event, ...args)
  }

312 313 314 315 316 317 318 319 320 321 322
  public on (event: 'dong'       , listener: string | ((this: Wechaty, data?: string) => void))                                                     : this
  public on (event: 'error'      , listener: string | ((this: Wechaty, error: Error) => void))                                                     : this
  public on (event: 'friendship' , listener: string | ((this: Wechaty, friendship: Friendship) => void))                                           : this
  public on (event: 'heartbeat'  , listener: string | ((this: Wechaty, data: any) => void))                                                        : this
  public on (event: 'login' | 'logout'     , listener: string | ((this: Wechaty, user: ContactSelf) => 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, newTopic: string, oldTopic: string, changer: Contact) => void)) : this
  public on (event: 'scan'       , listener: string | ((this: Wechaty, qrcode: string, status: number, data?: string) => void))                    : this
  public on (event: 'start' | 'stop'      , listener: string | ((this: Wechaty) => void))                                                                   : this
323

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
  /**
L
lijiarui 已提交
328 329 330 331 332 333 334 335 336 337 338 339
   * @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.
L
lijiarui 已提交
340 341
   * @property   {string}  scan       - A scan event will be emitted when the bot needs to show you a QR Code for scanning. </br>
   *                                    It is recommend to install qrcode-terminal(run `npm install qrcode-terminal`) in order to show qrcode in the terminal.
L
lijiarui 已提交
342 343 344 345 346 347
   */

  /**
   * @desc       Wechaty Class Event Function
   * @typedef    WechatyEventFunction
   * @property   {Function} error           -(this: Wechaty, error: Error) => void callback function
348 349
   * @property   {Function} login           -(this: Wechaty, user: ContactSelf)=> void
   * @property   {Function} logout          -(this: Wechaty, user: ContactSelf) => void
L
lijiarui 已提交
350 351 352 353 354 355 356 357 358 359 360 361
   * @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
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362
   * @property   {Function} friendship      -(this: Wechaty, friendship: Friendship) => void
L
lijiarui 已提交
363 364
   * @property   {Function} message         -(this: Wechaty, message: Message) => void
   * @property   {Function} room-join       -(this: Wechaty, room: Room, inviteeList: Contact[],  inviter: Contact) => void
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
365
   * @property   {Function} room-topic      -(this: Wechaty, room: Room, newTopic: string, oldTopic: string, changer: Contact) => void
L
lijiarui 已提交
366 367 368 369 370
   * @property   {Function} room-leave      -(this: Wechaty, room: Room, leaverList: Contact[]) => void
   */

  /**
   * @listens Wechaty
371
   * @param   {WechatyEventName}      event      - Emit WechatyEvent
L
lijiarui 已提交
372 373
   * @param   {WechatyEventFunction}  listener   - Depends on the WechatyEvent
   *
L
lijiarui 已提交
374 375 376 377 378
   * @return  {Wechaty}                          - this for chaining,
   * see advanced {@link https://github.com/Chatie/wechaty-getting-started/wiki/FAQ-EN#36-why-wechatyonevent-listener-return-wechaty|chaining usage}
   *
   * @desc
   * When the bot get message, it will emit the following Event.
L
lijiarui 已提交
379
   *
L
lijiarui 已提交
380 381 382 383 384 385 386 387 388 389
   * You can do anything you want when in these events functions.
   * The main Event name as follows:
   * - **scan**: Emit when the bot needs to show you a QR Code for scanning. After scan the qrcode, you can login
   * - **login**: Emit when bot login full successful.
   * - **logout**: Emit when bot detected log out.
   * - **message**: Emit when there's a new message.
   *
   * see more in {@link WechatyEventName}
   *
   * @example <caption>Event:scan</caption>
L
lijiarui 已提交
390
   * // Scan Event will emit when the bot needs to show you a QR Code for scanning
L
lijiarui 已提交
391 392
   *
   * bot.on('scan', (url: string, code: number) => {
L
lijiarui 已提交
393 394 395 396
   *   console.log(`[${code}] Scan ${url} to login.` )
   * })
   *
   * @example <caption>Event:login </caption>
L
lijiarui 已提交
397
   * // Login Event will emit when bot login full successful.
L
lijiarui 已提交
398
   *
399
   * bot.on('login', (user: ContactSelf) => {
L
lijiarui 已提交
400 401 402 403
   *   console.log(`user ${user} login`)
   * })
   *
   * @example <caption>Event:logout </caption>
L
lijiarui 已提交
404
   * // Logout Event will emit when bot detected log out.
L
lijiarui 已提交
405
   *
406
   * bot.on('logout', (user: ContactSelf) => {
L
lijiarui 已提交
407 408 409 410
   *   console.log(`user ${user} logout`)
   * })
   *
   * @example <caption>Event:message </caption>
L
lijiarui 已提交
411
   * // Message Event will emit when there's a new message.
L
lijiarui 已提交
412
   *
L
lijiarui 已提交
413 414 415 416
   * wechaty.on('message', (message: Message) => {
   *   console.log(`message ${message} received`)
   * })
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
   * @example <caption>Event:friendship </caption>
L
lijiarui 已提交
418
   * // Friendship Event will emit when got a new friend request, or friendship is confirmed.
L
lijiarui 已提交
419
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
420 421 422 423
   * bot.on('friendship', (friendship: Friendship) => {
   *   if(friendship.type() === Friendship.Type.RECEIVE){ // 1. receive new friendship request from new contact
   *     const contact = friendship.contact()
   *     let result = await friendship.accept()
L
lijiarui 已提交
424 425 426 427 428
   *       if(result){
   *         console.log(`Request from ${contact.name()} is accept succesfully!`)
   *       } else{
   *         console.log(`Request from ${contact.name()} failed to accept!`)
   *       }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
429
   * 	  } else if (friendship.type() === Friendship.Type.CONFIRM) { // 2. confirm friendship
L
lijiarui 已提交
430 431 432 433 434
   *       console.log(`new friendship confirmed with ${contact.name()}`)
   *    }
   *  })
   *
   * @example <caption>Event:room-join </caption>
L
lijiarui 已提交
435
   * // room-join Event will emit when someone join the room.
L
lijiarui 已提交
436
   *
L
lijiarui 已提交
437 438 439 440 441 442
   * 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>
L
lijiarui 已提交
443
   * // room-leave Event will emit when someone leave the room.
L
lijiarui 已提交
444
   *
L
lijiarui 已提交
445 446 447 448 449 450
   * 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>
L
lijiarui 已提交
451
   * // room-topic Event will emit when someone change the room's topic.
L
lijiarui 已提交
452
   *
L
lijiarui 已提交
453 454 455
   * 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()}`)
   * })
L
lijiarui 已提交
456 457
   *
   * @example <caption>Event:error </caption>
L
lijiarui 已提交
458
   * // error Event will emit when there's an error occurred.
L
lijiarui 已提交
459 460 461 462
   *
   * bot.on('error', (error) => {
   *   console.error(error)
   * })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463
   */
464
  public on (event: WechatyEventName, listener: string | ((...args: any[]) => any)): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465
    log.verbose('Wechaty', 'on(%s, %s) registered',
466 467 468 469 470 471
                            event,
                            typeof listener === 'string'
                              ? listener
                              : typeof listener,
                )

472 473
    // DEPRECATED for 'friend' event
    if (event as any === 'friend') {
474
      log.warn('Wechaty', `on('friend', contact, friendRequest) is DEPRECATED. use on('friendship', friendship) instead`)
475 476 477
      if (typeof listener === 'function') {
        const oldListener = listener
        listener = (...args: any[]) => {
478
          log.warn('Wechaty', `on('friend', contact, friendRequest) is DEPRECATED. use on('friendship', friendship) instead`)
479 480 481 482 483
          oldListener.apply(this, args)
        }
      }
    }

484
    if (typeof listener === 'function') {
485
      this.addListenerFunction(event, listener)
486
    } else {
487
      this.addListenerModuleFile(event, listener)
488
    }
489
    return this
490 491
  }

492
  private addListenerModuleFile (event: WechatyEventName, modulePath: string): void {
493
    const absoluteFilename = callerResolve(modulePath, __filename)
494 495
    log.verbose('Wechaty', 'onModulePath() hotImport(%s)', absoluteFilename)

496
    hotImport(absoluteFilename)
497
      .then((func: AnyFunction) => super.on(event, (...args: any[]) => {
498 499 500 501 502 503 504 505 506 507 508 509 510
        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)
      })
511 512 513 514

    if (isProduction()) {
      log.silly('Wechaty', 'addListenerModuleFile() disable watch for hotImport because NODE_ENV is production.')
      hotImport(absoluteFilename, false)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
515
        .catch(e => log.error('Wechaty', 'addListenerModuleFile() hotImport() rejection: %s', e))
516
    }
517 518
  }

519
  private addListenerFunction (event: WechatyEventName, listener: AnyFunction): void {
520 521 522 523 524 525 526 527 528 529 530 531
    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)
      }
    })
  }

532
  private async initPuppet (): Promise<void> {
533
    log.verbose('Wechaty', 'initPuppet() %s', this.options.puppet || '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
534

535 536 537 538 539 540 541 542 543 544 545 546
    let inited = false
    try {
      inited = !!this.puppet
    } catch (e) {
      inited = false
    }

    if (inited) {
      log.verbose('Wechaty', 'initPuppet(%s) had already been inited, no need to init twice', this.options.puppet)
      return
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
547
    const puppet = await this.initPuppetResolver(this.options.puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
548 549 550

    this.initPuppetVersionSatisfy(puppet)
    this.initPuppetEventBridge(puppet)
551

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
552 553
    this.initPuppetAccessory(puppet)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
554

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
555
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
556
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
557
   */
558
  private initPuppetVersionSatisfy (puppet: Puppet): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
559
    log.verbose('Wechaty', 'initPuppetVersionSatisfy(%s)', puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
560

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
561
    if (this.initPuppetSemverSatisfy(
562 563
      puppet.wechatyVersionRange(),
    )) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
564
      return
565 566
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
567 568 569 570
    throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
      + `requires a version range(${puppet.wechatyVersionRange()}) `
      + `that is not satisfying the Wechaty version: ${this.version()}.`,
    )
571 572 573
  }

  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
574 575
   * @private
   *
576 577
   * Init the Puppet
   */
578
  private async initPuppetResolver (puppet?: PuppetName | Puppet): Promise<Puppet> {
579 580
    log.verbose('Wechaty', 'initPuppetResolver(%s)', puppet)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
581
    if (!puppet) {
582 583
      puppet = config.systemPuppetName()
      log.info('Wechaty', 'initPuppet() using puppet: %s', puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
584 585
    }

586
    const puppetMemory = this.memory.sub(puppet.toString())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
587

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
588 589
    let puppetInstance: Puppet

590
    if (typeof puppet === 'string') {
591
      // tslint:disable-next-line:variable-name
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
592
      const MyPuppet = await puppetResolver(puppet)
593 594 595 596
      if (!MyPuppet) {
        throw new Error('no such puppet: ' + puppet)
      }

597 598 599 600 601 602 603 604 605 606 607
      /**
       * We will meet the following error:
       *
       *  [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.
       *
       * When we have different puppet with different `constructor()` args.
       * For example: PuppetA allow `constructor()` but PuppetB requires `constructor(options)`
       *
       * SOLUTION: we enforce all the PuppetImplenmentation to have `options` and should not allow default parameter.
       * Issue: https://github.com/Chatie/wechaty-puppet/issues/2
       */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
608
      puppetInstance = new MyPuppet(this.options.puppetOptions)
609

610
    } else if (puppet instanceof Puppet) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
611 612
      puppetInstance = puppet

613
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
614
      throw new Error('unsupported options.puppet: ' + puppet)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
615
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
616 617 618 619

    // give puppet the memory
    puppetInstance.setMemory(puppetMemory)

620 621
    log.info('Wechaty', 'initPuppet() inited puppet: %s', puppetInstance.toString())

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
622
    return puppetInstance
623
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
624

625
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
626 627
   * @private
   *
628 629
   * Plugin Version Range Check
   */
630
  private initPuppetSemverSatisfy (versionRange: string) {
631
    log.verbose('Wechaty', 'initPuppetSemverSatisfy(%s)', versionRange)
632
    return semver.satisfies(
633
      this.version(true),
634 635 636
      versionRange,
    )
  }
637

638
  protected initPuppetEventBridge (puppet: Puppet) {
639
    const eventNameList: PuppetEventName[] = Object.keys(PUPPET_EVENT_DICT) as any
640 641 642 643
    for (const eventName of eventNameList) {
      log.verbose('Wechaty', 'initPuppetEventBridge() puppet.on(%s) registered', eventName)

      switch (eventName) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
644 645 646 647 648 649 650
        case 'dong':
          puppet.removeAllListeners('dong')
          puppet.on('dong', data => {
            this.emit('dong', data)
          })
          break

651 652 653 654 655 656 657
        case 'error':
          puppet.removeAllListeners('error')
          puppet.on('error', error => {
            this.emit('error', new Error(error))
          })
          break

658
        case 'watchdog':
659
          puppet.removeAllListeners('heartbeat')
660 661 662 663
          puppet.on('watchdog', data => {
            /**
             * Use `watchdog` event from Puppet to `heartbeat` Wechaty.
             */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
664
            // TODO: use a throttle queue to prevent beat too fast.
665 666 667 668
            this.emit('heartbeat', data)
          })
          break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
669 670 671 672 673 674 675 676 677 678 679
        case 'friendship':
          puppet.removeAllListeners('friendship')
          puppet.on('friendship', async friendshipId => {
            const friendship = this.Friendship.load(friendshipId)
            await friendship.ready()
            this.emit('friendship', friendship)
            friendship.contact().emit('friendship', friendship)

            // support deprecated event name: friend.
            // Huan LI 201806
            this.emit('friend' as any, friendship as any)
680 681 682 683 684 685
          })
          break

        case 'login':
          puppet.removeAllListeners('login')
          puppet.on('login', async contactId => {
686
            const contact = this.ContactSelf.load(contactId)
687 688 689 690 691 692 693 694
            await contact.ready()
            this.emit('login', contact)
          })
          break

        case 'logout':
          puppet.removeAllListeners('logout')
          puppet.on('logout', async contactId => {
695
            const contact = this.ContactSelf.load(contactId)
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
            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)
723
            room.emit('join', inviteeList, inviter)
724 725 726 727 728
          })
          break

        case 'room-leave':
          puppet.removeAllListeners('room-leave')
729
          puppet.on('room-leave', async (roomId, leaverIdList, removerId) => {
730 731 732 733 734 735
            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()))

736
            let remover: undefined | Contact
737 738 739 740 741 742
            if (removerId) {
              remover = this.Contact.load(removerId)
              await remover.ready()
            }

            this.emit('room-leave', room, leaverList, remover)
743
            room.emit('leave', leaverList, remover)
744 745 746 747 748
          })
          break

        case 'room-topic':
          puppet.removeAllListeners('room-topic')
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
749
          puppet.on('room-topic', async (roomId, newTopic, oldTopic, changerId) => {
750 751 752 753 754 755
            const room = this.Room.load(roomId)
            await room.ready()

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

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
756 757
            this.emit('room-topic', room, newTopic, oldTopic, changer)
            room.emit('topic', newTopic, oldTopic, changer)
758 759 760 761 762
          })
          break

        case 'scan':
          puppet.removeAllListeners('scan')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
763 764
          puppet.on('scan', async (qrcode, status, data) => {
            this.emit('scan', qrcode, status, data)
765 766 767
          })
          break

768
        case 'watchdog':
769
        case 'reset':
770 771
          break

772
        default:
773
          throw new Error('eventName ' + eventName + ' unsupported!')
774 775

      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
776
    }
777
  }
778

779
  protected initPuppetAccessory (puppet: Puppet) {
780
    log.verbose('Wechaty', 'initAccessory(%s)', puppet)
781

782
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
783
     * 1. Set Wechaty
784
     */
785 786 787 788 789
    this.Contact.wechaty     = this
    this.ContactSelf.wechaty = this
    this.Friendship.wechaty  = this
    this.Message.wechaty     = this
    this.Room.wechaty        = this
790

791
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
792
     * 2. Set Puppet
793
     */
794 795 796 797 798
    this.Contact.puppet     = puppet
    this.ContactSelf.puppet = puppet
    this.Friendship.puppet  = puppet
    this.Message.puppet     = puppet
    this.Room.puppet        = puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
799 800

    this.puppet               = puppet
801 802
  }

803
  /**
L
lijiarui 已提交
804 805 806
   * @desc
   * use {@link Wechaty#start} instead
   * @deprecated
807
   */
808
  public async init (): Promise<void> {
809 810 811
    log.warn('Wechaty', 'init() DEPRECATED. use start() instead.')
    return this.start()
  }
812 813 814 815
  /**
   * Start the bot, return Promise.
   *
   * @returns {Promise<void>}
L
lijiarui 已提交
816 817 818
   * @description
   * When you start the bot, bot will begin to login, need you wechat scan qrcode to login
   * > Tips: All the bot operation needs to be triggered after start() is done
819 820 821 822
   * @example
   * await bot.start()
   * // do other stuff with bot here
   */
823
  public async start (): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
824 825 826 827
    log.info('Wechaty', '<%s> start() v%s is starting...' ,
                        this.options.puppet || config.systemPuppetName(),
                        this.version(),
            )
828 829
    log.verbose('Wechaty', 'puppet: %s'   , this.options.puppet)
    log.verbose('Wechaty', 'profile: %s'  , this.options.profile)
830
    log.verbose('Wechaty', 'id: %s'       , this.id)
831 832 833

    if (this.state.on()) {
      log.silly('Wechaty', 'start() on a starting/started instance')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
834
      await this.state.ready('on')
835 836 837 838
      log.silly('Wechaty', 'start() state.ready() resolved')
      return
    }

839 840 841 842
    if (this.lifeTimer) {
      throw new Error('start() lifeTimer exist')
    }

843 844 845
    this.state.on('pending')

    try {
846
      await this.memory.load()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
847

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
848
      await this.initPuppet()
849 850
      await this.puppet.start()

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
851 852 853 854 855 856 857 858
      if (this.options.ioToken) {
        this.io = new Io({
          token   : this.options.ioToken,
          wechaty : this,
        })
        await this.io.start()
      }

859
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
860
      console.error(e)
861 862
      log.error('Wechaty', 'start() exception: %s', e && e.message)
      Raven.captureException(e)
863 864 865 866 867 868 869 870 871
      this.emit('error', e)

      try {
        await this.stop()
      } catch (e) {
        log.error('Wechaty', 'start() stop() exception: %s', e && e.message)
        Raven.captureException(e)
        this.emit('error', e)
      }
872
      return
873 874 875 876
    }

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

877 878 879 880
    this.lifeTimer = setInterval(() => {
      log.silly('Wechaty', 'start() setInterval() this timer is to keep Wechaty running...')
    }, 1000 * 60 * 60)

881 882 883 884 885 886
    this.state.on(true)
    this.emit('start')

    return
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
887 888 889 890 891 892 893
  /**
   * Stop the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.stop()
   */
894
  public async stop (): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
895 896 897 898
    log.info('Wechaty', '<%s> stop() v%s is stoping ...' ,
                        this.options.puppet || config.systemPuppetName(),
                        this.version(),
            )
899

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
900
    if (this.state.off()) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
901 902 903
      log.silly('Wechaty', 'stop() on an stopping/stopped instance')
      await this.state.ready('off')
      log.silly('Wechaty', 'stop() state.ready(off) resolved')
904
      return
905
    }
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
906

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
907
    this.state.off('pending')
908
    await this.memory.save()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
909

910 911 912 913 914
    if (this.lifeTimer) {
      clearInterval(this.lifeTimer)
      this.lifeTimer = undefined
    }

915
    try {
916
      await this.puppet.stop()
917 918 919
    } catch (e) {
      log.warn('Wechaty', 'stop() puppet.stop() exception: %s', e.message)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
920

921
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
922 923 924 925 926
      if (this.io) {
        await this.io.stop()
        this.io = undefined
      }

927
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
928
      log.error('Wechaty', 'stop() exception: %s', e.message)
929
      Raven.captureException(e)
930
      this.emit('error', e)
931
    }
932 933 934 935 936 937 938 939 940 941

    this.state.off(true)
    this.emit('stop')

    /**
     * MUST use setImmediate at here(the end of this function),
     * because we need to run the micro task registered by the `emit` method
     */
    setImmediate(() => this.puppet.removeAllListeners())

942
    return
943
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
944

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
945
  /**
L
lijiarui 已提交
946 947 948 949 950
   * Logout the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
951
   */
952
  public async logout (): Promise<void>  {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
953 954
    log.verbose('Wechaty', 'logout()')

955 956 957 958 959 960 961
    try {
      await this.puppet.logout()
    } catch (e) {
      log.error('Wechaty', 'logout() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
962
    return
963
  }
964

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
965 966 967 968 969
  /**
   * Get the logon / logoff state
   *
   * @returns {boolean}
   * @example
970
   * if (bot.logonoff()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
971 972 973 974 975
   *   console.log('Bot logined')
   * } else {
   *   console.log('Bot not logined')
   * }
   */
976
  public logonoff (): boolean {
977
    return this.puppet.logonoff()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
978 979
  }

980
  /**
L
lijiarui 已提交
981 982
   * @description
   * Should use {@link Wechaty#userSelf} instead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
983
   * @deprecated Use `userSelf()` instead
984
   */
985
  public self (): Contact {
986
    log.warn('Wechaty', 'self() DEPRECATED. use userSelf() instead.')
987 988 989
    return this.userSelf()
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
990
  /**
L
lijiarui 已提交
991 992
   * Get current user
   *
993
   * @returns {ContactSelf}
L
lijiarui 已提交
994
   * @example
995
   * const contact = bot.userSelf()
L
lijiarui 已提交
996
   * console.log(`Bot is ${contact.name()}`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
997
   */
998
  public userSelf (): ContactSelf {
999
    const userId = this.puppet.selfId()
1000
    const user = this.ContactSelf.load(userId)
1001
    return user
1002
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1003

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1004
  /**
L
lijiarui 已提交
1005
   * Send message to userSelf, in other words, bot send message to itself.
1006 1007 1008
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
   *
L
lijiarui 已提交
1009 1010 1011
   * @param {(string | Contact | FileBox)} textOrContactOrFile
   * send text, Contact, or file to bot. </br>
   * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
1012
   *
L
lijiarui 已提交
1013
   * @returns {Promise<void>}
L
lijiarui 已提交
1014
   *
L
lijiarui 已提交
1015 1016 1017 1018 1019
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in
   *
L
lijiarui 已提交
1020
   * // 1. send text to bot itself
L
lijiarui 已提交
1021 1022
   * await bot.say('hello!')
   *
L
lijiarui 已提交
1023
   * // 2. send Contact to bot itself
L
lijiarui 已提交
1024 1025 1026
   * const contact = bot.Contact.load('contactId')
   * await bot.say(contact)
   *
L
lijiarui 已提交
1027
   * // 3. send Image to bot itself from remote url
L
lijiarui 已提交
1028 1029 1030 1031
   * import { FileBox }  from 'file-box'
   * const fileBox = FileBox.fromUrl('https://chatie.io/wechaty/images/bot-qr-code.png')
   * await bot.say(fileBox)
   *
L
lijiarui 已提交
1032
   * // 4. send Image to bot itself from local file
L
lijiarui 已提交
1033 1034 1035
   * import { FileBox }  from 'file-box'
   * const fileBox = FileBox.fromLocal('/tmp/text.jpg')
   * await bot.say(fileBox)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1036
   */
1037
  public async say (textOrContactOrFile: string | Contact | FileBox): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
    log.verbose('Wechaty', 'say(%s)', textOrContactOrFile)

    // Make Typescript Happy:
    if (typeof textOrContactOrFile === 'string') {
      await this.userSelf().say(textOrContactOrFile)
    } else if (textOrContactOrFile instanceof Contact) {
      await this.userSelf().say(textOrContactOrFile)
    } else if (textOrContactOrFile instanceof FileBox) {
      await this.userSelf().say(textOrContactOrFile)
    } else {
      throw new Error('unsupported')
    }
1050 1051
  }

L
lijiarui 已提交
1052 1053 1054
  /**
   * @private
   */
1055
  public static version (forceNpm = false): string {
L
lijiarui 已提交
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065
    if (!forceNpm) {
      const revision = config.gitRevision()
      if (revision) {
        return `#git[${revision}]`
      }
    }
    return VERSION
  }

 /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1066
  * @private
L
lijiarui 已提交
1067 1068 1069 1070 1071 1072 1073 1074 1075
  * Return version of Wechaty
  *
  * @param {boolean} [forceNpm=false]  - If set to true, will only return the version in package.json. </br>
  *                                      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'
  */
1076
  public version (forceNpm = false): string {
L
lijiarui 已提交
1077 1078 1079
    return Wechaty.version(forceNpm)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1080
  /**
L
lijiarui 已提交
1081
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1082
   */
1083
  public static async sleep (millisecond: number): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1084
    await new Promise(resolve => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1085 1086 1087 1088
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1089
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
1090
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1091
   */
1092
  public ding (data?: string): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1093 1094
    log.silly('Wechaty', 'ding(%s)', data || '')

1095
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1096
      this.puppet.ding(data)
1097 1098 1099
    } catch (e) {
      log.error('Wechaty', 'ding() exception: %s', e.message)
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1100
      throw e
1101
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1102
  }
1103 1104 1105 1106

  /**
   * @private
   */
1107
  private memoryCheck (minMegabyte = 4): void {
1108 1109 1110 1111 1112 1113 1114 1115 1116 1117
    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 (李卓桓) 已提交
1118 1119 1120 1121

  /**
   * @private
   */
1122
  public async reset (reason?: string): Promise<void> {
1123
    log.verbose('Wechaty', 'reset() because %s', reason || 'no reason')
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
1124 1125
    await this.puppet.stop()
    await this.puppet.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1126 1127 1128
    return
  }

1129
  public unref (): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1130 1131 1132 1133 1134 1135 1136
    log.verbose('Wechaty', 'unref()')

    if (this.lifeTimer) {
      this.lifeTimer.unref()
    }

    this.puppet.unref()
1137
  }
1138
}