UNPKG

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