UNPKG

32.6 kBPlain TextView Raw
1/**
2 * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
3 *
4 * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
5 * Wechaty Contributors <https://github.com/wechaty>.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 */
20import cuid from 'cuid'
21import os from 'os'
22
23import { instanceToClass } from 'clone-class'
24
25import {
26 Puppet,
27
28 MemoryCard,
29 StateSwitch,
30
31 PUPPET_EVENT_DICT,
32 PuppetEventName,
33 PuppetOptions,
34 PayloadType,
35} from 'wechaty-puppet'
36
37import { captureException } from './raven'
38
39import {
40 FileBox,
41 config,
42 log,
43} from './config'
44
45import {
46 VERSION,
47 GIT_COMMIT_HASH,
48} from './version'
49
50import {
51 Sayable,
52} from './types'
53
54import {
55 Io,
56} from './io'
57import {
58 PuppetModuleName,
59} from './puppet-config'
60import {
61 PuppetManager,
62} from './puppet-manager'
63
64import {
65 Contact,
66 ContactSelf,
67 Friendship,
68 Image,
69 Message,
70 MiniProgram,
71 Room,
72 RoomInvitation,
73 Tag,
74 UrlLink,
75
76 wechatifyContact,
77 wechatifyContactSelf,
78 wechatifyFriendship,
79 wechatifyImage,
80 wechatifyMessage,
81 wechatifyMiniProgram,
82 wechatifyRoom,
83 wechatifyRoomInvitation,
84 wechatifyTag,
85 wechatifyUrlLink,
86} from './user/mod'
87
88import { timestampToDate } from './helper-functions/pure/timestamp-to-date'
89
90import {
91 WechatyEventEmitter,
92 WechatyEventName,
93} from './events/wechaty-events'
94
95import {
96 WechatyPlugin,
97 WechatyPluginUninstaller,
98 isWechatyPluginUninstaller,
99} from './plugin'
100
101export interface WechatyOptions {
102 memory? : MemoryCard,
103 name? : string, // Wechaty Name
104
105 puppet? : PuppetModuleName | Puppet, // Puppet name or instance
106 puppetOptions? : PuppetOptions, // Puppet TOKEN
107 ioToken? : string, // Io TOKEN
108}
109
110const PUPPET_MEMORY_NAME = 'puppet'
111
112/**
113 * Main bot class.
114 *
115 * A `Bot` is a WeChat client depends on which puppet you use.
116 * It may equals
117 * - web-WeChat, when you use: [puppet-puppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer)/[puppet-wechat4u](https://github.com/wechaty/wechaty-puppet-wechat4u)
118 * - ipad-WeChat, when you use: [puppet-padchat](https://github.com/wechaty/wechaty-puppet-padchat)
119 * - ios-WeChat, when you use: puppet-ioscat
120 *
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)
123 *
124 * > If you want to know how to send message, see [Message](#Message) <br>
125 * > If you want to know how to get contact, see [Contact](#Contact)
126 *
127 * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
128 * const { Wechaty } = require('wechaty')
129 * 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`))
132 * bot.on('message', message => console.log(`Message: ${message}`))
133 * bot.start()
134 */
135class Wechaty extends WechatyEventEmitter implements Sayable {
136
137 static readonly VERSION = VERSION
138 static readonly log = log
139 readonly log = log
140
141 public readonly state : StateSwitch
142 private readonly readyState : StateSwitch
143 public readonly wechaty : Wechaty
144
145 /**
146 * singleton globalInstance
147 * @ignore
148 */
149 private static globalInstance: Wechaty
150
151 private static globalPluginList: WechatyPlugin[] = []
152
153 private pluginUninstallerList: WechatyPluginUninstaller[]
154
155 private memory?: MemoryCard
156
157 private lifeTimer? : NodeJS.Timer
158 private io? : Io
159
160 public puppet!: Puppet
161
162 /**
163 * the cuid
164 * @ignore
165 */
166 public readonly id : string
167
168 protected wechatifiedContact? : typeof Contact
169 protected wechatifiedContactSelf? : typeof ContactSelf
170 protected wechatifiedFriendship? : typeof Friendship
171 protected wechatifiedImage? : typeof Image
172 protected wechatifiedMessage? : typeof Message
173 protected wechatifiedMiniProgram? : typeof MiniProgram
174 protected wechatifiedRoom? : typeof Room
175 protected wechatifiedRoomInvitation? : typeof RoomInvitation
176 protected wechatifiedTag? : typeof Tag
177 protected wechatifiedUrlLink? : typeof UrlLink
178
179 get Contact () : typeof Contact { return guardWechatify(this.wechatifiedContact) }
180 get ContactSelf () : typeof ContactSelf { return guardWechatify(this.wechatifiedContactSelf) }
181 get Friendship () : typeof Friendship { return guardWechatify(this.wechatifiedFriendship) }
182 get Image () : typeof Image { return guardWechatify(this.wechatifiedImage) }
183 get Message () : typeof Message { return guardWechatify(this.wechatifiedMessage) }
184 get MiniProgram () : typeof MiniProgram { return guardWechatify(this.wechatifiedMiniProgram) }
185 get Room () : typeof Room { return guardWechatify(this.wechatifiedRoom) }
186 get RoomInvitation () : typeof RoomInvitation { return guardWechatify(this.wechatifiedRoomInvitation) }
187 get Tag () : typeof Tag { return guardWechatify(this.wechatifiedTag) }
188 get UrlLink () : typeof UrlLink { return guardWechatify(this.wechatifiedUrlLink) }
189
190 /**
191 * Get the global instance of Wechaty
192 *
193 * @param {WechatyOptions} [options={}]
194 *
195 * @example <caption>The World's Shortest ChatBot Code: 6 lines of JavaScript</caption>
196 * const { Wechaty } = require('wechaty')
197 *
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`))
201 * .on('message', message => console.log(`Message: ${message}`))
202 * .start()
203 */
204 public static instance (
205 options?: WechatyOptions,
206 ) {
207 if (options && this.globalInstance) {
208 throw new Error('instance can be only initialized once by options!')
209 }
210 if (!this.globalInstance) {
211 this.globalInstance = new Wechaty(options)
212 }
213 return this.globalInstance
214 }
215
216 /**
217 * @param {WechatyPlugin[]} plugins - The plugins you want to use
218 *
219 * @return {Wechaty} - this for chaining,
220 *
221 * @desc
222 * For wechaty ecosystem, allow user to define a 3rd party plugin for the all wechaty instances
223 *
224 * @example
225 * // Report all chat message to my server.
226 *
227 * function WechatyReportPlugin(options: { url: string }) {
228 * return function (this: Wechaty) {
229 * this.on('message', message => http.post(options.url, { data: message }))
230 * }
231 * }
232 *
233 * bot.use(WechatyReportPlugin({ url: 'http://somewhere.to.report.your.data.com' })
234 */
235 public static use (
236 ...plugins: (WechatyPlugin | WechatyPlugin[])[]
237 ) {
238 const pluginList = plugins.flat()
239 this.globalPluginList = this.globalPluginList.concat(pluginList)
240 }
241
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).
245 * The plugins are named XXXPuppet, for example:
246 * - [PuppetPuppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer):
247 * - [PuppetPadchat](https://github.com/wechaty/wechaty-puppet-padchat)
248 *
249 * @typedef PuppetModuleName
250 * @property {string} PUPPET_DEFAULT
251 * The default puppet.
252 * @property {string} wechaty-puppet-wechat4u
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.
254 * @property {string} wechaty-puppet-padchat
255 * - Using the WebSocket protocol to connect with a Protocol Server for controlling the iPad WeChat program.
256 * @property {string} wechaty-puppet-puppeteer
257 * - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
258 * @property {string} wechaty-puppet-mock
259 * - Using the mock data to mock wechat operation, just for test.
260 */
261
262 /**
263 * The option parameter to create a wechaty instance
264 *
265 * @typedef WechatyOptions
266 * @property {string} name -Wechaty Name. </br>
267 * When you set this: </br>
268 * `new Wechaty({name: 'wechaty-name'}) ` </br>
269 * it will generate a file called `wechaty-name.memory-card.json`. </br>
270 * This file stores the login information for bot. </br>
271 * If the file is valid, the bot can auto login so you don't need to scan the qrCode to login again. </br>
272 * Also, you can set the environment variable for `WECHATY_NAME` to set this value when you start. </br>
273 * eg: `WECHATY_NAME="your-cute-bot-name" node bot.js`
274 * @property {PuppetModuleName | Puppet} puppet -Puppet name or instance
275 * @property {Partial<PuppetOptions>} puppetOptions -Puppet TOKEN
276 * @property {string} ioToken -Io TOKEN
277 */
278
279 /**
280 * Creates an instance of Wechaty.
281 * @param {WechatyOptions} [options={}]
282 *
283 */
284 constructor (
285 private options: WechatyOptions = {},
286 ) {
287 super()
288 log.verbose('Wechaty', 'constructor()')
289
290 this.memory = this.options.memory
291
292 this.id = cuid()
293
294 this.state = new StateSwitch('Wechaty', { log })
295 this.readyState = new StateSwitch('WechatyReady', { log })
296
297 this.wechaty = this
298
299 /**
300 * Huan(202008):
301 *
302 * Set max listeners to 1K, so that we can add lots of listeners without the warning message.
303 * The listeners might be one of the following functionilities:
304 * 1. Plugins
305 * 2. Redux Observables
306 * 3. etc...
307 */
308 super.setMaxListeners(1024)
309
310 this.pluginUninstallerList = []
311 this.installGlobalPlugin()
312 }
313
314 /**
315 * @ignore
316 */
317 public override toString () {
318 if (!this.options) {
319 return this.constructor.name
320 }
321
322 return [
323 'Wechaty#',
324 this.id,
325 `<${(this.options && this.options.puppet) || ''}>`,
326 `(${(this.memory && this.memory.name) || ''})`,
327 ].join('')
328 }
329
330 /**
331 * Wechaty bot name set by `options.name`
332 * default: `wechaty`
333 */
334 public name () {
335 return this.options.name || 'wechaty'
336 }
337
338 public override on (event: WechatyEventName, listener: (...args: any[]) => any): this {
339 log.verbose('Wechaty', 'on(%s, listener) registering... listenerCount: %s',
340 event,
341 this.listenerCount(event),
342 )
343
344 return super.on(event, listener)
345 }
346
347 /**
348 * @param {WechatyPlugin[]} plugins - The plugins you want to use
349 *
350 * @return {Wechaty} - this for chaining,
351 *
352 * @desc
353 * For wechaty ecosystem, allow user to define a 3rd party plugin for the current wechaty instance.
354 *
355 * @example
356 * // The same usage with Wechaty.use().
357 *
358 */
359 public use (...plugins: (WechatyPlugin | WechatyPlugin[])[]) {
360 const pluginList = plugins.flat() as WechatyPlugin[]
361 const uninstallerList = pluginList
362 .map(plugin => plugin(this))
363 .filter(isWechatyPluginUninstaller)
364
365 this.pluginUninstallerList.push(
366 ...uninstallerList,
367 )
368 return this
369 }
370
371 private installGlobalPlugin () {
372
373 const uninstallerList = instanceToClass(this, Wechaty)
374 .globalPluginList
375 .map(plugin => plugin(this))
376 .filter(isWechatyPluginUninstaller)
377
378 this.pluginUninstallerList.push(
379 ...uninstallerList,
380 )
381 }
382
383 private async initPuppet (): Promise<void> {
384 log.verbose('Wechaty', 'initPuppet() %s', this.options.puppet || '')
385
386 const initialized = !!this.puppet
387
388 if (initialized) {
389 log.verbose('Wechaty', 'initPuppet(%s) had already been initialized, no need to init twice', this.options.puppet)
390 return
391 }
392
393 if (!this.memory) {
394 throw new Error('no memory')
395 }
396
397 const puppet = this.options.puppet || config.systemPuppetName()
398 const puppetMemory = this.memory.multiplex(PUPPET_MEMORY_NAME)
399
400 const puppetInstance = await PuppetManager.resolve({
401 puppet,
402 puppetOptions : this.options.puppetOptions,
403 // wechaty : this,
404 })
405
406 /**
407 * Plug the Memory Card to Puppet
408 */
409 puppetInstance.setMemory(puppetMemory)
410
411 this.initPuppetEventBridge(puppetInstance)
412 this.wechatifyUserModules(puppetInstance)
413
414 /**
415 * Private Event
416 * emit puppet when set
417 *
418 * Huan(202005)
419 */
420 ;(this.emit as any)('puppet', puppetInstance)
421 }
422
423 protected initPuppetEventBridge (puppet: Puppet) {
424 log.verbose('Wechaty', 'initPuppetEventBridge(%s)', puppet)
425
426 const eventNameList: PuppetEventName[] = Object.keys(PUPPET_EVENT_DICT) as PuppetEventName[]
427 for (const eventName of eventNameList) {
428 log.verbose('Wechaty',
429 'initPuppetEventBridge() puppet.on(%s) (listenerCount:%s) registering...',
430 eventName,
431 puppet.listenerCount(eventName),
432 )
433
434 switch (eventName) {
435 case 'dong':
436 puppet.on('dong', payload => {
437 this.emit('dong', payload.data)
438 })
439 break
440
441 case 'error':
442 puppet.on('error', payload => {
443 this.emit('error', new Error(payload.data))
444 })
445 break
446
447 case 'heartbeat':
448 puppet.on('heartbeat', payload => {
449 /**
450 * Use `watchdog` event from Puppet to `heartbeat` Wechaty.
451 */
452 // TODO: use a throttle queue to prevent beat too fast.
453 this.emit('heartbeat', payload.data)
454 })
455 break
456
457 case 'friendship':
458 puppet.on('friendship', async payload => {
459 const friendship = this.Friendship.load(payload.friendshipId)
460 try {
461 await friendship.ready()
462 this.emit('friendship', friendship)
463 friendship.contact().emit('friendship', friendship)
464 } catch (e) {
465 this.emit('error', e)
466 }
467 })
468 break
469
470 case 'login':
471 puppet.on('login', async payload => {
472 const contact = this.ContactSelf.load(payload.contactId)
473 try {
474 await contact.ready()
475 this.emit('login', contact)
476 } catch (e) {
477 this.emit('error', e)
478 }
479 })
480 break
481
482 case 'logout':
483 puppet.on('logout', async payload => {
484 const contact = this.ContactSelf.load(payload.contactId)
485 try {
486 await contact.ready()
487 this.emit('logout', contact, payload.data)
488 } catch (e) {
489 this.emit('error', e)
490 }
491 })
492 break
493
494 case 'message':
495 puppet.on('message', async payload => {
496 const msg = this.Message.load(payload.messageId)
497 try {
498 await msg.ready()
499 this.emit('message', msg)
500
501 const room = msg.room()
502 const listener = msg.listener()
503
504 if (room) {
505 room.emit('message', msg)
506 } else if (listener) {
507 listener.emit('message', msg)
508 } else {
509 this.emit('error', new Error('message without room or listener'))
510 }
511 } catch (e) {
512 this.emit('error', e)
513 }
514 })
515 break
516
517 case 'ready':
518 puppet.on('ready', () => {
519 log.silly('Wechaty', 'initPuppetEventBridge() puppet.on(ready)')
520
521 this.emit('ready')
522 this.readyState.on(true)
523 })
524 break
525
526 case 'room-invite':
527 puppet.on('room-invite', async payload => {
528 const roomInvitation = this.RoomInvitation.load(payload.roomInvitationId)
529 this.emit('room-invite', roomInvitation)
530 })
531 break
532
533 case 'room-join':
534 puppet.on('room-join', async payload => {
535 const room = this.Room.load(payload.roomId)
536 try {
537 await room.sync()
538
539 const inviteeList = payload.inviteeIdList.map(id => this.Contact.load(id))
540 await Promise.all(inviteeList.map(c => c.ready()))
541
542 const inviter = this.Contact.load(payload.inviterId)
543 await inviter.ready()
544 const date = timestampToDate(payload.timestamp)
545
546 this.emit('room-join', room, inviteeList, inviter, date)
547 room.emit('join', inviteeList, inviter, date)
548 } catch (e) {
549 this.emit('error', e)
550 }
551 })
552 break
553
554 case 'room-leave':
555 puppet.on('room-leave', async payload => {
556 try {
557 const room = this.Room.load(payload.roomId)
558
559 /**
560 * See: https://github.com/wechaty/wechaty/pull/1833
561 */
562 await room.sync()
563
564 const leaverList = payload.removeeIdList.map(id => this.Contact.load(id))
565 await Promise.all(leaverList.map(c => c.ready()))
566
567 const remover = this.Contact.load(payload.removerId)
568 await remover.ready()
569 const date = timestampToDate(payload.timestamp)
570
571 this.emit('room-leave', room, leaverList, remover, date)
572 room.emit('leave', leaverList, remover, date)
573
574 // issue #254
575 const selfId = this.puppet.selfId()
576 if (selfId && payload.removeeIdList.includes(selfId)) {
577 await this.puppet.dirtyPayload(PayloadType.Room, payload.roomId)
578 await this.puppet.dirtyPayload(PayloadType.RoomMember, payload.roomId)
579 }
580 } catch (e) {
581 this.emit('error', e)
582 }
583 })
584 break
585
586 case 'room-topic':
587 puppet.on('room-topic', async payload => {
588 try {
589 const room = this.Room.load(payload.roomId)
590 await room.sync()
591
592 const changer = this.Contact.load(payload.changerId)
593 await changer.ready()
594 const date = timestampToDate(payload.timestamp)
595
596 this.emit('room-topic', room, payload.newTopic, payload.oldTopic, changer, date)
597 room.emit('topic', payload.newTopic, payload.oldTopic, changer, date)
598 } catch (e) {
599 this.emit('error', e)
600 }
601 })
602 break
603
604 case 'scan':
605 puppet.on('scan', async payload => {
606 this.emit('scan', payload.qrcode || '', payload.status, payload.data)
607 })
608 break
609
610 case 'reset':
611 // Do not propagation `reset` event from puppet
612 break
613
614 case 'dirty':
615 /**
616 * https://github.com/wechaty/wechaty-puppet-service/issues/43
617 */
618 puppet.on('dirty', async ({ payloadType, payloadId }) => {
619 try {
620 switch (payloadType) {
621 case PayloadType.RoomMember:
622 case PayloadType.Contact:
623 await this.Contact.load(payloadId).sync()
624 break
625 case PayloadType.Room:
626 await this.Room.load(payloadId).sync()
627 break
628
629 /**
630 * Huan(202008): noop for the following
631 */
632 case PayloadType.Friendship:
633 // Friendship has no payload
634 break
635 case PayloadType.Message:
636 // Message does not need to dirty (?)
637 break
638
639 case PayloadType.Unknown:
640 default:
641 throw new Error('unknown payload type: ' + payloadType)
642 }
643 } catch (e) {
644 this.emit('error', e)
645 }
646 })
647 break
648
649 default:
650 /**
651 * Check: The eventName here should have the type `never`
652 */
653 throw new Error('eventName ' + eventName + ' unsupported!')
654
655 }
656 }
657 }
658
659 protected wechatifyUserModules (puppet: Puppet) {
660 log.verbose('Wechaty', 'wechatifyUserModules(%s)', puppet)
661
662 if (this.wechatifiedContactSelf) {
663 throw new Error('can not be initialized twice!')
664 }
665
666 /**
667 * 1. Setup Wechaty User Classes
668 */
669 this.wechatifiedContact = wechatifyContact(this)
670 this.wechatifiedContactSelf = wechatifyContactSelf(this)
671 this.wechatifiedFriendship = wechatifyFriendship(this)
672 this.wechatifiedImage = wechatifyImage(this)
673 this.wechatifiedMessage = wechatifyMessage(this)
674 this.wechatifiedMiniProgram = wechatifyMiniProgram(this)
675 this.wechatifiedRoom = wechatifyRoom(this)
676 this.wechatifiedRoomInvitation = wechatifyRoomInvitation(this)
677 this.wechatifiedTag = wechatifyTag(this)
678 this.wechatifiedUrlLink = wechatifyUrlLink(this)
679
680 this.puppet = puppet
681 }
682
683 /**
684 * Start the bot, return Promise.
685 *
686 * @returns {Promise<void>}
687 * @description
688 * When you start the bot, bot will begin to login, need you WeChat scan qrcode to login
689 * > Tips: All the bot operation needs to be triggered after start() is done
690 * @example
691 * await bot.start()
692 * // do other stuff with bot here
693 */
694 public async start (): Promise<void> {
695 log.verbose('Wechaty', '<%s>(%s) start() v%s is starting...',
696 this.options.puppet || config.systemPuppetName(),
697 this.options.name || '',
698 this.version(),
699 )
700 log.verbose('Wechaty', 'id: %s', this.id)
701
702 if (this.state.on()) {
703 log.silly('Wechaty', 'start() on a starting/started instance')
704 await this.state.ready('on')
705 log.silly('Wechaty', 'start() state.ready() resolved')
706 return
707 }
708
709 this.readyState.off(true)
710
711 if (this.lifeTimer) {
712 throw new Error('start() lifeTimer exist')
713 }
714
715 this.state.on('pending')
716
717 try {
718 if (!this.memory) {
719 this.memory = new MemoryCard(this.options.name)
720 }
721
722 try {
723 await this.memory.load()
724 } catch (e) {
725 log.silly('Wechaty', 'start() memory.load() had already loaded')
726 }
727
728 await this.initPuppet()
729 await this.puppet.start()
730
731 if (this.options.ioToken) {
732 this.io = new Io({
733 token : this.options.ioToken,
734 wechaty : this,
735 })
736 await this.io.start()
737 }
738
739 } catch (e) {
740 console.error(e)
741 log.error('Wechaty', 'start() exception: %s', e && e.message)
742 captureException(e)
743 this.emit('error', e)
744
745 try {
746 await this.stop()
747 } catch (e) {
748 log.error('Wechaty', 'start() stop() exception: %s', e && e.message)
749 captureException(e)
750 this.emit('error', e)
751 }
752 return
753 }
754
755 this.on('heartbeat', () => this.memoryCheck())
756
757 this.lifeTimer = setInterval(() => {
758 log.silly('Wechaty', 'start() setInterval() this timer is to keep Wechaty running...')
759 }, 1000 * 60 * 60)
760
761 this.state.on(true)
762 this.emit('start')
763 }
764
765 /**
766 * Stop the bot
767 *
768 * @returns {Promise<void>}
769 * @example
770 * await bot.stop()
771 */
772 public async stop (): Promise<void> {
773 log.verbose('Wechaty', '<%s> stop() v%s is stopping ...',
774 this.options.puppet || config.systemPuppetName(),
775 this.version(),
776 )
777
778 /**
779 * Uninstall Plugins
780 * no matter the state is `ON` or `OFF`.
781 */
782 while (this.pluginUninstallerList.length > 0) {
783 const uninstaller = this.pluginUninstallerList.pop()
784 if (uninstaller) uninstaller()
785 }
786
787 if (this.state.off()) {
788 log.silly('Wechaty', 'stop() on an stopping/stopped instance')
789 await this.state.ready('off')
790 log.silly('Wechaty', 'stop() state.ready(off) resolved')
791 return
792 }
793
794 this.readyState.off(true)
795
796 this.state.off('pending')
797
798 if (this.lifeTimer) {
799 clearInterval(this.lifeTimer)
800 this.lifeTimer = undefined
801 }
802
803 try {
804 await this.puppet.stop()
805 } catch (e) {
806 log.warn('Wechaty', 'stop() puppet.stop() exception: %s', e.message)
807 }
808
809 try {
810 if (this.io) {
811 await this.io.stop()
812 this.io = undefined
813 }
814
815 } catch (e) {
816 log.error('Wechaty', 'stop() exception: %s', e.message)
817 captureException(e)
818 this.emit('error', e)
819 }
820
821 this.state.off(true)
822 this.emit('stop')
823 }
824
825 public async ready (): Promise<void> {
826 log.verbose('Wechaty', 'ready()')
827 return this.readyState.ready('on').then(() => {
828 return log.silly('Wechaty', 'ready() this.readyState.ready(on) resolved')
829 })
830 }
831
832 /**
833 * Logout the bot
834 *
835 * @returns {Promise<void>}
836 * @example
837 * await bot.logout()
838 */
839 public async logout (): Promise<void> {
840 log.verbose('Wechaty', 'logout()')
841
842 try {
843 await this.puppet.logout()
844 } catch (e) {
845 log.error('Wechaty', 'logout() exception: %s', e.message)
846 captureException(e)
847 throw e
848 }
849 }
850
851 /**
852 * Get the logon / logoff state
853 *
854 * @returns {boolean}
855 * @example
856 * if (bot.logonoff()) {
857 * console.log('Bot logged in')
858 * } else {
859 * console.log('Bot not logged in')
860 * }
861 */
862 public logonoff (): boolean {
863 try {
864 return this.puppet.logonoff()
865 } catch (e) {
866 // https://github.com/wechaty/wechaty/issues/1878
867 return false
868 }
869 }
870
871 /**
872 * Get current user
873 *
874 * @returns {ContactSelf}
875 * @example
876 * const contact = bot.userSelf()
877 * console.log(`Bot is ${contact.name()}`)
878 */
879 public userSelf (): ContactSelf {
880 const userId = this.puppet.selfId()
881 const user = this.ContactSelf.load(userId)
882 return user
883 }
884
885 public async say (text: string) : Promise<void>
886 public async say (contact: Contact) : Promise<void>
887 public async say (file: FileBox) : Promise<void>
888 public async say (mini: MiniProgram) : Promise<void>
889 public async say (url: UrlLink) : Promise<void>
890
891 public async say (...args: never[]): Promise<never>
892
893 /**
894 * Send message to userSelf, in other words, bot send message to itself.
895 * > Tips:
896 * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
897 *
898 * @param {(string | Contact | FileBox | UrlLink | MiniProgram)} something
899 * send text, Contact, or file to bot. </br>
900 * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
901 *
902 * @returns {Promise<void>}
903 *
904 * @example
905 * const bot = new Wechaty()
906 * await bot.start()
907 * // after logged in
908 *
909 * // 1. send text to bot itself
910 * await bot.say('hello!')
911 *
912 * // 2. send Contact to bot itself
913 * const contact = await bot.Contact.find()
914 * await bot.say(contact)
915 *
916 * // 3. send Image to bot itself from remote url
917 * import { FileBox } from 'wechaty'
918 * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png')
919 * await bot.say(fileBox)
920 *
921 * // 4. send Image to bot itself from local file
922 * import { FileBox } from 'wechaty'
923 * const fileBox = FileBox.fromFile('/tmp/text.jpg')
924 * await bot.say(fileBox)
925 *
926 * // 5. send Link to bot itself
927 * const linkPayload = new UrlLink ({
928 * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love',
929 * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4',
930 * title : 'Welcome to Wechaty',
931 * url : 'https://github.com/wechaty/wechaty',
932 * })
933 * await bot.say(linkPayload)
934 *
935 * // 6. send MiniProgram to bot itself
936 * const miniPayload = new MiniProgram ({
937 * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com
938 * appid : '', //optional, get from mp.weixin.qq.com
939 * title : '', //optional
940 * pagepath : '', //optional
941 * description : '', //optional
942 * thumbnailurl : '', //optional
943 * })
944 * await bot.say(miniPayload)
945 */
946
947 public async say (
948 something: string
949 | Contact
950 | FileBox
951 | MiniProgram
952 | UrlLink
953 ): Promise<void> {
954 log.verbose('Wechaty', 'say(%s)', something)
955 // huan: to make TypeScript happy
956 await this.userSelf().say(something as any)
957 }
958
959 /**
960 * @ignore
961 */
962 public static version (gitHash = false): string {
963 if (gitHash && GIT_COMMIT_HASH) {
964 return `#git[${GIT_COMMIT_HASH}]`
965 }
966 return VERSION
967 }
968
969 /**
970 * @ignore
971 * Return version of Wechaty
972 *
973 * @param {boolean} [forceNpm=false] - If set to true, will only return the version in package.json. </br>
974 * Otherwise will return git commit hash if .git exists.
975 * @returns {string} - the version number
976 * @example
977 * console.log(Wechaty.instance().version()) // return '#git[af39df]'
978 * console.log(Wechaty.instance().version(true)) // return '0.7.9'
979 */
980 public version (forceNpm = false): string {
981 return Wechaty.version(forceNpm)
982 }
983
984 /**
985 * @ignore
986 */
987 public static async sleep (millisecond: number): Promise<void> {
988 await new Promise<void>(resolve => {
989 setTimeout(resolve, millisecond)
990 })
991 }
992
993 /**
994 * @ignore
995 */
996 public async sleep (millisecond: number): Promise<void> {
997 return Wechaty.sleep(millisecond)
998 }
999
1000 /**
1001 * @private
1002 */
1003 public ding (data?: string): void {
1004 log.silly('Wechaty', 'ding(%s)', data || '')
1005
1006 try {
1007 this.puppet.ding(data)
1008 } catch (e) {
1009 log.error('Wechaty', 'ding() exception: %s', e.message)
1010 captureException(e)
1011 throw e
1012 }
1013 }
1014
1015 /**
1016 * @ignore
1017 */
1018 private memoryCheck (minMegabyte = 4): void {
1019 const freeMegabyte = Math.floor(os.freemem() / 1024 / 1024)
1020 log.silly('Wechaty', 'memoryCheck() free: %d MB, require: %d MB',
1021 freeMegabyte, minMegabyte,
1022 )
1023
1024 if (freeMegabyte < minMegabyte) {
1025 const e = new Error(`memory not enough: free ${freeMegabyte} < require ${minMegabyte} MB`)
1026 log.warn('Wechaty', 'memoryCheck() %s', e.message)
1027 this.emit('error', e)
1028 }
1029 }
1030
1031 /**
1032 * @ignore
1033 */
1034 public reset (reason?: string): void {
1035 log.verbose('Wechaty', 'reset() with reason: %s, call stack: %s',
1036 reason || 'no reason',
1037 // https://stackoverflow.com/a/2060330/1123955
1038 new Error().stack,
1039 )
1040
1041 this.puppet.stop()
1042 .then(() => this.puppet.start())
1043 .finally(() => {
1044 log.verbose('Wechaty', 'reset() done.')
1045 })
1046 .catch(e => {
1047 log.warn('Wechaty', 'reset() rejection: %s', e && e.message)
1048
1049 /**
1050 * Dealing with https://github.com/wechaty/wechaty/issues/2197
1051 */
1052 setTimeout(
1053 () => this.reset(),
1054 Math.floor(
1055 (
1056 10 + 10 * Math.random()
1057 ) * 1000
1058 )
1059 )
1060
1061 })
1062 }
1063
1064 public unref (): void {
1065 log.verbose('Wechaty', 'unref()')
1066
1067 if (this.lifeTimer) {
1068 this.lifeTimer.unref()
1069 }
1070
1071 this.puppet.unref()
1072 }
1073
1074}
1075
1076/**
1077 * Huan(202008): we will bind the wechaty puppet with user modules (Contact, Room, etc) together inside the start() method
1078 */
1079function guardWechatify<T extends Function> (userModule?: T): T {
1080 if (!userModule) {
1081 throw new Error('Wechaty user module (for example, wechaty.Room) can not be used before wechaty.start()!')
1082 }
1083 return userModule
1084}
1085
1086export {
1087 Wechaty,
1088}