wechaty.ts 31.7 KB
Newer Older
1
/**
2
 *   Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
3
 *
4 5
 *   @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
 *                   Wechaty Contributors <https://github.com/wechaty>.
6
 *
7 8 9
 *   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
10
 *
11
 *       http://www.apache.org/licenses/LICENSE-2.0
12
 *
13 14 15 16 17
 *   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 已提交
18
 *
19
 */
20 21
import cuid             from 'cuid'
import os               from 'os'
22

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
23
import { instanceToClass }  from 'clone-class'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
24

25 26 27
import {
  Puppet,

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
28
  MemoryCard,
29
  StateSwitch,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
30

31 32
  PUPPET_EVENT_DICT,
  PuppetEventName,
33
  PuppetOptions,
34
  PayloadType,
35 36
}                       from 'wechaty-puppet'

37
import {
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
38 39 40
  FileBox,
  Raven,

41
  config,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
42
  log,
43
}                       from './config'
44

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45 46 47 48 49
import {
  VERSION,
  GIT_COMMIT_HASH,
}                       from './version'

50 51 52 53
import {
  Sayable,
}                       from './types'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
54 55 56
import {
  Io,
}                       from './io'
M
Mukaiu 已提交
57
import {
58
  PuppetModuleName,
59
}                       from './puppet-config'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60 61 62
import {
  PuppetManager,
}                       from './puppet-manager'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
63

64
import {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
65
  Contact,
66
  ContactSelf,
67
  Friendship,
68
  Image,
69 70
  Message,
  MiniProgram,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
71
  Room,
72
  RoomInvitation,
73
  Tag,
74
  UrlLink,
75 76 77 78 79 80 81 82 83 84 85

  wechatifyContact,
  wechatifyContactSelf,
  wechatifyFriendship,
  wechatifyImage,
  wechatifyMessage,
  wechatifyMiniProgram,
  wechatifyRoom,
  wechatifyRoomInvitation,
  wechatifyTag,
  wechatifyUrlLink,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86
}                       from './user/mod'
87

88 89
import { timestampToDate } from './helper-functions/pure/timestamp-to-date'

90 91 92
import {
  WechatyEventEmitter,
  WechatyEventName,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93 94 95 96 97 98 99
}                             from './events/wechaty-events'

import {
  WechatyPlugin,
  WechatyPluginUninstaller,
  isWechatyPluginUninstaller,
}                             from './plugin'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100

101
export interface WechatyOptions {
102
  memory?        : MemoryCard,
103
  name?          : string,                    // Wechaty Name
104

105 106 107
  puppet?        : PuppetModuleName | Puppet, // Puppet name or instance
  puppetOptions? : PuppetOptions,             // Puppet TOKEN
  ioToken?       : string,                    // Io TOKEN
108
}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
110 111
const PUPPET_MEMORY_NAME = 'puppet'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
112
/**
L
lijiarui 已提交
113
 * Main bot class.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
 *
115
 * A `Bot` is a WeChat client depends on which puppet you use.
L
lijiarui 已提交
116
 * It may equals
117 118 119
 * - web-WeChat, when you use: [puppet-puppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer)/[puppet-wechat4u](https://github.com/wechaty/wechaty-puppet-wechat4u)
 * - ipad-WeChat, when you use: [puppet-padchat](https://github.com/wechaty/wechaty-puppet-padchat)
 * - ios-WeChat, when you use: puppet-ioscat
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
 *
L
lijiarui 已提交
121
 * See more:
122
 * - [What is a Puppet in Wechaty](https://github.com/wechaty/wechaty-getting-started/wiki/FAQ-EN#31-what-is-a-puppet-in-wechaty)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123
 *
L
lijiarui 已提交
124 125 126 127 128 129
 * > 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()
130
 * bot.on('scan',    (qrCode, status) => console.log('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode)))
131
 * bot.on('login',   user => console.log(`User ${user} logged in`))
L
lijiarui 已提交
132 133
 * bot.on('message', message => console.log(`Message: ${message}`))
 * bot.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134
 */
