@juzi/wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
350 lines (312 loc) • 11.7 kB
text/typescript
/**
* Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
*
* @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
* Wechaty Contributors <https://github.com/wechaty>.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
*/
import {
serviceCtlMixin,
} from 'state-switch'
import { function as FP } from 'fp-ts'
import * as PUPPET from '@juzi/wechaty-puppet'
import {
config,
log,
VERSION,
} from '../config.js'
import type {
SayableSayer,
Sayable,
} from '../sayable/mod.js'
import {
gErrorMixin,
ioMixin,
loginMixin,
miscMixin,
pluginMixin,
puppetMixin,
wechatifyUserModuleMixin,
} from '../wechaty-mixins/mod.js'
import {
WechatySkeleton,
} from './wechaty-skeleton.js'
import type {
WechatyInterface,
} from './wechaty-impl.js'
import type {
WechatyOptions,
} from '../schemas/wechaty-options.js'
import type { PostInterface } from '../user-modules/post.js'
const mixinBase = FP.pipe(
WechatySkeleton,
gErrorMixin,
wechatifyUserModuleMixin,
ioMixin,
puppetMixin,
loginMixin,
miscMixin,
pluginMixin,
/**
* Huan(202002):
*
* The `serviceCtlMixin` must be the most outer mixin
* because the `wechaty.start/stop()` should first entry `serviceCtlMixin.start/stop()`
* which can be managed correctly by the `serviceCtlMixin`
*/
serviceCtlMixin('Wechaty', { log }),
)
/**
* Huan(2021211): Keep the below call back hell
* because it's easy for testing
* especially when there's some typing mismatch and we need to figure it out.
*/
// const mixinTest = serviceCtlMixin('Wechaty', { log })(
// pluginMixin(
// puppetMixin(
// wechatifyUserModuleMixin(
// gErrorMixin(
// WechatySkeleton,
// ),
// ),
// ),
// ),
// )
/**
* Main bot class.
*
* A `Bot` is a WeChat client depends on which puppet you use.
* It may equals
* - web-WeChat, when you use: [puppet-puppeteer](https://github.com/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
*
* See more:
* - [What is a Puppet in Wechaty](https://github.com/wechaty/getting-started/wiki/FAQ#31-what-is-a-puppet-in-wechaty)
*
* > 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>
* import { WechatyBuilder } from 'wechaty'
* const bot = WechatyBuilder.build()
* bot.on('scan', (qrCode, status) => console.log('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode)))
* bot.on('login', user => console.log(`User ${user} logged in`))
* bot.on('message', message => console.log(`Message: ${message}`))
* bot.start()
*/
class WechatyBase extends mixinBase implements SayableSayer {
static override readonly VERSION = VERSION
readonly wechaty : WechatyInterface
readonly _stopCallbackList: (() => void)[] = []
/**
* The term [Puppet](https://wechaty.js.org/docs/specs/puppet) in Wechaty is an Abstract Class for implementing protocol plugins.
* The plugins are the component that helps Wechaty to control the WeChat(that's the reason we call it puppet).
* The plugins are named XXXPuppet, for example:
* - [PuppetWeChat](https://github.com/wechaty/puppet-wechat):
* - [PuppetWeChat](https://github.com/wechaty/puppet-service):
* - [PuppetXP](https://github.com/wechaty/puppet-xp)
*
* @typedef PuppetModuleName
* @property {string} PUPPET_DEFAULT
* The default puppet.
* @property {string} wechaty-puppet-wechat4u
* The default puppet, using the [wechat4u](https://github.com/nodeWechat/wechat4u) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
* @property {string} wechaty-puppet-service
* - Using the gRPC protocol to connect with a Protocol Server for controlling the any protocol of any IM program.
* @property {string} wechaty-puppet-wechat
* - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser.
* @property {string} wechaty-puppet-mock
* - Using the mock data to mock wechat operation, just for test.
*/
/**
* The option parameter to create a wechaty instance
*
* @typedef WechatyOptions
* @property {string} name -Wechaty Name. </br>
* When you set this: </br>
* `new Wechaty({name: 'wechaty-name'}) ` </br>
* it will generate a file called `wechaty-name.memory-card.json`. </br>
* 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>
* 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`
* @property {PuppetModuleName | Puppet} puppet -Puppet name or instance
* @property {Partial<PuppetOptions>} puppetOptions -Puppet TOKEN
* @property {string} ioToken -Io TOKEN
*/
/**
* Creates an instance of Wechaty.
* @param {WechatyOptions} [options={}]
*
*/
constructor (
override __options: WechatyOptions = {},
) {
super(__options)
log.verbose('Wechaty', 'constructor()')
this.__memory = this.__options.memory
this.wechaty = this
}
override async start (): Promise<void> {
log.verbose('Wechaty', 'start()')
await this.init()
return super.start()
}
override async onStart (): Promise<void> {
log.verbose('Wechaty', 'onStart()')
log.verbose('Wechaty', '<%s>(%s) onStart() v%s is starting...',
this.__options.puppet || config.systemPuppetName(),
this.__options.name || '',
this.version(),
)
log.verbose('Wechaty', 'id: %s', this.id)
const lifeTimer = setInterval(() => {
log.silly('Wechaty', 'onStart() setInterval() this timer is to keep Wechaty running...')
}, 1000 * 60 * 60)
this._stopCallbackList.push(() => clearInterval(lifeTimer))
this.emit('start')
log.verbose('Wechaty', 'onStart() ... done')
}
override async onStop (): Promise<void> {
log.verbose('Wechaty', 'onStop()')
log.verbose('Wechaty', '<%s> onStop() v%s is stopping ...',
this.__options.puppet || config.systemPuppetName(),
this.version(),
)
// put to the end of the event loop in case of it need to be executed while stopping
this._stopCallbackList.map(setImmediate)
this._stopCallbackList.length = 0
this.emit('stop')
log.verbose('Wechaty', 'onStop() ... done')
}
/**
* Send message to currentUser, in other words, bot send message to itself.
* > Tips:
* This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
*
* @param {(string | Contact | FileBox | UrlLink | MiniProgram | Location | Channel | ChannelCard | ConsultCard | PremiumOnlineAppointmentCard)} sayable
* send text, Contact, or file to bot. </br>
* You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
*
* @returns {Promise<void>}
*
* @example
* const bot = new Wechaty()
* await bot.start()
* // after logged in
*
* // 1. send text to bot itself
* await bot.say('hello!')
*
* // 2. send Contact to bot itself
* const contact = await bot.Contact.find()
* await bot.say(contact)
*
* // 3. send Image to bot itself from remote url
* import { FileBox } from 'wechaty'
* const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png')
* await bot.say(fileBox)
*
* // 4. send Image to bot itself from local file
* import { FileBox } from 'wechaty'
* const fileBox = FileBox.fromFile('/tmp/text.jpg')
* await bot.say(fileBox)
*
* // 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',
* url : 'https://github.com/wechaty/wechaty',
* })
* await bot.say(linkPayload)
*
* // 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)
*/
async say (
sayable: Sayable,
): Promise<void> {
log.verbose('Wechaty', 'say(%s)', sayable)
await this.currentUser.say(sayable)
}
async publish (
post: PostInterface,
): Promise<void | PostInterface> {
log.verbose('Wechaty', 'publish(%s)',
(post.payload.sayableList as PUPPET.payloads.Sayable[])
.map(s => s.type).join(','),
)
const postId = await this.puppet.postPublish(post.payload)
if (postId) {
return this.Post.find({ id: postId })
}
}
async unpublish (
post: PostInterface,
): Promise<void> {
log.verbose('Wechaty', 'unpublish(%s)', post.id)
if (!post.id) {
throw new Error('cannot unpublish a post without id')
}
if (post.payload.type !== PUPPET.types.Post.Moment) {
throw new Error('cannot unpublish a post that is not a moment')
}
await this.puppet.postUnpublish(post.id)
}
async enterVerifyCode (
id: string,
code: string,
): Promise<void> {
log.verbose('Wechaty', 'enterVerifyCode(%s, %s)', id, code)
return this.puppet.enterVerifyCode(id, code)
}
async cancelVerifyCode (
id: string,
): Promise<void> {
log.verbose('Wechaty', 'cancelVerifyCode(%s)', id)
return this.puppet.cancelVerifyCode(id)
}
async refreshQrCode (
): Promise<void> {
log.verbose('Wechaty', 'refreshQrCode(%s)')
if (this.isLoggedIn) {
throw new Error('cannot refresh qr because bot is logged in')
}
return this.puppet.refreshQRCode()
}
}
type WechatyBaseProtectedProperty =
// | '_serviceCtlFsmInterpreter' // from ServiceCtlFsm
| '__serviceCtlLogger' // from ServiceCtl(&Fsm)
| '__serviceCtlResettingIndicator' // from ServiceCtl
| 'wechaty'
| 'onStart'
| 'onStop'
export type {
WechatyBaseProtectedProperty,
}
export {
WechatyBase,
}