135
class Wechaty extends WechatyEventEmitter implements Sayable {
136

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
137
  static readonly VERSION = VERSION
138 139
  static readonly log = log
  readonly log = log
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140

141 142
  public  readonly state      : StateSwitch
  private readonly readyState : StateSwitch
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143
  public  readonly wechaty    : Wechaty
144

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145
  /**
146
   * singleton globalInstance
147
   * @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148
   */
149
  private static globalInstance: Wechaty
150

G
Gcaufy 已提交
151 152
  private static globalPluginList: WechatyPlugin[] = []

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153 154
  private pluginUninstallerList: WechatyPluginUninstaller[]

155
  private memory?: MemoryCard
156

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157 158
  private lifeTimer? : NodeJS.Timer
  private io?        : Io
159

160 161
  public puppet!: Puppet

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
  /**
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
163
   * the cuid
164
   * @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
165
   */
166
  public readonly id : string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167

168 169 170 171 172 173 174 175 176 177 178
  protected wechatifiedContact?        : typeof Contact
  protected wechatifiedContactSelf?    : typeof ContactSelf
  protected wechatifiedFriendship?     : typeof Friendship
  protected wechatifiedImage?          : typeof Image
  protected wechatifiedMessage?        : typeof Message
  protected wechatifiedMiniProgram?    : typeof MiniProgram
  protected wechatifiedRoom?           : typeof Room
  protected wechatifiedRoomInvitation? : typeof RoomInvitation
  protected wechatifiedTag?            : typeof Tag
  protected wechatifiedUrlLink?        : typeof UrlLink

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179 180 181 182 183 184 185 186 187 188
  get Contact ()        : typeof Contact         { return guardWechatify(this.wechatifiedContact)        }
  get ContactSelf ()    : typeof ContactSelf     { return guardWechatify(this.wechatifiedContactSelf)    }
  get Friendship ()     : typeof Friendship      { return guardWechatify(this.wechatifiedFriendship)     }
  get Image ()          : typeof Image           { return guardWechatify(this.wechatifiedImage)          }
  get Message ()        : typeof Message         { return guardWechatify(this.wechatifiedMessage)        }
  get MiniProgram ()    : typeof MiniProgram     { return guardWechatify(this.wechatifiedMiniProgram)    }
  get Room ()           : typeof Room            { return guardWechatify(this.wechatifiedRoom)           }
  get RoomInvitation () : typeof RoomInvitation  { return guardWechatify(this.wechatifiedRoomInvitation) }
  get Tag ()            : typeof Tag             { return guardWechatify(this.wechatifiedTag)            }
  get UrlLink ()        : typeof UrlLink         { return guardWechatify(this.wechatifiedUrlLink)        }
189

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191
   * Get the global instance of Wechaty
L
lijiarui 已提交
192
   *
L
lijiarui 已提交
193 194
   * @param {WechatyOptions} [options={}]
   *
L
lijiarui 已提交
195 196 197
   * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
   * const { Wechaty } = require('wechaty')
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
198
   * Wechaty.instance() // Global instance
199
   * .on('scan', (url, status) => console.log(`Scan QR Code to login: ${status}\n${url}`))
200
   * .on('login',       user => console.log(`User ${user} logged in`))
L
lijiarui 已提交
201
   * .on('message',  message => console.log(`Message: ${message}`))
L
lijiarui 已提交
202
   * .start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203
   */
204
  public static instance (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205 206
    options?: WechatyOptions,
  ) {
207
    if (options && this.globalInstance) {
208
      throw new Error('instance can be only initialized once by options!')
209
    }
210 211
    if (!this.globalInstance) {
      this.globalInstance = new Wechaty(options)
212
    }
213
    return this.globalInstance
214 215
  }

G
Gcaufy 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
  /**
   * @param   {WechatyPlugin[]} plugins      - The plugins you want to use
   *
   * @return  {Wechaty}                      - this for chaining,
   *
   * @desc
   * For wechaty ecosystem, allow user to define a 3rd party plugin for the all wechaty instances
   *
   * @example
   * // Report all chat message to my server.
   *
   * function WechatyReportPlugin(options: { url: string }) {
   *   return function (this: Wechaty) {
   *     this.on('message', message => http.post(options.url, { data: message }))
   *   }
   * }
   *
   * bot.use(WechatyReportPlugin({ url: 'http://somewhere.to.report.your.data.com' })
   */
  public static use (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236
    ...plugins:  (WechatyPlugin | WechatyPlugin[])[]
G
Gcaufy 已提交
237
  ) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238 239
    const pluginList = plugins.flat()
    this.globalPluginList = this.globalPluginList.concat(pluginList)
G
Gcaufy 已提交
240 241
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
242
  /**
243
   * The term [Puppet](https://github.com/wechaty/wechaty/wiki/Puppet) in Wechaty is an Abstract Class for implementing protocol plugins.
244
   * The plugins are the component that helps Wechaty to control the WeChat(that's the reason we call it puppet).
L
lijiarui 已提交
245
   * The plugins are named XXXPuppet, for example:
246 247
   * - [PuppetPuppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer):
   * - [PuppetPadchat](https://github.com/wechaty/wechaty-puppet-padchat)
L
lijiarui 已提交
248
   *
L
lijiarui 已提交
249 250 251 252
   * @typedef    PuppetModuleName
   * @property   {string}  PUPPET_DEFAULT
   * The default puppet.
   * @property   {string}  wechaty-puppet-wechat4u
L
lijiarui 已提交
253
   * 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.
L
lijiarui 已提交
254
   * @property   {string}  wechaty-puppet-padchat
255
   * - Using the WebSocket protocol to connect with a Protocol Server for controlling the iPad WeChat program.
L
lijiarui 已提交
256
   * @property   {string}  wechaty-puppet-puppeteer
L
lijiarui 已提交
257
   * - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
L
lijiarui 已提交
258
   * @property   {string}  wechaty-puppet-mock
L
lijiarui 已提交
259 260 261 262 263 264 265
   * - Using the mock data to mock wechat operation, just for test.
   */

  /**
   * The option parameter to create a wechaty instance
   *
   * @typedef    WechatyOptions
266
   * @property   {string}                 name            -Wechaty Name. </br>
L
lijiarui 已提交
267
   *          When you set this: </br>
268 269
   *          `new Wechaty({name: 'wechaty-name'}) ` </br>
   *          it will generate a file called `wechaty-name.memory-card.json`. </br>
270 271
   *          This file stores the login information for bot. </br>
   *          If the file is valid, the bot can auto login so you don't need to scan the qrCode to login again. </br>
L
lijiarui 已提交
272 273
   *          Also, you can set the environment variable for `WECHATY_NAME` to set this value when you start. </br>
   *          eg:  `WECHATY_NAME="your-cute-bot-name" node bot.js`
274
   * @property   {PuppetModuleName | Puppet}    puppet             -Puppet name or instance
L
lijiarui 已提交
275 276 277 278 279 280 281 282
   * @property   {Partial<PuppetOptions>} puppetOptions      -Puppet TOKEN
   * @property   {string}                 ioToken            -Io TOKEN
   */

  /**
   * Creates an instance of Wechaty.
   * @param {WechatyOptions} [options={}]
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283
   */
284
  constructor (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
    private options: WechatyOptions = {},
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286
  ) {
287
    super()
Y
Yuan Gao 已提交
288
    log.verbose('Wechaty', 'constructor()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289

290
    this.memory = this.options.memory
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291 292

    this.id = cuid()
293

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294 295
    this.state      = new StateSwitch('Wechaty', { log })
    this.readyState = new StateSwitch('WechatyReady', { log })
296

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297 298
    this.wechaty = this

299
    /**
300
     * Huan(202008):
301
     *
302 303 304 305 306
     * Set max listeners to 1K, so that we can add lots of listeners without the warning message.
     * The listeners might be one of the following functionilities:
     *  1. Plugins
     *  2. Redux Observables
     *  3. etc...
307
     */
308
    super.setMaxListeners(1024)
G
Gcaufy 已提交
309

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
310
    this.pluginUninstallerList = []
Y
Yuan Gao 已提交
311
    this.installGlobalPlugin()
312 313
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
314
  /**
315
   * @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
316
   */
317
  public override toString () {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
318 319 320 321 322
    if (!this.options) {
      return this.constructor.name
    }

    return [
323 324
      'Wechaty#',
      this.id,
325 326
      `<${(this.options && this.options.puppet) || ''}>`,
      `(${(this.memory && this.memory.name) || ''})`,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327 328
    ].join('')
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
329

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
330
  /**
Y
Yuan Gao 已提交
331
   * Wechaty bot name set by `options.name`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
332 333 334 335 336 337
   * default: `wechaty`
   */
  public name () {
    return this.options.name || 'wechaty'
  }

338
  public override on (event: WechatyEventName, listener: (...args: any[]) => any): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
339 340 341 342 343 344
    log.verbose('Wechaty', 'on(%s, listener) registering... listenerCount: %s',
      event,
      this.listenerCount(event),
    )

    return super.on(event, listener)
345 346
  }

G
Gcaufy 已提交
347 348 349 350 351 352 353 354 355 356 357 358
  /**
   * @param   {WechatyPlugin[]} plugins      - The plugins you want to use
   *
   * @return  {Wechaty}                      - this for chaining,
   *
   * @desc
   * For wechaty ecosystem, allow user to define a 3rd party plugin for the current wechaty instance.
   *
   * @example
   * // The same usage with Wechaty.use().
   *
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
359
  public use (...plugins: (WechatyPlugin | WechatyPlugin[])[]) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
360 361 362 363 364 365 366 367
    const pluginList = plugins.flat() as WechatyPlugin[]
    const uninstallerList = pluginList
      .map(plugin => plugin(this))
      .filter(isWechatyPluginUninstaller)

    this.pluginUninstallerList.push(
      ...uninstallerList,
    )
G
Gcaufy 已提交
368 369 370
    return this
  }

Y
Yuan Gao 已提交
371
  private installGlobalPlugin () {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372 373 374 375 376 377 378 379 380

    const uninstallerList = instanceToClass(this, Wechaty)
      .globalPluginList
      .map(plugin => plugin(this))
      .filter(isWechatyPluginUninstaller)

    this.pluginUninstallerList.push(
      ...uninstallerList,
    )
G
Gcaufy 已提交
381 382
  }

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

386
    const initialized = !!this.puppet
387

388 389
    if (initialized) {
      log.verbose('Wechaty', 'initPuppet(%s) had already been initialized, no need to init twice', this.options.puppet)
390 391 392
      return
    }

393 394 395 396
    if (!this.memory) {
      throw new Error('no memory')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
397
    const puppet       = this.options.puppet || config.systemPuppetName()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
398
    const puppetMemory = this.memory.multiplex(PUPPET_MEMORY_NAME)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
400 401 402
    const puppetInstance = await PuppetManager.resolve({
      puppet,
      puppetOptions : this.options.puppetOptions,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403
      // wechaty       : this,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
404
    })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
406 407 408
    /**
     * Plug the Memory Card to Puppet
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
409 410
    puppetInstance.setMemory(puppetMemory)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411
    this.initPuppetEventBridge(puppetInstance)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
412 413 414 415 416 417 418 419 420
    this.wechatifyUserModules(puppetInstance)

    /**
      * Private Event
      *  emit puppet when set
      *
      * Huan(202005)
      */
    ;(this.emit as any)('puppet', puppetInstance)
421
  }
422

423
  protected initPuppetEventBridge (puppet: Puppet) {
424 425 426
    log.verbose('Wechaty', 'initPuppetEventBridge(%s)', puppet)

    const eventNameList: PuppetEventName[] = Object.keys(PUPPET_EVENT_DICT) as PuppetEventName[]
427
    for (const eventName of eventNameList) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
428 429 430 431 432
      log.verbose('Wechaty',
        'initPuppetEventBridge() puppet.on(%s) (listenerCount:%s) registering...',
        eventName,
        puppet.listenerCount(eventName),
      )
433 434

      switch (eventName) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
435
        case 'dong':
436 437
          puppet.on('dong', payload => {
            this.emit('dong', payload.data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
438 439 440
          })
          break

441
        case 'error':
442 443
          puppet.on('error', payload => {
            this.emit('error', new Error(payload.data))
444 445 446
          })
          break

Huan (李卓桓)'s avatar
clean  
Huan (李卓桓) 已提交
447 448
        case 'heartbeat':
          puppet.on('heartbeat', payload => {
449 450 451
            /**
             * Use `watchdog` event from Puppet to `heartbeat` Wechaty.
             */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
            // TODO: use a throttle queue to prevent beat too fast.
Huan (李卓桓)'s avatar
clean  
Huan (李卓桓) 已提交
453
            this.emit('heartbeat', payload.data)
454 455 456
          })
          break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457
        case 'friendship':
458 459
          puppet.on('friendship', async payload => {
            const friendship = this.Friendship.load(payload.friendshipId)
460 461 462 463 464 465 466
            try {
              await friendship.ready()
              this.emit('friendship', friendship)
              friendship.contact().emit('friendship', friendship)
            } catch (e) {
              this.emit('error', e)
            }
467 468 469 470
          })
          break

        case 'login':
471 472
          puppet.on('login', async payload => {
            const contact = this.ContactSelf.load(payload.contactId)
473 474 475 476 477 478
            try {
              await contact.ready()
              this.emit('login', contact)
            } catch (e) {
              this.emit('error', e)
            }
479 480 481 482
          })
          break

        case 'logout':
483 484
          puppet.on('logout', async payload => {
            const contact = this.ContactSelf.load(payload.contactId)
485 486 487 488 489 490
            try {
              await contact.ready()
              this.emit('logout', contact, payload.data)
            } catch (e) {
              this.emit('error', e)
            }
491 492 493 494
          })
          break

        case 'message':
495 496
          puppet.on('message', async payload => {
            const msg = this.Message.load(payload.messageId)
497 498 499 500 501 502 503 504 505 506 507 508
            try {
              await msg.ready()
              this.emit('message', msg)

              const room = msg.room()
              if (room) {
                room.emit('message', msg)
              } else {
                msg.talker().emit('message', msg)
              }
            } catch (e) {
              this.emit('error', e)
509
            }
510 511 512
          })
          break

513 514 515 516 517 518 519 520 521
        case 'ready':
          puppet.on('ready', () => {
            log.silly('Wechaty', 'initPuppetEventBridge() puppet.on(ready)')

            this.emit('ready')
            this.readyState.on(true)
          })
          break

522
        case 'room-invite':
523 524
          puppet.on('room-invite', async payload => {
            const roomInvitation = this.RoomInvitation.load(payload.roomInvitationId)
525
            this.emit('room-invite', roomInvitation)
526 527 528
          })
          break

529
        case 'room-join':
530 531
          puppet.on('room-join', async payload => {
            const room = this.Room.load(payload.roomId)
532 533
            try {
              await room.sync()
534

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

538 539 540
              const inviter = this.Contact.load(payload.inviterId)
              await inviter.ready()
              const date = timestampToDate(payload.timestamp)
541

542 543 544 545 546
              this.emit('room-join', room, inviteeList, inviter, date)
              room.emit('join', inviteeList, inviter, date)
            } catch (e) {
              this.emit('error', e)
            }
547 548 549 550
          })
          break

        case 'room-leave':
551
          puppet.on('room-leave', async payload => {
552 553
            try {
              const room = this.Room.load(payload.roomId)
S
SuperChang 已提交
554

555 556 557 558
              /**
               * See: https://github.com/wechaty/wechaty/pull/1833
               */
              await room.sync()
559

560 561
              const leaverList = payload.removeeIdList.map(id => this.Contact.load(id))
              await Promise.all(leaverList.map(c => c.ready()))
562

563 564 565
              const remover = this.Contact.load(payload.removerId)
              await remover.ready()
              const date = timestampToDate(payload.timestamp)
566

567 568
              this.emit('room-leave', room, leaverList, remover, date)
              room.emit('leave', leaverList, remover, date)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
569

570 571 572 573 574 575 576 577
              // issue #254
              const selfId = this.puppet.selfId()
              if (selfId && payload.removeeIdList.includes(selfId)) {
                await this.puppet.dirtyPayload(PayloadType.Room, payload.roomId)
                await this.puppet.dirtyPayload(PayloadType.RoomMember, payload.roomId)
              }
            } catch (e) {
              this.emit('error', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
578
            }
579 580 581 582
          })
          break

        case 'room-topic':
583
          puppet.on('room-topic', async payload => {
584 585 586
            try {
              const room = this.Room.load(payload.roomId)
              await room.sync()
587

588 589 590
              const changer = this.Contact.load(payload.changerId)
              await changer.ready()
              const date = timestampToDate(payload.timestamp)
591

592 593 594 595 596
              this.emit('room-topic', room, payload.newTopic, payload.oldTopic, changer, date)
              room.emit('topic', payload.newTopic, payload.oldTopic, changer, date)
            } catch (e) {
              this.emit('error', e)
            }
597 598 599 600
          })
          break

        case 'scan':
601 602
          puppet.on('scan', async payload => {
            this.emit('scan', payload.qrcode || '', payload.status, payload.data)
603 604 605
          })
          break

606
        case 'reset':
Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
607
          // Do not propagation `reset` event from puppet
608 609
          break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
610
        case 'dirty':
611
          /**
612
           * https://github.com/wechaty/wechaty-puppet-service/issues/43
613
           */
614
          puppet.on('dirty', async ({ payloadType, payloadId }) => {
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
            try {
              switch (payloadType) {
                case PayloadType.RoomMember:
                case PayloadType.Contact:
                  await this.Contact.load(payloadId).sync()
                  break
                case PayloadType.Room:
                  await this.Room.load(payloadId).sync()
                  break

                /**
                 * Huan(202008): noop for the following
                 */
                case PayloadType.Friendship:
                  // Friendship has no payload
                  break
                case PayloadType.Message:
                  // Message does not need to dirty (?)
                  break

                case PayloadType.Unknown:
                default:
                  throw new Error('unknown payload type: ' + payloadType)
              }
            } catch (e) {
              this.emit('error', e)
641 642
            }
          })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
643 644
          break

645
        default:
Huan (李卓桓)'s avatar
clean  
Huan (李卓桓) 已提交
646 647 648
          /**
           * Check: The eventName here should have the type `never`
           */
649
          throw new Error('eventName ' + eventName + ' unsupported!')
650 651

      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
652
    }
653
  }
654

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
655 656
  protected wechatifyUserModules (puppet: Puppet) {
    log.verbose('Wechaty', 'wechatifyUserModules(%s)', puppet)
657

658 659 660
    if (this.wechatifiedContactSelf) {
      throw new Error('can not be initialized twice!')
    }
661

662
    /**
663
     * 1. Setup Wechaty User Classes
664
     */
665 666 667 668 669 670 671 672 673 674
    this.wechatifiedContact        = wechatifyContact(this)
    this.wechatifiedContactSelf    = wechatifyContactSelf(this)
    this.wechatifiedFriendship     = wechatifyFriendship(this)
    this.wechatifiedImage          = wechatifyImage(this)
    this.wechatifiedMessage        = wechatifyMessage(this)
    this.wechatifiedMiniProgram    = wechatifyMiniProgram(this)
    this.wechatifiedRoom           = wechatifyRoom(this)
    this.wechatifiedRoomInvitation = wechatifyRoomInvitation(this)
    this.wechatifiedTag            = wechatifyTag(this)
    this.wechatifiedUrlLink        = wechatifyUrlLink(this)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
675

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
676
    this.puppet = puppet
677 678
  }

679 680 681 682
  /**
   * Start the bot, return Promise.
   *
   * @returns {Promise<void>}
L
lijiarui 已提交
683
   * @description
684
   * When you start the bot, bot will begin to login, need you WeChat scan qrcode to login
L
lijiarui 已提交
685
   * > Tips: All the bot operation needs to be triggered after start() is done
686 687 688 689
   * @example
   * await bot.start()
   * // do other stuff with bot here
   */
690
  public async start (): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
691
    log.verbose('Wechaty', '<%s>(%s) start() v%s is starting...',
692
      this.options.puppet || config.systemPuppetName(),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
693
      this.options.name   || '',
694 695
      this.version(),
    )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
696
    log.verbose('Wechaty', 'id: %s', this.id)
697 698 699

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

705 706
    this.readyState.off(true)

707 708 709 710
    if (this.lifeTimer) {
      throw new Error('start() lifeTimer exist')
    }

711 712 713
    this.state.on('pending')

    try {
714 715
      if (!this.memory) {
        this.memory = new MemoryCard(this.options.name)
716 717 718
      }

      try {
719
        await this.memory.load()
720 721
      } catch (e) {
        log.silly('Wechaty', 'start() memory.load() had already loaded')
722
      }
723

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
724
      await this.initPuppet()
725 726
      await this.puppet.start()

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
727 728 729 730 731 732 733 734
      if (this.options.ioToken) {
        this.io = new Io({
          token   : this.options.ioToken,
          wechaty : this,
        })
        await this.io.start()
      }

735
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
736
      console.error(e)
737 738
      log.error('Wechaty', 'start() exception: %s', e && e.message)
      Raven.captureException(e)
739 740 741 742 743 744 745 746 747
      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)
      }
748
      return
749 750 751 752
    }

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

753 754 755 756
    this.lifeTimer = setInterval(() => {
      log.silly('Wechaty', 'start() setInterval() this timer is to keep Wechaty running...')
    }, 1000 * 60 * 60)

757 758 759 760
    this.state.on(true)
    this.emit('start')
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
761 762 763 764 765 766 767
  /**
   * Stop the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.stop()
   */
768
  public async stop (): Promise<void> {
Y
Yuan Gao 已提交
769
    log.verbose('Wechaty', '<%s> stop() v%s is stopping ...',
770 771 772
      this.options.puppet || config.systemPuppetName(),
      this.version(),
    )
773

774 775 776 777 778 779 780 781 782
    /**
     * Uninstall Plugins
     *  no matter the state is `ON` or `OFF`.
     */
    while (this.pluginUninstallerList.length > 0) {
      const uninstaller = this.pluginUninstallerList.pop()
      if (uninstaller) uninstaller()
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
783
    if (this.state.off()) {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
784 785 786
      log.silly('Wechaty', 'stop() on an stopping/stopped instance')
      await this.state.ready('off')
      log.silly('Wechaty', 'stop() state.ready(off) resolved')
787
      return
788
    }
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
789

790 791
    this.readyState.off(true)

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

794 795 796 797 798
    if (this.lifeTimer) {
      clearInterval(this.lifeTimer)
      this.lifeTimer = undefined
    }

799
    try {
800
      await this.puppet.stop()
801 802 803
    } catch (e) {
      log.warn('Wechaty', 'stop() puppet.stop() exception: %s', e.message)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
804

805
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
806 807 808 809 810
      if (this.io) {
        await this.io.stop()
        this.io = undefined
      }

811
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
812
      log.error('Wechaty', 'stop() exception: %s', e.message)
813
      Raven.captureException(e)
814
      this.emit('error', e)
815
    }
816 817 818

    this.state.off(true)
    this.emit('stop')
819
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
820

821 822 823
  public async ready (): Promise<void> {
    log.verbose('Wechaty', 'ready()')
    return this.readyState.ready('on').then(() => {
824
      return log.silly('Wechaty', 'ready() this.readyState.ready(on) resolved')
825 826 827
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
828
  /**
L
lijiarui 已提交
829 830 831 832 833
   * Logout the bot
   *
   * @returns {Promise<void>}
   * @example
   * await bot.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
834
   */
835
  public async logout (): Promise<void>  {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
836 837
    log.verbose('Wechaty', 'logout()')

838 839 840 841 842 843 844
    try {
      await this.puppet.logout()
    } catch (e) {
      log.error('Wechaty', 'logout() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
845
  }
846

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
847 848 849 850 851
  /**
   * Get the logon / logoff state
   *
   * @returns {boolean}
   * @example
852
   * if (bot.logonoff()) {
853
   *   console.log('Bot logged in')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
854
   * } else {
855
   *   console.log('Bot not logged in')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
856 857
   * }
   */
858
  public logonoff (): boolean {
859 860 861 862 863 864
    try {
      return this.puppet.logonoff()
    } catch (e) {
      // https://github.com/wechaty/wechaty/issues/1878
      return false
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
865 866
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
867
  /**
L
lijiarui 已提交
868 869
   * Get current user
   *
870
   * @returns {ContactSelf}
L
lijiarui 已提交
871
   * @example
872
   * const contact = bot.userSelf()
L
lijiarui 已提交
873
   * console.log(`Bot is ${contact.name()}`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
874
   */
875
  public userSelf (): ContactSelf {
876
    const userId = this.puppet.selfId()
877
    const user = this.ContactSelf.load(userId)
878
    return user
879
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
880

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
881 882 883 884 885
  public async say (text:     string)      : Promise<void>
  public async say (contact:  Contact)     : Promise<void>
  public async say (file:     FileBox)     : Promise<void>
  public async say (mini:     MiniProgram) : Promise<void>
  public async say (url:      UrlLink)     : Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
886 887

  public async say (...args: never[]): Promise<never>
888

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
889
  /**
L
lijiarui 已提交
890
   * Send message to userSelf, in other words, bot send message to itself.
891
   * > Tips:
892
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
893
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
894
   * @param {(string | Contact | FileBox | UrlLink | MiniProgram)} something
L
lijiarui 已提交
895 896
   * send text, Contact, or file to bot. </br>
   * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
897
   *
L
lijiarui 已提交
898
   * @returns {Promise<void>}
L
lijiarui 已提交
899
   *
L
lijiarui 已提交
900 901 902 903 904
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in
   *
L
lijiarui 已提交
905
   * // 1. send text to bot itself
L
lijiarui 已提交
906 907
   * await bot.say('hello!')
   *
L
lijiarui 已提交
908
   * // 2. send Contact to bot itself
909
   * const contact = await bot.Contact.find()
L
lijiarui 已提交
910 911
   * await bot.say(contact)
   *
L
lijiarui 已提交
912
   * // 3. send Image to bot itself from remote url
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
913
   * import { FileBox }  from 'wechaty'
914
   * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png')
L
lijiarui 已提交
915 916
   * await bot.say(fileBox)
   *
L
lijiarui 已提交
917
   * // 4. send Image to bot itself from local file
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
918
   * import { FileBox }  from 'wechaty'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
919
   * const fileBox = FileBox.fromFile('/tmp/text.jpg')
L
lijiarui 已提交
920
   * await bot.say(fileBox)
L
linyimin 已提交
921 922 923 924 925 926
   *
   * // 5. send Link to bot itself
   * const linkPayload = new UrlLink ({
   *   description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love',
   *   thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4',
   *   title       : 'Welcome to Wechaty',
927
   *   url         : 'https://github.com/wechaty/wechaty',
L
linyimin 已提交
928 929
   * })
   * await bot.say(linkPayload)
Z
zhaoic 已提交
930 931 932 933 934 935 936 937 938 939 940
   *
   * // 6. send MiniProgram to bot itself
   * const miniPayload = new MiniProgram ({
   *   username           : 'gh_xxxxxxx',     //get from mp.weixin.qq.com
   *   appid              : '',               //optional, get from mp.weixin.qq.com
   *   title              : '',               //optional
   *   pagepath           : '',               //optional
   *   description        : '',               //optional
   *   thumbnailurl       : '',               //optional
   * })
   * await bot.say(miniPayload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
941
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
942

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
943 944 945 946 947 948 949 950 951 952
  public async say (
    something:  string
              | Contact
              | FileBox
              | MiniProgram
              | UrlLink
  ): Promise<void> {
    log.verbose('Wechaty', 'say(%s)', something)
    // huan: to make TypeScript happy
    await this.userSelf().say(something as any)
953 954
  }

L
lijiarui 已提交
955
  /**
956
   * @ignore
L
lijiarui 已提交
957
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
958 959 960
  public static version (gitHash = false): string {
    if (gitHash && GIT_COMMIT_HASH) {
      return `#git[${GIT_COMMIT_HASH}]`
L
lijiarui 已提交
961 962 963 964
    }
    return VERSION
  }

965
  /**
966
   * @ignore
967 968 969 970 971 972 973 974 975
   * 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'
   */
976
  public version (forceNpm = false): string {
L
lijiarui 已提交
977 978 979
    return Wechaty.version(forceNpm)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
980
  /**
981
   * @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
982
   */
983
  public static async sleep (millisecond: number): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
984
    await new Promise(resolve => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
985 986 987 988
      setTimeout(resolve, millisecond)
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
989 990 991 992 993 994 995
  /**
   * @ignore
   */
  public async sleep (millisecond: number): Promise<void> {
    return Wechaty.sleep(millisecond)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
996
  /**
Huan (李卓桓)'s avatar
jsdoc  
Huan (李卓桓) 已提交
997
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
998
   */
999
  public ding (data?: string): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1000 1001
    log.silly('Wechaty', 'ding(%s)', data || '')

1002
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1003
      this.puppet.ding(data)
1004 1005 1006
    } catch (e) {
      log.error('Wechaty', 'ding() exception: %s', e.message)
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1007
      throw e
1008
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1009
  }
1010 1011

  /**
1012
   * @ignore
1013
   */
1014
  private memoryCheck (minMegabyte = 4): void {
1015 1016
    const freeMegabyte = Math.floor(os.freemem() / 1024 / 1024)
    log.silly('Wechaty', 'memoryCheck() free: %d MB, require: %d MB',
1017 1018
      freeMegabyte, minMegabyte,
    )
1019 1020 1021 1022 1023 1024 1025

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

  /**
1028
   * @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1029
   */
1030
  public reset (reason?: string): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1031 1032 1033 1034 1035
    log.verbose('Wechaty', 'reset() with reason: %s, call stack: %s',
      reason || 'no reason',
      // https://stackoverflow.com/a/2060330/1123955
      new Error().stack,
    )
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057

    this.puppet.stop()
      .then(() => this.puppet.start())
      .finally(() => {
        log.verbose('Wechaty', 'reset() done.')
      })
      .catch(e => {
        log.warn('Wechaty', 'reset() rejection: %s', e && e.message)

        /**
         * Dealing with https://github.com/wechaty/wechaty/issues/2197
         */
        setTimeout(
          () => this.reset(),
          Math.floor(
            (
              10 + 10 * Math.random()
            ) * 1000
          )
        )

      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1058 1059
  }

1060
  public unref (): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1061 1062 1063 1064 1065 1066 1067
    log.verbose('Wechaty', 'unref()')

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

    this.puppet.unref()
1068
  }
1069

1070
}
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084

/**
 * Huan(202008): we will bind the wechaty puppet with user modules (Contact, Room, etc) together inside the start() method
 */
function guardWechatify<T extends Function> (userModule?: T): T {
  if (!userModule) {
    throw new Error('Wechaty user module (for example, wechaty.Room) can not be used before wechaty.start()!')
  }
  return userModule
}

export {
  Wechaty,
}